Merge branch 'main' into exif_get_ifd

This commit is contained in:
Andrew Murray 2024-08-25 00:05:02 +10:00 committed by GitHub
commit e3ffa380ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
192 changed files with 4621 additions and 2249 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

View File

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

View File

@ -1 +1,11 @@
mypy==1.10.1 mypy==1.11.1
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython
numpy
packaging
pytest
sphinx
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

@ -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,6 +41,7 @@ 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:
@ -88,6 +97,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:
@ -129,6 +139,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
@ -140,6 +151,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:
@ -201,6 +213,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
@ -225,6 +238,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
@ -243,8 +257,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.0
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.1
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

@ -5,6 +5,45 @@ Changelog (Pillow)
11.0.0 (unreleased) 11.0.0 (unreleased)
------------------- -------------------
- 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 - Drop support for Python 3.8 #8183
[hugovk, radarhere] [hugovk, radarhere]

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

View File

@ -105,91 +105,68 @@ 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(
"lut_mode, table_channels, table_size",
[
("RGB", 3, 3),
("CMYK", 4, 3),
("RGB", 3, (2, 3, 3)),
("RGB", 3, (65, 3, 3)),
("RGB", 3, (3, 65, 3)),
("RGB", 3, (2, 3, 65)),
],
)
def test_correct_args(
self, lut_mode: str, table_channels: int, table_size: int | tuple[int, int, int]
) -> None:
im = Image.new("RGB", (10, 10), 0) im = Image.new("RGB", (10, 10), 0)
assert im.im is not None
im.im.color_lut_3d( im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) lut_mode,
)
im.im.color_lut_3d(
"CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR, Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (2, 3, 3)), *self.generate_identity_table(table_channels, table_size),
) )
@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)
assert im.im is not None
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)
assert im.im is not None
im.im.color_lut_3d( im.im.color_lut_3d(
"RGB", lut_mode,
Image.Resampling.BILINEAR, Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (65, 3, 3)), *self.generate_identity_table(table_channels, table_size),
)
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.im.color_lut_3d(
"HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
) )
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

@ -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)

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:
@ -1019,13 +1019,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 +1048,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

@ -233,7 +233,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:

View File

@ -240,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))

View File

@ -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

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

@ -373,7 +373,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

@ -684,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

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

@ -15,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:

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"
@ -825,7 +830,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()
@ -947,7 +951,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))

View File

@ -230,7 +230,7 @@ class TestImagePutPixelError:
im.putpixel((0, 0), v) # type: ignore[arg-type] 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"),

View File

@ -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)

View File

@ -398,7 +398,8 @@ def test_logical() -> None:
for y in (a, b): for y in (a, b):
imy = Image.new("1", (1, 1), y) imy = Image.new("1", (1, 1), y)
value = op(imx, imy).getpixel((0, 0)) value = op(imx, imy).getpixel((0, 0))
assert not isinstance(value, tuple) and value is not None assert not isinstance(value, tuple)
assert value is not None
out.append(value) out.append(value)
return out return out

View File

@ -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",
[ [

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

@ -94,7 +94,6 @@ class TestImageFile:
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:
@ -127,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:
@ -305,7 +304,7 @@ class TestPyDecoder(CodecsTest):
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(b"") decoder.decode(b"")
@ -318,7 +317,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
@ -334,7 +339,7 @@ class TestPyEncoder(CodecsTest):
im.tile = [("MOCK", None, 32, None)] im.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
@ -351,7 +356,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
@ -359,7 +366,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:
@ -372,18 +381,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)
@ -395,9 +412,8 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode_to_pyfd() encoder.encode_to_pyfd()
fh = BytesIO()
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode_to_file(fh, 0) 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)

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

@ -46,7 +46,8 @@ def img_to_string(im: Image.Image) -> str:
line = "" line = ""
for c in range(im.width): for c in range(im.width):
value = im.getpixel((c, r)) value = im.getpixel((c, r))
assert not isinstance(value, tuple) and value is not None assert not isinstance(value, tuple)
assert value is not None
line += chars[value > 0] line += chars[value > 0]
result.append(line) result.append(line)
return "\n".join(result) return "\n".join(result)

View File

@ -390,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:

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

@ -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

@ -54,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

@ -109,6 +109,33 @@ 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
---------------- ----------------

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,20 @@ 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 = [(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) -> 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 +268,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) -> 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 +282,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)

View File

@ -55,10 +55,6 @@ Many of Pillow's features require external libraries:
* **libwebp** provides the WebP format. * **libwebp** provides the WebP format.
* Pillow has been tested with version **0.1.3**, which does not read
transparent WebP files. Versions **0.3.0** and above support
transparency.
* **openjpeg** provides JPEG 2000 functionality. * **openjpeg** provides JPEG 2000 functionality.
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
@ -275,18 +271,18 @@ Build Options
* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``,
``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``,
``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``, ``-C lcms=disable``, ``-C webp=disable``,
``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``.
Disable building the corresponding feature even if the development Disable building the corresponding feature even if the development
libraries are present on the building machine. libraries are present on the building machine.
* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, * Config settings: ``-C zlib=enable``, ``-C jpeg=enable``,
``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``,
``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, ``-C lcms=enable``, ``-C webp=enable``,
``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``.
Require that the corresponding feature is built. The build will raise Require that the corresponding feature is built. The build will raise
an exception if the libraries are not found. Webpmux (WebP metadata) an exception if the libraries are not found. Tcl and Tk must be used
relies on WebP support. Tcl and Tk also must be used together. together.
* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. * Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``.
These flags are used to compile a modified version of libraqm and These flags are used to compile a modified version of libraqm and

View File

@ -362,6 +362,7 @@ Classes
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
.. autoclass:: PIL.Image.ImagePointHandler .. autoclass:: PIL.Image.ImagePointHandler
.. autoclass:: PIL.Image.ImagePointTransform
.. autoclass:: PIL.Image.ImageTransformHandler .. autoclass:: PIL.Image.ImageTransformHandler
Protocols Protocols

View File

@ -37,6 +37,11 @@ Example: Parse an image
Classes Classes
------- -------
.. autoclass:: PIL.ImageFile._Tile()
:member-order: bysource
:members:
:show-inheritance:
.. autoclass:: PIL.ImageFile.Parser() .. autoclass:: PIL.ImageFile.Parser()
:members: :members:

View File

@ -91,3 +91,11 @@ Constants
Set to 1,000,000, to protect against potential DOS attacks. Pillow will Set to 1,000,000, to protect against potential DOS attacks. Pillow will
raise a :py:exc:`ValueError` if the number of characters is over this limit. The raise a :py:exc:`ValueError` if the number of characters is over this limit. The
check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.
Dictionaries
------------
.. autoclass:: Axis
:members:
:undoc-members:
:show-inheritance:

View File

@ -31,20 +31,21 @@ Example: Using the :py:mod:`~PIL.ImageMath` module
b=im2 b=im2
) )
.. py:function:: lambda_eval(expression, options) .. py:function:: lambda_eval(expression, options, **kw)
Returns the result of an image function. Returns the result of an image function.
:param expression: A function that receives a dictionary. :param expression: A function that receives a dictionary.
:param options: Values to add to the function's dictionary, mapping image :param options: Values to add to the function's dictionary. Note that the names
names to Image instances. You can use one or more keyword must be valid Python identifiers. Deprecated.
arguments instead of a dictionary, as shown in the above You can instead use one or more keyword arguments, as
example. Note that the names must be valid Python shown in the above example.
identifiers. :param \**kw: Values to add to the function's dictionary, mapping image names to
Image instances.
:return: An image, an integer value, a floating point value, :return: An image, an integer value, a floating point value,
or a pixel tuple, depending on the expression. or a pixel tuple, depending on the expression.
.. py:function:: unsafe_eval(expression, options) .. py:function:: unsafe_eval(expression, options, **kw)
Evaluates an image expression. Evaluates an image expression.
@ -61,11 +62,12 @@ Example: Using the :py:mod:`~PIL.ImageMath` module
:param expression: A string which uses the standard Python expression :param expression: A string which uses the standard Python expression
syntax. In addition to the standard operators, you can syntax. In addition to the standard operators, you can
also use the functions described below. also use the functions described below.
:param options: Values to add to the function's dictionary, mapping image :param options: Values to add to the evaluation context. Note that the names must
names to Image instances. You can use one or more keyword be valid Python identifiers. Deprecated.
arguments instead of a dictionary, as shown in the above You can instead use one or more keyword arguments, as
example. Note that the names must be valid Python shown in the above example.
identifiers. :param \**kw: Values to add to the evaluation context, mapping image names to Image
instances.
:return: An image, an integer value, a floating point value, :return: An image, an integer value, a floating point value,
or a pixel tuple, depending on the expression. or a pixel tuple, depending on the expression.

View File

@ -54,12 +54,12 @@ Feature version numbers are available only where stated.
Support for the following features can be checked: Support for the following features can be checked:
* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available.
* ``transp_webp``: Support for transparency in WebP images.
* ``webp_mux``: (compile time) Support for EXIF data in WebP images.
* ``webp_anim``: (compile time) Support for animated WebP images.
* ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer.
* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available.
* ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library.
* ``transp_webp``: Deprecated. Always ``True`` if WebP module is installed.
* ``webp_mux``: Deprecated. Always ``True`` if WebP module is installed.
* ``webp_anim``: Deprecated. Always ``True`` if WebP module is installed.
.. autofunction:: PIL.features.check_feature .. autofunction:: PIL.features.check_feature
.. autofunction:: PIL.features.version_feature .. autofunction:: PIL.features.version_feature

View File

@ -78,3 +78,7 @@ on some Python versions.
An internal interface module previously known as :mod:`~PIL._imaging`, An internal interface module previously known as :mod:`~PIL._imaging`,
implemented in :file:`_imaging.c`. implemented in :file:`_imaging.c`.
.. py:class:: ImagingCore
A representation of the image data.

View File

@ -185,6 +185,14 @@ Plugin reference
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:mod:`~PIL.MpoImagePlugin` Module
----------------------------------
.. automodule:: PIL.MpoImagePlugin
:members:
:undoc-members:
:show-inheritance:
:mod:`~PIL.MspImagePlugin` Module :mod:`~PIL.MspImagePlugin` Module
--------------------------------- ---------------------------------

View File

@ -43,10 +43,28 @@ similarly removed.
Deprecations Deprecations
============ ============
TODO ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO 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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``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).
API Changes API Changes
=========== ===========
@ -75,3 +93,10 @@ others prepare for 3.13, and to ensure Pillow could be used immediately at the r
of 3.13.0 final (2024-10-01, :pep:`719`). of 3.13.0 final (2024-10-01, :pep:`719`).
Pillow 11.0.0 now officially supports Python 3.13. Pillow 11.0.0 now officially supports Python 3.13.
C-level Flags
^^^^^^^^^^^^^
Some compiling flags like ``WITH_THREADING``, ``WITH_IMAGECHOPS``, and other
``WITH_*`` were removed. These flags were not available through the build system,
but they could be edited in the C source.

View File

