Merge branch 'main' into imagecms_core

This commit is contained in:
Andrew Murray 2024-09-08 23:21:25 +10:00 committed by GitHub
commit 72bc56b319
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
246 changed files with 6138 additions and 2995 deletions

View File

@ -51,7 +51,7 @@ build_script:
test_script: test_script:
- cd c:\pillow - cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma' - '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' - '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' - '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'

View File

@ -30,6 +30,7 @@ python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel python3 -m pip install --upgrade wheel
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile python3 -m pip install olefile
python3 -m pip install -U pytest python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov
@ -37,19 +38,22 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
python3 -m pip install numpy # TODO Update condition when NumPy supports free-threading
if [[ "$PYTHON_GIL" == "0" ]]; then
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
else
python3 -m pip install numpy
fi
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
python3 -m pip install pyqt6 # TODO Update condition when pyqt6 supports free-threading
if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
fi fi
# Pyroma uses non-isolated build and fails with old setuptools # Pyroma uses non-isolated build and fails with old setuptools
if [[ if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then
$GHA_PYTHON_VERSION == pypy3.9
|| $GHA_PYTHON_VERSION == 3.9
]]; then
# To match pyproject.toml # To match pyproject.toml
python3 -m pip install "setuptools>=67.8" python3 -m pip install "setuptools>=67.8"
fi fi

View File

@ -1 +1 @@
cibuildwheel==2.19.2 cibuildwheel==2.20.0

View File

@ -1 +1,12 @@
mypy==1.10.1 mypy==1.11.2
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython
numpy
packaging
pytest
sphinx
types-atheris
types-defusedxml
types-olefile
types-setuptools

View File

@ -3,7 +3,7 @@
BasedOnStyle: Google BasedOnStyle: Google
AlwaysBreakAfterReturnType: All AlwaysBreakAfterReturnType: All
AllowShortIfStatementsOnASingleLine: false AllowShortIfStatementsOnASingleLine: false
AlignAfterOpenBracket: AlwaysBreak AlignAfterOpenBracket: BlockIndent
BinPackArguments: false BinPackArguments: false
BinPackParameters: false BinPackParameters: false
BreakBeforeBraces: Attach BreakBeforeBraces: Attach

View File

@ -24,6 +24,8 @@ concurrency:
jobs: jobs:
Fuzzing: Fuzzing:
# Disabled until google/oss-fuzz#11419 upgrades Python to 3.9+
if: false
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Build Fuzzers - name: Build Fuzzers

View File

@ -2,6 +2,9 @@
set -e set -e
if [[ "$ImageOS" == "macos13" ]]; then
brew uninstall gradle maven
fi
brew install \ brew install \
freetype \ freetype \
ghostscript \ ghostscript \
@ -20,6 +23,7 @@ export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile python3 -m pip install olefile
python3 -m pip install -U pytest python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov

View File

@ -74,6 +74,7 @@ jobs:
perl perl
python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-ipython
python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-numpy
python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-sip
python3${{ matrix.python-minor-version }}-tkinter python3${{ matrix.python-minor-version }}-tkinter

View File

@ -35,7 +35,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["pypy3.10", "pypy3.9", "3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"]
timeout-minutes: 30 timeout-minutes: 30
@ -87,7 +87,7 @@ jobs:
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.3.1 --no-progress choco install ghostscript --version=10.3.1 --no-progress
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH echo "C:\Program Files\gs\gs10.03.1\bin" >> $env:GITHUB_PATH
# Install extra test images # Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images xcopy /S /Y Tests\test-images\* Tests\images

View File

@ -42,7 +42,6 @@ jobs:
] ]
python-version: [ python-version: [
"pypy3.10", "pypy3.10",
"pypy3.9",
"3.13", "3.13",
"3.12", "3.12",
"3.11", "3.11",
@ -50,26 +49,24 @@ jobs:
"3.9", "3.9",
] ]
include: include:
- python-version: "3.11" - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
PYTHONOPTIMIZE: 1 - { python-version: "3.10", PYTHONOPTIMIZE: 2 }
REVERSE: "--reverse" # Free-threaded
- python-version: "3.10" - { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true }
PYTHONOPTIMIZE: 2
# M1 only available for 3.10+ # M1 only available for 3.10+
- os: "macos-13" - { os: "macos-13", python-version: "3.9" }
python-version: "3.9"
exclude: exclude:
- os: "macos-14" - { os: "macos-14", python-version: "3.9" }
python-version: "3.9"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v5
if: "${{ !matrix.disable-gil }}"
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true allow-prereleases: true
@ -78,6 +75,18 @@ jobs:
".ci/*.sh" ".ci/*.sh"
"pyproject.toml" "pyproject.toml"
- name: Set up Python ${{ matrix.python-version }} (free-threaded)
uses: deadsnakes/action@v3.1.0
if: "${{ matrix.disable-gil }}"
with:
python-version: ${{ matrix.python-version }}
nogil: ${{ matrix.disable-gil }}
- name: Set PYTHON_GIL
if: "${{ matrix.disable-gil }}"
run: |
echo "PYTHON_GIL=0" >> $GITHUB_ENV
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py

View File

@ -12,8 +12,14 @@ elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
else else
yum install -y fribidi yum install -y fribidi
fi fi
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
# TODO Update condition when NumPy supports free-threading
if [ $(python3 -c "import sysconfig;print(sysconfig.get_config_var('Py_GIL_DISABLED'))") == "1" ]; then
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
else
python3 -m pip install numpy python3 -m pip install numpy
fi
fi fi
if [ ! -d "test-images-main" ]; then if [ ! -d "test-images-main" ]; then

View File

