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

View File

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

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]
omit =
Tests/32bit_segfault_check.py
Tests/bench_cffi_access.py
Tests/check_*.py
Tests/createfontdatachunk.py

View File

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

View File

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

View File

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

View File

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

View File

@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["pypy3.10", "pypy3.9", "3.9", "3.10", "3.11", "3.12", "3.13"]
timeout-minutes: 30

View File

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

View File

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

View File

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

View File

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

View File

@ -2,9 +2,39 @@
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
[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 sysconfig
import tempfile
from collections.abc import Sequence
from functools import lru_cache
from io import BytesIO
from typing import Any, Callable, Sequence
from typing import Any, Callable
import pytest
from packaging.version import parse as parse_version

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,
])).load()
# fmt: on
assert transformed is not None
assert transformed[0, 0] == (0, 0, 255)
assert transformed[50, 50] == (0, 0, 255)
assert transformed[255, 0] == (0, 255, 255)
@ -341,6 +342,7 @@ class TestColorLut3DCoreAPI:
-3, 5, 5, 5, 5, 5,
])).load()
# fmt: on
assert transformed is not None
assert transformed[0, 0] == (0, 0, 255)
assert transformed[50, 50] == (0, 0, 255)
assert transformed[255, 0] == (0, 255, 255)

View File

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

View File

