Merge branch 'main' into progress

This commit is contained in:
Andrew Murray 2024-07-03 18:26:35 +10:00 committed by GitHub
commit 26e2a9f29c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
97 changed files with 829 additions and 1250 deletions

View File

@ -21,9 +21,9 @@ environment:
- PYTHON: C:/Python312 - PYTHON: C:/Python312
ARCHITECTURE: x86 ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python38-x64 - PYTHON: C:/Python39-x64
ARCHITECTURE: AMD64 ARCHITECTURE: AMD64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
install: install:
@ -38,7 +38,7 @@ install:
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH% - path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH%
- cd c:\pillow\winbuild\ - cd c:\pillow\winbuild\
- ps: | - 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 c:\pillow\winbuild\build\build_dep_all.cmd
$host.SetShouldExit(0) $host.SetShouldExit(0)
- path C:\pillow\winbuild\build\bin;%PATH% - path C:\pillow\winbuild\build\bin;%PATH%

View File

@ -28,8 +28,6 @@ fi
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel 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 coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install olefile python3 -m pip install olefile
@ -39,8 +37,7 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
# TODO Update condition when NumPy supports 3.13 python3 -m pip install numpy
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
@ -51,7 +48,6 @@ if [[ $(uname) != CYGWIN* ]]; then
# Pyroma uses non-isolated build and fails with old setuptools # Pyroma uses non-isolated build and fails with old setuptools
if [[ if [[
$GHA_PYTHON_VERSION == pypy3.9 $GHA_PYTHON_VERSION == pypy3.9
|| $GHA_PYTHON_VERSION == 3.8
|| $GHA_PYTHON_VERSION == 3.9 || $GHA_PYTHON_VERSION == 3.9
]]; then ]]; then
# To match pyproject.toml # To match pyproject.toml

View File

@ -1 +1 @@
cibuildwheel==2.19.1 cibuildwheel==2.19.2

View File

@ -1 +1 @@
mypy==1.10.0 mypy==1.10.1

View File

@ -19,6 +19,5 @@ exclude_also =
[run] [run]
omit = omit =
Tests/32bit_segfault_check.py Tests/32bit_segfault_check.py
Tests/bench_cffi_access.py
Tests/check_*.py Tests/check_*.py
Tests/createfontdatachunk.py Tests/createfontdatachunk.py

View File

@ -18,9 +18,6 @@ else
fi fi
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" 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 coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install olefile 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-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
python3 -m pip install numpy
# TODO Update condition when NumPy supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd

View File

@ -35,7 +35,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-minor-version: [8, 9] python-minor-version: [9]
timeout-minutes: 40 timeout-minutes: 40
@ -72,7 +72,6 @@ jobs:
make make
netpbm netpbm
perl perl
python3${{ matrix.python-minor-version }}-cffi
python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-numpy

View File

@ -44,13 +44,11 @@ jobs:
amazon-2023-amd64, amazon-2023-amd64,
arch, arch,
centos-stream-9-amd64, centos-stream-9-amd64,
debian-11-bullseye-amd64,
debian-12-bookworm-x86, debian-12-bookworm-x86,
debian-12-bookworm-amd64, debian-12-bookworm-amd64,
fedora-39-amd64, fedora-39-amd64,
fedora-40-amd64, fedora-40-amd64,
gentoo, gentoo,
ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64, ubuntu-22.04-jammy-amd64,
ubuntu-24.04-noble-amd64, ubuntu-24.04-noble-amd64,
] ]

View File

@ -64,7 +64,6 @@ jobs:
mingw-w64-x86_64-libtiff \ mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-openjpeg2 \
mingw-w64-x86_64-python3-cffi \
mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python3-numpy \
mingw-w64-x86_64-python3-olefile \ mingw-w64-x86_64-python3-olefile \
mingw-w64-x86_64-python3-setuptools \ mingw-w64-x86_64-python3-setuptools \

View File

@ -35,7 +35,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: 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 timeout-minutes: 30

View File

@ -48,7 +48,6 @@ jobs:
"3.11", "3.11",
"3.10", "3.10",
"3.9", "3.9",
"3.8",
] ]
include: include:
- python-version: "3.11" - python-version: "3.11"
@ -59,13 +58,9 @@ jobs:
# M1 only available for 3.10+ # M1 only available for 3.10+
- os: "macos-13" - os: "macos-13"
python-version: "3.9" python-version: "3.9"
- os: "macos-13"
python-version: "3.8"
exclude: exclude:
- os: "macos-14" - os: "macos-14"
python-version: "3.9" python-version: "3.9"
- os: "macos-14"
python-version: "3.8"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}

View File

@ -12,7 +12,7 @@ elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
else else
yum install -y fribidi yum install -y fribidi
fi fi
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ] && !([[ "$OSTYPE" == "darwin"* ]] && [[ $(python3 --version) == *"3.13."* ]]); then if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
python3 -m pip install numpy python3 -m pip install numpy
fi fi

View File

@ -41,7 +41,6 @@ jobs:
python-version: python-version:
- pp39 - pp39
- pp310 - pp310
- cp38
- cp39 - cp39
- cp310 - cp310
- cp311 - cp311
@ -136,8 +135,6 @@ jobs:
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_PRERELEASE_PYTHONS: True CIBW_PRERELEASE_PYTHONS: True
CIBW_SKIP: pp38-*
CIBW_TEST_SKIP: cp38-macosx_arm64
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
@ -208,7 +205,6 @@ jobs:
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw" CIBW_CACHE_PATH: "C:\\cibw"
CIBW_PRERELEASE_PYTHONS: True CIBW_PRERELEASE_PYTHONS: True
CIBW_SKIP: pp38-*
CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow -v {project}:C:\pillow

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.7 rev: v0.5.0
hooks: hooks:
- id: ruff - id: ruff
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
@ -11,7 +11,7 @@ repos:
- id: black - id: black
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.7.8 rev: 1.7.9
hooks: hooks:
- id: bandit - id: bandit
args: [--severity-level=high] args: [--severity-level=high]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v18.1.5 rev: v18.1.8
hooks: hooks:
- id: clang-format - id: clang-format
types: [c] types: [c]
@ -50,7 +50,7 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.28.4 rev: 0.28.6
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows
- id: check-readthedocs - id: check-readthedocs
@ -62,7 +62,7 @@ repos:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: 1.8.0 rev: 2.1.3
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt

View File

@ -2,9 +2,39 @@
Changelog (Pillow) Changelog (Pillow)
================== ==================
10.4.0 (unreleased) 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 - Do not use first frame duration for other frames when saving APNG images #8104
[radarhere] [radarhere]

View File

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

View File

@ -11,9 +11,10 @@ import subprocess
import sys import sys
import sysconfig import sysconfig
import tempfile import tempfile
from collections.abc import Sequence
from functools import lru_cache from functools import lru_cache
from io import BytesIO from io import BytesIO
from typing import Any, Callable, Sequence from typing import Any, Callable
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 414 B

BIN
Tests/images/rgba16.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 B

View File

@ -321,6 +321,7 @@ class TestColorLut3DCoreAPI:
-1, 2, 2, 2, 2, 2, -1, 2, 2, 2, 2, 2,
])).load() ])).load()
# fmt: on # fmt: on
assert transformed is not None
assert transformed[0, 0] == (0, 0, 255) assert transformed[0, 0] == (0, 0, 255)
assert transformed[50, 50] == (0, 0, 255) assert transformed[50, 50] == (0, 0, 255)
assert transformed[255, 0] == (0, 255, 255) assert transformed[255, 0] == (0, 255, 255)
@ -341,6 +342,7 @@ class TestColorLut3DCoreAPI:
-3, 5, 5, 5, 5, 5, -3, 5, 5, 5, 5, 5,
])).load() ])).load()
# fmt: on # fmt: on
assert transformed is not None
assert transformed[0, 0] == (0, 0, 255) assert transformed[0, 0] == (0, 0, 255)
assert transformed[50, 50] == (0, 0, 255) assert transformed[50, 50] == (0, 0, 255)
assert transformed[255, 0] == (0, 255, 255) assert transformed[255, 0] == (0, 255, 255)

View File