@ -109,6 +109,7 @@ lint.select = [
"ISC", # flake8-implicit-str-concat "ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging "LOG", # flake8-logging
"PGH", # pygrep-hooks "PGH", # pygrep-hooks
"PT", # flake8-pytest-style
"PYI", # flake8-pyi "PYI", # flake8-pyi
"RUF100", # unused noqa (yesqa) "RUF100", # unused noqa (yesqa)
"UP", # pyupgrade "UP", # pyupgrade
@ -120,6 +121,12 @@ lint.ignore = [
"E221", # Multiple spaces before operator "E221", # Multiple spaces before operator
"E226", # Missing whitespace around arithmetic operator "E226", # Missing whitespace around arithmetic operator
"E241", # Multiple spaces after ',' "E241", # Multiple spaces after ','
"PT001", # pytest-fixture-incorrect-parentheses-style
"PT007", # pytest-parametrize-values-wrong-type
"PT011", # pytest-raises-too-broad
"PT012", # pytest-raises-with-multiple-statements
"PT016", # pytest-fail-without-message
"PT017", # pytest-assert-in-except
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
"PYI034", # flake8-pyi: typing.Self added in Python 3.11 "PYI034", # flake8-pyi: typing.Self added in Python 3.11
] ]
@ -129,6 +136,7 @@ lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [
lint.per-file-ignores."Tests/oss-fuzz/fuzz_pillow.py" = [ lint.per-file-ignores."Tests/oss-fuzz/fuzz_pillow.py" = [
"I002", "I002",
] ]
lint.flake8-pytest-style.parametrize-names-type = "csv"
lint.isort.known-first-party = [ lint.isort.known-first-party = [
"PIL", "PIL",
] ]
@ -159,7 +167,4 @@ exclude = [
'^Tests/oss-fuzz/fuzz_font.py$', '^Tests/oss-fuzz/fuzz_font.py$',
'^Tests/oss-fuzz/fuzz_pillow.py$', '^Tests/oss-fuzz/fuzz_pillow.py$',
'^Tests/test_qt_image_qapplication.py$', '^Tests/test_qt_image_qapplication.py$',
'^Tests/test_font_pcf_charsets.py$',
'^Tests/test_font_pcf.py$',
'^Tests/test_file_tar.py$',
] ]

View File

@ -295,7 +295,6 @@ class pil_build_ext(build_ext):
"raqm", "raqm",
"lcms", "lcms",
"webp", "webp",
"webpmux",
"jpeg2000", "jpeg2000",
"imagequant", "imagequant",
"xcb", "xcb",
@ -794,28 +793,18 @@ class pil_build_ext(build_ext):
if feature.want("webp"): if feature.want("webp"):
_dbg("Looking for webp") _dbg("Looking for webp")
if _find_include_file(self, "webp/encode.h") and _find_include_file( if all(
self, "webp/decode.h" _find_include_file(self, "webp/" + include)
for include in ("encode.h", "decode.h", "mux.h", "demux.h")
): ):
# In Google's precompiled zip it is call "libwebp": # In Google's precompiled zip it is called "libwebp"
if _find_library_file(self, "webp"): for prefix in ("", "lib"):
feature.webp = "webp" if all(
elif _find_library_file(self, "libwebp"): _find_library_file(self, prefix + library)
feature.webp = "libwebp" for library in ("webp", "webpmux", "webpdemux")
):
if feature.want("webpmux"): feature.webp = prefix + "webp"
_dbg("Looking for webpmux") break
if _find_include_file(self, "webp/mux.h") and _find_include_file(
self, "webp/demux.h"
):
if _find_library_file(self, "webpmux") and _find_library_file(
self, "webpdemux"
):
feature.webpmux = "webpmux"
if _find_library_file(self, "libwebpmux") and _find_library_file(
self, "libwebpdemux"
):
feature.webpmux = "libwebpmux"
if feature.want("xcb"): if feature.want("xcb"):
_dbg("Looking for xcb") _dbg("Looking for xcb")
@ -904,15 +893,8 @@ class pil_build_ext(build_ext):
self._remove_extension("PIL._imagingcms") self._remove_extension("PIL._imagingcms")
if feature.webp: if feature.webp:
libs = [feature.webp] libs = [feature.webp, feature.webp + "mux", feature.webp + "demux"]
defs = [] self._update_extension("PIL._webp", libs)
if feature.webpmux:
defs.append(("HAVE_WEBPMUX", None))
libs.append(feature.webpmux)
libs.append(feature.webpmux.replace("pmux", "pdemux"))
self._update_extension("PIL._webp", libs, defs)
else: else:
self._remove_extension("PIL._webp") self._remove_extension("PIL._webp")
@ -953,7 +935,6 @@ class pil_build_ext(build_ext):
(feature.raqm, "RAQM (Text shaping)", raqm_extra_info), (feature.raqm, "RAQM (Text shaping)", raqm_extra_info),
(feature.lcms, "LITTLECMS2"), (feature.lcms, "LITTLECMS2"),
(feature.webp, "WEBP"), (feature.webp, "WEBP"),
(feature.webpmux, "WEBPMUX"),
(feature.xcb, "XCB (X protocol)"), (feature.xcb, "XCB (X protocol)"),
] ]

View File

@ -477,7 +477,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.write(struct.pack("<i", 5)) fp.write(struct.pack("<i", 5))
fp.write(struct.pack("<i", 0)) fp.write(struct.pack("<i", 0))
ImageFile._save(im, fp, [("BLP", (0, 0) + im.size, 0, im.mode)]) ImageFile._save(im, fp, [ImageFile._Tile("BLP", (0, 0) + im.size, 0, im.mode)])
Image.register_open(BlpImageFile.format, BlpImageFile, _accept) Image.register_open(BlpImageFile.format, BlpImageFile, _accept)

View File

@ -482,7 +482,9 @@ def _save(
if palette: if palette:
fp.write(palette) fp.write(palette)
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]) ImageFile._save(
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]
)
# #

View File

@ -16,10 +16,11 @@
from __future__ import annotations from __future__ import annotations
import io import io
from typing import IO, AnyStr, Generic, Literal from collections.abc import Iterable
from typing import IO, AnyStr, NoReturn
class ContainerIO(Generic[AnyStr]): class ContainerIO(IO[AnyStr]):
""" """
A file object that provides read access to a part of an existing A file object that provides read access to a part of an existing
file (for example a TAR file). file (for example a TAR file).
@ -45,7 +46,10 @@ class ContainerIO(Generic[AnyStr]):
def isatty(self) -> bool: def isatty(self) -> bool:
return False return False
def seek(self, offset: int, mode: Literal[0, 1, 2] = io.SEEK_SET) -> None: def seekable(self) -> bool:
return True
def seek(self, offset: int, mode: int = io.SEEK_SET) -> int:
""" """
Move file pointer. Move file pointer.
@ -53,6 +57,7 @@ class ContainerIO(Generic[AnyStr]):
:param mode: Starting position. Use 0 for beginning of region, 1 :param mode: Starting position. Use 0 for beginning of region, 1
for current offset, and 2 for end of region. You cannot move for current offset, and 2 for end of region. You cannot move
the pointer outside the defined region. the pointer outside the defined region.
:returns: Offset from start of region, in bytes.
""" """
if mode == 1: if mode == 1:
self.pos = self.pos + offset self.pos = self.pos + offset
@ -63,6 +68,7 @@ class ContainerIO(Generic[AnyStr]):
# clamp # clamp
self.pos = max(0, min(self.pos, self.length)) self.pos = max(0, min(self.pos, self.length))
self.fh.seek(self.offset + self.pos) self.fh.seek(self.offset + self.pos)
return self.pos
def tell(self) -> int: def tell(self) -> int:
""" """
@ -72,27 +78,32 @@ class ContainerIO(Generic[AnyStr]):
""" """
return self.pos return self.pos
def read(self, n: int = 0) -> AnyStr: def readable(self) -> bool:
return True
def read(self, n: int = -1) -> AnyStr:
""" """
Read data. Read data.
:param n: Number of bytes to read. If omitted or zero, :param n: Number of bytes to read. If omitted, zero or negative,
read until end of region. read until end of region.
:returns: An 8-bit string. :returns: An 8-bit string.
""" """
if n: if n > 0:
n = min(n, self.length - self.pos) n = min(n, self.length - self.pos)
else: else:
n = self.length - self.pos n = self.length - self.pos
if not n: # EOF if n <= 0: # EOF
return b"" if "b" in self.fh.mode else "" # type: ignore[return-value] return b"" if "b" in self.fh.mode else "" # type: ignore[return-value]
self.pos = self.pos + n self.pos = self.pos + n
return self.fh.read(n) return self.fh.read(n)
def readline(self) -> AnyStr: def readline(self, n: int = -1) -> AnyStr:
""" """
Read a line of text. Read a line of text.
:param n: Number of bytes to read. If omitted, zero or negative,
read until end of line.
:returns: An 8-bit string. :returns: An 8-bit string.
""" """
s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment] s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment]
@ -102,14 +113,16 @@ class ContainerIO(Generic[AnyStr]):
if not c: if not c:
break break
s = s + c s = s + c
if c == newline_character: if c == newline_character or len(s) == n:
break break
return s return s
def readlines(self) -> list[AnyStr]: def readlines(self, n: int | None = -1) -> list[AnyStr]:
""" """
Read multiple lines of text. Read multiple lines of text.
:param n: Number of lines to read. If omitted, zero, negative or None,
read until end of region.
:returns: A list of 8-bit strings. :returns: A list of 8-bit strings.
""" """
lines = [] lines = []
@ -118,4 +131,43 @@ class ContainerIO(Generic[AnyStr]):
if not s: if not s:
break break
lines.append(s) lines.append(s)
if len(lines) == n:
break
return lines return lines
def writable(self) -> bool:
return False
def write(self, b: AnyStr) -> NoReturn:
raise NotImplementedError()
def writelines(self, lines: Iterable[AnyStr]) -> NoReturn:
raise NotImplementedError()
def truncate(self, size: int | None = None) -> int:
raise NotImplementedError()
def __enter__(self) -> ContainerIO[AnyStr]:
return self
def __exit__(self, *args: object) -> None:
self.close()
def __iter__(self) -> ContainerIO[AnyStr]:
return self
def __next__(self) -> AnyStr:
line = self.readline()
if not line:
msg = "end of region"
raise StopIteration(msg)
return line
def fileno(self) -> int:
return self.fh.fileno()
def flush(self) -> None:
self.fh.flush()
def close(self) -> None:
self.fh.close()

View File

@ -65,16 +65,24 @@ def has_ghostscript() -> bool:
return gs_binary is not False return gs_binary is not False
def Ghostscript(tile, size, fp, scale=1, transparency=False): def Ghostscript(
tile: list[ImageFile._Tile],
size: tuple[int, int],
fp: IO[bytes],
scale: int = 1,
transparency: bool = False,
) -> Image.Image:
"""Render an image using Ghostscript""" """Render an image using Ghostscript"""
global gs_binary global gs_binary
if not has_ghostscript(): if not has_ghostscript():
msg = "Unable to locate Ghostscript on paths" msg = "Unable to locate Ghostscript on paths"
raise OSError(msg) raise OSError(msg)
assert isinstance(gs_binary, str)
# Unpack decoder tile # Unpack decoder tile
decoder, tile, offset, data = tile[0] args = tile[0].args
length, bbox = data assert isinstance(args, tuple)
length, bbox = args
# Hack to support hi-res rendering # Hack to support hi-res rendering
scale = int(scale) or 1 scale = int(scale) or 1
@ -227,7 +235,11 @@ class EpsImageFile(ImageFile.ImageFile):
# put floating point values there anyway. # put floating point values there anyway.
box = [int(float(i)) for i in v.split()] box = [int(float(i)) for i in v.split()]
self._size = box[2] - box[0], box[3] - box[1] self._size = box[2] - box[0], box[3] - box[1]
self.tile = [("eps", (0, 0) + self.size, offset, (length, box))] self.tile = [
ImageFile._Tile(
"eps", (0, 0) + self.size, offset, (length, box)
)
]
except Exception: except Exception:
pass pass
return True return True
@ -422,7 +434,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -
if hasattr(fp, "flush"): if hasattr(fp, "flush"):
fp.flush() fp.flush()
ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)]) ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0, None)])
fp.write(b"\n%%%%EndBinary\n") fp.write(b"\n%%%%EndBinary\n")
fp.write(b"grestore end\n") fp.write(b"grestore end\n")

