Merge branch 'main' into fromarray_mode

This commit is contained in:
Andrew Murray 2025-09-01 08:30:22 +10:00 committed by GitHub
commit 1c70e716ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 298 additions and 186 deletions

View File

@ -1 +1 @@
cibuildwheel==3.1.2
cibuildwheel==3.1.4

View File

@ -1,4 +1,4 @@
mypy==1.17.0
mypy==1.17.1
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython

View File

@ -32,7 +32,7 @@ jobs:
name: Docs
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false

View File

@ -20,7 +20,7 @@ jobs:
name: Lint
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false

View File

@ -47,6 +47,8 @@ jobs:
centos-stream-10-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
debian-13-trixie-x86,
debian-13-trixie-amd64,
fedora-41-amd64,
fedora-42-amd64,
gentoo,
@ -66,7 +68,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false

View File

@ -45,7 +45,7 @@ jobs:
steps:
- name: Checkout Pillow
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false

View File

@ -41,7 +41,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false

View File

@ -39,7 +39,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false

View File

@ -47,19 +47,19 @@ jobs:
steps:
- name: Checkout Pillow
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false
- name: Checkout cached dependencies
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false
repository: python-pillow/pillow-depends
path: winbuild\depends
- name: Checkout extra test images
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false
repository: python-pillow/test-images

View File

@ -65,7 +65,7 @@ jobs:
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
@ -111,7 +111,7 @@ jobs:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Register gcc problem matcher
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'"
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'"
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Build

View File

@ -94,7 +94,7 @@ ARCHIVE_SDIR=pillow-depends-main
# annotations have a source code patch that is required for some platforms. If
# you change those versions, ensure the patch is also updated.
FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=11.2.1
HARFBUZZ_VERSION=11.3.3
LIBPNG_VERSION=1.6.50
JPEGTURBO_VERSION=3.1.1
OPENJPEG_VERSION=2.5.3
@ -165,7 +165,7 @@ function build_brotli {
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
(cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \
&& make install)
&& make -j4 install)
touch brotli-stamp
}
@ -249,7 +249,7 @@ function build_libavif {
cp $WORKDIR/meson-cross.txt $out_dir/crossfile-apple.meson
fi
(cd $out_dir && make install)
(cd $out_dir && make -j4 install)
touch libavif-stamp
}

View File

@ -99,14 +99,14 @@ jobs:
cibw_arch: arm64_iphoneos
- name: "iOS arm64 simulator"
platform: ios
os: macos-latest
os: macos-14
cibw_arch: arm64_iphonesimulator
- name: "iOS x86_64 simulator"
platform: ios
os: macos-13
cibw_arch: x86_64_iphonesimulator
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
submodules: true
@ -153,12 +153,12 @@ jobs:
- cibw_arch: ARM64
os: windows-11-arm
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Checkout extra test images
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false
repository: python-pillow/test-images
@ -234,7 +234,7 @@ jobs:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
@ -256,7 +256,7 @@ jobs:
runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
pattern: dist-*
path: dist
@ -278,7 +278,7 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
pattern: dist-*
path: dist

2
.github/zizmor.yml vendored
View File

@ -1,5 +1,5 @@
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
# https://woodruffw.github.io/zizmor/configuration/
# https://docs.zizmor.sh/configuration/
rules:
unpinned-uses:
config:

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.2
rev: v0.12.7
hooks:
- id: ruff-check
args: [--exit-non-zero-on-fix]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v20.1.7
rev: v20.1.8
hooks:
- id: clang-format
types: [c]
@ -57,7 +57,7 @@ repos:
- id: check-readthedocs
- id: check-renovate
- repo: https://github.com/woodruffw/zizmor-pre-commit
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.11.0
hooks:
- id: zizmor
@ -79,7 +79,7 @@ repos:
additional_dependencies: [trove-classifiers>=2024.10.12]
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.5.0
rev: 1.6.0
hooks:
- id: tox-ini-fmt

View File