@ -9,9 +9,9 @@ from PIL import _deprecate
"version, expected", "version, expected",
[ [
( (
11, 12,
"Old thing is deprecated and will be removed in Pillow 11 " "Old thing is deprecated and will be removed in Pillow 12 "
r"\(2024-10-15\)\. Use new thing instead\.", r"\(2025-10-15\)\. Use new thing instead\.",
), ),
( (
None, None,
@ -54,18 +54,18 @@ def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
def test_plural() -> None: def test_plural() -> None:
expected = ( 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\." r"Use new thing instead\."
) )
with pytest.warns(DeprecationWarning, match=expected): 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: def test_replacement_and_action() -> None:
expected = "Use only one of 'replacement' and 'action'" expected = "Use only one of 'replacement' and 'action'"
with pytest.raises(ValueError, match=expected): with pytest.raises(ValueError, match=expected):
_deprecate.deprecate( _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: def test_action(action: str) -> None:
expected = ( 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\." r"Upgrade to new thing\."
) )
with pytest.warns(DeprecationWarning, match=expected): 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: def test_no_replacement_or_action() -> None:
expected = ( 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): with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", 11) _deprecate.deprecate("Old thing", 12)

View File

@ -329,46 +329,6 @@ def test_read_binary_preview() -> None:
pass 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("prefix", (b"", simple_binary_header))
@pytest.mark.parametrize( @pytest.mark.parametrize(
"line_ending", "line_ending",
@ -425,9 +385,10 @@ def test_timeout(test_file: str) -> None:
def test_bounding_box_in_trailer() -> None: def test_bounding_box_in_trailer() -> None:
# Check bounding boxes are parsed in the same way # Check bounding boxes are parsed in the same way
# when specified in the header and the trailer # when specified in the header and the trailer
with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open( with (
FILE1 Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image,
) as header_image: Image.open(FILE1) as header_image,
):
assert trailer_image.size == header_image.size assert trailer_image.size == header_image.size

View File

@ -1,9 +1,9 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
from collections.abc import Generator
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Generator
import pytest import pytest

View File

@ -872,7 +872,7 @@ class TestFileJpeg:
def test_multiple_exif(self) -> None: def test_multiple_exif(self) -> None:
with Image.open("Tests/images/multiple_exif.jpg") as im: 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( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"

View File

@ -685,13 +685,18 @@ class TestFileLibTiff(LibTiffTestCase):
assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[530] == (1, 1)
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
def test_exif_ifd(self, tmp_path: Path) -> None: def test_exif_ifd(self) -> None:
outfile = str(tmp_path / "temp.tif") out = io.BytesIO()
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
assert im.tag_v2[34665] == 125456 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: if Image.core.libtiff_support_custom_tags:
assert reloaded.tag_v2[34665] == 125456 assert reloaded.tag_v2[34665] == 125456

View File

@ -76,6 +76,7 @@ def test_pil184() -> None:
def test_1px_width(tmp_path: Path) -> None: def test_1px_width(tmp_path: Path) -> None:
im = Image.new("L", (1, 256)) im = Image.new("L", (1, 256))
px = im.load() px = im.load()
assert px is not None
for y in range(256): for y in range(256):
px[0, y] = y px[0, y] = y
_roundtrip(tmp_path, im) _roundtrip(tmp_path, im)
@ -84,6 +85,7 @@ def test_1px_width(tmp_path: Path) -> None:
def test_large_count(tmp_path: Path) -> None: def test_large_count(tmp_path: Path) -> None:
im = Image.new("L", (256, 1)) im = Image.new("L", (256, 1))
px = im.load() px = im.load()
assert px is not None
for x in range(256): for x in range(256):
px[x, 0] = x // 67 * 67 px[x, 0] = x // 67 * 67
_roundtrip(tmp_path, im) _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: def test_break_in_count_overflow(tmp_path: Path) -> None:
im = Image.new("L", (256, 5)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
assert px is not None
for y in range(4): for y in range(4):
for x in range(256): for x in range(256):
px[x, y] = x % 128 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: def test_break_one_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
assert px is not None
for y in range(5): for y in range(5):
for x in range(256): for x in range(256):
px[x, y] = x % 128 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: def test_break_many_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
assert px is not None
for y in range(4): for y in range(4):
for x in range(256): for x in range(256):
px[x, y] = x % 128 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: def test_break_one_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
assert px is not None
for y in range(5): for y in range(5):
for x in range(256): for x in range(256):
px[x, y] = x % 128 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: def test_break_many_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
assert px is not None
for y in range(5): for y in range(5):
for x in range(256): for x in range(256):
px[x, y] = x % 128 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: def test_break_padding(tmp_path: Path) -> None:
im = Image.new("L", (257, 5)) im = Image.new("L", (257, 5))
px = im.load() px = im.load()
assert px is not None
for y in range(5): for y in range(5):
for x in range(257): for x in range(257):
px[x, y] = x % 128 px[x, y] = x % 128

View File

@ -4,9 +4,10 @@ import os
import os.path import os.path
import tempfile import tempfile
import time import time
from collections.abc import Generator
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any, Generator from typing import Any
import pytest import pytest

View File

@ -72,12 +72,21 @@ def test_palette_depth_8(tmp_path: Path) -> None:
def test_palette_depth_16(tmp_path: Path) -> None: def test_palette_depth_16(tmp_path: Path) -> None:
with Image.open("Tests/images/p_16.tga") as im: 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") out = str(tmp_path / "temp.png")
im.save(out) im.save(out)
with Image.open(out) as reloaded: 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: def test_id_field() -> None:

View File

@ -2,10 +2,10 @@ from __future__ import annotations
import os import os
import warnings import warnings
from collections.abc import Generator
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
from typing import Generator
import pytest import pytest

View File

@ -8,6 +8,7 @@ import sys
import tempfile import tempfile
import warnings import warnings
from pathlib import Path from pathlib import Path
from types import ModuleType
from typing import IO, Any from typing import IO, Any
import pytest import pytest
@ -35,6 +36,12 @@ from .helper import (
skip_unless_feature, skip_unless_feature,
) )
ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
ElementTree = None
# Deprecation helper # Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
@ -125,6 +132,15 @@ class TestImage:
assert im.mode == "RGB" assert im.mode == "RGB"
assert im.size == (128, 128) 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: def test_width_height(self) -> None:
im = Image.new("RGB", (1, 2)) im = Image.new("RGB", (1, 2))
assert im.width == 1 assert im.width == 1
@ -559,6 +575,7 @@ class TestImage:
for mode in ("I", "F", "L"): for mode in ("I", "F", "L"):
im = Image.new(mode, (100, 100), (5,)) im = Image.new(mode, (100, 100), (5,))
px = im.load() px = im.load()
assert px is not None
assert px[0, 0] == 5 assert px[0, 0] == 5
def test_linear_gradient_wrong_mode(self) -> None: def test_linear_gradient_wrong_mode(self) -> None:
@ -921,6 +938,21 @@ class TestImage:
with Image.open("Tests/images/hopper.gif") as im: with Image.open("Tests/images/hopper.gif") as im:
assert im.getxmp() == {} assert im.getxmp() == {}
def test_getxmp_padded(self) -> None:
im = Image.new("RGB", (1, 1))
im.info["xmp"] = (
b'<?xpacket begin="\xef\xbb\xbf" id="W5M0MpCehiHzreSzNTczkc9d"?>\n'
b'<x:xmpmeta xmlns:x="adobe:ns:meta/" />\n<?xpacket end="w"?>\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))) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size: tuple[int, int]) -> None: def test_zero_tobytes(self, size: tuple[int, int]) -> None:
im = Image.new("RGB", size) im = Image.new("RGB", size)

View File

@ -12,19 +12,6 @@ from PIL import Image
from .helper import assert_image_equal, hopper, is_win32 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 numpy: ModuleType | None
try: try:
import numpy import numpy
@ -32,21 +19,7 @@ except ImportError:
numpy = None numpy = None
class AccessTest: class TestImagePutPixel:
# 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):
def test_sanity(self) -> None: def test_sanity(self) -> None:
im1 = hopper() im1 = hopper()
im2 = Image.new(im1.mode, im1.size, 0) im2 = Image.new(im1.mode, im1.size, 0)
@ -74,6 +47,8 @@ class TestImagePutPixel(AccessTest):
pix1 = im1.load() pix1 = im1.load()
pix2 = im2.load() pix2 = im2.load()
assert pix1 is not None
assert pix2 is not None
with pytest.raises(TypeError): with pytest.raises(TypeError):
pix1[0, "0"] pix1[0, "0"]
with pytest.raises(TypeError): with pytest.raises(TypeError):
@ -116,6 +91,8 @@ class TestImagePutPixel(AccessTest):
pix1 = im1.load() pix1 = im1.load()
pix2 = im2.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 y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1): for x in range(-1, -im1.size[0] - 1, -1):
pix2[x, y] = pix1[x, y] pix2[x, y] = pix1[x, y]
@ -125,13 +102,14 @@ class TestImagePutPixel(AccessTest):
@pytest.mark.skipif(numpy is None, reason="NumPy not installed") @pytest.mark.skipif(numpy is None, reason="NumPy not installed")
def test_numpy(self) -> None: def test_numpy(self) -> None:
im = hopper() im = hopper()
pix = im.load() px = im.load()
assert px is not None
assert numpy 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 @staticmethod
def color(mode: str) -> int | tuple[int, ...]: def color(mode: str) -> int | tuple[int, ...]:
bands = Image.getmodebands(mode) bands = Image.getmodebands(mode)
@ -144,9 +122,6 @@ class TestImageGetPixel(AccessTest):
return tuple(range(1, bands + 1)) return tuple(range(1, bands + 1))
def check(self, mode: str, expected_color_int: int | None = None) -> None: 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 = ( expected_color = (
self.color(mode) if expected_color_int is None else expected_color_int self.color(mode) if expected_color_int is None else expected_color_int
) )
@ -171,15 +146,14 @@ class TestImageGetPixel(AccessTest):
# Check 0x0 image with None initial color # Check 0x0 image with None initial color
im = Image.new(mode, (0, 0), None) im = Image.new(mode, (0, 0), None)
assert im.load() is not None assert im.load() is not None
error = ValueError if self._need_cffi_access else IndexError with pytest.raises(IndexError):
with pytest.raises(error):
im.putpixel((0, 0), expected_color) im.putpixel((0, 0), expected_color)
with pytest.raises(error): with pytest.raises(IndexError):
im.getpixel((0, 0)) im.getpixel((0, 0))
# Check negative index # Check negative index
with pytest.raises(error): with pytest.raises(IndexError):
im.putpixel((-1, -1), expected_color) im.putpixel((-1, -1), expected_color)
with pytest.raises(error): with pytest.raises(IndexError):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
# Check initial color # Check initial color
@ -199,10 +173,10 @@ class TestImageGetPixel(AccessTest):
# Check 0x0 image with initial color # Check 0x0 image with initial color
im = Image.new(mode, (0, 0), expected_color) im = Image.new(mode, (0, 0), expected_color)
with pytest.raises(error): with pytest.raises(IndexError):
im.getpixel((0, 0)) im.getpixel((0, 0))
# Check negative index # Check negative index
with pytest.raises(error): with pytest.raises(IndexError):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
@pytest.mark.parametrize("mode", Image.MODES) @pytest.mark.parametrize("mode", Image.MODES)
@ -235,126 +209,7 @@ class TestImageGetPixel(AccessTest):
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
@pytest.mark.filterwarnings("ignore::DeprecationWarning") class TestImagePutPixelError:
@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):
IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"]
IMAGE_MODES2 = ["L", "I", "I;16"] IMAGE_MODES2 = ["L", "I", "I;16"]
INVALID_TYPES = ["foo", 1.0, None] INVALID_TYPES = ["foo", 1.0, None]

View File

@ -222,8 +222,10 @@ def test_l_macro_rounding(convert_mode: str) -> None:
converted_im = im.convert(convert_mode) converted_im = im.convert(convert_mode)
px = converted_im.load() px = converted_im.load()
assert px is not None
converted_color = px[0, 0] converted_color = px[0, 0]
if convert_mode == "LA": if convert_mode == "LA":
assert converted_color is not None
converted_color = converted_color[0] converted_color = converted_color[0]
assert converted_color == 1 assert converted_color == 1

View File

@ -12,9 +12,10 @@ from .helper import hopper
def test_sanity() -> None: def test_sanity() -> None:
im = hopper() 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: def test_close() -> None:

View File

@ -14,6 +14,7 @@ class TestImagingPaste:
self, im: Image.Image, expected: list[tuple[int, int, int, int]] self, im: Image.Image, expected: list[tuple[int, int, int, int]]
) -> None: ) -> None:
px = im.load() px = im.load()
assert px is not None
actual = [ actual = [
px[0, 0], px[0, 0],
px[self.size // 2, 0], px[self.size // 2, 0],
@ -48,6 +49,7 @@ class TestImagingPaste:
def mask_1(self) -> Image.Image: def mask_1(self) -> Image.Image:
mask = Image.new("1", (self.size, self.size)) mask = Image.new("1", (self.size, self.size))
px = mask.load() px = mask.load()
assert px is not None
for y in range(mask.height): for y in range(mask.height):
for x in range(mask.width): for x in range(mask.width):
px[y, x] = (x + y) % 2 px[y, x] = (x + y) % 2
@ -61,6 +63,7 @@ class TestImagingPaste:
def gradient_L(self) -> Image.Image: def gradient_L(self) -> Image.Image:
gradient = Image.new("L", (self.size, self.size)) gradient = Image.new("L", (self.size, self.size))
px = gradient.load() px = gradient.load()
assert px is not None
for y in range(gradient.height): for y in range(gradient.height):
for x in range(gradient.width): for x in range(gradient.width):
px[y, x] = (x + y) % 255 px[y, x] = (x + y) % 255

View File

@ -80,6 +80,7 @@ def test_quantize_no_dither2() -> None:
assert tuple(quantized.palette.palette) == data assert tuple(quantized.palette.palette) == data
px = quantized.load() px = quantized.load()
assert px is not None
for x in range(9): for x in range(9):
assert px[x, 0] == (0 if x < 5 else 1) assert px[x, 0] == (0 if x < 5 else 1)
@ -118,10 +119,12 @@ def test_colors() -> None:
def test_transparent_colors_equal() -> None: def test_transparent_colors_equal() -> None:
im = Image.new("RGBA", (1, 2), (0, 0, 0, 0)) im = Image.new("RGBA", (1, 2), (0, 0, 0, 0))
px = im.load() px = im.load()
assert px is not None
px[0, 1] = (255, 255, 255, 0) px[0, 1] = (255, 255, 255, 0)
converted = im.quantize() converted = im.quantize()
converted_px = converted.load() converted_px = converted.load()
assert converted_px is not None
assert converted_px[0, 0] == converted_px[0, 1] assert converted_px[0, 0] == converted_px[0, 1]
@ -139,6 +142,7 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None:
converted = im.quantize(method=method) converted = im.quantize(method=method)
converted_px = converted.load() converted_px = converted.load()
assert converted_px is not None
assert converted_px[0, 0] == converted.palette.colors[color] assert converted_px[0, 0] == converted.palette.colors[color]

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Generator
from contextlib import contextmanager from contextlib import contextmanager
from typing import Generator
import pytest import pytest
@ -74,6 +74,7 @@ class TestImagingCoreResampleAccuracy:
data = data.replace(" ", "") data = data.replace(" ", "")
sample = Image.new("L", size) sample = Image.new("L", size)
s_px = sample.load() s_px = sample.load()
assert s_px is not None
w, h = size[0] // 2, size[1] // 2 w, h = size[0] // 2, size[1] // 2
for y in range(h): for y in range(h):
for x in range(w): for x in range(w):
@ -87,6 +88,8 @@ class TestImagingCoreResampleAccuracy:
def check_case(self, case: Image.Image, sample: Image.Image) -> None: def check_case(self, case: Image.Image, sample: Image.Image) -> None:
s_px = sample.load() s_px = sample.load()
c_px = case.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 y in range(case.size[1]):
for x in range(case.size[0]): for x in range(case.size[0]):
if c_px[x, y] != s_px[x, y]: if c_px[x, y] != s_px[x, y]:
@ -98,6 +101,7 @@ class TestImagingCoreResampleAccuracy:
def serialize_image(self, image: Image.Image) -> str: def serialize_image(self, image: Image.Image) -> str:
s_px = image.load() s_px = image.load()
assert s_px is not None
return "\n".join( return "\n".join(
" ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0])) " ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0]))
for y in range(image.size[1]) for y in range(image.size[1])
@ -235,11 +239,14 @@ class TestCoreResampleConsistency:
self, mode: str, fill: tuple[int, int, int] | float self, mode: str, fill: tuple[int, int, int] | float
) -> tuple[Image.Image, tuple[int, ...]]: ) -> tuple[Image.Image, tuple[int, ...]]:
im = Image.new(mode, (512, 9), fill) 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, int | tuple[int, ...]]) -> None:
channel, color = case channel, color = case
px = channel.load() px = channel.load()
assert px is not None
for x in range(channel.size[0]): for x in range(channel.size[0]):
for y in range(channel.size[1]): for y in range(channel.size[1]):
if px[x, y] != color: if px[x, y] != color:
@ -271,6 +278,7 @@ class TestCoreResampleAlphaCorrect:
def make_levels_case(self, mode: str) -> Image.Image: def make_levels_case(self, mode: str) -> Image.Image:
i = Image.new(mode, (256, 16)) i = Image.new(mode, (256, 16))
px = i.load() px = i.load()
assert px is not None
for y in range(i.size[1]): for y in range(i.size[1]):
for x in range(i.size[0]): for x in range(i.size[0]):
pix = [x] * len(mode) pix = [x] * len(mode)
@ -280,6 +288,7 @@ class TestCoreResampleAlphaCorrect:
def run_levels_case(self, i: Image.Image) -> None: def run_levels_case(self, i: Image.Image) -> None:
px = i.load() px = i.load()
assert px is not None
for y in range(i.size[1]): for y in range(i.size[1]):
used_colors = {px[x, y][0] for x in range(i.size[0])} used_colors = {px[x, y][0] for x in range(i.size[0])}
assert 256 == len(used_colors), ( assert 256 == len(used_colors), (
@ -310,6 +319,7 @@ class TestCoreResampleAlphaCorrect:
) -> Image.Image: ) -> Image.Image:
i = Image.new(mode, (64, 64), dirty_pixel) i = Image.new(mode, (64, 64), dirty_pixel)
px = i.load() px = i.load()
assert px is not None
xdiv4 = i.size[0] // 4 xdiv4 = i.size[0] // 4
ydiv4 = i.size[1] // 4 ydiv4 = i.size[1] // 4
for y in range(ydiv4 * 2): for y in range(ydiv4 * 2):
@ -319,6 +329,7 @@ class TestCoreResampleAlphaCorrect:
def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None: def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None:
px = i.load() px = i.load()
assert px is not None
for y in range(i.size[1]): for y in range(i.size[1]):
for x in range(i.size[0]): for x in range(i.size[0]):
if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel: if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel:
@ -406,6 +417,7 @@ class TestCoreResampleCoefficients:
draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color) draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color)
px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load() px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load()
assert px is not None
if px[2, 0] != test_color // 2: if px[2, 0] != test_color // 2:
assert test_color // 2 == px[2, 0] assert test_color // 2 == px[2, 0]