View File

@ -591,7 +591,9 @@ def _write_single_frame(
_write_local_header(fp, im, (0, 0), flags) _write_local_header(fp, im, (0, 0), flags)
im_out.encoderconfig = (8, get_interlace(im)) im_out.encoderconfig = (8, get_interlace(im))
ImageFile._save(im_out, fp, [("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]) ImageFile._save(
im_out, fp, [ImageFile._Tile("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]
)
fp.write(b"\0") # end of image data fp.write(b"\0") # end of image data
@ -1054,7 +1056,9 @@ def _write_frame_data(
_write_local_header(fp, im_frame, offset, 0) _write_local_header(fp, im_frame, offset, 0)
ImageFile._save( ImageFile._save(
im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])] im_frame,
fp,
[ImageFile._Tile("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])],
) )
fp.write(b"\0") # end of image data fp.write(b"\0") # end of image data

View File

@ -25,7 +25,7 @@ from __future__ import annotations
import warnings import warnings
from io import BytesIO from io import BytesIO
from math import ceil, log from math import ceil, log
from typing import IO from typing import IO, NamedTuple
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
from ._binary import i16le as i16 from ._binary import i16le as i16
@ -97,7 +97,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if bits != 32: if bits != 32:
and_mask = Image.new("1", size) and_mask = Image.new("1", size)
ImageFile._save( ImageFile._save(
and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))] and_mask,
image_io,
[ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))],
) )
else: else:
frame.save(image_io, "png") frame.save(image_io, "png")
@ -119,8 +121,22 @@ def _accept(prefix: bytes) -> bool:
return prefix[:4] == _MAGIC return prefix[:4] == _MAGIC
class IconHeader(NamedTuple):
width: int
height: int
nb_color: int
reserved: int
planes: int
bpp: int
size: int
offset: int
dim: tuple[int, int]
square: int
color_depth: int
class IcoFile: class IcoFile:
def __init__(self, buf) -> None: def __init__(self, buf: IO[bytes]) -> None:
""" """
Parse image from file-like object containing ico file data Parse image from file-like object containing ico file data
""" """
@ -141,51 +157,44 @@ class IcoFile:
for i in range(self.nb_items): for i in range(self.nb_items):
s = buf.read(16) s = buf.read(16)
icon_header = {
"width": s[0],
"height": s[1],
"nb_color": s[2], # No. of colors in image (0 if >=8bpp)
"reserved": s[3],
"planes": i16(s, 4),
"bpp": i16(s, 6),
"size": i32(s, 8),
"offset": i32(s, 12),
}
# See Wikipedia # See Wikipedia
for j in ("width", "height"): width = s[0] or 256
if not icon_header[j]: height = s[1] or 256
icon_header[j] = 256
# See Wikipedia notes about color depth. # No. of colors in image (0 if >=8bpp)
# We need this just to differ images with equal sizes nb_color = s[2]
icon_header["color_depth"] = ( bpp = i16(s, 6)
icon_header["bpp"] icon_header = IconHeader(
or ( width=width,
icon_header["nb_color"] != 0 height=height,
and ceil(log(icon_header["nb_color"], 2)) nb_color=nb_color,
) reserved=s[3],
or 256 planes=i16(s, 4),
bpp=i16(s, 6),
size=i32(s, 8),
offset=i32(s, 12),
dim=(width, height),
square=width * height,
# See Wikipedia notes about color depth.
# We need this just to differ images with equal sizes
color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256,
) )
icon_header["dim"] = (icon_header["width"], icon_header["height"])
icon_header["square"] = icon_header["width"] * icon_header["height"]
self.entry.append(icon_header) self.entry.append(icon_header)
self.entry = sorted(self.entry, key=lambda x: x["color_depth"]) self.entry = sorted(self.entry, key=lambda x: x.color_depth)
# ICO images are usually squares # ICO images are usually squares
self.entry = sorted(self.entry, key=lambda x: x["square"], reverse=True) self.entry = sorted(self.entry, key=lambda x: x.square, reverse=True)
def sizes(self) -> set[tuple[int, int]]: def sizes(self) -> set[tuple[int, int]]:
""" """
Get a list of all available icon sizes and color depths. Get a set of all available icon sizes and color depths.
""" """
return {(h["width"], h["height"]) for h in self.entry} return {(h.width, h.height) for h in self.entry}
def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int: def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int:
for i, h in enumerate(self.entry): for i, h in enumerate(self.entry):
if size == h["dim"] and (bpp is False or bpp == h["color_depth"]): if size == h.dim and (bpp is False or bpp == h.color_depth):
return i return i
return 0 return 0
@ -202,9 +211,9 @@ class IcoFile:
header = self.entry[idx] header = self.entry[idx]
self.buf.seek(header["offset"]) self.buf.seek(header.offset)
data = self.buf.read(8) data = self.buf.read(8)
self.buf.seek(header["offset"]) self.buf.seek(header.offset)
im: Image.Image im: Image.Image
if data[:8] == PngImagePlugin._MAGIC: if data[:8] == PngImagePlugin._MAGIC:
@ -222,8 +231,7 @@ class IcoFile:
im.tile[0] = d, (0, 0) + im.size, o, a im.tile[0] = d, (0, 0) + im.size, o, a
# figure out where AND mask image starts # figure out where AND mask image starts
bpp = header["bpp"] if header.bpp == 32:
if 32 == bpp:
# 32-bit color depth icon image allows semitransparent areas # 32-bit color depth icon image allows semitransparent areas
# PIL's DIB format ignores transparency bits, recover them. # PIL's DIB format ignores transparency bits, recover them.
# The DIB is packed in BGRX byte order where X is the alpha # The DIB is packed in BGRX byte order where X is the alpha
@ -253,7 +261,7 @@ class IcoFile:
# padded row size * height / bits per char # padded row size * height / bits per char
total_bytes = int((w * im.size[1]) / 8) total_bytes = int((w * im.size[1]) / 8)
and_mask_offset = header["offset"] + header["size"] - total_bytes and_mask_offset = header.offset + header.size - total_bytes
self.buf.seek(and_mask_offset) self.buf.seek(and_mask_offset)
mask_data = self.buf.read(total_bytes) mask_data = self.buf.read(total_bytes)
@ -307,15 +315,15 @@ class IcoImageFile(ImageFile.ImageFile):
def _open(self) -> None: def _open(self) -> None:
self.ico = IcoFile(self.fp) self.ico = IcoFile(self.fp)
self.info["sizes"] = self.ico.sizes() self.info["sizes"] = self.ico.sizes()
self.size = self.ico.entry[0]["dim"] self.size = self.ico.entry[0].dim
self.load() self.load()
@property @property
def size(self): def size(self) -> tuple[int, int]:
return self._size return self._size
@size.setter @size.setter
def size(self, value): def size(self, value: tuple[int, int]) -> None:
if value not in self.info["sizes"]: if value not in self.info["sizes"]:
msg = "This is not one of the allowed sizes of this image" msg = "This is not one of the allowed sizes of this image"
raise ValueError(msg) raise ValueError(msg)

View File

@ -360,7 +360,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
palette += im_palette[colors * i : colors * (i + 1)] palette += im_palette[colors * i : colors * (i + 1)]
palette += b"\x00" * (256 - colors) palette += b"\x00" * (256 - colors)
fp.write(palette) # 768 bytes fp.write(palette) # 768 bytes
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]) ImageFile._save(
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]
)
# #

View File