@ -1,6 +1,14 @@
name: Wheels name: Wheels
on: on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# │ │ │ │ │
- cron: "42 1 * * 0,3"
push: push:
paths: paths:
- ".ci/requirements-cibw.txt" - ".ci/requirements-cibw.txt"
@ -33,25 +41,21 @@ env:
jobs: jobs:
build-1-QEMU-emulated-wheels: build-1-QEMU-emulated-wheels:
if: github.event_name != 'schedule' && github.event_name != 'workflow_dispatch'
name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: python-version:
- pp39
- pp310 - pp310
- cp39 - cp3{9,10,11}
- cp310 - cp3{12,13}
- cp311
- cp312
- cp313
spec: spec:
- manylinux2014 - manylinux2014
- manylinux_2_28 - manylinux_2_28
- musllinux - musllinux
exclude: exclude:
- { python-version: pp39, spec: musllinux }
- { python-version: pp310, spec: musllinux } - { python-version: pp310, spec: musllinux }
steps: steps:
@ -91,6 +95,7 @@ jobs:
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
build-2-native-wheels: build-2-native-wheels:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
name: ${{ matrix.name }} name: ${{ matrix.name }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@ -132,6 +137,7 @@ jobs:
env: env:
CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }} CIBW_BUILD: ${{ matrix.build }}
CIBW_FREE_THREADED_SUPPORT: True
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_PRERELEASE_PYTHONS: True CIBW_PRERELEASE_PYTHONS: True
@ -143,6 +149,7 @@ jobs:
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
windows: windows:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
name: Windows ${{ matrix.cibw_arch }} name: Windows ${{ matrix.cibw_arch }}
runs-on: windows-latest runs-on: windows-latest
strategy: strategy:
@ -204,6 +211,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw" CIBW_CACHE_PATH: "C:\\cibw"
CIBW_FREE_THREADED_SUPPORT: True
CIBW_PRERELEASE_PYTHONS: True CIBW_PRERELEASE_PYTHONS: True
CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm CIBW_TEST_COMMAND: 'docker run --rm
@ -228,6 +236,7 @@ jobs:
path: winbuild\build\bin\fribidi* path: winbuild\build\bin\fribidi*
sdist: sdist:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -246,8 +255,25 @@ jobs:
name: dist-sdist name: dist-sdist
path: dist/*.tar.gz path: dist/*.tar.gz
scientific-python-nightly-wheels-publish:
if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
needs: [build-2-native-wheels, windows]
runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels
steps:
- uses: actions/download-artifact@v4
with:
pattern: dist-*
path: dist
merge-multiple: true
- name: Upload wheels to scientific-python-nightly-wheels
uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0
with:
artifacts_path: dist
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
pypi-publish: pypi-publish:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist]
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Upload release to PyPI name: Upload release to PyPI

View File

@ -1,12 +1,12 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0 rev: v0.6.3
hooks: hooks:
- id: ruff - id: ruff
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.4.2 rev: 24.8.0
hooks: hooks:
- id: black - id: black
@ -50,7 +50,7 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.28.6 rev: 0.29.2
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows
- id: check-readthedocs - id: check-readthedocs
@ -62,12 +62,12 @@ repos:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: 2.1.3 rev: 2.2.1
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.18 rev: v0.19
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject

View File

@ -2,6 +2,93 @@
Changelog (Pillow) Changelog (Pillow)
================== ==================
11.0.0 (unreleased)
-------------------
- Removed unused TiffImagePlugin IFD_LEGACY_API #8355
[radarhere]
- Handle duplicate EXIF header #8350
[zakajd, radarhere]
- Return early from BoxBlur if either width or height is zero #8347
[radarhere]
- Check text is either string or bytes #8308
[radarhere]
- Added writing XMP bytes to JPEG #8286
[radarhere]
- Support JPEG2000 RGBA palettes #8256
[radarhere]
- Expand C image to match GIF frame image size #8237
[radarhere]
- Allow saving I;16 images as PPM #8231
[radarhere]
- When IFD is missing, connect get_ifd() dictionary to Exif #8230
[radarhere]
- Skip truncated ICO mask if LOAD_TRUNCATED_IMAGES is enabled #8180
[radarhere]
- Treat unknown JPEG2000 colorspace as unspecified #8343
[radarhere]
- Updated error message when saving WebP with invalid width or height #8322
[radarhere, hugovk]
- Remove warning if NumPy failed to raise an error during conversion #8326
[radarhere]
- If left and right sides meet in ImageDraw.rounded_rectangle(), do not draw rectangle to fill gap #8304
[radarhere]
- Remove WebP support without anim, mux/demux, and with buggy alpha #8213
[homm, radarhere]
- Add missing TIFF CMYK;16B reader #8298
[homm]
- Remove all WITH_* flags from _imaging.c and other flags #8211
[homm]
- Improve ImageDraw2 shape methods #8265
[radarhere]
- Lock around usages of imaging memory arenas #8238
[lysnikolaou]
- Deprecate JpegImageFile huffman_ac and huffman_dc #8274
[radarhere]
- Deprecate ImageMath lambda_eval and unsafe_eval options argument #8242
[radarhere]
- Changed ContainerIO to subclass IO #8240
[radarhere]
- Move away from APIs that use borrowed references under the free-threaded build #8216
[hugovk, lysnikolaou]
- Allow size argument to resize() to be a NumPy array #8201
[radarhere]
- Drop support for Python 3.8 #8183
[hugovk, radarhere]
- Add support for Python 3.13 #8181
[hugovk, radarhere]
- Fix incompatibility with NumPy 1.20 #8187
[neutrinoceros, radarhere]
- Remove PSFile, PyAccess and USE_CFFI_ACCESS #8182
[hugovk, radarhere]
10.4.0 (2024-07-01) 10.4.0 (2024-07-01)
------------------- -------------------

View File

@ -60,9 +60,7 @@ def convert_to_comparable(
return new_a, new_b return new_a, new_b
def assert_deep_equal( def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None:
a: Sequence[Any], b: Sequence[Any], msg: str | None = None
) -> None:
try: try:
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
except Exception: except Exception:

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

View File

@ -16,8 +16,9 @@
import atheris import atheris
from atheris.import_hook import instrument_imports
with atheris.instrument_imports(): with instrument_imports():
import sys import sys
import fuzzers import fuzzers

View File

@ -14,8 +14,9 @@
import atheris import atheris
from atheris.import_hook import instrument_imports
with atheris.instrument_imports(): with instrument_imports():
import sys import sys
import fuzzers import fuzzers

View File

@ -1,5 +1,5 @@
{ {
<py3_8_encode_current_locale> <py3_10_encode_current_locale>
Memcheck:Cond Memcheck:Cond
... ...
fun:encode_current_locale fun:encode_current_locale

View File

@ -71,6 +71,11 @@ def test_color_modes() -> None:
box_blur(sample.convert("YCbCr")) box_blur(sample.convert("YCbCr"))
@pytest.mark.parametrize("size", ((0, 1), (1, 0)))
def test_zero_dimension(size: tuple[int, int]) -> None:
assert box_blur(Image.new("L", size)).size == size
def test_radius_0() -> None: def test_radius_0() -> None:
assert_blur( assert_blur(
sample, sample,

View File

@ -105,91 +105,65 @@ class TestColorLut3DCoreAPI:
with pytest.raises(TypeError): with pytest.raises(TypeError):
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16)
def test_correct_args(self) -> None: @pytest.mark.parametrize(
im = Image.new("RGB", (10, 10), 0) "lut_mode, table_channels, table_size",
[
im.im.color_lut_3d( ("RGB", 3, 3),
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) ("CMYK", 4, 3),
) ("RGB", 3, (2, 3, 3)),
("RGB", 3, (65, 3, 3)),
im.im.color_lut_3d( ("RGB", 3, (3, 65, 3)),
"CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) ("RGB", 3, (2, 3, 65)),
) ],
)
im.im.color_lut_3d( def test_correct_args(
"RGB", self, lut_mode: str, table_channels: int, table_size: int | tuple[int, int, int]
Image.Resampling.BILINEAR, ) -> None:
*self.generate_identity_table(3, (2, 3, 3)),
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (65, 3, 3)),
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (3, 65, 3)),
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (3, 3, 65)),
)
def test_wrong_mode(self) -> None:
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d(
"L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
def test_correct_mode(self) -> None:
im = Image.new("RGBA", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
im = Image.new("RGBA", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
im = Image.new("RGB", (10, 10), 0) im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d( im.im.color_lut_3d(
"HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) lut_mode,
Image.Resampling.BILINEAR,
*self.generate_identity_table(table_channels, table_size),
) )
im = Image.new("RGB", (10, 10), 0) @pytest.mark.parametrize(
"image_mode, lut_mode, table_channels, table_size",
[
("L", "RGB", 3, 3),
("RGB", "L", 3, 3),
("L", "L", 3, 3),
("RGB", "RGBA", 3, 3),
("RGB", "RGB", 4, 3),
],
)
def test_wrong_mode(
self, image_mode: str, lut_mode: str, table_channels: int, table_size: int
) -> None:
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new(image_mode, (10, 10), 0)
im.im.color_lut_3d(
lut_mode,
Image.Resampling.BILINEAR,
*self.generate_identity_table(table_channels, table_size),
)
@pytest.mark.parametrize(
"image_mode, lut_mode, table_channels, table_size",
[
("RGBA", "RGBA", 3, 3),
("RGBA", "RGBA", 4, 3),
("RGB", "HSV", 3, 3),
("RGB", "RGBA", 4, 3),
],
)
def test_correct_mode(
self, image_mode: str, lut_mode: str, table_channels: int, table_size: int
) -> None:
im = Image.new(image_mode, (10, 10), 0)
im.im.color_lut_3d( im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) lut_mode,
Image.Resampling.BILINEAR,
*self.generate_identity_table(table_channels, table_size),
) )
def test_identities(self) -> None: def test_identities(self) -> None:

View File

@ -10,11 +10,6 @@ from PIL import features
from .helper import skip_unless_feature from .helper import skip_unless_feature
try:
from PIL import _webp
except ImportError:
pass
def test_check() -> None: def test_check() -> None:
# Check the correctness of the convenience function # Check the correctness of the convenience function
@ -23,7 +18,11 @@ def test_check() -> None:
for codec in features.codecs: for codec in features.codecs:
assert features.check_codec(codec) == features.check(codec) assert features.check_codec(codec) == features.check(codec)
for feature in features.features: for feature in features.features:
assert features.check_feature(feature) == features.check(feature) if "webp" in feature:
with pytest.warns(DeprecationWarning):
assert features.check_feature(feature) == features.check(feature)
else:
assert features.check_feature(feature) == features.check(feature)
def test_version() -> None: def test_version() -> None:
@ -48,23 +47,26 @@ def test_version() -> None:
for codec in features.codecs: for codec in features.codecs:
test(codec, features.version_codec) test(codec, features.version_codec)
for feature in features.features: for feature in features.features:
test(feature, features.version_feature) if "webp" in feature:
with pytest.warns(DeprecationWarning):
test(feature, features.version_feature)
else:
test(feature, features.version_feature)
@skip_unless_feature("webp")
def test_webp_transparency() -> None: def test_webp_transparency() -> None:
assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() with pytest.warns(DeprecationWarning):
assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY assert features.check("transp_webp") == features.check_module("webp")
@skip_unless_feature("webp")
def test_webp_mux() -> None: def test_webp_mux() -> None:
assert features.check("webp_mux") == _webp.HAVE_WEBPMUX with pytest.warns(DeprecationWarning):
assert features.check("webp_mux") == features.check_module("webp")
@skip_unless_feature("webp")
def test_webp_anim() -> None: def test_webp_anim() -> None:
assert features.check("webp_anim") == _webp.HAVE_WEBPANIM with pytest.warns(DeprecationWarning):
assert features.check("webp_anim") == features.check_module("webp")
@skip_unless_feature("libjpeg_turbo") @skip_unless_feature("libjpeg_turbo")

View File

@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
from typing import Literal
import pytest import pytest
from PIL import ContainerIO, Image from PIL import ContainerIO, Image
@ -23,6 +21,13 @@ def test_isatty() -> None:
assert container.isatty() is False assert container.isatty() is False
def test_seekable() -> None:
with hopper() as im:
container = ContainerIO.ContainerIO(im, 0, 0)
assert container.seekable() is True
@pytest.mark.parametrize( @pytest.mark.parametrize(
"mode, expected_position", "mode, expected_position",
( (
@ -31,7 +36,7 @@ def test_isatty() -> None:
(2, 100), (2, 100),
), ),
) )
def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None: def test_seek_mode(mode: int, expected_position: int) -> None:
# Arrange # Arrange
with open(TEST_FILE, "rb") as fh: with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
@ -44,6 +49,14 @@ def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None:
assert container.tell() == expected_position assert container.tell() == expected_position
@pytest.mark.parametrize("bytesmode", (True, False))
def test_readable(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
assert container.readable() is True
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_read_n0(bytesmode: bool) -> None: def test_read_n0(bytesmode: bool) -> None:
# Arrange # Arrange
@ -51,7 +64,7 @@ def test_read_n0(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
# Act # Act
container.seek(81) assert container.seek(81) == 81
data = container.read() data = container.read()
# Assert # Assert
@ -67,7 +80,7 @@ def test_read_n(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
# Act # Act
container.seek(81) assert container.seek(81) == 81
data = container.read(3) data = container.read(3)
# Assert # Assert
@ -83,7 +96,7 @@ def test_read_eof(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
# Act # Act
container.seek(100) assert container.seek(100) == 100
data = container.read() data = container.read()
# Assert # Assert
@ -94,21 +107,65 @@ def test_read_eof(bytesmode: bool) -> None:
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_readline(bytesmode: bool) -> None: def test_readline(bytesmode: bool) -> None:
# Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh: with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120) container = ContainerIO.ContainerIO(fh, 0, 120)
# Act
data = container.readline() data = container.readline()
# Assert
if bytesmode: if bytesmode:
data = data.decode() data = data.decode()
assert data == "This is line 1\n" assert data == "This is line 1\n"
data = container.readline(4)
if bytesmode:
data = data.decode()
assert data == "This"
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_readlines(bytesmode: bool) -> None: def test_readlines(bytesmode: bool) -> None:
expected = [
"This is line 1\n",
"This is line 2\n",
"This is line 3\n",
"This is line 4\n",
"This is line 5\n",
"This is line 6\n",
"This is line 7\n",
"This is line 8\n",
]
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
data = container.readlines()
if bytesmode:
data = [line.decode() for line in data]
assert data == expected
assert container.seek(0) == 0
data = container.readlines(2)
if bytesmode:
data = [line.decode() for line in data]
assert data == expected[:2]
@pytest.mark.parametrize("bytesmode", (True, False))
def test_write(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
assert container.writable() is False
with pytest.raises(NotImplementedError):
container.write(b"" if bytesmode else "")
with pytest.raises(NotImplementedError):
container.writelines([])
with pytest.raises(NotImplementedError):
container.truncate()
@pytest.mark.parametrize("bytesmode", (True, False))
def test_iter(bytesmode: bool) -> None:
# Arrange # Arrange
expected = [ expected = [
"This is line 1\n", "This is line 1\n",
@ -124,9 +181,21 @@ def test_readlines(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 0, 120) container = ContainerIO.ContainerIO(fh, 0, 120)
# Act # Act
data = container.readlines() data = []
for line in container:
data.append(line)
# Assert # Assert
if bytesmode: if bytesmode:
data = [line.decode() for line in data] data = [line.decode() for line in data]
assert data == expected assert data == expected
@pytest.mark.parametrize("bytesmode", (True, False))
def test_file(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
assert isinstance(container.fileno(), int)
container.flush()
container.close()

View File

@ -152,7 +152,7 @@ def test_sanity_ati2_bc5u(image_path: str) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
("image_path", "expected_path"), "image_path, expected_path",
( (
# hexeditted to be typeless # hexeditted to be typeless
(TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM), (TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM),
@ -248,7 +248,7 @@ def test_dx10_r8g8b8a8_unorm_srgb() -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mode", "size", "test_file"), "mode, size, test_file",
[ [
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L), ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA), ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
@ -373,7 +373,7 @@ def test_save_unsupported_mode(tmp_path: Path) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mode", "test_file"), "mode, test_file",
[ [
("L", "Tests/images/linear_gradient.png"), ("L", "Tests/images/linear_gradient.png"),
("LA", "Tests/images/uncompressed_la.png"), ("LA", "Tests/images/uncompressed_la.png"),

View File

@ -80,9 +80,7 @@ simple_eps_file_with_long_binary_data = (
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize( @pytest.mark.parametrize("filename, size", ((FILE1, (460, 352)), (FILE2, (360, 252))))
("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252)))
)
@pytest.mark.parametrize("scale", (1, 2)) @pytest.mark.parametrize("scale", (1, 2))
def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
expected_size = tuple(s * scale for s in size) expected_size = tuple(s * scale for s in size)

View File

@ -353,7 +353,7 @@ def test_palette_434(tmp_path: Path) -> None:
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
im.copy().save(out, **kwargs) im.copy().save(out, "GIF", **kwargs)
reloaded = Image.open(out) reloaded = Image.open(out)
return reloaded return reloaded
@ -978,7 +978,7 @@ def test_webp_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
# Test opaque WebP background # Test opaque WebP background
if features.check("webp") and features.check("webp_anim"): if features.check("webp"):
with Image.open("Tests/images/hopper.webp") as im: with Image.open("Tests/images/hopper.webp") as im:
assert im.info["background"] == (255, 255, 255, 255) assert im.info["background"] == (255, 255, 255, 255)
im.save(out) im.save(out)
@ -1378,16 +1378,39 @@ def test_lzw_bits() -> None:
im.load() im.load()
def test_extents() -> None: @pytest.mark.parametrize(
with Image.open("Tests/images/test_extents.gif") as im: "test_file, loading_strategy",
assert im.size == (100, 100) (
("test_extents.gif", GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST),
(
"test_extents.gif",
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
),
(
"test_extents_transparency.gif",
GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,
),
),
)
def test_extents(
test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy
) -> None:
GifImagePlugin.LOADING_STRATEGY = loading_strategy
try:
with Image.open("Tests/images/" + test_file) as im:
assert im.size == (100, 100)
# Check that n_frames does not change the size # Check that n_frames does not change the size
assert im.n_frames == 2 assert im.n_frames == 2
assert im.size == (100, 100) assert im.size == (100, 100)
im.seek(1) im.seek(1)
assert im.size == (150, 150) assert im.size == (150, 150)
im.load()
assert im.im.size == (150, 150)
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
def test_missing_background() -> None: def test_missing_background() -> None:

View File

@ -6,7 +6,7 @@ from pathlib import Path
import pytest import pytest
from PIL import IcoImagePlugin, Image, ImageDraw from PIL import IcoImagePlugin, Image, ImageDraw, ImageFile
from .helper import assert_image_equal, assert_image_equal_tofile, hopper from .helper import assert_image_equal, assert_image_equal_tofile, hopper
@ -241,3 +241,29 @@ def test_draw_reloaded(tmp_path: Path) -> None:
with Image.open(outfile) as im: with Image.open(outfile) as im:
assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico")
def test_truncated_mask() -> None:
# 1 bpp
with open("Tests/images/hopper_mask.ico", "rb") as fp:
data = fp.read()
ImageFile.LOAD_TRUNCATED_IMAGES = True
data = data[:-3]
try:
with Image.open(io.BytesIO(data)) as im:
with Image.open("Tests/images/hopper_mask.png") as expected:
assert im.mode == "1"
# 32 bpp
output = io.BytesIO()
expected = hopper("RGBA")
expected.save(output, "ico", bitmap_format="bmp")
data = output.getvalue()[:-1]
with Image.open(io.BytesIO(data)) as im:
assert im.mode == "RGB"
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False

View File

@ -57,6 +57,7 @@ def test_getiptcinfo_fotostation() -> None:
iptc = IptcImagePlugin.getiptcinfo(im) iptc = IptcImagePlugin.getiptcinfo(im)
# Assert # Assert
assert iptc is not None
for tag in iptc.keys(): for tag in iptc.keys():
if tag[0] == 240: if tag[0] == 240:
return return
@ -76,6 +77,16 @@ def test_getiptcinfo_zero_padding() -> None:
assert len(iptc) == 3 assert len(iptc) == 3
def test_getiptcinfo_tiff() -> None:
# Arrange
with Image.open("Tests/images/hopper.Lab.tif") as im:
# Act
iptc = IptcImagePlugin.getiptcinfo(im)
# Assert
assert iptc == {(1, 90): b"\x1b%G", (2, 0): b"\xcf\xc0"}
def test_getiptcinfo_tiff_none() -> None: def test_getiptcinfo_tiff_none() -> None:
# Arrange # Arrange
with Image.open("Tests/images/hopper.tif") as im: with Image.open("Tests/images/hopper.tif") as im:

View File

@ -154,7 +154,7 @@ class TestFileJpeg:
assert k > 0.9 assert k > 0.9
def test_rgb(self) -> None: def test_rgb(self) -> None:
def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]: def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]:
return tuple(v[0] for v in im.layer) return tuple(v[0] for v in im.layer)
im = hopper() im = hopper()
@ -829,7 +829,7 @@ class TestFileJpeg:
with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: with Image.open("Tests/images/no-dpi-in-exif.jpg") as im:
# Act / Assert # Act / Assert
# "When the image resolution is unknown, 72 [dpi] is designated." # "When the image resolution is unknown, 72 [dpi] is designated."
# https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html # https://exiv2.org/tags.html
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
def test_invalid_exif(self) -> None: def test_invalid_exif(self) -> None:
@ -991,12 +991,29 @@ class TestFileJpeg:
else: else:
assert im.getxmp() == {"xmpmeta": None} assert im.getxmp() == {"xmpmeta": None}
def test_save_xmp(self, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg")
im = hopper()
im.save(f, xmp=b"XMP test")
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"XMP test"
im.info["xmp"] = b"1" * 65504
im.save(f)
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"1" * 65504
with pytest.raises(ValueError):
im.save(f, xmp=b"1" * 65505)
@pytest.mark.timeout(timeout=1) @pytest.mark.timeout(timeout=1)
def test_eof(self) -> None: def test_eof(self) -> None:
# Even though this decoder never says that it is finished # Even though this decoder never says that it is finished
# the image should still end when there is no new data # the image should still end when there is no new data
class InfiniteMockPyDecoder(ImageFile.PyDecoder): class InfiniteMockPyDecoder(ImageFile.PyDecoder):
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(
self, buffer: bytes | Image.SupportsArrayInterface
) -> tuple[int, int]:
return 0, 0 return 0, 0
Image.register_decoder("INFINITE", InfiniteMockPyDecoder) Image.register_decoder("INFINITE", InfiniteMockPyDecoder)
@ -1019,13 +1036,16 @@ class TestFileJpeg:
# SOI, EOI # SOI, EOI
for marker in b"\xff\xd8", b"\xff\xd9": for marker in b"\xff\xd8", b"\xff\xd9":
assert marker in data[1] and marker in data[2] assert marker in data[1]
assert marker in data[2]
# DHT, DQT # DHT, DQT
for marker in b"\xff\xc4", b"\xff\xdb": for marker in b"\xff\xc4", b"\xff\xdb":
assert marker in data[1] and marker not in data[2] assert marker in data[1]
assert marker not in data[2]
# SOF0, SOS, APP0 (JFIF header) # SOF0, SOS, APP0 (JFIF header)
for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0": for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0":
assert marker not in data[1] and marker in data[2] assert marker not in data[1]
assert marker in data[2]
with Image.open(BytesIO(data[0])) as interchange_im: with Image.open(BytesIO(data[0])) as interchange_im:
with Image.open(BytesIO(data[1] + data[2])) as combined_im: with Image.open(BytesIO(data[1] + data[2])) as combined_im:
@ -1045,6 +1065,13 @@ class TestFileJpeg:
assert im._repr_jpeg_() is None assert im._repr_jpeg_() is None
def test_deprecation(self) -> None:
with Image.open(TEST_FILE) as im:
with pytest.warns(DeprecationWarning):
assert im.huffman_ac == {}
with pytest.warns(DeprecationWarning):
assert im.huffman_dc == {}
@pytest.mark.skipif(not is_win32(), reason="Windows only") @pytest.mark.skipif(not is_win32(), reason="Windows only")
@skip_unless_feature("jpg") @skip_unless_feature("jpg")

View File

@ -182,6 +182,15 @@ def test_restricted_icc_profile() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
def test_unknown_colorspace() -> None:
with Image.open(f"{EXTRA_DIR}/file8.jp2") as im:
im.load()
assert im.mode == "L"
def test_header_errors() -> None: def test_header_errors() -> None:
for path in ( for path in (
"Tests/images/invalid_header_length.jp2", "Tests/images/invalid_header_length.jp2",
@ -233,7 +242,7 @@ def test_layers() -> None:
("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"), ("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"),
("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"), ("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"),
("foo.jp2", {"no_jp2": False}, 4, b"jP"), ("foo.jp2", {"no_jp2": False}, 4, b"jP"),
("foo.jp2", {"no_jp2": False}, 4, b"jP"), (None, {"no_jp2": False}, 4, b"jP"),
), ),
) )
def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None:
@ -391,6 +400,13 @@ def test_pclr() -> None:
assert len(im.palette.colors) == 256 assert len(im.palette.colors) == 256
assert im.palette.colors[(255, 255, 255)] == 0 assert im.palette.colors[(255, 255, 255)] == 0
with Image.open(
f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2"
) as im:
assert im.mode == "P"
assert len(im.palette.colors) == 139
assert im.palette.colors[(0, 0, 0, 0)] == 0
def test_comment() -> None: def test_comment() -> None:
with Image.open("Tests/images/comment.jp2") as im: with Image.open("Tests/images/comment.jp2") as im:

View File

@ -92,11 +92,22 @@ class TestFileLibTiff(LibTiffTestCase):
def test_g4_non_disk_file_object(self, tmp_path: Path) -> None: def test_g4_non_disk_file_object(self, tmp_path: Path) -> None:
"""Testing loading from non-disk non-BytesIO file object""" """Testing loading from non-disk non-BytesIO file object"""
test_file = "Tests/images/hopper_g4_500.tif" test_file = "Tests/images/hopper_g4_500.tif"
s = io.BytesIO()
with open(test_file, "rb") as f: with open(test_file, "rb") as f:
s.write(f.read()) data = f.read()
s.seek(0)
r = io.BufferedReader(s) class NonBytesIO(io.RawIOBase):
def read(self, size: int = -1) -> bytes:
nonlocal data
if size == -1:
size = len(data)
result = data[:size]
data = data[size:]
return result
def readable(self) -> bool:
return True
r = io.BufferedReader(NonBytesIO())
with Image.open(r) as im: with Image.open(r) as im:
assert im.size == (500, 500) assert im.size == (500, 500)
self._assert_noerr(tmp_path, im) self._assert_noerr(tmp_path, im)
@ -229,9 +240,10 @@ class TestFileLibTiff(LibTiffTestCase):
new_ifd = TiffImagePlugin.ImageFileDirectory_v2() new_ifd = TiffImagePlugin.ImageFileDirectory_v2()
for tag, info in core_items.items(): for tag, info in core_items.items():
assert info.type is not None
if info.length == 1: if info.length == 1:
new_ifd[tag] = values[info.type] new_ifd[tag] = values[info.type]
if info.length == 0: elif not info.length:
new_ifd[tag] = tuple(values[info.type] for _ in range(3)) new_ifd[tag] = tuple(values[info.type] for _ in range(3))
else: else:
new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) new_ifd[tag] = tuple(values[info.type] for _ in range(info.length))
@ -1048,7 +1060,11 @@ class TestFileLibTiff(LibTiffTestCase):
], ],
) )
def test_wrong_bits_per_sample( def test_wrong_bits_per_sample(
self, file_name: str, mode: str, size: tuple[int, int], tile self,
file_name: str,
mode: str,
size: tuple[int, int],
tile: list[tuple[str, tuple[int, int, int, int], int, tuple[Any, ...]]],
) -> None: ) -> None:
with Image.open("Tests/images/" + file_name) as im: with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode assert im.mode == mode
@ -1135,7 +1151,7 @@ class TestFileLibTiff(LibTiffTestCase):
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument: if argument:
arguments["strip_size"] = 2**18 arguments["strip_size"] = 2**18
im.save(out, **arguments) im.save(out, "TIFF", **arguments)
with Image.open(out) as im: with Image.open(out) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile) assert isinstance(im, TiffImagePlugin.TiffImageFile)

View File

@ -2,11 +2,11 @@ from __future__ import annotations
import warnings import warnings
from io import BytesIO from io import BytesIO
from typing import Any, cast from typing import Any
import pytest import pytest
from PIL import Image, MpoImagePlugin from PIL import Image, ImageFile, MpoImagePlugin
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -20,11 +20,11 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
pytestmark = skip_unless_feature("jpg") pytestmark = skip_unless_feature("jpg")
def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile: def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
out = BytesIO() out = BytesIO()
im.save(out, "MPO", **options) im.save(out, "MPO", **options)
out.seek(0) out.seek(0)
return cast(MpoImagePlugin.MpoImageFile, Image.open(out)) return Image.open(out)
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
@ -85,7 +85,9 @@ def test_exif(test_file: str) -> None:
im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif())
for im in (im_original, im_reloaded): for im in (im_original, im_reloaded):
assert isinstance(im, MpoImagePlugin.MpoImageFile)
info = im._getexif() info = im._getexif()
assert info is not None
assert info[272] == "Nintendo 3DS" assert info[272] == "Nintendo 3DS"
assert info[296] == 2 assert info[296] == 2
assert info[34665] == 188 assert info[34665] == 188
@ -226,6 +228,12 @@ def test_eoferror() -> None:
im.seek(n_frames - 1) im.seek(n_frames - 1)
def test_adopt_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im:
with pytest.raises(ValueError):
MpoImagePlugin.MpoImageFile.adopt(im)
def test_ultra_hdr() -> None: def test_ultra_hdr() -> None:
with Image.open("Tests/images/ultrahdr.jpg") as im: with Image.open("Tests/images/ultrahdr.jpg") as im:
assert im.format == "JPEG" assert im.format == "JPEG"
@ -275,6 +283,8 @@ def test_save_all() -> None:
im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
assert_image_equal(im, im_reloaded) assert_image_equal(im, im_reloaded)
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
assert im_reloaded.mpinfo is not None
assert im_reloaded.mpinfo[45056] == b"0100" assert im_reloaded.mpinfo[45056] == b"0100"
im_reloaded.seek(1) im_reloaded.seek(1)

View File

@ -118,7 +118,7 @@ def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
im = hopper() im = hopper()
outfile = str(tmp_path / "temp.pdf") outfile = str(tmp_path / "temp.pdf")
im.save(outfile, **params) im.save(outfile, "PDF", **params)
with open(outfile, "rb") as fp: with open(outfile, "rb") as fp:
contents = fp.read() contents = fp.read()
@ -229,6 +229,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None:
def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None: def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None:
assert pdf.pages_ref is not None
pages_info = pdf.read_indirect(pdf.pages_ref) pages_info = pdf.read_indirect(pdf.pages_ref)
assert b"Parent" not in pages_info assert b"Parent" not in pages_info
assert b"Kids" in pages_info assert b"Kids" in pages_info

View File

@ -41,7 +41,7 @@ MAGIC = PngImagePlugin._MAGIC
def chunk(cid: bytes, *data: bytes) -> bytes: def chunk(cid: bytes, *data: bytes) -> bytes:
test_file = BytesIO() test_file = BytesIO()
PngImagePlugin.putchunk(*(test_file, cid) + data) PngImagePlugin.putchunk(test_file, cid, *data)
return test_file.getvalue() return test_file.getvalue()
@ -424,8 +424,10 @@ class TestFilePng:
im = roundtrip(im, pnginfo=info) im = roundtrip(im, pnginfo=info)
assert im.info == {"spam": "Eggs", "eggs": "Spam"} assert im.info == {"spam": "Eggs", "eggs": "Spam"}
assert im.text == {"spam": "Eggs", "eggs": "Spam"} assert im.text == {"spam": "Eggs", "eggs": "Spam"}
assert isinstance(im.text["spam"], PngImagePlugin.iTXt)
assert im.text["spam"].lang == "en" assert im.text["spam"].lang == "en"
assert im.text["spam"].tkey == "Spam" assert im.text["spam"].tkey == "Spam"
assert isinstance(im.text["eggs"], PngImagePlugin.iTXt)
assert im.text["eggs"].lang == "en" assert im.text["eggs"].lang == "en"
assert im.text["eggs"].tkey == "Eggs" assert im.text["eggs"].tkey == "Eggs"
@ -776,7 +778,7 @@ class TestFilePng:
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout # type: ignore[assignment] sys.stdout = mystdout
with Image.open(TEST_PNG_FILE) as im: with Image.open(TEST_PNG_FILE) as im:
im.save(sys.stdout, "PNG") im.save(sys.stdout, "PNG")

View File

@ -95,7 +95,9 @@ def test_16bit_pgm_write(tmp_path: Path) -> None:
with Image.open("Tests/images/16_bit_binary.pgm") as im: with Image.open("Tests/images/16_bit_binary.pgm") as im:
filename = str(tmp_path / "temp.pgm") filename = str(tmp_path / "temp.pgm")
im.save(filename, "PPM") im.save(filename, "PPM")
assert_image_equal_tofile(im, filename)
im.convert("I;16").save(filename, "PPM")
assert_image_equal_tofile(im, filename) assert_image_equal_tofile(im, filename)
@ -373,7 +375,7 @@ def test_save_stdout(buffer: bool) -> None:
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout # type: ignore[assignment] sys.stdout = mystdout
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.save(sys.stdout, "PPM") im.save(sys.stdout, "PPM")

View File

@ -78,6 +78,7 @@ class TestFileTiff:
def test_seek_after_close(self) -> None: def test_seek_after_close(self) -> None:
im = Image.open("Tests/images/multipage.tiff") im = Image.open("Tests/images/multipage.tiff")
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.close() im.close()
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -424,13 +425,13 @@ class TestFileTiff:
def test_load_float(self) -> None: def test_load_float(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdabcd" data = b"abcdabcd"
ret = ifd.load_float(data, False) ret = getattr(ifd, "load_float")(data, False)
assert ret == (1.6777999408082104e22, 1.6777999408082104e22) assert ret == (1.6777999408082104e22, 1.6777999408082104e22)
def test_load_double(self) -> None: def test_load_double(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdefghabcdefgh" data = b"abcdefghabcdefgh"
ret = ifd.load_double(data, False) ret = getattr(ifd, "load_double")(data, False)
assert ret == (8.540883223036124e194, 8.540883223036124e194) assert ret == (8.540883223036124e194, 8.540883223036124e194)
def test_ifd_tag_type(self) -> None: def test_ifd_tag_type(self) -> None:
@ -599,7 +600,7 @@ class TestFileTiff:
def test_with_underscores(self, tmp_path: Path) -> None: def test_with_underscores(self, tmp_path: Path) -> None:
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
filename = str(tmp_path / "temp.tif") filename = str(tmp_path / "temp.tif")
hopper("RGB").save(filename, **kwargs) hopper("RGB").save(filename, "TIFF", **kwargs)
with Image.open(filename) as im: with Image.open(filename) as im:
# legacy interface # legacy interface
assert im.tag[X_RESOLUTION][0][0] == 72 assert im.tag[X_RESOLUTION][0][0] == 72
@ -624,14 +625,17 @@ class TestFileTiff:
def test_iptc(self, tmp_path: Path) -> None: def test_iptc(self, tmp_path: Path) -> None:
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG # Do not preserve IPTC_NAA_CHUNK by default if type is LONG
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
im = hopper() with Image.open("Tests/images/hopper.tif") as im:
ifd = TiffImagePlugin.ImageFileDirectory_v2() im.load()
ifd[33723] = 1 assert isinstance(im, TiffImagePlugin.TiffImageFile)
ifd.tagtype[33723] = 4 ifd = TiffImagePlugin.ImageFileDirectory_v2()
im.tag_v2 = ifd ifd[33723] = 1
im.save(outfile) ifd.tagtype[33723] = 4
im.tag_v2 = ifd
im.save(outfile)
with Image.open(outfile) as im: with Image.open(outfile) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert 33723 not in im.tag_v2 assert 33723 not in im.tag_v2
def test_rowsperstrip(self, tmp_path: Path) -> None: def test_rowsperstrip(self, tmp_path: Path) -> None:
@ -680,6 +684,13 @@ class TestFileTiff:
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
assert_image_equal_tofile(reloaded, infile) assert_image_equal_tofile(reloaded, infile)
def test_invalid_tiled_dimensions(self) -> None:
with open("Tests/images/tiff_tiled_planar_raw.tif", "rb") as fp:
data = fp.read()
b = BytesIO(data[:144] + b"\x02" + data[145:])
with pytest.raises(ValueError):
Image.open(b)
@pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("mode", ("P", "PA"))
def test_palette(self, mode: str, tmp_path: Path) -> None: def test_palette(self, mode: str, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")

View File

@ -48,8 +48,6 @@ class TestFileWebp:
self.rgb_mode = "RGB" self.rgb_mode = "RGB"
def test_version(self) -> None: def test_version(self) -> None:
_webp.WebPDecoderVersion()
_webp.WebPDecoderBuggyAlpha()
version = features.version_module("webp") version = features.version_module("webp")
assert version is not None assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version) assert re.search(r"\d+\.\d+\.\d+$", version)
@ -117,7 +115,6 @@ class TestFileWebp:
hopper().save(buffer_method, format="WEBP", method=6) hopper().save(buffer_method, format="WEBP", method=6)
assert buffer_no_args.getbuffer() != buffer_method.getbuffer() assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
@skip_unless_feature("webp_anim")
def test_save_all(self, tmp_path: Path) -> None: def test_save_all(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp") temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
@ -132,10 +129,9 @@ class TestFileWebp:
def test_icc_profile(self, tmp_path: Path) -> None: def test_icc_profile(self, tmp_path: Path) -> None:
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
if _webp.HAVE_WEBPANIM: self._roundtrip(
self._roundtrip( tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True}
tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} )
)
def test_write_unsupported_mode_L(self, tmp_path: Path) -> None: def test_write_unsupported_mode_L(self, tmp_path: Path) -> None:
""" """
@ -161,27 +157,32 @@ class TestFileWebp:
im.save(temp_file, method=0) im.save(temp_file, method=0)
assert str(e.value) == "encoding error 6" assert str(e.value) == "encoding error 6"
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("L", (16384, 16384))
with pytest.raises(ValueError) as e:
im.save(temp_file)
assert (
str(e.value)
== "encoding error 5: Image size exceeds WebP limit of 16383 pixels"
)
def test_WebPEncode_with_invalid_args(self) -> None: def test_WebPEncode_with_invalid_args(self) -> None:
""" """
Calling encoder functions with no arguments should result in an error. Calling encoder functions with no arguments should result in an error.
""" """
with pytest.raises(TypeError):
if _webp.HAVE_WEBPANIM: _webp.WebPAnimEncoder()
with pytest.raises(TypeError):
_webp.WebPAnimEncoder()
with pytest.raises(TypeError): with pytest.raises(TypeError):
_webp.WebPEncode() _webp.WebPEncode()
def test_WebPDecode_with_invalid_args(self) -> None: def test_WebPAnimDecoder_with_invalid_args(self) -> None:
""" """
Calling decoder functions with no arguments should result in an error. Calling decoder functions with no arguments should result in an error.
""" """
if _webp.HAVE_WEBPANIM:
with pytest.raises(TypeError):
_webp.WebPAnimDecoder()
with pytest.raises(TypeError): with pytest.raises(TypeError):
_webp.WebPDecode() _webp.WebPAnimDecoder()
def test_no_resource_warning(self, tmp_path: Path) -> None: def test_no_resource_warning(self, tmp_path: Path) -> None:
file_path = "Tests/images/hopper.webp" file_path = "Tests/images/hopper.webp"
@ -200,7 +201,6 @@ class TestFileWebp:
"background", "background",
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
) )
@skip_unless_feature("webp_anim")
def test_invalid_background( def test_invalid_background(
self, background: int | tuple[int, ...], tmp_path: Path self, background: int | tuple[int, ...], tmp_path: Path
) -> None: ) -> None:
@ -209,7 +209,6 @@ class TestFileWebp:
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(temp_file, save_all=True, append_images=[im], background=background) im.save(temp_file, save_all=True, append_images=[im], background=background)
@skip_unless_feature("webp_anim")
def test_background_from_gif(self, tmp_path: Path) -> None: def test_background_from_gif(self, tmp_path: Path) -> None:
# Save L mode GIF with background # Save L mode GIF with background
with Image.open("Tests/images/no_palette_with_background.gif") as im: with Image.open("Tests/images/no_palette_with_background.gif") as im:
@ -234,7 +233,6 @@ class TestFileWebp:
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
assert difference < 5 assert difference < 5
@skip_unless_feature("webp_anim")
def test_duration(self, tmp_path: Path) -> None: def test_duration(self, tmp_path: Path) -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
assert im.info["duration"] == 1000 assert im.info["duration"] == 1000
@ -250,6 +248,7 @@ class TestFileWebp:
temp_file = str(tmp_path / "temp.webp") temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGBA", (1, 1)).convert("P") im = Image.new("RGBA", (1, 1)).convert("P")
assert im.mode == "P" assert im.mode == "P"
assert im.palette is not None
assert im.palette.mode == "RGBA" assert im.palette.mode == "RGBA"
im.save(temp_file) im.save(temp_file)

View File

@ -13,12 +13,7 @@ from .helper import (
hopper, hopper,
) )
_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") pytest.importorskip("PIL._webp", reason="WebP support not installed")
def setup_module() -> None:
if _webp.WebPDecoderBuggyAlpha():
pytest.skip("Buggy early version of WebP installed, not testing transparency")
def test_read_rgba() -> None: def test_read_rgba() -> None:
@ -81,9 +76,6 @@ def test_write_rgba(tmp_path: Path) -> None:
pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20))
pil_image.save(temp_file) pil_image.save(temp_file)
if _webp.WebPDecoderBuggyAlpha():
return
with Image.open(temp_file) as image: with Image.open(temp_file) as image:
image.load() image.load()
@ -93,12 +85,7 @@ def test_write_rgba(tmp_path: Path) -> None:
image.load() image.load()
image.getdata() image.getdata()
# Early versions of WebP are known to produce higher deviations: assert_image_similar(image, pil_image, 1.0)
# deal with it
if _webp.WebPDecoderVersion() <= 0x201:
assert_image_similar(image, pil_image, 3.0)
else:
assert_image_similar(image, pil_image, 1.0)
def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None:

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Generator
from pathlib import Path from pathlib import Path
import pytest import pytest
@ -14,10 +15,7 @@ from .helper import (
skip_unless_feature, skip_unless_feature,
) )
pytestmark = [ pytestmark = skip_unless_feature("webp")
skip_unless_feature("webp"),
skip_unless_feature("webp_anim"),
]
def test_n_frames() -> None: def test_n_frames() -> None:
@ -96,7 +94,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
check(temp_file1) check(temp_file1)
# Tests appending using a generator # Tests appending using a generator
def im_generator(ims): def im_generator(
ims: list[Image.Image],
) -> Generator[Image.Image, None, None]:
yield from ims yield from ims
temp_file2 = str(tmp_path / "temp_generator.webp") temp_file2 = str(tmp_path / "temp_generator.webp")

View File

@ -8,14 +8,11 @@ from PIL import Image
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, hopper
_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") pytest.importorskip("PIL._webp", reason="WebP support not installed")
RGB_MODE = "RGB" RGB_MODE = "RGB"
def test_write_lossless_rgb(tmp_path: Path) -> None: def test_write_lossless_rgb(tmp_path: Path) -> None:
if _webp.WebPDecoderVersion() < 0x0200:
pytest.skip("lossless not included")
temp_file = str(tmp_path / "temp.webp") temp_file = str(tmp_path / "temp.webp")
hopper(RGB_MODE).save(temp_file, lossless=True) hopper(RGB_MODE).save(temp_file, lossless=True)

View File

@ -10,10 +10,7 @@ from PIL import Image
from .helper import mark_if_feature_version, skip_unless_feature from .helper import mark_if_feature_version, skip_unless_feature
pytestmark = [ pytestmark = skip_unless_feature("webp")
skip_unless_feature("webp"),
skip_unless_feature("webp_mux"),
]
ElementTree: ModuleType | None ElementTree: ModuleType | None
try: try:
@ -119,7 +116,15 @@ def test_read_no_exif() -> None:
def test_getxmp() -> None: def test_getxmp() -> None:
with Image.open("Tests/images/flower.webp") as im: with Image.open("Tests/images/flower.webp") as im:
assert "xmp" not in im.info assert "xmp" not in im.info
assert im.getxmp() == {} if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
xmp = im.getxmp()
else:
xmp = im.getxmp()
assert xmp == {}
with Image.open("Tests/images/flower2.webp") as im: with Image.open("Tests/images/flower2.webp") as im:
if ElementTree is None: if ElementTree is None:
@ -136,7 +141,6 @@ def test_getxmp() -> None:
) )
@skip_unless_feature("webp_anim")
def test_write_animated_metadata(tmp_path: Path) -> None: def test_write_animated_metadata(tmp_path: Path) -> None:
iccp_data = b"<iccp_data>" iccp_data = b"<iccp_data>"
exif_data = b"<exif_data>" exif_data = b"<exif_data>"

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import os import os
from pathlib import Path from pathlib import Path
from typing import AnyStr
import pytest import pytest
@ -92,7 +93,7 @@ def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None:
def _test_high_characters( def _test_high_characters(
request: pytest.FixtureRequest, tmp_path: Path, message: str | bytes request: pytest.FixtureRequest, tmp_path: Path, message: AnyStr
) -> None: ) -> None:
tempname = save_font(request, tmp_path) tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname) font = ImageFont.load(tempname)

View File

@ -42,6 +42,12 @@ try:
except ImportError: except ImportError:
ElementTree = None ElementTree = None
PrettyPrinter: type | None
try:
from IPython.lib.pretty import PrettyPrinter
except ImportError:
PrettyPrinter = None
# Deprecation helper # Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
@ -91,16 +97,15 @@ class TestImage:
# with pytest.raises(MemoryError): # with pytest.raises(MemoryError):
# Image.new("L", (1000000, 1000000)) # Image.new("L", (1000000, 1000000))
@pytest.mark.skipif(PrettyPrinter is None, reason="IPython is not installed")
def test_repr_pretty(self) -> None: def test_repr_pretty(self) -> None:
class Pretty:
def text(self, text: str) -> None:
self.pretty_output = text
im = Image.new("L", (100, 100)) im = Image.new("L", (100, 100))
p = Pretty() output = io.StringIO()
im._repr_pretty_(p, None) assert PrettyPrinter is not None
assert p.pretty_output == "<PIL.Image.Image image mode=L size=100x100>" p = PrettyPrinter(output)
im._repr_pretty_(p, False)
assert output.getvalue() == "<PIL.Image.Image image mode=L size=100x100>"
def test_open_formats(self) -> None: def test_open_formats(self) -> None:
PNGFILE = "Tests/images/hopper.png" PNGFILE = "Tests/images/hopper.png"
@ -372,8 +377,9 @@ class TestImage:
img = Image.alpha_composite(dst, src) img = Image.alpha_composite(dst, src)
# Assert # Assert
img_colors = sorted(img.getcolors()) img_colors = img.getcolors()
assert img_colors == expected_colors assert img_colors is not None
assert sorted(img_colors) == expected_colors
def test_alpha_inplace(self) -> None: def test_alpha_inplace(self) -> None:
src = Image.new("RGBA", (128, 128), "blue") src = Image.new("RGBA", (128, 128), "blue")
@ -670,7 +676,9 @@ class TestImage:
im_remapped = im.remap_palette([1, 0]) im_remapped = im.remap_palette([1, 0])
assert im_remapped.info["transparency"] == 1 assert im_remapped.info["transparency"] == 1
assert len(im_remapped.getpalette()) == 6 palette = im_remapped.getpalette()
assert palette is not None
assert len(palette) == 6
# Test unused transparency # Test unused transparency
im.info["transparency"] = 2 im.info["transparency"] = 2
@ -697,11 +705,12 @@ class TestImage:
assert new_image.size == image.size assert new_image.size == image.size
assert new_image.info == base_image.info assert new_image.info == base_image.info
if palette_result is not None: if palette_result is not None:
assert new_image.palette is not None
assert new_image.palette.tobytes() == palette_result.tobytes() assert new_image.palette.tobytes() == palette_result.tobytes()
else: else:
assert new_image.palette is None assert new_image.palette is None
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3)) _make_new(im, im_p, ImagePalette.ImagePalette("RGB"))
_make_new(im_p, im, None) _make_new(im_p, im, None)
_make_new(im, blank_p, ImagePalette.ImagePalette()) _make_new(im, blank_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, ImagePalette.ImagePalette()) _make_new(im, blank_pa, ImagePalette.ImagePalette())
@ -766,6 +775,22 @@ class TestImage:
exif.load(b"Exif\x00\x00") exif.load(b"Exif\x00\x00")
assert not dict(exif) assert not dict(exif)
def test_duplicate_exif_header(self) -> None:
with Image.open("Tests/images/exif.png") as im:
im.load()
im.info["exif"] = b"Exif\x00\x00" + im.info["exif"]
exif = im.getexif()
assert exif[274] == 1
def test_empty_get_ifd(self) -> None:
exif = Image.Exif()
ifd = exif.get_ifd(0x8769)
assert ifd == {}
ifd[36864] = b"0220"
assert exif.get_ifd(0x8769) == {36864: b"0220"}
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
) )
@ -814,7 +839,6 @@ class TestImage:
assert reloaded_exif[305] == "Pillow test" assert reloaded_exif[305] == "Pillow test"
@skip_unless_feature("webp") @skip_unless_feature("webp")
@skip_unless_feature("webp_anim")
def test_exif_webp(self, tmp_path: Path) -> None: def test_exif_webp(self, tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.webp") as im: with Image.open("Tests/images/hopper.webp") as im:
exif = im.getexif() exif = im.getexif()
@ -936,7 +960,15 @@ class TestImage:
def test_empty_xmp(self) -> None: def test_empty_xmp(self) -> None:
with Image.open("Tests/images/hopper.gif") as im: with Image.open("Tests/images/hopper.gif") as im:
assert im.getxmp() == {} if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
xmp = im.getxmp()
else:
xmp = im.getxmp()
assert xmp == {}
def test_getxmp_padded(self) -> None: def test_getxmp_padded(self) -> None:
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
@ -987,12 +1019,14 @@ class TestImage:
# P mode with RGBA palette # P mode with RGBA palette
im = Image.new("RGBA", (1, 1)).convert("P") im = Image.new("RGBA", (1, 1)).convert("P")
assert im.mode == "P" assert im.mode == "P"
assert im.palette is not None
assert im.palette.mode == "RGBA" assert im.palette.mode == "RGBA"
assert im.has_transparency_data assert im.has_transparency_data
def test_apply_transparency(self) -> None: def test_apply_transparency(self) -> None:
im = Image.new("P", (1, 1)) im = Image.new("P", (1, 1))
im.putpalette((0, 0, 0, 1, 1, 1)) im.putpalette((0, 0, 0, 1, 1, 1))
assert im.palette is not None
assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1} assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1}
# Test that no transformation is applied without transparency # Test that no transformation is applied without transparency
@ -1010,13 +1044,16 @@ class TestImage:
im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA") im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA")
im.info["transparency"] = 0 im.info["transparency"] = 0
im.apply_transparency() im.apply_transparency()
assert im.palette is not None
assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 128): 1} assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 128): 1}
# Test that transparency bytes are applied # Test that transparency bytes are applied
with Image.open("Tests/images/pil123p.png") as im: with Image.open("Tests/images/pil123p.png") as im:
assert isinstance(im.info["transparency"], bytes) assert isinstance(im.info["transparency"], bytes)
assert im.palette is not None
assert im.palette.colors[(27, 35, 6)] == 24 assert im.palette.colors[(27, 35, 6)] == 24
im.apply_transparency() im.apply_transparency()
assert im.palette is not None
assert im.palette.colors[(27, 35, 6, 214)] == 24 assert im.palette.colors[(27, 35, 6, 214)] == 24
def test_constants(self) -> None: def test_constants(self) -> None:

View File

@ -27,7 +27,9 @@ class TestImagePutPixel:
for y in range(im1.size[1]): for y in range(im1.size[1]):
for x in range(im1.size[0]): for x in range(im1.size[0]):
pos = x, y pos = x, y
im2.putpixel(pos, im1.getpixel(pos)) value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert_image_equal(im1, im2) assert_image_equal(im1, im2)
@ -37,7 +39,9 @@ class TestImagePutPixel:
for y in range(im1.size[1]): for y in range(im1.size[1]):
for x in range(im1.size[0]): for x in range(im1.size[0]):
pos = x, y pos = x, y
im2.putpixel(pos, im1.getpixel(pos)) value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert not im2.readonly assert not im2.readonly
assert_image_equal(im1, im2) assert_image_equal(im1, im2)
@ -50,9 +54,9 @@ class TestImagePutPixel:
assert pix1 is not None assert pix1 is not None
assert pix2 is not None assert pix2 is not None
with pytest.raises(TypeError): with pytest.raises(TypeError):
pix1[0, "0"] pix1[0, "0"] # type: ignore[index]
with pytest.raises(TypeError): with pytest.raises(TypeError):
pix1["0", 0] pix1["0", 0] # type: ignore[index]
for y in range(im1.size[1]): for y in range(im1.size[1]):
for x in range(im1.size[0]): for x in range(im1.size[0]):
@ -71,7 +75,9 @@ class TestImagePutPixel:
for y in range(-1, -im1.size[1] - 1, -1): for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1): for x in range(-1, -im1.size[0] - 1, -1):
pos = x, y pos = x, y
im2.putpixel(pos, im1.getpixel(pos)) value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert_image_equal(im1, im2) assert_image_equal(im1, im2)
@ -81,7 +87,9 @@ class TestImagePutPixel:
for y in range(-1, -im1.size[1] - 1, -1): for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1): for x in range(-1, -im1.size[0] - 1, -1):
pos = x, y pos = x, y
im2.putpixel(pos, im1.getpixel(pos)) value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert not im2.readonly assert not im2.readonly
assert_image_equal(im1, im2) assert_image_equal(im1, im2)
@ -219,10 +227,10 @@ class TestImagePutPixelError:
im = hopper(mode) im = hopper(mode)
for v in self.INVALID_TYPES: for v in self.INVALID_TYPES:
with pytest.raises(TypeError, match="color must be int or tuple"): with pytest.raises(TypeError, match="color must be int or tuple"):
im.putpixel((0, 0), v) im.putpixel((0, 0), v) # type: ignore[arg-type]
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mode", "band_numbers", "match"), "mode, band_numbers, match",
( (
("L", (0, 2), "color must be int or single-element tuple"), ("L", (0, 2), "color must be int or single-element tuple"),
("LA", (0, 3), "color must be int, or tuple of one or two elements"), ("LA", (0, 3), "color must be int, or tuple of one or two elements"),
@ -253,7 +261,7 @@ class TestImagePutPixelError:
with pytest.raises( with pytest.raises(
TypeError, match="color must be int or single-element tuple" TypeError, match="color must be int or single-element tuple"
): ):
im.putpixel((0, 0), v) im.putpixel((0, 0), v) # type: ignore[arg-type]
@pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
def test_putpixel_overflow_error(self, mode: str) -> None: def test_putpixel_overflow_error(self, mode: str) -> None:

View File

@ -24,7 +24,7 @@ def test_toarray() -> None:
def test_with_dtype(dtype: npt.DTypeLike) -> None: def test_with_dtype(dtype: npt.DTypeLike) -> None:
ai = numpy.array(im, dtype=dtype) ai = numpy.array(im, dtype=dtype)
assert ai.dtype == dtype assert ai.dtype.type is dtype
# assert test("1") == ((100, 128), '|b1', 1600)) # assert test("1") == ((100, 128), '|b1', 1600))
assert test("L") == ((100, 128), "|u1", 12800) assert test("L") == ((100, 128), "|u1", 12800)
@ -47,7 +47,7 @@ def test_toarray() -> None:
with pytest.raises(OSError): with pytest.raises(OSError):
numpy.array(im_truncated) numpy.array(im_truncated)
else: else:
with pytest.warns(UserWarning): with pytest.warns(DeprecationWarning):
numpy.array(im_truncated) numpy.array(im_truncated)
@ -113,4 +113,5 @@ def test_fromarray_palette() -> None:
out = Image.fromarray(a, "P") out = Image.fromarray(a, "P")
# Assert that the Python and C palettes match # Assert that the Python and C palettes match
assert out.palette is not None
assert len(out.palette.colors) == len(out.im.getpalette()) / 3 assert len(out.palette.colors) == len(out.im.getpalette()) / 3

View File

@ -218,6 +218,7 @@ def test_trns_RGB(tmp_path: Path) -> None:
def test_l_macro_rounding(convert_mode: str) -> None: def test_l_macro_rounding(convert_mode: str) -> None:
for mode in ("P", "PA"): for mode in ("P", "PA"):
im = Image.new(mode, (1, 1)) im = Image.new(mode, (1, 1))
assert im.palette is not None
im.palette.getcolor((0, 1, 2)) im.palette.getcolor((0, 1, 2))
converted_im = im.convert(convert_mode) converted_im = im.convert(convert_mode)
@ -225,7 +226,7 @@ def test_l_macro_rounding(convert_mode: str) -> None:
assert px is not None assert px is not None
converted_color = px[0, 0] converted_color = px[0, 0]
if convert_mode == "LA": if convert_mode == "LA":
assert converted_color is not None assert isinstance(converted_color, tuple)
converted_color = converted_color[0] converted_color = converted_color[0]
assert converted_color == 1 assert converted_color == 1

View File

@ -54,17 +54,21 @@ def test_pack() -> None:
assert A is None assert A is None
A = im.getcolors(maxcolors=3) A = im.getcolors(maxcolors=3)
assert A is not None
A.sort() A.sort()
assert A == expected assert A == expected
A = im.getcolors(maxcolors=4) A = im.getcolors(maxcolors=4)
assert A is not None
A.sort() A.sort()
assert A == expected assert A == expected
A = im.getcolors(maxcolors=8) A = im.getcolors(maxcolors=8)
assert A is not None
A.sort() A.sort()
assert A == expected assert A == expected
A = im.getcolors(maxcolors=16) A = im.getcolors(maxcolors=16)
assert A is not None
A.sort() A.sort()
assert A == expected assert A == expected

View File

@ -31,7 +31,7 @@ def test_sanity() -> None:
def test_long_integers() -> None: def test_long_integers() -> None:
# see bug-200802-systemerror # see bug-200802-systemerror
def put(value: int) -> tuple[int, int, int, int]: def put(value: int) -> float | tuple[int, ...] | None:
im = Image.new("RGBA", (1, 1)) im = Image.new("RGBA", (1, 1))
im.putdata([value]) im.putdata([value])
return im.getpixel((0, 0)) return im.getpixel((0, 0))

View File

@ -86,6 +86,7 @@ def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:
im = Image.new("P", (1, 1)) im = Image.new("P", (1, 1))
im.putpalette(palette, mode) im.putpalette(palette, mode)
assert im.getpalette() == [1, 2, 3] assert im.getpalette() == [1, 2, 3]
assert im.palette is not None
assert im.palette.colors == {(1, 2, 3, 4): 0} assert im.palette.colors == {(1, 2, 3, 4): 0}

View File

@ -31,7 +31,9 @@ def test_libimagequant_quantize() -> None:
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
assert converted.mode == "P" assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 15) assert_image_similar(converted.convert("RGB"), image, 15)
assert len(converted.getcolors()) == 100 colors = converted.getcolors()
assert colors is not None
assert len(colors) == 100
def test_octree_quantize() -> None: def test_octree_quantize() -> None:
@ -39,7 +41,9 @@ def test_octree_quantize() -> None:
converted = image.quantize(100, Image.Quantize.FASTOCTREE) converted = image.quantize(100, Image.Quantize.FASTOCTREE)
assert converted.mode == "P" assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 20) assert_image_similar(converted.convert("RGB"), image, 20)
assert len(converted.getcolors()) == 100 colors = converted.getcolors()
assert colors is not None
assert len(colors) == 100
def test_rgba_quantize() -> None: def test_rgba_quantize() -> None:
@ -65,6 +69,7 @@ def test_quantize_no_dither() -> None:
converted = image.quantize(dither=Image.Dither.NONE, palette=palette) converted = image.quantize(dither=Image.Dither.NONE, palette=palette)
assert converted.mode == "P" assert converted.mode == "P"
assert converted.palette is not None
assert converted.palette.palette == palette.palette.palette assert converted.palette.palette == palette.palette.palette
@ -77,6 +82,7 @@ def test_quantize_no_dither2() -> None:
palette.putpalette(data) palette.putpalette(data)
quantized = im.quantize(dither=Image.Dither.NONE, palette=palette) quantized = im.quantize(dither=Image.Dither.NONE, palette=palette)
assert quantized.palette is not None
assert tuple(quantized.palette.palette) == data assert tuple(quantized.palette.palette) == data
px = quantized.load() px = quantized.load()
@ -113,6 +119,7 @@ def test_colors() -> None:
im = hopper() im = hopper()
colors = 2 colors = 2
converted = im.quantize(colors) converted = im.quantize(colors)
assert converted.palette is not None
assert len(converted.palette.palette) == colors * len("RGB") assert len(converted.palette.palette) == colors * len("RGB")
@ -143,6 +150,7 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None:
converted = im.quantize(method=method) converted = im.quantize(method=method)
converted_px = converted.load() converted_px = converted.load()
assert converted_px is not None assert converted_px is not None
assert converted.palette is not None
assert converted_px[0, 0] == converted.palette.colors[color] assert converted_px[0, 0] == converted.palette.colors[color]
@ -158,4 +166,6 @@ def test_small_palette() -> None:
im = im.quantize(palette=p) im = im.quantize(palette=p)
# Assert # Assert
assert len(im.getcolors()) == 2 quantized_colors = im.getcolors()
assert quantized_colors is not None
assert len(quantized_colors) == 2

View File

@ -237,13 +237,13 @@ class TestImagingCoreResampleAccuracy:
class TestCoreResampleConsistency: class TestCoreResampleConsistency:
def make_case( def make_case(
self, mode: str, fill: tuple[int, int, int] | float self, mode: str, fill: tuple[int, int, int] | float
) -> tuple[Image.Image, tuple[int, ...]]: ) -> tuple[Image.Image, float | tuple[int, ...]]:
im = Image.new(mode, (512, 9), fill) im = Image.new(mode, (512, 9), fill)
px = im.load() px = im.load()
assert px is not None assert px is not None
return im.resize((9, 512), Image.Resampling.LANCZOS), px[0, 0] return im.resize((9, 512), Image.Resampling.LANCZOS), px[0, 0]
def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None: def run_case(self, case: tuple[Image.Image, float | tuple[int, ...]]) -> None:
channel, color = case channel, color = case
px = channel.load() px = channel.load()
assert px is not None assert px is not None
@ -256,6 +256,7 @@ class TestCoreResampleConsistency:
def test_8u(self) -> None: def test_8u(self) -> None:
im, color = self.make_case("RGB", (0, 64, 255)) im, color = self.make_case("RGB", (0, 64, 255))
r, g, b = im.split() r, g, b = im.split()
assert isinstance(color, tuple)
self.run_case((r, color[0])) self.run_case((r, color[0]))
self.run_case((g, color[1])) self.run_case((g, color[1]))
self.run_case((b, color[2])) self.run_case((b, color[2]))
@ -290,7 +291,11 @@ class TestCoreResampleAlphaCorrect:
px = i.load() px = i.load()
assert px is not None assert px is not None
for y in range(i.size[1]): for y in range(i.size[1]):
used_colors = {px[x, y][0] for x in range(i.size[0])} used_colors = set()
for x in range(i.size[0]):
value = px[x, y]
assert isinstance(value, tuple)
used_colors.add(value[0])
assert 256 == len(used_colors), ( assert 256 == len(used_colors), (
"All colors should be present in resized image. " "All colors should be present in resized image. "
f"Only {len(used_colors)} on line {y}." f"Only {len(used_colors)} on line {y}."
@ -332,12 +337,13 @@ class TestCoreResampleAlphaCorrect:
assert px is not None assert px is not None
for y in range(i.size[1]): for y in range(i.size[1]):
for x in range(i.size[0]): for x in range(i.size[0]):
if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel: value = px[x, y]
assert isinstance(value, tuple)
if value[-1] != 0 and value[:-1] != clean_pixel:
message = ( message = (
f"pixel at ({x}, {y}) is different:\n" f"pixel at ({x}, {y}) is different:\n{value}\n{clean_pixel}"
f"{px[x, y]}\n{clean_pixel}"
) )
assert px[x, y][:3] == clean_pixel, message assert value[:3] == clean_pixel, message
def test_dirty_pixels_rgba(self) -> None: def test_dirty_pixels_rgba(self) -> None:
case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0))

View File

@ -285,14 +285,14 @@ class TestReducingGapResize:
class TestImageResize: class TestImageResize:
def test_resize(self) -> None: def test_resize(self) -> None:
def resize(mode: str, size: tuple[int, int]) -> None: def resize(mode: str, size: tuple[int, int] | list[int]) -> None:
out = hopper(mode).resize(size) out = hopper(mode).resize(size)
assert out.mode == mode assert out.mode == mode
assert out.size == size assert out.size == tuple(size)
for mode in "1", "P", "L", "RGB", "I", "F": for mode in "1", "P", "L", "RGB", "I", "F":
resize(mode, (112, 103)) resize(mode, (112, 103))
resize(mode, (188, 214)) resize(mode, [188, 214])
# Test unknown resampling filter # Test unknown resampling filter
with hopper() as im: with hopper() as im:

View File

@ -192,8 +192,9 @@ class TestImageTransform:
im = op(im, (40, 10)) im = op(im, (40, 10))
colors = sorted(im.getcolors()) colors = im.getcolors()
assert colors == sorted( assert colors is not None
assert sorted(colors) == sorted(
( (
(20 * 10, opaque), (20 * 10, opaque),
(20 * 10, transparent), (20 * 10, transparent),

View File

@ -391,23 +391,26 @@ def test_overlay() -> None:
def test_logical() -> None: def test_logical() -> None:
def table( def table(
op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int
) -> tuple[int, int, int, int]: ) -> list[float]:
out = [] out = []
for x in (a, b): for x in (a, b):
imx = Image.new("1", (1, 1), x) imx = Image.new("1", (1, 1), x)
for y in (a, b): for y in (a, b):
imy = Image.new("1", (1, 1), y) imy = Image.new("1", (1, 1), y)
out.append(op(imx, imy).getpixel((0, 0))) value = op(imx, imy).getpixel((0, 0))
return tuple(out) assert not isinstance(value, tuple)
assert value is not None
out.append(value)
return out
assert table(ImageChops.logical_and, 0, 1) == (0, 0, 0, 255) assert table(ImageChops.logical_and, 0, 1) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 1) == (0, 255, 255, 255) assert table(ImageChops.logical_or, 0, 1) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 1) == (0, 255, 255, 0) assert table(ImageChops.logical_xor, 0, 1) == [0, 255, 255, 0]
assert table(ImageChops.logical_and, 0, 128) == (0, 0, 0, 255) assert table(ImageChops.logical_and, 0, 128) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 128) == (0, 255, 255, 255) assert table(ImageChops.logical_or, 0, 128) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 128) == (0, 255, 255, 0) assert table(ImageChops.logical_xor, 0, 128) == [0, 255, 255, 0]
assert table(ImageChops.logical_and, 0, 255) == (0, 0, 0, 255) assert table(ImageChops.logical_and, 0, 255) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 255) == (0, 255, 255, 255) assert table(ImageChops.logical_or, 0, 255) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 255) == (0, 255, 255, 0) assert table(ImageChops.logical_xor, 0, 255) == [0, 255, 255, 0]

View File

@ -691,7 +691,9 @@ def test_rgb_lab(mode: str) -> None:
im = Image.new("LAB", (1, 1), (255, 0, 0)) im = Image.new("LAB", (1, 1), (255, 0, 0))
converted_im = im.convert(mode) converted_im = im.convert(mode)
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) value = converted_im.getpixel((0, 0))
assert isinstance(value, tuple)
assert value[:3] == (0, 255, 255)
def test_deprecation() -> None: def test_deprecation() -> None:

View File

@ -1,8 +1,8 @@
from __future__ import annotations from __future__ import annotations
import contextlib
import os.path import os.path
from collections.abc import Sequence from collections.abc import Sequence
from typing import Callable
import pytest import pytest
@ -857,6 +857,27 @@ def test_rounded_rectangle_corners(
) )
def test_rounded_rectangle_joined_x_different_corners() -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
# Act
draw.rounded_rectangle(
(20, 10, 80, 90),
30,
fill="red",
outline="green",
width=5,
corners=(True, False, False, False),
)
# Assert
assert_image_equal_tofile(
im, "Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png"
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"xy, radius, type", "xy, radius, type",
[ [
@ -1422,25 +1443,44 @@ def test_default_font_size() -> None:
im = Image.new("RGB", (220, 25)) im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
def check(func: Callable[[], None]) -> None:
if freetype_support:
func()
else:
with pytest.raises(ImportError):
func()
def draw_text() -> None:
draw.text((0, 0), text, font_size=16) draw.text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): check(draw_text)
def draw_textlength() -> None:
assert draw.textlength(text, font_size=16) == 216 assert draw.textlength(text, font_size=16) == 216
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): check(draw_textlength)
def draw_textbbox() -> None:
assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19) assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
check(draw_textbbox)
im = Image.new("RGB", (220, 25)) im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
def draw_multiline_text() -> None:
draw.multiline_text((0, 0), text, font_size=16) draw.multiline_text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): check(draw_multiline_text)
def draw_multiline_textbbox() -> None:
assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19) assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
check(draw_multiline_textbbox)
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_same_color_outline(bbox: Coords) -> None: def test_same_color_outline(bbox: Coords) -> None:

View File

@ -65,6 +65,36 @@ def test_mode() -> None:
ImageDraw2.Draw("L") ImageDraw2.Draw("L")
@pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4)))
def test_arc(bbox: Coords, start: float, end: float) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
pen = ImageDraw2.Pen("white", width=1)
# Act
draw.arc(bbox, pen, start, end)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1)
@pytest.mark.parametrize("bbox", BBOX)
def test_chord(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
pen = ImageDraw2.Pen("yellow")
brush = ImageDraw2.Brush("red")
# Act
draw.chord(bbox, pen, 0, 180, brush)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_RGB.png", 1)
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_ellipse(bbox: Coords) -> None: def test_ellipse(bbox: Coords) -> None:
# Arrange # Arrange
@ -123,6 +153,22 @@ def test_line_pen_as_brush(points: Coords) -> None:
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
@pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2)))
def test_pieslice(bbox: Coords, start: float, end: float) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
pen = ImageDraw2.Pen("blue")
brush = ImageDraw2.Brush("white")
# Act
draw.pieslice(bbox, pen, start, end, brush)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1)
@pytest.mark.parametrize("points", POINTS) @pytest.mark.parametrize("points", POINTS)
def test_polygon(points: Coords) -> None: def test_polygon(points: Coords) -> None:
# Arrange # Arrange

View File

@ -90,10 +90,10 @@ class TestImageFile:
data = f.read() data = f.read()
with ImageFile.Parser() as p: with ImageFile.Parser() as p:
p.feed(data) p.feed(data)
assert p.image is not None
assert (48, 48) == p.image.size assert (48, 48) == p.image.size
@skip_unless_feature("webp") @skip_unless_feature("webp")
@skip_unless_feature("webp_anim")
def test_incremental_webp(self) -> None: def test_incremental_webp(self) -> None:
with ImageFile.Parser() as p: with ImageFile.Parser() as p:
with open("Tests/images/hopper.webp", "rb") as f: with open("Tests/images/hopper.webp", "rb") as f:
@ -103,6 +103,7 @@ class TestImageFile:
assert not p.image assert not p.image
p.feed(f.read()) p.feed(f.read())
assert p.image is not None
assert (128, 128) == p.image.size assert (128, 128) == p.image.size
@skip_unless_feature("zlib") @skip_unless_feature("zlib")
@ -125,7 +126,7 @@ class TestImageFile:
def test_raise_typeerror(self) -> None: def test_raise_typeerror(self) -> None:
with pytest.raises(TypeError): with pytest.raises(TypeError):
parser = ImageFile.Parser() parser = ImageFile.Parser()
parser.feed(1) parser.feed(1) # type: ignore[arg-type]
def test_negative_stride(self) -> None: def test_negative_stride(self) -> None:
with open("Tests/images/raw_negative_stride.bin", "rb") as f: with open("Tests/images/raw_negative_stride.bin", "rb") as f:
@ -209,7 +210,7 @@ class MockPyDecoder(ImageFile.PyDecoder):
super().__init__(mode, *args) super().__init__(mode, *args)
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
# eof # eof
return -1, 0 return -1, 0
@ -237,7 +238,9 @@ class MockImageFile(ImageFile.ImageFile):
self.rawmode = "RGBA" self.rawmode = "RGBA"
self._mode = "RGBA" self._mode = "RGBA"
self._size = (200, 200) self._size = (200, 200)
self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] self.tile = [
ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)
]
class CodecsTest: class CodecsTest:
@ -267,7 +270,7 @@ class TestPyDecoder(CodecsTest):
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)] im.tile = [ImageFile._Tile("MOCK", None, 32, None)]
im.load() im.load()
@ -280,12 +283,12 @@ class TestPyDecoder(CodecsTest):
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)]
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() im.load()
im.tile = [("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)] im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)]
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() im.load()
@ -293,19 +296,27 @@ class TestPyDecoder(CodecsTest):
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)] im.tile = [
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None
)
]
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() im.load()
im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)] im.tile = [
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None
)
]
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() im.load()
def test_decode(self) -> None: def test_decode(self) -> None:
decoder = ImageFile.PyDecoder(None) decoder = ImageFile.PyDecoder("")
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
decoder.decode(None) decoder.decode(b"")
class TestPyEncoder(CodecsTest): class TestPyEncoder(CodecsTest):
@ -316,7 +327,13 @@ class TestPyEncoder(CodecsTest):
fp = BytesIO() fp = BytesIO()
ImageFile._save( ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] im,
fp,
[
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB"
)
],
) )
assert MockPyEncoder.last assert MockPyEncoder.last
@ -329,10 +346,10 @@ class TestPyEncoder(CodecsTest):
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)] im.tile = [ImageFile._Tile("MOCK", None, 32, None)]
fp = BytesIO() fp = BytesIO()
ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")])
assert MockPyEncoder.last assert MockPyEncoder.last
assert MockPyEncoder.last.state.xoff == 0 assert MockPyEncoder.last.state.xoff == 0
@ -349,7 +366,9 @@ class TestPyEncoder(CodecsTest):
MockPyEncoder.last = None MockPyEncoder.last = None
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFile._save( ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] im,
fp,
[ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")],
) )
last: MockPyEncoder | None = MockPyEncoder.last last: MockPyEncoder | None = MockPyEncoder.last
assert last assert last
@ -357,7 +376,9 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFile._save( ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] im,
fp,
[ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")],
) )
def test_oversize(self) -> None: def test_oversize(self) -> None:
@ -370,18 +391,26 @@ class TestPyEncoder(CodecsTest):
ImageFile._save( ImageFile._save(
im, im,
fp, fp,
[("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], [
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB"
)
],
) )
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFile._save( ImageFile._save(
im, im,
fp, fp,
[("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], [
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB"
)
],
) )
def test_encode(self) -> None: def test_encode(self) -> None:
encoder = ImageFile.PyEncoder(None) encoder = ImageFile.PyEncoder("")
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode(0) encoder.encode(0)
@ -394,7 +423,7 @@ class TestPyEncoder(CodecsTest):
encoder.encode_to_pyfd() encoder.encode_to_pyfd()
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode_to_file(None, None) encoder.encode_to_file(0, 0)
def test_zero_height(self) -> None: def test_zero_height(self) -> None:
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):

View File

@ -717,14 +717,14 @@ def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None:
font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
_check_text(font, "Tests/images/variation_adobe.png", 11) _check_text(font, "Tests/images/variation_adobe.png", 11)
for name in ["Bold", b"Bold"]: for name in ("Bold", b"Bold"):
font.set_variation_by_name(name) font.set_variation_by_name(name)
assert font.getname()[1] == "Bold" assert font.getname()[1] == "Bold"
_check_text(font, "Tests/images/variation_adobe_name.png", 16) _check_text(font, "Tests/images/variation_adobe_name.png", 16)
font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
_check_text(font, "Tests/images/variation_tiny.png", 40) _check_text(font, "Tests/images/variation_tiny.png", 40)
for name in ["200", b"200"]: for name in ("200", b"200"):
font.set_variation_by_name(name) font.set_variation_by_name(name)
assert font.getname()[1] == "200" assert font.getname()[1] == "200"
_check_text(font, "Tests/images/variation_tiny_name.png", 40) _check_text(font, "Tests/images/variation_tiny_name.png", 40)
@ -1113,6 +1113,9 @@ def test_bytes(font: ImageFont.FreeTypeFont) -> None:
) )
assert font.getmask2(b"test")[1] == font.getmask2("test")[1] assert font.getmask2(b"test")[1] == font.getmask2("test")[1]
with pytest.raises(TypeError):
font.getlength((0, 0)) # type: ignore[arg-type]
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_file", "test_file",

View File

@ -60,6 +60,8 @@ class TestImageGrab:
def test_grabclipboard(self) -> None: def test_grabclipboard(self) -> None:
if sys.platform == "darwin": if sys.platform == "darwin":
subprocess.call(["screencapture", "-cx"]) subprocess.call(["screencapture", "-cx"])
ImageGrab.grabclipboard()
elif sys.platform == "win32": elif sys.platform == "win32":
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
p.stdin.write( p.stdin.write(
@ -69,6 +71,8 @@ $bmp = New-Object Drawing.Bitmap 200, 200
[Windows.Forms.Clipboard]::SetImage($bmp)""" [Windows.Forms.Clipboard]::SetImage($bmp)"""
) )
p.communicate() p.communicate()
ImageGrab.grabclipboard()
else: else:
if not shutil.which("wl-paste") and not shutil.which("xclip"): if not shutil.which("wl-paste") and not shutil.which("xclip"):
with pytest.raises( with pytest.raises(
@ -77,9 +81,6 @@ $bmp = New-Object Drawing.Bitmap 200, 200
r" ImageGrab.grabclipboard\(\) on Linux", r" ImageGrab.grabclipboard\(\) on Linux",
): ):
ImageGrab.grabclipboard() ImageGrab.grabclipboard()
return
ImageGrab.grabclipboard()
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") @pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_file(self) -> None: def test_grabclipboard_file(self) -> None:

View File

@ -1,5 +1,9 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
import pytest
from PIL import Image, ImageMath from PIL import Image, ImageMath
@ -19,7 +23,7 @@ I = Image.new("I", (1, 1), 4) # noqa: E741
A2 = A.resize((2, 2)) A2 = A.resize((2, 2))
B2 = B.resize((2, 2)) B2 = B.resize((2, 2))
images = {"A": A, "B": B, "F": F, "I": I} images: dict[str, Any] = {"A": A, "B": B, "F": F, "I": I}
def test_sanity() -> None: def test_sanity() -> None:
@ -30,13 +34,13 @@ def test_sanity() -> None:
== "I 3" == "I 3"
) )
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images)) pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], **images))
== "I 3" == "I 3"
) )
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) + args["B"], images lambda args: args["float"](args["A"]) + args["B"], **images
) )
) )
== "F 3.0" == "F 3.0"
@ -44,42 +48,47 @@ def test_sanity() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["int"](args["float"](args["A"]) + args["B"]), images lambda args: args["int"](args["float"](args["A"]) + args["B"]), **images
) )
) )
== "I 3" == "I 3"
) )
def test_options_deprecated() -> None:
with pytest.warns(DeprecationWarning):
assert ImageMath.lambda_eval(lambda args: 1, images) == 1
def test_ops() -> None: def test_ops() -> None:
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, images)) == "I -1" assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, **images)) == "I -1"
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images)) pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], **images))
== "I 3" == "I 3"
) )
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], images)) pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], **images))
== "I -1" == "I -1"
) )
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], images)) pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], **images))
== "I 2" == "I 2"
) )
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], images)) pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], **images))
== "I 0" == "I 0"
) )
assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, images)) == "I 4" assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, **images)) == "I 4"
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, images)) pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, **images))
== "I 2147483647" == "I 2147483647"
) )
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) + args["B"], images lambda args: args["float"](args["A"]) + args["B"], **images
) )
) )
== "F 3.0" == "F 3.0"
@ -87,7 +96,7 @@ def test_ops() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) - args["B"], images lambda args: args["float"](args["A"]) - args["B"], **images
) )
) )
== "F -1.0" == "F -1.0"
@ -95,7 +104,7 @@ def test_ops() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) * args["B"], images lambda args: args["float"](args["A"]) * args["B"], **images
) )
) )
== "F 2.0" == "F 2.0"
@ -103,31 +112,33 @@ def test_ops() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) / args["B"], images lambda args: args["float"](args["A"]) / args["B"], **images
) )
) )
== "F 0.5" == "F 0.5"
) )
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, images)) pixel(
ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, **images)
)
== "F 4.0" == "F 4.0"
) )
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, images) ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, **images)
) )
== "F 8589934592.0" == "F 8589934592.0"
) )
def test_logical() -> None: def test_logical() -> None:
assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], images)) == 0 assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], **images)) == 0
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], images)) pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], **images))
== "L 2" == "L 2"
) )
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], images)) pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], **images))
== "L 1" == "L 1"
) )
@ -136,7 +147,7 @@ def test_convert() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["convert"](args["A"] + args["B"], "L"), images lambda args: args["convert"](args["A"] + args["B"], "L"), **images
) )
) )
== "L 3" == "L 3"
@ -144,7 +155,7 @@ def test_convert() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["convert"](args["A"] + args["B"], "1"), images lambda args: args["convert"](args["A"] + args["B"], "1"), **images
) )
) )
== "1 0" == "1 0"
@ -152,7 +163,7 @@ def test_convert() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["convert"](args["A"] + args["B"], "RGB"), images lambda args: args["convert"](args["A"] + args["B"], "RGB"), **images
) )
) )
== "RGB (3, 3, 3)" == "RGB (3, 3, 3)"
@ -163,7 +174,7 @@ def test_compare() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["min"](args["A"], args["B"]), images lambda args: args["min"](args["A"], args["B"]), **images
) )
) )
== "I 1" == "I 1"
@ -171,13 +182,13 @@ def test_compare() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["max"](args["A"], args["B"]), images lambda args: args["max"](args["A"], args["B"]), **images
) )
) )
== "I 2" == "I 2"
) )
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, images)) == "I 1" assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, **images)) == "I 1"
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, images)) == "I 0" assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, **images)) == "I 0"
def test_one_image_larger() -> None: def test_one_image_larger() -> None:

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
import pytest import pytest
from PIL import Image, ImageMath from PIL import Image, ImageMath
@ -21,16 +23,16 @@ I = Image.new("I", (1, 1), 4) # noqa: E741
A2 = A.resize((2, 2)) A2 = A.resize((2, 2))
B2 = B.resize((2, 2)) B2 = B.resize((2, 2))
images = {"A": A, "B": B, "F": F, "I": I} images: dict[str, Any] = {"A": A, "B": B, "F": F, "I": I}
def test_sanity() -> None: def test_sanity() -> None:
assert ImageMath.unsafe_eval("1") == 1 assert ImageMath.unsafe_eval("1") == 1
assert ImageMath.unsafe_eval("1+A", A=2) == 3 assert ImageMath.unsafe_eval("1+A", A=2) == 3
assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B)) == "I 3" assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B)) == "I 3"
assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3" assert pixel(ImageMath.unsafe_eval("A+B", **images)) == "I 3"
assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0" assert pixel(ImageMath.unsafe_eval("float(A)+B", **images)) == "F 3.0"
assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", images)) == "I 3" assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", **images)) == "I 3"
def test_eval_deprecated() -> None: def test_eval_deprecated() -> None:
@ -38,23 +40,28 @@ def test_eval_deprecated() -> None:
assert ImageMath.eval("1") == 1 assert ImageMath.eval("1") == 1
def test_options_deprecated() -> None:
with pytest.warns(DeprecationWarning):
assert ImageMath.unsafe_eval("1", images) == 1
def test_ops() -> None: def test_ops() -> None:
assert pixel(ImageMath.unsafe_eval("-A", images)) == "I -1" assert pixel(ImageMath.unsafe_eval("-A", **images)) == "I -1"
assert pixel(ImageMath.unsafe_eval("+B", images)) == "L 2" assert pixel(ImageMath.unsafe_eval("+B", **images)) == "L 2"
assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3" assert pixel(ImageMath.unsafe_eval("A+B", **images)) == "I 3"
assert pixel(ImageMath.unsafe_eval("A-B", images)) == "I -1" assert pixel(ImageMath.unsafe_eval("A-B", **images)) == "I -1"
assert pixel(ImageMath.unsafe_eval("A*B", images)) == "I 2" assert pixel(ImageMath.unsafe_eval("A*B", **images)) == "I 2"
assert pixel(ImageMath.unsafe_eval("A/B", images)) == "I 0" assert pixel(ImageMath.unsafe_eval("A/B", **images)) == "I 0"
assert pixel(ImageMath.unsafe_eval("B**2", images)) == "I 4" assert pixel(ImageMath.unsafe_eval("B**2", **images)) == "I 4"
assert pixel(ImageMath.unsafe_eval("B**33", images)) == "I 2147483647" assert pixel(ImageMath.unsafe_eval("B**33", **images)) == "I 2147483647"
assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0" assert pixel(ImageMath.unsafe_eval("float(A)+B", **images)) == "F 3.0"
assert pixel(ImageMath.unsafe_eval("float(A)-B", images)) == "F -1.0" assert pixel(ImageMath.unsafe_eval("float(A)-B", **images)) == "F -1.0"
assert pixel(ImageMath.unsafe_eval("float(A)*B", images)) == "F 2.0" assert pixel(ImageMath.unsafe_eval("float(A)*B", **images)) == "F 2.0"
assert pixel(ImageMath.unsafe_eval("float(A)/B", images)) == "F 0.5" assert pixel(ImageMath.unsafe_eval("float(A)/B", **images)) == "F 0.5"
assert pixel(ImageMath.unsafe_eval("float(B)**2", images)) == "F 4.0" assert pixel(ImageMath.unsafe_eval("float(B)**2", **images)) == "F 4.0"
assert pixel(ImageMath.unsafe_eval("float(B)**33", images)) == "F 8589934592.0" assert pixel(ImageMath.unsafe_eval("float(B)**33", **images)) == "F 8589934592.0"
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -72,33 +79,33 @@ def test_prevent_exec(expression: str) -> None:
def test_prevent_double_underscores() -> None: def test_prevent_double_underscores() -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageMath.unsafe_eval("1", {"__": None}) ImageMath.unsafe_eval("1", __=None)
def test_prevent_builtins() -> None: def test_prevent_builtins() -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageMath.unsafe_eval("(lambda: exec('exit()'))()", {"exec": None}) ImageMath.unsafe_eval("(lambda: exec('exit()'))()", exec=None)
def test_logical() -> None: def test_logical() -> None:
assert pixel(ImageMath.unsafe_eval("not A", images)) == 0 assert pixel(ImageMath.unsafe_eval("not A", **images)) == 0
assert pixel(ImageMath.unsafe_eval("A and B", images)) == "L 2" assert pixel(ImageMath.unsafe_eval("A and B", **images)) == "L 2"
assert pixel(ImageMath.unsafe_eval("A or B", images)) == "L 1" assert pixel(ImageMath.unsafe_eval("A or B", **images)) == "L 1"
def test_convert() -> None: def test_convert() -> None:
assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", images)) == "L 3" assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", **images)) == "L 3"
assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", images)) == "1 0" assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", **images)) == "1 0"
assert ( assert (
pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)" pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", **images)) == "RGB (3, 3, 3)"
) )
def test_compare() -> None: def test_compare() -> None:
assert pixel(ImageMath.unsafe_eval("min(A, B)", images)) == "I 1" assert pixel(ImageMath.unsafe_eval("min(A, B)", **images)) == "I 1"
assert pixel(ImageMath.unsafe_eval("max(A, B)", images)) == "I 2" assert pixel(ImageMath.unsafe_eval("max(A, B)", **images)) == "I 2"
assert pixel(ImageMath.unsafe_eval("A == 1", images)) == "I 1" assert pixel(ImageMath.unsafe_eval("A == 1", **images)) == "I 1"
assert pixel(ImageMath.unsafe_eval("A == 2", images)) == "I 0" assert pixel(ImageMath.unsafe_eval("A == 2", **images)) == "I 0"
def test_one_image_larger() -> None: def test_one_image_larger() -> None:

View File

@ -41,11 +41,16 @@ A = string_to_img(
def img_to_string(im: Image.Image) -> str: def img_to_string(im: Image.Image) -> str:
"""Turn a (small) binary image into a string representation""" """Turn a (small) binary image into a string representation"""
chars = ".1" chars = ".1"
width, height = im.size result = []
return "\n".join( for r in range(im.height):
"".join(chars[im.getpixel((c, r)) > 0] for c in range(width)) line = ""
for r in range(height) for c in range(im.width):
) value = im.getpixel((c, r))
assert not isinstance(value, tuple)
assert value is not None
line += chars[value > 0]
result.append(line)
return "\n".join(result)
def img_string_normalize(im: str) -> str: def img_string_normalize(im: str) -> str:

View File

@ -259,20 +259,26 @@ def test_colorize_2color() -> None:
left = (0, 1) left = (0, 1)
middle = (127, 1) middle = (127, 1)
right = (255, 1) right = (255, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(left), value,
(255, 0, 0), (255, 0, 0),
threshold=1, threshold=1,
msg="black test pixel incorrect", msg="black test pixel incorrect",
) )
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(middle), value,
(127, 63, 0), (127, 63, 0),
threshold=1, threshold=1,
msg="mid test pixel incorrect", msg="mid test pixel incorrect",
) )
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(right), value,
(0, 127, 0), (0, 127, 0),
threshold=1, threshold=1,
msg="white test pixel incorrect", msg="white test pixel incorrect",
@ -295,20 +301,26 @@ def test_colorize_2color_offset() -> None:
left = (25, 1) left = (25, 1)
middle = (75, 1) middle = (75, 1)
right = (125, 1) right = (125, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(left), value,
(255, 0, 0), (255, 0, 0),
threshold=1, threshold=1,
msg="black test pixel incorrect", msg="black test pixel incorrect",
) )
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(middle), value,
(127, 63, 0), (127, 63, 0),
threshold=1, threshold=1,
msg="mid test pixel incorrect", msg="mid test pixel incorrect",
) )
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(right), value,
(0, 127, 0), (0, 127, 0),
threshold=1, threshold=1,
msg="white test pixel incorrect", msg="white test pixel incorrect",
@ -339,29 +351,37 @@ def test_colorize_3color_offset() -> None:
middle = (100, 1) middle = (100, 1)
right_middle = (150, 1) right_middle = (150, 1)
right = (225, 1) right = (225, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(left), value,
(255, 0, 0), (255, 0, 0),
threshold=1, threshold=1,
msg="black test pixel incorrect", msg="black test pixel incorrect",
) )
value = im_test.getpixel(left_middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(left_middle), value,
(127, 0, 127), (127, 0, 127),
threshold=1, threshold=1,
msg="low-mid test pixel incorrect", msg="low-mid test pixel incorrect",
) )
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(value, (0, 0, 255), threshold=1, msg="mid incorrect")
value = im_test.getpixel(right_middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(middle), (0, 0, 255), threshold=1, msg="mid incorrect" value,
)
assert_tuple_approx_equal(
im_test.getpixel(right_middle),
(0, 63, 127), (0, 63, 127),
threshold=1, threshold=1,
msg="high-mid test pixel incorrect", msg="high-mid test pixel incorrect",
) )
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(right), value,
(0, 127, 0), (0, 127, 0),
threshold=1, threshold=1,
msg="white test pixel incorrect", msg="white test pixel incorrect",
@ -370,7 +390,7 @@ def test_colorize_3color_offset() -> None:
def test_exif_transpose() -> None: def test_exif_transpose() -> None:
exts = [".jpg"] exts = [".jpg"]
if features.check("webp") and features.check("webp_anim"): if features.check("webp"):
exts.append(".webp") exts.append(".webp")
for ext in exts: for ext in exts:
with Image.open("Tests/images/hopper" + ext) as base_im: with Image.open("Tests/images/hopper" + ext) as base_im:
@ -444,6 +464,7 @@ def test_exif_transpose_xml_without_xmp() -> None:
del im.info["xmp"] del im.info["xmp"]
transposed_im = ImageOps.exif_transpose(im) transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif() assert 0x0112 not in transposed_im.getexif()

View File

@ -16,8 +16,11 @@ def test_sanity() -> None:
def test_register() -> None: def test_register() -> None:
# Test registering a viewer that is not a class # Test registering a viewer that is an instance
ImageShow.register("not a class") class TestViewer(ImageShow.Viewer):
pass
ImageShow.register(TestViewer())
# Restore original state # Restore original state
ImageShow._viewers.pop() ImageShow._viewers.pop()

View File

@ -45,10 +45,12 @@ def test_kw() -> None:
# Test "file" # Test "file"
im = ImageTk._get_image_from_kw(kw) im = ImageTk._get_image_from_kw(kw)
assert im is not None
assert_image_equal(im, im1) assert_image_equal(im, im1)
# Test "data" # Test "data"
im = ImageTk._get_image_from_kw(kw) im = ImageTk._get_image_from_kw(kw)
assert im is not None
assert_image_equal(im, im2) assert_image_equal(im, im2)
# Test no relevant entry # Test no relevant entry
@ -107,3 +109,6 @@ def test_bitmapimage() -> None:
# reloaded = ImageTk.getimage(im_tk) # reloaded = ImageTk.getimage(im_tk)
# assert_image_equal(reloaded, im) # assert_image_equal(reloaded, im)
with pytest.raises(ValueError):
ImageTk.BitmapImage()

View File

@ -57,6 +57,9 @@ class TestImageWinDib:
# Assert # Assert
assert dib.size == (128, 128) assert dib.size == (128, 128)
with pytest.raises(ValueError):
ImageWin.Dib(mode)
def test_dib_paste(self) -> None: def test_dib_paste(self) -> None:
# Arrange # Arrange
im = hopper() im = hopper()

View File

@ -198,6 +198,15 @@ def test_putdata() -> None:
assert len(im.getdata()) == len(arr) assert len(im.getdata()) == len(arr)
def test_resize() -> None:
im = hopper()
size = (64, 64)
im_resized = im.resize(numpy.array(size))
assert im_resized.size == size
@pytest.mark.parametrize( @pytest.mark.parametrize(
"dtype", "dtype",
( (

View File

@ -46,7 +46,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non
@pytest.mark.parametrize( @pytest.mark.parametrize(
("test_file", "test_mode"), "test_file, test_mode",
[ [
("Tests/images/hopper.jpg", None), ("Tests/images/hopper.jpg", None),
("Tests/images/hopper.jpg", "L"), ("Tests/images/hopper.jpg", "L"),

View File

@ -5,8 +5,6 @@ import sys
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
import pytest
from PIL import Image, PSDraw from PIL import Image, PSDraw
@ -49,17 +47,16 @@ def test_draw_postscript(tmp_path: Path) -> None:
assert os.path.getsize(tempfile) > 0 assert os.path.getsize(tempfile) > 0
@pytest.mark.parametrize("buffer", (True, False)) def test_stdout() -> None:
def test_stdout(buffer: bool) -> None:
# Temporarily redirect stdout # Temporarily redirect stdout
old_stdout = sys.stdout old_stdout = sys.stdout
class MyStdOut: class MyStdOut:
buffer = BytesIO() buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() mystdout = MyStdOut()
sys.stdout = mystdout # type: ignore[assignment] sys.stdout = mystdout
ps = PSDraw.PSDraw() ps = PSDraw.PSDraw()
_create_document(ps) _create_document(ps)
@ -67,6 +64,4 @@ def test_stdout(buffer: bool) -> None:
# Reset stdout # Reset stdout
sys.stdout = old_stdout sys.stdout = old_stdout
if isinstance(mystdout, MyStdOut): assert mystdout.buffer.getvalue() != b""
mystdout = mystdout.buffer
assert mystdout.getvalue() != b""

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Union
import pytest import pytest
@ -8,6 +9,20 @@ from PIL import Image, ImageQt
from .helper import assert_image_equal_tofile, assert_image_similar, hopper from .helper import assert_image_equal_tofile, assert_image_similar, hopper
if TYPE_CHECKING:
import PyQt6
import PySide6
QApplication = Union[PyQt6.QtWidgets.QApplication, PySide6.QtWidgets.QApplication]
QHBoxLayout = Union[PyQt6.QtWidgets.QHBoxLayout, PySide6.QtWidgets.QHBoxLayout]
QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
QLabel = Union[PyQt6.QtWidgets.QLabel, PySide6.QtWidgets.QLabel]
QPainter = Union[PyQt6.QtGui.QPainter, PySide6.QtGui.QPainter]
QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
QPoint = Union[PyQt6.QtCore.QPoint, PySide6.QtCore.QPoint]
QRegion = Union[PyQt6.QtGui.QRegion, PySide6.QtGui.QRegion]
QWidget = Union[PyQt6.QtWidgets.QWidget, PySide6.QtWidgets.QWidget]
if ImageQt.qt_is_installed: if ImageQt.qt_is_installed:
from PIL.ImageQt import QPixmap from PIL.ImageQt import QPixmap
@ -20,7 +35,7 @@ if ImageQt.qt_is_installed:
from PySide6.QtGui import QImage, QPainter, QRegion from PySide6.QtGui import QImage, QPainter, QRegion
from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
class Example(QWidget): class Example(QWidget): # type: ignore[misc]
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@ -28,11 +43,12 @@ if ImageQt.qt_is_installed:
qimage = ImageQt.ImageQt(img) qimage = ImageQt.ImageQt(img)
pixmap1 = ImageQt.QPixmap.fromImage(qimage) pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage)
QHBoxLayout(self) # hbox # hbox
QHBoxLayout(self) # type: ignore[operator]
lbl = QLabel(self) lbl = QLabel(self) # type: ignore[operator]
# Segfault in the problem # Segfault in the problem
lbl.setPixmap(pixmap1.copy()) lbl.setPixmap(pixmap1.copy())
@ -46,7 +62,7 @@ def roundtrip(expected: Image.Image) -> None:
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
def test_sanity(tmp_path: Path) -> None: def test_sanity(tmp_path: Path) -> None:
# Segfault test # Segfault test
app: QApplication | None = QApplication([]) app: QApplication | None = QApplication([]) # type: ignore[operator]
ex = Example() ex = Example()
assert app # Silence warning assert app # Silence warning
assert ex # Silence warning assert ex # Silence warning
@ -56,7 +72,7 @@ def test_sanity(tmp_path: Path) -> None:
im = hopper(mode) im = hopper(mode)
data = ImageQt.toqpixmap(im) data = ImageQt.toqpixmap(im)
assert isinstance(data, QPixmap) assert data.__class__.__name__ == "QPixmap"
assert not data.isNull() assert not data.isNull()
# Test saving the file # Test saving the file
@ -64,14 +80,14 @@ def test_sanity(tmp_path: Path) -> None:
data.save(tempfile) data.save(tempfile)
# Render the image # Render the image
qimage = ImageQt.ImageQt(im) imageqt = ImageQt.ImageQt(im)
data = QPixmap.fromImage(qimage) data = getattr(QPixmap, "fromImage")(imageqt)
qt_format = QImage.Format if ImageQt.qt_version == "6" else QImage qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage
qimage = QImage(128, 128, qt_format.Format_ARGB32) qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) # type: ignore[operator]
painter = QPainter(qimage) painter = QPainter(qimage) # type: ignore[operator]
image_label = QLabel() image_label = QLabel() # type: ignore[operator]
image_label.setPixmap(data) image_label.setPixmap(data)
image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) # type: ignore[operator]
painter.end() painter.end()
rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png") rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png")
qimage.save(rendered_tempfile) qimage.save(rendered_tempfile)

View File

@ -21,7 +21,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
src = hopper(mode) src = hopper(mode)
data = ImageQt.toqimage(src) data = ImageQt.toqimage(src)
assert isinstance(data, QImage) assert isinstance(data, QImage) # type: ignore[arg-type, misc]
assert not data.isNull() assert not data.isNull()
# reload directly from the qimage # reload directly from the qimage

View File

@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
from .helper import hopper, skip_unless_feature from .helper import hopper, skip_unless_feature
def _test_equal(num, denom, target) -> None: def _test_equal(
num: float | Fraction | IFDRational,
denom: int,
target: float | Fraction | IFDRational,
) -> None:
t = IFDRational(num, denom) t = IFDRational(num, denom)
assert target == t assert target == t
@ -50,8 +54,8 @@ def test_nonetype() -> None:
assert xres.denominator is not None assert xres.denominator is not None
assert yres._val is not None assert yres._val is not None
assert xres and 1 assert xres
assert xres and yres assert yres
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -30,28 +30,6 @@ def test_is_not_path(tmp_path: Path) -> None:
assert not it_is_not assert not it_is_not
def test_is_directory() -> None:
# Arrange
directory = "Tests"
# Act
it_is = _util.is_directory(directory)
# Assert
assert it_is
def test_is_not_directory() -> None:
# Arrange
text = "abc"
# Act
it_is_not = _util.is_directory(text)
# Assert
assert not it_is_not
def test_deferred_error() -> None: def test_deferred_error() -> None:
# Arrange # Arrange

View File

@ -2,7 +2,7 @@
# install libimagequant # install libimagequant
archive_name=libimagequant archive_name=libimagequant
archive_version=4.3.1 archive_version=4.3.3
archive=$archive_name-$archive_version archive=$archive_name-$archive_version

View File

@ -121,7 +121,7 @@ nitpicky = True
# generating warnings in “nitpicky mode”. Note that type should include the domain name # generating warnings in “nitpicky mode”. Note that type should include the domain name
# if present. Example entries would be ('py:func', 'int') or # if present. Example entries would be ('py:func', 'int') or
# ('envvar', 'LD_LIBRARY_PATH'). # ('envvar', 'LD_LIBRARY_PATH').
nitpick_ignore = [("py:class", "_CmsProfileCompatible")] nitpick_ignore = [("py:class", "_io.BytesIO"), ("py:class", "_CmsProfileCompatible")]
# -- Options for HTML output ---------------------------------------------- # -- Options for HTML output ----------------------------------------------

View File

@ -109,12 +109,46 @@ ImageDraw.getdraw hints parameter
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and
:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword
arguments can be used instead.
JpegImageFile.huffman_ac and JpegImageFile.huffman_dc
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They
have been deprecated, and will be removed in Pillow 12 (2025-10-15).
Specific WebP Feature Checks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``features.check("transp_webp")``, ``features.check("webp_mux")`` and
``features.check("webp_anim")`` are now deprecated. They will always return
``True`` if the WebP module is installed, until they are removed in Pillow
12.0.0 (2025-10-15).
Removed features Removed features
---------------- ----------------
Deprecated features are only removed in major releases after an appropriate Deprecated features are only removed in major releases after an appropriate
period of deprecation has passed. period of deprecation has passed.
TiffImagePlugin IFD_LEGACY_API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionremoved:: 11.0.0
``TiffImagePlugin.IFD_LEGACY_API`` was removed, as it was an unused setting.
PSFile PSFile
~~~~~~ ~~~~~~

View File

@ -14,6 +14,7 @@ from __future__ import annotations
import struct import struct
from io import BytesIO from io import BytesIO
from typing import IO
from PIL import Image, ImageFile from PIL import Image, ImageFile
@ -94,26 +95,26 @@ DXT3_FOURCC = 0x33545844
DXT5_FOURCC = 0x35545844 DXT5_FOURCC = 0x35545844
def _decode565(bits): def _decode565(bits: int) -> tuple[int, int, int]:
a = ((bits >> 11) & 0x1F) << 3 a = ((bits >> 11) & 0x1F) << 3
b = ((bits >> 5) & 0x3F) << 2 b = ((bits >> 5) & 0x3F) << 2
c = (bits & 0x1F) << 3 c = (bits & 0x1F) << 3
return a, b, c return a, b, c
def _c2a(a, b): def _c2a(a: int, b: int) -> int:
return (2 * a + b) // 3 return (2 * a + b) // 3
def _c2b(a, b): def _c2b(a: int, b: int) -> int:
return (a + b) // 2 return (a + b) // 2
def _c3(a, b): def _c3(a: int, b: int) -> int:
return (2 * b + a) // 3 return (2 * b + a) // 3
def _dxt1(data, width, height): def _dxt1(data: IO[bytes], width: int, height: int) -> bytes:
# TODO implement this function as pixel format in decode.c # TODO implement this function as pixel format in decode.c
ret = bytearray(4 * width * height) ret = bytearray(4 * width * height)
@ -151,7 +152,7 @@ def _dxt1(data, width, height):
return bytes(ret) return bytes(ret)
def _dxtc_alpha(a0, a1, ac0, ac1, ai): def _dxtc_alpha(a0: int, a1: int, ac0: int, ac1: int, ai: int) -> int:
if ai <= 12: if ai <= 12:
ac = (ac0 >> ai) & 7 ac = (ac0 >> ai) & 7
elif ai == 15: elif ai == 15:
@ -175,7 +176,7 @@ def _dxtc_alpha(a0, a1, ac0, ac1, ai):
return alpha return alpha
def _dxt5(data, width, height): def _dxt5(data: IO[bytes], width: int, height: int) -> bytes:
# TODO implement this function as pixel format in decode.c # TODO implement this function as pixel format in decode.c
ret = bytearray(4 * width * height) ret = bytearray(4 * width * height)
@ -211,7 +212,7 @@ class DdsImageFile(ImageFile.ImageFile):
format = "DDS" format = "DDS"
format_description = "DirectDraw Surface" format_description = "DirectDraw Surface"
def _open(self): def _open(self) -> None:
if not _accept(self.fp.read(4)): if not _accept(self.fp.read(4)):
msg = "not a DDS file" msg = "not a DDS file"
raise SyntaxError(msg) raise SyntaxError(msg)
@ -242,19 +243,22 @@ class DdsImageFile(ImageFile.ImageFile):
elif fourcc == b"DXT5": elif fourcc == b"DXT5":
self.decoder = "DXT5" self.decoder = "DXT5"
else: else:
msg = f"Unimplemented pixel format {fourcc}" msg = f"Unimplemented pixel format {repr(fourcc)}"
raise NotImplementedError(msg) raise NotImplementedError(msg)
self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] self.tile = [
ImageFile._Tile(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))
]
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
pass pass
class DXT1Decoder(ImageFile.PyDecoder): class DXT1Decoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
try: try:
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e: except struct.error as e:
@ -266,7 +270,8 @@ class DXT1Decoder(ImageFile.PyDecoder):
class DXT5Decoder(ImageFile.PyDecoder): class DXT5Decoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
try: try:
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e: except struct.error as e:
@ -279,7 +284,7 @@ Image.register_decoder("DXT1", DXT1Decoder)
Image.register_decoder("DXT5", DXT5Decoder) Image.register_decoder("DXT5", DXT5Decoder)
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"DDS " return prefix[:4] == b"DDS "

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -1220,8 +1220,7 @@ using the general tags available through tiffinfo.
WebP WebP
^^^^ ^^^^
Pillow reads and writes WebP files. The specifics of Pillow's capabilities with Pillow reads and writes WebP files. Requires libwebp v0.5.0 or later.
this format are currently undocumented.
.. _webp-saving: .. _webp-saving:
@ -1249,29 +1248,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**exact** **exact**
If true, preserve the transparent RGB values. Otherwise, discard If true, preserve the transparent RGB values. Otherwise, discard
invisible RGB values for better compression. Defaults to false. invisible RGB values for better compression. Defaults to false.
Requires libwebp 0.5.0 or later.
**icc_profile** **icc_profile**
The ICC Profile to include in the saved file. Only supported if The ICC Profile to include in the saved file.
the system WebP library was built with webpmux support.
**exif** **exif**
The exif data to include in the saved file. Only supported if The exif data to include in the saved file.
the system WebP library was built with webpmux support.
**xmp** **xmp**
The XMP data to include in the saved file. Only supported if The XMP data to include in the saved file.
the system WebP library was built with webpmux support.
Saving sequences Saving sequences
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
.. note::
Support for animated WebP files will only be enabled if the system WebP
library is v0.5.0 or later. You can check webp animation support at
runtime by calling ``features.check("webp_anim")``.
When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default
only the first frame of a multiframe image will be saved. If the ``save_all`` only the first frame of a multiframe image will be saved. If the ``save_all``
argument is present and true, then all frames will be saved, and the following argument is present and true, then all frames will be saved, and the following
@ -1528,19 +1517,21 @@ To add other read or write support, use
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF
handler. :: handler. ::
from PIL import Image from typing import IO
from PIL import Image, ImageFile
from PIL import WmfImagePlugin from PIL import WmfImagePlugin
class WmfHandler: class WmfHandler(ImageFile.StubHandler):
def open(self, im): def open(self, im: ImageFile.StubImageFile) -> None:
... ...
def load(self, im): def load(self, im: ImageFile.StubImageFile) -> Image.Image:
... ...
return image return image
def save(self, im, fp, filename): def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
... ...

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -37,6 +37,9 @@ example, lets display the image we just loaded::
>>> im.show() >>> im.show()
.. image:: show_hopper.webp
:align: center
.. note:: .. note::
The standard version of :py:meth:`~PIL.Image.Image.show` is not very The standard version of :py:meth:`~PIL.Image.Image.show` is not very
@ -79,6 +82,9 @@ Convert files to JPEG
except OSError: except OSError:
print("cannot convert", infile) print("cannot convert", infile)
.. image:: ../../Tests/images/hopper.jpg
:align: center
A second argument can be supplied to the :py:meth:`~PIL.Image.Image.save` A second argument can be supplied to the :py:meth:`~PIL.Image.Image.save`
method which explicitly specifies a file format. If you use a non-standard method which explicitly specifies a file format. If you use a non-standard
extension, you must always specify the format this way: extension, you must always specify the format this way:
@ -103,6 +109,9 @@ Create JPEG thumbnails
except OSError: except OSError:
print("cannot create thumbnail for", infile) print("cannot create thumbnail for", infile)
.. image:: thumbnail_hopper.jpg
:align: center
It is important to note that the library doesnt decode or load the raster data It is important to note that the library doesnt decode or load the raster data
unless it really has to. When you open a file, the file header is read to unless it really has to. When you open a file, the file header is read to
determine the file format and extract things like mode, size, and other determine the file format and extract things like mode, size, and other
@ -140,16 +149,19 @@ Copying a subrectangle from an image
:: ::
box = (100, 100, 400, 400) box = (0, 0, 64, 64)
region = im.crop(box) region = im.crop(box)
The region is defined by a 4-tuple, where coordinates are (left, upper, right, The region is defined by a 4-tuple, where coordinates are (left, upper, right,
lower). The Python Imaging Library uses a coordinate system with (0, 0) in the lower). The Python Imaging Library uses a coordinate system with (0, 0) in the
upper left corner. Also note that coordinates refer to positions between the upper left corner. Also note that coordinates refer to positions between the
pixels, so the region in the above example is exactly 300x300 pixels. pixels, so the region in the above example is exactly 64x64 pixels.
The region could now be processed in a certain manner and pasted back. The region could now be processed in a certain manner and pasted back.
.. image:: cropped_hopper.webp
:align: center
Processing a subrectangle, and pasting it back Processing a subrectangle, and pasting it back
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -164,6 +176,9 @@ modes of the original image and the region do not need to match. If they dont
the region is automatically converted before being pasted (see the section on the region is automatically converted before being pasted (see the section on
:ref:`color-transforms` below for details). :ref:`color-transforms` below for details).
.. image:: pasted_hopper.webp
:align: center
Heres an additional example: Heres an additional example:
Rolling an image Rolling an image
@ -171,7 +186,7 @@ Rolling an image
:: ::
def roll(im, delta): def roll(im: Image.Image, delta: int) -> Image.Image:
"""Roll an image sideways.""" """Roll an image sideways."""
xsize, ysize = im.size xsize, ysize = im.size
@ -186,6 +201,9 @@ Rolling an image
return im return im
.. image:: rolled_hopper.webp
:align: center
Or if you would like to merge two images into a wider image: Or if you would like to merge two images into a wider image:
Merging images Merging images
@ -193,7 +211,7 @@ Merging images
:: ::
def merge(im1, im2): def merge(im1: Image.Image, im2: Image.Image) -> Image.Image:
w = im1.size[0] + im2.size[0] w = im1.size[0] + im2.size[0]
h = max(im1.size[1], im2.size[1]) h = max(im1.size[1], im2.size[1])
im = Image.new("RGBA", (w, h)) im = Image.new("RGBA", (w, h))
@ -203,6 +221,9 @@ Merging images
return im return im
.. image:: merged_hopper.webp
:align: center
For more advanced tricks, the paste method can also take a transparency mask as For more advanced tricks, the paste method can also take a transparency mask as
an optional argument. In this mask, the value 255 indicates that the pasted an optional argument. In this mask, the value 255 indicates that the pasted
image is opaque in that position (that is, the pasted image should be used as image is opaque in that position (that is, the pasted image should be used as
@ -229,6 +250,9 @@ Note that for a single-band image, :py:meth:`~PIL.Image.Image.split` returns
the image itself. To work with individual color bands, you may want to convert the image itself. To work with individual color bands, you may want to convert
the image to “RGB” first. the image to “RGB” first.
.. image:: rebanded_hopper.webp
:align: center
Geometrical transforms Geometrical transforms
---------------------- ----------------------
@ -245,6 +269,9 @@ Simple geometry transforms
out = im.resize((128, 128)) out = im.resize((128, 128))
out = im.rotate(45) # degrees counter-clockwise out = im.rotate(45) # degrees counter-clockwise
.. image:: rotated_hopper_90.webp
:align: center
To rotate the image in 90 degree steps, you can either use the To rotate the image in 90 degree steps, you can either use the
:py:meth:`~PIL.Image.Image.rotate` method or the :py:meth:`~PIL.Image.Image.rotate` method or the
:py:meth:`~PIL.Image.Image.transpose` method. The latter can also be used to :py:meth:`~PIL.Image.Image.transpose` method. The latter can also be used to
@ -256,11 +283,38 @@ Transposing an image
:: ::
out = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) out = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
.. image:: flip_left_right_hopper.webp
:align: center
::
out = im.transpose(Image.Transpose.FLIP_TOP_BOTTOM) out = im.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
.. image:: flip_top_bottom_hopper.webp
:align: center
::
out = im.transpose(Image.Transpose.ROTATE_90) out = im.transpose(Image.Transpose.ROTATE_90)
.. image:: rotated_hopper_90.webp
:align: center
::
out = im.transpose(Image.Transpose.ROTATE_180) out = im.transpose(Image.Transpose.ROTATE_180)
.. image:: rotated_hopper_180.webp
:align: center
::
out = im.transpose(Image.Transpose.ROTATE_270) out = im.transpose(Image.Transpose.ROTATE_270)
.. image:: rotated_hopper_270.webp
:align: center
``transpose(ROTATE)`` operations can also be performed identically with ``transpose(ROTATE)`` operations can also be performed identically with
:py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is :py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is
true, to provide for the same changes to the image's size. true, to provide for the same changes to the image's size.
@ -278,7 +332,7 @@ choose to resize relative to a given size.
from PIL import Image, ImageOps from PIL import Image, ImageOps
size = (100, 150) size = (100, 150)
with Image.open("Tests/images/hopper.webp") as im: with Image.open("hopper.webp") as im:
ImageOps.contain(im, size).save("imageops_contain.webp") ImageOps.contain(im, size).save("imageops_contain.webp")
ImageOps.cover(im, size).save("imageops_cover.webp") ImageOps.cover(im, size).save("imageops_cover.webp")
ImageOps.fit(im, size).save("imageops_fit.webp") ImageOps.fit(im, size).save("imageops_fit.webp")
@ -342,6 +396,9 @@ Applying filters
from PIL import ImageFilter from PIL import ImageFilter
out = im.filter(ImageFilter.DETAIL) out = im.filter(ImageFilter.DETAIL)
.. image:: enhanced_hopper.webp
:align: center
Point Operations Point Operations
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
@ -355,8 +412,11 @@ Applying point transforms
:: ::
# multiply each pixel by 1.2 # multiply each pixel by 20
out = im.point(lambda i: i * 1.2) out = im.point(lambda i: i * 20)
.. image:: transformed_hopper.webp
:align: center
Using the above technique, you can quickly apply any simple expression to an Using the above technique, you can quickly apply any simple expression to an
image. You can also combine the :py:meth:`~PIL.Image.Image.point` and image. You can also combine the :py:meth:`~PIL.Image.Image.point` and
@ -388,6 +448,9 @@ Note the syntax used to create the mask::
imout = im.point(lambda i: expression and 255) imout = im.point(lambda i: expression and 255)
.. image:: masked_hopper.webp
:align: center
Python only evaluates the portion of a logical expression as is necessary to Python only evaluates the portion of a logical expression as is necessary to
determine the outcome, and returns the last value examined as the result of the determine the outcome, and returns the last value examined as the result of the
expression. So if the expression above is false (0), Python does not look at expression. So if the expression above is false (0), Python does not look at
@ -412,6 +475,10 @@ Enhancing images
enh = ImageEnhance.Contrast(im) enh = ImageEnhance.Contrast(im)
enh.enhance(1.3).show("30% more contrast") enh.enhance(1.3).show("30% more contrast")
.. image:: contrasted_hopper.jpg
:align: center
Image sequences Image sequences
--------------- ---------------
@ -444,10 +511,43 @@ Reading sequences
As seen in this example, youll get an :py:exc:`EOFError` exception when the As seen in this example, youll get an :py:exc:`EOFError` exception when the
sequence ends. sequence ends.
Writing sequences
^^^^^^^^^^^^^^^^^
You can create animated GIFs with Pillow, e.g.
::
from PIL import Image
# List of image filenames
image_filenames = [
"hopper.jpg",
"rotated_hopper_270.jpg",
"rotated_hopper_180.jpg",
"rotated_hopper_90.jpg",
]
# Open images and create a list
images = [Image.open(filename) for filename in image_filenames]
# Save the images as an animated GIF
images[0].save(
"animated_hopper.gif",
save_all=True,
append_images=images[1:],
duration=500, # duration of each frame in milliseconds
loop=0, # loop forever
)
.. image:: animated_hopper.gif
:align: center
The following class lets you use the for-statement to loop over the sequence: The following class lets you use the for-statement to loop over the sequence:
Using the ImageSequence Iterator class Using the :py:class:`~PIL.ImageSequence.Iterator` class
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:: ::
@ -467,25 +567,61 @@ Drawing PostScript
:: ::
from PIL import Image from PIL import Image, PSDraw
from PIL import PSDraw import os
with Image.open("hopper.ppm") as im: # Define the PostScript file
title = "hopper" ps_file = open("hopper.ps", "wb")
box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points
ps = PSDraw.PSDraw() # default is sys.stdout or sys.stdout.buffer # Create a PSDraw object
ps.begin_document(title) ps = PSDraw.PSDraw(ps_file)
# draw the image (75 dpi) # Start the document
ps.image(box, im, 75) ps.begin_document()
ps.rectangle(box)
# draw title # Set the text to be drawn
ps.setfont("HelveticaNarrow-Bold", 36) text = "Hopper"
ps.text((3 * 72, 4 * 72), title)
ps.end_document() # Define the PostScript font
font_name = "Helvetica-Narrow-Bold"
font_size = 36
# Calculate text size (approximation as PSDraw doesn't provide direct method)
# Assuming average character width as 0.6 of the font size
text_width = len(text) * font_size * 0.6
text_height = font_size
# Set the position (top-center)
page_width, page_height = 595, 842 # A4 size in points
text_x = (page_width - text_width) // 2
text_y = page_height - text_height - 50 # Distance from the top of the page
# Load the image
image_path = "hopper.ppm" # Update this with your image path
with Image.open(image_path) as im:
# Resize the image if it's too large
im.thumbnail((page_width - 100, page_height // 2))
# Define the box where the image will be placed
img_x = (page_width - im.width) // 2
img_y = text_y + text_height - 200 # 200 points below the text
# Draw the image (75 dpi)
ps.image((img_x, img_y, img_x + im.width, img_y + im.height), im, 75)
# Draw the text
ps.setfont(font_name, font_size)
ps.text((text_x, text_y), text)
# End the document
ps.end_document()
ps_file.close()
.. image:: hopper_ps.webp
.. note::
PostScript converted to PDF for display purposes
More on reading images More on reading images
---------------------- ----------------------
@ -553,7 +689,7 @@ Reading from a tar archive
from PIL import Image, TarIO from PIL import Image, TarIO
fp = TarIO.TarIO("Tests/images/hopper.tar", "hopper.jpg") fp = TarIO.TarIO("hopper.tar", "hopper.jpg")
im = Image.open(fp) im = Image.open(fp)
@ -568,8 +704,7 @@ in the current directory can be saved as JPEGs at reduced quality.
import glob import glob
from PIL import Image from PIL import Image
def compress_image(source_path: str, dest_path: str) -> None:
def compress_image(source_path, dest_path):
with Image.open(source_path) as img: with Image.open(source_path) as img:
if img.mode != "RGB": if img.mode != "RGB":
img = img.convert("RGB") img = img.convert("RGB")

View File

@ -53,7 +53,7 @@ true color.
from PIL import Image, ImageFile from PIL import Image, ImageFile
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"SPAM" return prefix[:4] == b"SPAM"
@ -62,7 +62,7 @@ true color.
format = "SPAM" format = "SPAM"
format_description = "Spam raster image" format_description = "Spam raster image"
def _open(self): def _open(self) -> None:
header = self.fp.read(128).split() header = self.fp.read(128).split()
@ -82,7 +82,7 @@ true color.
raise SyntaxError(msg) raise SyntaxError(msg)
# data descriptor # data descriptor
self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))]
Image.register_open(SpamImageFile.format, SpamImageFile, _accept) Image.register_open(SpamImageFile.format, SpamImageFile, _accept)

Some files were not shown because too many files have changed in this diff Show More