@ -175,6 +175,14 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
return pytest.mark.skipif(not features.check(feature), reason=reason)
def has_feature_version(feature: str, required: str) -> bool:
version = features.version(feature)
assert version is not None
version_required = parse_version(required)
version_available = parse_version(version)
return version_available >= version_required
def skip_unless_feature_version(
feature: str, required: str, reason: str | None = None
) -> pytest.MarkDecorator:

View File

@ -1,8 +1,10 @@
from __future__ import annotations
from io import BytesIO
import pytest
from PIL import GbrImagePlugin, Image
from PIL import GbrImagePlugin, Image, _binary
from .helper import assert_image_equal_tofile
@ -31,8 +33,49 @@ def test_multiple_load_operations() -> None:
assert_image_equal_tofile(im, "Tests/images/gbr.png")
def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
def create_gbr_image(info: dict[str, int] = {}, magic_number=b"") -> BytesIO:
return BytesIO(
b"".join(
_binary.o32be(i)
for i in [
info.get("header_size", 20),
info.get("version", 1),
info.get("width", 1),
info.get("height", 1),
info.get("color_depth", 1),
]
)
+ magic_number
)
with pytest.raises(SyntaxError):
def test_invalid_file() -> None:
for f in [
create_gbr_image({"header_size": 0}),
create_gbr_image({"width": 0}),
create_gbr_image({"height": 0}),
]:
with pytest.raises(SyntaxError, match="not a GIMP brush"):
GbrImagePlugin.GbrImageFile(f)
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError, match="Unsupported GIMP brush version"):
GbrImagePlugin.GbrImageFile(invalid_file)
def test_unsupported_gimp_brush() -> None:
f = create_gbr_image({"color_depth": 2})
with pytest.raises(SyntaxError, match="Unsupported GIMP brush color depth: 2"):
GbrImagePlugin.GbrImageFile(f)
def test_bad_magic_number() -> None:
f = create_gbr_image({"version": 2}, magic_number=b"badm")
with pytest.raises(SyntaxError, match="not a GIMP brush, bad magic number"):
GbrImagePlugin.GbrImageFile(f)
def test_L() -> None:
f = create_gbr_image()
with Image.open(f) as im:
assert im.mode == "L"

View File

@ -1,10 +1,15 @@
from __future__ import annotations
from io import BytesIO
import pytest
from PIL import Image
def test_load_raw() -> None:
with Image.open("Tests/images/hopper.pcd") as im:
assert im.size == (768, 512)
im.load() # should not segfault.
# Note that this image was created with a resized hopper
@ -15,3 +20,13 @@ def test_load_raw() -> None:
# target = hopper().resize((768,512))
# assert_image_similar(im, target, 10)
@pytest.mark.parametrize("orientation", (1, 3))
def test_rotated(orientation: int) -> None:
with open("Tests/images/hopper.pcd", "rb") as fp:
data = bytearray(fp.read())
data[2048 + 1538] = orientation
f = BytesIO(data)
with Image.open(f) as im:
assert im.size == (512, 768)

View File