@ -329,46 +329,6 @@ def test_read_binary_preview() -> None:
pass
def test_readline_psfile(tmp_path: Path) -> None:
# check all the freaking line endings possible from the spec
# test_string = u'something\r\nelse\n\rbaz\rbif\n'
line_endings = ["\r\n", "\n", "\n\r", "\r"]
strings = ["something", "else", "baz", "bif"]
def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
ending = f"Failure with line ending: {''.join(str(ord(s)) for s in ending)}"
assert t.readline().strip("\r\n") == "something", ending
assert t.readline().strip("\r\n") == "else", ending
assert t.readline().strip("\r\n") == "baz", ending
assert t.readline().strip("\r\n") == "bif", ending
def _test_readline_io_psfile(test_string: str, ending: str) -> None:
f = io.BytesIO(test_string.encode("latin-1"))
with pytest.warns(DeprecationWarning):
t = EpsImagePlugin.PSFile(f)
_test_readline(t, ending)
def _test_readline_file_psfile(test_string: str, ending: str) -> None:
f = str(tmp_path / "temp.txt")
with open(f, "wb") as w:
w.write(test_string.encode("latin-1"))
with open(f, "rb") as r:
with pytest.warns(DeprecationWarning):
t = EpsImagePlugin.PSFile(r)
_test_readline(t, ending)
for ending in line_endings:
s = ending.join(strings)
_test_readline_io_psfile(s, ending)
_test_readline_file_psfile(s, ending)
def test_psfile_deprecation() -> None:
with pytest.warns(DeprecationWarning):
EpsImagePlugin.PSFile(None)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
@pytest.mark.parametrize(
"line_ending",
@ -425,9 +385,10 @@ def test_timeout(test_file: str) -> None:
def test_bounding_box_in_trailer() -> None:
# Check bounding boxes are parsed in the same way
# when specified in the header and the trailer
with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open(
FILE1
) as header_image:
with (
Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image,
Image.open(FILE1) as header_image,
):
assert trailer_image.size == header_image.size

View File

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

View File

@ -872,7 +872,7 @@ class TestFileJpeg:
def test_multiple_exif(self) -> None:
with Image.open("Tests/images/multiple_exif.jpg") as im:
assert im.info["exif"] == b"Exif\x00\x00firstsecond"
assert im.getexif()[270] == "firstsecond"
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"

View File

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

View File

@ -76,6 +76,7 @@ def test_pil184() -> None:
def test_1px_width(tmp_path: Path) -> None:
im = Image.new("L", (1, 256))
px = im.load()
assert px is not None
for y in range(256):
px[0, y] = y
_roundtrip(tmp_path, im)
@ -84,6 +85,7 @@ def test_1px_width(tmp_path: Path) -> None:
def test_large_count(tmp_path: Path) -> None:
im = Image.new("L", (256, 1))
px = im.load()
assert px is not None
for x in range(256):
px[x, 0] = x // 67 * 67
_roundtrip(tmp_path, im)
@ -101,6 +103,7 @@ def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) ->
def test_break_in_count_overflow(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(4):
for x in range(256):
px[x, y] = x % 128
@ -110,6 +113,7 @@ def test_break_in_count_overflow(tmp_path: Path) -> None:
def test_break_one_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@ -119,6 +123,7 @@ def test_break_one_in_loop(tmp_path: Path) -> None:
def test_break_many_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(4):
for x in range(256):
px[x, y] = x % 128
@ -130,6 +135,7 @@ def test_break_many_in_loop(tmp_path: Path) -> None:
def test_break_one_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@ -140,6 +146,7 @@ def test_break_one_at_end(tmp_path: Path) -> None:
def test_break_many_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@ -152,6 +159,7 @@ def test_break_many_at_end(tmp_path: Path) -> None:
def test_break_padding(tmp_path: Path) -> None:
im = Image.new("L", (257, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(257):
px[x, y] = x % 128

View File

@ -4,9 +4,10 @@ import os
import os.path
import tempfile
import time
from collections.abc import Generator
from io import BytesIO
from pathlib import Path
from typing import Any, Generator
from typing import Any
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:
with Image.open("Tests/images/p_16.tga") as im:
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png")
assert im.palette.mode == "RGBA"
assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png")
out = str(tmp_path / "temp.png")
im.save(out)
with Image.open(out) as reloaded:
assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png")
assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png")
def test_rgba_16() -> None:
with Image.open("Tests/images/rgba16.tga") as im:
assert im.mode == "RGBA"
assert im.getpixel((0, 0)) == (172, 0, 255, 255)
assert im.getpixel((1, 0)) == (0, 255, 82, 0)
def test_id_field() -> None:

View File

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

View File

@ -8,6 +8,7 @@ import sys
import tempfile
import warnings
from pathlib import Path
from types import ModuleType
from typing import IO, Any
import pytest
@ -35,6 +36,12 @@ from .helper import (
skip_unless_feature,
)
ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
ElementTree = None
# Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
@ -125,6 +132,15 @@ class TestImage:
assert im.mode == "RGB"
assert im.size == (128, 128)
def test_open_verbose_failure(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(Image, "WARN_POSSIBLE_FORMATS", True)
im = io.BytesIO(b"")
with pytest.warns(UserWarning):
with pytest.raises(UnidentifiedImageError):
with Image.open(im):
pass
def test_width_height(self) -> None:
im = Image.new("RGB", (1, 2))
assert im.width == 1
@ -559,6 +575,7 @@ class TestImage:
for mode in ("I", "F", "L"):
im = Image.new(mode, (100, 100), (5,))
px = im.load()
assert px is not None
assert px[0, 0] == 5
def test_linear_gradient_wrong_mode(self) -> None:
@ -921,6 +938,21 @@ class TestImage:
with Image.open("Tests/images/hopper.gif") as im:
assert im.getxmp() == {}
def test_getxmp_padded(self) -> None:
im = Image.new("RGB", (1, 1))
im.info["xmp"] = (
b'<?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)))
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
im = Image.new("RGB", size)

View File

@ -12,19 +12,6 @@ from PIL import Image
from .helper import assert_image_equal, hopper, is_win32
# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2
# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670
cffi: ModuleType | None
if os.environ.get("PYTHONOPTIMIZE") == "2":
cffi = None
else:
try:
import cffi
from PIL import PyAccess
except ImportError:
cffi = None
numpy: ModuleType | None
try:
import numpy
@ -32,21 +19,7 @@ except ImportError:
numpy = None
class AccessTest:
# Initial value
_init_cffi_access = Image.USE_CFFI_ACCESS
_need_cffi_access = False
@classmethod
def setup_class(cls) -> None:
Image.USE_CFFI_ACCESS = cls._need_cffi_access
@classmethod
def teardown_class(cls) -> None:
Image.USE_CFFI_ACCESS = cls._init_cffi_access
class TestImagePutPixel(AccessTest):
class TestImagePutPixel:
def test_sanity(self) -> None:
im1 = hopper()
im2 = Image.new(im1.mode, im1.size, 0)
@ -74,6 +47,8 @@ class TestImagePutPixel(AccessTest):
pix1 = im1.load()
pix2 = im2.load()
assert pix1 is not None
assert pix2 is not None
with pytest.raises(TypeError):
pix1[0, "0"]
with pytest.raises(TypeError):
@ -116,6 +91,8 @@ class TestImagePutPixel(AccessTest):
pix1 = im1.load()
pix2 = im2.load()
assert pix1 is not None
assert pix2 is not None
for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1):
pix2[x, y] = pix1[x, y]
@ -125,13 +102,14 @@ class TestImagePutPixel(AccessTest):
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
def test_numpy(self) -> None:
im = hopper()
pix = im.load()
px = im.load()
assert px is not None
assert numpy is not None
assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
assert px[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
class TestImageGetPixel(AccessTest):
class TestImageGetPixel:
@staticmethod
def color(mode: str) -> int | tuple[int, ...]:
bands = Image.getmodebands(mode)
@ -144,9 +122,6 @@ class TestImageGetPixel(AccessTest):
return tuple(range(1, bands + 1))
def check(self, mode: str, expected_color_int: int | None = None) -> None:
if self._need_cffi_access and mode.startswith("BGR;"):
pytest.skip("Support not added to deprecated module for BGR;* modes")
expected_color = (
self.color(mode) if expected_color_int is None else expected_color_int
)
@ -171,15 +146,14 @@ class TestImageGetPixel(AccessTest):
# Check 0x0 image with None initial color
im = Image.new(mode, (0, 0), None)
assert im.load() is not None
error = ValueError if self._need_cffi_access else IndexError
with pytest.raises(error):
with pytest.raises(IndexError):
im.putpixel((0, 0), expected_color)
with pytest.raises(error):
with pytest.raises(IndexError):
im.getpixel((0, 0))
# Check negative index
with pytest.raises(error):
with pytest.raises(IndexError):
im.putpixel((-1, -1), expected_color)
with pytest.raises(error):
with pytest.raises(IndexError):
im.getpixel((-1, -1))
# Check initial color
@ -199,10 +173,10 @@ class TestImageGetPixel(AccessTest):
# Check 0x0 image with initial color
im = Image.new(mode, (0, 0), expected_color)
with pytest.raises(error):
with pytest.raises(IndexError):
im.getpixel((0, 0))
# Check negative index
with pytest.raises(error):
with pytest.raises(IndexError):
im.getpixel((-1, -1))
@pytest.mark.parametrize("mode", Image.MODES)
@ -235,126 +209,7 @@ class TestImageGetPixel(AccessTest):
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
@pytest.mark.skipif(cffi is None, reason="No CFFI")
class TestCffiPutPixel(TestImagePutPixel):
_need_cffi_access = True
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
@pytest.mark.skipif(cffi is None, reason="No CFFI")
class TestCffiGetPixel(TestImageGetPixel):
_need_cffi_access = True
@pytest.mark.skipif(cffi is None, reason="No CFFI")
class TestCffi(AccessTest):
_need_cffi_access = True
def _test_get_access(self, im: Image.Image) -> None:
"""Do we get the same thing as the old pixel access
Using private interfaces, forcing a capi access and
a pyaccess for the same image"""
caccess = im.im.pixel_access(False)
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
assert access is not None
w, h = im.size
for x in range(0, w, 10):
for y in range(0, h, 10):
assert access[(x, y)] == caccess[(x, y)]
# Access an out-of-range pixel
with pytest.raises(ValueError):
access[(access.xsize + 1, access.ysize + 1)]
def test_get_vs_c(self) -> None:
with pytest.warns(DeprecationWarning):
rgb = hopper("RGB")
rgb.load()
self._test_get_access(rgb)
for mode in ("RGBA", "L", "LA", "1", "P", "F"):
self._test_get_access(hopper(mode))
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
im = Image.new(mode, (10, 10), 40000)
self._test_get_access(im)
def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None:
"""Are we writing the correct bits into the image?
Using private interfaces, forcing a capi access and
a pyaccess for the same image"""
caccess = im.im.pixel_access(False)
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
assert access is not None
w, h = im.size
for x in range(0, w, 10):
for y in range(0, h, 10):
access[(x, y)] = color
assert color == caccess[(x, y)]
# Attempt to set the value on a read-only image
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, True)
assert access is not None
with pytest.raises(ValueError):
access[(0, 0)] = color
def test_set_vs_c(self) -> None:
rgb = hopper("RGB")
with pytest.warns(DeprecationWarning):
rgb.load()
self._test_set_access(rgb, (255, 128, 0))
self._test_set_access(hopper("RGBA"), (255, 192, 128, 0))
self._test_set_access(hopper("L"), 128)
self._test_set_access(hopper("LA"), (128, 128))
self._test_set_access(hopper("1"), 255)
self._test_set_access(hopper("P"), 128)
self._test_set_access(hopper("PA"), (128, 128))
self._test_set_access(hopper("F"), 1024.0)
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
im = Image.new(mode, (10, 10), 40000)
self._test_set_access(im, 45000)
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_not_implemented(self) -> None:
assert PyAccess.new(hopper("BGR;15")) is None
# Ref https://github.com/python-pillow/Pillow/pull/2009
def test_reference_counting(self) -> None:
size = 10
for _ in range(10):
# Do not save references to the image, only to the access object
with pytest.warns(DeprecationWarning):
px = Image.new("L", (size, 1), 0).load()
for i in range(size):
# Pixels can contain garbage if image is released
assert px[i, 0] == 0
@pytest.mark.parametrize("mode", ("P", "PA"))
def test_p_putpixel_rgb_rgba(self, mode: str) -> None:
for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
im = Image.new(mode, (1, 1))
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
assert access is not None
access.putpixel((0, 0), color)
if len(color) == 3:
color += (255,)
assert im.convert("RGBA").getpixel((0, 0)) == color
class TestImagePutPixelError(AccessTest):
class TestImagePutPixelError:
IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"]
IMAGE_MODES2 = ["L", "I", "I;16"]
INVALID_TYPES = ["foo", 1.0, None]

View File

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

View File

@ -12,9 +12,10 @@ from .helper import hopper
def test_sanity() -> None:
im = hopper()
pix = im.load()
px = im.load()
assert pix[0, 0] == (20, 20, 70)
assert px is not None
assert px[0, 0] == (20, 20, 70)
def test_close() -> None:

View File

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

View File

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

View File

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

View File

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

View File

@ -679,7 +679,8 @@ def test_auxiliary_channels_isolated() -> None:
def test_long_modes() -> None:
p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc")
ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI")
with pytest.warns(DeprecationWarning):
ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI")
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
@ -700,3 +701,9 @@ def test_deprecation() -> None:
assert ImageCms.VERSION == "1.0.0 pil"
with pytest.warns(DeprecationWarning):
assert isinstance(ImageCms.FLAGS, dict)
profile = ImageCmsProfile(ImageCms.createProfile("sRGB"))
with pytest.warns(DeprecationWarning):
ImageCms.ImageCmsTransform(profile, profile, "RGBA;16B", "RGB")
with pytest.warns(DeprecationWarning):
ImageCms.ImageCmsTransform(profile, profile, "RGB", "RGBA;16B")

View File

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

View File

@ -9,51 +9,57 @@ from PIL import Image, ImageDraw, ImageFont, _util, features
from .helper import assert_image_equal_tofile
original_core = ImageFont.core
fonts = [ImageFont.load_default_imagefont()]
if not features.check_module("freetype2"):
default_font = ImageFont.load_default()
if isinstance(default_font, ImageFont.ImageFont):
fonts.append(default_font)
def setup_module() -> None:
if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
def teardown_module() -> None:
ImageFont.core = original_core
def test_default_font() -> None:
@pytest.mark.parametrize("font", fonts)
def test_default_font(font: ImageFont.ImageFont) -> None:
# Arrange
txt = 'This is a "better than nothing" default font.'
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
# Act
default_font = ImageFont.load_default()
draw.text((10, 10), txt, font=default_font)
draw.text((10, 10), txt, font=font)
# Assert
assert_image_equal_tofile(im, "Tests/images/default_font.png")
def test_size_without_freetype() -> None:
with pytest.raises(ImportError):
ImageFont.load_default(size=14)
def test_without_freetype() -> None:
original_core = ImageFont.core
if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
try:
with pytest.raises(ImportError):
ImageFont.truetype("Tests/fonts/FreeMono.ttf")
assert isinstance(ImageFont.load_default(), ImageFont.ImageFont)
with pytest.raises(ImportError):
ImageFont.load_default(size=14)
finally:
ImageFont.core = original_core
def test_unicode() -> None:
@pytest.mark.parametrize("font", fonts)
def test_unicode(font: ImageFont.ImageFont) -> None:
# should not segfault, should return UnicodeDecodeError
# issue #2826
font = ImageFont.load_default()
with pytest.raises(UnicodeEncodeError):
font.getbbox("")
def test_textbbox() -> None:
@pytest.mark.parametrize("font", fonts)
def test_textbbox(font: ImageFont.ImageFont) -> None:
im = Image.new("RGB", (200, 200))
d = ImageDraw.Draw(im)
default_font = ImageFont.load_default()
assert d.textlength("test", font=default_font) == 24
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
assert d.textlength("test", font=font) == 24
assert d.textbbox((0, 0), "test", font=font) == (0, 0, 24, 11)
def test_decompression_bomb() -> None:
@ -76,8 +82,3 @@ def test_oom() -> None:
font = ImageFont.ImageFont()
font._load_pilfont_data(fp, Image.new("L", (1, 1)))
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:
im = Image.new("1", (1, 1), 1)
new_im = ImageOps.pad(im, (4, 1))
assert new_im.load()[2, 0] == 1
px = new_im.load()
assert px is not None
assert px[2, 0] == 1
new_im = ImageOps.pad(im, (1, 4))
assert new_im.load()[0, 2] == 1
px = new_im.load()
assert px is not None
assert px[0, 2] == 1
@pytest.mark.parametrize("mode", ("P", "PA"))
@ -223,6 +227,7 @@ def test_expand_palette(border: int | tuple[int, int, int, int]) -> None:
else:
left, top, right, bottom = border
px = im_expanded.convert("RGB").load()
assert px is not None
for x in range(im_expanded.width):
for b in range(top):
assert px[x, b] == (255, 0, 0)
@ -432,6 +437,16 @@ def test_exif_transpose() -> None:
assert 0x0112 not in transposed_im.getexif()
def test_exif_transpose_xml_without_xmp() -> None:
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
assert im.getexif()[0x0112] == 3
assert "XML:com.adobe.xmp" in im.info
del im.info["xmp"]
transposed_im = ImageOps.exif_transpose(im)
assert 0x0112 not in transposed_im.getexif()
def test_exif_transpose_in_place() -> None:
with Image.open("Tests/images/orientation_rectangle.jpg") as im:
assert im.size == (2, 1)

View File

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

View File

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

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import os
from typing import Any
import pytest
@ -65,6 +66,27 @@ def test_show_without_viewers() -> None:
ImageShow._viewers = viewers
@pytest.mark.parametrize(
"viewer",
(
ImageShow.Viewer(),
ImageShow.WindowsViewer(),
ImageShow.MacViewer(),
ImageShow.XDGViewer(),
ImageShow.DisplayViewer(),
ImageShow.GmDisplayViewer(),
ImageShow.EogViewer(),
ImageShow.XVViewer(),
ImageShow.IPythonViewer(),
),
)
def test_show_file(viewer: ImageShow.Viewer) -> None:
assert not os.path.exists("missing.png")
with pytest.raises(FileNotFoundError):
viewer.show_file("missing.png")
def test_viewer() -> None:
viewer = ImageShow.Viewer()

View File

@ -70,6 +70,11 @@ def test_photoimage(mode: str) -> None:
reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded, im.convert("RGBA"))
with pytest.raises(ValueError):
ImageTk.PhotoImage()
with pytest.raises(ValueError):
ImageTk.PhotoImage(mode)
def test_photoimage_apply_transparency() -> None:
with Image.open("Tests/images/pil123p.png") as im:

View File

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

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

View File

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

View File

@ -338,7 +338,7 @@ linkcheck_allowed_redirects = {
# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html
_repo = "https://github.com/python-pillow/Pillow/"
extlinks = {
"cve": ("https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-%s", "CVE-%s"),
"cve": ("https://www.cve.org/CVERecord?id=CVE-%s", "CVE-%s"),
"cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
"issue": (_repo + "issues/%s", "#%s"),
"pr": (_repo + "pull/%s", "#%s"),

View File

@ -12,28 +12,6 @@ Deprecated features
Below are features which are considered deprecated. Where appropriate,
a :py:exc:`DeprecationWarning` is issued.
PSFile
~~~~~~
.. deprecated:: 9.5.0
The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will
be removed in Pillow 11 (2024-10-15). This class was only made as a helper to
be used internally, so there is no replacement. If you need this functionality
though, it is a very short class that can easily be recreated in your own code.
PyAccess and Image.USE_CFFI_ACCESS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 10.0.0
Since Pillow's C API is now faster than PyAccess on PyPy,
:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow
11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead.
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is
similarly deprecated.
ImageFile.raise_oserror
~~~~~~~~~~~~~~~~~~~~~~~
@ -107,6 +85,15 @@ BGR;15, BGR 16 and BGR;24
The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated.
Non-image modes in ImageCms
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.4.0
The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow
image modes has been deprecated. Defaulting to "L" or "1" if the mode cannot be mapped
is also deprecated.
Support for LibTIFF earlier than 4
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -128,6 +115,29 @@ Removed features
Deprecated features are only removed in major releases after an appropriate
period of deprecation has passed.
PSFile
~~~~~~
.. deprecated:: 9.5.0
.. versionremoved:: 11.0.0
The :py:class:`!PSFile` class was removed in Pillow 11 (2024-10-15).
This class was only made as a helper to be used internally,
so there is no replacement. If you need this functionality though,
it is a very short class that can easily be recreated in your own code.
PyAccess and Image.USE_CFFI_ACCESS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 10.0.0
.. versionremoved:: 11.0.0
Since Pillow's C API is now faster than PyAccess on PyPy, ``PyAccess`` has been
removed. Pillow's C API will now be used on PyPy instead.
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was
similarly removed.
Tk/Tcl 8.4
~~~~~~~~~~

View File

@ -68,8 +68,8 @@ and :pypi:`olefile` for Pillow to read FPX and MIC images::
.. tab:: Windows
We provide Pillow binaries for Windows compiled for the matrix of supported
Pythons in the wheel format. These include x86, x86-64 and arm64 versions
(with the exception of Python 3.8 on arm64). These binaries include support
Pythons in the wheel format. These include x86, x86-64 and arm64 versions.
These binaries include support
for all optional libraries except libimagequant and libxcb. Raqm support
requires FriBiDi to be installed separately::

View File

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

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 |
+----------------------------------+----------------------------+---------------------+
| Debian 11 Bullseye | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 39 | 3.12 | x86-64 |
@ -37,14 +35,12 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 13 Ventura | 3.8, 3.9 | x86-64 |
| macOS 13 Ventura | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
| | PyPy3 | |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 |
| Ubuntu Linux 22.04 LTS (Jammy) | 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | |
| +----------------------------+---------------------+
| | 3.10 | arm64v8 |
@ -52,16 +48,16 @@ These platforms are built and tested for every change.
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
| | | s390x |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2016 | 3.8 | x86-64 |
| Windows Server 2016 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 |
| Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 | x86 |
| +----------------------------+---------------------+
| | 3.9 (MinGW) | x86-64 |
| +----------------------------+---------------------+
| | 3.8, 3.9 (Cygwin) | x86-64 |
| | 3.9 (Cygwin) | x86-64 |
+----------------------------------+----------------------------+---------------------+

View File

@ -381,6 +381,11 @@ Constants
Set to 89,478,485, approximately 0.25GB for a 24-bit (3 bpp) image.
See :py:meth:`~PIL.Image.open` for more information about how this is used.
.. data:: WARN_POSSIBLE_FORMATS
Set to false. If true, when an image cannot be identified, warnings will be raised
from formats that attempted to read the data.
Transpose methods
^^^^^^^^^^^^^^^^^

View File

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

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
PSDraw
PixelAccess
PyAccess
features
../PIL
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,
:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow
:py:mod:`!PyAccess` has been deprecated and will be removed in Pillow
11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead.
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is

View File

@ -4,21 +4,16 @@
Security
========
TODO
^^^^
ImageShow.WindowsViewer.show_file
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
If an attacker has control over the ``path`` passed to
``ImageShow.WindowsViewer.show_file()``, they may be able to
execute arbitrary shell commands.
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards Incompatible Changes
==============================
TODO
^^^^
To prevent this, a :py:exc:`FileNotFoundError` will be raised if the ``path``
does not exist as a file. To provide a consistent experience, the error has
been added to all :py:class:`~PIL.ImageShow` viewers.
Deprecations
============
@ -28,6 +23,13 @@ BGR;15, BGR 16 and BGR;24
The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated.
Non-image modes in ImageCms
^^^^^^^^^^^^^^^^^^^^^^^^^^^
The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow
image modes has been deprecated. Defaulting to "L" or "1" if the mode cannot be mapped
is also deprecated.
Support for LibTIFF earlier than 4
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -39,14 +41,6 @@ ImageDraw.getdraw hints parameter
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
API Changes
===========
TODO
^^^^
TODO
API Additions
=============
@ -57,11 +51,6 @@ Added :py:meth:`~PIL.ImageDraw.ImageDraw.circle`. It provides the same functiona
:py:meth:`~PIL.ImageDraw.ImageDraw.ellipse`, but instead of taking a bounding box, it
takes a center point and radius.
TODO
^^^^
TODO
Other Changes
=============

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
^^^^^^
The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will
The :py:class:`!PSFile` class has been deprecated and will
be removed in Pillow 11 (2024-10-15). This class was only made as a helper to
be used internally, so there is no replacement. If you need this functionality
though, it is a very short class that can easily be recreated in your own code.

View File

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

View File

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

View File

@ -43,7 +43,7 @@ WEBP_ROOT = None
ZLIB_ROOT = None
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
if sys.platform == "win32" and sys.version_info >= (3, 13):
if sys.platform == "win32" and sys.version_info >= (3, 14):
import atexit
atexit.register(

View File

@ -31,7 +31,6 @@ from typing import IO
from . import Image, ImageFile
from ._binary import i32le as i32
from ._deprecate import deprecate
# --------------------------------------------------------------------
@ -159,43 +158,6 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
return im
class PSFile:
"""
Wrapper for bytesio object that treats either CR or LF as end of line.
This class is no longer used internally, but kept for backwards compatibility.
"""
def __init__(self, fp):
deprecate(
"PSFile",
11,
action="If you need the functionality of this class "
"you will need to implement it yourself.",
)
self.fp = fp
self.char = None
def seek(self, offset, whence=io.SEEK_SET):
self.char = None
self.fp.seek(offset, whence)
def readline(self) -> str:
s = [self.char or b""]
self.char = None
c = self.fp.read(1)
while (c not in b"\r\n") and len(c):
s.append(c)
c = self.fp.read(1)
self.char = self.fp.read(1)
# line endings can be 1 or 2 of \r \n, in either order
if self.char in b"\r\n":
self.char = None
return b"".join(s).decode("latin-1")
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)

View File

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

View File

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

View File

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

View File

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

View File

@ -299,6 +299,31 @@ class ImageCmsTransform(Image.ImagePointHandler):
proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC,
flags: Flags = Flags.NONE,
):
supported_modes = (
"RGB",
"RGBA",
"RGBX",
"CMYK",
"I;16",
"I;16L",
"I;16B",
"YCbCr",
"LAB",
"L",
"1",
)
for mode in (input_mode, output_mode):
if mode not in supported_modes:
deprecate(
mode,
12,
{
"L;16": "I;16 or I;16L",
"L:16B": "I;16B",
"YCCA": "YCbCr",
"YCC": "YCbCr",
}.get(mode),
)
if proof is None:
self.transform = core.buildTransform(
input.profile, output.profile, input_mode, output_mode, intent, flags

View File

@ -34,8 +34,9 @@ from __future__ import annotations
import math
import numbers
import struct
from collections.abc import Sequence
from types import ModuleType
from typing import TYPE_CHECKING, AnyStr, Callable, List, Sequence, Tuple, Union, cast
from typing import TYPE_CHECKING, AnyStr, Callable, Union, cast
from . import Image, ImageColor
from ._deprecate import deprecate
@ -51,7 +52,7 @@ except AttributeError:
if TYPE_CHECKING:
from . import ImageDraw2, ImageFont
_Ink = Union[float, Tuple[int, ...], str]
_Ink = Union[float, tuple[int, ...], str]
"""
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"
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)):
if not all(
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))"
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])
else:
msg = (

View File

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

View File

@ -906,6 +906,142 @@ def load_path(filename: str | bytes) -> ImageFont:
raise OSError(msg)
def load_default_imagefont() -> ImageFont:
f = ImageFont()
f._load_pilfont_data(
# courB08
BytesIO(
base64.b64decode(
b"""
UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA
BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL
AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA
AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB
ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A
BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB
//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA
AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH
AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA
ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv
AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/
/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5
AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA
AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG
AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA
BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA
AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA
2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF
AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA////
+gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA
////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA
BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv
AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA
AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA
AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA
BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP//
//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA
AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF
AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB
mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn
AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA
AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7
AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA
Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB
//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA
AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ
AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC
DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ
AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/
+wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5
AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/
///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG
AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA
BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA
Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC
eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG
AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA////
+gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA
////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA
BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT
AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A
AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA
Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA
Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP//
//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA
AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ
AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA
LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5
AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA
AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5
AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA
AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG
AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA
EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK
AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA
pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG
AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA////
+QAGAAIAzgAKANUAEw==
"""
)
),
Image.open(
BytesIO(
base64.b64decode(
b"""
iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u
Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9
M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g
LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F
IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA
Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791
NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx
in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9
SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY
AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt
y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG
ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY
lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H
/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3
AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47
c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/
/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw
pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv
oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR
evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA
AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v//
Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR
w7IkEbzhVQAAAABJRU5ErkJggg==
"""
)
)
),
)
return f
def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
"""If FreeType support is available, load a version of Aileron Regular,
https://dotcolon.net/font/aileron, with a more limited character set.
@ -920,9 +1056,8 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
:return: A font object.
"""
f: FreeTypeFont | ImageFont
if isinstance(core, ModuleType) or size is not None:
f = truetype(
return truetype(
BytesIO(
base64.b64decode(
b"""
@ -1152,137 +1287,4 @@ AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ==
10 if size is None else size,
layout_engine=Layout.BASIC,
)
else:
f = ImageFont()
f._load_pilfont_data(
# courB08
BytesIO(
base64.b64decode(
b"""
UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA
BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL
AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA
AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB
ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A
BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB
//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA
AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH
AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA
ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv
AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/
/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5
AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA
AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG
AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA
BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA
AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA
2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF
AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA////
+gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA
////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA
BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv
AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA
AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA
AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA
BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP//
//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA
AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF
AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB
mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn
AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA
AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7
AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA
Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB
//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA
AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ
AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC
DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ
AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/
+wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5
AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/
///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG
AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA
BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA
Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC
eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG
AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA////
+gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA
////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA
BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT
AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A
AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA
Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA
Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP//
//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA
AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ
AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA
LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5
AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA
AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5
AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA
AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG
AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA
EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK
AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA
pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG
AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA////
+QAGAAIAzgAKANUAEw==
"""
)
),
Image.open(
BytesIO(
base64.b64decode(
b"""
iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u
Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9
M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g
LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F
IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA
Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791
NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx
in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9
SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY
AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt
y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG
ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY
lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H
/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3
AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47
c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/
/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw
pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv
oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR
evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA
AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v//
Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR
w7IkEbzhVQAAAABJRU5ErkJggg==
"""
)
)
),
)
return f
return load_default_imagefont()

View File

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

View File

@ -18,7 +18,8 @@
from __future__ import annotations
import array
from typing import IO, TYPE_CHECKING, Sequence
from collections.abc import Sequence
from typing import IO, TYPE_CHECKING
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -96,7 +96,7 @@ def APP(self, marker):
self.info["exif"] = s
self._exif_offset = self.fp.tell() - n + 6
elif marker == 0xFFE1 and s[:29] == b"http://ns.adobe.com/xap/1.0/\x00":
self.info["xmp"] = s.split(b"\x00")[1]
self.info["xmp"] = s.split(b"\x00", 1)[1]
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete)
self.info["flashpix"] = s # FIXME: value will change
@ -161,38 +161,6 @@ def APP(self, marker):
# plus constant header size
self.info["mpoffset"] = self.fp.tell() - n + 4
# If DPI isn't in JPEG header, fetch from EXIF
if "dpi" not in self.info and "exif" in self.info:
try:
exif = self.getexif()
resolution_unit = exif[0x0128]
x_resolution = exif[0x011A]
try:
dpi = float(x_resolution[0]) / x_resolution[1]
except TypeError:
dpi = x_resolution
if math.isnan(dpi):
msg = "DPI is not a number"
raise ValueError(msg)
if resolution_unit == 3: # cm
# 1 dpcm = 2.54 dpi
dpi *= 2.54
self.info["dpi"] = dpi, dpi
except (
struct.error,
KeyError,
SyntaxError,
TypeError,
ValueError,
ZeroDivisionError,
):
# struct.error for truncated EXIF
# KeyError for dpi not included
# SyntaxError for invalid/unreadable EXIF
# ValueError or TypeError for dpi being an invalid float
# ZeroDivisionError for invalid dpi rational value
self.info["dpi"] = 72, 72
def COM(self: JpegImageFile, marker: int) -> None:
#
@ -411,6 +379,8 @@ class JpegImageFile(ImageFile.ImageFile):
msg = "no marker found"
raise SyntaxError(msg)
self._read_dpi_from_exif()
def load_read(self, read_bytes: int) -> bytes:
"""
internal: read more image data
@ -499,6 +469,35 @@ class JpegImageFile(ImageFile.ImageFile):
def _getexif(self) -> dict[str, Any] | None:
return _getexif(self)
def _read_dpi_from_exif(self) -> None:
# If DPI isn't in JPEG header, fetch from EXIF
if "dpi" in self.info or "exif" not in self.info:
return
try:
exif = self.getexif()
resolution_unit = exif[0x0128]
x_resolution = exif[0x011A]
try:
dpi = float(x_resolution[0]) / x_resolution[1]
except TypeError:
dpi = x_resolution
if math.isnan(dpi):
msg = "DPI is not a number"
raise ValueError(msg)
if resolution_unit == 3: # cm
# 1 dpcm = 2.54 dpi
dpi *= 2.54
self.info["dpi"] = dpi, dpi
except (
struct.error, # truncated EXIF
KeyError, # dpi not included
SyntaxError, # invalid/unreadable EXIF
TypeError, # dpi is an invalid float
ValueError, # dpi is an invalid float
ZeroDivisionError, # invalid dpi rational value
):
self.info["dpi"] = 72, 72
def _getmp(self):
return _getmp(self)

View File

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

View File

@ -8,7 +8,7 @@ import os
import re
import time
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
@ -240,7 +240,7 @@ class PdfName:
return bytes(result)
class PdfArray(List[Any]):
class PdfArray(list[Any]):
def __bytes__(self) -> bytes:
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"

View File

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

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

View File

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

View File

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

View File

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

View File

@ -2,14 +2,16 @@ from __future__ import annotations
import os
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:
import numpy.typing as npt
if TYPE_CHECKING:
try:
import numpy.typing as npt
NumpyArray = npt.NDArray[Any]
except ImportError:
pass
NumpyArray = npt.NDArray[Any] # requires numpy>=1.21
except (ImportError, AttributeError):
pass
if sys.version_info >= (3, 10):
from typing import TypeGuard

View File

@ -1,4 +1,4 @@
# Master version for Pillow
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) {
return TYPE_CMYK_8;
}
if (strcmp(PILmode, "L;16") == 0) {
if (strcmp(PILmode, "I;16") == 0 || strcmp(PILmode, "I;16L") == 0 ||
strcmp(PILmode, "L;16") == 0) {
return TYPE_GRAY_16;
}
if (strcmp(PILmode, "L;16B") == 0) {
if (strcmp(PILmode, "I;16B") == 0 || strcmp(PILmode, "L;16B") == 0) {
return TYPE_GRAY_16_SE;
}
if (strcmp(PILmode, "YCCA") == 0 || strcmp(PILmode, "YCC") == 0) {
if (strcmp(PILmode, "YCbCr") == 0 || strcmp(PILmode, "YCCA") == 0 ||
strcmp(PILmode, "YCC") == 0) {
return TYPE_YCbCr_8;
}
if (strcmp(PILmode, "LAB") == 0) {
// LabX equivalent like ALab, but not reversed -- no #define in lcms2
return (COLORSPACE_SH(PT_LabV2) | CHANNELS_SH(3) | BYTES_SH(1) | EXTRA_SH(1));
}
/* presume "L" by default */
/* presume "1" or "L" by default */
return TYPE_GRAY_8;
}

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

View File

@ -3,11 +3,10 @@ requires =
tox>=4.2
env_list =
lint
py{py3, 312, 311, 310, 39, 38}
py{py3, 313, 312, 311, 310, 39}
[testenv]
deps =
cffi
numpy
extras =
tests
@ -39,7 +38,6 @@ deps =
ipython
numpy
packaging
types-cffi
types-defusedxml
types-olefile
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:
```
set PYTHON=C:\Python38\bin
set PYTHON=C:\Python39\bin
cd /D C:\Pillow\winbuild
%PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends
build\build_dep_all.cmd

View File

@ -114,7 +114,7 @@ Example
The following is a simplified version of the script used on AppVeyor::
set PYTHON=C:\Python38\bin
set PYTHON=C:\Python39\bin
cd /D C:\Pillow\winbuild
%PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends
build\build_dep_all.cmd