@ -38,7 +38,7 @@ import struct
import sys import sys
import tempfile import tempfile
import warnings import warnings
from collections.abc import Callable, MutableMapping, Sequence from collections.abc import Callable, Iterator, MutableMapping, Sequence
from enum import IntEnum from enum import IntEnum
from types import ModuleType from types import ModuleType
from typing import ( from typing import (
@ -218,7 +218,12 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# Registries # Registries
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ImageFile, ImagePalette import mmap
from xml.etree.ElementTree import Element
from IPython.lib.pretty import PrettyPrinter
from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
ID: list[str] = [] ID: list[str] = []
OPEN: dict[ OPEN: dict[
@ -241,9 +246,9 @@ ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {}
_ENDIAN = "<" if sys.byteorder == "little" else ">" _ENDIAN = "<" if sys.byteorder == "little" else ">"
def _conv_type_shape(im): def _conv_type_shape(im: Image) -> tuple[tuple[int, ...], str]:
m = ImageMode.getmode(im.mode) m = ImageMode.getmode(im.mode)
shape = (im.height, im.width) shape: tuple[int, ...] = (im.height, im.width)
extra = len(m.bands) extra = len(m.bands)
if extra != 1: if extra != 1:
shape += (extra,) shape += (extra,)
@ -465,43 +470,53 @@ def _getencoder(
# Simple expression analyzer # Simple expression analyzer
class _E: class ImagePointTransform:
def __init__(self, scale, offset) -> None: """
Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than
8 bits, this represents an affine transformation, where the value is multiplied by
``scale`` and ``offset`` is added.
"""
def __init__(self, scale: float, offset: float) -> None:
self.scale = scale self.scale = scale
self.offset = offset self.offset = offset
def __neg__(self): def __neg__(self) -> ImagePointTransform:
return _E(-self.scale, -self.offset) return ImagePointTransform(-self.scale, -self.offset)
def __add__(self, other): def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, _E): if isinstance(other, ImagePointTransform):
return _E(self.scale + other.scale, self.offset + other.offset) return ImagePointTransform(
return _E(self.scale, self.offset + other) self.scale + other.scale, self.offset + other.offset
)
return ImagePointTransform(self.scale, self.offset + other)
__radd__ = __add__ __radd__ = __add__
def __sub__(self, other): def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform:
return self + -other return self + -other
def __rsub__(self, other): def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform:
return other + -self return other + -self
def __mul__(self, other): def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, _E): if isinstance(other, ImagePointTransform):
return NotImplemented return NotImplemented
return _E(self.scale * other, self.offset * other) return ImagePointTransform(self.scale * other, self.offset * other)
__rmul__ = __mul__ __rmul__ = __mul__
def __truediv__(self, other): def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, _E): if isinstance(other, ImagePointTransform):
return NotImplemented return NotImplemented
return _E(self.scale / other, self.offset / other) return ImagePointTransform(self.scale / other, self.offset / other)
def _getscaleoffset(expr): def _getscaleoffset(
a = expr(_E(1, 0)) expr: Callable[[ImagePointTransform], ImagePointTransform | float]
return (a.scale, a.offset) if isinstance(a, _E) else (0, a) ) -> tuple[float, float]:
a = expr(ImagePointTransform(1, 0))
return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a)
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -610,7 +625,7 @@ class Image:
logger.debug("Error closing: %s", msg) logger.debug("Error closing: %s", msg)
if getattr(self, "map", None): if getattr(self, "map", None):
self.map = None self.map: mmap.mmap | None = None
# Instead of simply setting to None, we're setting up a # Instead of simply setting to None, we're setting up a
# deferred error that will better explain that the core image # deferred error that will better explain that the core image
@ -674,7 +689,7 @@ class Image:
id(self), id(self),
) )
def _repr_pretty_(self, p, cycle) -> None: def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None:
"""IPython plain text display support""" """IPython plain text display support"""
# Same as __repr__ but without unpredictable id(self), # Same as __repr__ but without unpredictable id(self),
@ -718,35 +733,23 @@ class Image:
return self._repr_image("JPEG") return self._repr_image("JPEG")
@property @property
def __array_interface__(self): def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]:
# numpy array interface support # numpy array interface support
new = {"version": 3} new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3}
try: if self.mode == "1":
if self.mode == "1": # Binary images need to be extended from bits to bytes
# Binary images need to be extended from bits to bytes # See: https://github.com/python-pillow/Pillow/issues/350
# See: https://github.com/python-pillow/Pillow/issues/350 new["data"] = self.tobytes("raw", "L")
new["data"] = self.tobytes("raw", "L") else:
else: new["data"] = self.tobytes()
new["data"] = self.tobytes()
except Exception as e:
if not isinstance(e, (MemoryError, RecursionError)):
try:
import numpy
from packaging.version import parse as parse_version
except ImportError:
pass
else:
if parse_version(numpy.__version__) < parse_version("1.23"):
warnings.warn(str(e))
raise
new["shape"], new["typestr"] = _conv_type_shape(self) new["shape"], new["typestr"] = _conv_type_shape(self)
return new return new
def __getstate__(self): def __getstate__(self) -> list[Any]:
im_data = self.tobytes() # load image first im_data = self.tobytes() # load image first
return [self.info, self.mode, self.size, self.getpalette(), im_data] return [self.info, self.mode, self.size, self.getpalette(), im_data]
def __setstate__(self, state) -> None: def __setstate__(self, state: list[Any]) -> None:
Image.__init__(self) Image.__init__(self)
info, mode, size, palette, data = state info, mode, size, palette, data = state
self.info = info self.info = info
@ -1334,9 +1337,6 @@ class Image:
self.load() self.load()
return self._new(self.im.expand(xmargin, ymargin)) return self._new(self.im.expand(xmargin, ymargin))
if TYPE_CHECKING:
from . import ImageFilter
def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image:
""" """
Filters this image using the given filter. For a list of Filters this image using the given filter. For a list of
@ -1418,7 +1418,7 @@ class Image:
return out return out
return self.im.getcolors(maxcolors) return self.im.getcolors(maxcolors)
def getdata(self, band: int | None = None): def getdata(self, band: int | None = None) -> core.ImagingCore:
""" """
Returns the contents of this image as a sequence object Returns the contents of this image as a sequence object
containing pixel values. The sequence object is flattened, so containing pixel values. The sequence object is flattened, so
@ -1467,8 +1467,8 @@ class Image:
def get_name(tag: str) -> str: def get_name(tag: str) -> str:
return re.sub("^{[^}]+}", "", tag) return re.sub("^{[^}]+}", "", tag)
def get_value(element): def get_value(element: Element) -> str | dict[str, Any] | None:
value = {get_name(k): v for k, v in element.attrib.items()} value: dict[str, Any] = {get_name(k): v for k, v in element.attrib.items()}
children = list(element) children = list(element)
if children: if children:
for child in children: for child in children:
@ -1549,6 +1549,7 @@ class Image:
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
if ifd1 and ifd1.get(513): if ifd1 and ifd1.get(513):
assert exif._info is not None
ifds.append((ifd1, exif._info.next)) ifds.append((ifd1, exif._info.next))
offset = None offset = None
@ -1558,12 +1559,13 @@ class Image:
offset = current_offset offset = current_offset
fp = self.fp fp = self.fp
thumbnail_offset = ifd.get(513) if ifd is not None:
if thumbnail_offset is not None: thumbnail_offset = ifd.get(513)
thumbnail_offset += getattr(self, "_exif_offset", 0) if thumbnail_offset is not None:
self.fp.seek(thumbnail_offset) thumbnail_offset += getattr(self, "_exif_offset", 0)
data = self.fp.read(ifd.get(514)) self.fp.seek(thumbnail_offset)
fp = io.BytesIO(data) data = self.fp.read(ifd.get(514))
fp = io.BytesIO(data)
with open(fp) as im: with open(fp) as im:
from . import TiffImagePlugin from . import TiffImagePlugin
@ -1681,7 +1683,9 @@ class Image:
x, y = self.im.getprojection() x, y = self.im.getprojection()
return list(x), list(y) return list(x), list(y)
def histogram(self, mask: Image | None = None, extrema=None) -> list[int]: def histogram(
self, mask: Image | None = None, extrema: tuple[float, float] | None = None
) -> list[int]:
""" """
Returns a histogram for the image. The histogram is returned as a Returns a histogram for the image. The histogram is returned as a
list of pixel counts, one for each pixel value in the source list of pixel counts, one for each pixel value in the source
@ -1707,12 +1711,14 @@ class Image:
mask.load() mask.load()
return self.im.histogram((0, 0), mask.im) return self.im.histogram((0, 0), mask.im)
if self.mode in ("I", "F"): if self.mode in ("I", "F"):
if extrema is None: return self.im.histogram(
extrema = self.getextrema() extrema if extrema is not None else self.getextrema()
return self.im.histogram(extrema) )
return self.im.histogram() return self.im.histogram()
def entropy(self, mask=None, extrema=None): def entropy(
self, mask: Image | None = None, extrema: tuple[float, float] | None = None
) -> float:
""" """
Calculates and returns the entropy for the image. Calculates and returns the entropy for the image.
@ -1733,9 +1739,9 @@ class Image:
mask.load() mask.load()
return self.im.entropy((0, 0), mask.im) return self.im.entropy((0, 0), mask.im)
if self.mode in ("I", "F"): if self.mode in ("I", "F"):
if extrema is None: return self.im.entropy(
extrema = self.getextrema() extrema if extrema is not None else self.getextrema()
return self.im.entropy(extrema) )
return self.im.entropy() return self.im.entropy()
def paste( def paste(
@ -1886,7 +1892,13 @@ class Image:
def point( def point(
self, self,
lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler, lut: (
Sequence[float]
| NumpyArray
| Callable[[int], float]
| Callable[[ImagePointTransform], ImagePointTransform | float]
| ImagePointHandler
),
mode: str | None = None, mode: str | None = None,
) -> Image: ) -> Image:
""" """
@ -1903,7 +1915,7 @@ class Image:
object:: object::
class Example(Image.ImagePointHandler): class Example(Image.ImagePointHandler):
def point(self, data): def point(self, im: Image) -> Image:
# Return result # Return result
:param mode: Output mode (default is same as input). This can only be used if :param mode: Output mode (default is same as input). This can only be used if
the source image has mode "L" or "P", and the output has mode "1" or the the source image has mode "L" or "P", and the output has mode "1" or the
@ -1922,10 +1934,10 @@ class Image:
# check if the function can be used with point_transform # check if the function can be used with point_transform
# UNDONE wiredfool -- I think this prevents us from ever doing # UNDONE wiredfool -- I think this prevents us from ever doing
# a gamma function point transform on > 8bit images. # a gamma function point transform on > 8bit images.
scale, offset = _getscaleoffset(lut) scale, offset = _getscaleoffset(lut) # type: ignore[arg-type]
return self._new(self.im.point_transform(scale, offset)) return self._new(self.im.point_transform(scale, offset))
# for other modes, convert the function to a table # for other modes, convert the function to a table
flatLut = [lut(i) for i in range(256)] * self.im.bands flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type]
else: else:
flatLut = lut flatLut = lut
@ -1996,7 +2008,7 @@ class Image:
def putdata( def putdata(
self, self,
data: Sequence[float] | Sequence[Sequence[int]] | NumpyArray, data: Sequence[float] | Sequence[Sequence[int]] | core.ImagingCore | NumpyArray,
scale: float = 1.0, scale: float = 1.0,
offset: float = 0.0, offset: float = 0.0,
) -> None: ) -> None:
@ -2046,7 +2058,11 @@ class Image:
msg = "illegal image mode" msg = "illegal image mode"
raise ValueError(msg) raise ValueError(msg)
if isinstance(data, ImagePalette.ImagePalette): if isinstance(data, ImagePalette.ImagePalette):
palette = ImagePalette.raw(data.rawmode, data.palette) if data.rawmode is not None:
palette = ImagePalette.raw(data.rawmode, data.palette)
else:
palette = ImagePalette.ImagePalette(palette=data.palette)
palette.dirty = 1
else: else:
if not isinstance(data, bytes): if not isinstance(data, bytes):
data = bytes(data) data = bytes(data)
@ -2184,7 +2200,12 @@ class Image:
return m_im return m_im
def _get_safe_box(self, size, resample, box): def _get_safe_box(
self,
size: tuple[int, int],
resample: Resampling,
box: tuple[float, float, float, float],
) -> tuple[int, int, int, int]:
"""Expands the box so it includes adjacent pixels """Expands the box so it includes adjacent pixels
that may be used by resampling with the given resampling filter. that may be used by resampling with the given resampling filter.
""" """
@ -2294,7 +2315,7 @@ class Image:
factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1 factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1
factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1 factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1
if factor_x > 1 or factor_y > 1: if factor_x > 1 or factor_y > 1:
reduce_box = self._get_safe_box(size, resample, box) reduce_box = self._get_safe_box(size, cast(Resampling, resample), box)
factor = (factor_x, factor_y) factor = (factor_x, factor_y)
self = ( self = (
self.reduce(factor, box=reduce_box) self.reduce(factor, box=reduce_box)
@ -2430,7 +2451,7 @@ class Image:
0.0, 0.0,
] ]
def transform(x, y, matrix): def transform(x: float, y: float, matrix: list[float]) -> tuple[float, float]:
(a, b, c, d, e, f) = matrix (a, b, c, d, e, f) = matrix
return a * x + b * y + c, d * x + e * y + f return a * x + b * y + c, d * x + e * y + f
@ -2445,9 +2466,9 @@ class Image:
xx = [] xx = []
yy = [] yy = []
for x, y in ((0, 0), (w, 0), (w, h), (0, h)): for x, y in ((0, 0), (w, 0), (w, h), (0, h)):
x, y = transform(x, y, matrix) transformed_x, transformed_y = transform(x, y, matrix)
xx.append(x) xx.append(transformed_x)
yy.append(y) yy.append(transformed_y)
nw = math.ceil(max(xx)) - math.floor(min(xx)) nw = math.ceil(max(xx)) - math.floor(min(xx))
nh = math.ceil(max(yy)) - math.floor(min(yy)) nh = math.ceil(max(yy)) - math.floor(min(yy))
@ -2705,7 +2726,7 @@ class Image:
provided_size = tuple(map(math.floor, size)) provided_size = tuple(map(math.floor, size))
def preserve_aspect_ratio() -> tuple[int, int] | None: def preserve_aspect_ratio() -> tuple[int, int] | None:
def round_aspect(number, key): def round_aspect(number: float, key: Callable[[int], float]) -> int:
return max(min(math.floor(number), math.ceil(number), key=key), 1) return max(min(math.floor(number), math.ceil(number), key=key), 1)
x, y = provided_size x, y = provided_size
@ -2849,8 +2870,14 @@ class Image:
return im return im
def __transformer( def __transformer(
self, box, image, method, data, resample=Resampling.NEAREST, fill=1 self,
): box: tuple[int, int, int, int],
image: Image,
method: Transform,
data: Sequence[float],
resample: int = Resampling.NEAREST,
fill: bool = True,
) -> None:
w = box[2] - box[0] w = box[2] - box[0]
h = box[3] - box[1] h = box[3] - box[1]
@ -2899,11 +2926,12 @@ class Image:
Resampling.BICUBIC, Resampling.BICUBIC,
): ):
if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS):
msg = { unusable: dict[int, str] = {
Resampling.BOX: "Image.Resampling.BOX", Resampling.BOX: "Image.Resampling.BOX",
Resampling.HAMMING: "Image.Resampling.HAMMING", Resampling.HAMMING: "Image.Resampling.HAMMING",
Resampling.LANCZOS: "Image.Resampling.LANCZOS", Resampling.LANCZOS: "Image.Resampling.LANCZOS",
}[resample] + f" ({resample}) cannot be used." }
msg = unusable[resample] + f" ({resample}) cannot be used."
else: else:
msg = f"Unknown resampling filter ({resample})." msg = f"Unknown resampling filter ({resample})."
@ -3286,7 +3314,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)
def fromqimage(im): def fromqimage(im) -> ImageFile.ImageFile:
"""Creates an image instance from a QImage image""" """Creates an image instance from a QImage image"""
from . import ImageQt from . import ImageQt
@ -3296,7 +3324,7 @@ def fromqimage(im):
return ImageQt.fromqimage(im) return ImageQt.fromqimage(im)
def fromqpixmap(im): def fromqpixmap(im) -> ImageFile.ImageFile:
"""Creates an image instance from a QPixmap image""" """Creates an image instance from a QPixmap image"""
from . import ImageQt from . import ImageQt
@ -3843,18 +3871,18 @@ class Exif(_ExifBase):
print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99
""" """
endian = None endian: str | None = None
bigtiff = False bigtiff = False
_loaded = False _loaded = False
def __init__(self): def __init__(self) -> None:
self._data = {} self._data: dict[int, Any] = {}
self._hidden_data = {} self._hidden_data: dict[int, Any] = {}
self._ifds = {} self._ifds: dict[int, dict[int, Any]] = {}
self._info = None self._info: TiffImagePlugin.ImageFileDirectory_v2 | None = None
self._loaded_exif = None self._loaded_exif: bytes | None = None
def _fixup(self, value): def _fixup(self, value: Any) -> Any:
try: try:
if len(value) == 1 and isinstance(value, tuple): if len(value) == 1 and isinstance(value, tuple):
return value[0] return value[0]
@ -3862,26 +3890,28 @@ class Exif(_ExifBase):
pass pass
return value return value
def _fixup_dict(self, src_dict): def _fixup_dict(self, src_dict: dict[int, Any]) -> dict[int, Any]:
# Helper function # Helper function
# returns a dict with any single item tuples/lists as individual values # returns a dict with any single item tuples/lists as individual values
return {k: self._fixup(v) for k, v in src_dict.items()} return {k: self._fixup(v) for k, v in src_dict.items()}
def _get_ifd_dict(self, offset, group=None): def _get_ifd_dict(
self, offset: int, group: int | None = None
) -> dict[int, Any] | None:
try: try:
# an offset pointer to the location of the nested embedded IFD. # an offset pointer to the location of the nested embedded IFD.
# It should be a long, but may be corrupted. # It should be a long, but may be corrupted.
self.fp.seek(offset) self.fp.seek(offset)
except (KeyError, TypeError): except (KeyError, TypeError):
pass return None
else: else:
from . import TiffImagePlugin from . import TiffImagePlugin
info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group)
info.load(self.fp) info.load(self.fp)
return self._fixup_dict(info) return self._fixup_dict(dict(info))
def _get_head(self): def _get_head(self) -> bytes:
version = b"\x2B" if self.bigtiff else b"\x2A" version = b"\x2B" if self.bigtiff else b"\x2A"
if self.endian == "<": if self.endian == "<":
head = b"II" + version + b"\x00" + o32le(8) head = b"II" + version + b"\x00" + o32le(8)
@ -3892,7 +3922,7 @@ class Exif(_ExifBase):
head += b"\x00\x00\x00\x00" head += b"\x00\x00\x00\x00"
return head return head
def load(self, data): def load(self, data: bytes) -> None:
# Extract EXIF information. This is highly experimental, # Extract EXIF information. This is highly experimental,
# and is likely to be replaced with something better in a future # and is likely to be replaced with something better in a future
# version. # version.
@ -3911,7 +3941,7 @@ class Exif(_ExifBase):
self._info = None self._info = None
return return
self.fp = io.BytesIO(data) self.fp: IO[bytes] = io.BytesIO(data)
self.head = self.fp.read(8) self.head = self.fp.read(8)
# process dictionary # process dictionary
from . import TiffImagePlugin from . import TiffImagePlugin
@ -3921,7 +3951,7 @@ class Exif(_ExifBase):
self.fp.seek(self._info.next) self.fp.seek(self._info.next)
self._info.load(self.fp) self._info.load(self.fp)
def load_from_fp(self, fp, offset=None): def load_from_fp(self, fp: IO[bytes], offset: int | None = None) -> None:
self._loaded_exif = None self._loaded_exif = None
self._data.clear() self._data.clear()
self._hidden_data.clear() self._hidden_data.clear()
@ -3944,7 +3974,7 @@ class Exif(_ExifBase):
self.fp.seek(offset) self.fp.seek(offset)
self._info.load(self.fp) self._info.load(self.fp)
def _get_merged_dict(self): def _get_merged_dict(self) -> dict[int, Any]:
merged_dict = dict(self) merged_dict = dict(self)
# get EXIF extension # get EXIF extension
@ -3982,15 +4012,19 @@ class Exif(_ExifBase):
ifd[tag] = value ifd[tag] = value
return b"Exif\x00\x00" + head + ifd.tobytes(offset) return b"Exif\x00\x00" + head + ifd.tobytes(offset)
def get_ifd(self, tag): def get_ifd(self, tag: int) -> dict[int, Any]:
if tag not in self._ifds: if tag not in self._ifds:
if tag == ExifTags.IFD.IFD1: if tag == ExifTags.IFD.IFD1:
if self._info is not None and self._info.next != 0: if self._info is not None and self._info.next != 0:
self._ifds[tag] = self._get_ifd_dict(self._info.next) ifd = self._get_ifd_dict(self._info.next)
if ifd is not None:
self._ifds[tag] = ifd
elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]:
offset = self._hidden_data.get(tag, self.get(tag)) offset = self._hidden_data.get(tag, self.get(tag))
if offset is not None: if offset is not None:
self._ifds[tag] = self._get_ifd_dict(offset, tag) ifd = self._get_ifd_dict(offset, tag)
if ifd is not None:
self._ifds[tag] = ifd
elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]:
if ExifTags.IFD.Exif not in self._ifds: if ExifTags.IFD.Exif not in self._ifds:
self.get_ifd(ExifTags.IFD.Exif) self.get_ifd(ExifTags.IFD.Exif)
@ -4047,7 +4081,9 @@ class Exif(_ExifBase):
(offset,) = struct.unpack(">L", data) (offset,) = struct.unpack(">L", data)
self.fp.seek(offset) self.fp.seek(offset)
camerainfo = {"ModelID": self.fp.read(4)} camerainfo: dict[str, int | bytes] = {
"ModelID": self.fp.read(4)
}
self.fp.read(4) self.fp.read(4)
# Seconds since 2000 # Seconds since 2000
@ -4063,16 +4099,18 @@ class Exif(_ExifBase):
][1] ][1]
camerainfo["Parallax"] = handler( camerainfo["Parallax"] = handler(
ImageFileDirectory_v2(), parallax, False ImageFileDirectory_v2(), parallax, False
) )[0]
self.fp.read(4) self.fp.read(4)
camerainfo["Category"] = self.fp.read(2) camerainfo["Category"] = self.fp.read(2)
makernote = {0x1101: dict(self._fixup_dict(camerainfo))} makernote = {0x1101: camerainfo}
self._ifds[tag] = makernote self._ifds[tag] = makernote
else: else:
# Interop # Interop
self._ifds[tag] = self._get_ifd_dict(tag_data, tag) ifd = self._get_ifd_dict(tag_data, tag)
if ifd is not None:
self._ifds[tag] = ifd
ifd = self._ifds.setdefault(tag, {}) ifd = self._ifds.setdefault(tag, {})
if tag == ExifTags.IFD.Exif and self._hidden_data: if tag == ExifTags.IFD.Exif and self._hidden_data:
ifd = { ifd = {
@ -4102,16 +4140,16 @@ class Exif(_ExifBase):
keys.update(self._info) keys.update(self._info)
return len(keys) return len(keys)
def __getitem__(self, tag): def __getitem__(self, tag: int) -> Any:
if self._info is not None and tag not in self._data and tag in self._info: if self._info is not None and tag not in self._data and tag in self._info:
self._data[tag] = self._fixup(self._info[tag]) self._data[tag] = self._fixup(self._info[tag])
del self._info[tag] del self._info[tag]
return self._data[tag] return self._data[tag]
def __contains__(self, tag) -> bool: def __contains__(self, tag: object) -> bool:
return tag in self._data or (self._info is not None and tag in self._info) return tag in self._data or (self._info is not None and tag in self._info)
def __setitem__(self, tag, value) -> None: def __setitem__(self, tag: int, value: Any) -> None:
if self._info is not None and tag in self._info: if self._info is not None and tag in self._info:
del self._info[tag] del self._info[tag]
self._data[tag] = value self._data[tag] = value
@ -4122,7 +4160,7 @@ class Exif(_ExifBase):
else: else:
del self._data[tag] del self._data[tag]
def __iter__(self): def __iter__(self) -> Iterator[int]:
keys = set(self._data) keys = set(self._data)
if self._info is not None: if self._info is not None:
keys.update(self._info) keys.update(self._info)

View File

@ -36,7 +36,7 @@ import numbers
import struct import struct
from collections.abc import Sequence from collections.abc import Sequence
from types import ModuleType from types import ModuleType
from typing import TYPE_CHECKING, AnyStr, Callable, Union, cast from typing import TYPE_CHECKING, Any, AnyStr, Callable, Union, cast
from . import Image, ImageColor from . import Image, ImageColor
from ._deprecate import deprecate from ._deprecate import deprecate
@ -505,7 +505,7 @@ class ImageDraw:
if full_x: if full_x:
self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1) self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1)
else: elif x1 - r - 1 > x0 + r + 1:
self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1) self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1)
if not full_x and not full_y: if not full_x and not full_y:
left = [x0, y0, x0 + r, y1] left = [x0, y0, x0 + r, y1]
@ -561,7 +561,12 @@ class ImageDraw:
def _multiline_split(self, text: AnyStr) -> list[AnyStr]: def _multiline_split(self, text: AnyStr) -> list[AnyStr]:
return text.split("\n" if isinstance(text, str) else b"\n") return text.split("\n" if isinstance(text, str) else b"\n")
def _multiline_spacing(self, font, spacing, stroke_width): def _multiline_spacing(
self,
font: ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont,
spacing: float,
stroke_width: float,
) -> float:
return ( return (
self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3]
+ stroke_width + stroke_width
@ -571,25 +576,25 @@ class ImageDraw:
def text( def text(
self, self,
xy: tuple[float, float], xy: tuple[float, float],
text: str, text: AnyStr,
fill=None, fill: _Ink | None = None,
font: ( font: (
ImageFont.ImageFont ImageFont.ImageFont
| ImageFont.FreeTypeFont | ImageFont.FreeTypeFont
| ImageFont.TransposedFont | ImageFont.TransposedFont
| None | None
) = None, ) = None,
anchor=None, anchor: str | None = None,
spacing=4, spacing: float = 4,
align="left", align: str = "left",
direction=None, direction: str | None = None,
features=None, features: list[str] | None = None,
language=None, language: str | None = None,
stroke_width=0, stroke_width: float = 0,
stroke_fill=None, stroke_fill: _Ink | None = None,
embedded_color=False, embedded_color: bool = False,
*args, *args: Any,
**kwargs, **kwargs: Any,
) -> None: ) -> None:
"""Draw text.""" """Draw text."""
if embedded_color and self.mode not in ("RGB", "RGBA"): if embedded_color and self.mode not in ("RGB", "RGBA"):
@ -623,15 +628,14 @@ class ImageDraw:
return fill_ink return fill_ink
return ink return ink
def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: def draw_text(ink: int, stroke_width: float = 0) -> None:
mode = self.fontmode mode = self.fontmode
if stroke_width == 0 and embedded_color: if stroke_width == 0 and embedded_color:
mode = "RGBA" mode = "RGBA"
coord = [] coord = []
start = []
for i in range(2): for i in range(2):
coord.append(int(xy[i])) coord.append(int(xy[i]))
start.append(math.modf(xy[i])[0]) start = (math.modf(xy[0])[0], math.modf(xy[1])[0])
try: try:
mask, offset = font.getmask2( # type: ignore[union-attr,misc] mask, offset = font.getmask2( # type: ignore[union-attr,misc]
text, text,
@ -664,8 +668,6 @@ class ImageDraw:
) )
except TypeError: except TypeError:
mask = font.getmask(text) mask = font.getmask(text)
if stroke_offset:
coord = [coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]]
if mode == "RGBA": if mode == "RGBA":
# font.getmask2(mode="RGBA") returns color in RGB bands and mask in A # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A
# extract mask and set text alpha # extract mask and set text alpha
@ -699,25 +701,25 @@ class ImageDraw:
def multiline_text( def multiline_text(
self, self,
xy: tuple[float, float], xy: tuple[float, float],
text: str, text: AnyStr,
fill=None, fill: _Ink | None = None,
font: ( font: (
ImageFont.ImageFont ImageFont.ImageFont
| ImageFont.FreeTypeFont | ImageFont.FreeTypeFont
| ImageFont.TransposedFont | ImageFont.TransposedFont
| None | None
) = None, ) = None,
anchor=None, anchor: str | None = None,
spacing=4, spacing: float = 4,
align="left", align: str = "left",
direction=None, direction: str | None = None,
features=None, features: list[str] | None = None,
language=None, language: str | None = None,
stroke_width=0, stroke_width: float = 0,
stroke_fill=None, stroke_fill: _Ink | None = None,
embedded_color=False, embedded_color: bool = False,
*, *,
font_size=None, font_size: float | None = None,
) -> None: ) -> None:
if direction == "ttb": if direction == "ttb":
msg = "ttb direction is unsupported for multiline text" msg = "ttb direction is unsupported for multiline text"
@ -790,19 +792,19 @@ class ImageDraw:
def textlength( def textlength(
self, self,
text: str, text: AnyStr,
font: ( font: (
ImageFont.ImageFont ImageFont.ImageFont
| ImageFont.FreeTypeFont | ImageFont.FreeTypeFont
| ImageFont.TransposedFont | ImageFont.TransposedFont
| None | None
) = None, ) = None,
direction=None, direction: str | None = None,
features=None, features: list[str] | None = None,
language=None, language: str | None = None,
embedded_color=False, embedded_color: bool = False,
*, *,
font_size=None, font_size: float | None = None,
) -> float: ) -> float:
"""Get the length of a given string, in pixels with 1/64 precision.""" """Get the length of a given string, in pixels with 1/64 precision."""
if self._multiline_check(text): if self._multiline_check(text):
@ -819,20 +821,25 @@ class ImageDraw:
def textbbox( def textbbox(
self, self,
xy, xy: tuple[float, float],
text, text: AnyStr,
font=None, font: (
anchor=None, ImageFont.ImageFont
spacing=4, | ImageFont.FreeTypeFont
align="left", | ImageFont.TransposedFont
direction=None, | None
features=None, ) = None,
language=None, anchor: str | None = None,
stroke_width=0, spacing: float = 4,
embedded_color=False, align: str = "left",
direction: str | None = None,
features: list[str] | None = None,
language: str | None = None,
stroke_width: float = 0,
embedded_color: bool = False,
*, *,
font_size=None, font_size: float | None = None,
) -> tuple[int, int, int, int]: ) -> tuple[float, float, float, float]:
"""Get the bounding box of a given string, in pixels.""" """Get the bounding box of a given string, in pixels."""
if embedded_color and self.mode not in ("RGB", "RGBA"): if embedded_color and self.mode not in ("RGB", "RGBA"):
msg = "Embedded color supported only in RGB and RGBA modes" msg = "Embedded color supported only in RGB and RGBA modes"
@ -864,20 +871,25 @@ class ImageDraw:
def multiline_textbbox( def multiline_textbbox(
self, self,
xy, xy: tuple[float, float],
text, text: AnyStr,
font=None, font: (
anchor=None, ImageFont.ImageFont
spacing=4, | ImageFont.FreeTypeFont
align="left", | ImageFont.TransposedFont
direction=None, | None
features=None, ) = None,
language=None, anchor: str | None = None,
stroke_width=0, spacing: float = 4,
embedded_color=False, align: str = "left",
direction: str | None = None,
features: list[str] | None = None,
language: str | None = None,
stroke_width: float = 0,
embedded_color: bool = False,
*, *,
font_size=None, font_size: float | None = None,
) -> tuple[int, int, int, int]: ) -> tuple[float, float, float, float]:
if direction == "ttb": if direction == "ttb":
msg = "ttb direction is unsupported for multiline text" msg = "ttb direction is unsupported for multiline text"
raise ValueError(msg) raise ValueError(msg)
@ -916,7 +928,7 @@ class ImageDraw:
elif anchor[1] == "d": elif anchor[1] == "d":
top -= (len(lines) - 1) * line_spacing top -= (len(lines) - 1) * line_spacing
bbox: tuple[int, int, int, int] | None = None bbox: tuple[float, float, float, float] | None = None
for idx, line in enumerate(lines): for idx, line in enumerate(lines):
left = xy[0] left = xy[0]