@ -4,13 +4,13 @@ from collections.abc import Generator
from pathlib import Path
import pytest
from packaging.version import parse as parse_version
from PIL import GifImagePlugin, Image, WebPImagePlugin, features
from PIL import GifImagePlugin, Image, WebPImagePlugin
from .helper import (
assert_image_equal,
assert_image_similar,
has_feature_version,
is_big_endian,
skip_unless_feature,
)
@ -53,11 +53,8 @@ def test_write_animation_L(tmp_path: Path) -> None:
im.load()
assert_image_similar(im, orig.convert("RGBA"), 32.9)
if is_big_endian():
version = features.version_module("webp")
assert version is not None
if parse_version(version) < parse_version("1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
if is_big_endian() and not has_feature_version("webp", "1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
orig.seek(orig.n_frames - 1)
im.seek(im.n_frames - 1)
orig.load()
@ -81,11 +78,8 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
assert_image_equal(im, frame1.convert("RGBA"))
# Compare second frame to original
if is_big_endian():
version = features.version_module("webp")
assert version is not None
if parse_version(version) < parse_version("1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
if is_big_endian() and not has_feature_version("webp", "1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
im.seek(1)
im.load()
assert_image_equal(im, frame2.convert("RGBA"))

View File

@ -388,6 +388,37 @@ class TestImage:
assert img_colors is not None
assert sorted(img_colors) == expected_colors
def test_alpha_composite_la(self) -> None:
# Arrange
expected_colors = sorted(
[
(3300, (255, 255)),
(1156, (170, 192)),
(1122, (128, 255)),
(1089, (0, 0)),
(1122, (255, 128)),
(1122, (0, 128)),
(1089, (0, 255)),
]
)
dst = Image.new("LA", size=(100, 100), color=(0, 255))
draw = ImageDraw.Draw(dst)
draw.rectangle((0, 33, 100, 66), fill=(0, 128))
draw.rectangle((0, 67, 100, 100), fill=(0, 0))
src = Image.new("LA", size=(100, 100), color=(255, 255))
draw = ImageDraw.Draw(src)
draw.rectangle((33, 0, 66, 100), fill=(255, 128))
draw.rectangle((67, 0, 100, 100), fill=(255, 0))
# Act
img = Image.alpha_composite(dst, src)
# Assert
img_colors = img.getcolors()
assert img_colors is not None
assert sorted(img_colors) == expected_colors
def test_alpha_inplace(self) -> None:
src = Image.new("RGBA", (128, 128), "blue")
@ -922,6 +953,17 @@ class TestImage:
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769)
def test_delete_ifd_tag(self) -> None:
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
exif.get_ifd(0x8769)
assert 0x8769 in exif
del exif[0x8769]
reloaded_exif = Image.Exif()
reloaded_exif.load(exif.tobytes())
assert 0x8769 not in reloaded_exif
def test_exif_load_from_fp(self) -> None:
with Image.open("Tests/images/flower.jpg") as im:
data = im.info["exif"]

View File

@ -1,11 +1,16 @@
from __future__ import annotations
import pytest
from packaging.version import parse as parse_version
from PIL import Image, features
from PIL import Image
from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature
from .helper import (
assert_image_similar,
has_feature_version,
hopper,
is_ppc64le,
skip_unless_feature,
)
def test_sanity() -> None:
@ -23,11 +28,8 @@ def test_sanity() -> None:
@skip_unless_feature("libimagequant")
def test_libimagequant_quantize() -> None:
image = hopper()
if is_ppc64le():
version = features.version_feature("libimagequant")
assert version is not None
if parse_version(version) < parse_version("4"):
pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
if is_ppc64le() and not has_feature_version("libimagequant", "4"):
pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 15)

View File

@ -7,6 +7,7 @@ from PIL import Image, ImageDraw, ImageFont
from .helper import (
assert_image_equal_tofile,
assert_image_similar_tofile,
has_feature_version,
skip_unless_feature,
)
@ -104,11 +105,9 @@ def test_text_direction_ttb() -> None:
im = Image.new(mode="RGB", size=(100, 300))
draw = ImageDraw.Draw(im)
try:
draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb")
except ValueError as ex:
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
pytest.skip("libraqm 0.7 or greater not available")
if not has_feature_version("raqm", "0.7"):
pytest.skip("libraqm 0.7 or greater not available")
draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb")
target = "Tests/images/test_direction_ttb.png"
assert_image_similar_tofile(im, target, 2.8)
@ -119,19 +118,17 @@ def test_text_direction_ttb_stroke() -> None:
im = Image.new(mode="RGB", size=(100, 300))
draw = ImageDraw.Draw(im)
try:
draw.text(
(27, 27),
"あい",
font=ttf,
fill=500,
direction="ttb",
stroke_width=2,
stroke_fill="#0f0",
)
except ValueError as ex:
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
pytest.skip("libraqm 0.7 or greater not available")
if not has_feature_version("raqm", "0.7"):
pytest.skip("libraqm 0.7 or greater not available")
draw.text(
(27, 27),
"あい",
font=ttf,
fill=500,
direction="ttb",
stroke_width=2,
stroke_fill="#0f0",
)
target = "Tests/images/test_direction_ttb_stroke.png"
assert_image_similar_tofile(im, target, 19.4)
@ -219,14 +216,9 @@ def test_getlength(
im = Image.new(mode, (1, 1), 0)
d = ImageDraw.Draw(im)
try:
assert d.textlength(text, ttf, direction) == expected
except ValueError as ex:
if (
direction == "ttb"
and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction"
):
pytest.skip("libraqm 0.7 or greater not available")
if direction == "ttb" and not has_feature_version("raqm", "0.7"):
pytest.skip("libraqm 0.7 or greater not available")
assert d.textlength(text, ttf, direction) == expected
@pytest.mark.parametrize("mode", ("L", "1"))
@ -242,17 +234,12 @@ def test_getlength_combine(mode: str, direction: str, text: str) -> None:
ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
try:
target = ttf.getlength("ii", mode, direction)
actual = ttf.getlength(text, mode, direction)
if direction == "ttb" and not has_feature_version("raqm", "0.7"):
pytest.skip("libraqm 0.7 or greater not available")
target = ttf.getlength("ii", mode, direction)
actual = ttf.getlength(text, mode, direction)
assert actual == target
except ValueError as ex:
if (
direction == "ttb"
and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction"
):
pytest.skip("libraqm 0.7 or greater not available")
assert actual == target
@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm"))
@ -265,11 +252,9 @@ def test_anchor_ttb(anchor: str) -> None:
d = ImageDraw.Draw(im)
d.line(((0, 200), (200, 200)), "gray")
d.line(((100, 0), (100, 400)), "gray")
try:
d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f)
except ValueError as ex:
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
pytest.skip("libraqm 0.7 or greater not available")
if not has_feature_version("raqm", "0.7"):
pytest.skip("libraqm 0.7 or greater not available")
d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f)
assert_image_similar_tofile(im, path, 1) # fails at 5
@ -310,10 +295,12 @@ combine_tests = (
# this tests various combining characters for anchor alignment and clipping
@pytest.mark.parametrize(
"name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests]
"name, text, anchor, direction, epsilon",
combine_tests,
ids=[r[0] for r in combine_tests],
)
def test_combine(
name: str, text: str, dir: str | None, anchor: str | None, epsilon: float
name: str, text: str, direction: str | None, anchor: str | None, epsilon: float
) -> None:
path = f"Tests/images/test_combine_{name}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
@ -322,11 +309,9 @@ def test_combine(
d = ImageDraw.Draw(im)
d.line(((0, 200), (400, 200)), "gray")
d.line(((200, 0), (200, 400)), "gray")
try:
d.text((200, 200), text, fill="black", anchor=anchor, direction=dir, font=f)
except ValueError as ex:
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
pytest.skip("libraqm 0.7 or greater not available")
if direction == "ttb" and not has_feature_version("raqm", "0.7"):
pytest.skip("libraqm 0.7 or greater not available")
d.text((200, 200), text, fill="black", anchor=anchor, direction=direction, font=f)
assert_image_similar_tofile(im, path, epsilon)

View File

@ -7,7 +7,7 @@ import pytest
from PIL import Image, ImageMorph, _imagingmorph
from .helper import assert_image_equal_tofile, hopper
from .helper import assert_image_equal_tofile, hopper, timeout_unless_slower_valgrind
def string_to_img(image_string: str) -> Image.Image:
@ -266,16 +266,18 @@ def test_unknown_pattern() -> None:
ImageMorph.LutBuilder(op_name="unknown")
def test_pattern_syntax_error() -> None:
@pytest.mark.parametrize(
"pattern", ("a pattern with a syntax error", "4:(" + "X" * 30000)
)
@timeout_unless_slower_valgrind(1)
def test_pattern_syntax_error(pattern: str) -> None:
# Arrange
lb = ImageMorph.LutBuilder(op_name="corner")
new_patterns = ["a pattern with a syntax error"]
new_patterns = [pattern]
lb.add_patterns(new_patterns)
# Act / Assert
with pytest.raises(
Exception, match='Syntax error in pattern "a pattern with a syntax error"'
):
with pytest.raises(Exception, match='Syntax error in pattern "'):
lb.build_lut()

View File

@ -57,3 +57,13 @@ def test_constant() -> None:
assert st.rms[0] == 128
assert st.var[0] == 0
assert st.stddev[0] == 0
def test_zero_count() -> None:
im = Image.new("L", (0, 0))
st = ImageStat.Stat(im)
assert st.mean == [0]
assert st.rms == [0]
assert st.var == [0]

View File

@ -28,15 +28,13 @@ def test_numpy_to_image() -> None:
a = numpy.array(data, dtype=dtype)
a.shape = TEST_IMAGE_SIZE
i = Image.fromarray(a)
if list(i.getdata()) != data:
print("data mismatch for", dtype)
assert list(i.getdata()) == data
else:
data = list(range(100))
a = numpy.array([[x] * bands for x in data], dtype=dtype)
a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands
i = Image.fromarray(a)
if list(i.getchannel(0).getdata()) != list(range(100)):
print("data mismatch for", dtype)
assert list(i.getchannel(0).getdata()) == list(range(100))
return i
# Check supported 1-bit integer formats

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3
from __future__ import annotations
import sys
from collections.abc import Callable
from typing import Any
@ -8,12 +9,12 @@ import pytest
from PIL import Image
from .helper import is_win32
min_iterations = 100
max_iterations = 10000
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
pytestmark = pytest.mark.skipif(
sys.platform.startswith("win32"), reason="requires Unix or macOS"
)
def _get_mem_usage() -> float:

View File

@ -1,12 +1,11 @@
from __future__ import annotations
import sys
from io import BytesIO
import pytest
from PIL import Image
from .helper import is_win32, skip_unless_feature
from PIL import Image, features
# Limits for testing the leak
mem_limit = 1024 * 1048576
@ -15,8 +14,10 @@ iterations = int((mem_limit / stack_size) * 2)
test_file = "Tests/images/rgb_trns_ycbc.jp2"
pytestmark = [
pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"),
skip_unless_feature("jpg_2000"),
pytest.mark.skipif(
sys.platform.startswith("win32"), reason="requires Unix or macOS"
),
pytest.mark.skipif(not features.check("jpg_2000"), reason="jpg_2000 not available"),
]

View File

@ -1,10 +1,11 @@
from __future__ import annotations
import sys
from io import BytesIO
import pytest
from .helper import hopper, is_win32
from PIL import Image
iterations = 5000
@ -18,7 +19,9 @@ valgrind --tool=massif python test-installed.py -s -v checks/check_jpeg_leaks.py
"""
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
pytestmark = pytest.mark.skipif(
sys.platform.startswith("win32"), reason="requires Unix or macOS"
)
"""
pre patch:
@ -112,10 +115,10 @@ standard_chrominance_qtable = (
),
)
def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None:
im = hopper("RGB")
for _ in range(iterations):
test_output = BytesIO()
im.save(test_output, "JPEG", qtables=qtables)
with Image.open("Tests/images/hopper.ppm") as im:
for _ in range(iterations):
test_output = BytesIO()
im.save(test_output, "JPEG", qtables=qtables)
def test_exif_leak() -> None:
@ -173,12 +176,12 @@ def test_exif_leak() -> None:
0 +----------------------------------------------------------------------->Gi
0 11.33
"""
im = hopper("RGB")
exif = b"12345678" * 4096
for _ in range(iterations):
test_output = BytesIO()
im.save(test_output, "JPEG", exif=exif)
with Image.open("Tests/images/hopper.ppm") as im:
for _ in range(iterations):
test_output = BytesIO()
im.save(test_output, "JPEG", exif=exif)
def test_base_save() -> None:
@ -207,8 +210,7 @@ def test_base_save() -> None:
| :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@:::
0 +----------------------------------------------------------------------->Gi
0 7.882"""
im = hopper("RGB")
for _ in range(iterations):
test_output = BytesIO()
im.save(test_output, "JPEG")
with Image.open("Tests/images/hopper.ppm") as im:
for _ in range(iterations):
test_output = BytesIO()
im.save(test_output, "JPEG")

View File

@ -2,7 +2,7 @@
# install libimagequant
archive_name=libimagequant
archive_version=4.3.4
archive_version=4.4.0
archive=$archive_name-$archive_version

View File

@ -11,7 +11,7 @@ Here is a list of PyPI projects that offer additional plugins:
* :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library.
* :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL.
* :pypi:`pillow-heif`: Python bindings to libheif for working with HEIF images.
* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implemetation. Python bindings implemented using pybind11.
* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implementation. Python bindings implemented using pybind11.
* :pypi:`pillow-jxl-plugin`: Plugin for JPEG-XL, using Rust for bindings.
* :pypi:`pillow-mbm`: Adds support for KSP's proprietary MBM texture format.
* :pypi:`pillow-svg`: Implements basic SVG read support. Supports basic paths, shapes, and text.

View File

@ -64,7 +64,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.3.4**
* Pillow has been tested with libimagequant **2.6-4.4.0**
* Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled.

View File

@ -31,6 +31,8 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 13 Trixie | 3.13 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 41 | 3.13 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 42 | 3.13 | x86-64 |
@ -39,7 +41,7 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| macOS 13 Ventura | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 14 Sonoma | 3.11, 3.12, 3.13, 3.14 | arm64 |
| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14 | arm64 |
| | PyPy3 | |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |

View File

@ -74,5 +74,6 @@ Constants
---------
.. autodata:: PIL.ImageFile.LOAD_TRUNCATED_IMAGES
.. autodata:: PIL.ImageFile.MAXBLOCK
.. autodata:: PIL.ImageFile.ERRORS
:annotation:

View File

@ -159,3 +159,10 @@ deprecated. Part of this functionality has been restored in Pillow 12.0.0. Since
values do not contain information about palettes or color spaces, the parameter can be
used to place grayscale L mode data within a P mode image, or read RGB data as YCbCr
for example.
ImageMorph operations must have length 1
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Valid ImageMorph operations are 4, N, 1 and M. By limiting the length to 1 character
within Pillow, long execution times can be avoided if a user provided long pattern
strings. Reported by Jang Choi.

View File

@ -30,7 +30,7 @@ from ._util import DeferredError
def _accept(prefix: bytes) -> bool:
return (
len(prefix) >= 6
len(prefix) >= 16
and i16(prefix, 4) in [0xAF11, 0xAF12]
and i16(prefix, 14) in [0, 3] # flags
)

View File

@ -54,7 +54,7 @@ class GbrImageFile(ImageFile.ImageFile):
width = i32(self.fp.read(4))
height = i32(self.fp.read(4))
color_depth = i32(self.fp.read(4))
if width <= 0 or height <= 0:
if width == 0 or height == 0:
msg = "not a GIMP brush"
raise SyntaxError(msg)
if color_depth not in (1, 4):
@ -71,7 +71,7 @@ class GbrImageFile(ImageFile.ImageFile):
raise SyntaxError(msg)
self.info["spacing"] = i32(self.fp.read(4))
comment = self.fp.read(comment_length)[:-1]
self.info["comment"] = self.fp.read(comment_length)[:-1]
if color_depth == 1:
self._mode = "L"
@ -80,8 +80,6 @@ class GbrImageFile(ImageFile.ImageFile):
self._size = width, height
self.info["comment"] = comment
# Image might not be small
Image._decompression_bomb_check(self.size)

View File

@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None:
def _accept(prefix: bytes) -> bool:
return prefix.startswith(b"GRIB") and prefix[7] == 1
return len(prefix) >= 8 and prefix.startswith(b"GRIB") and prefix[7] == 1
class GribStubImageFile(ImageFile.StubImageFile):

View File

@ -103,7 +103,6 @@ try:
raise ImportError(msg)
except ImportError as v:
core = DeferredError.new(ImportError("The _imaging C module is not installed."))
# Explanations for ways that we know we might have an import error
if str(v).startswith("Module use of python"):
# The _imaging C module is present, but not compiled for
@ -3569,9 +3568,8 @@ def alpha_composite(im1: Image, im2: Image) -> Image:
"""
Alpha composite im2 over im1.
:param im1: The first image. Must have mode RGBA.
:param im2: The second image. Must have mode RGBA, and the same size as
the first image.
:param im1: The first image. Must have mode RGBA or LA.
:param im2: The second image. Must have the same mode and size as the first image.
:returns: An :py:class:`~PIL.Image.Image` object.
"""
@ -4218,6 +4216,8 @@ class Exif(_ExifBase):
del self._info[tag]
else:
del self._data[tag]
if tag in self._ifds:
del self._ifds[tag]
def __iter__(self) -> Iterator[int]:
keys = set(self._data)

View File

@ -46,6 +46,18 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
MAXBLOCK = 65536
"""
By default, Pillow processes image data in blocks. This helps to prevent excessive use
of resources. Codecs may disable this behaviour with ``_pulls_fd`` or ``_pushes_fd``.
When reading an image, this is the number of bytes to read at once.
When writing an image, this is the number of bytes to write at once.
If the image width times 4 is greater, then that will be used instead.
Plugins may also set a greater number.
User code may set this to another number.
"""
SAFEBLOCK = 1024 * 1024

View File

@ -671,11 +671,7 @@ class FreeTypeFont:
:returns: A list of the named styles in a variation font.
:exception OSError: If the font is not a variation font.
"""
try:
names = self.font.getvarnames()
except AttributeError as e:
msg = "FreeType 2.9.1 or greater is required"
raise NotImplementedError(msg) from e
names = self.font.getvarnames()
return [name.replace(b"\x00", b"") for name in names]
def set_variation_by_name(self, name: str | bytes) -> None:
@ -702,11 +698,7 @@ class FreeTypeFont:
:returns: A list of the axes in a variation font.
:exception OSError: If the font is not a variation font.
"""
try:
axes = self.font.getvaraxes()
except AttributeError as e:
msg = "FreeType 2.9.1 or greater is required"
raise NotImplementedError(msg) from e
axes = self.font.getvaraxes()
for axis in axes:
if axis["name"]:
axis["name"] = axis["name"].replace(b"\x00", b"")
@ -717,11 +709,7 @@ class FreeTypeFont:
:param axes: A list of values for each axis.
:exception OSError: If the font is not a variation font.
"""
try:
self.font.setvaraxes(axes)
except AttributeError as e:
msg = "FreeType 2.9.1 or greater is required"
raise NotImplementedError(msg) from e
self.font.setvaraxes(axes)
class TransposedFont:

View File

@ -150,7 +150,7 @@ class LutBuilder:
# Parse and create symmetries of the patterns strings
for p in self.patterns:
m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
m = re.search(r"(\w):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
if not m:
msg = 'Syntax error in pattern "' + p + '"'
raise Exception(msg)

View File

@ -120,7 +120,7 @@ class Stat:
@cached_property
def mean(self) -> list[float]:
"""Average (arithmetic mean) pixel level for each band in the image."""
return [self.sum[i] / self.count[i] for i in self.bands]
return [self.sum[i] / self.count[i] if self.count[i] else 0 for i in self.bands]
@cached_property
def median(self) -> list[int]:
@ -141,13 +141,20 @@ class Stat:
@cached_property
def rms(self) -> list[float]:
"""RMS (root-mean-square) for each band in the image."""
return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
return [
math.sqrt(self.sum2[i] / self.count[i]) if self.count[i] else 0
for i in self.bands
]
@cached_property
def var(self) -> list[float]:
"""Variance for each band in the image."""
return [
(self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
(
(self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
if self.count[i]
else 0
)
for i in self.bands
]

View File

@ -32,7 +32,7 @@ class PcdImageFile(ImageFile.ImageFile):
assert self.fp is not None
self.fp.seek(2048)
s = self.fp.read(2048)
s = self.fp.read(1539)
if not s.startswith(b"PCD_"):
msg = "not a PCD file"
@ -46,14 +46,13 @@ class PcdImageFile(ImageFile.ImageFile):
self.tile_post_rotate = -90
self._mode = "RGB"
self._size = 768, 512 # FIXME: not correct for rotated images!
self._size = (512, 768) if orientation in (1, 3) else (768, 512)
self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)]
def load_end(self) -> None:
if self.tile_post_rotate:
# Handle rotated PCDs
self.im = self.im.rotate(self.tile_post_rotate)
self._size = self.im.size
#

View File

@ -39,7 +39,7 @@ logger = logging.getLogger(__name__)
def _accept(prefix: bytes) -> bool:
return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5]
return len(prefix) >= 2 and prefix[0] == 10 and prefix[1] in [0, 2, 3, 5]
##

View File

@ -47,7 +47,7 @@ MODES = {
def _accept(prefix: bytes) -> bool:
return prefix.startswith(b"P") and prefix[1] in b"0123456fy"
return len(prefix) >= 2 and prefix.startswith(b"P") and prefix[1] in b"0123456fy"
##

View File

@ -80,7 +80,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
format_description = "Windows Metafile"
def _open(self) -> None:
# check placable header
# check placeable header
s = self.fp.read(44)
if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"):

View File

@ -1221,8 +1221,6 @@ glyph_error:
return NULL;
}
#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \
(FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1)
static PyObject *
font_getvarnames(FontObject *self) {
int error;
@ -1432,7 +1430,6 @@ font_setvaraxes(FontObject *self, PyObject *args) {
Py_RETURN_NONE;
}
#endif
static void
font_dealloc(FontObject *self) {
@ -1451,13 +1448,10 @@ static PyMethodDef font_methods[] = {
{"render", (PyCFunction)font_render, METH_VARARGS},
{"getsize", (PyCFunction)font_getsize, METH_VARARGS},
{"getlength", (PyCFunction)font_getlength, METH_VARARGS},
#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \
(FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1)
{"getvarnames", (PyCFunction)font_getvarnames, METH_NOARGS},
{"getvaraxes", (PyCFunction)font_getvaraxes, METH_NOARGS},
{"setvarname", (PyCFunction)font_setvarname, METH_VARARGS},
{"setvaraxes", (PyCFunction)font_setvaraxes, METH_VARARGS},
#endif
{NULL, NULL}
};

View File

@ -25,13 +25,12 @@ ImagingAlphaComposite(Imaging imDst, Imaging imSrc) {
int x, y;
/* Check arguments */
if (!imDst || !imSrc || strcmp(imDst->mode, "RGBA") ||
imDst->type != IMAGING_TYPE_UINT8 || imDst->bands != 4) {
if (!imDst || !imSrc ||
(strcmp(imDst->mode, "RGBA") && strcmp(imDst->mode, "LA"))) {
return ImagingError_ModeError();
}
if (strcmp(imDst->mode, imSrc->mode) || imDst->type != imSrc->type ||
imDst->bands != imSrc->bands || imDst->xsize != imSrc->xsize ||
if (strcmp(imDst->mode, imSrc->mode) || imDst->xsize != imSrc->xsize ||
imDst->ysize != imSrc->ysize) {
return ImagingError_Mismatch();
}

View File

@ -30,4 +30,4 @@ skip_install = true
deps =
-r .ci/requirements-mypy.txt
commands =
mypy conftest.py selftest.py setup.py docs src winbuild Tests {posargs}
mypy conftest.py selftest.py setup.py checks docs src winbuild Tests {posargs}

@ -1 +1 @@
Subproject commit 42d761728d141d8462cd9943f4329f12fe62b155
Subproject commit 64739327166fcad1fa41ad9b23fa910fa244c84f

View File

@ -116,11 +116,11 @@ V = {
"BROTLI": "1.1.0",
"FREETYPE": "2.13.3",
"FRIBIDI": "1.0.16",
"HARFBUZZ": "11.2.1",
"HARFBUZZ": "11.3.3",
"JPEGTURBO": "3.1.1",
"LCMS2": "2.17",
"LIBAVIF": "1.3.0",
"LIBIMAGEQUANT": "4.3.4",
"LIBIMAGEQUANT": "4.4.0",
"LIBPNG": "1.6.50",
"LIBWEBP": "1.6.0",
"OPENJPEG": "2.5.3",