View File

@ -4,9 +4,9 @@ Tests for resize functionality.
from __future__ import annotations from __future__ import annotations
from collections.abc import Generator
from itertools import permutations from itertools import permutations
from pathlib import Path from pathlib import Path
from typing import Generator
import pytest import pytest

View File

@ -679,7 +679,8 @@ def test_auxiliary_channels_isolated() -> None:
def test_long_modes() -> None: def test_long_modes() -> None:
p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc") 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")) @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
@ -700,3 +701,9 @@ def test_deprecation() -> None:
assert ImageCms.VERSION == "1.0.0 pil" assert ImageCms.VERSION == "1.0.0 pil"
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert isinstance(ImageCms.FLAGS, dict) 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")

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import contextlib import contextlib
import os.path import os.path
from typing import Sequence from collections.abc import Sequence
import pytest import pytest

View File

@ -9,51 +9,57 @@ from PIL import Image, ImageDraw, ImageFont, _util, features
from .helper import assert_image_equal_tofile 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: @pytest.mark.parametrize("font", fonts)
if features.check_module("freetype2"): def test_default_font(font: ImageFont.ImageFont) -> None:
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
def teardown_module() -> None:
ImageFont.core = original_core
def test_default_font() -> None:
# Arrange # Arrange
txt = 'This is a "better than nothing" default font.' txt = 'This is a "better than nothing" default font.'
im = Image.new(mode="RGB", size=(300, 100)) im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
default_font = ImageFont.load_default() draw.text((10, 10), txt, font=font)
draw.text((10, 10), txt, font=default_font)
# Assert # Assert
assert_image_equal_tofile(im, "Tests/images/default_font.png") assert_image_equal_tofile(im, "Tests/images/default_font.png")
def test_size_without_freetype() -> None: def test_without_freetype() -> None:
with pytest.raises(ImportError): original_core = ImageFont.core
ImageFont.load_default(size=14) 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 # should not segfault, should return UnicodeDecodeError
# issue #2826 # issue #2826
font = ImageFont.load_default()
with pytest.raises(UnicodeEncodeError): with pytest.raises(UnicodeEncodeError):
font.getbbox("") font.getbbox("")
def test_textbbox() -> None: @pytest.mark.parametrize("font", fonts)
def test_textbbox(font: ImageFont.ImageFont) -> None:
im = Image.new("RGB", (200, 200)) im = Image.new("RGB", (200, 200))
d = ImageDraw.Draw(im) d = ImageDraw.Draw(im)
default_font = ImageFont.load_default() assert d.textlength("test", font=font) == 24
assert d.textlength("test", font=default_font) == 24 assert d.textbbox((0, 0), "test", font=font) == (0, 0, 24, 11)
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
def test_decompression_bomb() -> None: def test_decompression_bomb() -> None:
@ -76,8 +82,3 @@ def test_oom() -> None:
font = ImageFont.ImageFont() font = ImageFont.ImageFont()
font._load_pilfont_data(fp, Image.new("L", (1, 1))) font._load_pilfont_data(fp, Image.new("L", (1, 1)))
font.getmask("A" * 1_000_000) font.getmask("A" * 1_000_000)
def test_freetypefont_without_freetype() -> None:
with pytest.raises(ImportError):
ImageFont.truetype("Tests/fonts/FreeMono.ttf")