View File

@ -24,10 +24,10 @@
""" """
from __future__ import annotations from __future__ import annotations
from typing import BinaryIO from typing import Any, AnyStr, BinaryIO
from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
from ._typing import StrOrBytesPath from ._typing import Coords, StrOrBytesPath
class Pen: class Pen:
@ -74,12 +74,19 @@ class Draw:
image = Image.new(image, size, color) image = Image.new(image, size, color)
self.draw = ImageDraw.Draw(image) self.draw = ImageDraw.Draw(image)
self.image = image self.image = image
self.transform = None self.transform: tuple[float, float, float, float, float, float] | None = None
def flush(self) -> Image.Image: def flush(self) -> Image.Image:
return self.image return self.image
def render(self, op, xy, pen, brush=None): def render(
self,
op: str,
xy: Coords,
pen: Pen | Brush | None,
brush: Brush | Pen | None = None,
**kwargs: Any,
) -> None:
# handle color arguments # handle color arguments
outline = fill = None outline = fill = None
width = 1 width = 1
@ -95,63 +102,89 @@ class Draw:
fill = pen.color fill = pen.color
# handle transformation # handle transformation
if self.transform: if self.transform:
xy = ImagePath.Path(xy) path = ImagePath.Path(xy)
xy.transform(self.transform) path.transform(self.transform)
xy = path
# render the item # render the item
if op == "line": if op in ("arc", "line"):
self.draw.line(xy, fill=outline, width=width) kwargs.setdefault("fill", outline)
else: else:
getattr(self.draw, op)(xy, fill=fill, outline=outline) kwargs.setdefault("fill", fill)
kwargs.setdefault("outline", outline)
if op == "line":
kwargs.setdefault("width", width)
getattr(self.draw, op)(xy, **kwargs)
def settransform(self, offset): def settransform(self, offset: tuple[float, float]) -> None:
"""Sets a transformation offset.""" """Sets a transformation offset."""
(xoffset, yoffset) = offset (xoffset, yoffset) = offset
self.transform = (1, 0, xoffset, 0, 1, yoffset) self.transform = (1, 0, xoffset, 0, 1, yoffset)
def arc(self, xy, start, end, *options): def arc(
self,
xy: Coords,
pen: Pen | Brush | None,
start: float,
end: float,
*options: Any,
) -> None:
""" """
Draws an arc (a portion of a circle outline) between the start and end Draws an arc (a portion of a circle outline) between the start and end
angles, inside the given bounding box. angles, inside the given bounding box.
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc`
""" """
self.render("arc", xy, start, end, *options) self.render("arc", xy, pen, *options, start=start, end=end)
def chord(self, xy, start, end, *options): def chord(
self,
xy: Coords,
pen: Pen | Brush | None,
start: float,
end: float,
*options: Any,
) -> None:
""" """
Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points
with a straight line. with a straight line.
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord`
""" """
self.render("chord", xy, start, end, *options) self.render("chord", xy, pen, *options, start=start, end=end)
def ellipse(self, xy, *options): def ellipse(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
""" """
Draws an ellipse inside the given bounding box. Draws an ellipse inside the given bounding box.
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse`
""" """
self.render("ellipse", xy, *options) self.render("ellipse", xy, pen, *options)
def line(self, xy, *options): def line(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
""" """
Draws a line between the coordinates in the ``xy`` list. Draws a line between the coordinates in the ``xy`` list.
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line`
""" """
self.render("line", xy, *options) self.render("line", xy, pen, *options)
def pieslice(self, xy, start, end, *options): def pieslice(
self,
xy: Coords,
pen: Pen | Brush | None,
start: float,
end: float,
*options: Any,
) -> None:
""" """
Same as arc, but also draws straight lines between the end points and the Same as arc, but also draws straight lines between the end points and the
center of the bounding box. center of the bounding box.
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice`
""" """
self.render("pieslice", xy, start, end, *options) self.render("pieslice", xy, pen, *options, start=start, end=end)
def polygon(self, xy, *options): def polygon(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
""" """
Draws a polygon. Draws a polygon.
@ -162,28 +195,31 @@ class Draw:
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon`
""" """
self.render("polygon", xy, *options) self.render("polygon", xy, pen, *options)
def rectangle(self, xy, *options): def rectangle(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
""" """
Draws a rectangle. Draws a rectangle.
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle`
""" """
self.render("rectangle", xy, *options) self.render("rectangle", xy, pen, *options)
def text(self, xy, text, font): def text(self, xy: tuple[float, float], text: AnyStr, font: Font) -> None:
""" """
Draws the string at the given position. Draws the string at the given position.
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text`
""" """
if self.transform: if self.transform:
xy = ImagePath.Path(xy) path = ImagePath.Path(xy)
xy.transform(self.transform) path.transform(self.transform)
xy = path
self.draw.text(xy, text, font=font.font, fill=font.color) self.draw.text(xy, text, font=font.font, fill=font.color)
def textbbox(self, xy, text, font): def textbbox(
self, xy: tuple[float, float], text: AnyStr, font: Font
) -> tuple[float, float, float, float]:
""" """
Returns bounding box (in pixels) of given text. Returns bounding box (in pixels) of given text.
@ -192,11 +228,12 @@ class Draw:
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox`
""" """
if self.transform: if self.transform:
xy = ImagePath.Path(xy) path = ImagePath.Path(xy)
xy.transform(self.transform) path.transform(self.transform)
xy = path
return self.draw.textbbox(xy, text, font=font.font) return self.draw.textbbox(xy, text, font=font.font)
def textlength(self, text, font): def textlength(self, text: AnyStr, font: Font) -> float:
""" """
Returns length (in pixels) of given text. Returns length (in pixels) of given text.
This is the amount by which following text should be offset. This is the amount by which following text should be offset.

View File

@ -31,6 +31,7 @@ from __future__ import annotations
import abc import abc
import io import io
import itertools import itertools
import os
import struct import struct
import sys import sys
from typing import IO, Any, NamedTuple from typing import IO, Any, NamedTuple
@ -86,14 +87,14 @@ def raise_oserror(error: int) -> OSError:
raise _get_oserror(error, encoder=False) raise _get_oserror(error, encoder=False)
def _tilesort(t): def _tilesort(t: _Tile) -> int:
# sort on offset # sort on offset
return t[2] return t[2]
class _Tile(NamedTuple): class _Tile(NamedTuple):
codec_name: str codec_name: str
extents: tuple[int, int, int, int] extents: tuple[int, int, int, int] | None
offset: int offset: int
args: tuple[Any, ...] | str | None args: tuple[Any, ...] | str | None
@ -161,7 +162,7 @@ class ImageFile(Image.Image):
return Image.MIME.get(self.format.upper()) return Image.MIME.get(self.format.upper())
return None return None
def __setstate__(self, state): def __setstate__(self, state: list[Any]) -> None:
self.tile = [] self.tile = []
super().__setstate__(state) super().__setstate__(state)
@ -174,7 +175,7 @@ class ImageFile(Image.Image):
self.fp.close() self.fp.close()
self.fp = None self.fp = None
def load(self): def load(self) -> Image.core.PixelAccess | None:
"""Load image data based on tile list""" """Load image data based on tile list"""
if self.tile is None: if self.tile is None:
@ -185,7 +186,7 @@ class ImageFile(Image.Image):
if not self.tile: if not self.tile:
return pixel return pixel
self.map = None self.map: mmap.mmap | None = None
use_mmap = self.filename and len(self.tile) == 1 use_mmap = self.filename and len(self.tile) == 1
# As of pypy 2.1.0, memory mapping was failing here. # As of pypy 2.1.0, memory mapping was failing here.
use_mmap = use_mmap and not hasattr(sys, "pypy_version_info") use_mmap = use_mmap and not hasattr(sys, "pypy_version_info")
@ -193,17 +194,17 @@ class ImageFile(Image.Image):
readonly = 0 readonly = 0
# look for read/seek overrides # look for read/seek overrides
try: if hasattr(self, "load_read"):
read = self.load_read read = self.load_read
# don't use mmap if there are custom read/seek functions # don't use mmap if there are custom read/seek functions
use_mmap = False use_mmap = False
except AttributeError: else:
read = self.fp.read read = self.fp.read
try: if hasattr(self, "load_seek"):
seek = self.load_seek seek = self.load_seek
use_mmap = False use_mmap = False
except AttributeError: else:
seek = self.fp.seek seek = self.fp.seek
if use_mmap: if use_mmap:
@ -243,11 +244,8 @@ class ImageFile(Image.Image):
# sort tiles in file order # sort tiles in file order
self.tile.sort(key=_tilesort) self.tile.sort(key=_tilesort)
try: # FIXME: This is a hack to handle TIFF's JpegTables tag.
# FIXME: This is a hack to handle TIFF's JpegTables tag. prefix = getattr(self, "tile_prefix", b"")
prefix = self.tile_prefix
except AttributeError:
prefix = b""
# Remove consecutive duplicates that only differ by their offset # Remove consecutive duplicates that only differ by their offset
self.tile = [ self.tile = [
@ -333,14 +331,14 @@ class ImageFile(Image.Image):
# def load_read(self, read_bytes: int) -> bytes: # def load_read(self, read_bytes: int) -> bytes:
# pass # pass
def _seek_check(self, frame): def _seek_check(self, frame: int) -> bool:
if ( if (
frame < self._min_frame frame < self._min_frame
# Only check upper limit on frames if additional seek operations # Only check upper limit on frames if additional seek operations
# are not required to do so # are not required to do so
or ( or (
not (hasattr(self, "_n_frames") and self._n_frames is None) not (hasattr(self, "_n_frames") and self._n_frames is None)
and frame >= self.n_frames + self._min_frame and frame >= getattr(self, "n_frames") + self._min_frame
) )
): ):
msg = "attempt to seek outside sequence" msg = "attempt to seek outside sequence"
@ -370,7 +368,7 @@ class StubImageFile(ImageFile):
msg = "StubImageFile subclass must implement _open" msg = "StubImageFile subclass must implement _open"
raise NotImplementedError(msg) raise NotImplementedError(msg)
def load(self): def load(self) -> Image.core.PixelAccess | None:
loader = self._load() loader = self._load()
if loader is None: if loader is None:
msg = f"cannot find loader for this {self.format} file" msg = f"cannot find loader for this {self.format} file"
@ -378,7 +376,7 @@ class StubImageFile(ImageFile):
image = loader.load(self) image = loader.load(self)
assert image is not None assert image is not None
# become the other object (!) # become the other object (!)
self.__class__ = image.__class__ self.__class__ = image.__class__ # type: ignore[assignment]
self.__dict__ = image.__dict__ self.__dict__ = image.__dict__
return image.load() return image.load()
@ -396,8 +394,8 @@ class Parser:
incremental = None incremental = None
image: Image.Image | None = None image: Image.Image | None = None
data = None data: bytes | None = None
decoder = None decoder: Image.core.ImagingDecoder | PyDecoder | None = None
offset = 0 offset = 0
finished = 0 finished = 0
@ -409,7 +407,7 @@ class Parser:
""" """
assert self.data is None, "cannot reuse parsers" assert self.data is None, "cannot reuse parsers"
def feed(self, data): def feed(self, data: bytes) -> None:
""" """
(Consumer) Feed data to the parser. (Consumer) Feed data to the parser.
@ -491,7 +489,7 @@ class Parser:
def __exit__(self, *args: object) -> None: def __exit__(self, *args: object) -> None:
self.close() self.close()
def close(self): def close(self) -> Image.Image:
""" """
(Consumer) Close the stream. (Consumer) Close the stream.
@ -525,7 +523,7 @@ class Parser:
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _save(im, fp, tile, bufsize=0) -> None: def _save(im: Image.Image, fp: IO[bytes], tile: list[_Tile], bufsize: int = 0) -> None:
"""Helper to save image based on tile list """Helper to save image based on tile list
:param im: Image object. :param im: Image object.
@ -553,7 +551,14 @@ def _save(im, fp, tile, bufsize=0) -> None:
fp.flush() fp.flush()
def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None): def _encode_tile(
im: Image.Image,
fp: IO[bytes],
tile: list[_Tile],
bufsize: int,
fh: int | None,
exc: BaseException | None = None,
) -> None:
for encoder_name, extents, offset, args in tile: for encoder_name, extents, offset, args in tile:
if offset > 0: if offset > 0:
fp.seek(offset) fp.seek(offset)
@ -573,6 +578,7 @@ def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None):
break break
else: else:
# slight speedup: compress to real file object # slight speedup: compress to real file object
assert fh is not None
errcode = encoder.encode_to_file(fh, bufsize) errcode = encoder.encode_to_file(fh, bufsize)
if errcode < 0: if errcode < 0:
raise _get_oserror(errcode, encoder=True) from exc raise _get_oserror(errcode, encoder=True) from exc
@ -629,18 +635,18 @@ class PyCodecState:
class PyCodec: class PyCodec:
fd: IO[bytes] | None fd: IO[bytes] | None
def __init__(self, mode, *args): def __init__(self, mode: str, *args: Any) -> None:
self.im = None self.im: Image.core.ImagingCore | None = None
self.state = PyCodecState() self.state = PyCodecState()
self.fd = None self.fd = None
self.mode = mode self.mode = mode
self.init(args) self.init(args)
def init(self, args) -> None: def init(self, args: tuple[Any, ...]) -> None:
""" """
Override to perform codec specific initialization Override to perform codec specific initialization
:param args: Array of args items from the tile entry :param args: Tuple of arg items from the tile entry
:returns: None :returns: None
""" """
self.args = args self.args = args
@ -653,7 +659,7 @@ class PyCodec:
""" """
pass pass
def setfd(self, fd) -> None: def setfd(self, fd: IO[bytes]) -> None:
""" """
Called from ImageFile to set the Python file-like object Called from ImageFile to set the Python file-like object
@ -662,7 +668,11 @@ class PyCodec:
""" """
self.fd = fd self.fd = fd
def setimage(self, im, extents: tuple[int, int, int, int] | None = None) -> None: def setimage(
self,
im: Image.core.ImagingCore,
extents: tuple[int, int, int, int] | None = None,
) -> None:
""" """
Called from ImageFile to set the core output image for the codec Called from ImageFile to set the core output image for the codec
@ -793,7 +803,7 @@ class PyEncoder(PyCodec):
self.fd.write(data) self.fd.write(data)
return bytes_consumed, errcode return bytes_consumed, errcode
def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int: def encode_to_file(self, fh: int, bufsize: int) -> int:
""" """
:param fh: File handle. :param fh: File handle.
:param bufsize: Buffer size. :param bufsize: Buffer size.
@ -806,5 +816,5 @@ class PyEncoder(PyCodec):
while errcode == 0: while errcode == 0:
status, errcode, buf = self.encode(bufsize) status, errcode, buf = self.encode(bufsize)
if status > 0: if status > 0:
fh.write(buf[status:]) os.write(fh, buf[status:])
return errcode return errcode

View File

@ -34,7 +34,7 @@ import warnings
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from types import ModuleType from types import ModuleType
from typing import IO, TYPE_CHECKING, Any, BinaryIO from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict
from . import Image from . import Image
from ._typing import StrOrBytesPath from ._typing import StrOrBytesPath
@ -46,6 +46,13 @@ if TYPE_CHECKING:
from ._imagingft import Font from ._imagingft import Font
class Axis(TypedDict):
minimum: int | None
default: int | None
maximum: int | None
name: bytes | None
class Layout(IntEnum): class Layout(IntEnum):
BASIC = 0 BASIC = 0
RAQM = 1 RAQM = 1
@ -138,7 +145,9 @@ class ImageFont:
self.font = Image.core.font(image.im, data) self.font = Image.core.font(image.im, data)
def getmask(self, text, mode="", *args, **kwargs): def getmask(
self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any
) -> Image.core.ImagingCore:
""" """
Create a bitmap for the text. Create a bitmap for the text.
@ -236,7 +245,7 @@ class FreeTypeFont:
self.layout_engine = layout_engine self.layout_engine = layout_engine
def load_from_bytes(f): def load_from_bytes(f) -> None:
self.font_bytes = f.read() self.font_bytes = f.read()
self.font = core.getfont( self.font = core.getfont(
"", size, index, encoding, self.font_bytes, layout_engine "", size, index, encoding, self.font_bytes, layout_engine
@ -260,12 +269,12 @@ class FreeTypeFont:
else: else:
load_from_bytes(font) load_from_bytes(font)
def __getstate__(self): def __getstate__(self) -> list[Any]:
return [self.path, self.size, self.index, self.encoding, self.layout_engine] return [self.path, self.size, self.index, self.encoding, self.layout_engine]
def __setstate__(self, state): def __setstate__(self, state: list[Any]) -> None:
path, size, index, encoding, layout_engine = state path, size, index, encoding, layout_engine = state
self.__init__(path, size, index, encoding, layout_engine) FreeTypeFont.__init__(self, path, size, index, encoding, layout_engine)
def getname(self) -> tuple[str | None, str | None]: def getname(self) -> tuple[str | None, str | None]:
""" """
@ -283,7 +292,12 @@ class FreeTypeFont:
return self.font.ascent, self.font.descent return self.font.ascent, self.font.descent
def getlength( def getlength(
self, text: str | bytes, mode="", direction=None, features=None, language=None self,
text: str | bytes,
mode: str = "",
direction: str | None = None,
features: list[str] | None = None,
language: str | None = None,
) -> float: ) -> float:
""" """
Returns length (in pixels with 1/64 precision) of given text when rendered Returns length (in pixels with 1/64 precision) of given text when rendered
@ -424,16 +438,16 @@ class FreeTypeFont:
def getmask( def getmask(
self, self,
text, text: str | bytes,
mode="", mode: str = "",
direction=None, direction: str | None = None,
features=None, features: list[str] | None = None,
language=None, language: str | None = None,
stroke_width=0, stroke_width: float = 0,
anchor=None, anchor: str | None = None,
ink=0, ink: int = 0,
start=None, start: tuple[float, float] | None = None,
): ) -> Image.core.ImagingCore:
""" """
Create a bitmap for the text. Create a bitmap for the text.
@ -516,17 +530,17 @@ class FreeTypeFont:
def getmask2( def getmask2(
self, self,
text: str | bytes, text: str | bytes,
mode="", mode: str = "",
direction=None, direction: str | None = None,
features=None, features: list[str] | None = None,
language=None, language: str | None = None,
stroke_width=0, stroke_width: float = 0,
anchor=None, anchor: str | None = None,
ink=0, ink: int = 0,
start=None, start: tuple[float, float] | None = None,
*args, *args: Any,
**kwargs, **kwargs: Any,
): ) -> tuple[Image.core.ImagingCore, tuple[int, int]]:
""" """
Create a bitmap for the text. Create a bitmap for the text.
@ -599,7 +613,7 @@ class FreeTypeFont:
if start is None: if start is None:
start = (0, 0) start = (0, 0)
def fill(width, height): def fill(width: int, height: int) -> Image.core.ImagingCore:
size = (width, height) size = (width, height)
Image._decompression_bomb_check(size) Image._decompression_bomb_check(size)
return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) return Image.core.fill("RGBA" if mode == "RGBA" else "L", size)
@ -619,8 +633,13 @@ class FreeTypeFont:
) )
def font_variant( def font_variant(
self, font=None, size=None, index=None, encoding=None, layout_engine=None self,
): font: StrOrBytesPath | BinaryIO | None = None,
size: float | None = None,
index: int | None = None,
encoding: str | None = None,
layout_engine: Layout | None = None,
) -> FreeTypeFont:
""" """
Create a copy of this FreeTypeFont object, Create a copy of this FreeTypeFont object,
using any specified arguments to override the settings. using any specified arguments to override the settings.
@ -655,7 +674,7 @@ class FreeTypeFont:
raise NotImplementedError(msg) from e raise NotImplementedError(msg) from e
return [name.replace(b"\x00", b"") for name in names] return [name.replace(b"\x00", b"") for name in names]
def set_variation_by_name(self, name): def set_variation_by_name(self, name: str | bytes) -> None:
""" """
:param name: The name of the style. :param name: The name of the style.
:exception OSError: If the font is not a variation font. :exception OSError: If the font is not a variation font.
@ -674,7 +693,7 @@ class FreeTypeFont:
self.font.setvarname(index) self.font.setvarname(index)
def get_variation_axes(self): def get_variation_axes(self) -> list[Axis]:
""" """
:returns: A list of the axes in a variation font. :returns: A list of the axes in a variation font.
:exception OSError: If the font is not a variation font. :exception OSError: If the font is not a variation font.
@ -704,7 +723,9 @@ class FreeTypeFont:
class TransposedFont: class TransposedFont:
"""Wrapper for writing rotated or mirrored text""" """Wrapper for writing rotated or mirrored text"""
def __init__(self, font, orientation=None): def __init__(
self, font: ImageFont | FreeTypeFont, orientation: Image.Transpose | None = None
):
""" """
Wrapper that creates a transposed font from any existing font Wrapper that creates a transposed font from any existing font
object. object.
@ -718,13 +739,17 @@ class TransposedFont:
self.font = font self.font = font
self.orientation = orientation # any 'transpose' argument, or None self.orientation = orientation # any 'transpose' argument, or None
def getmask(self, text, mode="", *args, **kwargs): def getmask(
self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any
) -> Image.core.ImagingCore:
im = self.font.getmask(text, mode, *args, **kwargs) im = self.font.getmask(text, mode, *args, **kwargs)
if self.orientation is not None: if self.orientation is not None:
return im.transpose(self.orientation) return im.transpose(self.orientation)
return im return im
def getbbox(self, text, *args, **kwargs): def getbbox(
self, text: str | bytes, *args: Any, **kwargs: Any
) -> tuple[int, int, float, float]:
# TransposedFont doesn't support getmask2, move top-left point to (0, 0) # TransposedFont doesn't support getmask2, move top-left point to (0, 0)
# this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont
left, top, right, bottom = self.font.getbbox(text, *args, **kwargs) left, top, right, bottom = self.font.getbbox(text, *args, **kwargs)
@ -734,7 +759,7 @@ class TransposedFont:
return 0, 0, height, width return 0, 0, height, width
return 0, 0, width, height return 0, 0, width, height
def getlength(self, text: str | bytes, *args, **kwargs) -> float: def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> float:
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
msg = "text length is undefined for text rotated by 90 or 270 degrees" msg = "text length is undefined for text rotated by 90 or 270 degrees"
raise ValueError(msg) raise ValueError(msg)

View File

@ -249,14 +249,21 @@ def lambda_eval(
:py:func:`~PIL.Image.merge` function. :py:func:`~PIL.Image.merge` function.
:param expression: A function that receives a dictionary. :param expression: A function that receives a dictionary.
:param options: Values to add to the function's dictionary. You :param options: Values to add to the function's dictionary. Deprecated.
can either use a dictionary, or one or more keyword You can instead use one or more keyword arguments.
arguments. :param **kw: Values to add to the function's dictionary.
:return: The expression result. This is usually an image object, but can :return: The expression result. This is usually an image object, but can
also be an integer, a floating point value, or a pixel tuple, also be an integer, a floating point value, or a pixel tuple,
depending on the expression. depending on the expression.
""" """
if options:
deprecate(
"ImageMath.lambda_eval options",
12,
"ImageMath.lambda_eval keyword arguments",
)
args: dict[str, Any] = ops.copy() args: dict[str, Any] = ops.copy()
args.update(options) args.update(options)
args.update(kw) args.update(kw)
@ -287,14 +294,21 @@ def unsafe_eval(
:py:func:`~PIL.Image.merge` function. :py:func:`~PIL.Image.merge` function.
:param expression: A string containing a Python-style expression. :param expression: A string containing a Python-style expression.
:param options: Values to add to the evaluation context. You :param options: Values to add to the evaluation context. Deprecated.
can either use a dictionary, or one or more keyword You can instead use one or more keyword arguments.
arguments. :param **kw: Values to add to the evaluation context.
:return: The evaluated expression. This is usually an image object, but can :return: The evaluated expression. This is usually an image object, but can
also be an integer, a floating point value, or a pixel tuple, also be an integer, a floating point value, or a pixel tuple,
depending on the expression. depending on the expression.
""" """
if options:
deprecate(
"ImageMath.unsafe_eval options",
12,
"ImageMath.unsafe_eval keyword arguments",
)
# build execution namespace # build execution namespace
args: dict[str, Any] = ops.copy() args: dict[str, Any] = ops.copy()
for k in list(options.keys()) + list(kw.keys()): for k in list(options.keys()) + list(kw.keys()):

View File

@ -208,7 +208,7 @@ class ImagePalette:
# Internal # Internal
def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette: def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette:
palette = ImagePalette() palette = ImagePalette()
palette.rawmode = rawmode palette.rawmode = rawmode
palette.palette = data palette.palette = data

View File

@ -19,11 +19,14 @@ from __future__ import annotations
import sys import sys
from io import BytesIO from io import BytesIO
from typing import Callable from typing import TYPE_CHECKING, Callable
from . import Image from . import Image
from ._util import is_path from ._util import is_path
if TYPE_CHECKING:
from . import ImageFile
qt_version: str | None qt_version: str | None
qt_versions = [ qt_versions = [
["6", "PyQt6"], ["6", "PyQt6"],
@ -55,7 +58,7 @@ else:
qt_version = None qt_version = None
def rgb(r, g, b, a=255): def rgb(r: int, g: int, b: int, a: int = 255) -> int:
"""(Internal) Turns an RGB color into a Qt compatible color integer.""" """(Internal) Turns an RGB color into a Qt compatible color integer."""
# use qRgb to pack the colors, and then turn the resulting long # use qRgb to pack the colors, and then turn the resulting long
# into a negative integer with the same bitpattern. # into a negative integer with the same bitpattern.
@ -90,11 +93,11 @@ def fromqimage(im):
return Image.open(b) return Image.open(b)
def fromqpixmap(im): def fromqpixmap(im) -> ImageFile.ImageFile:
return fromqimage(im) return fromqimage(im)
def align8to32(bytes, width, mode): def align8to32(bytes: bytes, width: int, mode: str) -> bytes:
""" """
converts each scanline of data from 8 bit to 32 bit aligned converts each scanline of data from 8 bit to 32 bit aligned
""" """
@ -172,7 +175,7 @@ def _toqclass_helper(im):
if qt_is_installed: if qt_is_installed:
class ImageQt(QImage): class ImageQt(QImage):
def __init__(self, im): def __init__(self, im) -> None:
""" """
An PIL image wrapper for Qt. This is a subclass of PyQt's QImage An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
class. class.

View File

@ -33,7 +33,7 @@ class Iterator:
:param im: An image object. :param im: An image object.
""" """
def __init__(self, im: Image.Image): def __init__(self, im: Image.Image) -> None:
if not hasattr(im, "seek"): if not hasattr(im, "seek"):
msg = "im must have seek method" msg = "im must have seek method"
raise AttributeError(msg) raise AttributeError(msg)

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