diff --git a/.appveyor.yml b/.appveyor.yml
index 6ce5200b6..e12987a5f 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -21,9 +21,9 @@ environment:
- PYTHON: C:/Python312
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- - PYTHON: C:/Python38-x64
+ - PYTHON: C:/Python39-x64
ARCHITECTURE: AMD64
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
+ APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
install:
@@ -38,7 +38,7 @@ install:
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH%
- cd c:\pillow\winbuild\
- ps: |
- c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
+ c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
c:\pillow\winbuild\build\build_dep_all.cmd
$host.SetShouldExit(0)
- path C:\pillow\winbuild\build\bin;%PATH%
diff --git a/.ci/install.sh b/.ci/install.sh
index 30b64349d..1eb098be9 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -28,8 +28,6 @@ fi
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel
-# TODO Update condition when cffi supports 3.13
-if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install olefile
@@ -39,8 +37,7 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then
- # TODO Update condition when NumPy supports 3.13
- if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
+ python3 -m pip install numpy
# PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
@@ -51,7 +48,6 @@ if [[ $(uname) != CYGWIN* ]]; then
# Pyroma uses non-isolated build and fails with old setuptools
if [[
$GHA_PYTHON_VERSION == pypy3.9
- || $GHA_PYTHON_VERSION == 3.8
|| $GHA_PYTHON_VERSION == 3.9
]]; then
# To match pyproject.toml
diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt
index 0d0f81fbf..a2bf2a7b0 100644
--- a/.ci/requirements-cibw.txt
+++ b/.ci/requirements-cibw.txt
@@ -1 +1 @@
-cibuildwheel==2.19.1
+cibuildwheel==2.19.2
diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt
index a0dcb92d2..6dd432488 100644
--- a/.ci/requirements-mypy.txt
+++ b/.ci/requirements-mypy.txt
@@ -1 +1 @@
-mypy==1.10.0
+mypy==1.10.1
diff --git a/.coveragerc b/.coveragerc
index 018cc1cbf..a94a25678 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -19,6 +19,5 @@ exclude_also =
[run]
omit =
Tests/32bit_segfault_check.py
- Tests/bench_cffi_access.py
Tests/check_*.py
Tests/createfontdatachunk.py
diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh
index f8f191d38..d35cfcd31 100755
--- a/.github/workflows/macos-install.sh
+++ b/.github/workflows/macos-install.sh
@@ -18,9 +18,6 @@ else
fi
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
-# TODO Update condition when cffi supports 3.13
-if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
-
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install olefile
@@ -28,9 +25,7 @@ python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
-
-# TODO Update condition when NumPy supports 3.13
-if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
+python3 -m pip install numpy
# extra test images
pushd depends && ./install_extra_test_images.sh && popd
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index 1269ef8cb..8e2827099 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.yml
@@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-minor-version: [8, 9]
+ python-minor-version: [9]
timeout-minutes: 40
@@ -72,7 +72,6 @@ jobs:
make
netpbm
perl
- python3${{ matrix.python-minor-version }}-cffi
python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-numpy
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index 6afed74db..21e1275e7 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -44,13 +44,11 @@ jobs:
amazon-2023-amd64,
arch,
centos-stream-9-amd64,
- debian-11-bullseye-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
fedora-39-amd64,
fedora-40-amd64,
gentoo,
- ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64,
ubuntu-24.04-noble-amd64,
]
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index a773ca453..e5e1ec32e 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -64,7 +64,6 @@ jobs:
mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \
- mingw-w64-x86_64-python3-cffi \
mingw-w64-x86_64-python3-numpy \
mingw-w64-x86_64-python3-olefile \
mingw-w64-x86_64-python3-setuptools \
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index ee265774b..5b34d6703 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
+ python-version: ["pypy3.10", "pypy3.9", "3.9", "3.10", "3.11", "3.12", "3.13"]
timeout-minutes: 30
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index aa5646caf..0972459b0 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -48,7 +48,6 @@ jobs:
"3.11",
"3.10",
"3.9",
- "3.8",
]
include:
- python-version: "3.11"
@@ -59,13 +58,9 @@ jobs:
# M1 only available for 3.10+
- os: "macos-13"
python-version: "3.9"
- - os: "macos-13"
- python-version: "3.8"
exclude:
- os: "macos-14"
python-version: "3.9"
- - os: "macos-14"
- python-version: "3.8"
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh
index a3376ac92..3fbf3be69 100755
--- a/.github/workflows/wheels-test.sh
+++ b/.github/workflows/wheels-test.sh
@@ -12,7 +12,7 @@ elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
else
yum install -y fribidi
fi
-if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ] && !([[ "$OSTYPE" == "darwin"* ]] && [[ $(python3 --version) == *"3.13."* ]]); then
+if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
python3 -m pip install numpy
fi
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 3d6099c1c..f8dff3a3e 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -41,12 +41,8 @@ jobs:
python-version:
- pp39
- pp310
- - cp38
- - cp39
- - cp310
- - cp311
- - cp312
- - cp313
+ - cp3{9,10,11}
+ - cp3{12,13}
spec:
- manylinux2014
- manylinux_2_28
@@ -136,8 +132,6 @@ jobs:
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_PRERELEASE_PYTHONS: True
- CIBW_SKIP: pp38-*
- CIBW_TEST_SKIP: cp38-macosx_arm64
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v4
@@ -208,7 +202,6 @@ jobs:
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_PRERELEASE_PYTHONS: True
- CIBW_SKIP: pp38-*
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6a76e8c00..659409de4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.4.7
+ rev: v0.5.0
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
@@ -11,7 +11,7 @@ repos:
- id: black
- repo: https://github.com/PyCQA/bandit
- rev: 1.7.8
+ rev: 1.7.9
hooks:
- id: bandit
args: [--severity-level=high]
@@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
- rev: v18.1.5
+ rev: v18.1.8
hooks:
- id: clang-format
types: [c]
@@ -50,7 +50,7 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
- rev: 0.28.4
+ rev: 0.28.6
hooks:
- id: check-github-workflows
- id: check-readthedocs
@@ -62,7 +62,7 @@ repos:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
- rev: 1.8.0
+ rev: 2.1.3
hooks:
- id: pyproject-fmt
diff --git a/CHANGES.rst b/CHANGES.rst
index d7231ebea..856458aa9 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,9 +2,99 @@
Changelog (Pillow)
==================
-10.4.0 (unreleased)
+11.0.0 (unreleased)
-------------------
+- Drop support for Python 3.8 #8183
+ [hugovk, radarhere]
+
+- Add support for Python 3.13 #8181
+ [hugovk, radarhere]
+
+- Fix incompatibility with NumPy 1.20 #8187
+ [neutrinoceros, radarhere]
+
+- Remove PSFile, PyAccess and USE_CFFI_ACCESS #8182
+ [hugovk, radarhere]
+
+10.4.0 (2024-07-01)
+-------------------
+
+- Raise FileNotFoundError if show_file() path does not exist #8178
+ [radarhere]
+
+- Improved reading 16-bit TGA images with colour #7965
+ [Yay295, radarhere]
+
+- Deprecate non-image ImageCms modes #8031
+ [radarhere]
+
+- Fixed processing multiple JPEG EXIF markers #8127
+ [radarhere]
+
+- Do not preserve EXIFIFD tag by default when saving TIFF images #8110
+ [radarhere]
+
+- Added ImageFont.load_default_imagefont() #8086
+ [radarhere]
+
+- Added Image.WARN_POSSIBLE_FORMATS #8063
+ [radarhere]
+
+- Remove zero-byte end padding when parsing any XMP data #8171
+ [radarhere]
+
+- Do not detect Ultra HDR images as MPO #8056
+ [radarhere]
+
+- Raise SyntaxError specific to JP2 #8146
+ [Yay295, radarhere]
+
+- Do not use first frame duration for other frames when saving APNG images #8104
+ [radarhere]
+
+- Consider I;16 pixel size when using a 1 mode mask #8112
+ [radarhere]
+
+- When saving multiple PNG frames, convert to mode rather than raw mode #8087
+ [radarhere]
+
+- Added byte support to FreeTypeFont #8141
+ [radarhere]
+
+- Allow float center for rotate operations #8114
+ [radarhere]
+
+- Do not read layers immediately when opening PSD images #8039
+ [radarhere]
+
+- Restore original thread state #8065
+ [radarhere]
+
+- Read IM and TIFF images as RGB, rather than RGBX #7997
+ [radarhere]
+
+- Only preserve TIFF IPTC_NAA_CHUNK tag if type is BYTE or UNDEFINED #7948
+ [radarhere]
+
+- Clarify ImageDraw2 error message when size is missing #8165
+ [radarhere]
+
+- Support unpacking more rawmodes to RGBA palettes #7966
+ [radarhere]
+
+- Removed support for Qt 5 #8159
+ [radarhere]
+
+- Improve ``ImageFont.freetype`` support for XDG directories on Linux #8135
+ [mamg22, radarhere]
+
+- Improved consistency of XMP handling #8069
+ [radarhere]
+
+- Use pkg-config to help find libwebp and raqm #8142
+ [radarhere]
+
- Accept 't' suffix for libtiff version #8126, #8129
[radarhere]
diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py
deleted file mode 100644
index c7d105836..000000000
--- a/Tests/bench_cffi_access.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from __future__ import annotations
-
-import time
-
-from PIL import PyAccess
-
-from .helper import hopper
-
-# Not running this test by default. No DOS against CI.
-
-
-def iterate_get(size, access) -> None:
- (w, h) = size
- for x in range(w):
- for y in range(h):
- access[(x, y)]
-
-
-def iterate_set(size, access) -> None:
- (w, h) = size
- for x in range(w):
- for y in range(h):
- access[(x, y)] = (x % 256, y % 256, 0)
-
-
-def timer(func, label, *args) -> None:
- iterations = 5000
- starttime = time.time()
- for x in range(iterations):
- func(*args)
- if time.time() - starttime > 10:
- break
- endtime = time.time()
- print(
- f"{label}: completed {x + 1} iterations in {endtime - starttime:.4f}s, "
- f"{(endtime - starttime) / (x + 1.0):.6f}s per iteration"
- )
-
-
-def test_direct() -> None:
- im = hopper()
- im.load()
- # im = Image.new("RGB", (2000, 2000), (1, 3, 2))
- caccess = im.im.pixel_access(False)
- access = PyAccess.new(im, False)
-
- assert access is not None
- assert caccess[(0, 0)] == access[(0, 0)]
-
- print(f"Size: {im.width}x{im.height}")
- timer(iterate_get, "PyAccess - get", im.size, access)
- timer(iterate_set, "PyAccess - set", im.size, access)
- timer(iterate_get, "C-api - get", im.size, caccess)
- timer(iterate_set, "C-api - set", im.size, caccess)
diff --git a/Tests/helper.py b/Tests/helper.py
index fe337c09f..d6a93a803 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -11,14 +11,15 @@ import subprocess
import sys
import sysconfig
import tempfile
+from collections.abc import Sequence
from functools import lru_cache
from io import BytesIO
-from typing import Any, Callable, Sequence
+from typing import Any, Callable
import pytest
from packaging.version import parse as parse_version
-from PIL import Image, ImageMath, features
+from PIL import Image, ImageFile, ImageMath, features
logger = logging.getLogger(__name__)
@@ -59,9 +60,7 @@ def convert_to_comparable(
return new_a, new_b
-def assert_deep_equal(
- a: Sequence[Any], b: Sequence[Any], msg: str | None = None
-) -> None:
+def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None:
try:
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
except Exception:
@@ -240,7 +239,7 @@ class PillowLeakTestCase:
# helpers
-def fromstring(data: bytes) -> Image.Image:
+def fromstring(data: bytes) -> ImageFile.ImageFile:
return Image.open(BytesIO(data))
diff --git a/Tests/images/imagedraw_polygon_width_I.tiff b/Tests/images/imagedraw_polygon_width_I.tiff
new file mode 100644
index 000000000..ce1917d61
Binary files /dev/null and b/Tests/images/imagedraw_polygon_width_I.tiff differ
diff --git a/Tests/images/multiple_exif.jpg b/Tests/images/multiple_exif.jpg
index 32e0aa301..f9f343507 100644
Binary files a/Tests/images/multiple_exif.jpg and b/Tests/images/multiple_exif.jpg differ
diff --git a/Tests/images/p_16.png b/Tests/images/p_16.png
index e35886412..458f7138e 100644
Binary files a/Tests/images/p_16.png and b/Tests/images/p_16.png differ
diff --git a/Tests/images/rgba16.tga b/Tests/images/rgba16.tga
new file mode 100644
index 000000000..3918647a2
Binary files /dev/null and b/Tests/images/rgba16.tga differ
diff --git a/Tests/images/ultrahdr.jpg b/Tests/images/ultrahdr.jpg
new file mode 100644
index 000000000..34f615b61
Binary files /dev/null and b/Tests/images/ultrahdr.jpg differ
diff --git a/Tests/images/unknown_mode.j2k b/Tests/images/unknown_mode.j2k
new file mode 100644
index 000000000..38719fe25
Binary files /dev/null and b/Tests/images/unknown_mode.j2k differ
diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py
index c8886a779..0d9c0b419 100644
--- a/Tests/test_color_lut.py
+++ b/Tests/test_color_lut.py
@@ -321,6 +321,7 @@ class TestColorLut3DCoreAPI:
-1, 2, 2, 2, 2, 2,
])).load()
# fmt: on
+ assert transformed is not None
assert transformed[0, 0] == (0, 0, 255)
assert transformed[50, 50] == (0, 0, 255)
assert transformed[255, 0] == (0, 255, 255)
@@ -341,6 +342,7 @@ class TestColorLut3DCoreAPI:
-3, 5, 5, 5, 5, 5,
])).load()
# fmt: on
+ assert transformed is not None
assert transformed[0, 0] == (0, 0, 255)
assert transformed[50, 50] == (0, 0, 255)
assert transformed[255, 0] == (0, 255, 255)
@@ -354,10 +356,10 @@ class TestColorLut3DCoreAPI:
class TestColorLut3DFilter:
def test_wrong_args(self) -> None:
with pytest.raises(ValueError, match="should be either an integer"):
- ImageFilter.Color3DLUT("small", [1])
+ ImageFilter.Color3DLUT("small", [1]) # type: ignore[arg-type]
with pytest.raises(ValueError, match="should be either an integer"):
- ImageFilter.Color3DLUT((11, 11), [1])
+ ImageFilter.Color3DLUT((11, 11), [1]) # type: ignore[arg-type]
with pytest.raises(ValueError, match=r"in \[2, 65\] range"):
ImageFilter.Color3DLUT((11, 11, 1), [1])
diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py
index 584d8f91d..82ff14181 100644
--- a/Tests/test_deprecate.py
+++ b/Tests/test_deprecate.py
@@ -9,9 +9,9 @@ from PIL import _deprecate
"version, expected",
[
(
- 11,
- "Old thing is deprecated and will be removed in Pillow 11 "
- r"\(2024-10-15\)\. Use new thing instead\.",
+ 12,
+ "Old thing is deprecated and will be removed in Pillow 12 "
+ r"\(2025-10-15\)\. Use new thing instead\.",
),
(
None,
@@ -54,18 +54,18 @@ def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
def test_plural() -> None:
expected = (
- r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
+ r"Old things are deprecated and will be removed in Pillow 12 \(2025-10-15\)\. "
r"Use new thing instead\."
)
with pytest.warns(DeprecationWarning, match=expected):
- _deprecate.deprecate("Old things", 11, "new thing", plural=True)
+ _deprecate.deprecate("Old things", 12, "new thing", plural=True)
def test_replacement_and_action() -> None:
expected = "Use only one of 'replacement' and 'action'"
with pytest.raises(ValueError, match=expected):
_deprecate.deprecate(
- "Old thing", 11, replacement="new thing", action="Upgrade to new thing"
+ "Old thing", 12, replacement="new thing", action="Upgrade to new thing"
)
@@ -78,16 +78,16 @@ def test_replacement_and_action() -> None:
)
def test_action(action: str) -> None:
expected = (
- r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
+ r"Old thing is deprecated and will be removed in Pillow 12 \(2025-10-15\)\. "
r"Upgrade to new thing\."
)
with pytest.warns(DeprecationWarning, match=expected):
- _deprecate.deprecate("Old thing", 11, action=action)
+ _deprecate.deprecate("Old thing", 12, action=action)
def test_no_replacement_or_action() -> None:
expected = (
- r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)"
+ r"Old thing is deprecated and will be removed in Pillow 12 \(2025-10-15\)"
)
with pytest.warns(DeprecationWarning, match=expected):
- _deprecate.deprecate("Old thing", 11)
+ _deprecate.deprecate("Old thing", 12)
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index 1b393a3ff..e95850212 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -706,10 +706,21 @@ def test_different_modes_in_later_frames(
assert reloaded.mode == mode
-def test_apng_repeated_seeks_give_correct_info() -> None:
+def test_different_durations(tmp_path: Path) -> None:
+ test_file = str(tmp_path / "temp.png")
+
with Image.open("Tests/images/apng/different_durations.png") as im:
- for i in range(3):
+ for _ in range(3):
im.seek(0)
assert im.info["duration"] == 4000
+
im.seek(1)
assert im.info["duration"] == 1000
+
+ im.save(test_file, save_all=True)
+
+ with Image.open(test_file) as reloaded:
+ assert reloaded.info["duration"] == 4000
+
+ reloaded.seek(1)
+ assert reloaded.info["duration"] == 1000
diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py
index 939e82e77..77ee5b0ea 100644
--- a/Tests/test_file_bufrstub.py
+++ b/Tests/test_file_bufrstub.py
@@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
im.fp.close()
return Image.new("RGB", (1, 1))
+ def is_loaded(self) -> bool:
+ return self.loaded
+
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True
@@ -71,10 +74,10 @@ def test_handler(tmp_path: Path) -> None:
BufrStubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im:
assert handler.opened
- assert not handler.loaded
+ assert not handler.is_loaded()
im.load()
- assert handler.loaded
+ assert handler.is_loaded()
temp_file = str(tmp_path / "temp.bufr")
im.save(temp_file)
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
index 1c21aa8ca..b54238132 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -329,46 +329,6 @@ def test_read_binary_preview() -> None:
pass
-def test_readline_psfile(tmp_path: Path) -> None:
- # check all the freaking line endings possible from the spec
- # test_string = u'something\r\nelse\n\rbaz\rbif\n'
- line_endings = ["\r\n", "\n", "\n\r", "\r"]
- strings = ["something", "else", "baz", "bif"]
-
- def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
- ending = f"Failure with line ending: {''.join(str(ord(s)) for s in ending)}"
- assert t.readline().strip("\r\n") == "something", ending
- assert t.readline().strip("\r\n") == "else", ending
- assert t.readline().strip("\r\n") == "baz", ending
- assert t.readline().strip("\r\n") == "bif", ending
-
- def _test_readline_io_psfile(test_string: str, ending: str) -> None:
- f = io.BytesIO(test_string.encode("latin-1"))
- with pytest.warns(DeprecationWarning):
- t = EpsImagePlugin.PSFile(f)
- _test_readline(t, ending)
-
- def _test_readline_file_psfile(test_string: str, ending: str) -> None:
- f = str(tmp_path / "temp.txt")
- with open(f, "wb") as w:
- w.write(test_string.encode("latin-1"))
-
- with open(f, "rb") as r:
- with pytest.warns(DeprecationWarning):
- t = EpsImagePlugin.PSFile(r)
- _test_readline(t, ending)
-
- for ending in line_endings:
- s = ending.join(strings)
- _test_readline_io_psfile(s, ending)
- _test_readline_file_psfile(s, ending)
-
-
-def test_psfile_deprecation() -> None:
- with pytest.warns(DeprecationWarning):
- EpsImagePlugin.PSFile(None)
-
-
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
@pytest.mark.parametrize(
"line_ending",
@@ -425,9 +385,10 @@ def test_timeout(test_file: str) -> None:
def test_bounding_box_in_trailer() -> None:
# Check bounding boxes are parsed in the same way
# when specified in the header and the trailer
- with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open(
- FILE1
- ) as header_image:
+ with (
+ Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image,
+ Image.open(FILE1) as header_image,
+ ):
assert trailer_image.size == header_image.size
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index e19c88a47..85b017d29 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -1,9 +1,9 @@
from __future__ import annotations
import warnings
+from collections.abc import Generator
from io import BytesIO
from pathlib import Path
-from typing import Generator
import pytest
@@ -353,7 +353,7 @@ def test_palette_434(tmp_path: Path) -> None:
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
out = str(tmp_path / "temp.gif")
- im.copy().save(out, **kwargs)
+ im.copy().save(out, "GIF", **kwargs)
reloaded = Image.open(out)
return reloaded
diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py
index 86a9064fc..aba473d24 100644
--- a/Tests/test_file_gribstub.py
+++ b/Tests/test_file_gribstub.py
@@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
im.fp.close()
return Image.new("RGB", (1, 1))
+ def is_loaded(self) -> bool:
+ return self.loaded
+
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True
@@ -71,10 +74,10 @@ def test_handler(tmp_path: Path) -> None:
GribStubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im:
assert handler.opened
- assert not handler.loaded
+ assert not handler.is_loaded()
im.load()
- assert handler.loaded
+ assert handler.is_loaded()
temp_file = str(tmp_path / "temp.grib")
im.save(temp_file)
diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py
index ee1544c51..8275bd0d8 100644
--- a/Tests/test_file_hdf5stub.py
+++ b/Tests/test_file_hdf5stub.py
@@ -66,6 +66,9 @@ def test_handler(tmp_path: Path) -> None:
im.fp.close()
return Image.new("RGB", (1, 1))
+ def is_loaded(self) -> bool:
+ return self.loaded
+
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True
@@ -73,10 +76,10 @@ def test_handler(tmp_path: Path) -> None:
Hdf5StubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im:
assert handler.opened
- assert not handler.loaded
+ assert not handler.is_loaded()
im.load()
- assert handler.loaded
+ assert handler.is_loaded()
temp_file = str(tmp_path / "temp.h5")
im.save(temp_file)
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index 1459a87eb..68705094b 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -872,7 +872,7 @@ class TestFileJpeg:
def test_multiple_exif(self) -> None:
with Image.open("Tests/images/multiple_exif.jpg") as im:
- assert im.info["exif"] == b"Exif\x00\x00firstsecond"
+ assert im.getexif()[270] == "firstsecond"
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@@ -948,6 +948,7 @@ class TestFileJpeg:
):
assert im.getxmp() == {}
else:
+ assert "xmp" in im.info
xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]
@@ -1032,8 +1033,10 @@ class TestFileJpeg:
def test_repr_jpeg(self) -> None:
im = hopper()
+ b = im._repr_jpeg_()
+ assert b is not None
- with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg:
+ with Image.open(BytesIO(b)) as repr_jpeg:
assert repr_jpeg.format == "JPEG"
assert_image_similar(im, repr_jpeg, 17)
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index ed7ea4fcf..a5cfa7c6c 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -335,9 +335,15 @@ def test_issue_6194() -> None:
assert im.getpixel((5, 5)) == 31
+def test_unknown_j2k_mode() -> None:
+ with pytest.raises(UnidentifiedImageError):
+ with Image.open("Tests/images/unknown_mode.j2k"):
+ pass
+
+
def test_unbound_local() -> None:
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
- with pytest.raises(OSError):
+ with pytest.raises(UnidentifiedImageError):
with Image.open("Tests/images/unbound_variable.jp2"):
pass
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index fe9d017c0..d5dbeeb6f 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -92,11 +92,22 @@ class TestFileLibTiff(LibTiffTestCase):
def test_g4_non_disk_file_object(self, tmp_path: Path) -> None:
"""Testing loading from non-disk non-BytesIO file object"""
test_file = "Tests/images/hopper_g4_500.tif"
- s = io.BytesIO()
with open(test_file, "rb") as f:
- s.write(f.read())
- s.seek(0)
- r = io.BufferedReader(s)
+ data = f.read()
+
+ class NonBytesIO(io.RawIOBase):
+ def read(self, size: int = -1) -> bytes:
+ nonlocal data
+ if size == -1:
+ size = len(data)
+ result = data[:size]
+ data = data[size:]
+ return result
+
+ def readable(self) -> bool:
+ return True
+
+ r = io.BufferedReader(NonBytesIO())
with Image.open(r) as im:
assert im.size == (500, 500)
self._assert_noerr(tmp_path, im)
@@ -685,13 +696,18 @@ class TestFileLibTiff(LibTiffTestCase):
assert reloaded.tag_v2[530] == (1, 1)
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
- def test_exif_ifd(self, tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp.tif")
+ def test_exif_ifd(self) -> None:
+ out = io.BytesIO()
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
assert im.tag_v2[34665] == 125456
- im.save(outfile)
+ im.save(out, "TIFF")
- with Image.open(outfile) as reloaded:
+ with Image.open(out) as reloaded:
+ assert 34665 not in reloaded.tag_v2
+
+ im.save(out, "TIFF", tiffinfo={34665: 125456})
+
+ with Image.open(out) as reloaded:
if Image.core.libtiff_support_custom_tags:
assert reloaded.tag_v2[34665] == 125456
@@ -1043,7 +1059,11 @@ class TestFileLibTiff(LibTiffTestCase):
],
)
def test_wrong_bits_per_sample(
- self, file_name: str, mode: str, size: tuple[int, int], tile
+ self,
+ file_name: str,
+ mode: str,
+ size: tuple[int, int],
+ tile: list[tuple[str, tuple[int, int, int, int], int, tuple[Any, ...]]],
) -> None:
with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode
@@ -1130,7 +1150,7 @@ class TestFileLibTiff(LibTiffTestCase):
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument:
arguments["strip_size"] = 2**18
- im.save(out, **arguments)
+ im.save(out, "TIFF", **arguments)
with Image.open(out) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py
index a50188700..39b9c60b7 100644
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -226,6 +226,11 @@ def test_eoferror() -> None:
im.seek(n_frames - 1)
+def test_ultra_hdr() -> None:
+ with Image.open("Tests/images/ultrahdr.jpg") as im:
+ assert im.format == "JPEG"
+
+
@pytest.mark.parametrize("test_file", test_files)
def test_image_grab(test_file: str) -> None:
with Image.open(test_file) as im:
diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py
index ab9f9663e..b3f38c3e5 100644
--- a/Tests/test_file_pcx.py
+++ b/Tests/test_file_pcx.py
@@ -76,6 +76,7 @@ def test_pil184() -> None:
def test_1px_width(tmp_path: Path) -> None:
im = Image.new("L", (1, 256))
px = im.load()
+ assert px is not None
for y in range(256):
px[0, y] = y
_roundtrip(tmp_path, im)
@@ -84,6 +85,7 @@ def test_1px_width(tmp_path: Path) -> None:
def test_large_count(tmp_path: Path) -> None:
im = Image.new("L", (256, 1))
px = im.load()
+ assert px is not None
for x in range(256):
px[x, 0] = x // 67 * 67
_roundtrip(tmp_path, im)
@@ -101,6 +103,7 @@ def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) ->
def test_break_in_count_overflow(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
+ assert px is not None
for y in range(4):
for x in range(256):
px[x, y] = x % 128
@@ -110,6 +113,7 @@ def test_break_in_count_overflow(tmp_path: Path) -> None:
def test_break_one_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
+ assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@@ -119,6 +123,7 @@ def test_break_one_in_loop(tmp_path: Path) -> None:
def test_break_many_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
+ assert px is not None
for y in range(4):
for x in range(256):
px[x, y] = x % 128
@@ -130,6 +135,7 @@ def test_break_many_in_loop(tmp_path: Path) -> None:
def test_break_one_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
+ assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@@ -140,6 +146,7 @@ def test_break_one_at_end(tmp_path: Path) -> None:
def test_break_many_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
+ assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@@ -152,6 +159,7 @@ def test_break_many_at_end(tmp_path: Path) -> None:
def test_break_padding(tmp_path: Path) -> None:
im = Image.new("L", (257, 5))
px = im.load()
+ assert px is not None
for y in range(5):
for x in range(257):
px[x, y] = x % 128
diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py
index d39a86565..3729ca58b 100644
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -5,8 +5,9 @@ import os
import os.path
import tempfile
import time
+from collections.abc import Generator
from pathlib import Path
-from typing import Any, Generator
+from typing import Any
import pytest
@@ -117,7 +118,7 @@ def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
im = hopper()
outfile = str(tmp_path / "temp.pdf")
- im.save(outfile, **params)
+ im.save(outfile, "PDF", **params)
with open(outfile, "rb") as fp:
contents = fp.read()
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index c7c9f6fab..dfe8f9e99 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -535,8 +535,10 @@ class TestFilePng:
def test_repr_png(self) -> None:
im = hopper()
+ b = im._repr_png_()
+ assert b is not None
- with Image.open(BytesIO(im._repr_png_())) as repr_png:
+ with Image.open(BytesIO(b)) as repr_png:
assert repr_png.format == "PNG"
assert_image_equal(im, repr_png)
@@ -655,11 +657,12 @@ class TestFilePng:
png.call(cid, 0, 0)
ImageFile.LOAD_TRUNCATED_IMAGES = False
- def test_specify_bits(self, tmp_path: Path) -> None:
+ @pytest.mark.parametrize("save_all", (True, False))
+ def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
im = hopper("P")
out = str(tmp_path / "temp.png")
- im.save(out, bits=4)
+ im.save(out, bits=4, save_all=save_all)
with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 48
@@ -683,6 +686,7 @@ class TestFilePng:
):
assert im.getxmp() == {}
else:
+ assert "xmp" in im.info
xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]
@@ -767,16 +771,12 @@ class TestFilePng:
def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout
- if buffer:
+ class MyStdOut:
+ buffer = BytesIO()
- class MyStdOut:
- buffer = BytesIO()
+ mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
- mystdout = MyStdOut()
- else:
- mystdout = BytesIO()
-
- sys.stdout = mystdout
+ sys.stdout = mystdout # type: ignore[assignment]
with Image.open(TEST_PNG_FILE) as im:
im.save(sys.stdout, "PNG")
@@ -784,7 +784,7 @@ class TestFilePng:
# Reset stdout
sys.stdout = old_stdout
- if buffer:
+ if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py
index 1bfd0434e..d6451ec18 100644
--- a/Tests/test_file_ppm.py
+++ b/Tests/test_file_ppm.py
@@ -368,16 +368,12 @@ def test_mimetypes(tmp_path: Path) -> None:
def test_save_stdout(buffer: bool) -> None:
old_stdout = sys.stdout
- if buffer:
+ class MyStdOut:
+ buffer = BytesIO()
- class MyStdOut:
- buffer = BytesIO()
+ mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
- mystdout = MyStdOut()
- else:
- mystdout = BytesIO()
-
- sys.stdout = mystdout
+ sys.stdout = mystdout # type: ignore[assignment]
with Image.open(TEST_FILE) as im:
im.save(sys.stdout, "PPM")
@@ -385,7 +381,7 @@ def test_save_stdout(buffer: bool) -> None:
# Reset stdout
sys.stdout = old_stdout
- if buffer:
+ if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_FILE)
diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py
index 484a1be8f..e6c79e40b 100644
--- a/Tests/test_file_psd.py
+++ b/Tests/test_file_psd.py
@@ -4,7 +4,7 @@ import warnings
import pytest
-from PIL import Image, PsdImagePlugin, UnidentifiedImageError
+from PIL import Image, PsdImagePlugin
from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy
@@ -150,20 +150,26 @@ def test_combined_larger_than_size() -> None:
@pytest.mark.parametrize(
"test_file,raises",
[
- (
- "Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
- UnidentifiedImageError,
- ),
- (
- "Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
- UnidentifiedImageError,
- ),
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
],
)
-def test_crashes(test_file: str, raises) -> None:
+def test_crashes(test_file: str, raises: type[Exception]) -> None:
with open(test_file, "rb") as f:
with pytest.raises(raises):
with Image.open(f):
pass
+
+
+@pytest.mark.parametrize(
+ "test_file",
+ [
+ "Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
+ "Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
+ ],
+)
+def test_layer_crashes(test_file: str) -> None:
+ with open(test_file, "rb") as f:
+ with Image.open(f) as im:
+ with pytest.raises(SyntaxError):
+ im.layers
diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py
index 9b82a962a..66c88e9d8 100644
--- a/Tests/test_file_spider.py
+++ b/Tests/test_file_spider.py
@@ -105,6 +105,7 @@ def test_load_image_series() -> None:
img_list = SpiderImagePlugin.loadImageSeries(file_list)
# Assert
+ assert img_list is not None
assert len(img_list) == 1
assert isinstance(img_list[0], Image.Image)
assert img_list[0].size == (128, 128)
diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py
index ff6dab00d..a03a6a6e1 100644
--- a/Tests/test_file_tga.py
+++ b/Tests/test_file_tga.py
@@ -72,12 +72,21 @@ def test_palette_depth_8(tmp_path: Path) -> None:
def test_palette_depth_16(tmp_path: Path) -> None:
with Image.open("Tests/images/p_16.tga") as im:
- assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png")
+ assert im.palette.mode == "RGBA"
+ assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png")
out = str(tmp_path / "temp.png")
im.save(out)
with Image.open(out) as reloaded:
- assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png")
+ assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png")
+
+
+def test_rgba_16() -> None:
+ with Image.open("Tests/images/rgba16.tga") as im:
+ assert im.mode == "RGBA"
+
+ assert im.getpixel((0, 0)) == (172, 0, 255, 255)
+ assert im.getpixel((1, 0)) == (0, 255, 82, 0)
def test_id_field() -> None:
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index 06591a29a..8cad25272 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -2,10 +2,10 @@ from __future__ import annotations
import os
import warnings
+from collections.abc import Generator
from io import BytesIO
from pathlib import Path
from types import ModuleType
-from typing import Generator
import pytest
@@ -78,6 +78,7 @@ class TestFileTiff:
def test_seek_after_close(self) -> None:
im = Image.open("Tests/images/multipage.tiff")
+ assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.close()
with pytest.raises(ValueError):
@@ -120,7 +121,7 @@ class TestFileTiff:
def test_set_legacy_api(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
with pytest.raises(Exception) as e:
- ifd.legacy_api = None
+ ifd.legacy_api = False
assert str(e.value) == "Not allowing setting of legacy api"
def test_xyres_tiff(self) -> None:
@@ -424,13 +425,13 @@ class TestFileTiff:
def test_load_float(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdabcd"
- ret = ifd.load_float(data, False)
+ ret = getattr(ifd, "load_float")(data, False)
assert ret == (1.6777999408082104e22, 1.6777999408082104e22)
def test_load_double(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdefghabcdefgh"
- ret = ifd.load_double(data, False)
+ ret = getattr(ifd, "load_double")(data, False)
assert ret == (8.540883223036124e194, 8.540883223036124e194)
def test_ifd_tag_type(self) -> None:
@@ -599,7 +600,7 @@ class TestFileTiff:
def test_with_underscores(self, tmp_path: Path) -> None:
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
filename = str(tmp_path / "temp.tif")
- hopper("RGB").save(filename, **kwargs)
+ hopper("RGB").save(filename, "TIFF", **kwargs)
with Image.open(filename) as im:
# legacy interface
assert im.tag[X_RESOLUTION][0][0] == 72
@@ -621,6 +622,22 @@ class TestFileTiff:
assert_image_equal_tofile(im, tmpfile)
+ def test_iptc(self, tmp_path: Path) -> None:
+ # Do not preserve IPTC_NAA_CHUNK by default if type is LONG
+ outfile = str(tmp_path / "temp.tif")
+ with Image.open("Tests/images/hopper.tif") as im:
+ im.load()
+ assert isinstance(im, TiffImagePlugin.TiffImageFile)
+ ifd = TiffImagePlugin.ImageFileDirectory_v2()
+ ifd[33723] = 1
+ ifd.tagtype[33723] = 4
+ im.tag_v2 = ifd
+ im.save(outfile)
+
+ with Image.open(outfile) as im:
+ assert isinstance(im, TiffImagePlugin.TiffImageFile)
+ assert 33723 not in im.tag_v2
+
def test_rowsperstrip(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
im = hopper()
@@ -759,6 +776,7 @@ class TestFileTiff:
):
assert im.getxmp() == {}
else:
+ assert "xmp" in im.info
xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
index 8b816aa4f..1e0310001 100644
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
from .helper import assert_deep_equal, hopper
-TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()}
+TAG_IDS: dict[str, int] = {
+ info.name: info.value
+ for info in TiffTags.TAGS_V2.values()
+ if info.value is not None
+}
def test_rt_metadata(tmp_path: Path) -> None:
@@ -411,8 +415,8 @@ def test_empty_values() -> None:
info = TiffImagePlugin.ImageFileDirectory_v2(head)
info.load(data)
# Should not raise ValueError.
- info = dict(info)
- assert 33432 in info
+ info_dict = dict(info)
+ assert 33432 in info_dict
def test_photoshop_info(tmp_path: Path) -> None:
diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py
index 1caf032f6..cbc905de4 100644
--- a/Tests/test_file_webp.py
+++ b/Tests/test_file_webp.py
@@ -5,6 +5,7 @@ import re
import sys
import warnings
from pathlib import Path
+from typing import Any
import pytest
@@ -70,7 +71,9 @@ class TestFileWebp:
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0)
- def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None:
+ def _roundtrip(
+ self, tmp_path: Path, mode: str, epsilon: float, args: dict[str, Any] = {}
+ ) -> None:
temp_file = str(tmp_path / "temp.webp")
hopper(mode).save(temp_file, **args)
diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py
index 882dccb32..e0d7999e3 100644
--- a/Tests/test_file_webp_animated.py
+++ b/Tests/test_file_webp_animated.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+from collections.abc import Generator
from pathlib import Path
import pytest
@@ -96,7 +97,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
check(temp_file1)
# Tests appending using a generator
- def im_generator(ims):
+ def im_generator(
+ ims: list[Image.Image],
+ ) -> Generator[Image.Image, None, None]:
yield from ims
temp_file2 = str(tmp_path / "temp_generator.webp")
diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py
index 875941240..c3df4ad7b 100644
--- a/Tests/test_file_webp_metadata.py
+++ b/Tests/test_file_webp_metadata.py
@@ -129,6 +129,7 @@ def test_getxmp() -> None:
):
assert im.getxmp() == {}
else:
+ assert "xmp" in im.info
assert (
im.getxmp()["xmpmeta"]["xmptk"]
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py
index 3fb92a62e..ab8a7f9ec 100644
--- a/Tests/test_font_leaks.py
+++ b/Tests/test_font_leaks.py
@@ -34,7 +34,7 @@ class TestDefaultFontLeak(TestTTypeFontLeak):
def test_leak(self) -> None:
if features.check_module("freetype2"):
- ImageFont.core = _util.DeferredError(ImportError)
+ ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
try:
default_font = ImageFont.load_default()
finally:
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 3ad4d1305..dde7a8d8b 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -8,7 +8,8 @@ import sys
import tempfile
import warnings
from pathlib import Path
-from typing import IO
+from types import ModuleType
+from typing import IO, Any
import pytest
@@ -35,6 +36,12 @@ from .helper import (
skip_unless_feature,
)
+ElementTree: ModuleType | None
+try:
+ from defusedxml import ElementTree
+except ImportError:
+ ElementTree = None
+
# Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
@@ -129,6 +136,15 @@ class TestImage:
assert im.mode == "RGB"
assert im.size == (128, 128)
+ def test_open_verbose_failure(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setattr(Image, "WARN_POSSIBLE_FORMATS", True)
+
+ im = io.BytesIO(b"")
+ with pytest.warns(UserWarning):
+ with pytest.raises(UnidentifiedImageError):
+ with Image.open(im):
+ pass
+
def test_width_height(self) -> None:
im = Image.new("RGB", (1, 2))
assert im.width == 1
@@ -179,11 +195,19 @@ class TestImage:
def test_fp_name(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.jpg")
- class FP:
+ class FP(io.BytesIO):
name: str
- def write(self, b: bytes) -> None:
- pass
+ if sys.version_info >= (3, 12):
+ from collections.abc import Buffer
+
+ def write(self, data: Buffer) -> int:
+ return len(data)
+
+ else:
+
+ def write(self, data: Any) -> int:
+ return len(data)
fp = FP()
fp.name = temp_file
@@ -352,8 +376,9 @@ class TestImage:
img = Image.alpha_composite(dst, src)
# Assert
- img_colors = sorted(img.getcolors())
- assert img_colors == expected_colors
+ img_colors = img.getcolors()
+ assert img_colors is not None
+ assert sorted(img_colors) == expected_colors
def test_alpha_inplace(self) -> None:
src = Image.new("RGBA", (128, 128), "blue")
@@ -397,13 +422,13 @@ class TestImage:
# errors
with pytest.raises(ValueError):
- source.alpha_composite(over, "invalid source")
+ source.alpha_composite(over, "invalid destination") # type: ignore[arg-type]
with pytest.raises(ValueError):
- source.alpha_composite(over, (0, 0), "invalid destination")
+ source.alpha_composite(over, (0, 0), "invalid source") # type: ignore[arg-type]
with pytest.raises(ValueError):
- source.alpha_composite(over, 0)
+ source.alpha_composite(over, 0) # type: ignore[arg-type]
with pytest.raises(ValueError):
- source.alpha_composite(over, (0, 0), 0)
+ source.alpha_composite(over, (0, 0), 0) # type: ignore[arg-type]
with pytest.raises(ValueError):
source.alpha_composite(over, (0, 0), (0, -1))
@@ -555,6 +580,7 @@ class TestImage:
for mode in ("I", "F", "L"):
im = Image.new(mode, (100, 100), (5,))
px = im.load()
+ assert px is not None
assert px[0, 0] == 5
def test_linear_gradient_wrong_mode(self) -> None:
@@ -649,7 +675,9 @@ class TestImage:
im_remapped = im.remap_palette([1, 0])
assert im_remapped.info["transparency"] == 1
- assert len(im_remapped.getpalette()) == 6
+ palette = im_remapped.getpalette()
+ assert palette is not None
+ assert len(palette) == 6
# Test unused transparency
im.info["transparency"] = 2
@@ -680,7 +708,7 @@ class TestImage:
else:
assert new_image.palette is None
- _make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
+ _make_new(im, im_p, ImagePalette.ImagePalette("RGB"))
_make_new(im_p, im, None)
_make_new(im, blank_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, ImagePalette.ImagePalette())
@@ -913,6 +941,25 @@ class TestImage:
assert tag not in exif.get_ifd(0x8769)
assert exif.get_ifd(0xA005)
+ def test_empty_xmp(self) -> None:
+ with Image.open("Tests/images/hopper.gif") as im:
+ assert im.getxmp() == {}
+
+ def test_getxmp_padded(self) -> None:
+ im = Image.new("RGB", (1, 1))
+ im.info["xmp"] = (
+ b'\n'
+ b'\n\x00\x00'
+ )
+ if ElementTree is None:
+ with pytest.warns(
+ UserWarning,
+ match="XMP data cannot be read without defusedxml dependency",
+ ):
+ assert im.getxmp() == {}
+ else:
+ assert im.getxmp() == {"xmpmeta": None}
+
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
im = Image.new("RGB", size)
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
index 8abb1f69f..854c79dae 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -12,19 +12,6 @@ from PIL import Image
from .helper import assert_image_equal, hopper, is_win32
-# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2
-# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670
-cffi: ModuleType | None
-if os.environ.get("PYTHONOPTIMIZE") == "2":
- cffi = None
-else:
- try:
- import cffi
-
- from PIL import PyAccess
- except ImportError:
- cffi = None
-
numpy: ModuleType | None
try:
import numpy
@@ -32,21 +19,7 @@ except ImportError:
numpy = None
-class AccessTest:
- # Initial value
- _init_cffi_access = Image.USE_CFFI_ACCESS
- _need_cffi_access = False
-
- @classmethod
- def setup_class(cls) -> None:
- Image.USE_CFFI_ACCESS = cls._need_cffi_access
-
- @classmethod
- def teardown_class(cls) -> None:
- Image.USE_CFFI_ACCESS = cls._init_cffi_access
-
-
-class TestImagePutPixel(AccessTest):
+class TestImagePutPixel:
def test_sanity(self) -> None:
im1 = hopper()
im2 = Image.new(im1.mode, im1.size, 0)
@@ -54,7 +27,9 @@ class TestImagePutPixel(AccessTest):
for y in range(im1.size[1]):
for x in range(im1.size[0]):
pos = x, y
- im2.putpixel(pos, im1.getpixel(pos))
+ value = im1.getpixel(pos)
+ assert value is not None
+ im2.putpixel(pos, value)
assert_image_equal(im1, im2)
@@ -64,7 +39,9 @@ class TestImagePutPixel(AccessTest):
for y in range(im1.size[1]):
for x in range(im1.size[0]):
pos = x, y
- im2.putpixel(pos, im1.getpixel(pos))
+ value = im1.getpixel(pos)
+ assert value is not None
+ im2.putpixel(pos, value)
assert not im2.readonly
assert_image_equal(im1, im2)
@@ -74,10 +51,12 @@ class TestImagePutPixel(AccessTest):
pix1 = im1.load()
pix2 = im2.load()
+ assert pix1 is not None
+ assert pix2 is not None
with pytest.raises(TypeError):
- pix1[0, "0"]
+ pix1[0, "0"] # type: ignore[index]
with pytest.raises(TypeError):
- pix1["0", 0]
+ pix1["0", 0] # type: ignore[index]
for y in range(im1.size[1]):
for x in range(im1.size[0]):
@@ -96,7 +75,9 @@ class TestImagePutPixel(AccessTest):
for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1):
pos = x, y
- im2.putpixel(pos, im1.getpixel(pos))
+ value = im1.getpixel(pos)
+ assert value is not None
+ im2.putpixel(pos, value)
assert_image_equal(im1, im2)
@@ -106,7 +87,9 @@ class TestImagePutPixel(AccessTest):
for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1):
pos = x, y
- im2.putpixel(pos, im1.getpixel(pos))
+ value = im1.getpixel(pos)
+ assert value is not None
+ im2.putpixel(pos, value)
assert not im2.readonly
assert_image_equal(im1, im2)
@@ -116,6 +99,8 @@ class TestImagePutPixel(AccessTest):
pix1 = im1.load()
pix2 = im2.load()
+ assert pix1 is not None
+ assert pix2 is not None
for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1):
pix2[x, y] = pix1[x, y]
@@ -125,13 +110,14 @@ class TestImagePutPixel(AccessTest):
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
def test_numpy(self) -> None:
im = hopper()
- pix = im.load()
+ px = im.load()
+ assert px is not None
assert numpy is not None
- assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
+ assert px[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
-class TestImageGetPixel(AccessTest):
+class TestImageGetPixel:
@staticmethod
def color(mode: str) -> int | tuple[int, ...]:
bands = Image.getmodebands(mode)
@@ -144,9 +130,6 @@ class TestImageGetPixel(AccessTest):
return tuple(range(1, bands + 1))
def check(self, mode: str, expected_color_int: int | None = None) -> None:
- if self._need_cffi_access and mode.startswith("BGR;"):
- pytest.skip("Support not added to deprecated module for BGR;* modes")
-
expected_color = (
self.color(mode) if expected_color_int is None else expected_color_int
)
@@ -171,15 +154,14 @@ class TestImageGetPixel(AccessTest):
# Check 0x0 image with None initial color
im = Image.new(mode, (0, 0), None)
assert im.load() is not None
- error = ValueError if self._need_cffi_access else IndexError
- with pytest.raises(error):
+ with pytest.raises(IndexError):
im.putpixel((0, 0), expected_color)
- with pytest.raises(error):
+ with pytest.raises(IndexError):
im.getpixel((0, 0))
# Check negative index
- with pytest.raises(error):
+ with pytest.raises(IndexError):
im.putpixel((-1, -1), expected_color)
- with pytest.raises(error):
+ with pytest.raises(IndexError):
im.getpixel((-1, -1))
# Check initial color
@@ -199,10 +181,10 @@ class TestImageGetPixel(AccessTest):
# Check 0x0 image with initial color
im = Image.new(mode, (0, 0), expected_color)
- with pytest.raises(error):
+ with pytest.raises(IndexError):
im.getpixel((0, 0))
# Check negative index
- with pytest.raises(error):
+ with pytest.raises(IndexError):
im.getpixel((-1, -1))
@pytest.mark.parametrize("mode", Image.MODES)
@@ -235,126 +217,7 @@ class TestImageGetPixel(AccessTest):
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
-@pytest.mark.filterwarnings("ignore::DeprecationWarning")
-@pytest.mark.skipif(cffi is None, reason="No CFFI")
-class TestCffiPutPixel(TestImagePutPixel):
- _need_cffi_access = True
-
-
-@pytest.mark.filterwarnings("ignore::DeprecationWarning")
-@pytest.mark.skipif(cffi is None, reason="No CFFI")
-class TestCffiGetPixel(TestImageGetPixel):
- _need_cffi_access = True
-
-
-@pytest.mark.skipif(cffi is None, reason="No CFFI")
-class TestCffi(AccessTest):
- _need_cffi_access = True
-
- def _test_get_access(self, im: Image.Image) -> None:
- """Do we get the same thing as the old pixel access
-
- Using private interfaces, forcing a capi access and
- a pyaccess for the same image"""
- caccess = im.im.pixel_access(False)
- with pytest.warns(DeprecationWarning):
- access = PyAccess.new(im, False)
- assert access is not None
-
- w, h = im.size
- for x in range(0, w, 10):
- for y in range(0, h, 10):
- assert access[(x, y)] == caccess[(x, y)]
-
- # Access an out-of-range pixel
- with pytest.raises(ValueError):
- access[(access.xsize + 1, access.ysize + 1)]
-
- def test_get_vs_c(self) -> None:
- with pytest.warns(DeprecationWarning):
- rgb = hopper("RGB")
- rgb.load()
- self._test_get_access(rgb)
- for mode in ("RGBA", "L", "LA", "1", "P", "F"):
- self._test_get_access(hopper(mode))
-
- for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
- im = Image.new(mode, (10, 10), 40000)
- self._test_get_access(im)
-
- def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None:
- """Are we writing the correct bits into the image?
-
- Using private interfaces, forcing a capi access and
- a pyaccess for the same image"""
- caccess = im.im.pixel_access(False)
- with pytest.warns(DeprecationWarning):
- access = PyAccess.new(im, False)
- assert access is not None
-
- w, h = im.size
- for x in range(0, w, 10):
- for y in range(0, h, 10):
- access[(x, y)] = color
- assert color == caccess[(x, y)]
-
- # Attempt to set the value on a read-only image
- with pytest.warns(DeprecationWarning):
- access = PyAccess.new(im, True)
- assert access is not None
-
- with pytest.raises(ValueError):
- access[(0, 0)] = color
-
- def test_set_vs_c(self) -> None:
- rgb = hopper("RGB")
- with pytest.warns(DeprecationWarning):
- rgb.load()
- self._test_set_access(rgb, (255, 128, 0))
- self._test_set_access(hopper("RGBA"), (255, 192, 128, 0))
- self._test_set_access(hopper("L"), 128)
- self._test_set_access(hopper("LA"), (128, 128))
- self._test_set_access(hopper("1"), 255)
- self._test_set_access(hopper("P"), 128)
- self._test_set_access(hopper("PA"), (128, 128))
- self._test_set_access(hopper("F"), 1024.0)
-
- for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
- im = Image.new(mode, (10, 10), 40000)
- self._test_set_access(im, 45000)
-
- @pytest.mark.filterwarnings("ignore::DeprecationWarning")
- def test_not_implemented(self) -> None:
- assert PyAccess.new(hopper("BGR;15")) is None
-
- # Ref https://github.com/python-pillow/Pillow/pull/2009
- def test_reference_counting(self) -> None:
- size = 10
-
- for _ in range(10):
- # Do not save references to the image, only to the access object
- with pytest.warns(DeprecationWarning):
- px = Image.new("L", (size, 1), 0).load()
- for i in range(size):
- # Pixels can contain garbage if image is released
- assert px[i, 0] == 0
-
- @pytest.mark.parametrize("mode", ("P", "PA"))
- def test_p_putpixel_rgb_rgba(self, mode: str) -> None:
- for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
- im = Image.new(mode, (1, 1))
- with pytest.warns(DeprecationWarning):
- access = PyAccess.new(im, False)
- assert access is not None
-
- access.putpixel((0, 0), color)
-
- if len(color) == 3:
- color += (255,)
- assert im.convert("RGBA").getpixel((0, 0)) == color
-
-
-class TestImagePutPixelError(AccessTest):
+class TestImagePutPixelError:
IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"]
IMAGE_MODES2 = ["L", "I", "I;16"]
INVALID_TYPES = ["foo", 1.0, None]
@@ -364,7 +227,7 @@ class TestImagePutPixelError(AccessTest):
im = hopper(mode)
for v in self.INVALID_TYPES:
with pytest.raises(TypeError, match="color must be int or tuple"):
- im.putpixel((0, 0), v)
+ im.putpixel((0, 0), v) # type: ignore[arg-type]
@pytest.mark.parametrize(
("mode", "band_numbers", "match"),
@@ -398,7 +261,7 @@ class TestImagePutPixelError(AccessTest):
with pytest.raises(
TypeError, match="color must be int or single-element tuple"
):
- im.putpixel((0, 0), v)
+ im.putpixel((0, 0), v) # type: ignore[arg-type]
@pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
def test_putpixel_overflow_error(self, mode: str) -> None:
diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py
index d7e6c562c..bb6064882 100644
--- a/Tests/test_image_array.py
+++ b/Tests/test_image_array.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import Any
+from typing import TYPE_CHECKING, Any
import pytest
from packaging.version import parse as parse_version
@@ -13,13 +13,16 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed")
im = hopper().resize((128, 100))
+if TYPE_CHECKING:
+ import numpy.typing as npt
+
def test_toarray() -> None:
def test(mode: str) -> tuple[tuple[int, ...], str, int]:
ai = numpy.array(im.convert(mode))
return ai.shape, ai.dtype.str, ai.nbytes
- def test_with_dtype(dtype) -> None:
+ def test_with_dtype(dtype: npt.DTypeLike) -> None:
ai = numpy.array(im, dtype=dtype)
assert ai.dtype == dtype
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
index 2fb45854a..ebb7f2822 100644
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -222,8 +222,10 @@ def test_l_macro_rounding(convert_mode: str) -> None:
converted_im = im.convert(convert_mode)
px = converted_im.load()
+ assert px is not None
converted_color = px[0, 0]
if convert_mode == "LA":
+ assert isinstance(converted_color, tuple)
converted_color = converted_color[0]
assert converted_color == 1
diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py
index 1ce1a7cd8..c9d8c93f3 100644
--- a/Tests/test_image_draft.py
+++ b/Tests/test_image_draft.py
@@ -16,7 +16,9 @@ def draft_roundtrip(
im = Image.new(in_mode, in_size)
data = tostring(im, "JPEG")
im = fromstring(data)
- mode, box = im.draft(req_mode, req_size)
+ result = im.draft(req_mode, req_size)
+ assert result is not None
+ box = result[1]
scale, _ = im.decoderconfig
assert box[:2] == (0, 0)
assert (im.width - scale) < box[2] <= im.width
diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py
index 1f0644471..412ab44c3 100644
--- a/Tests/test_image_filter.py
+++ b/Tests/test_image_filter.py
@@ -137,7 +137,7 @@ def test_builtinfilter_p() -> None:
builtin_filter = ImageFilter.BuiltinFilter()
with pytest.raises(ValueError):
- builtin_filter.filter(hopper("P"))
+ builtin_filter.filter(hopper("P").im)
def test_kernel_not_enough_coefficients() -> None:
diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py
index 8f8870f4f..8dbe82b29 100644
--- a/Tests/test_image_getcolors.py
+++ b/Tests/test_image_getcolors.py
@@ -54,17 +54,21 @@ def test_pack() -> None:
assert A is None
A = im.getcolors(maxcolors=3)
+ assert A is not None
A.sort()
assert A == expected
A = im.getcolors(maxcolors=4)
+ assert A is not None
A.sort()
assert A == expected
A = im.getcolors(maxcolors=8)
+ assert A is not None
A.sort()
assert A == expected
A = im.getcolors(maxcolors=16)
+ assert A is not None
A.sort()
assert A == expected
diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py
index 0605821e0..4f1d63b8f 100644
--- a/Tests/test_image_load.py
+++ b/Tests/test_image_load.py
@@ -12,9 +12,10 @@ from .helper import hopper
def test_sanity() -> None:
im = hopper()
- pix = im.load()
+ px = im.load()
- assert pix[0, 0] == (20, 20, 70)
+ assert px is not None
+ assert px[0, 0] == (20, 20, 70)
def test_close() -> None:
diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py
index d8f6b65e0..2cff6c893 100644
--- a/Tests/test_image_paste.py
+++ b/Tests/test_image_paste.py
@@ -14,6 +14,7 @@ class TestImagingPaste:
self, im: Image.Image, expected: list[tuple[int, int, int, int]]
) -> None:
px = im.load()
+ assert px is not None
actual = [
px[0, 0],
px[self.size // 2, 0],
@@ -48,6 +49,7 @@ class TestImagingPaste:
def mask_1(self) -> Image.Image:
mask = Image.new("1", (self.size, self.size))
px = mask.load()
+ assert px is not None
for y in range(mask.height):
for x in range(mask.width):
px[y, x] = (x + y) % 2
@@ -61,6 +63,7 @@ class TestImagingPaste:
def gradient_L(self) -> Image.Image:
gradient = Image.new("L", (self.size, self.size))
px = gradient.load()
+ assert px is not None
for y in range(gradient.height):
for x in range(gradient.width):
px[y, x] = (x + y) % 255
@@ -338,3 +341,8 @@ class TestImagingPaste:
im.copy().paste(im2)
im.copy().paste(im2, (0, 0))
+
+ def test_incorrect_abbreviated_form(self) -> None:
+ im = Image.new("L", (1, 1))
+ with pytest.raises(ValueError):
+ im.paste(im, im, im)
diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py
index 05f209351..a5d5a15db 100644
--- a/Tests/test_image_point.py
+++ b/Tests/test_image_point.py
@@ -61,4 +61,4 @@ def test_f_lut() -> None:
def test_f_mode() -> None:
im = hopper("F")
with pytest.raises(ValueError):
- im.point(None)
+ im.point([])
diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py
index 5e57e4c4c..27cb7c59d 100644
--- a/Tests/test_image_putdata.py
+++ b/Tests/test_image_putdata.py
@@ -31,7 +31,7 @@ def test_sanity() -> None:
def test_long_integers() -> None:
# see bug-200802-systemerror
- def put(value: int) -> tuple[int, int, int, int]:
+ def put(value: int) -> float | tuple[int, ...] | None:
im = Image.new("RGBA", (1, 1))
im.putdata([value])
return im.getpixel((0, 0))
@@ -113,13 +113,13 @@ def test_array_F() -> None:
def test_not_flattened() -> None:
im = Image.new("L", (1, 1))
with pytest.raises(TypeError):
- im.putdata([[0]]) # type: ignore[list-item]
+ im.putdata([[0]])
with pytest.raises(TypeError):
- im.putdata([[0]], 2) # type: ignore[list-item]
+ im.putdata([[0]], 2)
with pytest.raises(TypeError):
im = Image.new("I", (1, 1))
- im.putdata([[0]]) # type: ignore[list-item]
+ im.putdata([[0]])
with pytest.raises(TypeError):
im = Image.new("F", (1, 1))
- im.putdata([[0]]) # type: ignore[list-item]
+ im.putdata([[0]])
diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py
index cc7cf58f0..75d9d2fc1 100644
--- a/Tests/test_image_putpalette.py
+++ b/Tests/test_image_putpalette.py
@@ -79,6 +79,7 @@ def test_putpalette_with_alpha_values() -> None:
(
("RGBA", (1, 2, 3, 4)),
("RGBAX", (1, 2, 3, 4, 0)),
+ ("ARGB", (4, 1, 2, 3)),
),
)
def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:
diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py
index 2d461d985..903cd8550 100644
--- a/Tests/test_image_quantize.py
+++ b/Tests/test_image_quantize.py
@@ -31,7 +31,9 @@ def test_libimagequant_quantize() -> None:
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 15)
- assert len(converted.getcolors()) == 100
+ colors = converted.getcolors()
+ assert colors is not None
+ assert len(colors) == 100
def test_octree_quantize() -> None:
@@ -39,7 +41,9 @@ def test_octree_quantize() -> None:
converted = image.quantize(100, Image.Quantize.FASTOCTREE)
assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 20)
- assert len(converted.getcolors()) == 100
+ colors = converted.getcolors()
+ assert colors is not None
+ assert len(colors) == 100
def test_rgba_quantize() -> None:
@@ -80,6 +84,7 @@ def test_quantize_no_dither2() -> None:
assert tuple(quantized.palette.palette) == data
px = quantized.load()
+ assert px is not None
for x in range(9):
assert px[x, 0] == (0 if x < 5 else 1)
@@ -118,10 +123,12 @@ def test_colors() -> None:
def test_transparent_colors_equal() -> None:
im = Image.new("RGBA", (1, 2), (0, 0, 0, 0))
px = im.load()
+ assert px is not None
px[0, 1] = (255, 255, 255, 0)
converted = im.quantize()
converted_px = converted.load()
+ assert converted_px is not None
assert converted_px[0, 0] == converted_px[0, 1]
@@ -139,6 +146,7 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None:
converted = im.quantize(method=method)
converted_px = converted.load()
+ assert converted_px is not None
assert converted_px[0, 0] == converted.palette.colors[color]
@@ -154,4 +162,6 @@ def test_small_palette() -> None:
im = im.quantize(palette=p)
# Assert
- assert len(im.getcolors()) == 2
+ quantized_colors = im.getcolors()
+ assert quantized_colors is not None
+ assert len(quantized_colors) == 2
diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py
index 9b3bdf330..ce6209c0d 100644
--- a/Tests/test_image_resample.py
+++ b/Tests/test_image_resample.py
@@ -1,7 +1,7 @@
from __future__ import annotations
+from collections.abc import Generator
from contextlib import contextmanager
-from typing import Generator
import pytest
@@ -74,6 +74,7 @@ class TestImagingCoreResampleAccuracy:
data = data.replace(" ", "")
sample = Image.new("L", size)
s_px = sample.load()
+ assert s_px is not None
w, h = size[0] // 2, size[1] // 2
for y in range(h):
for x in range(w):
@@ -87,6 +88,8 @@ class TestImagingCoreResampleAccuracy:
def check_case(self, case: Image.Image, sample: Image.Image) -> None:
s_px = sample.load()
c_px = case.load()
+ assert s_px is not None
+ assert c_px is not None
for y in range(case.size[1]):
for x in range(case.size[0]):
if c_px[x, y] != s_px[x, y]:
@@ -98,6 +101,7 @@ class TestImagingCoreResampleAccuracy:
def serialize_image(self, image: Image.Image) -> str:
s_px = image.load()
+ assert s_px is not None
return "\n".join(
" ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0]))
for y in range(image.size[1])
@@ -233,13 +237,16 @@ class TestImagingCoreResampleAccuracy:
class TestCoreResampleConsistency:
def make_case(
self, mode: str, fill: tuple[int, int, int] | float
- ) -> tuple[Image.Image, tuple[int, ...]]:
+ ) -> tuple[Image.Image, float | tuple[int, ...]]:
im = Image.new(mode, (512, 9), fill)
- return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0]
+ px = im.load()
+ assert px is not None
+ return im.resize((9, 512), Image.Resampling.LANCZOS), px[0, 0]
- def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None:
+ def run_case(self, case: tuple[Image.Image, float | tuple[int, ...]]) -> None:
channel, color = case
px = channel.load()
+ assert px is not None
for x in range(channel.size[0]):
for y in range(channel.size[1]):
if px[x, y] != color:
@@ -249,6 +256,7 @@ class TestCoreResampleConsistency:
def test_8u(self) -> None:
im, color = self.make_case("RGB", (0, 64, 255))
r, g, b = im.split()
+ assert isinstance(color, tuple)
self.run_case((r, color[0]))
self.run_case((g, color[1]))
self.run_case((b, color[2]))
@@ -271,6 +279,7 @@ class TestCoreResampleAlphaCorrect:
def make_levels_case(self, mode: str) -> Image.Image:
i = Image.new(mode, (256, 16))
px = i.load()
+ assert px is not None
for y in range(i.size[1]):
for x in range(i.size[0]):
pix = [x] * len(mode)
@@ -280,8 +289,13 @@ class TestCoreResampleAlphaCorrect:
def run_levels_case(self, i: Image.Image) -> None:
px = i.load()
+ assert px is not None
for y in range(i.size[1]):
- used_colors = {px[x, y][0] for x in range(i.size[0])}
+ used_colors = set()
+ for x in range(i.size[0]):
+ value = px[x, y]
+ assert isinstance(value, tuple)
+ used_colors.add(value[0])
assert 256 == len(used_colors), (
"All colors should be present in resized image. "
f"Only {len(used_colors)} on line {y}."
@@ -310,6 +324,7 @@ class TestCoreResampleAlphaCorrect:
) -> Image.Image:
i = Image.new(mode, (64, 64), dirty_pixel)
px = i.load()
+ assert px is not None
xdiv4 = i.size[0] // 4
ydiv4 = i.size[1] // 4
for y in range(ydiv4 * 2):
@@ -319,14 +334,16 @@ class TestCoreResampleAlphaCorrect:
def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None:
px = i.load()
+ assert px is not None
for y in range(i.size[1]):
for x in range(i.size[0]):
- if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel:
+ value = px[x, y]
+ assert isinstance(value, tuple)
+ if value[-1] != 0 and value[:-1] != clean_pixel:
message = (
- f"pixel at ({x}, {y}) is different:\n"
- f"{px[x, y]}\n{clean_pixel}"
+ f"pixel at ({x}, {y}) is different:\n{value}\n{clean_pixel}"
)
- assert px[x, y][:3] == clean_pixel, message
+ assert value[:3] == clean_pixel, message
def test_dirty_pixels_rgba(self) -> None:
case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0))
@@ -406,6 +423,7 @@ class TestCoreResampleCoefficients:
draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color)
px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load()
+ assert px is not None
if px[2, 0] != test_color // 2:
assert test_color // 2 == px[2, 0]
@@ -445,7 +463,7 @@ class TestCoreResampleBox:
im.resize((32, 32), resample, (20, 20, 100, 20))
with pytest.raises(TypeError, match="must be sequence of length 4"):
- im.resize((32, 32), resample, (im.width, im.height))
+ im.resize((32, 32), resample, (im.width, im.height)) # type: ignore[arg-type]
with pytest.raises(ValueError, match="can't be negative"):
im.resize((32, 32), resample, (-20, 20, 100, 100))
diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py
index 64098f80f..c9e304512 100644
--- a/Tests/test_image_resize.py
+++ b/Tests/test_image_resize.py
@@ -4,9 +4,9 @@ Tests for resize functionality.
from __future__ import annotations
+from collections.abc import Generator
from itertools import permutations
from pathlib import Path
-from typing import Generator
import pytest
@@ -285,14 +285,14 @@ class TestReducingGapResize:
class TestImageResize:
def test_resize(self) -> None:
- def resize(mode: str, size: tuple[int, int]) -> None:
+ def resize(mode: str, size: tuple[int, int] | list[int]) -> None:
out = hopper(mode).resize(size)
assert out.mode == mode
- assert out.size == size
+ assert out.size == tuple(size)
for mode in "1", "P", "L", "RGB", "I", "F":
resize(mode, (112, 103))
- resize(mode, (188, 214))
+ resize(mode, [188, 214])
# Test unknown resampling filter
with hopper() as im:
diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py
index 638d12247..7e83396de 100644
--- a/Tests/test_image_transform.py
+++ b/Tests/test_image_transform.py
@@ -192,8 +192,9 @@ class TestImageTransform:
im = op(im, (40, 10))
- colors = sorted(im.getcolors())
- assert colors == sorted(
+ colors = im.getcolors()
+ assert colors is not None
+ assert sorted(colors) == sorted(
(
(20 * 10, opaque),
(20 * 10, transparent),
diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py
index 7e2290c15..4fc28cdb9 100644
--- a/Tests/test_imagechops.py
+++ b/Tests/test_imagechops.py
@@ -391,23 +391,25 @@ def test_overlay() -> None:
def test_logical() -> None:
def table(
op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int
- ) -> tuple[int, int, int, int]:
+ ) -> list[float]:
out = []
for x in (a, b):
imx = Image.new("1", (1, 1), x)
for y in (a, b):
imy = Image.new("1", (1, 1), y)
- out.append(op(imx, imy).getpixel((0, 0)))
- return tuple(out)
+ value = op(imx, imy).getpixel((0, 0))
+ assert not isinstance(value, tuple) and value is not None
+ out.append(value)
+ return out
- assert table(ImageChops.logical_and, 0, 1) == (0, 0, 0, 255)
- assert table(ImageChops.logical_or, 0, 1) == (0, 255, 255, 255)
- assert table(ImageChops.logical_xor, 0, 1) == (0, 255, 255, 0)
+ assert table(ImageChops.logical_and, 0, 1) == [0, 0, 0, 255]
+ assert table(ImageChops.logical_or, 0, 1) == [0, 255, 255, 255]
+ assert table(ImageChops.logical_xor, 0, 1) == [0, 255, 255, 0]
- assert table(ImageChops.logical_and, 0, 128) == (0, 0, 0, 255)
- assert table(ImageChops.logical_or, 0, 128) == (0, 255, 255, 255)
- assert table(ImageChops.logical_xor, 0, 128) == (0, 255, 255, 0)
+ assert table(ImageChops.logical_and, 0, 128) == [0, 0, 0, 255]
+ assert table(ImageChops.logical_or, 0, 128) == [0, 255, 255, 255]
+ assert table(ImageChops.logical_xor, 0, 128) == [0, 255, 255, 0]
- assert table(ImageChops.logical_and, 0, 255) == (0, 0, 0, 255)
- assert table(ImageChops.logical_or, 0, 255) == (0, 255, 255, 255)
- assert table(ImageChops.logical_xor, 0, 255) == (0, 255, 255, 0)
+ assert table(ImageChops.logical_and, 0, 255) == [0, 0, 0, 255]
+ assert table(ImageChops.logical_or, 0, 255) == [0, 255, 255, 255]
+ assert table(ImageChops.logical_xor, 0, 255) == [0, 255, 255, 0]
diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py
index 55f72c3b9..5ee5fcedf 100644
--- a/Tests/test_imagecms.py
+++ b/Tests/test_imagecms.py
@@ -103,7 +103,7 @@ def test_sanity() -> None:
def test_flags() -> None:
- assert ImageCms.Flags.NONE == 0
+ assert ImageCms.Flags.NONE.value == 0
assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE
assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE
@@ -569,9 +569,9 @@ def assert_aux_channel_preserved(
for delta in nine_grid_deltas:
channel_data.paste(
channel_pattern,
- tuple(
- paste_offset[c] + delta[c] * channel_pattern.size[c]
- for c in range(2)
+ (
+ paste_offset[0] + delta[0] * channel_pattern.size[0],
+ paste_offset[1] + delta[1] * channel_pattern.size[1],
),
)
chans.append(channel_data)
@@ -642,7 +642,8 @@ def test_auxiliary_channels_isolated() -> None:
# convert with and without AUX data, test colors are equal
src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1])
source_profile = ImageCms.createProfile(src_colorSpace)
- destination_profile = ImageCms.createProfile(dst_format[1])
+ dst_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], dst_format[1])
+ destination_profile = ImageCms.createProfile(dst_colorSpace)
source_image = src_format[3]
test_transform = ImageCms.buildTransform(
source_profile,
@@ -678,7 +679,8 @@ def test_auxiliary_channels_isolated() -> None:
def test_long_modes() -> None:
p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc")
- ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI")
+ with pytest.warns(DeprecationWarning):
+ ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI")
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
@@ -689,7 +691,9 @@ def test_rgb_lab(mode: str) -> None:
im = Image.new("LAB", (1, 1), (255, 0, 0))
converted_im = im.convert(mode)
- assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255)
+ value = converted_im.getpixel((0, 0))
+ assert isinstance(value, tuple)
+ assert value[:3] == (0, 255, 255)
def test_deprecation() -> None:
@@ -699,3 +703,9 @@ def test_deprecation() -> None:
assert ImageCms.VERSION == "1.0.0 pil"
with pytest.warns(DeprecationWarning):
assert isinstance(ImageCms.FLAGS, dict)
+
+ profile = ImageCmsProfile(ImageCms.createProfile("sRGB"))
+ with pytest.warns(DeprecationWarning):
+ ImageCms.ImageCmsTransform(profile, profile, "RGBA;16B", "RGB")
+ with pytest.warns(DeprecationWarning):
+ ImageCms.ImageCmsTransform(profile, profile, "RGB", "RGBA;16B")
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index d9593c60f..e397978cb 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -1,8 +1,8 @@
from __future__ import annotations
-import contextlib
import os.path
-from typing import Sequence
+from collections.abc import Sequence
+from typing import Callable
import pytest
@@ -448,6 +448,7 @@ def test_shape1() -> None:
x3, y3 = 95, 5
# Act
+ assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@@ -469,6 +470,7 @@ def test_shape2() -> None:
x3, y3 = 5, 95
# Act
+ assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@@ -487,6 +489,7 @@ def test_transform() -> None:
draw = ImageDraw.Draw(im)
# Act
+ assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.line(0, 0)
s.transform((0, 0, 0, 0, 0, 0))
@@ -631,6 +634,19 @@ def test_polygon(points: Coords) -> None:
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
+@pytest.mark.parametrize("points", POINTS)
+def test_polygon_width_I16(points: Coords) -> None:
+ # Arrange
+ im = Image.new("I;16", (W, H))
+ draw = ImageDraw.Draw(im)
+
+ # Act
+ draw.polygon(points, outline=0xFFFF, width=2)
+
+ # Assert
+ assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon_width_I.tiff")
+
+
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("kite_points", KITE_POINTS)
def test_polygon_kite(
@@ -913,7 +929,12 @@ def test_rounded_rectangle_translucent(
def test_floodfill(bbox: Coords) -> None:
red = ImageColor.getrgb("red")
- for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]:
+ mode_values: list[tuple[str, int | tuple[int, ...]]] = [
+ ("L", 1),
+ ("RGBA", (255, 0, 0, 0)),
+ ("RGB", red),
+ ]
+ for mode, value in mode_values:
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
@@ -1401,25 +1422,44 @@ def test_default_font_size() -> None:
im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im)
- with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
+
+ def check(func: Callable[[], None]) -> None:
+ if freetype_support:
+ func()
+ else:
+ with pytest.raises(ImportError):
+ func()
+
+ def draw_text() -> None:
draw.text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
- with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
+ check(draw_text)
+
+ def draw_textlength() -> None:
assert draw.textlength(text, font_size=16) == 216
- with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
+ check(draw_textlength)
+
+ def draw_textbbox() -> None:
assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
+ check(draw_textbbox)
+
im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im)
- with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
+
+ def draw_multiline_text() -> None:
draw.multiline_text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
- with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
+ check(draw_multiline_text)
+
+ def draw_multiline_textbbox() -> None:
assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
+ check(draw_multiline_textbbox)
+
@pytest.mark.parametrize("bbox", BBOX)
def test_same_color_outline(bbox: Coords) -> None:
@@ -1429,6 +1469,7 @@ def test_same_color_outline(bbox: Coords) -> None:
x2, y2 = 95, 50
x3, y3 = 95, 5
+ assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@@ -1467,7 +1508,7 @@ def test_same_color_outline(bbox: Coords) -> None:
(4, "square", {}),
(8, "regular_octagon", {}),
(4, "square_rotate_45", {"rotation": 45}),
- (3, "triangle_width", {"width": 5, "outline": "yellow"}),
+ (3, "triangle_width", {"outline": "yellow", "width": 5}),
],
)
def test_draw_regular_polygon(
@@ -1477,7 +1518,10 @@ def test_draw_regular_polygon(
filename = f"Tests/images/imagedraw_{polygon_name}.png"
draw = ImageDraw.Draw(im)
bounding_circle = ((W // 2, H // 2), 25)
- draw.regular_polygon(bounding_circle, n_sides, fill="red", **args)
+ rotation = int(args.get("rotation", 0))
+ outline = args.get("outline")
+ width = int(args.get("width", 1))
+ draw.regular_polygon(bounding_circle, n_sides, rotation, "red", outline, width)
assert_image_equal_tofile(im, filename)
@@ -1569,7 +1613,7 @@ def test_compute_regular_polygon_vertices_input_error_handling(
error_message: str,
) -> None:
with pytest.raises(expected_error) as e:
- ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
+ ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
assert str(e.value) == error_message
@@ -1630,6 +1674,6 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None:
draw.rounded_rectangle(xy)
-def test_getdraw():
+def test_getdraw() -> None:
with pytest.warns(DeprecationWarning):
ImageDraw.getdraw(None, [])
diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py
index 3171eb9ae..c80aa739c 100644
--- a/Tests/test_imagedraw2.py
+++ b/Tests/test_imagedraw2.py
@@ -51,9 +51,18 @@ def test_sanity() -> None:
pen = ImageDraw2.Pen("blue", width=7)
draw.line(list(range(10)), pen)
- draw, handler = ImageDraw.getdraw(im)
+ draw2, handler = ImageDraw.getdraw(im)
+ assert draw2 is not None
pen = ImageDraw2.Pen("blue", width=7)
- draw.line(list(range(10)), pen)
+ draw2.line(list(range(10)), pen)
+
+
+def test_mode() -> None:
+ draw = ImageDraw2.Draw("L", (1, 1))
+ assert draw.image.mode == "L"
+
+ with pytest.raises(ValueError):
+ ImageDraw2.Draw("L")
@pytest.mark.parametrize("bbox", BBOX)
diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py
index c9dba2943..bb686bb3b 100644
--- a/Tests/test_imagefile.py
+++ b/Tests/test_imagefile.py
@@ -209,7 +209,7 @@ class MockPyDecoder(ImageFile.PyDecoder):
super().__init__(mode, *args)
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
# eof
return -1, 0
@@ -222,7 +222,7 @@ class MockPyEncoder(ImageFile.PyEncoder):
super().__init__(mode, *args)
- def encode(self, buffer):
+ def encode(self, bufsize: int) -> tuple[int, int, bytes]:
return 1, 1, b""
def cleanup(self) -> None:
@@ -305,7 +305,7 @@ class TestPyDecoder(CodecsTest):
def test_decode(self) -> None:
decoder = ImageFile.PyDecoder(None)
with pytest.raises(NotImplementedError):
- decoder.decode(None)
+ decoder.decode(b"")
class TestPyEncoder(CodecsTest):
@@ -351,7 +351,9 @@ class TestPyEncoder(CodecsTest):
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
)
- assert MockPyEncoder.last.cleanup_called
+ last: MockPyEncoder | None = MockPyEncoder.last
+ assert last
+ assert last.cleanup_called
with pytest.raises(ValueError):
ImageFile._save(
@@ -381,7 +383,7 @@ class TestPyEncoder(CodecsTest):
def test_encode(self) -> None:
encoder = ImageFile.PyEncoder(None)
with pytest.raises(NotImplementedError):
- encoder.encode(None)
+ encoder.encode(0)
bytes_consumed, errcode = encoder.encode_to_pyfd()
assert bytes_consumed == 0
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 73cad513e..9cb420371 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -209,7 +209,7 @@ def test_getlength(
assert length == length_raqm
-def test_float_size() -> None:
+def test_float_size(layout_engine: ImageFont.Layout) -> None:
lengths = []
for size in (48, 48.5, 49):
f = ImageFont.truetype(
@@ -494,8 +494,8 @@ def test_default_font() -> None:
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
-@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
-def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None:
+@pytest.mark.parametrize("mode", ("", "1", "RGBA"))
+def test_getbbox(font: ImageFont.FreeTypeFont, mode: str) -> None:
assert (0, 4, 12, 16) == font.getbbox("A", mode)
@@ -548,7 +548,7 @@ def test_find_font(
def loadable_font(
filepath: str, size: int, index: int, encoding: str, *args: Any
- ):
+ ) -> ImageFont.FreeTypeFont:
_freeTypeFont = getattr(ImageFont, "_FreeTypeFont")
if filepath == path_to_fake:
return _freeTypeFont(FONT_PATH, size, index, encoding, *args)
@@ -564,6 +564,7 @@ def test_find_font(
# catching syntax like errors
monkeypatch.setattr(sys, "platform", platform)
if platform == "linux":
+ monkeypatch.setenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
@@ -1096,6 +1097,23 @@ def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None:
imagefont.getmask("A" * 1_000_001)
+def test_bytes(font: ImageFont.FreeTypeFont) -> None:
+ assert font.getlength(b"test") == font.getlength("test")
+
+ assert font.getbbox(b"test") == font.getbbox("test")
+
+ assert_image_equal(
+ Image.Image()._new(font.getmask(b"test")),
+ Image.Image()._new(font.getmask("test")),
+ )
+
+ assert_image_equal(
+ Image.Image()._new(font.getmask2(b"test")[0]),
+ Image.Image()._new(font.getmask2("test")[0]),
+ )
+ assert font.getmask2(b"test")[1] == font.getmask2("test")[1]
+
+
@pytest.mark.parametrize(
"test_file",
[
diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py
index 3b1c14b4e..695aecbde 100644
--- a/Tests/test_imagefontpil.py
+++ b/Tests/test_imagefontpil.py
@@ -9,51 +9,57 @@ from PIL import Image, ImageDraw, ImageFont, _util, features
from .helper import assert_image_equal_tofile
-original_core = ImageFont.core
+fonts = [ImageFont.load_default_imagefont()]
+if not features.check_module("freetype2"):
+ default_font = ImageFont.load_default()
+ if isinstance(default_font, ImageFont.ImageFont):
+ fonts.append(default_font)
-def setup_module() -> None:
- if features.check_module("freetype2"):
- ImageFont.core = _util.DeferredError(ImportError)
-
-
-def teardown_module() -> None:
- ImageFont.core = original_core
-
-
-def test_default_font() -> None:
+@pytest.mark.parametrize("font", fonts)
+def test_default_font(font: ImageFont.ImageFont) -> None:
# Arrange
txt = 'This is a "better than nothing" default font.'
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
# Act
- default_font = ImageFont.load_default()
- draw.text((10, 10), txt, font=default_font)
+ draw.text((10, 10), txt, font=font)
# Assert
assert_image_equal_tofile(im, "Tests/images/default_font.png")
-def test_size_without_freetype() -> None:
- with pytest.raises(ImportError):
- ImageFont.load_default(size=14)
+def test_without_freetype() -> None:
+ original_core = ImageFont.core
+ if features.check_module("freetype2"):
+ ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
+ try:
+ with pytest.raises(ImportError):
+ ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+
+ assert isinstance(ImageFont.load_default(), ImageFont.ImageFont)
+
+ with pytest.raises(ImportError):
+ ImageFont.load_default(size=14)
+ finally:
+ ImageFont.core = original_core
-def test_unicode() -> None:
+@pytest.mark.parametrize("font", fonts)
+def test_unicode(font: ImageFont.ImageFont) -> None:
# should not segfault, should return UnicodeDecodeError
# issue #2826
- font = ImageFont.load_default()
with pytest.raises(UnicodeEncodeError):
font.getbbox("’")
-def test_textbbox() -> None:
+@pytest.mark.parametrize("font", fonts)
+def test_textbbox(font: ImageFont.ImageFont) -> None:
im = Image.new("RGB", (200, 200))
d = ImageDraw.Draw(im)
- default_font = ImageFont.load_default()
- assert d.textlength("test", font=default_font) == 24
- assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
+ assert d.textlength("test", font=font) == 24
+ assert d.textbbox((0, 0), "test", font=font) == (0, 0, 24, 11)
def test_decompression_bomb() -> None:
diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py
index e23adeb70..5cd510751 100644
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -60,6 +60,8 @@ class TestImageGrab:
def test_grabclipboard(self) -> None:
if sys.platform == "darwin":
subprocess.call(["screencapture", "-cx"])
+
+ ImageGrab.grabclipboard()
elif sys.platform == "win32":
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
p.stdin.write(
@@ -69,6 +71,8 @@ $bmp = New-Object Drawing.Bitmap 200, 200
[Windows.Forms.Clipboard]::SetImage($bmp)"""
)
p.communicate()
+
+ ImageGrab.grabclipboard()
else:
if not shutil.which("wl-paste") and not shutil.which("xclip"):
with pytest.raises(
@@ -77,9 +81,6 @@ $bmp = New-Object Drawing.Bitmap 200, 200
r" ImageGrab.grabclipboard\(\) on Linux",
):
ImageGrab.grabclipboard()
- return
-
- ImageGrab.grabclipboard()
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_file(self) -> None:
@@ -89,6 +90,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
p.communicate()
im = ImageGrab.grabclipboard()
+ assert isinstance(im, list)
assert len(im) == 1
assert os.path.samefile(im[0], "Tests/images/hopper.gif")
@@ -105,6 +107,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
p.communicate()
im = ImageGrab.grabclipboard()
+ assert isinstance(im, Image.Image)
assert_image_equal_tofile(im, "Tests/images/hopper.png")
@pytest.mark.skipif(
@@ -120,6 +123,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
with open(image_path, "rb") as fp:
subprocess.call(["wl-copy"], stdin=fp)
im = ImageGrab.grabclipboard()
+ assert isinstance(im, Image.Image)
assert_image_equal_tofile(im, image_path)
@pytest.mark.skipif(
diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py
index 32615cf0e..4363f456e 100644
--- a/Tests/test_imagemorph.py
+++ b/Tests/test_imagemorph.py
@@ -41,11 +41,15 @@ A = string_to_img(
def img_to_string(im: Image.Image) -> str:
"""Turn a (small) binary image into a string representation"""
chars = ".1"
- width, height = im.size
- return "\n".join(
- "".join(chars[im.getpixel((c, r)) > 0] for c in range(width))
- for r in range(height)
- )
+ result = []
+ for r in range(im.height):
+ line = ""
+ for c in range(im.width):
+ value = im.getpixel((c, r))
+ assert not isinstance(value, tuple) and value is not None
+ line += chars[value > 0]
+ result.append(line)
+ return "\n".join(result)
def img_string_normalize(im: str) -> str:
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
index 27a6090c5..e33e6d4c8 100644
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -165,10 +165,14 @@ def test_pad() -> None:
def test_pad_round() -> None:
im = Image.new("1", (1, 1), 1)
new_im = ImageOps.pad(im, (4, 1))
- assert new_im.load()[2, 0] == 1
+ px = new_im.load()
+ assert px is not None
+ assert px[2, 0] == 1
new_im = ImageOps.pad(im, (1, 4))
- assert new_im.load()[0, 2] == 1
+ px = new_im.load()
+ assert px is not None
+ assert px[0, 2] == 1
@pytest.mark.parametrize("mode", ("P", "PA"))
@@ -223,6 +227,7 @@ def test_expand_palette(border: int | tuple[int, int, int, int]) -> None:
else:
left, top, right, bottom = border
px = im_expanded.convert("RGB").load()
+ assert px is not None
for x in range(im_expanded.width):
for b in range(top):
assert px[x, b] == (255, 0, 0)
@@ -254,20 +259,26 @@ def test_colorize_2color() -> None:
left = (0, 1)
middle = (127, 1)
right = (255, 1)
+ value = im_test.getpixel(left)
+ assert isinstance(value, tuple)
assert_tuple_approx_equal(
- im_test.getpixel(left),
+ value,
(255, 0, 0),
threshold=1,
msg="black test pixel incorrect",
)
+ value = im_test.getpixel(middle)
+ assert isinstance(value, tuple)
assert_tuple_approx_equal(
- im_test.getpixel(middle),
+ value,
(127, 63, 0),
threshold=1,
msg="mid test pixel incorrect",
)
+ value = im_test.getpixel(right)
+ assert isinstance(value, tuple)
assert_tuple_approx_equal(
- im_test.getpixel(right),
+ value,
(0, 127, 0),
threshold=1,
msg="white test pixel incorrect",
@@ -290,20 +301,26 @@ def test_colorize_2color_offset() -> None:
left = (25, 1)
middle = (75, 1)
right = (125, 1)
+ value = im_test.getpixel(left)
+ assert isinstance(value, tuple)
assert_tuple_approx_equal(
- im_test.getpixel(left),
+ value,
(255, 0, 0),
threshold=1,
msg="black test pixel incorrect",
)
+ value = im_test.getpixel(middle)
+ assert isinstance(value, tuple)
assert_tuple_approx_equal(
- im_test.getpixel(middle),
+ value,
(127, 63, 0),
threshold=1,
msg="mid test pixel incorrect",
)
+ value = im_test.getpixel(right)
+ assert isinstance(value, tuple)
assert_tuple_approx_equal(
- im_test.getpixel(right),
+ value,
(0, 127, 0),
threshold=1,
msg="white test pixel incorrect",
@@ -334,29 +351,37 @@ def test_colorize_3color_offset() -> None:
middle = (100, 1)
right_middle = (150, 1)
right = (225, 1)
+ value = im_test.getpixel(left)
+ assert isinstance(value, tuple)
assert_tuple_approx_equal(
- im_test.getpixel(left),
+ value,
(255, 0, 0),
threshold=1,
msg="black test pixel incorrect",
)
+ value = im_test.getpixel(left_middle)
+ assert isinstance(value, tuple)
assert_tuple_approx_equal(
- im_test.getpixel(left_middle),
+ value,
(127, 0, 127),
threshold=1,
msg="low-mid test pixel incorrect",
)
+ value = im_test.getpixel(middle)
+ assert isinstance(value, tuple)
+ assert_tuple_approx_equal(value, (0, 0, 255), threshold=1, msg="mid incorrect")
+ value = im_test.getpixel(right_middle)
+ assert isinstance(value, tuple)
assert_tuple_approx_equal(
- im_test.getpixel(middle), (0, 0, 255), threshold=1, msg="mid incorrect"
- )
- assert_tuple_approx_equal(
- im_test.getpixel(right_middle),
+ value,
(0, 63, 127),
threshold=1,
msg="high-mid test pixel incorrect",
)
+ value = im_test.getpixel(right)
+ assert isinstance(value, tuple)
assert_tuple_approx_equal(
- im_test.getpixel(right),
+ value,
(0, 127, 0),
threshold=1,
msg="white test pixel incorrect",
@@ -432,6 +457,17 @@ def test_exif_transpose() -> None:
assert 0x0112 not in transposed_im.getexif()
+def test_exif_transpose_xml_without_xmp() -> None:
+ with Image.open("Tests/images/xmp_tags_orientation.png") as im:
+ assert im.getexif()[0x0112] == 3
+ assert "XML:com.adobe.xmp" in im.info
+
+ del im.info["xmp"]
+ transposed_im = ImageOps.exif_transpose(im)
+ assert transposed_im is not None
+ assert 0x0112 not in transposed_im.getexif()
+
+
def test_exif_transpose_in_place() -> None:
with Image.open("Tests/images/orientation_rectangle.jpg") as im:
assert im.size == (2, 1)
diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py
index dbdd5b317..920012d86 100644
--- a/Tests/test_imageops_usm.py
+++ b/Tests/test_imageops_usm.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import Generator
+from collections.abc import Generator
import pytest
@@ -101,7 +101,7 @@ def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None:
assert i.im.getpixel((x, y))[c] >= 250
# Fuzzy match.
- def gp(x, y):
+ def gp(x: int, y: int) -> tuple[int, ...]:
return i.im.getpixel((x, y))
assert 236 <= gp(7, 4)[0] <= 239
diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py
index 8e2db15aa..6cf0079dd 100644
--- a/Tests/test_imagepalette.py
+++ b/Tests/test_imagepalette.py
@@ -45,7 +45,7 @@ def test_getcolor() -> None:
# Test unknown color specifier
with pytest.raises(ValueError):
- palette.getcolor("unknown")
+ palette.getcolor("unknown") # type: ignore[arg-type]
def test_getcolor_rgba_color_rgb_palette() -> None:
@@ -88,13 +88,13 @@ def test_file(tmp_path: Path) -> None:
palette.save(f)
- p = ImagePalette.load(f)
+ lut = ImagePalette.load(f)
# load returns raw palette information
- assert len(p[0]) == 768
- assert p[1] == "RGB"
+ assert len(lut[0]) == 768
+ assert lut[1] == "RGB"
- p = ImagePalette.raw(p[1], p[0])
+ p = ImagePalette.raw(lut[1], lut[0])
assert isinstance(p, ImagePalette.ImagePalette)
assert p.palette == palette.tobytes()
diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py
index 9487560af..cda2584e7 100644
--- a/Tests/test_imagepath.py
+++ b/Tests/test_imagepath.py
@@ -3,7 +3,7 @@ from __future__ import annotations
import array
import math
import struct
-from typing import Sequence
+from collections.abc import Sequence
import pytest
diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py
index 88ad1f9ee..22cd674ce 100644
--- a/Tests/test_imageqt.py
+++ b/Tests/test_imageqt.py
@@ -41,18 +41,13 @@ def test_rgb() -> None:
checkrgb(0, 0, 255)
-def test_image() -> None:
- modes = ["1", "RGB", "RGBA", "L", "P"]
- qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage
- if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+
- modes.append("I;16")
-
- for mode in modes:
- im = hopper(mode)
- roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
- if mode not in ("RGB", "RGBA"):
- im = im.convert("RGB")
- assert_image_similar(roundtripped_im, im, 1)
+@pytest.mark.parametrize("mode", ("1", "RGB", "RGBA", "L", "P", "I;16"))
+def test_image(mode: str) -> None:
+ im = hopper(mode)
+ roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
+ if mode not in ("RGB", "RGBA"):
+ im = im.convert("RGB")
+ assert_image_similar(roundtripped_im, im, 1)
def test_closed_file() -> None:
diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py
index 4e9291fbb..a4f7e5cc5 100644
--- a/Tests/test_imageshow.py
+++ b/Tests/test_imageshow.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import os
from typing import Any
import pytest
@@ -15,8 +16,11 @@ def test_sanity() -> None:
def test_register() -> None:
- # Test registering a viewer that is not a class
- ImageShow.register("not a class")
+ # Test registering a viewer that is an instance
+ class TestViewer(ImageShow.Viewer):
+ pass
+
+ ImageShow.register(TestViewer())
# Restore original state
ImageShow._viewers.pop()
@@ -65,6 +69,27 @@ def test_show_without_viewers() -> None:
ImageShow._viewers = viewers
+@pytest.mark.parametrize(
+ "viewer",
+ (
+ ImageShow.Viewer(),
+ ImageShow.WindowsViewer(),
+ ImageShow.MacViewer(),
+ ImageShow.XDGViewer(),
+ ImageShow.DisplayViewer(),
+ ImageShow.GmDisplayViewer(),
+ ImageShow.EogViewer(),
+ ImageShow.XVViewer(),
+ ImageShow.IPythonViewer(),
+ ),
+)
+def test_show_file(viewer: ImageShow.Viewer) -> None:
+ assert not os.path.exists("missing.png")
+
+ with pytest.raises(FileNotFoundError):
+ viewer.show_file("missing.png")
+
+
def test_viewer() -> None:
viewer = ImageShow.Viewer()
diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py
index b607b8c43..f84c6c03a 100644
--- a/Tests/test_imagetk.py
+++ b/Tests/test_imagetk.py
@@ -45,10 +45,12 @@ def test_kw() -> None:
# Test "file"
im = ImageTk._get_image_from_kw(kw)
+ assert im is not None
assert_image_equal(im, im1)
# Test "data"
im = ImageTk._get_image_from_kw(kw)
+ assert im is not None
assert_image_equal(im, im2)
# Test no relevant entry
@@ -70,6 +72,11 @@ def test_photoimage(mode: str) -> None:
reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded, im.convert("RGBA"))
+ with pytest.raises(ValueError):
+ ImageTk.PhotoImage()
+ with pytest.raises(ValueError):
+ ImageTk.PhotoImage(mode)
+
def test_photoimage_apply_transparency() -> None:
with Image.open("Tests/images/pil123p.png") as im:
diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py
index e6c312a0c..c23a5c690 100644
--- a/Tests/test_imagewin_pointers.py
+++ b/Tests/test_imagewin_pointers.py
@@ -45,21 +45,22 @@ if is_win32():
memcpy = ctypes.cdll.msvcrt.memcpy
memcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]
- CreateCompatibleDC = ctypes.windll.gdi32.CreateCompatibleDC
+ windll = getattr(ctypes, "windll")
+ CreateCompatibleDC = windll.gdi32.CreateCompatibleDC
CreateCompatibleDC.argtypes = [ctypes.wintypes.HDC]
CreateCompatibleDC.restype = ctypes.wintypes.HDC
- DeleteDC = ctypes.windll.gdi32.DeleteDC
+ DeleteDC = windll.gdi32.DeleteDC
DeleteDC.argtypes = [ctypes.wintypes.HDC]
- SelectObject = ctypes.windll.gdi32.SelectObject
+ SelectObject = windll.gdi32.SelectObject
SelectObject.argtypes = [ctypes.wintypes.HDC, ctypes.wintypes.HGDIOBJ]
SelectObject.restype = ctypes.wintypes.HGDIOBJ
- DeleteObject = ctypes.windll.gdi32.DeleteObject
+ DeleteObject = windll.gdi32.DeleteObject
DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ]
- CreateDIBSection = ctypes.windll.gdi32.CreateDIBSection
+ CreateDIBSection = windll.gdi32.CreateDIBSection
CreateDIBSection.argtypes = [
ctypes.wintypes.HDC,
ctypes.c_void_p,
diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py
index 1b01f95ce..e26f5d283 100644
--- a/Tests/test_mode_i16.py
+++ b/Tests/test_mode_i16.py
@@ -16,6 +16,8 @@ def verify(im1: Image.Image) -> None:
assert im1.size == im2.size
pix1 = im1.load()
pix2 = im2.load()
+ assert pix1 is not None
+ assert pix2 is not None
for y in range(im1.size[1]):
for x in range(im1.size[0]):
xy = x, y
diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py
index 36cdb3682..312e32e0c 100644
--- a/Tests/test_numpy.py
+++ b/Tests/test_numpy.py
@@ -1,17 +1,17 @@
from __future__ import annotations
import warnings
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
import pytest
-from PIL import Image
+from PIL import Image, _typing
from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature
if TYPE_CHECKING:
import numpy
- import numpy.typing
+ import numpy.typing as npt
else:
numpy = pytest.importorskip("numpy", reason="NumPy not installed")
@@ -19,9 +19,7 @@ TEST_IMAGE_SIZE = (10, 10)
def test_numpy_to_image() -> None:
- def to_image(
- dtype: numpy.typing.DTypeLike, bands: int = 1, boolean: int = 0
- ) -> Image.Image:
+ def to_image(dtype: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Image:
if bands == 1:
if boolean:
data = [0, 255] * 50
@@ -106,13 +104,12 @@ def test_1d_array() -> None:
assert_image(Image.fromarray(a), "L", (1, 5))
-def _test_img_equals_nparray(
- img: Image.Image, np_img: numpy.typing.NDArray[Any]
-) -> None:
+def _test_img_equals_nparray(img: Image.Image, np_img: _typing.NumpyArray) -> None:
assert len(np_img.shape) >= 2
np_size = np_img.shape[1], np_img.shape[0]
assert img.size == np_size
px = img.load()
+ assert px is not None
for x in range(0, img.size[0], int(img.size[0] / 10)):
for y in range(0, img.size[1], int(img.size[1] / 10)):
assert_deep_equal(px[x, y], np_img[y, x])
@@ -145,6 +142,7 @@ def test_save_tiff_uint16() -> None:
img = Image.fromarray(a)
img_px = img.load()
+ assert img_px is not None
assert img_px[0, 0] == pixel_value
@@ -166,7 +164,7 @@ def test_save_tiff_uint16() -> None:
("HSV", numpy.uint8),
),
)
-def test_to_array(mode: str, dtype: numpy.typing.DTypeLike) -> None:
+def test_to_array(mode: str, dtype: npt.DTypeLike) -> None:
img = hopper(mode)
# Resize to non-square
@@ -200,6 +198,15 @@ def test_putdata() -> None:
assert len(im.getdata()) == len(arr)
+def test_resize() -> None:
+ im = hopper()
+ size = (64, 64)
+
+ im_resized = im.resize(numpy.array(size))
+
+ assert im_resized.size == size
+
+
@pytest.mark.parametrize(
"dtype",
(
@@ -216,7 +223,7 @@ def test_putdata() -> None:
numpy.float64,
),
)
-def test_roundtrip_eye(dtype: numpy.typing.DTypeLike) -> None:
+def test_roundtrip_eye(dtype: npt.DTypeLike) -> None:
arr = numpy.eye(10, dtype=dtype)
numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr)))
diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py
index 64dfb2c95..130ffa863 100644
--- a/Tests/test_psdraw.py
+++ b/Tests/test_psdraw.py
@@ -54,16 +54,12 @@ def test_stdout(buffer: bool) -> None:
# Temporarily redirect stdout
old_stdout = sys.stdout
- if buffer:
+ class MyStdOut:
+ buffer = BytesIO()
- class MyStdOut:
- buffer = BytesIO()
+ mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
- mystdout = MyStdOut()
- else:
- mystdout = BytesIO()
-
- sys.stdout = mystdout
+ sys.stdout = mystdout # type: ignore[assignment]
ps = PSDraw.PSDraw()
_create_document(ps)
@@ -71,6 +67,6 @@ def test_stdout(buffer: bool) -> None:
# Reset stdout
sys.stdout = old_stdout
- if buffer:
+ if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
assert mystdout.getvalue() != b""
diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py
index ae80b98b8..9d06a9332 100644
--- a/Tests/test_tiff_ifdrational.py
+++ b/Tests/test_tiff_ifdrational.py
@@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
from .helper import hopper, skip_unless_feature
-def _test_equal(num, denom, target) -> None:
+def _test_equal(
+ num: float | Fraction | IFDRational,
+ denom: int,
+ target: float | Fraction | IFDRational,
+) -> None:
t = IFDRational(num, denom)
assert target == t
diff --git a/codecov.yml b/codecov.yml
index 1ea7974eb..8646576bb 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -17,6 +17,5 @@ coverage:
# Matches 'omit:' in .coveragerc
ignore:
- "Tests/32bit_segfault_check.py"
- - "Tests/bench_cffi_access.py"
- "Tests/check_*.py"
- "Tests/createfontdatachunk.py"
diff --git a/docs/conf.py b/docs/conf.py
index f12b30e65..41f9c0e38 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -338,7 +338,7 @@ linkcheck_allowed_redirects = {
# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html
_repo = "https://github.com/python-pillow/Pillow/"
extlinks = {
- "cve": ("https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-%s", "CVE-%s"),
+ "cve": ("https://www.cve.org/CVERecord?id=CVE-%s", "CVE-%s"),
"cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
"issue": (_repo + "issues/%s", "#%s"),
"pr": (_repo + "pull/%s", "#%s"),
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index 627672e1f..792fd1c70 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -12,28 +12,6 @@ Deprecated features
Below are features which are considered deprecated. Where appropriate,
a :py:exc:`DeprecationWarning` is issued.
-PSFile
-~~~~~~
-
-.. deprecated:: 9.5.0
-
-The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will
-be removed in Pillow 11 (2024-10-15). This class was only made as a helper to
-be used internally, so there is no replacement. If you need this functionality
-though, it is a very short class that can easily be recreated in your own code.
-
-PyAccess and Image.USE_CFFI_ACCESS
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. deprecated:: 10.0.0
-
-Since Pillow's C API is now faster than PyAccess on PyPy,
-:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow
-11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead.
-
-``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is
-similarly deprecated.
-
ImageFile.raise_oserror
~~~~~~~~~~~~~~~~~~~~~~~
@@ -107,6 +85,15 @@ BGR;15, BGR 16 and BGR;24
The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated.
+Non-image modes in ImageCms
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. deprecated:: 10.4.0
+
+The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow
+image modes has been deprecated. Defaulting to "L" or "1" if the mode cannot be mapped
+is also deprecated.
+
Support for LibTIFF earlier than 4
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -128,6 +115,29 @@ Removed features
Deprecated features are only removed in major releases after an appropriate
period of deprecation has passed.
+PSFile
+~~~~~~
+
+.. deprecated:: 9.5.0
+.. versionremoved:: 11.0.0
+
+The :py:class:`!PSFile` class was removed in Pillow 11 (2024-10-15).
+This class was only made as a helper to be used internally,
+so there is no replacement. If you need this functionality though,
+it is a very short class that can easily be recreated in your own code.
+
+PyAccess and Image.USE_CFFI_ACCESS
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 10.0.0
+.. versionremoved:: 11.0.0
+
+Since Pillow's C API is now faster than PyAccess on PyPy, ``PyAccess`` has been
+removed. Pillow's C API will now be used on PyPy instead.
+
+``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was
+similarly removed.
+
Tk/Tcl 8.4
~~~~~~~~~~
@@ -286,7 +296,7 @@ Previously, the ``size`` methods returned a ``height`` that included the vertica
offset of the text, while the new ``bbox`` methods distinguish this as a ``top``
offset.
-.. image:: ./example/size_vs_bbox.png
+.. image:: ./example/size_vs_bbox.webp
:alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself.
:align: center
diff --git a/docs/example/anchors.png b/docs/example/anchors.png
deleted file mode 100644
index 40476b092..000000000
Binary files a/docs/example/anchors.png and /dev/null differ
diff --git a/docs/example/anchors.py b/docs/example/anchors.py
index b5d76b4fe..2ee11103f 100644
--- a/docs/example/anchors.py
+++ b/docs/example/anchors.py
@@ -26,5 +26,5 @@ if __name__ == "__main__":
d.line(((x * 200, y * 100), (x * 200, (y + 1) * 100)), "black", 3)
if y != 0:
d.line(((x * 200, y * 100), ((x + 1) * 200, y * 100)), "black", 3)
- im.save("docs/example/anchors.png")
+ im.save("docs/example/anchors.webp")
im.show()
diff --git a/docs/example/anchors.webp b/docs/example/anchors.webp
new file mode 100644
index 000000000..216b6c235
Binary files /dev/null and b/docs/example/anchors.webp differ
diff --git a/docs/example/image_thumbnail.png b/docs/example/image_thumbnail.png
deleted file mode 100644
index 293b05794..000000000
Binary files a/docs/example/image_thumbnail.png and /dev/null differ
diff --git a/docs/example/image_thumbnail.webp b/docs/example/image_thumbnail.webp
new file mode 100644
index 000000000..9780f2852
Binary files /dev/null and b/docs/example/image_thumbnail.webp differ
diff --git a/docs/example/imageops_contain.png b/docs/example/imageops_contain.png
deleted file mode 100644
index 293b05794..000000000
Binary files a/docs/example/imageops_contain.png and /dev/null differ
diff --git a/docs/example/imageops_contain.webp b/docs/example/imageops_contain.webp
new file mode 100644
index 000000000..9780f2852
Binary files /dev/null and b/docs/example/imageops_contain.webp differ
diff --git a/docs/example/imageops_cover.png b/docs/example/imageops_cover.png
deleted file mode 100644
index 929e1d874..000000000
Binary files a/docs/example/imageops_cover.png and /dev/null differ
diff --git a/docs/example/imageops_cover.webp b/docs/example/imageops_cover.webp
new file mode 100644
index 000000000..a0b6c10bf
Binary files /dev/null and b/docs/example/imageops_cover.webp differ
diff --git a/docs/example/imageops_fit.png b/docs/example/imageops_fit.png
deleted file mode 100644
index 13a3d5e3f..000000000
Binary files a/docs/example/imageops_fit.png and /dev/null differ
diff --git a/docs/example/imageops_fit.webp b/docs/example/imageops_fit.webp
new file mode 100644
index 000000000..803ee66d2
Binary files /dev/null and b/docs/example/imageops_fit.webp differ
diff --git a/docs/example/imageops_pad.png b/docs/example/imageops_pad.png
deleted file mode 100644
index 69649d6e5..000000000
Binary files a/docs/example/imageops_pad.png and /dev/null differ
diff --git a/docs/example/imageops_pad.webp b/docs/example/imageops_pad.webp
new file mode 100644
index 000000000..0ab63ef42
Binary files /dev/null and b/docs/example/imageops_pad.webp differ
diff --git a/docs/example/size_vs_bbox.png b/docs/example/size_vs_bbox.png
deleted file mode 100644
index 11a05d2a8..000000000
Binary files a/docs/example/size_vs_bbox.png and /dev/null differ
diff --git a/docs/example/size_vs_bbox.webp b/docs/example/size_vs_bbox.webp
new file mode 100644
index 000000000..391162d2d
Binary files /dev/null and b/docs/example/size_vs_bbox.webp differ
diff --git a/docs/handbook/text-anchors.rst b/docs/handbook/text-anchors.rst
index 3a9572ab2..48de4bc95 100644
--- a/docs/handbook/text-anchors.rst
+++ b/docs/handbook/text-anchors.rst
@@ -132,7 +132,7 @@ of the two lines.
.. comment: Image generated with ../example/anchors.py
-.. image:: ../example/anchors.png
+.. image:: ../example/anchors.webp
:alt: Text anchor examples
:align: center
diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst
index 523e2ad74..6cb1e2639 100644
--- a/docs/handbook/tutorial.rst
+++ b/docs/handbook/tutorial.rst
@@ -278,26 +278,26 @@ choose to resize relative to a given size.
from PIL import Image, ImageOps
size = (100, 150)
- with Image.open("Tests/images/hopper.png") as im:
- ImageOps.contain(im, size).save("imageops_contain.png")
- ImageOps.cover(im, size).save("imageops_cover.png")
- ImageOps.fit(im, size).save("imageops_fit.png")
- ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
+ with Image.open("Tests/images/hopper.webp") as im:
+ ImageOps.contain(im, size).save("imageops_contain.webp")
+ ImageOps.cover(im, size).save("imageops_cover.webp")
+ ImageOps.fit(im, size).save("imageops_fit.webp")
+ ImageOps.pad(im, size, color="#f00").save("imageops_pad.webp")
# thumbnail() can also be used,
# but will modify the image object in place
im.thumbnail(size)
- im.save("imageops_thumbnail.png")
+ im.save("image_thumbnail.webp")
-+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
-| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
-+================+===========================================+============================================+==========================================+========================================+========================================+
-|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
-+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
-|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
-+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
-|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
-+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
++----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
+| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
++================+============================================+=============================================+===========================================+=========================================+=========================================+
+|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
++----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
+|Resulting image | .. image:: ../example/image_thumbnail.webp | .. image:: ../example/imageops_contain.webp | .. image:: ../example/imageops_cover.webp | .. image:: ../example/imageops_fit.webp | .. image:: ../example/imageops_pad.webp |
++----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
+|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
++----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
.. _color-transforms:
diff --git a/docs/installation/basic-installation.rst b/docs/installation/basic-installation.rst
index 01981aa4f..989f72ddd 100644
--- a/docs/installation/basic-installation.rst
+++ b/docs/installation/basic-installation.rst
@@ -68,8 +68,8 @@ and :pypi:`olefile` for Pillow to read FPX and MIC images::
.. tab:: Windows
We provide Pillow binaries for Windows compiled for the matrix of supported
- Pythons in the wheel format. These include x86, x86-64 and arm64 versions
- (with the exception of Python 3.8 on arm64). These binaries include support
+ Pythons in the wheel format. These include x86, x86-64 and arm64 versions.
+ These binaries include support
for all optional libraries except libimagequant and libxcb. Raqm support
requires FriBiDi to be installed separately::
diff --git a/docs/installation/newer-versions.csv b/docs/installation/newer-versions.csv
index e21caf520..19816af58 100644
--- a/docs/installation/newer-versions.csv
+++ b/docs/installation/newer-versions.csv
@@ -1,8 +1,9 @@
-Python,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5
-Pillow >= 10.1,Yes,Yes,Yes,Yes,Yes,,,
-Pillow 10.0,,Yes,Yes,Yes,Yes,,,
-Pillow 9.3 - 9.5,,Yes,Yes,Yes,Yes,Yes,,
-Pillow 9.0 - 9.2,,,Yes,Yes,Yes,Yes,,
-Pillow 8.3.2 - 8.4,,,Yes,Yes,Yes,Yes,Yes,
-Pillow 8.0 - 8.3.1,,,,Yes,Yes,Yes,Yes,
-Pillow 7.0 - 7.2,,,,,Yes,Yes,Yes,Yes
+Python,3.13,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5
+Pillow >= 11,Yes,Yes,Yes,Yes,Yes,,,,
+Pillow 10.1 - 10.4,,Yes,Yes,Yes,Yes,Yes,,,
+Pillow 10.0,,,Yes,Yes,Yes,Yes,,,
+Pillow 9.3 - 9.5,,,Yes,Yes,Yes,Yes,Yes,,
+Pillow 9.0 - 9.2,,,,Yes,Yes,Yes,Yes,,
+Pillow 8.3.2 - 8.4,,,,Yes,Yes,Yes,Yes,Yes,
+Pillow 8.0 - 8.3.1,,,,,Yes,Yes,Yes,Yes,
+Pillow 7.0 - 7.2,,,,,,Yes,Yes,Yes,Yes
diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst
index ed25d33a6..f2ef9cacb 100644
--- a/docs/installation/platform-support.rst
+++ b/docs/installation/platform-support.rst
@@ -27,8 +27,6 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 9 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Debian 11 Bullseye | 3.9 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 39 | 3.12 | x86-64 |
@@ -37,14 +35,12 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| macOS 13 Ventura | 3.8, 3.9 | x86-64 |
+| macOS 13 Ventura | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
| | PyPy3 | |
+----------------------------------+----------------------------+---------------------+
-| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 |
+| Ubuntu Linux 22.04 LTS (Jammy) | 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | |
| +----------------------------+---------------------+
| | 3.10 | arm64v8 |
@@ -52,16 +48,16 @@ These platforms are built and tested for every change.
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
| | | s390x |
+----------------------------------+----------------------------+---------------------+
-| Windows Server 2016 | 3.8 | x86-64 |
+| Windows Server 2016 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 |
+| Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 | x86 |
| +----------------------------+---------------------+
| | 3.9 (MinGW) | x86-64 |
| +----------------------------+---------------------+
-| | 3.8, 3.9 (Cygwin) | x86-64 |
+| | 3.9 (Cygwin) | x86-64 |
+----------------------------------+----------------------------+---------------------+
@@ -79,7 +75,7 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+============================+==================+==============+
-| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.3.0 |arm |
+| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm |
+----------------------------------+----------------------------+------------------+--------------+
| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
| +----------------------------+------------------+ |
diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst
index 1c095a114..66c5e5422 100644
--- a/docs/reference/Image.rst
+++ b/docs/reference/Image.rst
@@ -195,6 +195,7 @@ This helps to get the bounding box coordinates of the input image::
.. automethod:: PIL.Image.Image.getpalette
.. automethod:: PIL.Image.Image.getpixel
.. automethod:: PIL.Image.Image.getprojection
+.. automethod:: PIL.Image.Image.getxmp
.. automethod:: PIL.Image.Image.histogram
.. automethod:: PIL.Image.Image.paste
.. automethod:: PIL.Image.Image.point
@@ -380,6 +381,11 @@ Constants
Set to 89,478,485, approximately 0.25GB for a 24-bit (3 bpp) image.
See :py:meth:`~PIL.Image.open` for more information about how this is used.
+.. data:: WARN_POSSIBLE_FORMATS
+
+ Set to false. If true, when an image cannot be identified, warnings will be raised
+ from formats that attempted to read the data.
+
Transpose methods
^^^^^^^^^^^^^^^^^
diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst
index 1404869ca..3e9aa73f8 100644
--- a/docs/reference/ImageDraw.rst
+++ b/docs/reference/ImageDraw.rst
@@ -691,23 +691,7 @@ Methods
:param hints: An optional list of hints.
:returns: A (drawing context, drawing resource factory) tuple.
-.. py:method:: floodfill(image, xy, value, border=None, thresh=0)
-
- .. warning:: This method is experimental.
-
- Fills a bounded region with a given color.
-
- :param image: Target image.
- :param xy: Seed position (a 2-item coordinate tuple).
- :param value: Fill color.
- :param border: Optional border value. If given, the region consists of
- pixels with a color different from the border color. If not given,
- the region consists of pixels having the same color as the seed
- pixel.
- :param thresh: Optional threshold value which specifies a maximum
- tolerable difference of a pixel value from the 'background' in
- order for it to be replaced. Useful for filling regions of non-
- homogeneous, but similar, colors.
+.. autofunction:: PIL.ImageDraw.floodfill
.. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/
.. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst
index 6edf4b05c..edbdd9a32 100644
--- a/docs/reference/ImageFont.rst
+++ b/docs/reference/ImageFont.rst
@@ -53,6 +53,7 @@ Functions
.. autofunction:: PIL.ImageFont.load_path
.. autofunction:: PIL.ImageFont.truetype
.. autofunction:: PIL.ImageFont.load_default
+.. autofunction:: PIL.ImageFont.load_default_imagefont
Methods
-------
diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst
index 051fdcfc9..fcaa3c8f6 100644
--- a/docs/reference/ImageOps.rst
+++ b/docs/reference/ImageOps.rst
@@ -36,26 +36,26 @@ Resize relative to a given size
from PIL import Image, ImageOps
size = (100, 150)
- with Image.open("Tests/images/hopper.png") as im:
- ImageOps.contain(im, size).save("imageops_contain.png")
- ImageOps.cover(im, size).save("imageops_cover.png")
- ImageOps.fit(im, size).save("imageops_fit.png")
- ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
+ with Image.open("Tests/images/hopper.webp") as im:
+ ImageOps.contain(im, size).save("imageops_contain.webp")
+ ImageOps.cover(im, size).save("imageops_cover.webp")
+ ImageOps.fit(im, size).save("imageops_fit.webp")
+ ImageOps.pad(im, size, color="#f00").save("imageops_pad.webp")
# thumbnail() can also be used,
# but will modify the image object in place
im.thumbnail(size)
- im.save("imageops_thumbnail.png")
+ im.save("image_thumbnail.webp")
-+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
-| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
-+================+===========================================+============================================+==========================================+========================================+========================================+
-|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
-+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
-|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
-+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
-|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
-+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
++----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
+| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
++================+============================================+=============================================+===========================================+=========================================+=========================================+
+|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
++----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
+|Resulting image | .. image:: ../example/image_thumbnail.webp | .. image:: ../example/imageops_contain.webp | .. image:: ../example/imageops_cover.webp | .. image:: ../example/imageops_fit.webp | .. image:: ../example/imageops_pad.webp |
++----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
+|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
++----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
.. autofunction:: contain
.. autofunction:: cover
diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst
index 04d6f5dcd..1ac3d034b 100644
--- a/docs/reference/PixelAccess.rst
+++ b/docs/reference/PixelAccess.rst
@@ -44,42 +44,23 @@ Access using negative indexes is also possible. ::
-----------------------------
.. class:: PixelAccess
+ :canonical: PIL.Image.core.PixelAccess
- .. method:: __setitem__(self, xy, color):
+ .. method:: __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]
- Modifies the pixel at x,y. The color is given as a single
- numerical value for single band images, and a tuple for
- multi-band images
-
- :param xy: The pixel coordinate, given as (x, y).
- :param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
-
- .. method:: __getitem__(self, xy):
-
- Returns the pixel at x,y. The pixel is returned as a single
- value for single band images or a tuple for multiple band
- images
+ Returns the pixel at x,y. The pixel is returned as a single
+ value for single band images or a tuple for multi-band images.
:param xy: The pixel coordinate, given as (x, y).
:returns: a pixel value for single band images, a tuple of
- pixel values for multiband images.
+ pixel values for multiband images.
- .. method:: putpixel(self, xy, color):
+ .. method:: __setitem__(self, xy: tuple[int, int], color: float | tuple[int, ...]) -> None
Modifies the pixel at x,y. The color is given as a single
numerical value for single band images, and a tuple for
- multi-band images. In addition to this, RGB and RGBA tuples
- are accepted for P and PA images.
+ multi-band images.
:param xy: The pixel coordinate, given as (x, y).
- :param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
-
- .. method:: getpixel(self, xy):
-
- Returns the pixel at x,y. The pixel is returned as a single
- value for single band images or a tuple for multiple band
- images
-
- :param xy: The pixel coordinate, given as (x, y).
- :returns: a pixel value for single band images, a tuple of
- pixel values for multiband images.
+ :param color: The pixel value according to its mode,
+ e.g. tuple (r, g, b) for RGB mode.
diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst
deleted file mode 100644
index ed58ca3a5..000000000
--- a/docs/reference/PyAccess.rst
+++ /dev/null
@@ -1,46 +0,0 @@
-.. py:module:: PIL.PyAccess
-.. py:currentmodule:: PIL.PyAccess
-
-:py:mod:`~PIL.PyAccess` Module
-==============================
-
-The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version.
-
-.. note:: Accessing individual pixels is fairly slow. If you are
- looping over all of the pixels in an image, there is likely
- a faster way using other parts of the Pillow API.
-
- :mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps`
- have methods for many standard operations. If you wish to perform
- a custom mapping, check out :py:meth:`~PIL.Image.Image.point`.
-
-Example
--------
-
-The following script loads an image, accesses one pixel from it, then changes it. ::
-
- from PIL import Image
-
- with Image.open("hopper.jpg") as im:
- px = im.load()
- print(px[4, 4])
- px[4, 4] = (0, 0, 0)
- print(px[4, 4])
-
-Results in the following::
-
- (23, 24, 68)
- (0, 0, 0)
-
-Access using negative indexes is also possible. ::
-
- px[-1, -1] = (0, 0, 0)
- print(px[-1, -1])
-
-
-
-:py:class:`PyAccess` Class
---------------------------
-
-.. autoclass:: PIL.PyAccess.PyAccess()
- :members:
diff --git a/docs/reference/index.rst b/docs/reference/index.rst
index 82c75e373..effcd3c46 100644
--- a/docs/reference/index.rst
+++ b/docs/reference/index.rst
@@ -32,7 +32,6 @@ Reference
JpegPresets
PSDraw
PixelAccess
- PyAccess
features
../PIL
plugins
diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst
index 899e4966f..e4cb17c4d 100644
--- a/docs/reference/internal_modules.rst
+++ b/docs/reference/internal_modules.rst
@@ -33,6 +33,10 @@ Internal Modules
Provides a convenient way to import type hints that are not available
on some Python versions.
+.. py:class:: NumpyArray
+
+ Typing alias.
+
.. py:class:: StrOrBytesPath
Typing alias.
diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst
index adada6e01..2ea973c5c 100644
--- a/docs/releasenotes/10.0.0.rst
+++ b/docs/releasenotes/10.0.0.rst
@@ -158,7 +158,7 @@ PyAccess and Image.USE_CFFI_ACCESS
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Since Pillow's C API is now faster than PyAccess on PyPy,
-:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow
+:py:mod:`!PyAccess` has been deprecated and will be removed in Pillow
11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead.
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is
diff --git a/docs/releasenotes/10.4.0.rst b/docs/releasenotes/10.4.0.rst
index 44727efd4..8d3706be6 100644
--- a/docs/releasenotes/10.4.0.rst
+++ b/docs/releasenotes/10.4.0.rst
@@ -4,21 +4,16 @@
Security
========
-TODO
-^^^^
+ImageShow.WindowsViewer.show_file
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-TODO
+If an attacker has control over the ``path`` passed to
+``ImageShow.WindowsViewer.show_file()``, they may be able to
+execute arbitrary shell commands.
-:cve:`YYYY-XXXXX`: TODO
-^^^^^^^^^^^^^^^^^^^^^^^
-
-TODO
-
-Backwards Incompatible Changes
-==============================
-
-TODO
-^^^^
+To prevent this, a :py:exc:`FileNotFoundError` will be raised if the ``path``
+does not exist as a file. To provide a consistent experience, the error has
+been added to all :py:class:`~PIL.ImageShow` viewers.
Deprecations
============
@@ -28,6 +23,13 @@ BGR;15, BGR 16 and BGR;24
The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated.
+Non-image modes in ImageCms
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow
+image modes has been deprecated. Defaulting to "L" or "1" if the mode cannot be mapped
+is also deprecated.
+
Support for LibTIFF earlier than 4
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -39,14 +41,6 @@ ImageDraw.getdraw hints parameter
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
-API Changes
-===========
-
-TODO
-^^^^
-
-TODO
-
API Additions
=============
@@ -57,11 +51,6 @@ Added :py:meth:`~PIL.ImageDraw.ImageDraw.circle`. It provides the same functiona
:py:meth:`~PIL.ImageDraw.ImageDraw.ellipse`, but instead of taking a bounding box, it
takes a center point and radius.
-TODO
-^^^^
-
-TODO
-
Other Changes
=============
diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst
new file mode 100644
index 000000000..964423ae0
--- /dev/null
+++ b/docs/releasenotes/11.0.0.rst
@@ -0,0 +1,77 @@
+11.0.0
+------
+
+Security
+========
+
+TODO
+^^^^
+
+TODO
+
+:cve:`YYYY-XXXXX`: TODO
+^^^^^^^^^^^^^^^^^^^^^^^
+
+TODO
+
+Backwards Incompatible Changes
+==============================
+
+Python 3.8
+^^^^^^^^^^
+
+Pillow has dropped support for Python 3.8,
+which reached end-of-life in October 2024.
+
+PSFile
+^^^^^^
+
+The :py:class:`!PSFile` class was removed in Pillow 11 (2024-10-15).
+This class was only made as a helper to be used internally,
+so there is no replacement. If you need this functionality though,
+it is a very short class that can easily be recreated in your own code.
+
+PyAccess and Image.USE_CFFI_ACCESS
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Since Pillow's C API is now faster than PyAccess on PyPy, ``PyAccess`` has been
+removed. Pillow's C API will now be used on PyPy instead.
+
+``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was
+similarly removed.
+
+Deprecations
+============
+
+TODO
+^^^^
+
+TODO
+
+API Changes
+===========
+
+TODO
+^^^^
+
+TODO
+
+API Additions
+=============
+
+TODO
+^^^^
+
+TODO
+
+Other Changes
+=============
+
+Python 3.13
+^^^^^^^^^^^
+
+Pillow 10.4.0 had wheels built against Python 3.13 beta, available as a preview to help
+others prepare for 3.13, and to ensure Pillow could be used immediately at the release
+of 3.13.0 final (2024-10-01, :pep:`719`).
+
+Pillow 11.0.0 now officially supports Python 3.13.
diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst
index 9f46cc1e9..4ef914f64 100644
--- a/docs/releasenotes/8.3.0.rst
+++ b/docs/releasenotes/8.3.0.rst
@@ -18,9 +18,9 @@ is not secure.
- :py:meth:`~PIL.Image.Image.getexif` has used ``xml`` to potentially retrieve
orientation data since Pillow 7.2.0. It has been refactored to use ``re`` instead.
-- :py:meth:`~PIL.JpegImagePlugin.JpegImageFile.getxmp` was added in Pillow 8.2.0. It
- will now use ``defusedxml`` instead. If the dependency is not present, an empty
- dictionary will be returned and a warning raised.
+- ``getxmp()`` was added to :py:class:`~PIL.JpegImagePlugin.JpegImageFile` in Pillow
+ 8.2.0. It will now use ``defusedxml`` instead. If the dependency is not present, an
+ empty dictionary will be returned and a warning raised.
Deprecations
============
diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst
index fe29f2e4f..6e0647343 100644
--- a/docs/releasenotes/9.2.0.rst
+++ b/docs/releasenotes/9.2.0.rst
@@ -98,7 +98,7 @@ Previously, the ``size`` methods returned a ``height`` that included the vertica
offset of the text, while the new ``bbox`` methods distinguish this as a ``top``
offset.
-.. image:: ../example/size_vs_bbox.png
+.. image:: ../example/size_vs_bbox.webp
:alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself.
:align: center
diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst
index 08e9ec2a4..501479bb6 100644
--- a/docs/releasenotes/9.5.0.rst
+++ b/docs/releasenotes/9.5.0.rst
@@ -32,7 +32,7 @@ Deprecations
PSFile
^^^^^^
-The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will
+The :py:class:`!PSFile` class has been deprecated and will
be removed in Pillow 11 (2024-10-15). This class was only made as a helper to
be used internally, so there is no replacement. If you need this functionality
though, it is a very short class that can easily be recreated in your own code.
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index 6ee5fb6c8..641cda4ef 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ 11.0.0
10.4.0
10.3.0
10.2.0
diff --git a/pyproject.toml b/pyproject.toml
index 20e87ad32..cd7248669 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,18 +14,20 @@ readme = "README.md"
keywords = [
"Imaging",
]
-license = {text = "HPND"}
-authors = [{name = "Jeffrey A. Clark", email = "aclark@aclark.net"}]
-requires-python = ">=3.8"
+license = { text = "HPND" }
+authors = [
+ { name = "Jeffrey A. Clark", email = "aclark@aclark.net" },
+]
+requires-python = ">=3.9"
classifiers = [
"Development Status :: 6 - Mature",
"License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Multimedia :: Graphics",
@@ -38,8 +40,7 @@ classifiers = [
dynamic = [
"version",
]
-[project.optional-dependencies]
-docs = [
+optional-dependencies.docs = [
"furo",
"olefile",
"sphinx>=7.3",
@@ -47,13 +48,13 @@ docs = [
"sphinx-inline-tabs",
"sphinxext-opengraph",
]
-fpx = [
+optional-dependencies.fpx = [
"olefile",
]
-mic = [
+optional-dependencies.mic = [
"olefile",
]
-tests = [
+optional-dependencies.tests = [
"check-manifest",
"coverage",
"defusedxml",
@@ -65,28 +66,29 @@ tests = [
"pytest-cov",
"pytest-timeout",
]
-typing = [
- 'typing-extensions; python_version < "3.10"',
+optional-dependencies.typing = [
+ "typing-extensions; python_version<'3.10'",
]
-xmp = [
+optional-dependencies.xmp = [
"defusedxml",
]
-[project.urls]
-Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst"
-Documentation = "https://pillow.readthedocs.io"
-Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi"
-Homepage = "https://python-pillow.org"
-Mastodon = "https://fosstodon.org/@pillow"
-"Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
-Source = "https://github.com/python-pillow/Pillow"
+urls.Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst"
+urls.Documentation = "https://pillow.readthedocs.io"
+urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi"
+urls.Homepage = "https://python-pillow.org"
+urls.Mastodon = "https://fosstodon.org/@pillow"
+urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
+urls.Source = "https://github.com/python-pillow/Pillow"
[tool.setuptools]
-packages = ["PIL"]
+packages = [
+ "PIL",
+]
include-package-data = true
-package-dir = {"" = "src"}
+package-dir = { "" = "src" }
[tool.setuptools.dynamic]
-version = {attr = "PIL.__version__"}
+version = { attr = "PIL.__version__" }
[tool.cibuildwheel]
before-all = ".github/workflows/wheels-dependencies.sh"
@@ -98,45 +100,53 @@ test-extras = "tests"
[tool.ruff]
fix = true
-[tool.ruff.lint]
-select = [
- "C4", # flake8-comprehensions
- "E", # pycodestyle errors
- "EM", # flake8-errmsg
- "F", # pyflakes errors
- "I", # isort
- "ISC", # flake8-implicit-str-concat
- "LOG", # flake8-logging
- "PGH", # pygrep-hooks
- "PYI", # flake8-pyi
+lint.select = [
+ "C4", # flake8-comprehensions
+ "E", # pycodestyle errors
+ "EM", # flake8-errmsg
+ "F", # pyflakes errors
+ "I", # isort
+ "ISC", # flake8-implicit-str-concat
+ "LOG", # flake8-logging
+ "PGH", # pygrep-hooks
+ "PYI", # flake8-pyi
"RUF100", # unused noqa (yesqa)
- "UP", # pyupgrade
- "W", # pycodestyle warnings
- "YTT", # flake8-2020
+ "UP", # pyupgrade
+ "W", # pycodestyle warnings
+ "YTT", # flake8-2020
]
-ignore = [
- "E203", # Whitespace before ':'
- "E221", # Multiple spaces before operator
- "E226", # Missing whitespace around arithmetic operator
- "E241", # Multiple spaces after ','
+lint.ignore = [
+ "E203", # Whitespace before ':'
+ "E221", # Multiple spaces before operator
+ "E226", # Missing whitespace around arithmetic operator
+ "E241", # Multiple spaces after ','
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
"PYI034", # flake8-pyi: typing.Self added in Python 3.11
]
+lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [
+ "I002",
+]
+lint.per-file-ignores."Tests/oss-fuzz/fuzz_pillow.py" = [
+ "I002",
+]
+lint.isort.known-first-party = [
+ "PIL",
+]
+lint.isort.required-imports = [
+ "from __future__ import annotations",
+]
-[tool.ruff.lint.per-file-ignores]
-"Tests/oss-fuzz/fuzz_font.py" = ["I002"]
-"Tests/oss-fuzz/fuzz_pillow.py" = ["I002"]
-
-[tool.ruff.lint.isort]
-known-first-party = ["PIL"]
-required-imports = ["from __future__ import annotations"]
+[tool.pyproject-fmt]
+max_supported_python = "3.13"
[tool.pytest.ini_options]
addopts = "-ra --color=yes"
-testpaths = ["Tests"]
+testpaths = [
+ "Tests",
+]
[tool.mypy]
-python_version = "3.8"
+python_version = "3.9"
pretty = true
disallow_any_generics = true
enable_error_code = "ignore-without-code"
@@ -145,3 +155,11 @@ follow_imports = "silent"
warn_redundant_casts = true
warn_unreachable = true
warn_unused_ignores = true
+exclude = [
+ '^Tests/oss-fuzz/fuzz_font.py$',
+ '^Tests/oss-fuzz/fuzz_pillow.py$',
+ '^Tests/test_qt_image_qapplication.py$',
+ '^Tests/test_font_pcf_charsets.py$',
+ '^Tests/test_font_pcf.py$',
+ '^Tests/test_file_tar.py$',
+]
diff --git a/setup.py b/setup.py
index 0abfaaddc..b26852b0b 100644
--- a/setup.py
+++ b/setup.py
@@ -43,7 +43,7 @@ WEBP_ROOT = None
ZLIB_ROOT = None
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
-if sys.platform == "win32" and sys.version_info >= (3, 13):
+if sys.platform == "win32" and sys.version_info >= (3, 14):
import atexit
atexit.register(
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index 59246c6e2..b9cefafdd 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -430,6 +430,7 @@ class BLPEncoder(ImageFile.PyEncoder):
def _write_palette(self) -> bytes:
data = b""
+ assert self.im is not None
palette = self.im.getpalette("RGBA", "RGBA")
for i in range(len(palette) // 4):
r, g, b, a = palette[i * 4 : (i + 1) * 4]
@@ -444,6 +445,7 @@ class BLPEncoder(ImageFile.PyEncoder):
offset = 20 + 16 * 4 * 2 + len(palette_data)
data = struct.pack("<16I", offset, *((0,) * 15))
+ assert self.im is not None
w, h = self.im.size
data += struct.pack("<16I", w * h, *((0,) * 15))
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index e74727007..a57e4aea2 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -380,6 +380,7 @@ class DdsImageFile(ImageFile.ImageFile):
elif pfflags & DDPF.PALETTEINDEXED8:
self._mode = "P"
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
+ self.palette.mode = "RGBA"
elif pfflags & DDPF.FOURCC:
offset = header_size + 4
if fourcc == D3DFMT.DXT1:
diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py
index 380b1cf0e..59bb8594d 100644
--- a/src/PIL/EpsImagePlugin.py
+++ b/src/PIL/EpsImagePlugin.py
@@ -31,7 +31,6 @@ from typing import IO
from . import Image, ImageFile
from ._binary import i32le as i32
-from ._deprecate import deprecate
# --------------------------------------------------------------------
@@ -159,43 +158,6 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
return im
-class PSFile:
- """
- Wrapper for bytesio object that treats either CR or LF as end of line.
- This class is no longer used internally, but kept for backwards compatibility.
- """
-
- def __init__(self, fp):
- deprecate(
- "PSFile",
- 11,
- action="If you need the functionality of this class "
- "you will need to implement it yourself.",
- )
- self.fp = fp
- self.char = None
-
- def seek(self, offset, whence=io.SEEK_SET):
- self.char = None
- self.fp.seek(offset, whence)
-
- def readline(self) -> str:
- s = [self.char or b""]
- self.char = None
-
- c = self.fp.read(1)
- while (c not in b"\r\n") and len(c):
- s.append(c)
- c = self.fp.read(1)
-
- self.char = self.fp.read(1)
- # line endings can be 1 or 2 of \r \n, in either order
- if self.char in b"\r\n":
- self.char = None
-
- return b"".join(s).decode("latin-1")
-
-
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
@@ -230,6 +192,11 @@ class EpsImageFile(ImageFile.ImageFile):
trailer_reached = False
def check_required_header_comments() -> None:
+ """
+ The EPS specification requires that some headers exist.
+ This should be checked when the header comments formally end,
+ when image data starts, or when the file ends, whichever comes first.
+ """
if "PS-Adobe" not in self.info:
msg = 'EPS header missing "%!PS-Adobe" comment'
raise SyntaxError(msg)
@@ -270,6 +237,8 @@ class EpsImageFile(ImageFile.ImageFile):
if byte == b"":
# if we didn't read a byte we must be at the end of the file
if bytes_read == 0:
+ if reading_header_comments:
+ check_required_header_comments()
break
elif byte in b"\r\n":
# if we read a line ending character, ignore it and parse what
@@ -365,13 +334,11 @@ class EpsImageFile(ImageFile.ImageFile):
trailer_reached = True
bytes_read = 0
- check_required_header_comments()
-
if not self._size:
msg = "cannot determine EPS bounding box"
raise OSError(msg)
- def _find_offset(self, fp):
+ def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]:
s = fp.read(4)
if s == b"%!PS":
@@ -394,7 +361,9 @@ class EpsImageFile(ImageFile.ImageFile):
return length, offset
- def load(self, scale=1, transparency=False):
+ def load(
+ self, scale: int = 1, transparency: bool = False
+ ) -> Image.core.PixelAccess | None:
# Load EPS via Ghostscript
if self.tile:
self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py
index dceb83927..52d1fce31 100644
--- a/src/PIL/FliImagePlugin.py
+++ b/src/PIL/FliImagePlugin.py
@@ -45,7 +45,7 @@ class FliImageFile(ImageFile.ImageFile):
format_description = "Autodesk FLI/FLC Animation"
_close_exclusive_fp_after_loading = False
- def _open(self):
+ def _open(self) -> None:
# HEAD
s = self.fp.read(128)
if not (_accept(s) and s[20:22] == b"\x00\x00"):
@@ -83,7 +83,7 @@ class FliImageFile(ImageFile.ImageFile):
if i16(s, 4) == 0xF1FA:
# look for palette chunk
number_of_subchunks = i16(s, 6)
- chunk_size = None
+ chunk_size: int | None = None
for _ in range(number_of_subchunks):
if chunk_size is not None:
self.fp.seek(chunk_size - 6, os.SEEK_CUR)
@@ -96,8 +96,9 @@ class FliImageFile(ImageFile.ImageFile):
if not chunk_size:
break
- palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette]
- self.palette = ImagePalette.raw("RGB", b"".join(palette))
+ self.palette = ImagePalette.raw(
+ "RGB", b"".join(o8(r) + o8(g) + o8(b) for (r, g, b) in palette)
+ )
# set things up to decode first frame
self.__frame = -1
@@ -105,7 +106,7 @@ class FliImageFile(ImageFile.ImageFile):
self.__rewind = self.fp.tell()
self.seek(0)
- def _palette(self, palette, shift):
+ def _palette(self, palette: list[tuple[int, int, int]], shift: int) -> None:
# load palette
i = 0
diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py
index c1927bd26..386e37233 100644
--- a/src/PIL/FpxImagePlugin.py
+++ b/src/PIL/FpxImagePlugin.py
@@ -53,7 +53,7 @@ class FpxImageFile(ImageFile.ImageFile):
format = "FPX"
format_description = "FlashPix"
- def _open(self):
+ def _open(self) -> None:
#
# read the OLE directory and see if this is a likely
# to be a FlashPix file
@@ -64,7 +64,8 @@ class FpxImageFile(ImageFile.ImageFile):
msg = "not an FPX file; invalid OLE file"
raise SyntaxError(msg) from e
- if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
+ root = self.ole.root
+ if not root or root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
msg = "not an FPX file; bad root CLSID"
raise SyntaxError(msg)
@@ -99,8 +100,7 @@ class FpxImageFile(ImageFile.ImageFile):
s = prop[0x2000002 | id]
- bands = i32(s, 4)
- if bands > 4:
+ if not isinstance(s, bytes) or (bands := i32(s, 4)) > 4:
msg = "Invalid number of bands"
raise OSError(msg)
@@ -231,7 +231,7 @@ class FpxImageFile(ImageFile.ImageFile):
self._fp = self.fp
self.fp = None
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if not self.fp:
self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"])
diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py
index 93e89b1e6..3c8feea5f 100644
--- a/src/PIL/GbrImagePlugin.py
+++ b/src/PIL/GbrImagePlugin.py
@@ -88,7 +88,7 @@ class GbrImageFile(ImageFile.ImageFile):
# Data is an uncompressed block of w * h * bytes/pixel
self._data_size = width * height * color_depth
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if not self.im:
self.im = Image.core.new(self.mode, self.size)
self.frombytes(self.fp.read(self._data_size))
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index 541d97f8c..bf74f9356 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -32,7 +32,7 @@ import subprocess
import sys
from enum import IntEnum
from functools import cached_property
-from typing import IO, TYPE_CHECKING, Any, List, Literal, NamedTuple, Union
+from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union
from . import (
Image,
@@ -331,7 +331,6 @@ class GifImageFile(ImageFile.ImageFile):
LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
or palette
):
- self.pyaccess = None
if "transparency" in self.info:
self.im.putpalettealpha(self.info["transparency"], 0)
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
@@ -433,7 +432,7 @@ class GifImageFile(ImageFile.ImageFile):
self._prev_im = self.im
if self._frame_palette:
self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
- self.im.putpalette(*self._frame_palette.getdata())
+ self.im.putpalette("RGB", *self._frame_palette.getdata())
else:
self.im = None
self._mode = temp_mode
@@ -505,7 +504,7 @@ def _normalize_mode(im: Image.Image) -> Image.Image:
return im.convert("L")
-_Palette = Union[bytes, bytearray, List[int], ImagePalette.ImagePalette]
+_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette]
def _normalize_palette(
diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py
index 2a89d498c..8729f7643 100644
--- a/src/PIL/IcnsImagePlugin.py
+++ b/src/PIL/IcnsImagePlugin.py
@@ -34,11 +34,13 @@ MAGIC = b"icns"
HEADERSIZE = 8
-def nextheader(fobj):
+def nextheader(fobj: IO[bytes]) -> tuple[bytes, int]:
return struct.unpack(">4sI", fobj.read(HEADERSIZE))
-def read_32t(fobj, start_length, size):
+def read_32t(
+ fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
+) -> dict[str, Image.Image]:
# The 128x128 icon seems to have an extra header for some reason.
(start, length) = start_length
fobj.seek(start)
@@ -49,7 +51,9 @@ def read_32t(fobj, start_length, size):
return read_32(fobj, (start + 4, length - 4), size)
-def read_32(fobj, start_length, size):
+def read_32(
+ fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
+) -> dict[str, Image.Image]:
"""
Read a 32bit RGB icon resource. Seems to be either uncompressed or
an RLE packbits-like scheme.
@@ -72,14 +76,14 @@ def read_32(fobj, start_length, size):
byte = fobj.read(1)
if not byte:
break
- byte = byte[0]
- if byte & 0x80:
- blocksize = byte - 125
+ byte_int = byte[0]
+ if byte_int & 0x80:
+ blocksize = byte_int - 125
byte = fobj.read(1)
for i in range(blocksize):
data.append(byte)
else:
- blocksize = byte + 1
+ blocksize = byte_int + 1
data.append(fobj.read(blocksize))
bytesleft -= blocksize
if bytesleft <= 0:
@@ -92,7 +96,9 @@ def read_32(fobj, start_length, size):
return {"RGB": im}
-def read_mk(fobj, start_length, size):
+def read_mk(
+ fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
+) -> dict[str, Image.Image]:
# Alpha masks seem to be uncompressed
start = start_length[0]
fobj.seek(start)
@@ -102,10 +108,14 @@ def read_mk(fobj, start_length, size):
return {"A": band}
-def read_png_or_jpeg2000(fobj, start_length, size):
+def read_png_or_jpeg2000(
+ fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
+) -> dict[str, Image.Image]:
(start, length) = start_length
fobj.seek(start)
sig = fobj.read(12)
+
+ im: Image.Image
if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a":
fobj.seek(start)
im = PngImagePlugin.PngImageFile(fobj)
@@ -164,12 +174,12 @@ class IcnsFile:
],
}
- def __init__(self, fobj):
+ def __init__(self, fobj: IO[bytes]) -> None:
"""
fobj is a file-like object as an icns resource
"""
# signature : (start, length)
- self.dct = dct = {}
+ self.dct = {}
self.fobj = fobj
sig, filesize = nextheader(fobj)
if not _accept(sig):
@@ -183,11 +193,11 @@ class IcnsFile:
raise SyntaxError(msg)
i += HEADERSIZE
blocksize -= HEADERSIZE
- dct[sig] = (i, blocksize)
+ self.dct[sig] = (i, blocksize)
fobj.seek(blocksize, io.SEEK_CUR)
i += blocksize
- def itersizes(self):
+ def itersizes(self) -> list[tuple[int, int, int]]:
sizes = []
for size, fmts in self.SIZES.items():
for fmt, reader in fmts:
@@ -196,14 +206,14 @@ class IcnsFile:
break
return sizes
- def bestsize(self):
+ def bestsize(self) -> tuple[int, int, int]:
sizes = self.itersizes()
if not sizes:
msg = "No 32bit icon resources found"
raise SyntaxError(msg)
return max(sizes)
- def dataforsize(self, size):
+ def dataforsize(self, size: tuple[int, int, int]) -> dict[str, Image.Image]:
"""
Get an icon resource as {channel: array}. Note that
the arrays are bottom-up like windows bitmaps and will likely
@@ -216,18 +226,20 @@ class IcnsFile:
dct.update(reader(self.fobj, desc, size))
return dct
- def getimage(self, size=None):
+ def getimage(
+ self, size: tuple[int, int] | tuple[int, int, int] | None = None
+ ) -> Image.Image:
if size is None:
size = self.bestsize()
- if len(size) == 2:
+ elif len(size) == 2:
size = (size[0], size[1], 1)
channels = self.dataforsize(size)
- im = channels.get("RGBA", None)
+ im = channels.get("RGBA")
if im:
return im
- im = channels.get("RGB").copy()
+ im = channels["RGB"].copy()
try:
im.putalpha(channels["A"])
except KeyError:
@@ -268,7 +280,7 @@ class IcnsImageFile(ImageFile.ImageFile):
return self._size
@size.setter
- def size(self, value):
+ def size(self, value) -> None:
info_size = value
if info_size not in self.info["sizes"] and len(info_size) == 2:
info_size = (info_size[0], info_size[1], 1)
@@ -287,7 +299,7 @@ class IcnsImageFile(ImageFile.ImageFile):
raise ValueError(msg)
self._size = value
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if len(self.size) == 3:
self.best_size = self.size
self.size = (
diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py
index 227fcf35c..650f5e4f1 100644
--- a/src/PIL/IcoImagePlugin.py
+++ b/src/PIL/IcoImagePlugin.py
@@ -120,7 +120,7 @@ def _accept(prefix: bytes) -> bool:
class IcoFile:
- def __init__(self, buf):
+ def __init__(self, buf) -> None:
"""
Parse image from file-like object containing ico file data
"""
@@ -177,19 +177,19 @@ class IcoFile:
# ICO images are usually squares
self.entry = sorted(self.entry, key=lambda x: x["square"], reverse=True)
- def sizes(self):
+ def sizes(self) -> set[tuple[int, int]]:
"""
Get a list of all available icon sizes and color depths.
"""
return {(h["width"], h["height"]) for h in self.entry}
- def getentryindex(self, size, bpp=False):
+ def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int:
for i, h in enumerate(self.entry):
if size == h["dim"] and (bpp is False or bpp == h["color_depth"]):
return i
return 0
- def getimage(self, size, bpp=False):
+ def getimage(self, size: tuple[int, int], bpp: int | bool = False) -> Image.Image:
"""
Get an image from the icon
"""
@@ -321,7 +321,7 @@ class IcoImageFile(ImageFile.ImageFile):
raise ValueError(msg)
self._size = value
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if self.im is not None and self.im.size == self.size:
# Already loaded
return Image.Image.load(self)
@@ -329,7 +329,6 @@ class IcoImageFile(ImageFile.ImageFile):
# if tile is PNG, it won't really be loaded yet
im.load()
self.im = im.im
- self.pyaccess = None
self._mode = im.mode
if im.palette:
self.palette = im.palette
@@ -342,6 +341,7 @@ class IcoImageFile(ImageFile.ImageFile):
self.info["sizes"] = set(sizes)
self.size = im.size
+ return None
def load_seek(self, pos: int) -> None:
# Flag the ImageFile.Parser so that it
diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py
index 015c2febe..2fb7ecd52 100644
--- a/src/PIL/ImImagePlugin.py
+++ b/src/PIL/ImImagePlugin.py
@@ -79,7 +79,7 @@ OPEN = {
"LA image": ("LA", "LA;L"),
"PA image": ("LA", "PA;L"),
"RGBA image": ("RGBA", "RGBA;L"),
- "RGBX image": ("RGBX", "RGBX;L"),
+ "RGBX image": ("RGB", "RGBX;L"),
"CMYK image": ("CMYK", "CMYK;L"),
"YCC image": ("YCbCr", "YCbCr;L"),
}
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index f4d64292c..8dcc1715e 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -38,10 +38,17 @@ import struct
import sys
import tempfile
import warnings
-from collections.abc import Callable, MutableMapping
+from collections.abc import Callable, MutableMapping, Sequence
from enum import IntEnum
from types import ModuleType
-from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, Tuple, cast
+from typing import (
+ IO,
+ TYPE_CHECKING,
+ Any,
+ Literal,
+ Protocol,
+ cast,
+)
# VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0.
@@ -56,7 +63,6 @@ from . import (
)
from ._binary import i32le, o32be, o32le
from ._deprecate import deprecate
-from ._typing import StrOrBytesPath, TypeGuard
from ._util import DeferredError, is_path
ElementTree: ModuleType | None
@@ -76,6 +82,8 @@ class DecompressionBombError(Exception):
pass
+WARN_POSSIBLE_FORMATS: bool = False
+
# Limit to around a quarter gigabyte for a 24-bit (3 bpp) image
MAX_IMAGE_PIXELS: int | None = int(1024 * 1024 * 1024 // 4 // 3)
@@ -114,14 +122,6 @@ except ImportError as v:
raise
-USE_CFFI_ACCESS = False
-cffi: ModuleType | None
-try:
- import cffi
-except ImportError:
- cffi = None
-
-
def isImageType(t: Any) -> TypeGuard[Image]:
"""
Checks if an object is an image object.
@@ -218,7 +218,8 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# Registries
if TYPE_CHECKING:
- from . import ImageFile
+ from . import ImageFile, ImagePalette
+ from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
ID: list[str] = []
OPEN: dict[
str,
@@ -559,7 +560,6 @@ class Image:
self.palette = None
self.info = {}
self.readonly = 0
- self.pyaccess = None
self._exif = None
@classmethod
@@ -630,7 +630,6 @@ class Image:
def _copy(self) -> None:
self.load()
self.im = self.im.copy()
- self.pyaccess = None
self.readonly = 0
def _ensure_mutable(self) -> None:
@@ -881,7 +880,7 @@ class Image:
msg = "cannot decode image data"
raise ValueError(msg)
- def load(self):
+ def load(self) -> core.PixelAccess | None:
"""
Allocates storage for the image and loads the pixel data. In
normal cases, you don't need to call this method, since the
@@ -894,12 +893,12 @@ class Image:
operations. See :ref:`file-handling` for more information.
:returns: An image access object.
- :rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess`
+ :rtype: :py:class:`.PixelAccess`
"""
if self.im is not None and self.palette and self.palette.dirty:
# realize palette
mode, arr = self.palette.getdata()
- self.im.putpalette(mode, arr)
+ self.im.putpalette(self.palette.mode, mode, arr)
self.palette.dirty = 0
self.palette.rawmode = None
if "transparency" in self.info and mode in ("LA", "PA"):
@@ -909,20 +908,13 @@ class Image:
self.im.putpalettealphas(self.info["transparency"])
self.palette.mode = "RGBA"
else:
- palette_mode = "RGBA" if mode.startswith("RGBA") else "RGB"
- self.palette.mode = palette_mode
- self.palette.palette = self.im.getpalette(palette_mode, palette_mode)
+ self.palette.palette = self.im.getpalette(
+ self.palette.mode, self.palette.mode
+ )
if self.im is not None:
- if cffi and USE_CFFI_ACCESS:
- if self.pyaccess:
- return self.pyaccess
- from . import PyAccess
-
- self.pyaccess = PyAccess.new(self, self.readonly)
- if self.pyaccess:
- return self.pyaccess
return self.im.pixel_access(self.readonly)
+ return None
def verify(self) -> None:
"""
@@ -1112,7 +1104,10 @@ class Image:
del new_im.info["transparency"]
if trns is not None:
try:
- new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
+ new_im.info["transparency"] = new_im.palette.getcolor(
+ cast(tuple[int, ...], trns), # trns was converted to RGB
+ new_im,
+ )
except Exception:
# if we can't make a transparent color, don't leave the old
# transparency hanging around to mess us up.
@@ -1160,7 +1155,7 @@ class Image:
# crash fail if we leave a bytes transparency in an rgb/l mode.
del new_im.info["transparency"]
if trns is not None:
- if new_im.mode == "P":
+ if new_im.mode == "P" and new_im.palette:
try:
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
except ValueError as e:
@@ -1178,9 +1173,9 @@ class Image:
def quantize(
self,
colors: int = 256,
- method: Quantize | None = None,
+ method: int | None = None,
kmeans: int = 0,
- palette=None,
+ palette: Image | None = None,
dither: Dither = Dither.FLOYDSTEINBERG,
) -> Image:
"""
@@ -1252,8 +1247,8 @@ class Image:
from . import ImagePalette
mode = im.im.getpalettemode()
- palette = im.im.getpalette(mode, mode)[: colors * len(mode)]
- im.palette = ImagePalette.ImagePalette(mode, palette)
+ palette_data = im.im.getpalette(mode, mode)[: colors * len(mode)]
+ im.palette = ImagePalette.ImagePalette(mode, palette_data)
return im
@@ -1319,7 +1314,7 @@ class Image:
return im.crop((x0, y0, x1, y1))
def draft(
- self, mode: str | None, size: tuple[int, int]
+ self, mode: str | None, size: tuple[int, int] | None
) -> tuple[str, tuple[int, int, float, float]] | None:
"""
Configures the image file loader so it returns a version of the
@@ -1408,7 +1403,9 @@ class Image:
self.load()
return self.im.getbbox(alpha_only)
- def getcolors(self, maxcolors: int = 256):
+ def getcolors(
+ self, maxcolors: int = 256
+ ) -> list[tuple[int, tuple[int, ...]]] | list[tuple[int, float]] | None:
"""
Returns a list of colors used in this image.
@@ -1425,7 +1422,7 @@ class Image:
self.load()
if self.mode in ("1", "L", "P"):
h = self.im.histogram()
- out = [(h[i], i) for i in range(256) if h[i]]
+ out: list[tuple[int, float]] = [(h[i], i) for i in range(256) if h[i]]
if len(out) > maxcolors:
return None
return out
@@ -1469,7 +1466,14 @@ class Image:
return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands))
return self.im.getextrema()
- def _getxmp(self, xmp_tags):
+ def getxmp(self) -> dict[str, Any]:
+ """
+ Returns a dictionary containing the XMP tags.
+ Requires defusedxml to be installed.
+
+ :returns: XMP tags in a dictionary.
+ """
+
def get_name(tag: str) -> str:
return re.sub("^{[^}]+}", "", tag)
@@ -1496,9 +1500,10 @@ class Image:
if ElementTree is None:
warnings.warn("XMP data cannot be read without defusedxml dependency")
return {}
- else:
- root = ElementTree.fromstring(xmp_tags)
- return {get_name(root.tag): get_value(root)}
+ if "xmp" not in self.info:
+ return {}
+ root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00"))
+ return {get_name(root.tag): get_value(root)}
def getexif(self) -> Exif:
"""
@@ -1659,7 +1664,9 @@ class Image:
del self.info["transparency"]
- def getpixel(self, xy):
+ def getpixel(
+ self, xy: tuple[int, int] | list[int]
+ ) -> float | tuple[int, ...] | None:
"""
Returns the pixel value at a given position.
@@ -1670,8 +1677,6 @@ class Image:
"""
self.load()
- if self.pyaccess:
- return self.pyaccess.getpixel(xy)
return self.im.getpixel(tuple(xy))
def getprojection(self) -> tuple[list[int], list[int]]:
@@ -1746,7 +1751,7 @@ class Image:
def paste(
self,
im: Image | str | float | tuple[float, ...],
- box: tuple[int, int, int, int] | tuple[int, int] | None = None,
+ box: Image | tuple[int, int, int, int] | tuple[int, int] | None = None,
mask: Image | None = None,
) -> None:
"""
@@ -1788,10 +1793,14 @@ class Image:
:param mask: An optional mask image.
"""
- if isImageType(box) and mask is None:
+ if isImageType(box):
+ if mask is not None:
+ msg = "If using second argument as mask, third argument must be None"
+ raise ValueError(msg)
# abbreviated paste(im, mask) syntax
mask = box
box = None
+ assert not isinstance(box, Image)
if box is None:
box = (0, 0)
@@ -1887,7 +1896,7 @@ class Image:
def point(
self,
- lut: Sequence[float] | Callable[[int], float] | ImagePointHandler,
+ lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler,
mode: str | None = None,
) -> Image:
"""
@@ -1939,15 +1948,14 @@ class Image:
flatLut = [round(i) for i in flatLut]
return self._new(self.im.point(flatLut, mode))
- def putalpha(self, alpha):
+ def putalpha(self, alpha: Image | int) -> None:
"""
Adds or replaces the alpha layer in this image. If the image
does not have an alpha layer, it's converted to "LA" or "RGBA".
The new layer must be either "L" or "1".
:param alpha: The new alpha layer. This can either be an "L" or "1"
- image having the same size as this image, or an integer or
- other color value.
+ image having the same size as this image, or an integer.
"""
self._ensure_mutable()
@@ -1965,7 +1973,6 @@ class Image:
msg = "alpha channel could not be added"
raise ValueError(msg) from e # sanity check
self.im = im
- self.pyaccess = None
self._mode = self.im.mode
except KeyError as e:
msg = "illegal image mode"
@@ -1986,6 +1993,7 @@ class Image:
alpha = alpha.convert("L")
else:
# constant alpha
+ alpha = cast(int, alpha) # see python/typing#1013
try:
self.im.fillband(band, alpha)
except (AttributeError, ValueError):
@@ -1997,7 +2005,10 @@ class Image:
self.im.putband(alpha.im, band)
def putdata(
- self, data: Sequence[float], scale: float = 1.0, offset: float = 0.0
+ self,
+ data: Sequence[float] | Sequence[Sequence[int]] | NumpyArray,
+ scale: float = 1.0,
+ offset: float = 0.0,
) -> None:
"""
Copies pixel data from a flattened sequence object into the image. The
@@ -2016,7 +2027,11 @@ class Image:
self.im.putdata(data, scale, offset)
- def putpalette(self, data, rawmode="RGB") -> None:
+ def putpalette(
+ self,
+ data: ImagePalette.ImagePalette | bytes | Sequence[int],
+ rawmode: str = "RGB",
+ ) -> None:
"""
Attaches a palette to this image. The image must be a "P", "PA", "L"
or "LA" image.
@@ -2048,10 +2063,12 @@ class Image:
palette = ImagePalette.raw(rawmode, data)
self._mode = "PA" if "A" in self.mode else "P"
self.palette = palette
- self.palette.mode = "RGB"
+ self.palette.mode = "RGBA" if "A" in rawmode else "RGB"
self.load() # install new palette
- def putpixel(self, xy, value):
+ def putpixel(
+ self, xy: tuple[int, int], value: float | tuple[int, ...] | list[int]
+ ) -> None:
"""
Modifies the pixel at the given position. The color is given as
a single numerical value for single-band images, and a tuple for
@@ -2077,9 +2094,6 @@ class Image:
self._copy()
self.load()
- if self.pyaccess:
- return self.pyaccess.putpixel(xy, value)
-
if (
self.mode in ("P", "PA")
and isinstance(value, (list, tuple))
@@ -2089,12 +2103,13 @@ class Image:
if self.mode == "PA":
alpha = value[3] if len(value) == 4 else 255
value = value[:3]
- value = self.palette.getcolor(value, self)
- if self.mode == "PA":
- value = (value, alpha)
+ palette_index = self.palette.getcolor(value, self)
+ value = (palette_index, alpha) if self.mode == "PA" else palette_index
return self.im.putpixel(xy, value)
- def remap_palette(self, dest_map, source_palette=None):
+ def remap_palette(
+ self, dest_map: list[int], source_palette: bytes | bytearray | None = None
+ ) -> Image:
"""
Rewrites the image to reorder the palette.
@@ -2163,7 +2178,7 @@ class Image:
# m_im.putpalette(mapping_palette, 'L') # converts to 'P'
# or just force it.
# UNDONE -- this is part of the general issue with palettes
- m_im.im.putpalette(palette_mode + ";L", m_im.palette.tobytes())
+ m_im.im.putpalette(palette_mode, palette_mode + ";L", m_im.palette.tobytes())
m_im = m_im.convert("L")
@@ -2198,7 +2213,7 @@ class Image:
def resize(
self,
- size: tuple[int, int],
+ size: tuple[int, int] | list[int] | NumpyArray,
resample: int | None = None,
box: tuple[float, float, float, float] | None = None,
reducing_gap: float | None = None,
@@ -2206,7 +2221,7 @@ class Image:
"""
Returns a resized copy of this image.
- :param size: The requested size in pixels, as a 2-tuple:
+ :param size: The requested size in pixels, as a tuple or array:
(width, height).
:param resample: An optional resampling filter. This can be
one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`,
@@ -2271,6 +2286,7 @@ class Image:
if box is None:
box = (0, 0) + self.size
+ size = tuple(size)
if self.size == size and box == (0, 0) + self.size:
return self.copy()
@@ -2344,7 +2360,7 @@ class Image:
angle: float,
resample: Resampling = Resampling.NEAREST,
expand: int | bool = False,
- center: tuple[int, int] | None = None,
+ center: tuple[float, float] | None = None,
translate: tuple[int, int] | None = None,
fillcolor: float | tuple[float, ...] | str | None = None,
) -> Image:
@@ -2412,10 +2428,7 @@ class Image:
else:
post_trans = translate
if center is None:
- # FIXME These should be rounded to ints?
- rotn_center = (w / 2.0, h / 2.0)
- else:
- rotn_center = center
+ center = (w / 2, h / 2)
angle = -math.radians(angle)
matrix = [
@@ -2432,10 +2445,10 @@ class Image:
return a * x + b * y + c, d * x + e * y + f
matrix[2], matrix[5] = transform(
- -rotn_center[0] - post_trans[0], -rotn_center[1] - post_trans[1], matrix
+ -center[0] - post_trans[0], -center[1] - post_trans[1], matrix
)
- matrix[2] += rotn_center[0]
- matrix[5] += rotn_center[1]
+ matrix[2] += center[0]
+ matrix[5] += center[1]
if expand:
# calculate output size
@@ -2658,7 +2671,7 @@ class Image:
self,
size: tuple[float, float],
resample: Resampling = Resampling.BICUBIC,
- reducing_gap: float = 2.0,
+ reducing_gap: float | None = 2.0,
) -> None:
"""
Make this image into a thumbnail. This method modifies the
@@ -2719,11 +2732,12 @@ class Image:
return x, y
box = None
+ final_size: tuple[int, int]
if reducing_gap is not None:
preserved_size = preserve_aspect_ratio()
if preserved_size is None:
return
- size = preserved_size
+ final_size = preserved_size
res = self.draft(
None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap))
@@ -2737,17 +2751,16 @@ class Image:
preserved_size = preserve_aspect_ratio()
if preserved_size is None:
return
- size = preserved_size
+ final_size = preserved_size
- if self.size != size:
- im = self.resize(size, resample, box=box, reducing_gap=reducing_gap)
+ if self.size != final_size:
+ im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap)
self.im = im.im
- self._size = size
+ self._size = final_size
self._mode = self.im.mode
self.readonly = 0
- self.pyaccess = None
# FIXME: the different transform methods need further explanation
# instead of bloating the method docs, add a separate chapter.
@@ -3071,7 +3084,7 @@ def new(
and isinstance(color, (list, tuple))
and all(isinstance(i, int) for i in color)
):
- color_ints: tuple[int, ...] = cast(Tuple[int, ...], tuple(color))
+ color_ints: tuple[int, ...] = cast(tuple[int, ...], tuple(color))
if len(color_ints) == 3 or len(color_ints) == 4:
# RGB or RGBA value for a P image
rgb_color = True
@@ -3425,7 +3438,7 @@ def open(
preinit()
- accept_warnings: list[str] = []
+ warning_messages: list[str] = []
def _open_core(
fp: IO[bytes],
@@ -3441,17 +3454,15 @@ def open(
factory, accept = OPEN[i]
result = not accept or accept(prefix)
if isinstance(result, str):
- accept_warnings.append(result)
+ warning_messages.append(result)
elif result:
fp.seek(0)
im = factory(fp, filename)
_decompression_bomb_check(im.size)
return im
- except (SyntaxError, IndexError, TypeError, struct.error):
- # Leave disabled by default, spams the logs with image
- # opening failures that are entirely expected.
- # logger.debug("", exc_info=True)
- continue
+ except (SyntaxError, IndexError, TypeError, struct.error) as e:
+ if WARN_POSSIBLE_FORMATS:
+ warning_messages.append(i + " opening failed. " + str(e))
except BaseException:
if exclusive_fp:
fp.close()
@@ -3476,7 +3487,7 @@ def open(
if exclusive_fp:
fp.close()
- for message in accept_warnings:
+ for message in warning_messages:
warnings.warn(message)
msg = "cannot identify image file %r" % (filename if filename else fp)
raise UnidentifiedImageError(msg)
@@ -3541,7 +3552,7 @@ def composite(image1: Image, image2: Image, mask: Image) -> Image:
return image
-def eval(image, *args):
+def eval(image: Image, *args: Callable[[int], float]) -> Image:
"""
Applies the function (which should take one argument) to each pixel
in the given image. If the image has more than one band, the same
diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py
index 19a79facc..ec10230f1 100644
--- a/src/PIL/ImageCms.py
+++ b/src/PIL/ImageCms.py
@@ -299,6 +299,31 @@ class ImageCmsTransform(Image.ImagePointHandler):
proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC,
flags: Flags = Flags.NONE,
):
+ supported_modes = (
+ "RGB",
+ "RGBA",
+ "RGBX",
+ "CMYK",
+ "I;16",
+ "I;16L",
+ "I;16B",
+ "YCbCr",
+ "LAB",
+ "L",
+ "1",
+ )
+ for mode in (input_mode, output_mode):
+ if mode not in supported_modes:
+ deprecate(
+ mode,
+ 12,
+ {
+ "L;16": "I;16 or I;16L",
+ "L:16B": "I;16B",
+ "YCCA": "YCbCr",
+ "YCC": "YCbCr",
+ }.get(mode),
+ )
if proof is None:
self.transform = core.buildTransform(
input.profile, output.profile, input_mode, output_mode, intent, flags
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
index 41a3eb0cb..2b3620e71 100644
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -34,16 +34,26 @@ from __future__ import annotations
import math
import numbers
import struct
+from collections.abc import Sequence
from types import ModuleType
-from typing import TYPE_CHECKING, AnyStr, Sequence, cast
+from typing import TYPE_CHECKING, AnyStr, Callable, Union, cast
from . import Image, ImageColor
from ._deprecate import deprecate
from ._typing import Coords
+# experimental access to the outline API
+Outline: Callable[[], Image.core._Outline] | None
+try:
+ Outline = Image.core.outline
+except AttributeError:
+ Outline = None
+
if TYPE_CHECKING:
from . import ImageDraw2, ImageFont
+_Ink = Union[float, tuple[int, ...], str]
+
"""
A simple 2D drawing interface for PIL images.
@@ -53,7 +63,9 @@ directly.
class ImageDraw:
- font = None
+ font: (
+ ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None
+ ) = None
def __init__(self, im: Image.Image, mode: str | None = None) -> None:
"""
@@ -134,34 +146,47 @@ class ImageDraw:
else:
return self.getfont()
- def _getink(self, ink, fill=None) -> tuple[int | None, int | None]:
+ def _getink(
+ self, ink: _Ink | None, fill: _Ink | None = None
+ ) -> tuple[int | None, int | None]:
+ result_ink = None
+ result_fill = None
if ink is None and fill is None:
if self.fill:
- fill = self.ink
+ result_fill = self.ink
else:
- ink = self.ink
+ result_ink = self.ink
else:
if ink is not None:
if isinstance(ink, str):
ink = ImageColor.getcolor(ink, self.mode)
if self.palette and not isinstance(ink, numbers.Number):
ink = self.palette.getcolor(ink, self._image)
- ink = self.draw.draw_ink(ink)
+ result_ink = self.draw.draw_ink(ink)
if fill is not None:
if isinstance(fill, str):
fill = ImageColor.getcolor(fill, self.mode)
if self.palette and not isinstance(fill, numbers.Number):
fill = self.palette.getcolor(fill, self._image)
- fill = self.draw.draw_ink(fill)
- return ink, fill
+ result_fill = self.draw.draw_ink(fill)
+ return result_ink, result_fill
- def arc(self, xy: Coords, start, end, fill=None, width=1) -> None:
+ def arc(
+ self,
+ xy: Coords,
+ start: float,
+ end: float,
+ fill: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
"""Draw an arc."""
ink, fill = self._getink(fill)
if ink is not None:
self.draw.draw_arc(xy, start, end, ink, width)
- def bitmap(self, xy: Sequence[int], bitmap, fill=None) -> None:
+ def bitmap(
+ self, xy: Sequence[int], bitmap: Image.Image, fill: _Ink | None = None
+ ) -> None:
"""Draw a bitmap."""
bitmap.load()
ink, fill = self._getink(fill)
@@ -170,30 +195,55 @@ class ImageDraw:
if ink is not None:
self.draw.draw_bitmap(xy, bitmap.im, ink)
- def chord(self, xy: Coords, start, end, fill=None, outline=None, width=1) -> None:
+ def chord(
+ self,
+ xy: Coords,
+ start: float,
+ end: float,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
"""Draw a chord."""
- ink, fill = self._getink(outline, fill)
- if fill is not None:
- self.draw.draw_chord(xy, start, end, fill, 1)
- if ink is not None and ink != fill and width != 0:
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_chord(xy, start, end, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
self.draw.draw_chord(xy, start, end, ink, 0, width)
- def ellipse(self, xy: Coords, fill=None, outline=None, width=1) -> None:
+ def ellipse(
+ self,
+ xy: Coords,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
"""Draw an ellipse."""
- ink, fill = self._getink(outline, fill)
- if fill is not None:
- self.draw.draw_ellipse(xy, fill, 1)
- if ink is not None and ink != fill and width != 0:
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_ellipse(xy, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
self.draw.draw_ellipse(xy, ink, 0, width)
def circle(
- self, xy: Sequence[float], radius: float, fill=None, outline=None, width=1
+ self,
+ xy: Sequence[float],
+ radius: float,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
) -> None:
"""Draw a circle given center coordinates and a radius."""
ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius)
self.ellipse(ellipse_xy, fill, outline, width)
- def line(self, xy: Coords, fill=None, width=0, joint=None) -> None:
+ def line(
+ self,
+ xy: Coords,
+ fill: _Ink | None = None,
+ width: int = 0,
+ joint: str | None = None,
+ ) -> None:
"""Draw a line, or a connected sequence of line segments."""
ink = self._getink(fill)[0]
if ink is not None:
@@ -223,7 +273,7 @@ class ImageDraw:
def coord_at_angle(
coord: Sequence[float], angle: float
- ) -> tuple[float, float]:
+ ) -> tuple[float, ...]:
x, y = coord
angle -= 90
distance = width / 2 - 1
@@ -264,37 +314,54 @@ class ImageDraw:
]
self.line(gap_coords, fill, width=3)
- def shape(self, shape, fill=None, outline=None) -> None:
+ def shape(
+ self,
+ shape: Image.core._Outline,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ ) -> None:
"""(Experimental) Draw a shape."""
shape.close()
- ink, fill = self._getink(outline, fill)
- if fill is not None:
- self.draw.draw_outline(shape, fill, 1)
- if ink is not None and ink != fill:
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_outline(shape, fill_ink, 1)
+ if ink is not None and ink != fill_ink:
self.draw.draw_outline(shape, ink, 0)
def pieslice(
- self, xy: Coords, start, end, fill=None, outline=None, width=1
+ self,
+ xy: Coords,
+ start: float,
+ end: float,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
) -> None:
"""Draw a pieslice."""
- ink, fill = self._getink(outline, fill)
- if fill is not None:
- self.draw.draw_pieslice(xy, start, end, fill, 1)
- if ink is not None and ink != fill and width != 0:
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_pieslice(xy, start, end, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
self.draw.draw_pieslice(xy, start, end, ink, 0, width)
- def point(self, xy: Coords, fill=None) -> None:
+ def point(self, xy: Coords, fill: _Ink | None = None) -> None:
"""Draw one or more individual pixels."""
ink, fill = self._getink(fill)
if ink is not None:
self.draw.draw_points(xy, ink)
- def polygon(self, xy: Coords, fill=None, outline=None, width=1) -> None:
+ def polygon(
+ self,
+ xy: Coords,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
"""Draw a polygon."""
- ink, fill = self._getink(outline, fill)
- if fill is not None:
- self.draw.draw_polygon(xy, fill, 1)
- if ink is not None and ink != fill and width != 0:
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_polygon(xy, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
if width == 1:
self.draw.draw_polygon(xy, ink, 0, width)
elif self.im is not None:
@@ -320,22 +387,41 @@ class ImageDraw:
self.im.paste(im.im, (0, 0) + im.size, mask.im)
def regular_polygon(
- self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1
+ self,
+ bounding_circle: Sequence[Sequence[float] | float],
+ n_sides: int,
+ rotation: float = 0,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
) -> None:
"""Draw a regular polygon."""
xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
self.polygon(xy, fill, outline, width)
- def rectangle(self, xy: Coords, fill=None, outline=None, width=1) -> None:
+ def rectangle(
+ self,
+ xy: Coords,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
"""Draw a rectangle."""
- ink, fill = self._getink(outline, fill)
- if fill is not None:
- self.draw.draw_rectangle(xy, fill, 1)
- if ink is not None and ink != fill and width != 0:
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_rectangle(xy, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
self.draw.draw_rectangle(xy, ink, 0, width)
def rounded_rectangle(
- self, xy: Coords, radius=0, fill=None, outline=None, width=1, *, corners=None
+ self,
+ xy: Coords,
+ radius: float = 0,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ *,
+ corners: tuple[bool, bool, bool, bool] | None = None,
) -> None:
"""Draw a rounded rectangle."""
if isinstance(xy[0], (list, tuple)):
@@ -377,10 +463,10 @@ class ImageDraw:
# that is a rectangle
return self.rectangle(xy, fill, outline, width)
- r = d // 2
- ink, fill = self._getink(outline, fill)
+ r = int(d // 2)
+ ink, fill_ink = self._getink(outline, fill)
- def draw_corners(pieslice) -> None:
+ def draw_corners(pieslice: bool) -> None:
parts: tuple[tuple[tuple[float, float, float, float], int, int], ...]
if full_x:
# Draw top and bottom halves
@@ -410,32 +496,32 @@ class ImageDraw:
)
for part in parts:
if pieslice:
- self.draw.draw_pieslice(*(part + (fill, 1)))
+ self.draw.draw_pieslice(*(part + (fill_ink, 1)))
else:
self.draw.draw_arc(*(part + (ink, width)))
- if fill is not None:
+ if fill_ink is not None:
draw_corners(True)
if full_x:
- self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill, 1)
+ self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1)
else:
- self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1)
+ self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1)
if not full_x and not full_y:
left = [x0, y0, x0 + r, y1]
if corners[0]:
left[1] += r + 1
if corners[3]:
left[3] -= r + 1
- self.draw.draw_rectangle(left, fill, 1)
+ self.draw.draw_rectangle(left, fill_ink, 1)
right = [x1 - r, y0, x1, y1]
if corners[1]:
right[1] += r + 1
if corners[2]:
right[3] -= r + 1
- self.draw.draw_rectangle(right, fill, 1)
- if ink is not None and ink != fill and width != 0:
+ self.draw.draw_rectangle(right, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
draw_corners(False)
if not full_x:
@@ -530,10 +616,11 @@ class ImageDraw:
embedded_color,
)
- def getink(fill):
- ink, fill = self._getink(fill)
+ def getink(fill: _Ink | None) -> int:
+ ink, fill_ink = self._getink(fill)
if ink is None:
- return fill
+ assert fill_ink is not None
+ return fill_ink
return ink
def draw_text(ink, stroke_width=0, stroke_offset=None) -> None:
@@ -897,13 +984,6 @@ def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
return ImageDraw(im, mode)
-# experimental access to the outline API
-try:
- Outline = Image.core.outline
-except AttributeError:
- Outline = None
-
-
def getdraw(
im: Image.Image | None = None, hints: list[str] | None = None
) -> tuple[ImageDraw2.Draw | None, ModuleType]:
@@ -928,7 +1008,9 @@ def floodfill(
thresh: float = 0,
) -> None:
"""
- (experimental) Fills a bounded region with a given color.
+ .. warning:: This method is experimental.
+
+ Fills a bounded region with a given color.
:param image: Target image.
:param xy: Seed position (a 2-item coordinate tuple). See
@@ -946,6 +1028,7 @@ def floodfill(
# based on an implementation by Eric S. Raymond
# amended by yo1995 @20180806
pixel = image.load()
+ assert pixel is not None
x, y = xy
try:
background = pixel[x, y]
@@ -983,12 +1066,12 @@ def floodfill(
def _compute_regular_polygon_vertices(
- bounding_circle, n_sides, rotation
+ bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float
) -> list[tuple[float, float]]:
"""
Generate a list of vertices for a 2D regular polygon.
- :param bounding_circle: The bounding circle is a tuple defined
+ :param bounding_circle: The bounding circle is a sequence defined
by a point and radius. The polygon is inscribed in this circle.
(e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``)
:param n_sides: Number of sides
@@ -1026,7 +1109,7 @@ def _compute_regular_polygon_vertices(
# 1. Error Handling
# 1.1 Check `n_sides` has an appropriate value
if not isinstance(n_sides, int):
- msg = "n_sides should be an int"
+ msg = "n_sides should be an int" # type: ignore[unreachable]
raise TypeError(msg)
if n_sides < 3:
msg = "n_sides should be an int > 2"
@@ -1038,9 +1121,24 @@ def _compute_regular_polygon_vertices(
raise TypeError(msg)
if len(bounding_circle) == 3:
- *centroid, polygon_radius = bounding_circle
- elif len(bounding_circle) == 2:
- centroid, polygon_radius = bounding_circle
+ if not all(isinstance(i, (int, float)) for i in bounding_circle):
+ msg = "bounding_circle should only contain numeric data"
+ raise ValueError(msg)
+
+ *centroid, polygon_radius = cast(list[float], list(bounding_circle))
+ elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)):
+ if not all(
+ isinstance(i, (int, float)) for i in bounding_circle[0]
+ ) or not isinstance(bounding_circle[1], (int, float)):
+ msg = "bounding_circle should only contain numeric data"
+ raise ValueError(msg)
+
+ if len(bounding_circle[0]) != 2:
+ msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
+ raise ValueError(msg)
+
+ centroid = cast(list[float], list(bounding_circle[0]))
+ polygon_radius = cast(float, bounding_circle[1])
else:
msg = (
"bounding_circle should contain 2D coordinates "
@@ -1048,25 +1146,17 @@ def _compute_regular_polygon_vertices(
)
raise ValueError(msg)
- if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)):
- msg = "bounding_circle should only contain numeric data"
- raise ValueError(msg)
-
- if not len(centroid) == 2:
- msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
- raise ValueError(msg)
-
if polygon_radius <= 0:
msg = "bounding_circle radius should be > 0"
raise ValueError(msg)
# 1.3 Check `rotation` has an appropriate value
if not isinstance(rotation, (int, float)):
- msg = "rotation should be an int or float"
+ msg = "rotation should be an int or float" # type: ignore[unreachable]
raise ValueError(msg)
# 2. Define Helper Functions
- def _apply_rotation(point: list[float], degrees: float) -> tuple[int, int]:
+ def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]:
return (
round(
point[0] * math.cos(math.radians(360 - degrees))
@@ -1082,7 +1172,7 @@ def _compute_regular_polygon_vertices(
),
)
- def _compute_polygon_vertex(angle: float) -> tuple[int, int]:
+ def _compute_polygon_vertex(angle: float) -> tuple[float, float]:
start_point = [polygon_radius, 0]
return _apply_rotation(start_point, angle)
diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py
index b42f5d9ea..e89a78be4 100644
--- a/src/PIL/ImageDraw2.py
+++ b/src/PIL/ImageDraw2.py
@@ -24,7 +24,10 @@
"""
from __future__ import annotations
+from typing import BinaryIO
+
from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
+from ._typing import StrOrBytesPath
class Pen:
@@ -45,7 +48,9 @@ class Brush:
class Font:
"""Stores a TrueType font and color"""
- def __init__(self, color, file, size=12):
+ def __init__(
+ self, color: str, file: StrOrBytesPath | BinaryIO, size: float = 12
+ ) -> None:
# FIXME: add support for bitmap fonts
self.color = ImageColor.getrgb(color)
self.font = ImageFont.truetype(file, size)
@@ -56,8 +61,16 @@ class Draw:
(Experimental) WCK-style drawing interface
"""
- def __init__(self, image, size=None, color=None):
- if not hasattr(image, "im"):
+ def __init__(
+ self,
+ image: Image.Image | str,
+ size: tuple[int, int] | list[int] | None = None,
+ color: float | tuple[float, ...] | str | None = None,
+ ) -> None:
+ if isinstance(image, str):
+ if size is None:
+ msg = "If image argument is mode string, size must be a list or tuple"
+ raise ValueError(msg)
image = Image.new(image, size, color)
self.draw = ImageDraw.Draw(image)
self.image = image
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index 36bd004bf..746106fba 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -65,7 +65,7 @@ Dict of known error codes returned from :meth:`.PyDecoder.decode`,
# Helpers
-def _get_oserror(error, *, encoder):
+def _get_oserror(error: int, *, encoder: bool) -> OSError:
try:
msg = Image.core.getcodecstatus(error)
except AttributeError:
@@ -76,7 +76,7 @@ def _get_oserror(error, *, encoder):
return OSError(msg)
-def raise_oserror(error):
+def raise_oserror(error: int) -> OSError:
deprecate(
"raise_oserror",
12,
@@ -154,11 +154,12 @@ class ImageFile(Image.Image):
self.fp.close()
raise
- def get_format_mimetype(self):
+ def get_format_mimetype(self) -> str | None:
if self.custom_mimetype:
return self.custom_mimetype
if self.format is not None:
return Image.MIME.get(self.format.upper())
+ return None
def __setstate__(self, state):
self.tile = []
@@ -365,7 +366,7 @@ class StubImageFile(ImageFile):
certain format, but relies on external code to load the file.
"""
- def _open(self):
+ def _open(self) -> None:
msg = "StubImageFile subclass must implement _open"
raise NotImplementedError(msg)
@@ -381,7 +382,7 @@ class StubImageFile(ImageFile):
self.__dict__ = image.__dict__
return image.load()
- def _load(self):
+ def _load(self) -> StubHandler | None:
"""(Hook) Find actual image loader."""
msg = "StubImageFile subclass must implement _load"
raise NotImplementedError(msg)
@@ -621,7 +622,7 @@ class PyCodecState:
self.xoff = 0
self.yoff = 0
- def extents(self):
+ def extents(self) -> tuple[int, int, int, int]:
return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize
@@ -661,7 +662,7 @@ class PyCodec:
"""
self.fd = fd
- def setimage(self, im, extents=None):
+ def setimage(self, im, extents: tuple[int, int, int, int] | None = None) -> None:
"""
Called from ImageFile to set the core output image for the codec
@@ -710,10 +711,10 @@ class PyDecoder(PyCodec):
_pulls_fd = False
@property
- def pulls_fd(self):
+ def pulls_fd(self) -> bool:
return self._pulls_fd
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
"""
Override to perform the decoding process.
@@ -738,6 +739,7 @@ class PyDecoder(PyCodec):
if not rawmode:
rawmode = self.mode
d = Image._getdecoder(self.mode, "raw", rawmode)
+ assert self.im is not None
d.setimage(self.im, self.state.extents())
s = d.decode(data)
@@ -760,7 +762,7 @@ class PyEncoder(PyCodec):
_pushes_fd = False
@property
- def pushes_fd(self):
+ def pushes_fd(self) -> bool:
return self._pushes_fd
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
@@ -775,7 +777,7 @@ class PyEncoder(PyCodec):
msg = "unavailable in base encoder"
raise NotImplementedError(msg)
- def encode_to_pyfd(self):
+ def encode_to_pyfd(self) -> tuple[int, int]:
"""
If ``pushes_fd`` is ``True``, then this method will be used,
and ``encode()`` will only be called once.
@@ -787,6 +789,7 @@ class PyEncoder(PyCodec):
return 0, -8 # bad configuration
bytes_consumed, errcode, data = self.encode(0)
if data:
+ assert self.fd is not None
self.fd.write(data)
return bytes_consumed, errcode
diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py
index 02288e135..8b0974b2c 100644
--- a/src/PIL/ImageFilter.py
+++ b/src/PIL/ImageFilter.py
@@ -18,13 +18,18 @@ from __future__ import annotations
import abc
import functools
+from collections.abc import Sequence
from types import ModuleType
-from typing import Any, Sequence
+from typing import TYPE_CHECKING, Any, Callable, cast
+
+if TYPE_CHECKING:
+ from . import _imaging
+ from ._typing import NumpyArray
class Filter:
@abc.abstractmethod
- def filter(self, image):
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
pass
@@ -33,7 +38,9 @@ class MultibandFilter(Filter):
class BuiltinFilter(MultibandFilter):
- def filter(self, image):
+ filterargs: tuple[Any, ...]
+
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
if image.mode == "P":
msg = "cannot filter palette images"
raise ValueError(msg)
@@ -91,7 +98,7 @@ class RankFilter(Filter):
self.size = size
self.rank = rank
- def filter(self, image):
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
if image.mode == "P":
msg = "cannot filter palette images"
raise ValueError(msg)
@@ -158,7 +165,7 @@ class ModeFilter(Filter):
def __init__(self, size: int = 3) -> None:
self.size = size
- def filter(self, image):
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
return image.modefilter(self.size)
@@ -176,9 +183,9 @@ class GaussianBlur(MultibandFilter):
def __init__(self, radius: float | Sequence[float] = 2) -> None:
self.radius = radius
- def filter(self, image):
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
xy = self.radius
- if not isinstance(xy, (tuple, list)):
+ if isinstance(xy, (int, float)):
xy = (xy, xy)
if xy == (0, 0):
return image.copy()
@@ -208,9 +215,9 @@ class BoxBlur(MultibandFilter):
raise ValueError(msg)
self.radius = radius
- def filter(self, image):
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
xy = self.radius
- if not isinstance(xy, (tuple, list)):
+ if isinstance(xy, (int, float)):
xy = (xy, xy)
if xy == (0, 0):
return image.copy()
@@ -241,7 +248,7 @@ class UnsharpMask(MultibandFilter):
self.percent = percent
self.threshold = threshold
- def filter(self, image):
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
return image.unsharp_mask(self.radius, self.percent, self.threshold)
@@ -387,8 +394,13 @@ class Color3DLUT(MultibandFilter):
name = "Color 3D LUT"
def __init__(
- self, size, table, channels: int = 3, target_mode: str | None = None, **kwargs
- ):
+ self,
+ size: int | tuple[int, int, int],
+ table: Sequence[float] | Sequence[Sequence[int]] | NumpyArray,
+ channels: int = 3,
+ target_mode: str | None = None,
+ **kwargs: bool,
+ ) -> None:
if channels not in (3, 4):
msg = "Only 3 or 4 output channels are supported"
raise ValueError(msg)
@@ -410,15 +422,16 @@ class Color3DLUT(MultibandFilter):
pass
if numpy and isinstance(table, numpy.ndarray):
+ numpy_table: NumpyArray = table
if copy_table:
- table = table.copy()
+ numpy_table = numpy_table.copy()
- if table.shape in [
+ if numpy_table.shape in [
(items * channels,),
(items, channels),
(size[2], size[1], size[0], channels),
]:
- table = table.reshape(items * channels)
+ table = numpy_table.reshape(items * channels)
else:
wrong_size = True
@@ -428,7 +441,8 @@ class Color3DLUT(MultibandFilter):
# Convert to a flat list
if table and isinstance(table[0], (list, tuple)):
- table, raw_table = [], table
+ raw_table = cast(Sequence[Sequence[int]], table)
+ flat_table: list[int] = []
for pixel in raw_table:
if len(pixel) != channels:
msg = (
@@ -436,7 +450,8 @@ class Color3DLUT(MultibandFilter):
f"have a length of {channels}."
)
raise ValueError(msg)
- table.extend(pixel)
+ flat_table.extend(pixel)
+ table = flat_table
if wrong_size or len(table) != items * channels:
msg = (
@@ -449,7 +464,7 @@ class Color3DLUT(MultibandFilter):
self.table = table
@staticmethod
- def _check_size(size: Any) -> list[int]:
+ def _check_size(size: Any) -> tuple[int, int, int]:
try:
_, _, _ = size
except ValueError as e:
@@ -457,7 +472,7 @@ class Color3DLUT(MultibandFilter):
raise ValueError(msg) from e
except TypeError:
size = (size, size, size)
- size = [int(x) for x in size]
+ size = tuple(int(x) for x in size)
for size_1d in size:
if not 2 <= size_1d <= 65:
msg = "Size should be in [2, 65] range."
@@ -465,7 +480,13 @@ class Color3DLUT(MultibandFilter):
return size
@classmethod
- def generate(cls, size, callback, channels=3, target_mode=None):
+ def generate(
+ cls,
+ size: int | tuple[int, int, int],
+ callback: Callable[[float, float, float], tuple[float, ...]],
+ channels: int = 3,
+ target_mode: str | None = None,
+ ) -> Color3DLUT:
"""Generates new LUT using provided callback.
:param size: Size of the table. Passed to the constructor.
@@ -482,7 +503,7 @@ class Color3DLUT(MultibandFilter):
msg = "Only 3 or 4 output channels are supported"
raise ValueError(msg)
- table = [0] * (size_1d * size_2d * size_3d * channels)
+ table: list[float] = [0] * (size_1d * size_2d * size_3d * channels)
idx_out = 0
for b in range(size_3d):
for g in range(size_2d):
@@ -500,7 +521,13 @@ class Color3DLUT(MultibandFilter):
_copy_table=False,
)
- def transform(self, callback, with_normals=False, channels=None, target_mode=None):
+ def transform(
+ self,
+ callback: Callable[..., tuple[float, ...]],
+ with_normals: bool = False,
+ channels: int | None = None,
+ target_mode: str | None = None,
+ ) -> Color3DLUT:
"""Transforms the table values using provided callback and returns
a new LUT with altered values.
@@ -564,7 +591,7 @@ class Color3DLUT(MultibandFilter):
r.append(f"target_mode={self.mode}")
return "<{}>".format(" ".join(r))
- def filter(self, image):
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
from . import Image
return image.color_lut_3d(
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index fa5608e6c..d260eef69 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -33,11 +33,12 @@ import sys
import warnings
from enum import IntEnum
from io import BytesIO
+from types import ModuleType
from typing import IO, TYPE_CHECKING, Any, BinaryIO
from . import Image
from ._typing import StrOrBytesPath
-from ._util import is_path
+from ._util import DeferredError, is_path
if TYPE_CHECKING:
from . import ImageFile
@@ -53,11 +54,10 @@ class Layout(IntEnum):
MAX_STRING_LENGTH = 1_000_000
+core: ModuleType | DeferredError
try:
from . import _imagingft as core
except ImportError as ex:
- from ._util import DeferredError
-
core = DeferredError.new(ex)
@@ -199,6 +199,7 @@ class FreeTypeFont:
"""FreeType font wrapper (requires _imagingft service)"""
font: Font
+ font_bytes: bytes
def __init__(
self,
@@ -210,6 +211,9 @@ class FreeTypeFont:
) -> None:
# FIXME: use service provider instead
+ if isinstance(core, DeferredError):
+ raise core.ex
+
if size <= 0:
msg = "font size must be greater than 0"
raise ValueError(msg)
@@ -279,7 +283,7 @@ class FreeTypeFont:
return self.font.ascent, self.font.descent
def getlength(
- self, text: str, mode="", direction=None, features=None, language=None
+ self, text: str | bytes, mode="", direction=None, features=None, language=None
) -> float:
"""
Returns length (in pixels with 1/64 precision) of given text when rendered
@@ -354,7 +358,7 @@ class FreeTypeFont:
def getbbox(
self,
- text: str,
+ text: str | bytes,
mode: str = "",
direction: str | None = None,
features: list[str] | None = None,
@@ -511,7 +515,7 @@ class FreeTypeFont:
def getmask2(
self,
- text: str,
+ text: str | bytes,
mode="",
direction=None,
features=None,
@@ -730,7 +734,7 @@ class TransposedFont:
return 0, 0, height, width
return 0, 0, width, height
- def getlength(self, text: str, *args, **kwargs) -> float:
+ def getlength(self, text: str | bytes, *args, **kwargs) -> float:
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"
raise ValueError(msg)
@@ -775,10 +779,15 @@ def truetype(
:param font: A filename or file-like object containing a TrueType font.
If the file is not found in this filename, the loader may also
- search in other directories, such as the :file:`fonts/`
- directory on Windows or :file:`/Library/Fonts/`,
- :file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on
- macOS.
+ search in other directories, such as:
+
+ * The :file:`fonts/` directory on Windows,
+ * :file:`/Library/Fonts/`, :file:`/System/Library/Fonts/`
+ and :file:`~/Library/Fonts/` on macOS.
+ * :file:`~/.local/share/fonts`, :file:`/usr/local/share/fonts`,
+ and :file:`/usr/share/fonts` on Linux; or those specified by
+ the ``XDG_DATA_HOME`` and ``XDG_DATA_DIRS`` environment variables
+ for user-installed and system-wide fonts, respectively.
:param size: The requested size, in pixels.
:param index: Which font face to load (default is first available face).
@@ -837,12 +846,21 @@ def truetype(
if windir:
dirs.append(os.path.join(windir, "fonts"))
elif sys.platform in ("linux", "linux2"):
- lindirs = os.environ.get("XDG_DATA_DIRS")
- if not lindirs:
- # According to the freedesktop spec, XDG_DATA_DIRS should
- # default to /usr/share
- lindirs = "/usr/share"
- dirs += [os.path.join(lindir, "fonts") for lindir in lindirs.split(":")]
+ data_home = os.environ.get("XDG_DATA_HOME")
+ if not data_home:
+ # The freedesktop spec defines the following default directory for
+ # when XDG_DATA_HOME is unset or empty. This user-level directory
+ # takes precedence over system-level directories.
+ data_home = os.path.expanduser("~/.local/share")
+ xdg_dirs = [data_home]
+
+ data_dirs = os.environ.get("XDG_DATA_DIRS")
+ if not data_dirs:
+ # Similarly, defaults are defined for the system-level directories
+ data_dirs = "/usr/local/share:/usr/share"
+ xdg_dirs += data_dirs.split(":")
+
+ dirs += [os.path.join(xdg_dir, "fonts") for xdg_dir in xdg_dirs]
elif sys.platform == "darwin":
dirs += [
"/Library/Fonts",
@@ -888,6 +906,142 @@ def load_path(filename: str | bytes) -> ImageFont:
raise OSError(msg)
+def load_default_imagefont() -> ImageFont:
+ f = ImageFont()
+ f._load_pilfont_data(
+ # courB08
+ BytesIO(
+ base64.b64decode(
+ b"""
+UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA
+BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL
+AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA
+AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB
+ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A
+BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB
+//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA
+AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH
+AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA
+ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv
+AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/
+/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5
+AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA
+AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG
+AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA
+BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA
+AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA
+2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF
+AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA////
++gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA
+////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA
+BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv
+AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA
+AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA
+AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA
+BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP//
+//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA
+AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF
+AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB
+mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn
+AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA
+AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7
+AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA
+Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB
+//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA
+AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ
+AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC
+DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ
+AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/
++wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5
+AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/
+///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG
+AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA
+BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA
+Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC
+eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG
+AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA////
++gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA
+////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA
+BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT
+AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A
+AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA
+Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA
+Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP//
+//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA
+AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ
+AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA
+LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5
+AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA
+AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5
+AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA
+AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG
+AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA
+EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK
+AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA
+pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG
+AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA////
++QAGAAIAzgAKANUAEw==
+"""
+ )
+ ),
+ Image.open(
+ BytesIO(
+ base64.b64decode(
+ b"""
+iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u
+Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9
+M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g
+LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F
+IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA
+Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791
+NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx
+in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9
+SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY
+AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt
+y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG
+ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY
+lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H
+/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3
+AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47
+c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/
+/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw
+pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv
+oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR
+evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA
+AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v//
+Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR
+w7IkEbzhVQAAAABJRU5ErkJggg==
+"""
+ )
+ )
+ ),
+ )
+ return f
+
+
def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
"""If FreeType support is available, load a version of Aileron Regular,
https://dotcolon.net/font/aileron, with a more limited character set.
@@ -902,9 +1056,8 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
:return: A font object.
"""
- f: FreeTypeFont | ImageFont
- if core.__class__.__name__ == "module" or size is not None:
- f = truetype(
+ if isinstance(core, ModuleType) or size is not None:
+ return truetype(
BytesIO(
base64.b64decode(
b"""
@@ -1134,137 +1287,4 @@ AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ==
10 if size is None else size,
layout_engine=Layout.BASIC,
)
- else:
- f = ImageFont()
- f._load_pilfont_data(
- # courB08
- BytesIO(
- base64.b64decode(
- b"""
-UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA
-BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL
-AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA
-AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB
-ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A
-BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB
-//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA
-AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH
-AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA
-ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv
-AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/
-/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5
-AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA
-AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG
-AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA
-BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA
-AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA
-2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF
-AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA////
-+gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA
-////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA
-BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv
-AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA
-AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA
-AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA
-BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP//
-//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA
-AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF
-AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB
-mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn
-AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA
-AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7
-AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA
-Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB
-//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA
-AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ
-AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC
-DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ
-AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/
-+wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5
-AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/
-///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG
-AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA
-BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA
-Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC
-eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG
-AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA////
-+gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA
-////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA
-BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT
-AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A
-AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA
-Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA
-Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP//
-//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA
-AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ
-AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA
-LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5
-AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA
-AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5
-AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA
-AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG
-AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA
-EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK
-AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA
-pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG
-AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA////
-+QAGAAIAzgAKANUAEw==
-"""
- )
- ),
- Image.open(
- BytesIO(
- base64.b64decode(
- b"""
-iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u
-Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9
-M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g
-LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F
-IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA
-Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791
-NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx
-in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9
-SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY
-AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt
-y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG
-ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY
-lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H
-/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3
-AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47
-c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/
-/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw
-pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv
-oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR
-evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA
-AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v//
-Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR
-w7IkEbzhVQAAAABJRU5ErkJggg==
-"""
- )
- )
- ),
- )
- return f
+ return load_default_imagefont()
diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
index 3f3be706d..e27ca7e50 100644
--- a/src/PIL/ImageGrab.py
+++ b/src/PIL/ImageGrab.py
@@ -26,7 +26,13 @@ import tempfile
from . import Image
-def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None):
+def grab(
+ bbox: tuple[int, int, int, int] | None = None,
+ include_layered_windows: bool = False,
+ all_screens: bool = False,
+ xdisplay: str | None = None,
+) -> Image.Image:
+ im: Image.Image
if xdisplay is None:
if sys.platform == "darwin":
fh, filepath = tempfile.mkstemp(".png")
@@ -63,14 +69,16 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
left, top, right, bottom = bbox
im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
return im
+ # Cast to Optional[str] needed for Windows and macOS.
+ display_name: str | None = xdisplay
try:
if not Image.core.HAVE_XCB:
msg = "Pillow was built without XCB support"
raise OSError(msg)
- size, data = Image.core.grabscreen_x11(xdisplay)
+ size, data = Image.core.grabscreen_x11(display_name)
except OSError:
if (
- xdisplay is None
+ display_name is None
and sys.platform not in ("darwin", "win32")
and shutil.which("gnome-screenshot")
):
@@ -94,7 +102,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
return im
-def grabclipboard():
+def grabclipboard() -> Image.Image | list[str] | None:
if sys.platform == "darwin":
fh, filepath = tempfile.mkstemp(".png")
os.close(fh)
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index cbe189cc9..44aad0c3c 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -21,7 +21,8 @@ from __future__ import annotations
import functools
import operator
import re
-from typing import Protocol, Sequence, cast
+from collections.abc import Sequence
+from typing import Protocol, cast
from . import ExifTags, Image, ImagePalette
@@ -361,7 +362,9 @@ def pad(
else:
out = Image.new(image.mode, size, color)
if resized.palette:
- out.putpalette(resized.getpalette())
+ palette = resized.getpalette()
+ if palette is not None:
+ out.putpalette(palette)
if resized.width != size[0]:
x = round((size[0] - resized.width) * max(0, min(centering[0], 1)))
out.paste(resized, (x, 0))
@@ -698,7 +701,6 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
transposed_image = image.transpose(method)
if in_place:
image.im = transposed_image.im
- image.pyaccess = None
image._size = transposed_image._size
exif_image = image if in_place else transposed_image
@@ -709,14 +711,18 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
exif_image.info["exif"] = exif.tobytes()
elif "Raw profile type exif" in exif_image.info:
exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
- elif "XML:com.adobe.xmp" in exif_image.info:
- for pattern in (
- r'tiff:Orientation="([0-9])"',
- r"([0-9])",
- ):
- exif_image.info["XML:com.adobe.xmp"] = re.sub(
- pattern, "", exif_image.info["XML:com.adobe.xmp"]
- )
+ for key in ("XML:com.adobe.xmp", "xmp"):
+ if key in exif_image.info:
+ for pattern in (
+ r'tiff:Orientation="([0-9])"',
+ r"([0-9])",
+ ):
+ value = exif_image.info[key]
+ exif_image.info[key] = (
+ re.sub(pattern, "", value)
+ if isinstance(value, str)
+ else re.sub(pattern.encode(), b"", value)
+ )
if not in_place:
return transposed_image
elif not in_place:
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
index 6473c4577..8ccecbd07 100644
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -18,7 +18,8 @@
from __future__ import annotations
import array
-from typing import IO, TYPE_CHECKING, Sequence
+from collections.abc import Sequence
+from typing import IO, TYPE_CHECKING
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
@@ -38,23 +39,27 @@ class ImagePalette:
Defaults to an empty palette.
"""
- def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None:
+ def __init__(
+ self,
+ mode: str = "RGB",
+ palette: Sequence[int] | bytes | bytearray | None = None,
+ ) -> None:
self.mode = mode
- self.rawmode = None # if set, palette contains raw data
+ self.rawmode: str | None = None # if set, palette contains raw data
self.palette = palette or bytearray()
self.dirty: int | None = None
@property
- def palette(self):
+ def palette(self) -> Sequence[int] | bytes | bytearray:
return self._palette
@palette.setter
- def palette(self, palette):
- self._colors = None
+ def palette(self, palette: Sequence[int] | bytes | bytearray) -> None:
+ self._colors: dict[tuple[int, ...], int] | None = None
self._palette = palette
@property
- def colors(self) -> dict[tuple[int, int, int] | tuple[int, int, int, int], int]:
+ def colors(self) -> dict[tuple[int, ...], int]:
if self._colors is None:
mode_len = len(self.mode)
self._colors = {}
@@ -66,9 +71,7 @@ class ImagePalette:
return self._colors
@colors.setter
- def colors(
- self, colors: dict[tuple[int, int, int] | tuple[int, int, int, int], int]
- ) -> None:
+ def colors(self, colors: dict[tuple[int, ...], int]) -> None:
self._colors = colors
def copy(self) -> ImagePalette:
@@ -82,7 +85,7 @@ class ImagePalette:
return new
- def getdata(self) -> tuple[str, bytes]:
+ def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]:
"""
Get palette contents in format suitable for the low-level
``im.putpalette`` primitive.
@@ -137,7 +140,7 @@ class ImagePalette:
def getcolor(
self,
- color: tuple[int, int, int] | tuple[int, int, int, int],
+ color: tuple[int, ...],
image: Image.Image | None = None,
) -> int:
"""Given an rgb tuple, allocate palette entry.
@@ -162,12 +165,13 @@ class ImagePalette:
except KeyError as e:
# allocate new color slot
index = self._new_color_index(image, e)
+ assert isinstance(self._palette, bytearray)
self.colors[color] = index
if index * 3 < len(self.palette):
self._palette = (
- self.palette[: index * 3]
+ self._palette[: index * 3]
+ bytes(color)
- + self.palette[index * 3 + 3 :]
+ + self._palette[index * 3 + 3 :]
)
else:
self._palette += bytes(color)
@@ -204,7 +208,7 @@ class ImagePalette:
# Internal
-def raw(rawmode, data) -> ImagePalette:
+def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette:
palette = ImagePalette()
palette.rawmode = rawmode
palette.palette = data
@@ -216,9 +220,9 @@ def raw(rawmode, data) -> ImagePalette:
# Factories
-def make_linear_lut(black, white):
+def make_linear_lut(black: int, white: float) -> list[int]:
if black == 0:
- return [white * i // 255 for i in range(256)]
+ return [int(white * i // 255) for i in range(256)]
msg = "unavailable when black is non-zero"
raise NotImplementedError(msg) # FIXME
@@ -251,15 +255,22 @@ def wedge(mode: str = "RGB") -> ImagePalette:
return ImagePalette(mode, [i // len(mode) for i in palette])
-def load(filename):
+def load(filename: str) -> tuple[bytes, str]:
# FIXME: supports GIMP gradients only
with open(filename, "rb") as fp:
- for paletteHandler in [
+ paletteHandlers: list[
+ type[
+ GimpPaletteFile.GimpPaletteFile
+ | GimpGradientFile.GimpGradientFile
+ | PaletteFile.PaletteFile
+ ]
+ ] = [
GimpPaletteFile.GimpPaletteFile,
GimpGradientFile.GimpGradientFile,
PaletteFile.PaletteFile,
- ]:
+ ]
+ for paletteHandler in paletteHandlers:
try:
fp.seek(0)
lut = paletteHandler(fp).getpalette()
diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py
index 293ba4941..35a37760c 100644
--- a/src/PIL/ImageQt.py
+++ b/src/PIL/ImageQt.py
@@ -152,7 +152,7 @@ def _toqclass_helper(im):
elif im.mode == "RGBA":
data = im.tobytes("raw", "BGRA")
format = qt_format.Format_ARGB32
- elif im.mode == "I;16" and hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+
+ elif im.mode == "I;16":
im = im.point(lambda i: i * 256)
format = qt_format.Format_Grayscale16
@@ -196,7 +196,7 @@ if qt_is_installed:
self.setColorTable(im_data["colortable"])
-def toqimage(im):
+def toqimage(im) -> ImageQt:
return ImageQt(im)
diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py
index f60b1e11e..d62893d9c 100644
--- a/src/PIL/ImageShow.py
+++ b/src/PIL/ImageShow.py
@@ -26,7 +26,7 @@ from . import Image
_viewers = []
-def register(viewer, order: int = 1) -> None:
+def register(viewer: type[Viewer] | Viewer, order: int = 1) -> None:
"""
The :py:func:`register` function is used to register additional viewers::
@@ -40,11 +40,8 @@ def register(viewer, order: int = 1) -> None:
Zero or a negative integer to prepend this viewer to the list,
a positive integer to append it.
"""
- try:
- if issubclass(viewer, Viewer):
- viewer = viewer()
- except TypeError:
- pass # raised if viewer wasn't a class
+ if isinstance(viewer, type) and issubclass(viewer, Viewer):
+ viewer = viewer()
if order > 0:
_viewers.append(viewer)
else:
@@ -118,6 +115,8 @@ class Viewer:
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
os.system(self.get_command(path, **options)) # nosec
return 1
@@ -142,6 +141,8 @@ class WindowsViewer(Viewer):
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
subprocess.Popen(
self.get_command(path, **options),
shell=True,
@@ -171,6 +172,8 @@ class MacViewer(Viewer):
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
subprocess.call(["open", "-a", "Preview.app", path])
executable = sys.executable or shutil.which("python3")
if executable:
@@ -215,6 +218,8 @@ class XDGViewer(UnixViewer):
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
subprocess.Popen(["xdg-open", path])
return 1
@@ -237,6 +242,8 @@ class DisplayViewer(UnixViewer):
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
args = ["display"]
title = options.get("title")
if title:
@@ -259,6 +266,8 @@ class GmDisplayViewer(UnixViewer):
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
subprocess.Popen(["gm", "display", path])
return 1
@@ -275,6 +284,8 @@ class EogViewer(UnixViewer):
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
subprocess.Popen(["eog", "-n", path])
return 1
@@ -299,6 +310,8 @@ class XVViewer(UnixViewer):
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
args = ["xv"]
title = options.get("title")
if title:
diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py
index 90defdbbc..6aa70ced3 100644
--- a/src/PIL/ImageTk.py
+++ b/src/PIL/ImageTk.py
@@ -28,8 +28,9 @@ from __future__ import annotations
import tkinter
from io import BytesIO
+from typing import Any
-from . import Image
+from . import Image, ImageFile
# --------------------------------------------------------------------
# Check for Tkinter interface hooks
@@ -49,14 +50,15 @@ def _pilbitmap_check() -> int:
return _pilbitmap_ok
-def _get_image_from_kw(kw):
+def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
source = None
if "file" in kw:
source = kw.pop("file")
elif "data" in kw:
source = BytesIO(kw.pop("data"))
- if source:
- return Image.open(source)
+ if not source:
+ return None
+ return Image.open(source)
def _pyimagingtkcall(command, photo, id):
@@ -96,12 +98,27 @@ class PhotoImage:
image file).
"""
- def __init__(self, image=None, size=None, **kw):
+ def __init__(
+ self,
+ image: Image.Image | str | None = None,
+ size: tuple[int, int] | None = None,
+ **kw: Any,
+ ) -> None:
# Tk compatibility: file or data
if image is None:
image = _get_image_from_kw(kw)
- if hasattr(image, "mode") and hasattr(image, "size"):
+ if image is None:
+ msg = "Image is required"
+ raise ValueError(msg)
+ elif isinstance(image, str):
+ mode = image
+ image = None
+
+ if size is None:
+ msg = "If first argument is mode, size is required"
+ raise ValueError(msg)
+ else:
# got an image instead of a mode
mode = image.mode
if mode == "P":
@@ -114,9 +131,6 @@ class PhotoImage:
mode = "RGB" # default
size = image.size
kw["width"], kw["height"] = size
- else:
- mode = image
- image = None
if mode not in ["1", "L", "RGB", "RGBA"]:
mode = Image.getmodebase(mode)
diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py
index 80a6116b7..a3d8f441a 100644
--- a/src/PIL/ImageTransform.py
+++ b/src/PIL/ImageTransform.py
@@ -14,7 +14,8 @@
#
from __future__ import annotations
-from typing import Any, Sequence
+from collections.abc import Sequence
+from typing import Any
from . import Image
@@ -24,7 +25,7 @@ class Transform(Image.ImageTransformHandler):
method: Image.Transform
- def __init__(self, data: Sequence[int]) -> None:
+ def __init__(self, data: Sequence[Any]) -> None:
self.data = data
def getdata(self) -> tuple[Image.Transform, Sequence[int]]:
diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py
index 6c29e2590..978c5a9d1 100644
--- a/src/PIL/ImageWin.py
+++ b/src/PIL/ImageWin.py
@@ -69,19 +69,22 @@ class Dib:
defines the size of the image.
"""
- def __init__(self, image, size=None):
- if hasattr(image, "mode") and hasattr(image, "size"):
+ def __init__(
+ self, image: Image.Image | str, size: tuple[int, int] | list[int] | None = None
+ ) -> None:
+ if isinstance(image, str):
+ mode = image
+ image = ""
+ else:
mode = image.mode
size = image.size
- else:
- mode = image
- image = None
if mode not in ["1", "L", "P", "RGB"]:
mode = Image.getmodebase(mode)
self.image = Image.core.display(mode, size)
self.mode = mode
self.size = size
if image:
+ assert not isinstance(image, str)
self.paste(image)
def expose(self, handle):
diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py
index 73df83bfb..a04616fbd 100644
--- a/src/PIL/IptcImagePlugin.py
+++ b/src/PIL/IptcImagePlugin.py
@@ -16,8 +16,8 @@
#
from __future__ import annotations
+from collections.abc import Sequence
from io import BytesIO
-from typing import Sequence
from . import Image, ImageFile
from ._binary import i16be as i16
@@ -148,7 +148,7 @@ class IptcImageFile(ImageFile.ImageFile):
if tag == (8, 10):
self.tile = [("iptc", (0, 0) + self.size, offset, compression)]
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
return ImageFile.ImageFile.load(self)
@@ -176,6 +176,7 @@ class IptcImageFile(ImageFile.ImageFile):
with Image.open(o) as _im:
_im.load()
self.im = _im.im
+ return None
Image.register_open(IptcImageFile.format, IptcImageFile)
diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py
index 39eb1c203..992b9ccaf 100644
--- a/src/PIL/Jpeg2KImagePlugin.py
+++ b/src/PIL/Jpeg2KImagePlugin.py
@@ -18,7 +18,7 @@ from __future__ import annotations
import io
import os
import struct
-from typing import IO, Tuple, cast
+from typing import IO, cast
from . import Image, ImageFile, ImagePalette, _binary
@@ -82,7 +82,7 @@ class BoxReader:
self.remaining_in_box = -1
# Read the length and type of the next box
- lbox, tbox = cast(Tuple[int, bytes], self.read_fields(">I4s"))
+ lbox, tbox = cast(tuple[int, bytes], self.read_fields(">I4s"))
if lbox == 1:
lbox = cast(int, self.read_fields(">Q")[0])
hlen = 16
@@ -97,7 +97,7 @@ class BoxReader:
return tbox
-def _parse_codestream(fp):
+def _parse_codestream(fp) -> tuple[tuple[int, int], str]:
"""Parse the JPEG 2000 codestream to extract the size and component
count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
@@ -122,7 +122,8 @@ def _parse_codestream(fp):
elif csiz == 4:
mode = "RGBA"
else:
- mode = ""
+ msg = "unable to determine J2K image mode"
+ raise SyntaxError(msg)
return size, mode
@@ -237,10 +238,6 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
msg = "not a JPEG 2000 file"
raise SyntaxError(msg)
- if self.size is None or not self.mode:
- msg = "unable to determine size/mode"
- raise SyntaxError(msg)
-
self._reduce = 0
self.layers = 0
@@ -302,7 +299,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
def reduce(self, value):
self._reduce = value
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if self.tile and self._reduce:
power = 1 << self._reduce
adjust = power >> 1
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index 0c8a67888..b15bf06d2 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -95,6 +95,8 @@ def APP(self, marker):
else:
self.info["exif"] = s
self._exif_offset = self.fp.tell() - n + 6
+ elif marker == 0xFFE1 and s[:29] == b"http://ns.adobe.com/xap/1.0/\x00":
+ self.info["xmp"] = s.split(b"\x00", 1)[1]
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete)
self.info["flashpix"] = s # FIXME: value will change
@@ -159,38 +161,6 @@ def APP(self, marker):
# plus constant header size
self.info["mpoffset"] = self.fp.tell() - n + 4
- # If DPI isn't in JPEG header, fetch from EXIF
- if "dpi" not in self.info and "exif" in self.info:
- try:
- exif = self.getexif()
- resolution_unit = exif[0x0128]
- x_resolution = exif[0x011A]
- try:
- dpi = float(x_resolution[0]) / x_resolution[1]
- except TypeError:
- dpi = x_resolution
- if math.isnan(dpi):
- msg = "DPI is not a number"
- raise ValueError(msg)
- if resolution_unit == 3: # cm
- # 1 dpcm = 2.54 dpi
- dpi *= 2.54
- self.info["dpi"] = dpi, dpi
- except (
- struct.error,
- KeyError,
- SyntaxError,
- TypeError,
- ValueError,
- ZeroDivisionError,
- ):
- # struct.error for truncated EXIF
- # KeyError for dpi not included
- # SyntaxError for invalid/unreadable EXIF
- # ValueError or TypeError for dpi being an invalid float
- # ZeroDivisionError for invalid dpi rational value
- self.info["dpi"] = 72, 72
-
def COM(self: JpegImageFile, marker: int) -> None:
#
@@ -409,6 +379,8 @@ class JpegImageFile(ImageFile.ImageFile):
msg = "no marker found"
raise SyntaxError(msg)
+ self._read_dpi_from_exif()
+
def load_read(self, read_bytes: int) -> bytes:
"""
internal: read more image data
@@ -426,7 +398,7 @@ class JpegImageFile(ImageFile.ImageFile):
return s
def draft(
- self, mode: str | None, size: tuple[int, int]
+ self, mode: str | None, size: tuple[int, int] | None
) -> tuple[str, tuple[int, int, float, float]] | None:
if len(self.tile) != 1:
return None
@@ -497,24 +469,38 @@ class JpegImageFile(ImageFile.ImageFile):
def _getexif(self) -> dict[str, Any] | None:
return _getexif(self)
+ def _read_dpi_from_exif(self) -> None:
+ # If DPI isn't in JPEG header, fetch from EXIF
+ if "dpi" in self.info or "exif" not in self.info:
+ return
+ try:
+ exif = self.getexif()
+ resolution_unit = exif[0x0128]
+ x_resolution = exif[0x011A]
+ try:
+ dpi = float(x_resolution[0]) / x_resolution[1]
+ except TypeError:
+ dpi = x_resolution
+ if math.isnan(dpi):
+ msg = "DPI is not a number"
+ raise ValueError(msg)
+ if resolution_unit == 3: # cm
+ # 1 dpcm = 2.54 dpi
+ dpi *= 2.54
+ self.info["dpi"] = dpi, dpi
+ except (
+ struct.error, # truncated EXIF
+ KeyError, # dpi not included
+ SyntaxError, # invalid/unreadable EXIF
+ TypeError, # dpi is an invalid float
+ ValueError, # dpi is an invalid float
+ ZeroDivisionError, # invalid dpi rational value
+ ):
+ self.info["dpi"] = 72, 72
+
def _getmp(self):
return _getmp(self)
- def getxmp(self) -> dict[str, Any]:
- """
- Returns a dictionary containing the XMP tags.
- Requires defusedxml to be installed.
-
- :returns: XMP tags in a dictionary.
- """
-
- for segment, content in self.applist:
- if segment == "APP1":
- marker, xmp_tags = content.split(b"\x00")[:2]
- if marker == b"http://ns.adobe.com/xap/1.0/":
- return self._getxmp(xmp_tags)
- return {}
-
def _getexif(self) -> dict[str, Any] | None:
if "exif" not in self.info:
@@ -844,6 +830,10 @@ def jpeg_factory(fp=None, filename=None):
try:
mpheader = im._getmp()
if mpheader[45057] > 1:
+ for segment, content in im.applist:
+ if segment == "APP1" and b' hdrgm:Version="' in content:
+ # Ultra HDR images are not yet supported
+ return im
# It's actually an MPO
from .MpoImagePlugin import MpoImageFile
diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py
index 07239887f..5f23a34b9 100644
--- a/src/PIL/MicImagePlugin.py
+++ b/src/PIL/MicImagePlugin.py
@@ -70,7 +70,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self.__fp = self.fp
self.seek(0)
- def seek(self, frame):
+ def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
try:
diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py
index 9e2231347..7cb2d241b 100644
--- a/src/PIL/PdfParser.py
+++ b/src/PIL/PdfParser.py
@@ -8,7 +8,7 @@ import os
import re
import time
import zlib
-from typing import TYPE_CHECKING, Any, List, NamedTuple, Union
+from typing import IO, TYPE_CHECKING, Any, NamedTuple, Union
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
@@ -62,7 +62,7 @@ PDFDocEncoding = {
}
-def decode_text(b):
+def decode_text(b: bytes) -> str:
if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE:
return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be")
else:
@@ -99,7 +99,7 @@ class IndirectReference(IndirectReferenceTuple):
assert isinstance(other, IndirectReference)
return other.object_id == self.object_id and other.generation == self.generation
- def __ne__(self, other):
+ def __ne__(self, other: object) -> bool:
return not (self == other)
def __hash__(self) -> int:
@@ -112,13 +112,17 @@ class IndirectObjectDef(IndirectReference):
class XrefTable:
- def __init__(self):
- self.existing_entries = {} # object ID => (offset, generation)
- self.new_entries = {} # object ID => (offset, generation)
+ def __init__(self) -> None:
+ self.existing_entries: dict[int, tuple[int, int]] = (
+ {}
+ ) # object ID => (offset, generation)
+ self.new_entries: dict[int, tuple[int, int]] = (
+ {}
+ ) # object ID => (offset, generation)
self.deleted_entries = {0: 65536} # object ID => generation
self.reading_finished = False
- def __setitem__(self, key, value):
+ def __setitem__(self, key: int, value: tuple[int, int]) -> None:
if self.reading_finished:
self.new_entries[key] = value
else:
@@ -126,13 +130,13 @@ class XrefTable:
if key in self.deleted_entries:
del self.deleted_entries[key]
- def __getitem__(self, key):
+ def __getitem__(self, key: int) -> tuple[int, int]:
try:
return self.new_entries[key]
except KeyError:
return self.existing_entries[key]
- def __delitem__(self, key):
+ def __delitem__(self, key: int) -> None:
if key in self.new_entries:
generation = self.new_entries[key][1] + 1
del self.new_entries[key]
@@ -146,7 +150,7 @@ class XrefTable:
msg = f"object ID {key} cannot be deleted because it doesn't exist"
raise IndexError(msg)
- def __contains__(self, key):
+ def __contains__(self, key: int) -> bool:
return key in self.existing_entries or key in self.new_entries
def __len__(self) -> int:
@@ -156,19 +160,19 @@ class XrefTable:
| set(self.deleted_entries.keys())
)
- def keys(self):
+ def keys(self) -> set[int]:
return (
set(self.existing_entries.keys()) - set(self.deleted_entries.keys())
) | set(self.new_entries.keys())
- def write(self, f):
+ def write(self, f: IO[bytes]) -> int:
keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys()))
deleted_keys = sorted(set(self.deleted_entries.keys()))
startxref = f.tell()
f.write(b"xref\n")
while keys:
# find a contiguous sequence of object IDs
- prev = None
+ prev: int | None = None
for index, key in enumerate(keys):
if prev is None or prev + 1 == key:
prev = key
@@ -178,7 +182,7 @@ class XrefTable:
break
else:
contiguous_keys = keys
- keys = None
+ keys = []
f.write(b"%d %d\n" % (contiguous_keys[0], len(contiguous_keys)))
for object_id in contiguous_keys:
if object_id in self.new_entries:
@@ -202,7 +206,9 @@ class XrefTable:
class PdfName:
- def __init__(self, name):
+ name: bytes
+
+ def __init__(self, name: PdfName | bytes | str) -> None:
if isinstance(name, PdfName):
self.name = name.name
elif isinstance(name, bytes):
@@ -213,7 +219,7 @@ class PdfName:
def name_as_str(self) -> str:
return self.name.decode("us-ascii")
- def __eq__(self, other):
+ def __eq__(self, other: object) -> bool:
return (
isinstance(other, PdfName) and other.name == self.name
) or other == self.name
@@ -225,7 +231,7 @@ class PdfName:
return f"{self.__class__.__name__}({repr(self.name)})"
@classmethod
- def from_pdf_stream(cls, data):
+ def from_pdf_stream(cls, data: bytes) -> PdfName:
return cls(PdfParser.interpret_name(data))
allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"}
@@ -240,7 +246,7 @@ class PdfName:
return bytes(result)
-class PdfArray(List[Any]):
+class PdfArray(list[Any]):
def __bytes__(self) -> bytes:
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
@@ -252,13 +258,13 @@ else:
class PdfDict(_DictBase):
- def __setattr__(self, key, value):
+ def __setattr__(self, key: str, value: Any) -> None:
if key == "data":
collections.UserDict.__setattr__(self, key, value)
else:
self[key.encode("us-ascii")] = value
- def __getattr__(self, key):
+ def __getattr__(self, key: str) -> str | time.struct_time:
try:
value = self[key.encode("us-ascii")]
except KeyError as e:
@@ -300,7 +306,7 @@ class PdfDict(_DictBase):
class PdfBinary:
- def __init__(self, data):
+ def __init__(self, data: list[int] | bytes) -> None:
self.data = data
def __bytes__(self) -> bytes:
@@ -308,27 +314,27 @@ class PdfBinary:
class PdfStream:
- def __init__(self, dictionary, buf):
+ def __init__(self, dictionary: PdfDict, buf: bytes) -> None:
self.dictionary = dictionary
self.buf = buf
- def decode(self):
+ def decode(self) -> bytes:
try:
- filter = self.dictionary.Filter
- except AttributeError:
+ filter = self.dictionary[b"Filter"]
+ except KeyError:
return self.buf
if filter == b"FlateDecode":
try:
- expected_length = self.dictionary.DL
- except AttributeError:
- expected_length = self.dictionary.Length
+ expected_length = self.dictionary[b"DL"]
+ except KeyError:
+ expected_length = self.dictionary[b"Length"]
return zlib.decompress(self.buf, bufsize=int(expected_length))
else:
- msg = f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported"
+ msg = f"stream filter {repr(filter)} unknown/unsupported"
raise NotImplementedError(msg)
-def pdf_repr(x):
+def pdf_repr(x: Any) -> bytes:
if x is True:
return b"true"
elif x is False:
@@ -363,12 +369,19 @@ class PdfParser:
Supports PDF up to 1.4
"""
- def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"):
+ def __init__(
+ self,
+ filename: str | None = None,
+ f: IO[bytes] | None = None,
+ buf: bytes | bytearray | None = None,
+ start_offset: int = 0,
+ mode: str = "rb",
+ ) -> None:
if buf and f:
msg = "specify buf or f or filename, but not both buf and f"
raise RuntimeError(msg)
self.filename = filename
- self.buf = buf
+ self.buf: bytes | bytearray | mmap.mmap | None = buf
self.f = f
self.start_offset = start_offset
self.should_close_buf = False
@@ -377,12 +390,16 @@ class PdfParser:
self.f = f = open(filename, mode)
self.should_close_file = True
if f is not None:
- self.buf = buf = self.get_buf_from_file(f)
+ self.buf = self.get_buf_from_file(f)
self.should_close_buf = True
if not filename and hasattr(f, "name"):
self.filename = f.name
- self.cached_objects = {}
- if buf:
+ self.cached_objects: dict[IndirectReference, Any] = {}
+ self.root_ref: IndirectReference | None
+ self.info_ref: IndirectReference | None
+ self.pages_ref: IndirectReference | None
+ self.last_xref_section_offset: int | None
+ if self.buf:
self.read_pdf_info()
else:
self.file_size_total = self.file_size_this = 0
@@ -390,12 +407,12 @@ class PdfParser:
self.root_ref = None
self.info = PdfDict()
self.info_ref = None
- self.page_tree_root = {}
- self.pages = []
- self.orig_pages = []
+ self.page_tree_root = PdfDict()
+ self.pages: list[IndirectReference] = []
+ self.orig_pages: list[IndirectReference] = []
self.pages_ref = None
self.last_xref_section_offset = None
- self.trailer_dict = {}
+ self.trailer_dict: dict[bytes, Any] = {}
self.xref_table = XrefTable()
self.xref_table.reading_finished = True
if f:
@@ -412,10 +429,8 @@ class PdfParser:
self.seek_end()
def close_buf(self) -> None:
- try:
+ if isinstance(self.buf, mmap.mmap):
self.buf.close()
- except AttributeError:
- pass
self.buf = None
def close(self) -> None:
@@ -426,15 +441,19 @@ class PdfParser:
self.f = None
def seek_end(self) -> None:
+ assert self.f is not None
self.f.seek(0, os.SEEK_END)
def write_header(self) -> None:
+ assert self.f is not None
self.f.write(b"%PDF-1.4\n")
- def write_comment(self, s):
+ def write_comment(self, s: str) -> None:
+ assert self.f is not None
self.f.write(f"% {s}\n".encode())
def write_catalog(self) -> IndirectReference:
+ assert self.f is not None
self.del_root()
self.root_ref = self.next_object_id(self.f.tell())
self.pages_ref = self.next_object_id(0)
@@ -477,7 +496,10 @@ class PdfParser:
pages_tree_node_ref = pages_tree_node.get(b"Parent", None)
self.orig_pages = []
- def write_xref_and_trailer(self, new_root_ref=None):
+ def write_xref_and_trailer(
+ self, new_root_ref: IndirectReference | None = None
+ ) -> None:
+ assert self.f is not None
if new_root_ref:
self.del_root()
self.root_ref = new_root_ref
@@ -485,7 +507,10 @@ class PdfParser:
self.info_ref = self.write_obj(None, self.info)
start_xref = self.xref_table.write(self.f)
num_entries = len(self.xref_table)
- trailer_dict = {b"Root": self.root_ref, b"Size": num_entries}
+ trailer_dict: dict[str | bytes, Any] = {
+ b"Root": self.root_ref,
+ b"Size": num_entries,
+ }
if self.last_xref_section_offset is not None:
trailer_dict[b"Prev"] = self.last_xref_section_offset
if self.info:
@@ -497,16 +522,20 @@ class PdfParser:
+ b"\nstartxref\n%d\n%%%%EOF" % start_xref
)
- def write_page(self, ref, *objs, **dict_obj):
- if isinstance(ref, int):
- ref = self.pages[ref]
+ def write_page(
+ self, ref: int | IndirectReference | None, *objs: Any, **dict_obj: Any
+ ) -> IndirectReference:
+ obj_ref = self.pages[ref] if isinstance(ref, int) else ref
if "Type" not in dict_obj:
dict_obj["Type"] = PdfName(b"Page")
if "Parent" not in dict_obj:
dict_obj["Parent"] = self.pages_ref
- return self.write_obj(ref, *objs, **dict_obj)
+ return self.write_obj(obj_ref, *objs, **dict_obj)
- def write_obj(self, ref, *objs, **dict_obj):
+ def write_obj(
+ self, ref: IndirectReference | None, *objs: Any, **dict_obj: Any
+ ) -> IndirectReference:
+ assert self.f is not None
f = self.f
if ref is None:
ref = self.next_object_id(f.tell())
@@ -534,7 +563,7 @@ class PdfParser:
del self.xref_table[self.root[b"Pages"].object_id]
@staticmethod
- def get_buf_from_file(f):
+ def get_buf_from_file(f: IO[bytes]) -> bytes | mmap.mmap:
if hasattr(f, "getbuffer"):
return f.getbuffer()
elif hasattr(f, "getvalue"):
@@ -546,10 +575,15 @@ class PdfParser:
return b""
def read_pdf_info(self) -> None:
+ assert self.buf is not None
self.file_size_total = len(self.buf)
self.file_size_this = self.file_size_total - self.start_offset
self.read_trailer()
+ check_format_condition(
+ self.trailer_dict.get(b"Root") is not None, "Root is missing"
+ )
self.root_ref = self.trailer_dict[b"Root"]
+ assert self.root_ref is not None
self.info_ref = self.trailer_dict.get(b"Info", None)
self.root = PdfDict(self.read_indirect(self.root_ref))
if self.info_ref is None:
@@ -560,12 +594,15 @@ class PdfParser:
check_format_condition(
self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog"
)
- check_format_condition(b"Pages" in self.root, "/Pages missing in Root")
+ check_format_condition(
+ self.root.get(b"Pages") is not None, "/Pages missing in Root"
+ )
check_format_condition(
isinstance(self.root[b"Pages"], IndirectReference),
"/Pages in Root is not an indirect reference",
)
self.pages_ref = self.root[b"Pages"]
+ assert self.pages_ref is not None
self.page_tree_root = self.read_indirect(self.pages_ref)
self.pages = self.linearize_page_tree(self.page_tree_root)
# save the original list of page references
@@ -573,7 +610,7 @@ class PdfParser:
# and we need to rewrite the pages and their list
self.orig_pages = self.pages[:]
- def next_object_id(self, offset=None):
+ def next_object_id(self, offset: int | None = None) -> IndirectReference:
try:
# TODO: support reuse of deleted objects
reference = IndirectReference(max(self.xref_table.keys()) + 1, 0)
@@ -623,12 +660,13 @@ class PdfParser:
re.DOTALL,
)
- def read_trailer(self):
+ def read_trailer(self) -> None:
+ assert self.buf is not None
search_start_offset = len(self.buf) - 16384
if search_start_offset < self.start_offset:
search_start_offset = self.start_offset
m = self.re_trailer_end.search(self.buf, search_start_offset)
- check_format_condition(m, "trailer end not found")
+ check_format_condition(m is not None, "trailer end not found")
# make sure we found the LAST trailer
last_match = m
while m:
@@ -636,6 +674,7 @@ class PdfParser:
m = self.re_trailer_end.search(self.buf, m.start() + 16)
if not m:
m = last_match
+ assert m is not None
trailer_data = m.group(1)
self.last_xref_section_offset = int(m.group(2))
self.trailer_dict = self.interpret_trailer(trailer_data)
@@ -644,12 +683,14 @@ class PdfParser:
if b"Prev" in self.trailer_dict:
self.read_prev_trailer(self.trailer_dict[b"Prev"])
- def read_prev_trailer(self, xref_section_offset):
+ def read_prev_trailer(self, xref_section_offset: int) -> None:
+ assert self.buf is not None
trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset)
m = self.re_trailer_prev.search(
self.buf[trailer_offset : trailer_offset + 16384]
)
- check_format_condition(m, "previous trailer not found")
+ check_format_condition(m is not None, "previous trailer not found")
+ assert m is not None
trailer_data = m.group(1)
check_format_condition(
int(m.group(2)) == xref_section_offset,
@@ -670,7 +711,7 @@ class PdfParser:
re_dict_end = re.compile(whitespace_optional + rb">>" + whitespace_optional)
@classmethod
- def interpret_trailer(cls, trailer_data):
+ def interpret_trailer(cls, trailer_data: bytes) -> dict[bytes, Any]:
trailer = {}
offset = 0
while True:
@@ -678,14 +719,18 @@ class PdfParser:
if not m:
m = cls.re_dict_end.match(trailer_data, offset)
check_format_condition(
- m and m.end() == len(trailer_data),
+ m is not None and m.end() == len(trailer_data),
"name not found in trailer, remaining data: "
+ repr(trailer_data[offset:]),
)
break
key = cls.interpret_name(m.group(1))
- value, offset = cls.get_value(trailer_data, m.end())
+ assert isinstance(key, bytes)
+ value, value_offset = cls.get_value(trailer_data, m.end())
trailer[key] = value
+ if value_offset is None:
+ break
+ offset = value_offset
check_format_condition(
b"Size" in trailer and isinstance(trailer[b"Size"], int),
"/Size not in trailer or not an integer",
@@ -699,7 +744,7 @@ class PdfParser:
re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?")
@classmethod
- def interpret_name(cls, raw, as_text=False):
+ def interpret_name(cls, raw: bytes, as_text: bool = False) -> str | bytes:
name = b""
for m in cls.re_hashes_in_name.finditer(raw):
if m.group(3):
@@ -761,7 +806,13 @@ class PdfParser:
)
@classmethod
- def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1):
+ def get_value(
+ cls,
+ data: bytes | bytearray | mmap.mmap,
+ offset: int,
+ expect_indirect: IndirectReference | None = None,
+ max_nesting: int = -1,
+ ) -> tuple[Any, int | None]:
if max_nesting == 0:
return None, None
m = cls.re_comment.match(data, offset)
@@ -783,11 +834,16 @@ class PdfParser:
== IndirectReference(int(m.group(1)), int(m.group(2))),
"indirect object definition different than expected",
)
- object, offset = cls.get_value(data, m.end(), max_nesting=max_nesting - 1)
- if offset is None:
+ object, object_offset = cls.get_value(
+ data, m.end(), max_nesting=max_nesting - 1
+ )
+ if object_offset is None:
return object, None
- m = cls.re_indirect_def_end.match(data, offset)
- check_format_condition(m, "indirect object definition end not found")
+ m = cls.re_indirect_def_end.match(data, object_offset)
+ check_format_condition(
+ m is not None, "indirect object definition end not found"
+ )
+ assert m is not None
return object, m.end()
check_format_condition(
not expect_indirect, "indirect object definition not found"
@@ -806,46 +862,53 @@ class PdfParser:
m = cls.re_dict_start.match(data, offset)
if m:
offset = m.end()
- result = {}
+ result: dict[Any, Any] = {}
m = cls.re_dict_end.match(data, offset)
+ current_offset: int | None = offset
while not m:
- key, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
- if offset is None:
+ assert current_offset is not None
+ key, current_offset = cls.get_value(
+ data, current_offset, max_nesting=max_nesting - 1
+ )
+ if current_offset is None:
return result, None
- value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
+ value, current_offset = cls.get_value(
+ data, current_offset, max_nesting=max_nesting - 1
+ )
result[key] = value
- if offset is None:
+ if current_offset is None:
return result, None
- m = cls.re_dict_end.match(data, offset)
- offset = m.end()
- m = cls.re_stream_start.match(data, offset)
+ m = cls.re_dict_end.match(data, current_offset)
+ current_offset = m.end()
+ m = cls.re_stream_start.match(data, current_offset)
if m:
- try:
- stream_len_str = result.get(b"Length")
- stream_len = int(stream_len_str)
- except (TypeError, ValueError) as e:
- msg = f"bad or missing Length in stream dict ({stream_len_str})"
- raise PdfFormatError(msg) from e
+ stream_len = result.get(b"Length")
+ if stream_len is None or not isinstance(stream_len, int):
+ msg = f"bad or missing Length in stream dict ({stream_len})"
+ raise PdfFormatError(msg)
stream_data = data[m.end() : m.end() + stream_len]
m = cls.re_stream_end.match(data, m.end() + stream_len)
- check_format_condition(m, "stream end not found")
- offset = m.end()
- result = PdfStream(PdfDict(result), stream_data)
- else:
- result = PdfDict(result)
- return result, offset
+ check_format_condition(m is not None, "stream end not found")
+ assert m is not None
+ current_offset = m.end()
+ return PdfStream(PdfDict(result), stream_data), current_offset
+ return PdfDict(result), current_offset
m = cls.re_array_start.match(data, offset)
if m:
offset = m.end()
- result = []
+ results = []
m = cls.re_array_end.match(data, offset)
+ current_offset = offset
while not m:
- value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
- result.append(value)
- if offset is None:
- return result, None
- m = cls.re_array_end.match(data, offset)
- return result, m.end()
+ assert current_offset is not None
+ value, current_offset = cls.get_value(
+ data, current_offset, max_nesting=max_nesting - 1
+ )
+ results.append(value)
+ if current_offset is None:
+ return results, None
+ m = cls.re_array_end.match(data, current_offset)
+ return results, m.end()
m = cls.re_null.match(data, offset)
if m:
return None, m.end()
@@ -905,7 +968,9 @@ class PdfParser:
}
@classmethod
- def get_literal_string(cls, data, offset):
+ def get_literal_string(
+ cls, data: bytes | bytearray | mmap.mmap, offset: int
+ ) -> tuple[bytes, int]:
nesting_depth = 0
result = bytearray()
for m in cls.re_lit_str_token.finditer(data, offset):
@@ -941,12 +1006,14 @@ class PdfParser:
)
re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)")
- def read_xref_table(self, xref_section_offset):
+ def read_xref_table(self, xref_section_offset: int) -> int:
+ assert self.buf is not None
subsection_found = False
m = self.re_xref_section_start.match(
self.buf, xref_section_offset + self.start_offset
)
- check_format_condition(m, "xref section start not found")
+ check_format_condition(m is not None, "xref section start not found")
+ assert m is not None
offset = m.end()
while True:
m = self.re_xref_subsection_start.match(self.buf, offset)
@@ -961,7 +1028,8 @@ class PdfParser:
num_objects = int(m.group(2))
for i in range(first_object, first_object + num_objects):
m = self.re_xref_entry.match(self.buf, offset)
- check_format_condition(m, "xref entry not found")
+ check_format_condition(m is not None, "xref entry not found")
+ assert m is not None
offset = m.end()
is_free = m.group(3) == b"f"
if not is_free:
@@ -971,13 +1039,14 @@ class PdfParser:
self.xref_table[i] = new_entry
return offset
- def read_indirect(self, ref, max_nesting=-1):
+ def read_indirect(self, ref: IndirectReference, max_nesting: int = -1) -> Any:
offset, generation = self.xref_table[ref[0]]
check_format_condition(
generation == ref[1],
f"expected to find generation {ref[1]} for object ID {ref[0]} in xref "
f"table, instead found generation {generation} at offset {offset}",
)
+ assert self.buf is not None
value = self.get_value(
self.buf,
offset + self.start_offset,
@@ -987,14 +1056,15 @@ class PdfParser:
self.cached_objects[ref] = value
return value
- def linearize_page_tree(self, node=None):
- if node is None:
- node = self.page_tree_root
+ def linearize_page_tree(
+ self, node: PdfDict | None = None
+ ) -> list[IndirectReference]:
+ page_node = node if node is not None else self.page_tree_root
check_format_condition(
- node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages"
+ page_node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages"
)
pages = []
- for kid in node[b"Kids"]:
+ for kid in page_node[b"Kids"]:
kid_object = self.read_indirect(kid)
if kid_object[b"Type"] == b"Page":
pages.append(kid)
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index 927d6c0cf..34ea77c5e 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -624,6 +624,8 @@ class PngStream(ChunkStream):
return s
else:
return s
+ if k == b"XML:com.adobe.xmp":
+ self.im_info["xmp"] = v
try:
k = k.decode("latin-1", "strict")
lang = lang.decode("utf-8", "strict")
@@ -849,8 +851,6 @@ class PngImageFile(ImageFile.ImageFile):
self.png.rewind()
self.__prepare_idat = self.__rewind_idat
self.im = None
- if self.pyaccess:
- self.pyaccess = None
self.info = self.png.im_info
self.tile = self.png.im_tile
self.fp = self._fp
@@ -1037,8 +1037,6 @@ class PngImageFile(ImageFile.ImageFile):
mask = updated.convert("RGBA")
self._prev_im.paste(updated, self.dispose_extent, mask)
self.im = self._prev_im
- if self.pyaccess:
- self.pyaccess = None
def _getexif(self) -> dict[str, Any] | None:
if "exif" not in self.info:
@@ -1053,19 +1051,6 @@ class PngImageFile(ImageFile.ImageFile):
return super().getexif()
- def getxmp(self) -> dict[str, Any]:
- """
- Returns a dictionary containing the XMP tags.
- Requires defusedxml to be installed.
-
- :returns: XMP tags in a dictionary.
- """
- return (
- self._getxmp(self.info["XML:com.adobe.xmp"])
- if "XML:com.adobe.xmp" in self.info
- else {}
- )
-
# --------------------------------------------------------------------
# PNG writer
@@ -1125,8 +1110,8 @@ class _fdat:
self.seq_num += 1
-def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images):
- duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
+def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_images):
+ duration = im.encoderinfo.get("duration")
loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))
@@ -1140,13 +1125,15 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
frame_count = 0
for im_seq in chain:
for im_frame in ImageSequence.Iterator(im_seq):
- if im_frame.mode == rawmode:
+ if im_frame.mode == mode:
im_frame = im_frame.copy()
else:
- im_frame = im_frame.convert(rawmode)
+ im_frame = im_frame.convert(mode)
encoderinfo = im.encoderinfo.copy()
if isinstance(duration, (list, tuple)):
encoderinfo["duration"] = duration[frame_count]
+ elif duration is None and "duration" in im_frame.info:
+ encoderinfo["duration"] = im_frame.info["duration"]
if isinstance(disposal, (list, tuple)):
encoderinfo["disposal"] = disposal[frame_count]
if isinstance(blend, (list, tuple)):
@@ -1181,15 +1168,12 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
not bbox
and prev_disposal == encoderinfo.get("disposal")
and prev_blend == encoderinfo.get("blend")
+ and "duration" in encoderinfo
):
- previous["encoderinfo"]["duration"] += encoderinfo.get(
- "duration", duration
- )
+ previous["encoderinfo"]["duration"] += encoderinfo["duration"]
continue
else:
bbox = None
- if "duration" not in encoderinfo:
- encoderinfo["duration"] = duration
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
if len(im_frames) == 1 and not default_image:
@@ -1205,8 +1189,8 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
# default image IDAT (if it exists)
if default_image:
- if im.mode != rawmode:
- im = im.convert(rawmode)
+ if im.mode != mode:
+ im = im.convert(mode)
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
seq_num = 0
@@ -1219,7 +1203,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
im_frame = im_frame.crop(bbox)
size = im_frame.size
encoderinfo = frame_data["encoderinfo"]
- frame_duration = int(round(encoderinfo["duration"]))
+ frame_duration = int(round(encoderinfo.get("duration", 0)))
frame_disposal = encoderinfo.get("disposal", disposal)
frame_blend = encoderinfo.get("blend", blend)
# frame control
@@ -1283,6 +1267,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
size = im.size
mode = im.mode
+ outmode = mode
if mode == "P":
#
# attempt to minimize storage requirements for palette images
@@ -1303,7 +1288,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
bits = 2
else:
bits = 4
- mode = f"{mode};{bits}"
+ outmode += f";{bits}"
# encoder options
im.encoderconfig = (
@@ -1315,7 +1300,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
# get the corresponding PNG mode
try:
- rawmode, bit_depth, color_type = _OUTMODES[mode]
+ rawmode, bit_depth, color_type = _OUTMODES[outmode]
except KeyError as e:
msg = f"cannot write mode {mode} as PNG"
raise OSError(msg) from e
@@ -1436,7 +1421,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
if save_all:
im = _write_multiple_frames(
- im, fp, chunk, rawmode, default_image, append_images
+ im, fp, chunk, mode, rawmode, default_image, append_images
)
if im:
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
index 86c1a6763..edf698bf0 100644
--- a/src/PIL/PsdImagePlugin.py
+++ b/src/PIL/PsdImagePlugin.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import io
+from functools import cached_property
from . import Image, ImageFile, ImagePalette
from ._binary import i8
@@ -118,18 +119,17 @@ class PsdImageFile(ImageFile.ImageFile):
#
# layer and mask information
- self.layers = []
+ self._layers_position = None
size = i32(read(4))
if size:
end = self.fp.tell() + size
size = i32(read(4))
if size:
- _layer_data = io.BytesIO(ImageFile._safe_read(self.fp, size))
- self.layers = _layerinfo(_layer_data, size)
+ self._layers_position = self.fp.tell()
+ self._layers_size = size
self.fp.seek(end)
- self.n_frames = len(self.layers)
- self.is_animated = self.n_frames > 1
+ self._n_frames: int | None = None
#
# image descriptor
@@ -141,6 +141,26 @@ class PsdImageFile(ImageFile.ImageFile):
self.frame = 1
self._min_frame = 1
+ @cached_property
+ def layers(self):
+ layers = []
+ if self._layers_position is not None:
+ self._fp.seek(self._layers_position)
+ _layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size))
+ layers = _layerinfo(_layer_data, self._layers_size)
+ self._n_frames = len(layers)
+ return layers
+
+ @property
+ def n_frames(self) -> int:
+ if self._n_frames is None:
+ self._n_frames = len(self.layers)
+ return self._n_frames
+
+ @property
+ def is_animated(self) -> bool:
+ return len(self.layers) > 1
+
def seek(self, layer: int) -> None:
if not self._seek_check(layer):
return
diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py
deleted file mode 100644
index fe12cb641..000000000
--- a/src/PIL/PyAccess.py
+++ /dev/null
@@ -1,376 +0,0 @@
-#
-# The Python Imaging Library
-# Pillow fork
-#
-# Python implementation of the PixelAccess Object
-#
-# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved.
-# Copyright (c) 1995-2009 by Fredrik Lundh.
-# Copyright (c) 2013 Eric Soroos
-#
-# See the README file for information on usage and redistribution
-#
-
-# Notes:
-#
-# * Implements the pixel access object following Access.c
-# * Taking only the tuple form, which is used from python.
-# * Fill.c uses the integer form, but it's still going to use the old
-# Access.c implementation.
-#
-from __future__ import annotations
-
-import logging
-import sys
-from typing import TYPE_CHECKING
-
-from ._deprecate import deprecate
-
-FFI: type
-try:
- from cffi import FFI
-
- defs = """
- struct Pixel_RGBA {
- unsigned char r,g,b,a;
- };
- struct Pixel_I16 {
- unsigned char l,r;
- };
- """
- ffi = FFI()
- ffi.cdef(defs)
-except ImportError as ex:
- # Allow error import for doc purposes, but error out when accessing
- # anything in core.
- from ._util import DeferredError
-
- FFI = ffi = DeferredError.new(ex)
-
-logger = logging.getLogger(__name__)
-
-if TYPE_CHECKING:
- from . import Image
-
-
-class PyAccess:
- def __init__(self, img: Image.Image, readonly: bool = False) -> None:
- deprecate("PyAccess", 11)
- vals = dict(img.im.unsafe_ptrs)
- self.readonly = readonly
- self.image8 = ffi.cast("unsigned char **", vals["image8"])
- self.image32 = ffi.cast("int **", vals["image32"])
- self.image = ffi.cast("unsigned char **", vals["image"])
- self.xsize, self.ysize = img.im.size
- self._img = img
-
- # Keep pointer to im object to prevent dereferencing.
- self._im = img.im
- if self._im.mode in ("P", "PA"):
- self._palette = img.palette
-
- # Debugging is polluting test traces, only useful here
- # when hacking on PyAccess
- # logger.debug("%s", vals)
- self._post_init()
-
- def _post_init(self) -> None:
- pass
-
- def __setitem__(self, xy, color):
- """
- Modifies the pixel at x,y. The color is given as a single
- numerical value for single band images, and a tuple for
- multi-band images. In addition to this, RGB and RGBA tuples
- are accepted for P and PA images.
-
- :param xy: The pixel coordinate, given as (x, y). See
- :ref:`coordinate-system`.
- :param color: The pixel value.
- """
- if self.readonly:
- msg = "Attempt to putpixel a read only image"
- raise ValueError(msg)
- (x, y) = xy
- if x < 0:
- x = self.xsize + x
- if y < 0:
- y = self.ysize + y
- (x, y) = self.check_xy((x, y))
-
- if (
- self._im.mode in ("P", "PA")
- and isinstance(color, (list, tuple))
- and len(color) in [3, 4]
- ):
- # RGB or RGBA value for a P or PA image
- if self._im.mode == "PA":
- alpha = color[3] if len(color) == 4 else 255
- color = color[:3]
- color = self._palette.getcolor(color, self._img)
- if self._im.mode == "PA":
- color = (color, alpha)
-
- return self.set_pixel(x, y, color)
-
- def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]:
- """
- Returns the pixel at x,y. The pixel is returned as a single
- value for single band images or a tuple for multiple band
- images
-
- :param xy: The pixel coordinate, given as (x, y). See
- :ref:`coordinate-system`.
- :returns: a pixel value for single band images, a tuple of
- pixel values for multiband images.
- """
- (x, y) = xy
- if x < 0:
- x = self.xsize + x
- if y < 0:
- y = self.ysize + y
- (x, y) = self.check_xy((x, y))
- return self.get_pixel(x, y)
-
- putpixel = __setitem__
- getpixel = __getitem__
-
- def check_xy(self, xy: tuple[int, int]) -> tuple[int, int]:
- (x, y) = xy
- if not (0 <= x < self.xsize and 0 <= y < self.ysize):
- msg = "pixel location out of range"
- raise ValueError(msg)
- return xy
-
- def get_pixel(self, x: int, y: int) -> float | tuple[int, ...]:
- raise NotImplementedError()
-
- def set_pixel(self, x: int, y: int, color: float | tuple[int, ...]) -> None:
- raise NotImplementedError()
-
-
-class _PyAccess32_2(PyAccess):
- """PA, LA, stored in first and last bytes of a 32 bit word"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
-
- def get_pixel(self, x: int, y: int) -> tuple[int, int]:
- pixel = self.pixels[y][x]
- return pixel.r, pixel.a
-
- def set_pixel(self, x, y, color):
- pixel = self.pixels[y][x]
- # tuple
- pixel.r = min(color[0], 255)
- pixel.a = min(color[1], 255)
-
-
-class _PyAccess32_3(PyAccess):
- """RGB and friends, stored in the first three bytes of a 32 bit word"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
-
- def get_pixel(self, x: int, y: int) -> tuple[int, int, int]:
- pixel = self.pixels[y][x]
- return pixel.r, pixel.g, pixel.b
-
- def set_pixel(self, x, y, color):
- pixel = self.pixels[y][x]
- # tuple
- pixel.r = min(color[0], 255)
- pixel.g = min(color[1], 255)
- pixel.b = min(color[2], 255)
- pixel.a = 255
-
-
-class _PyAccess32_4(PyAccess):
- """RGBA etc, all 4 bytes of a 32 bit word"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
-
- def get_pixel(self, x: int, y: int) -> tuple[int, int, int, int]:
- pixel = self.pixels[y][x]
- return pixel.r, pixel.g, pixel.b, pixel.a
-
- def set_pixel(self, x, y, color):
- pixel = self.pixels[y][x]
- # tuple
- pixel.r = min(color[0], 255)
- pixel.g = min(color[1], 255)
- pixel.b = min(color[2], 255)
- pixel.a = min(color[3], 255)
-
-
-class _PyAccess8(PyAccess):
- """1, L, P, 8 bit images stored as uint8"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = self.image8
-
- def get_pixel(self, x: int, y: int) -> int:
- return self.pixels[y][x]
-
- def set_pixel(self, x, y, color):
- try:
- # integer
- self.pixels[y][x] = min(color, 255)
- except TypeError:
- # tuple
- self.pixels[y][x] = min(color[0], 255)
-
-
-class _PyAccessI16_N(PyAccess):
- """I;16 access, native bitendian without conversion"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = ffi.cast("unsigned short **", self.image)
-
- def get_pixel(self, x: int, y: int) -> int:
- return self.pixels[y][x]
-
- def set_pixel(self, x, y, color):
- try:
- # integer
- self.pixels[y][x] = min(color, 65535)
- except TypeError:
- # tuple
- self.pixels[y][x] = min(color[0], 65535)
-
-
-class _PyAccessI16_L(PyAccess):
- """I;16L access, with conversion"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
-
- def get_pixel(self, x: int, y: int) -> int:
- pixel = self.pixels[y][x]
- return pixel.l + pixel.r * 256
-
- def set_pixel(self, x, y, color):
- pixel = self.pixels[y][x]
- try:
- color = min(color, 65535)
- except TypeError:
- color = min(color[0], 65535)
-
- pixel.l = color & 0xFF
- pixel.r = color >> 8
-
-
-class _PyAccessI16_B(PyAccess):
- """I;16B access, with conversion"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
-
- def get_pixel(self, x: int, y: int) -> int:
- pixel = self.pixels[y][x]
- return pixel.l * 256 + pixel.r
-
- def set_pixel(self, x, y, color):
- pixel = self.pixels[y][x]
- try:
- color = min(color, 65535)
- except Exception:
- color = min(color[0], 65535)
-
- pixel.l = color >> 8
- pixel.r = color & 0xFF
-
-
-class _PyAccessI32_N(PyAccess):
- """Signed Int32 access, native endian"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = self.image32
-
- def get_pixel(self, x: int, y: int) -> int:
- return self.pixels[y][x]
-
- def set_pixel(self, x, y, color):
- self.pixels[y][x] = color
-
-
-class _PyAccessI32_Swap(PyAccess):
- """I;32L/B access, with byteswapping conversion"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = self.image32
-
- def reverse(self, i):
- orig = ffi.new("int *", i)
- chars = ffi.cast("unsigned char *", orig)
- chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0]
- return ffi.cast("int *", chars)[0]
-
- def get_pixel(self, x: int, y: int) -> int:
- return self.reverse(self.pixels[y][x])
-
- def set_pixel(self, x, y, color):
- self.pixels[y][x] = self.reverse(color)
-
-
-class _PyAccessF(PyAccess):
- """32 bit float access"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = ffi.cast("float **", self.image32)
-
- def get_pixel(self, x: int, y: int) -> float:
- return self.pixels[y][x]
-
- def set_pixel(self, x, y, color):
- try:
- # not a tuple
- self.pixels[y][x] = color
- except TypeError:
- # tuple
- self.pixels[y][x] = color[0]
-
-
-mode_map = {
- "1": _PyAccess8,
- "L": _PyAccess8,
- "P": _PyAccess8,
- "I;16N": _PyAccessI16_N,
- "LA": _PyAccess32_2,
- "La": _PyAccess32_2,
- "PA": _PyAccess32_2,
- "RGB": _PyAccess32_3,
- "LAB": _PyAccess32_3,
- "HSV": _PyAccess32_3,
- "YCbCr": _PyAccess32_3,
- "RGBA": _PyAccess32_4,
- "RGBa": _PyAccess32_4,
- "RGBX": _PyAccess32_4,
- "CMYK": _PyAccess32_4,
- "F": _PyAccessF,
- "I": _PyAccessI32_N,
-}
-
-if sys.byteorder == "little":
- mode_map["I;16"] = _PyAccessI16_N
- mode_map["I;16L"] = _PyAccessI16_N
- mode_map["I;16B"] = _PyAccessI16_B
-
- mode_map["I;32L"] = _PyAccessI32_N
- mode_map["I;32B"] = _PyAccessI32_Swap
-else:
- mode_map["I;16"] = _PyAccessI16_L
- mode_map["I;16L"] = _PyAccessI16_L
- mode_map["I;16B"] = _PyAccessI16_N
-
- mode_map["I;32L"] = _PyAccessI32_Swap
- mode_map["I;32B"] = _PyAccessI32_N
-
-
-def new(img: Image.Image, readonly: bool = False) -> PyAccess | None:
- access_type = mode_map.get(img.mode, None)
- if not access_type:
- logger.debug("PyAccess Not Implemented: %s", img.mode)
- return None
- return access_type(img, readonly)
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index a6cc00019..a07101e54 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -37,12 +37,12 @@ from __future__ import annotations
import os
import struct
import sys
-from typing import IO, TYPE_CHECKING
+from typing import IO, TYPE_CHECKING, Any, cast
from . import Image, ImageFile
-def isInt(f):
+def isInt(f: Any) -> int:
try:
i = int(f)
if f - i == 0:
@@ -62,7 +62,7 @@ iforms = [1, 3, -11, -12, -21, -22]
# otherwise returns 0
-def isSpiderHeader(t):
+def isSpiderHeader(t: tuple[float, ...]) -> int:
h = (99,) + t # add 1 value so can use spider header index start=1
# header values 1,2,5,12,13,22,23 should be integers
for i in [1, 2, 5, 12, 13, 22, 23]:
@@ -82,7 +82,7 @@ def isSpiderHeader(t):
return labbyt
-def isSpiderImage(filename):
+def isSpiderImage(filename: str) -> int:
with open(filename, "rb") as fp:
f = fp.read(92) # read 23 * 4 bytes
t = struct.unpack(">23f", f) # try big-endian first
@@ -184,13 +184,15 @@ class SpiderImageFile(ImageFile.ImageFile):
self._open()
# returns a byte image after rescaling to 0..255
- def convert2byte(self, depth=255):
- (minimum, maximum) = self.getextrema()
- m = 1
+ def convert2byte(self, depth: int = 255) -> Image.Image:
+ extrema = self.getextrema()
+ assert isinstance(extrema[0], float)
+ minimum, maximum = cast(tuple[float, float], extrema)
+ m: float = 1
if maximum != minimum:
m = depth / (maximum - minimum)
b = -m * minimum
- return self.point(lambda i, m=m, b=b: i * m + b).convert("L")
+ return self.point(lambda i: i * m + b).convert("L")
if TYPE_CHECKING:
from . import ImageTk
@@ -207,10 +209,10 @@ class SpiderImageFile(ImageFile.ImageFile):
# given a list of filenames, return a list of images
-def loadImageSeries(filelist=None):
+def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] | None:
"""create a list of :py:class:`~PIL.Image.Image` objects for use in a montage"""
if filelist is None or len(filelist) < 1:
- return
+ return None
imglist = []
for img in filelist:
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index f16f075df..39104aece 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -36,7 +36,7 @@ MODES = {
(3, 1): "1",
(3, 8): "L",
(3, 16): "LA",
- (2, 16): "BGR;5",
+ (2, 16): "BGRA;15Z",
(2, 24): "BGR",
(2, 32): "BGRA",
}
@@ -87,9 +87,7 @@ class TgaImageFile(ImageFile.ImageFile):
elif imagetype in (1, 9):
self._mode = "P" if colormaptype else "L"
elif imagetype in (2, 10):
- self._mode = "RGB"
- if depth == 32:
- self._mode = "RGBA"
+ self._mode = "RGB" if depth == 24 else "RGBA"
else:
msg = "unknown TGA mode"
raise SyntaxError(msg)
@@ -118,15 +116,16 @@ class TgaImageFile(ImageFile.ImageFile):
start, size, mapdepth = i16(s, 3), i16(s, 5), s[7]
if mapdepth == 16:
self.palette = ImagePalette.raw(
- "BGR;15", b"\0" * 2 * start + self.fp.read(2 * size)
+ "BGRA;15Z", bytes(2 * start) + self.fp.read(2 * size)
)
+ self.palette.mode = "RGBA"
elif mapdepth == 24:
self.palette = ImagePalette.raw(
- "BGR", b"\0" * 3 * start + self.fp.read(3 * size)
+ "BGR", bytes(3 * start) + self.fp.read(3 * size)
)
elif mapdepth == 32:
self.palette = ImagePalette.raw(
- "BGRA", b"\0" * 4 * start + self.fp.read(4 * size)
+ "BGRA", bytes(4 * start) + self.fp.read(4 * size)
)
else:
msg = "unknown TGA map depth"
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 833e12d2b..b89144803 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -201,12 +201,12 @@ OPEN_INFO = {
(MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
(II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
(MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
- (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"),
- (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"),
- (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"),
- (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"),
- (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"),
- (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"),
+ (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"),
+ (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"),
+ (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"),
(II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
(MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
(II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
@@ -225,8 +225,8 @@ OPEN_INFO = {
(MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"),
(II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"),
(MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"),
- (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16L"),
- (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16B"),
+ (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16L"),
+ (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16B"),
(II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"),
(MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"),
(II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"),
@@ -1197,6 +1197,10 @@ class TiffImageFile(ImageFile.ImageFile):
self.__frame += 1
self.fp.seek(self._frame_pos[frame])
self.tag_v2.load(self.fp)
+ if XMP in self.tag_v2:
+ self.info["xmp"] = self.tag_v2[XMP]
+ elif "xmp" in self.info:
+ del self.info["xmp"]
self._reload_exif()
# fill the legacy tag/ifd entries
self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
@@ -1207,15 +1211,6 @@ class TiffImageFile(ImageFile.ImageFile):
"""Return the current frame number"""
return self.__frame
- def getxmp(self) -> dict[str, Any]:
- """
- Returns a dictionary containing the XMP tags.
- Requires defusedxml to be installed.
-
- :returns: XMP tags in a dictionary.
- """
- return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {}
-
def get_photoshop_blocks(self):
"""
Returns a dictionary of Photoshop "Image Resource Blocks".
@@ -1237,7 +1232,7 @@ class TiffImageFile(ImageFile.ImageFile):
val = val[math.ceil((10 + n + size) / 2) * 2 :]
return blocks
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if self.tile and self.use_load_libtiff:
return self._load_libtiff()
return super().load()
@@ -1663,6 +1658,20 @@ def _save(im, fp, filename):
except Exception:
pass # might not be an IFD. Might not have populated type
+ legacy_ifd = {}
+ if hasattr(im, "tag"):
+ legacy_ifd = im.tag.to_v2()
+
+ supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})}
+ for tag in (
+ # IFD offset that may not be correct in the saved image
+ EXIFIFD,
+ # Determined by the image format and should not be copied from legacy_ifd.
+ SAMPLEFORMAT,
+ ):
+ if tag in supplied_tags:
+ del supplied_tags[tag]
+
# additions written by Greg Couch, gregc@cgl.ucsf.edu
# inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com
if hasattr(im, "tag_v2"):
@@ -1676,8 +1685,14 @@ def _save(im, fp, filename):
XMP,
):
if key in im.tag_v2:
- ifd[key] = im.tag_v2[key]
- ifd.tagtype[key] = im.tag_v2.tagtype[key]
+ if key == IPTC_NAA_CHUNK and im.tag_v2.tagtype[key] not in (
+ TiffTags.BYTE,
+ TiffTags.UNDEFINED,
+ ):
+ del supplied_tags[key]
+ else:
+ ifd[key] = im.tag_v2[key]
+ ifd.tagtype[key] = im.tag_v2.tagtype[key]
# preserve ICC profile (should also work when saving other formats
# which support profiles as TIFF) -- 2008-06-06 Florian Hoech
@@ -1817,16 +1832,6 @@ def _save(im, fp, filename):
# Merge the ones that we have with (optional) more bits from
# the original file, e.g x,y resolution so that we can
# save(load('')) == original file.
- legacy_ifd = {}
- if hasattr(im, "tag"):
- legacy_ifd = im.tag.to_v2()
-
- # SAMPLEFORMAT is determined by the image format and should not be copied
- # from legacy_ifd.
- supplied_tags = {**getattr(im, "tag_v2", {}), **legacy_ifd}
- if SAMPLEFORMAT in supplied_tags:
- del supplied_tags[SAMPLEFORMAT]
-
for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):
# Libtiff can only process certain core items without adding
# them to the custom dictionary.
diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py
index 89fad7033..e318c8739 100644
--- a/src/PIL/TiffTags.py
+++ b/src/PIL/TiffTags.py
@@ -89,7 +89,7 @@ DOUBLE = 12
IFD = 13
LONG8 = 16
-TAGS_V2 = {
+_tags_v2 = {
254: ("NewSubfileType", LONG, 1),
255: ("SubfileType", SHORT, 1),
256: ("ImageWidth", LONG, 1),
@@ -425,9 +425,11 @@ TAGS = {
50784: "Alias Layer Metadata",
}
+TAGS_V2: dict[int, TagInfo] = {}
+
def _populate():
- for k, v in TAGS_V2.items():
+ for k, v in _tags_v2.items():
# Populate legacy structure.
TAGS[k] = v[0]
if len(v) == 4:
diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py
index fbd7be6ed..895d5616a 100644
--- a/src/PIL/WalImageFile.py
+++ b/src/PIL/WalImageFile.py
@@ -50,7 +50,7 @@ class WalImageFile(ImageFile.ImageFile):
if next_name:
self.info["next_name"] = next_name
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if not self.im:
self.im = Image.core.new(self.mode, self.size)
self.frombytes(self.fp.read(self.size[0] * self.size[1]))
diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py
index 97debc2ed..530b88c8b 100644
--- a/src/PIL/WebPImagePlugin.py
+++ b/src/PIL/WebPImagePlugin.py
@@ -101,15 +101,6 @@ class WebPImageFile(ImageFile.ImageFile):
return None
return self.getexif()._get_merged_dict()
- def getxmp(self) -> dict[str, Any]:
- """
- Returns a dictionary containing the XMP tags.
- Requires defusedxml to be installed.
-
- :returns: XMP tags in a dictionary.
- """
- return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {}
-
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
@@ -153,7 +144,7 @@ class WebPImageFile(ImageFile.ImageFile):
while self.__physical_frame < frame:
self._get_next() # Advance to the requested frame
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if _webp.HAVE_WEBPANIM:
if self.__loaded != self.__logical_frame:
self._seek(self.__logical_frame)
diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py
index 33a0e07b3..83952b397 100644
--- a/src/PIL/_deprecate.py
+++ b/src/PIL/_deprecate.py
@@ -45,8 +45,6 @@ def deprecate(
elif when <= int(__version__.split(".")[0]):
msg = f"{deprecated} {is_} deprecated and should be removed."
raise RuntimeError(msg)
- elif when == 11:
- removed = "Pillow 11 (2024-10-15)"
elif when == 12:
removed = "Pillow 12 (2025-10-15)"
else:
diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi
index dda06b5a1..8cccd3ac7 100644
--- a/src/PIL/_imaging.pyi
+++ b/src/PIL/_imaging.pyi
@@ -10,7 +10,10 @@ class ImagingDraw:
def __getattr__(self, name: str) -> Any: ...
class PixelAccess:
- def __getattr__(self, name: str) -> Any: ...
+ def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]: ...
+ def __setitem__(
+ self, xy: tuple[int, int], color: float | tuple[int, ...]
+ ) -> None: ...
class ImagingDecoder:
def __getattr__(self, name: str) -> Any: ...
@@ -18,5 +21,10 @@ class ImagingDecoder:
class ImagingEncoder:
def __getattr__(self, name: str) -> Any: ...
+class _Outline:
+ def close(self) -> None: ...
+ def __getattr__(self, name: str) -> Any: ...
+
def font(image: ImagingCore, glyphdata: bytes) -> ImagingFont: ...
+def outline() -> _Outline: ...
def __getattr__(name: str) -> Any: ...
diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi
index 6e0ddd2f1..5e97b40b2 100644
--- a/src/PIL/_imagingft.pyi
+++ b/src/PIL/_imagingft.pyi
@@ -27,7 +27,7 @@ class Font:
def glyphs(self) -> int: ...
def render(
self,
- string: str,
+ string: str | bytes,
fill,
mode=...,
dir=...,
@@ -51,7 +51,7 @@ class Font:
/,
) -> tuple[tuple[int, int], tuple[int, int]]: ...
def getlength(
- self, string: str, mode=..., dir=..., features=..., lang=..., /
+ self, string: str | bytes, mode=..., dir=..., features=..., lang=..., /
) -> float: ...
def getvarnames(self) -> list[bytes]: ...
def getvaraxes(self) -> list[_Axis] | None: ...
diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py
index 7075e8672..b6bb8d89a 100644
--- a/src/PIL/_typing.py
+++ b/src/PIL/_typing.py
@@ -2,7 +2,16 @@ from __future__ import annotations
import os
import sys
-from typing import Protocol, Sequence, TypeVar, Union
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union
+
+if TYPE_CHECKING:
+ try:
+ import numpy.typing as npt
+
+ NumpyArray = npt.NDArray[Any] # requires numpy>=1.21
+ except (ImportError, AttributeError):
+ pass
if sys.version_info >= (3, 10):
from typing import TypeGuard
@@ -10,7 +19,6 @@ else:
try:
from typing_extensions import TypeGuard
except ImportError:
- from typing import Any
class TypeGuard: # type: ignore[no-redef]
def __class_getitem__(cls, item: Any) -> type[bool]:
diff --git a/src/PIL/_version.py b/src/PIL/_version.py
index 12d7412ea..c4a72ad7e 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,4 +1,4 @@
# Master version for Pillow
from __future__ import annotations
-__version__ = "10.4.0.dev0"
+__version__ = "11.0.0.dev0"
diff --git a/src/_imaging.c b/src/_imaging.c
index f398c6c7c..ddc8d2885 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -1725,10 +1725,11 @@ _putpalette(ImagingObject *self, PyObject *args) {
ImagingShuffler unpack;
int bits;
- char *rawmode, *palette_mode;
+ char *palette_mode, *rawmode;
UINT8 *palette;
Py_ssize_t palettesize;
- if (!PyArg_ParseTuple(args, "sy#", &rawmode, &palette, &palettesize)) {
+ if (!PyArg_ParseTuple(
+ args, "ssy#", &palette_mode, &rawmode, &palette, &palettesize)) {
return NULL;
}
@@ -1738,7 +1739,6 @@ _putpalette(ImagingObject *self, PyObject *args) {
return NULL;
}
- palette_mode = strncmp("RGBA", rawmode, 4) == 0 ? "RGBA" : "RGB";
unpack = ImagingFindUnpacker(palette_mode, rawmode, &bits);
if (!unpack) {
PyErr_SetString(PyExc_ValueError, wrong_raw_mode);
diff --git a/src/_imagingcms.c b/src/_imagingcms.c
index 2b9612db7..590e1b983 100644
--- a/src/_imagingcms.c
+++ b/src/_imagingcms.c
@@ -223,20 +223,22 @@ findLCMStype(char *PILmode) {
if (strcmp(PILmode, "CMYK") == 0) {
return TYPE_CMYK_8;
}
- if (strcmp(PILmode, "L;16") == 0) {
+ if (strcmp(PILmode, "I;16") == 0 || strcmp(PILmode, "I;16L") == 0 ||
+ strcmp(PILmode, "L;16") == 0) {
return TYPE_GRAY_16;
}
- if (strcmp(PILmode, "L;16B") == 0) {
+ if (strcmp(PILmode, "I;16B") == 0 || strcmp(PILmode, "L;16B") == 0) {
return TYPE_GRAY_16_SE;
}
- if (strcmp(PILmode, "YCCA") == 0 || strcmp(PILmode, "YCC") == 0) {
+ if (strcmp(PILmode, "YCbCr") == 0 || strcmp(PILmode, "YCCA") == 0 ||
+ strcmp(PILmode, "YCC") == 0) {
return TYPE_YCbCr_8;
}
if (strcmp(PILmode, "LAB") == 0) {
// LabX equivalent like ALab, but not reversed -- no #define in lcms2
return (COLORSPACE_SH(PT_LabV2) | CHANNELS_SH(3) | BYTES_SH(1) | EXTRA_SH(1));
}
- /* presume "L" by default */
+ /* presume "1" or "L" by default */
return TYPE_GRAY_8;
}
diff --git a/src/_imagingft.c b/src/_imagingft.c
index e83ddfec1..ba36cc72c 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -233,18 +233,6 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
return (PyObject *)self;
}
-static int
-font_getchar(PyObject *string, int index, FT_ULong *char_out) {
- if (PyUnicode_Check(string)) {
- if (index >= PyUnicode_GET_LENGTH(string)) {
- return 0;
- }
- *char_out = PyUnicode_READ_CHAR(string, index);
- return 1;
- }
- return 0;
-}
-
#ifdef HAVE_RAQM
static size_t
@@ -266,28 +254,34 @@ text_layout_raqm(
goto failed;
}
+ Py_ssize_t size;
+ int set_text;
if (PyUnicode_Check(string)) {
Py_UCS4 *text = PyUnicode_AsUCS4Copy(string);
- Py_ssize_t size = PyUnicode_GET_LENGTH(string);
+ size = PyUnicode_GET_LENGTH(string);
if (!text || !size) {
/* return 0 and clean up, no glyphs==no size,
and raqm fails with empty strings */
goto failed;
}
- int set_text = raqm_set_text(rq, text, size);
+ set_text = raqm_set_text(rq, text, size);
PyMem_Free(text);
- if (!set_text) {
- PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed");
+ } else {
+ char *buffer;
+ PyBytes_AsStringAndSize(string, &buffer, &size);
+ if (!buffer || !size) {
+ /* return 0 and clean up, no glyphs==no size,
+ and raqm fails with empty strings */
goto failed;
}
- if (lang) {
- if (!raqm_set_language(rq, lang, start, size)) {
- PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed");
- goto failed;
- }
- }
- } else {
- PyErr_SetString(PyExc_TypeError, "expected string");
+ set_text = raqm_set_text_utf8(rq, buffer, size);
+ }
+ if (!set_text) {
+ PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed");
+ goto failed;
+ }
+ if (lang && !raqm_set_language(rq, lang, start, size)) {
+ PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed");
goto failed;
}
@@ -405,13 +399,13 @@ text_layout_fallback(
GlyphInfo **glyph_info,
int mask,
int color) {
- int error, load_flags;
+ int error, load_flags, i;
+ char *buffer = NULL;
FT_ULong ch;
Py_ssize_t count;
FT_GlyphSlot glyph;
FT_Bool kerning = FT_HAS_KERNING(self->face);
FT_UInt last_index = 0;
- int i;
if (features != Py_None || dir != NULL || lang != NULL) {
PyErr_SetString(
@@ -419,14 +413,11 @@ text_layout_fallback(
"setting text direction, language or font features is not supported "
"without libraqm");
}
- if (!PyUnicode_Check(string)) {
- PyErr_SetString(PyExc_TypeError, "expected string");
- return 0;
- }
- count = 0;
- while (font_getchar(string, count, &ch)) {
- count++;
+ if (PyUnicode_Check(string)) {
+ count = PyUnicode_GET_LENGTH(string);
+ } else {
+ PyBytes_AsStringAndSize(string, &buffer, &count);
}
if (count == 0) {
return 0;
@@ -445,7 +436,12 @@ text_layout_fallback(
if (color) {
load_flags |= FT_LOAD_COLOR;
}
- for (i = 0; font_getchar(string, i, &ch); i++) {
+ for (i = 0; i < count; i++) {
+ if (buffer) {
+ ch = buffer[i];
+ } else {
+ ch = PyUnicode_READ_CHAR(string, i);
+ }
(*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch);
error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags);
if (error) {
diff --git a/src/display.c b/src/display.c
index abf94f1e1..990f4b0a5 100644
--- a/src/display.c
+++ b/src/display.c
@@ -618,7 +618,7 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) {
if (callback) {
/* restore thread state */
PyEval_SaveThread();
- PyThreadState_Swap(threadstate);
+ PyThreadState_Swap(current_threadstate);
}
return status;
diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c
index a018225b2..dc67cb41d 100644
--- a/src/libImaging/Paste.c
+++ b/src/libImaging/Paste.c
@@ -65,15 +65,32 @@ paste_mask_1(
int x, y;
if (imOut->image8) {
+ int in_i16 = strncmp(imIn->mode, "I;16", 4) == 0;
+ int out_i16 = strncmp(imOut->mode, "I;16", 4) == 0;
for (y = 0; y < ysize; y++) {
UINT8 *out = imOut->image8[y + dy] + dx;
+ if (out_i16) {
+ out += dx;
+ }
UINT8 *in = imIn->image8[y + sy] + sx;
+ if (in_i16) {
+ in += sx;
+ }
UINT8 *mask = imMask->image8[y + sy] + sx;
for (x = 0; x < xsize; x++) {
- if (*mask++) {
+ if (*mask) {
*out = *in;
}
- out++, in++;
+ if (in_i16) {
+ in++;
+ }
+ if (out_i16) {
+ out++;
+ if (*mask) {
+ *out = *in;
+ }
+ }
+ out++, in++, mask++;
}
}
@@ -415,15 +432,16 @@ fill_mask_L(
unsigned int tmp1;
if (imOut->image8) {
+ int i16 = strncmp(imOut->mode, "I;16", 4) == 0;
for (y = 0; y < ysize; y++) {
UINT8 *out = imOut->image8[y + dy] + dx;
- if (strncmp(imOut->mode, "I;16", 4) == 0) {
+ if (i16) {
out += dx;
}
UINT8 *mask = imMask->image8[y + sy] + sx;
for (x = 0; x < xsize; x++) {
*out = BLEND(*mask, *out, ink[0], tmp1);
- if (strncmp(imOut->mode, "I;16", 4) == 0) {
+ if (i16) {
out++;
*out = BLEND(*mask, *out, ink[1], tmp1);
}
diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c
index 1b84cd68f..eaa4374e3 100644
--- a/src/libImaging/Unpack.c
+++ b/src/libImaging/Unpack.c
@@ -718,6 +718,21 @@ ImagingUnpackBGRA15(UINT8 *out, const UINT8 *in, int pixels) {
}
}
+void
+ImagingUnpackBGRA15Z(UINT8 *out, const UINT8 *in, int pixels) {
+ int i, pixel;
+ /* RGB, rearranged channels, 5/5/5/1 bits per pixel, inverted alpha */
+ for (i = 0; i < pixels; i++) {
+ pixel = in[0] + (in[1] << 8);
+ out[B] = (pixel & 31) * 255 / 31;
+ out[G] = ((pixel >> 5) & 31) * 255 / 31;
+ out[R] = ((pixel >> 10) & 31) * 255 / 31;
+ out[A] = ~((pixel >> 15) * 255);
+ out += 4;
+ in += 2;
+ }
+}
+
void
ImagingUnpackRGB16(UINT8 *out, const UINT8 *in, int pixels) {
int i, pixel;
@@ -1538,7 +1553,7 @@ static struct {
/* flags: "I" inverted data; "R" reversed bit order; "B" big
endian byte order (default is little endian); "L" line
- interleave, "S" signed, "F" floating point */
+ interleave, "S" signed, "F" floating point, "Z" inverted alpha */
/* exception: rawmodes "I" and "F" are always native endian byte order */
@@ -1600,10 +1615,14 @@ static struct {
{"RGB", "BGR;15", 16, ImagingUnpackBGR15},
{"RGB", "RGB;16", 16, ImagingUnpackRGB16},
{"RGB", "BGR;16", 16, ImagingUnpackBGR16},
+ {"RGB", "RGBX;16L", 64, unpackRGBA16L},
+ {"RGB", "RGBX;16B", 64, unpackRGBA16B},
{"RGB", "RGB;4B", 16, ImagingUnpackRGB4B},
{"RGB", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */
{"RGB", "RGBX", 32, copy4},
{"RGB", "RGBX;L", 32, unpackRGBAL},
+ {"RGB", "RGBXX", 40, copy4skip1},
+ {"RGB", "RGBXXX", 48, copy4skip2},
{"RGB", "RGBA;L", 32, unpackRGBAL},
{"RGB", "RGBA;15", 16, ImagingUnpackRGBA15},
{"RGB", "BGRX", 32, ImagingUnpackBGRX},
@@ -1642,6 +1661,7 @@ static struct {
{"RGBA", "RGBA;L", 32, unpackRGBAL},
{"RGBA", "RGBA;15", 16, ImagingUnpackRGBA15},
{"RGBA", "BGRA;15", 16, ImagingUnpackBGRA15},
+ {"RGBA", "BGRA;15Z", 16, ImagingUnpackBGRA15Z},
{"RGBA", "RGBA;4B", 16, ImagingUnpackRGBA4B},
{"RGBA", "RGBA;16L", 64, unpackRGBA16L},
{"RGBA", "RGBA;16B", 64, unpackRGBA16B},
diff --git a/tox.ini b/tox.ini
index 85a2020d6..c1bc3b17d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,11 +3,10 @@ requires =
tox>=4.2
env_list =
lint
- py{py3, 312, 311, 310, 39, 38}
+ py{py3, 313, 312, 311, 310, 39}
[testenv]
deps =
- cffi
numpy
extras =
tests
@@ -39,10 +38,11 @@ deps =
ipython
numpy
packaging
- types-cffi
+ pytest
types-defusedxml
types-olefile
+ types-setuptools
extras =
typing
commands =
- mypy src {posargs}
+ mypy src Tests {posargs}
diff --git a/winbuild/README.md b/winbuild/README.md
index 7e81abcb0..c8048bcc9 100644
--- a/winbuild/README.md
+++ b/winbuild/README.md
@@ -16,7 +16,7 @@ For more extensive info, see the [Windows build instructions](build.rst).
The following is a simplified version of the script used on AppVeyor:
```
-set PYTHON=C:\Python38\bin
+set PYTHON=C:\Python39\bin
cd /D C:\Pillow\winbuild
%PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends
build\build_dep_all.cmd
diff --git a/winbuild/build.rst b/winbuild/build.rst
index d0be2943e..96b8803b4 100644
--- a/winbuild/build.rst
+++ b/winbuild/build.rst
@@ -114,7 +114,7 @@ Example
The following is a simplified version of the script used on AppVeyor::
- set PYTHON=C:\Python38\bin
+ set PYTHON=C:\Python39\bin
cd /D C:\Pillow\winbuild
%PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends
build\build_dep_all.cmd