View File

@ -165,10 +165,14 @@ def test_pad() -> None:
def test_pad_round() -> None: def test_pad_round() -> None:
im = Image.new("1", (1, 1), 1) im = Image.new("1", (1, 1), 1)
new_im = ImageOps.pad(im, (4, 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)) 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")) @pytest.mark.parametrize("mode", ("P", "PA"))
@ -223,6 +227,7 @@ def test_expand_palette(border: int | tuple[int, int, int, int]) -> None:
else: else:
left, top, right, bottom = border left, top, right, bottom = border
px = im_expanded.convert("RGB").load() px = im_expanded.convert("RGB").load()
assert px is not None
for x in range(im_expanded.width): for x in range(im_expanded.width):
for b in range(top): for b in range(top):
assert px[x, b] == (255, 0, 0) assert px[x, b] == (255, 0, 0)
@ -432,6 +437,16 @@ def test_exif_transpose() -> None:
assert 0x0112 not in transposed_im.getexif() 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 0x0112 not in transposed_im.getexif()
def test_exif_transpose_in_place() -> None: def test_exif_transpose_in_place() -> None:
with Image.open("Tests/images/orientation_rectangle.jpg") as im: with Image.open("Tests/images/orientation_rectangle.jpg") as im:
assert im.size == (2, 1) assert im.size == (2, 1)

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Generator from collections.abc import Generator
import pytest import pytest

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import array import array
import math import math
import struct import struct
from typing import Sequence from collections.abc import Sequence
import pytest import pytest

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import os
from typing import Any from typing import Any
import pytest import pytest
@ -65,6 +66,27 @@ def test_show_without_viewers() -> None:
ImageShow._viewers = viewers 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: def test_viewer() -> None:
viewer = ImageShow.Viewer() viewer = ImageShow.Viewer()

View File

@ -70,6 +70,11 @@ def test_photoimage(mode: str) -> None:
reloaded = ImageTk.getimage(im_tk) reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded, im.convert("RGBA")) 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: def test_photoimage_apply_transparency() -> None:
with Image.open("Tests/images/pil123p.png") as im: with Image.open("Tests/images/pil123p.png") as im:

View File

@ -16,6 +16,8 @@ def verify(im1: Image.Image) -> None:
assert im1.size == im2.size assert im1.size == im2.size
pix1 = im1.load() pix1 = im1.load()
pix2 = im2.load() pix2 = im2.load()
assert pix1 is not None
assert pix2 is not None
for y in range(im1.size[1]): for y in range(im1.size[1]):
for x in range(im1.size[0]): for x in range(im1.size[0]):
xy = x, y xy = x, y

View File

@ -109,6 +109,7 @@ def _test_img_equals_nparray(img: Image.Image, np_img: _typing.NumpyArray) -> No
np_size = np_img.shape[1], np_img.shape[0] np_size = np_img.shape[1], np_img.shape[0]
assert img.size == np_size assert img.size == np_size
px = img.load() px = img.load()
assert px is not None
for x in range(0, img.size[0], int(img.size[0] / 10)): 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)): for y in range(0, img.size[1], int(img.size[1] / 10)):
assert_deep_equal(px[x, y], np_img[y, x]) assert_deep_equal(px[x, y], np_img[y, x])
@ -141,6 +142,7 @@ def test_save_tiff_uint16() -> None:
img = Image.fromarray(a) img = Image.fromarray(a)
img_px = img.load() img_px = img.load()
assert img_px is not None
assert img_px[0, 0] == pixel_value assert img_px[0, 0] == pixel_value

View File

@ -17,6 +17,5 @@ coverage:
# Matches 'omit:' in .coveragerc # Matches 'omit:' in .coveragerc
ignore: ignore:
- "Tests/32bit_segfault_check.py" - "Tests/32bit_segfault_check.py"
- "Tests/bench_cffi_access.py"
- "Tests/check_*.py" - "Tests/check_*.py"
- "Tests/createfontdatachunk.py" - "Tests/createfontdatachunk.py"

View File

@ -338,7 +338,7 @@ linkcheck_allowed_redirects = {
# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html # https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html
_repo = "https://github.com/python-pillow/Pillow/" _repo = "https://github.com/python-pillow/Pillow/"
extlinks = { 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"), "cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
"issue": (_repo + "issues/%s", "#%s"), "issue": (_repo + "issues/%s", "#%s"),
"pr": (_repo + "pull/%s", "#%s"), "pr": (_repo + "pull/%s", "#%s"),

View File

@ -12,28 +12,6 @@ Deprecated features
Below are features which are considered deprecated. Where appropriate, Below are features which are considered deprecated. Where appropriate,
a :py:exc:`DeprecationWarning` is issued. 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 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. 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 Support for LibTIFF earlier than 4
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -128,6 +115,29 @@ Removed features
Deprecated features are only removed in major releases after an appropriate Deprecated features are only removed in major releases after an appropriate
period of deprecation has passed. period of deprecation has passed.
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 Tk/Tcl 8.4
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -68,8 +68,8 @@ and :pypi:`olefile` for Pillow to read FPX and MIC images::
.. tab:: Windows .. tab:: Windows
We provide Pillow binaries for Windows compiled for the matrix of supported 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 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 These binaries include support
for all optional libraries except libimagequant and libxcb. Raqm support for all optional libraries except libimagequant and libxcb. Raqm support
requires FriBiDi to be installed separately:: requires FriBiDi to be installed separately::

View File

@ -1,8 +1,9 @@
Python,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5 Python,3.13,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 >= 11,Yes,Yes,Yes,Yes,Yes,,,,
Pillow 10.0,,Yes,Yes,Yes,Yes,,, Pillow 10.1 - 10.4,,Yes,Yes,Yes,Yes,Yes,,,
Pillow 9.3 - 9.5,,Yes,Yes,Yes,Yes,Yes,, Pillow 10.0,,,Yes,Yes,Yes,Yes,,,
Pillow 9.0 - 9.2,,,Yes,Yes,Yes,Yes,, Pillow 9.3 - 9.5,,,Yes,Yes,Yes,Yes,Yes,,
Pillow 8.3.2 - 8.4,,,Yes,Yes,Yes,Yes,Yes, Pillow 9.0 - 9.2,,,,Yes,Yes,Yes,Yes,,
Pillow 8.0 - 8.3.1,,,,Yes,Yes,Yes,Yes, Pillow 8.3.2 - 8.4,,,,Yes,Yes,Yes,Yes,Yes,
Pillow 7.0 - 7.2,,,,,Yes,Yes,Yes,Yes Pillow 8.0 - 8.3.1,,,,,Yes,Yes,Yes,Yes,
Pillow 7.0 - 7.2,,,,,,Yes,Yes,Yes,Yes

1 Python 3.13 3.12 3.11 3.10 3.9 3.8 3.7 3.6 3.5
2 Pillow >= 10.1 Pillow >= 11 Yes Yes Yes Yes Yes Yes
3 Pillow 10.0 Pillow 10.1 - 10.4 Yes Yes Yes Yes Yes
4 Pillow 9.3 - 9.5 Pillow 10.0 Yes Yes Yes Yes Yes
5 Pillow 9.0 - 9.2 Pillow 9.3 - 9.5 Yes Yes Yes Yes Yes
6 Pillow 8.3.2 - 8.4 Pillow 9.0 - 9.2 Yes Yes Yes Yes Yes
7 Pillow 8.0 - 8.3.1 Pillow 8.3.2 - 8.4 Yes Yes Yes Yes Yes
8 Pillow 7.0 - 7.2 Pillow 8.0 - 8.3.1 Yes Yes Yes Yes Yes
9 Pillow 7.0 - 7.2 Yes Yes Yes Yes

View File

@ -27,8 +27,6 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| CentOS Stream 9 | 3.9 | x86-64 | | CentOS Stream 9 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 11 Bullseye | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 | | Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 39 | 3.12 | 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 | | 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 | | macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
| | PyPy3 | | | | PyPy3 | |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | | Ubuntu Linux 22.04 LTS (Jammy) | 3.9, 3.10, 3.11, | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | | | | 3.12, 3.13, PyPy3 | |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.10 | arm64v8 | | | 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, | | Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
| | | s390x | | | | 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, 3.13, PyPy3 | |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.12 | x86 | | | 3.12 | x86 |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.9 (MinGW) | x86-64 | | | 3.9 (MinGW) | x86-64 |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.8, 3.9 (Cygwin) | x86-64 | | | 3.9 (Cygwin) | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+

View File

@ -381,6 +381,11 @@ Constants
Set to 89,478,485, approximately 0.25GB for a 24-bit (3 bpp) image. 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. 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 Transpose methods
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^

View File

@ -53,6 +53,7 @@ Functions
.. autofunction:: PIL.ImageFont.load_path .. autofunction:: PIL.ImageFont.load_path
.. autofunction:: PIL.ImageFont.truetype .. autofunction:: PIL.ImageFont.truetype
.. autofunction:: PIL.ImageFont.load_default .. autofunction:: PIL.ImageFont.load_default
.. autofunction:: PIL.ImageFont.load_default_imagefont
Methods Methods
------- -------

View File

@ -1,47 +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:
:special-members: __getitem__, __setitem__

View File

@ -32,7 +32,6 @@ Reference
JpegPresets JpegPresets
PSDraw PSDraw
PixelAccess PixelAccess
PyAccess
features features
../PIL ../PIL
plugins plugins

View File

@ -158,7 +158,7 @@ PyAccess and Image.USE_CFFI_ACCESS
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Since Pillow's C API is now faster than PyAccess on PyPy, 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. 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 ``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is

View File

@ -4,21 +4,16 @@
Security 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 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.
TODO
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations 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. 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 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. The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
API Changes
===========
TODO
^^^^
TODO
API Additions 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 :py:meth:`~PIL.ImageDraw.ImageDraw.ellipse`, but instead of taking a bounding box, it
takes a center point and radius. takes a center point and radius.
TODO
^^^^
TODO
Other Changes Other Changes
============= =============

View File

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

View File

@ -32,7 +32,7 @@ Deprecations
PSFile 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 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 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. though, it is a very short class that can easily be recreated in your own code.

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
11.0.0
10.4.0 10.4.0
10.3.0 10.3.0
10.2.0 10.2.0

View File

@ -14,18 +14,20 @@ readme = "README.md"
keywords = [ keywords = [
"Imaging", "Imaging",
] ]
license = {text = "HPND"} license = { text = "HPND" }
authors = [{name = "Jeffrey A. Clark", email = "aclark@aclark.net"}] authors = [
requires-python = ">=3.8" { name = "Jeffrey A. Clark", email = "aclark@aclark.net" },
]
requires-python = ">=3.9"
classifiers = [ classifiers = [
"Development Status :: 6 - Mature", "Development Status :: 6 - Mature",
"License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Multimedia :: Graphics", "Topic :: Multimedia :: Graphics",
@ -38,8 +40,7 @@ classifiers = [
dynamic = [ dynamic = [
"version", "version",
] ]
[project.optional-dependencies] optional-dependencies.docs = [
docs = [
"furo", "furo",
"olefile", "olefile",
"sphinx>=7.3", "sphinx>=7.3",
@ -47,13 +48,13 @@ docs = [
"sphinx-inline-tabs", "sphinx-inline-tabs",
"sphinxext-opengraph", "sphinxext-opengraph",
] ]
fpx = [ optional-dependencies.fpx = [
"olefile", "olefile",
] ]
mic = [ optional-dependencies.mic = [
"olefile", "olefile",
] ]
tests = [ optional-dependencies.tests = [
"check-manifest", "check-manifest",
"coverage", "coverage",
"defusedxml", "defusedxml",
@ -65,28 +66,29 @@ tests = [
"pytest-cov", "pytest-cov",
"pytest-timeout", "pytest-timeout",
] ]
typing = [ optional-dependencies.typing = [
'typing-extensions; python_version < "3.10"', "typing-extensions; python_version<'3.10'",
] ]
xmp = [ optional-dependencies.xmp = [
"defusedxml", "defusedxml",
] ]
[project.urls] urls.Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst"
Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst" urls.Documentation = "https://pillow.readthedocs.io"
Documentation = "https://pillow.readthedocs.io" urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi"
Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" urls.Homepage = "https://python-pillow.org"
Homepage = "https://python-pillow.org" urls.Mastodon = "https://fosstodon.org/@pillow"
Mastodon = "https://fosstodon.org/@pillow" urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
"Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" urls.Source = "https://github.com/python-pillow/Pillow"
Source = "https://github.com/python-pillow/Pillow"
[tool.setuptools] [tool.setuptools]
packages = ["PIL"] packages = [
"PIL",
]
include-package-data = true include-package-data = true
package-dir = {"" = "src"} package-dir = { "" = "src" }
[tool.setuptools.dynamic] [tool.setuptools.dynamic]
version = {attr = "PIL.__version__"} version = { attr = "PIL.__version__" }
[tool.cibuildwheel] [tool.cibuildwheel]
before-all = ".github/workflows/wheels-dependencies.sh" before-all = ".github/workflows/wheels-dependencies.sh"
@ -98,45 +100,53 @@ test-extras = "tests"
[tool.ruff] [tool.ruff]
fix = true fix = true
[tool.ruff.lint] lint.select = [
select = [ "C4", # flake8-comprehensions
"C4", # flake8-comprehensions "E", # pycodestyle errors
"E", # pycodestyle errors "EM", # flake8-errmsg
"EM", # flake8-errmsg "F", # pyflakes errors
"F", # pyflakes errors "I", # isort
"I", # isort "ISC", # flake8-implicit-str-concat
"ISC", # flake8-implicit-str-concat "LOG", # flake8-logging
"LOG", # flake8-logging "PGH", # pygrep-hooks
"PGH", # pygrep-hooks "PYI", # flake8-pyi
"PYI", # flake8-pyi
"RUF100", # unused noqa (yesqa) "RUF100", # unused noqa (yesqa)
"UP", # pyupgrade "UP", # pyupgrade
"W", # pycodestyle warnings "W", # pycodestyle warnings
"YTT", # flake8-2020 "YTT", # flake8-2020
] ]
ignore = [ lint.ignore = [
"E203", # Whitespace before ':' "E203", # Whitespace before ':'
"E221", # Multiple spaces before operator "E221", # Multiple spaces before operator
"E226", # Missing whitespace around arithmetic operator "E226", # Missing whitespace around arithmetic operator
"E241", # Multiple spaces after ',' "E241", # Multiple spaces after ','
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
"PYI034", # flake8-pyi: typing.Self added in Python 3.11 "PYI034", # flake8-pyi: typing.Self added in Python 3.11
] ]
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] [tool.pyproject-fmt]
"Tests/oss-fuzz/fuzz_font.py" = ["I002"] max_supported_python = "3.13"
"Tests/oss-fuzz/fuzz_pillow.py" = ["I002"]
[tool.ruff.lint.isort]
known-first-party = ["PIL"]
required-imports = ["from __future__ import annotations"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "-ra --color=yes" addopts = "-ra --color=yes"
testpaths = ["Tests"] testpaths = [
"Tests",
]
[tool.mypy] [tool.mypy]
python_version = "3.8" python_version = "3.9"
pretty = true pretty = true
disallow_any_generics = true disallow_any_generics = true
enable_error_code = "ignore-without-code" enable_error_code = "ignore-without-code"

View File

@ -43,7 +43,7 @@ WEBP_ROOT = None
ZLIB_ROOT = None ZLIB_ROOT = None
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ 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 import atexit
atexit.register( atexit.register(

View File

@ -31,7 +31,6 @@ from typing import IO
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i32le as i32 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 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: def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)

View File

@ -53,7 +53,7 @@ class FpxImageFile(ImageFile.ImageFile):
format = "FPX" format = "FPX"
format_description = "FlashPix" format_description = "FlashPix"
def _open(self): def _open(self) -> None:
# #
# read the OLE directory and see if this is a likely # read the OLE directory and see if this is a likely
# to be a FlashPix file # to be a FlashPix file
@ -64,7 +64,8 @@ class FpxImageFile(ImageFile.ImageFile):
msg = "not an FPX file; invalid OLE file" msg = "not an FPX file; invalid OLE file"
raise SyntaxError(msg) from e 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" msg = "not an FPX file; bad root CLSID"
raise SyntaxError(msg) raise SyntaxError(msg)
@ -99,8 +100,7 @@ class FpxImageFile(ImageFile.ImageFile):
s = prop[0x2000002 | id] s = prop[0x2000002 | id]
bands = i32(s, 4) if not isinstance(s, bytes) or (bands := i32(s, 4)) > 4:
if bands > 4:
msg = "Invalid number of bands" msg = "Invalid number of bands"
raise OSError(msg) raise OSError(msg)

View File

@ -31,7 +31,7 @@ import subprocess
import sys import sys
from enum import IntEnum from enum import IntEnum
from functools import cached_property 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 ( from . import (
Image, Image,
@ -330,7 +330,6 @@ class GifImageFile(ImageFile.ImageFile):
LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
or palette or palette
): ):
self.pyaccess = None
if "transparency" in self.info: if "transparency" in self.info:
self.im.putpalettealpha(self.info["transparency"], 0) self.im.putpalettealpha(self.info["transparency"], 0)
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
@ -504,7 +503,7 @@ def _normalize_mode(im: Image.Image) -> Image.Image:
return im.convert("L") return im.convert("L")
_Palette = Union[bytes, bytearray, List[int], ImagePalette.ImagePalette] _Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette]
def _normalize_palette( def _normalize_palette(

View File

@ -329,7 +329,6 @@ class IcoImageFile(ImageFile.ImageFile):
# if tile is PNG, it won't really be loaded yet # if tile is PNG, it won't really be loaded yet
im.load() im.load()
self.im = im.im self.im = im.im
self.pyaccess = None
self._mode = im.mode self._mode = im.mode
if im.palette: if im.palette:
self.palette = im.palette self.palette = im.palette

View File

@ -38,7 +38,7 @@ import struct
import sys import sys
import tempfile import tempfile
import warnings import warnings
from collections.abc import Callable, MutableMapping from collections.abc import Callable, MutableMapping, Sequence
from enum import IntEnum from enum import IntEnum
from types import ModuleType from types import ModuleType
from typing import ( from typing import (
@ -47,8 +47,6 @@ from typing import (
Any, Any,
Literal, Literal,
Protocol, Protocol,
Sequence,
Tuple,
cast, cast,
) )
@ -85,6 +83,8 @@ class DecompressionBombError(Exception):
pass pass
WARN_POSSIBLE_FORMATS: bool = False
# Limit to around a quarter gigabyte for a 24-bit (3 bpp) image # Limit to around a quarter gigabyte for a 24-bit (3 bpp) image
MAX_IMAGE_PIXELS: int | None = int(1024 * 1024 * 1024 // 4 // 3) MAX_IMAGE_PIXELS: int | None = int(1024 * 1024 * 1024 // 4 // 3)
@ -123,14 +123,6 @@ except ImportError as v:
raise raise
USE_CFFI_ACCESS = False
cffi: ModuleType | None
try:
import cffi
except ImportError:
cffi = None
def isImageType(t: Any) -> TypeGuard[Image]: def isImageType(t: Any) -> TypeGuard[Image]:
""" """
Checks if an object is an image object. Checks if an object is an image object.
@ -227,7 +219,7 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# Registries # Registries
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ImageFile, PyAccess from . import ImageFile, ImagePalette
ID: list[str] = [] ID: list[str] = []
OPEN: dict[ OPEN: dict[
str, str,
@ -547,7 +539,6 @@ class Image:
self.palette = None self.palette = None
self.info = {} self.info = {}
self.readonly = 0 self.readonly = 0
self.pyaccess = None
self._exif = None self._exif = None
@property @property
@ -629,7 +620,6 @@ class Image:
def _copy(self) -> None: def _copy(self) -> None:
self.load() self.load()
self.im = self.im.copy() self.im = self.im.copy()
self.pyaccess = None
self.readonly = 0 self.readonly = 0
def _ensure_mutable(self) -> None: def _ensure_mutable(self) -> None:
@ -880,7 +870,7 @@ class Image:
msg = "cannot decode image data" msg = "cannot decode image data"
raise ValueError(msg) raise ValueError(msg)
def load(self) -> core.PixelAccess | PyAccess.PyAccess | None: def load(self) -> core.PixelAccess | None:
""" """
Allocates storage for the image and loads the pixel data. In Allocates storage for the image and loads the pixel data. In
normal cases, you don't need to call this method, since the normal cases, you don't need to call this method, since the
@ -893,7 +883,7 @@ class Image:
operations. See :ref:`file-handling` for more information. operations. See :ref:`file-handling` for more information.
:returns: An image access object. :returns: An image access object.
:rtype: :py:class:`.PixelAccess` or :py:class:`.PyAccess` :rtype: :py:class:`.PixelAccess`
""" """
if self.im is not None and self.palette and self.palette.dirty: if self.im is not None and self.palette and self.palette.dirty:
# realize palette # realize palette
@ -913,14 +903,6 @@ class Image:
) )
if self.im is not None: 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 self.im.pixel_access(self.readonly)
return None return None
@ -1113,7 +1095,7 @@ class Image:
if trns is not None: if trns is not None:
try: try:
new_im.info["transparency"] = new_im.palette.getcolor( new_im.info["transparency"] = new_im.palette.getcolor(
cast(Tuple[int, int, int], trns), # trns was converted to RGB cast(tuple[int, ...], trns), # trns was converted to RGB
new_im, new_im,
) )
except Exception: except Exception:
@ -1163,12 +1145,9 @@ class Image:
# crash fail if we leave a bytes transparency in an rgb/l mode. # crash fail if we leave a bytes transparency in an rgb/l mode.
del new_im.info["transparency"] del new_im.info["transparency"]
if trns is not None: if trns is not None:
if new_im.mode == "P": if new_im.mode == "P" and new_im.palette:
try: try:
new_im.info["transparency"] = new_im.palette.getcolor( new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
cast(Tuple[int, int, int], trns), # trns was converted to RGB
new_im,
)
except ValueError as e: except ValueError as e:
del new_im.info["transparency"] del new_im.info["transparency"]
if str(e) != "cannot allocate more than 256 colors": if str(e) != "cannot allocate more than 256 colors":
@ -1186,7 +1165,7 @@ class Image:
colors: int = 256, colors: int = 256,
method: int | None = None, method: int | None = None,
kmeans: int = 0, kmeans: int = 0,
palette=None, palette: Image | None = None,
dither: Dither = Dither.FLOYDSTEINBERG, dither: Dither = Dither.FLOYDSTEINBERG,
) -> Image: ) -> Image:
""" """
@ -1258,8 +1237,8 @@ class Image:
from . import ImagePalette from . import ImagePalette
mode = im.im.getpalettemode() mode = im.im.getpalettemode()
palette = im.im.getpalette(mode, mode)[: colors * len(mode)] palette_data = im.im.getpalette(mode, mode)[: colors * len(mode)]
im.palette = ImagePalette.ImagePalette(mode, palette) im.palette = ImagePalette.ImagePalette(mode, palette_data)
return im return im
@ -1414,7 +1393,9 @@ class Image:
self.load() self.load()
return self.im.getbbox(alpha_only) return self.im.getbbox(alpha_only)
def getcolors(self, maxcolors: int = 256): def getcolors(
self, maxcolors: int = 256
) -> list[tuple[int, int]] | list[tuple[int, float]] | None:
""" """
Returns a list of colors used in this image. Returns a list of colors used in this image.
@ -1475,7 +1456,7 @@ class Image:
return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands)) return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands))
return self.im.getextrema() return self.im.getextrema()
def getxmp(self): def getxmp(self) -> dict[str, Any]:
""" """
Returns a dictionary containing the XMP tags. Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed. Requires defusedxml to be installed.
@ -1511,7 +1492,7 @@ class Image:
return {} return {}
if "xmp" not in self.info: if "xmp" not in self.info:
return {} return {}
root = ElementTree.fromstring(self.info["xmp"]) root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00"))
return {get_name(root.tag): get_value(root)} return {get_name(root.tag): get_value(root)}
def getexif(self) -> Exif: def getexif(self) -> Exif:
@ -1686,8 +1667,6 @@ class Image:
""" """
self.load() self.load()
if self.pyaccess:
return self.pyaccess.getpixel(xy)
return self.im.getpixel(tuple(xy)) return self.im.getpixel(tuple(xy))
def getprojection(self) -> tuple[list[int], list[int]]: def getprojection(self) -> tuple[list[int], list[int]]:
@ -1984,7 +1963,6 @@ class Image:
msg = "alpha channel could not be added" msg = "alpha channel could not be added"
raise ValueError(msg) from e # sanity check raise ValueError(msg) from e # sanity check
self.im = im self.im = im
self.pyaccess = None
self._mode = self.im.mode self._mode = self.im.mode
except KeyError as e: except KeyError as e:
msg = "illegal image mode" msg = "illegal image mode"
@ -2039,7 +2017,11 @@ class Image:
self.im.putdata(data, scale, offset) 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" Attaches a palette to this image. The image must be a "P", "PA", "L"
or "LA" image. or "LA" image.
@ -2102,9 +2084,6 @@ class Image:
self._copy() self._copy()
self.load() self.load()
if self.pyaccess:
return self.pyaccess.putpixel(xy, value)
if ( if (
self.mode in ("P", "PA") self.mode in ("P", "PA")
and isinstance(value, (list, tuple)) and isinstance(value, (list, tuple))
@ -2118,7 +2097,9 @@ class Image:
value = (palette_index, alpha) if self.mode == "PA" else palette_index value = (palette_index, alpha) if self.mode == "PA" else palette_index
return self.im.putpixel(xy, value) 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. Rewrites the image to reorder the palette.
@ -2786,7 +2767,6 @@ class Image:
self._mode = self.im.mode self._mode = self.im.mode
self.readonly = 0 self.readonly = 0
self.pyaccess = None
# FIXME: the different transform methods need further explanation # FIXME: the different transform methods need further explanation
# instead of bloating the method docs, add a separate chapter. # instead of bloating the method docs, add a separate chapter.
@ -3110,7 +3090,7 @@ def new(
and isinstance(color, (list, tuple)) and isinstance(color, (list, tuple))
and all(isinstance(i, int) for i in color) 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: if len(color_ints) == 3 or len(color_ints) == 4:
# RGB or RGBA value for a P image # RGB or RGBA value for a P image
from . import ImagePalette from . import ImagePalette
@ -3461,7 +3441,7 @@ def open(
preinit() preinit()
accept_warnings: list[str] = [] warning_messages: list[str] = []
def _open_core( def _open_core(
fp: IO[bytes], fp: IO[bytes],
@ -3477,17 +3457,15 @@ def open(
factory, accept = OPEN[i] factory, accept = OPEN[i]
result = not accept or accept(prefix) result = not accept or accept(prefix)
if isinstance(result, str): if isinstance(result, str):
accept_warnings.append(result) warning_messages.append(result)
elif result: elif result:
fp.seek(0) fp.seek(0)
im = factory(fp, filename) im = factory(fp, filename)
_decompression_bomb_check(im.size) _decompression_bomb_check(im.size)
return im return im
except (SyntaxError, IndexError, TypeError, struct.error): except (SyntaxError, IndexError, TypeError, struct.error) as e:
# Leave disabled by default, spams the logs with image if WARN_POSSIBLE_FORMATS:
# opening failures that are entirely expected. warning_messages.append(i + " opening failed. " + str(e))
# logger.debug("", exc_info=True)
continue
except BaseException: except BaseException:
if exclusive_fp: if exclusive_fp:
fp.close() fp.close()
@ -3512,7 +3490,7 @@ def open(
if exclusive_fp: if exclusive_fp:
fp.close() fp.close()
for message in accept_warnings: for message in warning_messages:
warnings.warn(message) warnings.warn(message)
msg = "cannot identify image file %r" % (filename if filename else fp) msg = "cannot identify image file %r" % (filename if filename else fp)
raise UnidentifiedImageError(msg) raise UnidentifiedImageError(msg)
@ -3577,7 +3555,7 @@ def composite(image1: Image, image2: Image, mask: Image) -> Image:
return 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 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 in the given image. If the image has more than one band, the same

View File

@ -299,6 +299,31 @@ class ImageCmsTransform(Image.ImagePointHandler):
proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC, proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC,
flags: Flags = Flags.NONE, 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: if proof is None:
self.transform = core.buildTransform( self.transform = core.buildTransform(
input.profile, output.profile, input_mode, output_mode, intent, flags input.profile, output.profile, input_mode, output_mode, intent, flags

View File

@ -34,8 +34,9 @@ from __future__ import annotations
import math import math
import numbers import numbers
import struct import struct
from collections.abc import Sequence
from types import ModuleType from types import ModuleType
from typing import TYPE_CHECKING, AnyStr, Callable, List, Sequence, Tuple, Union, cast from typing import TYPE_CHECKING, AnyStr, Callable, Union, cast
from . import Image, ImageColor from . import Image, ImageColor
from ._deprecate import deprecate from ._deprecate import deprecate
@ -51,7 +52,7 @@ except AttributeError:
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ImageDraw2, ImageFont from . import ImageDraw2, ImageFont
_Ink = Union[float, Tuple[int, ...], str] _Ink = Union[float, tuple[int, ...], str]
""" """
A simple 2D drawing interface for PIL images. A simple 2D drawing interface for PIL images.
@ -1124,7 +1125,7 @@ def _compute_regular_polygon_vertices(
msg = "bounding_circle should only contain numeric data" msg = "bounding_circle should only contain numeric data"
raise ValueError(msg) raise ValueError(msg)
*centroid, polygon_radius = cast(List[float], list(bounding_circle)) *centroid, polygon_radius = cast(list[float], list(bounding_circle))
elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)): elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)):
if not all( if not all(
isinstance(i, (int, float)) for i in bounding_circle[0] isinstance(i, (int, float)) for i in bounding_circle[0]
@ -1136,7 +1137,7 @@ def _compute_regular_polygon_vertices(
msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
raise ValueError(msg) raise ValueError(msg)
centroid = cast(List[float], list(bounding_circle[0])) centroid = cast(list[float], list(bounding_circle[0]))
polygon_radius = cast(float, bounding_circle[1]) polygon_radius = cast(float, bounding_circle[1])
else: else:
msg = ( msg = (

View File

@ -18,8 +18,9 @@ from __future__ import annotations
import abc import abc
import functools import functools
from collections.abc import Sequence
from types import ModuleType from types import ModuleType
from typing import TYPE_CHECKING, Any, Callable, Sequence, cast from typing import TYPE_CHECKING, Any, Callable, cast
if TYPE_CHECKING: if TYPE_CHECKING:
from . import _imaging from . import _imaging

View File

@ -906,6 +906,142 @@ def load_path(filename: str | bytes) -> ImageFont:
raise OSError(msg) 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: def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
"""If FreeType support is available, load a version of Aileron Regular, """If FreeType support is available, load a version of Aileron Regular,
https://dotcolon.net/font/aileron, with a more limited character set. https://dotcolon.net/font/aileron, with a more limited character set.
@ -920,9 +1056,8 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
:return: A font object. :return: A font object.
""" """
f: FreeTypeFont | ImageFont
if isinstance(core, ModuleType) or size is not None: if isinstance(core, ModuleType) or size is not None:
f = truetype( return truetype(
BytesIO( BytesIO(
base64.b64decode( base64.b64decode(
b""" b"""
@ -1152,137 +1287,4 @@ AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ==
10 if size is None else size, 10 if size is None else size,
layout_engine=Layout.BASIC, layout_engine=Layout.BASIC,
) )
else: return load_default_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

View File

@ -21,7 +21,8 @@ from __future__ import annotations
import functools import functools
import operator import operator
import re import re
from typing import Protocol, Sequence, cast from collections.abc import Sequence
from typing import Protocol, cast
from . import ExifTags, Image, ImagePalette from . import ExifTags, Image, ImagePalette
@ -361,7 +362,9 @@ def pad(
else: else:
out = Image.new(image.mode, size, color) out = Image.new(image.mode, size, color)
if resized.palette: if resized.palette:
out.putpalette(resized.getpalette()) palette = resized.getpalette()
if palette is not None:
out.putpalette(palette)
if resized.width != size[0]: if resized.width != size[0]:
x = round((size[0] - resized.width) * max(0, min(centering[0], 1))) x = round((size[0] - resized.width) * max(0, min(centering[0], 1)))
out.paste(resized, (x, 0)) 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) transposed_image = image.transpose(method)
if in_place: if in_place:
image.im = transposed_image.im image.im = transposed_image.im
image.pyaccess = None
image._size = transposed_image._size image._size = transposed_image._size
exif_image = image if in_place else transposed_image exif_image = image if in_place else transposed_image
@ -709,17 +711,18 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
exif_image.info["exif"] = exif.tobytes() exif_image.info["exif"] = exif.tobytes()
elif "Raw profile type exif" in exif_image.info: elif "Raw profile type exif" in exif_image.info:
exif_image.info["Raw profile type exif"] = exif.tobytes().hex() exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
elif "XML:com.adobe.xmp" in exif_image.info: for key in ("XML:com.adobe.xmp", "xmp"):
for pattern in ( if key in exif_image.info:
r'tiff:Orientation="([0-9])"', for pattern in (
r"<tiff:Orientation>([0-9])</tiff:Orientation>", r'tiff:Orientation="([0-9])"',
): r"<tiff:Orientation>([0-9])</tiff:Orientation>",
exif_image.info["XML:com.adobe.xmp"] = re.sub( ):
pattern, "", exif_image.info["XML:com.adobe.xmp"] value = exif_image.info[key]
) exif_image.info[key] = (
exif_image.info["xmp"] = re.sub( re.sub(pattern, "", value)
pattern.encode(), b"", exif_image.info["xmp"] if isinstance(value, str)
) else re.sub(pattern.encode(), b"", value)
)
if not in_place: if not in_place:
return transposed_image return transposed_image
elif not in_place: elif not in_place:

View File

@ -18,7 +18,8 @@
from __future__ import annotations from __future__ import annotations
import array 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 from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile

View File

@ -118,6 +118,8 @@ class Viewer:
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
os.system(self.get_command(path, **options)) # nosec os.system(self.get_command(path, **options)) # nosec
return 1 return 1
@ -142,6 +144,8 @@ class WindowsViewer(Viewer):
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
subprocess.Popen( subprocess.Popen(
self.get_command(path, **options), self.get_command(path, **options),
shell=True, shell=True,
@ -171,6 +175,8 @@ class MacViewer(Viewer):
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
subprocess.call(["open", "-a", "Preview.app", path]) subprocess.call(["open", "-a", "Preview.app", path])
executable = sys.executable or shutil.which("python3") executable = sys.executable or shutil.which("python3")
if executable: if executable:
@ -215,6 +221,8 @@ class XDGViewer(UnixViewer):
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
subprocess.Popen(["xdg-open", path]) subprocess.Popen(["xdg-open", path])
return 1 return 1
@ -237,6 +245,8 @@ class DisplayViewer(UnixViewer):
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
args = ["display"] args = ["display"]
title = options.get("title") title = options.get("title")
if title: if title:
@ -259,6 +269,8 @@ class GmDisplayViewer(UnixViewer):
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
subprocess.Popen(["gm", "display", path]) subprocess.Popen(["gm", "display", path])
return 1 return 1
@ -275,6 +287,8 @@ class EogViewer(UnixViewer):
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
subprocess.Popen(["eog", "-n", path]) subprocess.Popen(["eog", "-n", path])
return 1 return 1
@ -299,6 +313,8 @@ class XVViewer(UnixViewer):
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
args = ["xv"] args = ["xv"]
title = options.get("title") title = options.get("title")
if title: if title:

View File

@ -28,8 +28,9 @@ from __future__ import annotations
import tkinter import tkinter
from io import BytesIO from io import BytesIO
from typing import Any
from . import Image from . import Image, ImageFile
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Check for Tkinter interface hooks # Check for Tkinter interface hooks
@ -49,14 +50,15 @@ def _pilbitmap_check() -> int:
return _pilbitmap_ok return _pilbitmap_ok
def _get_image_from_kw(kw): def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
source = None source = None
if "file" in kw: if "file" in kw:
source = kw.pop("file") source = kw.pop("file")
elif "data" in kw: elif "data" in kw:
source = BytesIO(kw.pop("data")) source = BytesIO(kw.pop("data"))
if source: if not source:
return Image.open(source) return None
return Image.open(source)
def _pyimagingtkcall(command, photo, id): def _pyimagingtkcall(command, photo, id):
@ -96,12 +98,27 @@ class PhotoImage:
image file). 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 # Tk compatibility: file or data
if image is None: if image is None:
image = _get_image_from_kw(kw) 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 # got an image instead of a mode
mode = image.mode mode = image.mode
if mode == "P": if mode == "P":
@ -114,9 +131,6 @@ class PhotoImage:
mode = "RGB" # default mode = "RGB" # default
size = image.size size = image.size
kw["width"], kw["height"] = size kw["width"], kw["height"] = size
else:
mode = image
image = None
if mode not in ["1", "L", "RGB", "RGBA"]: if mode not in ["1", "L", "RGB", "RGBA"]:
mode = Image.getmodebase(mode) mode = Image.getmodebase(mode)

View File

@ -14,7 +14,8 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import Any, Sequence from collections.abc import Sequence
from typing import Any
from . import Image from . import Image

View File

@ -16,8 +16,8 @@
# #
from __future__ import annotations from __future__ import annotations
from collections.abc import Sequence
from io import BytesIO from io import BytesIO
from typing import Sequence
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i16be as i16 from ._binary import i16be as i16

View File

@ -18,7 +18,7 @@ from __future__ import annotations
import io import io
import os import os
import struct import struct
from typing import IO, Tuple, cast from typing import IO, cast
from . import Image, ImageFile, ImagePalette, _binary from . import Image, ImageFile, ImagePalette, _binary
@ -82,7 +82,7 @@ class BoxReader:
self.remaining_in_box = -1 self.remaining_in_box = -1
# Read the length and type of the next box # 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: if lbox == 1:
lbox = cast(int, self.read_fields(">Q")[0]) lbox = cast(int, self.read_fields(">Q")[0])
hlen = 16 hlen = 16

View File

@ -96,7 +96,7 @@ def APP(self, marker):
self.info["exif"] = s self.info["exif"] = s
self._exif_offset = self.fp.tell() - n + 6 self._exif_offset = self.fp.tell() - n + 6
elif marker == 0xFFE1 and s[:29] == b"http://ns.adobe.com/xap/1.0/\x00": elif marker == 0xFFE1 and s[:29] == b"http://ns.adobe.com/xap/1.0/\x00":
self.info["xmp"] = s.split(b"\x00")[1] self.info["xmp"] = s.split(b"\x00", 1)[1]
elif marker == 0xFFE2 and s[:5] == b"FPXR\0": elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete) # extract FlashPix information (incomplete)
self.info["flashpix"] = s # FIXME: value will change self.info["flashpix"] = s # FIXME: value will change
@ -161,38 +161,6 @@ def APP(self, marker):
# plus constant header size # plus constant header size
self.info["mpoffset"] = self.fp.tell() - n + 4 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: def COM(self: JpegImageFile, marker: int) -> None:
# #
@ -411,6 +379,8 @@ class JpegImageFile(ImageFile.ImageFile):
msg = "no marker found" msg = "no marker found"
raise SyntaxError(msg) raise SyntaxError(msg)
self._read_dpi_from_exif()
def load_read(self, read_bytes: int) -> bytes: def load_read(self, read_bytes: int) -> bytes:
""" """
internal: read more image data internal: read more image data
@ -499,6 +469,35 @@ class JpegImageFile(ImageFile.ImageFile):
def _getexif(self) -> dict[str, Any] | None: def _getexif(self) -> dict[str, Any] | None:
return _getexif(self) 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): def _getmp(self):
return _getmp(self) return _getmp(self)

View File

@ -70,7 +70,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self.__fp = self.fp self.__fp = self.fp
self.seek(0) self.seek(0)
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
try: try:

View File

@ -8,7 +8,7 @@ import os
import re import re
import time import time
import zlib import zlib
from typing import TYPE_CHECKING, Any, List, NamedTuple, Union from typing import TYPE_CHECKING, Any, NamedTuple, Union
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
@ -240,7 +240,7 @@ class PdfName:
return bytes(result) return bytes(result)
class PdfArray(List[Any]): class PdfArray(list[Any]):
def __bytes__(self) -> bytes: def __bytes__(self) -> bytes:
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"

View File

@ -851,8 +851,6 @@ class PngImageFile(ImageFile.ImageFile):
self.png.rewind() self.png.rewind()
self.__prepare_idat = self.__rewind_idat self.__prepare_idat = self.__rewind_idat
self.im = None self.im = None
if self.pyaccess:
self.pyaccess = None
self.info = self.png.im_info self.info = self.png.im_info
self.tile = self.png.im_tile self.tile = self.png.im_tile
self.fp = self._fp self.fp = self._fp
@ -1039,8 +1037,6 @@ class PngImageFile(ImageFile.ImageFile):
mask = updated.convert("RGBA") mask = updated.convert("RGBA")
self._prev_im.paste(updated, self.dispose_extent, mask) self._prev_im.paste(updated, self.dispose_extent, mask)
self.im = self._prev_im self.im = self._prev_im
if self.pyaccess:
self.pyaccess = None
def _getexif(self) -> dict[str, Any] | None: def _getexif(self) -> dict[str, Any] | None:
if "exif" not in self.info: if "exif" not in self.info:

View File

@ -1,381 +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: tuple[int, int] | list[int],
color: float | tuple[int, ...] | list[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.
: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]
palette_index = self._palette.getcolor(color, self._img)
color = (palette_index, alpha) if self._im.mode == "PA" else palette_index
return self.set_pixel(x, y, color)
def __getitem__(self, xy: tuple[int, int] | list[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, ...] | list[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)

View File

@ -37,7 +37,7 @@ from __future__ import annotations
import os import os
import struct import struct
import sys import sys
from typing import IO, TYPE_CHECKING, Any, Tuple, cast from typing import IO, TYPE_CHECKING, Any, cast
from . import Image, ImageFile from . import Image, ImageFile
@ -187,7 +187,7 @@ class SpiderImageFile(ImageFile.ImageFile):
def convert2byte(self, depth: int = 255) -> Image.Image: def convert2byte(self, depth: int = 255) -> Image.Image:
extrema = self.getextrema() extrema = self.getextrema()
assert isinstance(extrema[0], float) assert isinstance(extrema[0], float)
minimum, maximum = cast(Tuple[float, float], extrema) minimum, maximum = cast(tuple[float, float], extrema)
m: float = 1 m: float = 1
if maximum != minimum: if maximum != minimum:
m = depth / (maximum - minimum) m = depth / (maximum - minimum)

View File

@ -36,7 +36,7 @@ MODES = {
(3, 1): "1", (3, 1): "1",
(3, 8): "L", (3, 8): "L",
(3, 16): "LA", (3, 16): "LA",
(2, 16): "BGR;5", (2, 16): "BGRA;15Z",
(2, 24): "BGR", (2, 24): "BGR",
(2, 32): "BGRA", (2, 32): "BGRA",
} }
@ -87,9 +87,7 @@ class TgaImageFile(ImageFile.ImageFile):
elif imagetype in (1, 9): elif imagetype in (1, 9):
self._mode = "P" if colormaptype else "L" self._mode = "P" if colormaptype else "L"
elif imagetype in (2, 10): elif imagetype in (2, 10):
self._mode = "RGB" self._mode = "RGB" if depth == 24 else "RGBA"
if depth == 32:
self._mode = "RGBA"
else: else:
msg = "unknown TGA mode" msg = "unknown TGA mode"
raise SyntaxError(msg) raise SyntaxError(msg)
@ -118,15 +116,16 @@ class TgaImageFile(ImageFile.ImageFile):
start, size, mapdepth = i16(s, 3), i16(s, 5), s[7] start, size, mapdepth = i16(s, 3), i16(s, 5), s[7]
if mapdepth == 16: if mapdepth == 16:
self.palette = ImagePalette.raw( 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: elif mapdepth == 24:
self.palette = ImagePalette.raw( 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: elif mapdepth == 32:
self.palette = ImagePalette.raw( self.palette = ImagePalette.raw(
"BGRA", b"\0" * 4 * start + self.fp.read(4 * size) "BGRA", bytes(4 * start) + self.fp.read(4 * size)
) )
else: else:
msg = "unknown TGA map depth" msg = "unknown TGA map depth"

View File

@ -1663,10 +1663,14 @@ def _save(im, fp, filename):
legacy_ifd = im.tag.to_v2() legacy_ifd = im.tag.to_v2()
supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})} supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})}
if SAMPLEFORMAT in supplied_tags: for tag in (
# SAMPLEFORMAT is determined by the image format and should not be copied # IFD offset that may not be correct in the saved image
# from legacy_ifd. EXIFIFD,
del supplied_tags[SAMPLEFORMAT] # 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 # additions written by Greg Couch, gregc@cgl.ucsf.edu
# inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com

View File

@ -45,8 +45,6 @@ def deprecate(
elif when <= int(__version__.split(".")[0]): elif when <= int(__version__.split(".")[0]):
msg = f"{deprecated} {is_} deprecated and should be removed." msg = f"{deprecated} {is_} deprecated and should be removed."
raise RuntimeError(msg) raise RuntimeError(msg)
elif when == 11:
removed = "Pillow 11 (2024-10-15)"
elif when == 12: elif when == 12:
removed = "Pillow 12 (2025-10-15)" removed = "Pillow 12 (2025-10-15)"
else: else:

View File

@ -2,14 +2,16 @@ from __future__ import annotations
import os import os
import sys import sys
from typing import Any, Protocol, Sequence, TypeVar, Union from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union
try: if TYPE_CHECKING:
import numpy.typing as npt try:
import numpy.typing as npt
NumpyArray = npt.NDArray[Any] NumpyArray = npt.NDArray[Any] # requires numpy>=1.21
except ImportError: except (ImportError, AttributeError):
pass pass
if sys.version_info >= (3, 10): if sys.version_info >= (3, 10):
from typing import TypeGuard from typing import TypeGuard

View File

@ -1,4 +1,4 @@
# Master version for Pillow # Master version for Pillow
from __future__ import annotations from __future__ import annotations
__version__ = "10.4.0.dev0" __version__ = "11.0.0.dev0"

View File

@ -223,20 +223,22 @@ findLCMStype(char *PILmode) {
if (strcmp(PILmode, "CMYK") == 0) { if (strcmp(PILmode, "CMYK") == 0) {
return TYPE_CMYK_8; 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; 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; 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; return TYPE_YCbCr_8;
} }
if (strcmp(PILmode, "LAB") == 0) { if (strcmp(PILmode, "LAB") == 0) {
// LabX equivalent like ALab, but not reversed -- no #define in lcms2 // 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)); 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; return TYPE_GRAY_8;
} }

View File

@ -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 void
ImagingUnpackRGB16(UINT8 *out, const UINT8 *in, int pixels) { ImagingUnpackRGB16(UINT8 *out, const UINT8 *in, int pixels) {
int i, pixel; int i, pixel;
@ -1538,7 +1553,7 @@ static struct {
/* flags: "I" inverted data; "R" reversed bit order; "B" big /* flags: "I" inverted data; "R" reversed bit order; "B" big
endian byte order (default is little endian); "L" line 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 */ /* exception: rawmodes "I" and "F" are always native endian byte order */
@ -1646,6 +1661,7 @@ static struct {
{"RGBA", "RGBA;L", 32, unpackRGBAL}, {"RGBA", "RGBA;L", 32, unpackRGBAL},
{"RGBA", "RGBA;15", 16, ImagingUnpackRGBA15}, {"RGBA", "RGBA;15", 16, ImagingUnpackRGBA15},
{"RGBA", "BGRA;15", 16, ImagingUnpackBGRA15}, {"RGBA", "BGRA;15", 16, ImagingUnpackBGRA15},
{"RGBA", "BGRA;15Z", 16, ImagingUnpackBGRA15Z},
{"RGBA", "RGBA;4B", 16, ImagingUnpackRGBA4B}, {"RGBA", "RGBA;4B", 16, ImagingUnpackRGBA4B},
{"RGBA", "RGBA;16L", 64, unpackRGBA16L}, {"RGBA", "RGBA;16L", 64, unpackRGBA16L},
{"RGBA", "RGBA;16B", 64, unpackRGBA16B}, {"RGBA", "RGBA;16B", 64, unpackRGBA16B},

View File

@ -3,11 +3,10 @@ requires =
tox>=4.2 tox>=4.2
env_list = env_list =
lint lint
py{py3, 312, 311, 310, 39, 38} py{py3, 313, 312, 311, 310, 39}
[testenv] [testenv]
deps = deps =
cffi
numpy numpy
extras = extras =
tests tests
@ -39,7 +38,6 @@ deps =
ipython ipython
numpy numpy
packaging packaging
types-cffi
types-defusedxml types-defusedxml
types-olefile types-olefile
extras = extras =

View File

@ -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: 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 cd /D C:\Pillow\winbuild
%PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends %PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends
build\build_dep_all.cmd build\build_dep_all.cmd

View File

@ -114,7 +114,7 @@ Example
The following is a simplified version of the script used on AppVeyor:: 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 cd /D C:\Pillow\winbuild
%PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends %PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends
build\build_dep_all.cmd build\build_dep_all.cmd