Merge branch 'main' into rgba_pa

This commit is contained in:
Andrew Murray 2025-10-04 18:58:00 +10:00 committed by GitHub
commit 762bdce34f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
80 changed files with 695 additions and 404 deletions

View File

@ -51,10 +51,10 @@ pushd depends && ./install_webp.sh && popd
pushd depends && ./install_imagequant.sh && popd pushd depends && ./install_imagequant.sh && popd
# raqm # raqm
pushd depends && ./install_raqm.sh && popd pushd depends && sudo ./install_raqm.sh && popd
# libavif # libavif
pushd depends && ./install_libavif.sh && popd pushd depends && sudo ./install_libavif.sh && popd
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd

View File

@ -1 +1 @@
cibuildwheel==3.1.3 cibuildwheel==3.2.0

View File

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

View File

@ -32,12 +32,12 @@ jobs:
name: Docs name: Docs
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: "3.x" python-version: "3.x"
cache: pip cache: pip

View File

@ -20,7 +20,7 @@ jobs:
name: Lint name: Lint
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@ -33,7 +33,7 @@ jobs:
lint-pre-commit- lint-pre-commit-
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: "3.x" python-version: "3.x"
cache: pip cache: pip

View File

@ -4,11 +4,19 @@ set -e
if [[ "$ImageOS" == "macos13" ]]; then if [[ "$ImageOS" == "macos13" ]]; then
brew uninstall gradle maven brew uninstall gradle maven
wget https://raw.githubusercontent.com/python-pillow/pillow-depends/main/freetype-2.14.1.tar.gz
tar -xvzf freetype-2.14.1.tar.gz
(cd freetype-2.14.1 \
&& ./configure \
&& make -j4 \
&& make install)
else
brew install freetype
fi fi
brew install \ brew install \
aom \ aom \
dav1d \ dav1d \
freetype \
ghostscript \ ghostscript \
jpeg-turbo \ jpeg-turbo \
libimagequant \ libimagequant \

View File

@ -22,7 +22,7 @@ jobs:
steps: steps:
- name: "Check issues" - name: "Check issues"
uses: actions/stale@v9 uses: actions/stale@v10
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "Awaiting OP Action" only-labels: "Awaiting OP Action"

View File

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

View File

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

View File

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

View File

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

View File

@ -47,19 +47,19 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Checkout cached dependencies - name: Checkout cached dependencies
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
repository: python-pillow/pillow-depends repository: python-pillow/pillow-depends
path: winbuild\depends path: winbuild\depends
- name: Checkout extra test images - name: Checkout extra test images
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
repository: python-pillow/test-images repository: python-pillow/test-images
@ -67,7 +67,7 @@ jobs:
# sets env: pythonLocation # sets env: pythonLocation
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true allow-prereleases: true
@ -97,8 +97,8 @@ jobs:
choco install nasm --no-progress choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.5.1 --no-progress choco install ghostscript --version=10.6.0 --no-progress
echo "C:\Program Files\gs\gs10.05.1\bin" >> $env:GITHUB_PATH echo "C:\Program Files\gs\gs10.06.0\bin" >> $env:GITHUB_PATH
# Install extra test images # Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images xcopy /S /Y Tests\test-images\* Tests\images

View File

@ -65,12 +65,12 @@ jobs:
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true allow-prereleases: true

View File

@ -93,16 +93,20 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds. Version numbers with "Patched" # Package versions for fresh source builds. Version numbers with "Patched"
# annotations have a source code patch that is required for some platforms. If # annotations have a source code patch that is required for some platforms. If
# you change those versions, ensure the patch is also updated. # you change those versions, ensure the patch is also updated.
if [[ -n "$IOS_SDK" ]]; then
FREETYPE_VERSION=2.13.3 FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=11.3.3 else
FREETYPE_VERSION=2.14.1
fi
HARFBUZZ_VERSION=11.5.0
LIBPNG_VERSION=1.6.50 LIBPNG_VERSION=1.6.50
JPEGTURBO_VERSION=3.1.1 JPEGTURBO_VERSION=3.1.2
OPENJPEG_VERSION=2.5.3 OPENJPEG_VERSION=2.5.4
XZ_VERSION=5.8.1 XZ_VERSION=5.8.1
TIFF_VERSION=4.7.0 ZSTD_VERSION=1.5.7
TIFF_VERSION=4.7.1
LCMS2_VERSION=2.17 LCMS2_VERSION=2.17
ZLIB_VERSION=1.3.1 ZLIB_NG_VERSION=2.2.5
ZLIB_NG_VERSION=2.2.4
LIBWEBP_VERSION=1.6.0 LIBWEBP_VERSION=1.6.0
BZIP2_VERSION=1.0.8 BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0 LIBXCB_VERSION=1.17.0
@ -254,16 +258,20 @@ function build_libavif {
touch libavif-stamp touch libavif-stamp
} }
function build_zstd {
if [ -e zstd-stamp ]; then return; fi
local out_dir=$(fetch_unpack https://github.com/facebook/zstd/releases/download/v$ZSTD_VERSION/zstd-$ZSTD_VERSION.tar.gz)
(cd $out_dir \
&& make -j4 install)
touch zstd-stamp
}
function build { function build {
build_xz build_xz
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
yum remove -y zlib-devel yum remove -y zlib-devel
fi fi
if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then
build_new_zlib
else
build_zlib_ng build_zlib_ng
fi
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [[ -n "$IS_MACOS" ]]; then if [[ -n "$IS_MACOS" ]]; then
@ -285,6 +293,7 @@ function build {
--with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \
--disable-webp --disable-libdeflate --disable-zstd --disable-webp --disable-libdeflate --disable-zstd
else else
build_zstd
build_tiff build_tiff
fi fi
@ -309,6 +318,10 @@ function build {
if [[ -n "$IS_MACOS" ]]; then if [[ -n "$IS_MACOS" ]]; then
# Custom freetype build # Custom freetype build
if [[ -z "$IOS_SDK" ]]; then
build_simple sed 4.9 https://mirrors.middlendian.com/gnu/sed
fi
build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no
else else
build_freetype build_freetype

View File

@ -54,7 +54,7 @@ jobs:
platform: macos platform: macos
os: macos-13 os: macos-13
cibw_arch: x86_64 cibw_arch: x86_64
build: "cp3{9,10,11}*" build: "cp3{10,11}*"
macosx_deployment_target: "10.10" macosx_deployment_target: "10.10"
- name: "macOS 10.13 x86_64" - name: "macOS 10.13 x86_64"
platform: macos platform: macos
@ -99,19 +99,19 @@ jobs:
cibw_arch: arm64_iphoneos cibw_arch: arm64_iphoneos
- name: "iOS arm64 simulator" - name: "iOS arm64 simulator"
platform: ios platform: ios
os: macos-latest os: macos-14
cibw_arch: arm64_iphonesimulator cibw_arch: arm64_iphonesimulator
- name: "iOS x86_64 simulator" - name: "iOS x86_64 simulator"
platform: ios platform: ios
os: macos-13 os: macos-13
cibw_arch: x86_64_iphonesimulator cibw_arch: x86_64_iphonesimulator
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
submodules: true submodules: true
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: "3.x" python-version: "3.x"
@ -153,18 +153,18 @@ jobs:
- cibw_arch: ARM64 - cibw_arch: ARM64
os: windows-11-arm os: windows-11-arm
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Checkout extra test images - name: Checkout extra test images
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
repository: python-pillow/test-images repository: python-pillow/test-images
path: Tests\test-images path: Tests\test-images
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: "3.x" python-version: "3.x"
@ -234,12 +234,12 @@ jobs:
if: github.event_name != 'schedule' if: github.event_name != 'schedule'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: "3.x" python-version: "3.x"

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.7 rev: v0.12.11
hooks: hooks:
- id: ruff-check - id: ruff-check
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$)
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v20.1.8 rev: v21.1.0
hooks: hooks:
- id: clang-format - id: clang-format
types: [c] types: [c]
@ -36,7 +36,7 @@ repos:
- id: rst-backticks - id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 rev: v6.0.0
hooks: hooks:
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable - id: check-shebang-scripts-are-executable
@ -51,14 +51,14 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.33.2 rev: 0.33.3
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows
- id: check-readthedocs - id: check-readthedocs
- id: check-renovate - id: check-renovate
- repo: https://github.com/zizmorcore/zizmor-pre-commit - repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.11.0 rev: v1.12.1
hooks: hooks:
- id: zizmor - id: zizmor

View File

@ -175,6 +175,14 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
return pytest.mark.skipif(not features.check(feature), reason=reason) 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( def skip_unless_feature_version(
feature: str, required: str, reason: str | None = None feature: str, required: str, reason: str | None = None
) -> pytest.MarkDecorator: ) -> pytest.MarkDecorator:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,8 +1,10 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
import pytest import pytest
from PIL import GbrImagePlugin, Image from PIL import GbrImagePlugin, Image, _binary
from .helper import assert_image_equal_tofile 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") assert_image_equal_tofile(im, "Tests/images/gbr.png")
def test_invalid_file() -> None: def create_gbr_image(info: dict[str, int] = {}, magic_number=b"") -> BytesIO:
invalid_file = "Tests/images/flower.jpg" 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) 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

@ -2,6 +2,8 @@ from __future__ import annotations
from io import BytesIO from io import BytesIO
import pytest
from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, hopper
@ -9,21 +11,78 @@ from .helper import assert_image_equal, hopper
TEST_FILE = "Tests/images/iptc.jpg" TEST_FILE = "Tests/images/iptc.jpg"
def create_iptc_image(info: dict[str, int] = {}) -> BytesIO:
def field(tag, value):
return bytes((0x1C,) + tag + (0, len(value))) + value
data = field((3, 60), bytes((info.get("layers", 1), info.get("component", 0))))
data += field((3, 120), bytes((info.get("compression", 1),)))
if "band" in info:
data += field((3, 65), bytes((info["band"] + 1,)))
data += field((3, 20), b"\x01") # width
data += field((3, 30), b"\x01") # height
data += field(
(8, 10),
bytes((info.get("data", 0),)),
)
return BytesIO(data)
def test_open() -> None: def test_open() -> None:
expected = Image.new("L", (1, 1)) expected = Image.new("L", (1, 1))
f = BytesIO( f = create_iptc_image()
b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01"
b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00"
)
with Image.open(f) as im: with Image.open(f) as im:
assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] assert im.tile == [("iptc", (0, 0, 1, 1), 25, ("raw", None))]
assert_image_equal(im, expected) assert_image_equal(im, expected)
with Image.open(f) as im: with Image.open(f) as im:
assert im.load() is not None assert im.load() is not None
def test_field_length() -> None:
f = create_iptc_image()
f.seek(28)
f.write(b"\xff")
with pytest.raises(OSError, match="illegal field length in IPTC/NAA file"):
with Image.open(f):
pass
@pytest.mark.parametrize("layers, mode", ((3, "RGB"), (4, "CMYK")))
def test_layers(layers: int, mode: str) -> None:
for band in range(-1, layers):
info = {"layers": layers, "component": 1, "data": 5}
if band != -1:
info["band"] = band
f = create_iptc_image(info)
with Image.open(f) as im:
assert im.mode == mode
data = [0] * layers
data[max(band, 0)] = 5
assert im.getpixel((0, 0)) == tuple(data)
def test_unknown_compression() -> None:
f = create_iptc_image({"compression": 2})
with pytest.raises(OSError, match="Unknown IPTC image compression"):
with Image.open(f):
pass
def test_getiptcinfo() -> None:
f = create_iptc_image()
with Image.open(f) as im:
assert IptcImagePlugin.getiptcinfo(im) == {
(3, 60): b"\x01\x00",
(3, 120): b"\x01",
(3, 20): b"\x01",
(3, 30): b"\x01",
}
def test_getiptcinfo_jpg_none() -> None: def test_getiptcinfo_jpg_none() -> None:
# Arrange # Arrange
with hopper() as im: with hopper() as im:

View File

@ -365,7 +365,6 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
if 700 in reloaded.tag_v2:
assert reloaded.tag_v2[700] == b"xmlpacket tag" assert reloaded.tag_v2[700] == b"xmlpacket tag"
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:

View File

@ -1,10 +1,15 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
import pytest
from PIL import Image from PIL import Image
def test_load_raw() -> None: def test_load_raw() -> None:
with Image.open("Tests/images/hopper.pcd") as im: with Image.open("Tests/images/hopper.pcd") as im:
assert im.size == (768, 512)
im.load() # should not segfault. im.load() # should not segfault.
# Note that this image was created with a resized hopper # Note that this image was created with a resized hopper
@ -15,3 +20,13 @@ def test_load_raw() -> None:
# target = hopper().resize((768,512)) # target = hopper().resize((768,512))
# assert_image_similar(im, target, 10) # 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

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
from PIL import WalImageFile from PIL import WalImageFile
from .helper import assert_image_equal_tofile from .helper import assert_image_equal_tofile
@ -13,12 +15,22 @@ def test_open() -> None:
assert im.format_description == "Quake2 Texture" assert im.format_description == "Quake2 Texture"
assert im.mode == "P" assert im.mode == "P"
assert im.size == (128, 128) assert im.size == (128, 128)
assert "next_name" not in im.info
assert isinstance(im, WalImageFile.WalImageFile) assert isinstance(im, WalImageFile.WalImageFile)
assert_image_equal_tofile(im, "Tests/images/hopper_wal.png") assert_image_equal_tofile(im, "Tests/images/hopper_wal.png")
def test_next_name() -> None:
with open(TEST_FILE, "rb") as fp:
data = bytearray(fp.read())
data[56:60] = b"Test"
f = BytesIO(data)
with WalImageFile.open(f) as im:
assert im.info["next_name"] == b"Test"
def test_load() -> None: def test_load() -> None:
with WalImageFile.open(TEST_FILE) as im: with WalImageFile.open(TEST_FILE) as im:
px = im.load() px = im.load()

View File

@ -4,13 +4,13 @@ from collections.abc import Generator
from pathlib import Path from pathlib import Path
import pytest 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 ( from .helper import (
assert_image_equal, assert_image_equal,
assert_image_similar, assert_image_similar,
has_feature_version,
is_big_endian, is_big_endian,
skip_unless_feature, skip_unless_feature,
) )
@ -53,10 +53,7 @@ def test_write_animation_L(tmp_path: Path) -> None:
im.load() im.load()
assert_image_similar(im, orig.convert("RGBA"), 32.9) assert_image_similar(im, orig.convert("RGBA"), 32.9)
if is_big_endian(): if is_big_endian() and not has_feature_version("webp", "1.2.2"):
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") pytest.skip("Fails with libwebp earlier than 1.2.2")
orig.seek(orig.n_frames - 1) orig.seek(orig.n_frames - 1)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
@ -81,10 +78,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
assert_image_equal(im, frame1.convert("RGBA")) assert_image_equal(im, frame1.convert("RGBA"))
# Compare second frame to original # Compare second frame to original
if is_big_endian(): if is_big_endian() and not has_feature_version("webp", "1.2.2"):
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") pytest.skip("Fails with libwebp earlier than 1.2.2")
im.seek(1) im.seek(1)
im.load() im.load()

View File

@ -9,7 +9,8 @@ from .helper import skip_unless_feature
class TestFontCrash: class TestFontCrash:
def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None:
# from fuzzers.fuzz_font # Copy of the code from fuzz_font() in Tests/oss-fuzz/fuzzers.py
# that triggered a problem when fuzzing
font.getbbox("ABC") font.getbbox("ABC")
font.getmask("test text") font.getmask("test text")
with Image.new(mode="RGBA", size=(200, 200)) as im: with Image.new(mode="RGBA", size=(200, 200)) as im:

View File

@ -19,6 +19,7 @@ from PIL import (
ImageDraw, ImageDraw,
ImageFile, ImageFile,
ImagePalette, ImagePalette,
ImageShow,
UnidentifiedImageError, UnidentifiedImageError,
features, features,
) )
@ -283,33 +284,6 @@ class TestImage:
assert item is not None assert item is not None
assert item != num assert item != num
def test_expand_x(self) -> None:
# Arrange
im = hopper()
orig_size = im.size
xmargin = 5
# Act
im = im._expand(xmargin)
# Assert
assert im.size[0] == orig_size[0] + 2 * xmargin
assert im.size[1] == orig_size[1] + 2 * xmargin
def test_expand_xy(self) -> None:
# Arrange
im = hopper()
orig_size = im.size
xmargin = 5
ymargin = 3
# Act
im = im._expand(xmargin, ymargin)
# Assert
assert im.size[0] == orig_size[0] + 2 * xmargin
assert im.size[1] == orig_size[1] + 2 * ymargin
def test_getbands(self) -> None: def test_getbands(self) -> None:
# Assert # Assert
assert hopper("RGB").getbands() == ("R", "G", "B") assert hopper("RGB").getbands() == ("R", "G", "B")
@ -388,6 +362,37 @@ class TestImage:
assert img_colors is not None assert img_colors is not None
assert sorted(img_colors) == expected_colors 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: def test_alpha_inplace(self) -> None:
src = Image.new("RGBA", (128, 128), "blue") src = Image.new("RGBA", (128, 128), "blue")
@ -922,6 +927,17 @@ class TestImage:
reloaded_exif.load(exif.tobytes()) reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) 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: def test_exif_load_from_fp(self) -> None:
with Image.open("Tests/images/flower.jpg") as im: with Image.open("Tests/images/flower.jpg") as im:
data = im.info["exif"] data = im.info["exif"]
@ -1005,6 +1021,13 @@ class TestImage:
with pytest.warns(DeprecationWarning, match="Image.Image.get_child_images"): with pytest.warns(DeprecationWarning, match="Image.Image.get_child_images"):
assert im.get_child_images() == [] assert im.get_child_images() == []
def test_show(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageShow, "_viewers", [])
im = Image.new("RGB", (1, 1))
with pytest.warns(DeprecationWarning, match="Image._show"):
Image._show(im)
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size: tuple[int, int]) -> None: def test_zero_tobytes(self, size: tuple[int, int]) -> None:
im = Image.new("RGB", size) im = Image.new("RGB", size)

View File

@ -101,8 +101,7 @@ def test_fromarray_strides_without_tobytes() -> None:
self.__array_interface__ = arr_params self.__array_interface__ = arr_params
with pytest.raises(ValueError): with pytest.raises(ValueError):
wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1), "typestr": "|u1"})
with pytest.warns(DeprecationWarning, match="'mode' parameter"):
Image.fromarray(wrapped, "L") Image.fromarray(wrapped, "L")
@ -112,9 +111,16 @@ def test_fromarray_palette() -> None:
a = numpy.array(i) a = numpy.array(i)
# Act # Act
with pytest.warns(DeprecationWarning, match="'mode' parameter"):
out = Image.fromarray(a, "P") out = Image.fromarray(a, "P")
# Assert that the Python and C palettes match # Assert that the Python and C palettes match
assert out.palette is not None assert out.palette is not None
assert len(out.palette.colors) == len(out.im.getpalette()) / 3 assert len(out.palette.colors) == len(out.im.getpalette()) / 3
def test_deprecation() -> None:
a = numpy.array(im.convert("L"))
with pytest.warns(
DeprecationWarning, match="'mode' parameter for changing data types"
):
Image.fromarray(a, "1")

View File

@ -97,6 +97,13 @@ def test_opaque() -> None:
assert_image_equal(alpha, solid) assert_image_equal(alpha, solid)
def test_rgba() -> None:
with Image.open("Tests/images/transparent.png") as im:
assert im.mode == "RGBA"
assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5)
def test_rgba_p() -> None: def test_rgba_p() -> None:
im = hopper("RGBA") im = hopper("RGBA")
im.putalpha(hopper("L")) im.putalpha(hopper("L"))
@ -114,11 +121,12 @@ def test_rgba_pa() -> None:
assert_image_similar(im, expected, 9.3) assert_image_similar(im, expected, 9.3)
def test_rgba() -> None: def test_pa() -> None:
with Image.open("Tests/images/transparent.png") as im: im = hopper().convert("PA")
assert im.mode == "RGBA"
assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5) palette = im.palette
assert palette is not None
assert palette.colors != {}
def test_trns_p(tmp_path: Path) -> None: def test_trns_p(tmp_path: Path) -> None:

View File

@ -1,11 +1,16 @@
from __future__ import annotations from __future__ import annotations
import pytest 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: def test_sanity() -> None:
@ -23,10 +28,7 @@ def test_sanity() -> None:
@skip_unless_feature("libimagequant") @skip_unless_feature("libimagequant")
def test_libimagequant_quantize() -> None: def test_libimagequant_quantize() -> None:
image = hopper() image = hopper()
if is_ppc64le(): if is_ppc64le() and not has_feature_version("libimagequant", "4"):
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") pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
assert converted.mode == "P" assert converted.mode == "P"

View File

@ -1494,7 +1494,9 @@ def test_default_font_size() -> None:
def draw_text() -> None: def draw_text() -> None:
draw.text((0, 0), text, font_size=16) draw.text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png") assert_image_similar_tofile(
im, "Tests/images/imagedraw_default_font_size.png", 1
)
check(draw_text) check(draw_text)
@ -1513,7 +1515,9 @@ def test_default_font_size() -> None:
def draw_multiline_text() -> None: def draw_multiline_text() -> None:
draw.multiline_text((0, 0), text, font_size=16) draw.multiline_text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png") assert_image_similar_tofile(
im, "Tests/images/imagedraw_default_font_size.png", 1
)
check(draw_multiline_text) check(draw_multiline_text)

View File

@ -19,6 +19,7 @@ from .helper import (
assert_image_equal, assert_image_equal,
assert_image_equal_tofile, assert_image_equal_tofile,
assert_image_similar_tofile, assert_image_similar_tofile,
has_feature_version,
is_win32, is_win32,
skip_unless_feature, skip_unless_feature,
skip_unless_feature_version, skip_unless_feature_version,
@ -492,6 +493,11 @@ def test_stroke_mask() -> None:
assert mask.getpixel((42, 5)) == 255 assert mask.getpixel((42, 5)) == 255
def test_load_invalid_file() -> None:
with pytest.raises(SyntaxError, match="Not a PILfont file"):
ImageFont.load("Tests/images/1_trns.png")
def test_load_when_image_not_found() -> None: def test_load_when_image_not_found() -> None:
with tempfile.NamedTemporaryFile(delete=False) as tmp: with tempfile.NamedTemporaryFile(delete=False) as tmp:
pass pass
@ -549,7 +555,7 @@ def test_default_font() -> None:
draw.text((10, 60), txt, font=larger_default_font) draw.text((10, 60), txt, font=larger_default_font)
# Assert # Assert
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png") assert_image_similar_tofile(im, "Tests/images/default_font_freetype.png", 0.13)
@pytest.mark.parametrize("mode", ("", "1", "RGBA")) @pytest.mark.parametrize("mode", ("", "1", "RGBA"))
@ -1055,7 +1061,10 @@ def test_colr(layout_engine: ImageFont.Layout) -> None:
d.text((15, 5), "Bungee", font=font, embedded_color=True) d.text((15, 5), "Bungee", font=font, embedded_color=True)
assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21) if has_feature_version("freetype2", "2.14.0"):
assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 6.1)
else:
assert_image_similar_tofile(im, "Tests/images/colr_bungee_older.png", 21)
@skip_unless_feature_version("freetype2", "2.10.0") @skip_unless_feature_version("freetype2", "2.10.0")
@ -1071,7 +1080,7 @@ def test_colr_mask(layout_engine: ImageFont.Layout) -> None:
d.text((15, 5), "Bungee", "black", font=font) d.text((15, 5), "Bungee", "black", font=font)
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 14.1)
def test_woff2(layout_engine: ImageFont.Layout) -> None: def test_woff2(layout_engine: ImageFont.Layout) -> None:

View File

@ -7,6 +7,7 @@ from PIL import Image, ImageDraw, ImageFont
from .helper import ( from .helper import (
assert_image_equal_tofile, assert_image_equal_tofile,
assert_image_similar_tofile, assert_image_similar_tofile,
has_feature_version,
skip_unless_feature, skip_unless_feature,
) )
@ -104,11 +105,9 @@ def test_text_direction_ttb() -> None:
im = Image.new(mode="RGB", size=(100, 300)) im = Image.new(mode="RGB", size=(100, 300))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
try: if not has_feature_version("raqm", "0.7"):
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") 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" target = "Tests/images/test_direction_ttb.png"
assert_image_similar_tofile(im, target, 2.8) assert_image_similar_tofile(im, target, 2.8)
@ -119,7 +118,8 @@ def test_text_direction_ttb_stroke() -> None:
im = Image.new(mode="RGB", size=(100, 300)) im = Image.new(mode="RGB", size=(100, 300))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
try: if not has_feature_version("raqm", "0.7"):
pytest.skip("libraqm 0.7 or greater not available")
draw.text( draw.text(
(27, 27), (27, 27),
"あい", "あい",
@ -129,9 +129,6 @@ def test_text_direction_ttb_stroke() -> None:
stroke_width=2, stroke_width=2,
stroke_fill="#0f0", 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")
target = "Tests/images/test_direction_ttb_stroke.png" target = "Tests/images/test_direction_ttb_stroke.png"
assert_image_similar_tofile(im, target, 19.4) assert_image_similar_tofile(im, target, 19.4)
@ -186,7 +183,7 @@ def test_x_max_and_y_offset() -> None:
draw.text((0, 0), "لح", font=ttf, fill=500) draw.text((0, 0), "لح", font=ttf, fill=500)
target = "Tests/images/test_x_max_and_y_offset.png" target = "Tests/images/test_x_max_and_y_offset.png"
assert_image_similar_tofile(im, target, 0.5) assert_image_similar_tofile(im, target, 3.8)
def test_language() -> None: def test_language() -> None:
@ -219,14 +216,9 @@ def test_getlength(
im = Image.new(mode, (1, 1), 0) im = Image.new(mode, (1, 1), 0)
d = ImageDraw.Draw(im) d = ImageDraw.Draw(im)
try: if direction == "ttb" and not has_feature_version("raqm", "0.7"):
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") pytest.skip("libraqm 0.7 or greater not available")
assert d.textlength(text, ttf, direction) == expected
@pytest.mark.parametrize("mode", ("L", "1")) @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) ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
try: 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) target = ttf.getlength("ii", mode, direction)
actual = ttf.getlength(text, mode, direction) actual = ttf.getlength(text, mode, direction)
assert actual == target 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")
@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm"))
@ -265,11 +252,9 @@ def test_anchor_ttb(anchor: str) -> None:
d = ImageDraw.Draw(im) d = ImageDraw.Draw(im)
d.line(((0, 200), (200, 200)), "gray") d.line(((0, 200), (200, 200)), "gray")
d.line(((100, 0), (100, 400)), "gray") d.line(((100, 0), (100, 400)), "gray")
try: if not has_feature_version("raqm", "0.7"):
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") 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 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 # this tests various combining characters for anchor alignment and clipping
@pytest.mark.parametrize( @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( 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: ) -> None:
path = f"Tests/images/test_combine_{name}.png" path = f"Tests/images/test_combine_{name}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
@ -322,11 +309,9 @@ def test_combine(
d = ImageDraw.Draw(im) d = ImageDraw.Draw(im)
d.line(((0, 200), (400, 200)), "gray") d.line(((0, 200), (400, 200)), "gray")
d.line(((200, 0), (200, 400)), "gray") d.line(((200, 0), (200, 400)), "gray")
try: if direction == "ttb" and not has_feature_version("raqm", "0.7"):
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") 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) assert_image_similar_tofile(im, path, epsilon)

View File

@ -30,6 +30,14 @@ def test_default_font(font: ImageFont.ImageFont) -> None:
assert_image_equal_tofile(im, "Tests/images/default_font.png") assert_image_equal_tofile(im, "Tests/images/default_font.png")
def test_invalid_mode() -> None:
font = ImageFont.ImageFont()
fp = BytesIO()
with Image.open("Tests/images/hopper.png") as im:
with pytest.raises(TypeError, match="invalid font image mode"):
font._load_pilfont_data(fp, im)
def test_without_freetype() -> None: def test_without_freetype() -> None:
original_core = ImageFont.core original_core = ImageFont.core
if features.check_module("freetype2"): if features.check_module("freetype2"):

View File

@ -7,7 +7,7 @@ import pytest
from PIL import Image, ImageMorph, _imagingmorph 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: def string_to_img(image_string: str) -> Image.Image:
@ -266,16 +266,18 @@ def test_unknown_pattern() -> None:
ImageMorph.LutBuilder(op_name="unknown") 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 # Arrange
lb = ImageMorph.LutBuilder(op_name="corner") lb = ImageMorph.LutBuilder(op_name="corner")
new_patterns = ["a pattern with a syntax error"] new_patterns = [pattern]
lb.add_patterns(new_patterns) lb.add_patterns(new_patterns)
# Act / Assert # Act / Assert
with pytest.raises( with pytest.raises(Exception, match='Syntax error in pattern "'):
Exception, match='Syntax error in pattern "a pattern with a syntax error"'
):
lb.build_lut() lb.build_lut()

View File

@ -59,15 +59,12 @@ def test_show(mode: str) -> None:
assert ImageShow.show(im) assert ImageShow.show(im)
def test_show_without_viewers() -> None: def test_show_without_viewers(monkeypatch: pytest.MonkeyPatch) -> None:
viewers = ImageShow._viewers monkeypatch.setattr(ImageShow, "_viewers", [])
ImageShow._viewers = []
with hopper() as im: with hopper() as im:
assert not ImageShow.show(im) assert not ImageShow.show(im)
ImageShow._viewers = viewers
@pytest.mark.parametrize( @pytest.mark.parametrize(
"viewer", "viewer",

View File

@ -57,3 +57,13 @@ def test_constant() -> None:
assert st.rms[0] == 128 assert st.rms[0] == 128
assert st.var[0] == 0 assert st.var[0] == 0
assert st.stddev[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 = numpy.array(data, dtype=dtype)
a.shape = TEST_IMAGE_SIZE a.shape = TEST_IMAGE_SIZE
i = Image.fromarray(a) i = Image.fromarray(a)
if list(i.getdata()) != data: assert list(i.getdata()) == data
print("data mismatch for", dtype)
else: else:
data = list(range(100)) data = list(range(100))
a = numpy.array([[x] * bands for x in data], dtype=dtype) a = numpy.array([[x] * bands for x in data], dtype=dtype)
a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands
i = Image.fromarray(a) i = Image.fromarray(a)
if list(i.getchannel(0).getdata()) != list(range(100)): assert list(i.getchannel(0).getdata()) == list(range(100))
print("data mismatch for", dtype)
return i return i
# Check supported 1-bit integer formats # Check supported 1-bit integer formats

View File

@ -9,7 +9,7 @@ from PIL import __version__
pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed")
def map_metadata_keys(metadata): def map_metadata_keys(md):
# Convert installed wheel metadata into canonical Core Metadata 2.4 format. # Convert installed wheel metadata into canonical Core Metadata 2.4 format.
# This was a utility method in pyroma 4.3.3; it was removed in 5.0. # This was a utility method in pyroma 4.3.3; it was removed in 5.0.
# This implementation is constructed from the relevant logic from # This implementation is constructed from the relevant logic from
@ -17,8 +17,8 @@ def map_metadata_keys(metadata):
# upstream to Pyroma as https://github.com/regebro/pyroma/pull/116, # upstream to Pyroma as https://github.com/regebro/pyroma/pull/116,
# so it may be possible to simplify this test in future. # so it may be possible to simplify this test in future.
data = {} data = {}
for key in set(metadata.keys()): for key in set(md.keys()):
value = metadata.get_all(key) value = md.get_all(key)
key = pyroma.projectdata.normalize(key) key = pyroma.projectdata.normalize(key)
if len(value) == 1: if len(value) == 1:

View File

@ -4,7 +4,6 @@ import platform
import sys import sys
from PIL import features from PIL import features
from Tests.helper import is_pypy
def test_wheel_modules() -> None: def test_wheel_modules() -> None:
@ -48,8 +47,6 @@ def test_wheel_features() -> None:
if sys.platform == "win32": if sys.platform == "win32":
expected_features.remove("xcb") expected_features.remove("xcb")
elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm":
expected_features.remove("zlib_ng")
elif sys.platform == "ios": elif sys.platform == "ios":
# Can't distribute raqm due to licensing, and there's no system version; # Can't distribute raqm due to licensing, and there's no system version;
# fribidi and harfbuzz won't be available if raqm isn't available. # fribidi and harfbuzz won't be available if raqm isn't available.

View File

@ -59,6 +59,6 @@ cmake \
"${LIBAVIF_CMAKE_FLAGS[@]}" \ "${LIBAVIF_CMAKE_FLAGS[@]}" \
. .
sudo make install make install
popd popd

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install openjpeg # install openjpeg
archive=openjpeg-2.5.3 archive=openjpeg-2.5.4
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -2,12 +2,12 @@
# install raqm # install raqm
archive=libraqm-0.10.2 archive=libraqm-0.10.3
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
pushd $archive pushd $archive
meson build --prefix=/usr && sudo ninja -C build install meson build --prefix=/usr && ninja -C build install
popd popd

View File

@ -35,8 +35,12 @@ Image.fromarray mode parameter
.. deprecated:: 11.3.0 .. deprecated:: 11.3.0
The ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` has been deprecated. The Using the ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` was deprecated in
mode can be automatically determined from the object's shape and type instead. Pillow 11.3.0. In Pillow 12.0.0, this was partially reverted, and it is now only
deprecated when changing data types. Since pixel values do not contain information
about palettes or color spaces, the parameter can still be used to place grayscale L
mode data within a P mode image, or read RGB data as YCbCr for example. If omitted, the
mode will be automatically determined from the object's shape and type.
Saving I mode images as PNG Saving I mode images as PNG
^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -61,6 +65,14 @@ ImageCms.ImageCmsProfile.product_name and .product_info
``.product_info`` attributes have been deprecated, and will be removed in ``.product_info`` attributes have been deprecated, and will be removed in
Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0. Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0.
Image._show
~~~~~~~~~~~
.. deprecated:: 12.0.0
``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15).
Use :py:meth:`~PIL.ImageShow.show` instead.
Removed features Removed features
---------------- ----------------

View File

@ -44,7 +44,7 @@ Many of Pillow's features require external libraries:
* **libtiff** provides compressed TIFF functionality * **libtiff** provides compressed TIFF functionality
* Pillow has been tested with libtiff versions **4.0-4.7.0** * Pillow has been tested with libtiff versions **4.0-4.7.1**
* **libfreetype** provides type related services * **libfreetype** provides type related services
@ -58,7 +58,7 @@ Many of Pillow's features require external libraries:
* **openjpeg** provides JPEG 2000 functionality. * **openjpeg** provides JPEG 2000 functionality.
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
**2.4.0**, **2.5.0**, **2.5.2** and **2.5.3**. **2.4.0**, **2.5.0**, **2.5.2**, **2.5.3** and **2.5.4**.
* Pillow does **not** support the earlier **1.5** series which ships * Pillow does **not** support the earlier **1.5** series which ships
with Debian Jessie. with Debian Jessie.

View File

@ -41,7 +41,7 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| macOS 13 Ventura | 3.10 | x86-64 | | 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 | | | | PyPy3 | |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
@ -75,6 +75,8 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested | | Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors | | | | versions | | Pillow version | | processors |
+==================================+============================+==================+==============+ +==================================+============================+==================+==============+
| macOS 26 Tahoe | 3.9, 3.10, 3.11, 3.12, 3.13| 11.3.0 |arm |
+----------------------------------+----------------------------+------------------+--------------+
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.3.0 |arm | | macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.3.0 |arm |
| +----------------------------+------------------+ | | +----------------------------+------------------+ |
| | 3.8 | 10.4.0 | | | | 3.8 | 10.4.0 | |

View File

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

View File

@ -20,7 +20,9 @@ or the clipboard to a PIL image memory.
used as a fallback if they are installed. To disable this behaviour, pass used as a fallback if they are installed. To disable this behaviour, pass
``xdisplay=""`` instead. ``xdisplay=""`` instead.
.. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) .. versionadded:: 1.1.3 Windows support
.. versionadded:: 3.0.0 macOS support
.. versionadded:: 7.1.0 Linux support
:param bbox: What region to copy. Default is the entire screen. :param bbox: What region to copy. Default is the entire screen.
On macOS, this is not increased to 2x for Retina screens, so the full On macOS, this is not increased to 2x for Retina screens, so the full
@ -53,7 +55,9 @@ or the clipboard to a PIL image memory.
On Linux, ``wl-paste`` or ``xclip`` is required. On Linux, ``wl-paste`` or ``xclip`` is required.
.. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS), 9.4.0 (Linux) .. versionadded:: 1.1.4 Windows support
.. versionadded:: 3.3.0 macOS support
.. versionadded:: 9.4.0 Linux support
:return: On Windows, an image, a list of filenames, :return: On Windows, an image, a list of filenames,
or None if the clipboard does not contain image data or filenames. or None if the clipboard does not contain image data or filenames.

View File

@ -29,6 +29,13 @@ Image.fromarray mode parameter
The ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` has been deprecated. The The ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` has been deprecated. The
mode can be automatically determined from the object's shape and type instead. mode can be automatically determined from the object's shape and type instead.
.. note::
Since pixel values do not contain information about palettes or color spaces, part
of this functionality was restored in Pillow 12.0.0. The parameter can be used to
place grayscale L mode data within a P mode image, or read RGB data as YCbCr for
example.
Saving I mode images as PNG Saving I mode images as PNG
^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -116,6 +116,12 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).
Deprecations Deprecations
============ ============
Image._show
^^^^^^^^^^^
``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15).
Use :py:meth:`~PIL.ImageShow.show` instead.
ImageCms.ImageCmsProfile.product_name and .product_info ImageCms.ImageCmsProfile.product_name and .product_info
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -150,3 +156,19 @@ others prepare for 3.14, and to ensure Pillow could be used immediately at the r
of 3.14.0 final (2025-10-07, :pep:`745`). of 3.14.0 final (2025-10-07, :pep:`745`).
Pillow 12.0.0 now officially supports Python 3.14. Pillow 12.0.0 now officially supports Python 3.14.
Image.fromarray mode parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In Pillow 11.3.0, the ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` was
deprecated. Part of this functionality has been restored in Pillow 12.0.0. Since pixel
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 <https://github.com/uko3211>`__.

View File

@ -30,7 +30,7 @@ from ._util import DeferredError
def _accept(prefix: bytes) -> bool: def _accept(prefix: bytes) -> bool:
return ( return (
len(prefix) >= 6 len(prefix) >= 16
and i16(prefix, 4) in [0xAF11, 0xAF12] and i16(prefix, 4) in [0xAF11, 0xAF12]
and i16(prefix, 14) in [0, 3] # flags and i16(prefix, 14) in [0, 3] # flags
) )
@ -49,7 +49,12 @@ class FliImageFile(ImageFile.ImageFile):
def _open(self) -> None: def _open(self) -> None:
# HEAD # HEAD
s = self.fp.read(128) s = self.fp.read(128)
if not (_accept(s) and s[20:22] == b"\x00\x00"): if not (
_accept(s)
and s[20:22] == b"\x00" * 2
and s[42:80] == b"\x00" * 38
and s[88:] == b"\x00" * 40
):
msg = "not an FLI/FLC file" msg = "not an FLI/FLC file"
raise SyntaxError(msg) raise SyntaxError(msg)

View File

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

View File

@ -103,7 +103,6 @@ try:
raise ImportError(msg) raise ImportError(msg)
except ImportError as v: 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 # Explanations for ways that we know we might have an import error
if str(v).startswith("Module use of python"): if str(v).startswith("Module use of python"):
# The _imaging C module is present, but not compiled for # The _imaging C module is present, but not compiled for
@ -1149,7 +1148,7 @@ class Image:
raise ValueError(msg) from e raise ValueError(msg) from e
new_im = self._new(im) new_im = self._new(im)
if mode == "P" and palette != Palette.ADAPTIVE: if mode in ("P", "PA") and palette != Palette.ADAPTIVE:
from . import ImagePalette from . import ImagePalette
new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB")) new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB"))
@ -1343,12 +1342,6 @@ class Image:
""" """
pass pass
def _expand(self, xmargin: int, ymargin: int | None = None) -> Image:
if ymargin is None:
ymargin = xmargin
self.load()
return self._new(self.im.expand(xmargin, ymargin))
def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image:
""" """
Filters this image using the given filter. For a list of Filters this image using the given filter. For a list of
@ -2639,7 +2632,9 @@ class Image:
:param title: Optional title to use for the image window, where possible. :param title: Optional title to use for the image window, where possible.
""" """
_show(self, title=title) from . import ImageShow
ImageShow.show(self, title)
def split(self) -> tuple[Image, ...]: def split(self) -> tuple[Image, ...]:
""" """
@ -3264,19 +3259,10 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
transferred. This means that P and PA mode images will lose their palette. transferred. This means that P and PA mode images will lose their palette.
:param obj: Object with array interface :param obj: Object with array interface
:param mode: Optional mode to use when reading ``obj``. Will be determined from :param mode: Optional mode to use when reading ``obj``. Since pixel values do not
type if ``None``. Deprecated. contain information about palettes or color spaces, this can be used to place
grayscale L mode data within a P mode image, or read RGB data as YCbCr for
This will not be used to convert the data after reading, but will be used to example.
change how the data is read::
from PIL import Image
import numpy as np
a = np.full((1, 1), 300)
im = Image.fromarray(a, mode="L")
im.getpixel((0, 0)) # 44
im = Image.fromarray(a, mode="RGB")
im.getpixel((0, 0)) # (44, 1, 0)
See: :ref:`concept-modes` for general information about modes. See: :ref:`concept-modes` for general information about modes.
:returns: An image object. :returns: An image object.
@ -3287,21 +3273,28 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
shape = arr["shape"] shape = arr["shape"]
ndim = len(shape) ndim = len(shape)
strides = arr.get("strides", None) strides = arr.get("strides", None)
if mode is None:
try: try:
typekey = (1, 1) + shape[2:], arr["typestr"] typekey = (1, 1) + shape[2:], arr["typestr"]
except KeyError as e: except KeyError as e:
if mode is not None:
typekey = None
color_modes: list[str] = []
else:
msg = "Cannot handle this data type" msg = "Cannot handle this data type"
raise TypeError(msg) from e raise TypeError(msg) from e
if typekey is not None:
try: try:
mode, rawmode = _fromarray_typemap[typekey] typemode, rawmode, color_modes = _fromarray_typemap[typekey]
except KeyError as e: except KeyError as e:
typekey_shape, typestr = typekey typekey_shape, typestr = typekey
msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" msg = f"Cannot handle this data type: {typekey_shape}, {typestr}"
raise TypeError(msg) from e raise TypeError(msg) from e
else: if mode is not None:
deprecate("'mode' parameter", 13) if mode != typemode and mode not in color_modes:
deprecate("'mode' parameter for changing data types", 13)
rawmode = mode rawmode = mode
else:
mode = typemode
if mode in ["1", "L", "I", "P", "F"]: if mode in ["1", "L", "I", "P", "F"]:
ndmax = 2 ndmax = 2
elif mode == "RGB": elif mode == "RGB":
@ -3398,29 +3391,29 @@ def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile:
_fromarray_typemap = { _fromarray_typemap = {
# (shape, typestr) => mode, rawmode # (shape, typestr) => mode, rawmode, color modes
# first two members of shape are set to one # first two members of shape are set to one
((1, 1), "|b1"): ("1", "1;8"), ((1, 1), "|b1"): ("1", "1;8", []),
((1, 1), "|u1"): ("L", "L"), ((1, 1), "|u1"): ("L", "L", ["P"]),
((1, 1), "|i1"): ("I", "I;8"), ((1, 1), "|i1"): ("I", "I;8", []),
((1, 1), "<u2"): ("I", "I;16"), ((1, 1), "<u2"): ("I", "I;16", []),
((1, 1), ">u2"): ("I", "I;16B"), ((1, 1), ">u2"): ("I", "I;16B", []),
((1, 1), "<i2"): ("I", "I;16S"), ((1, 1), "<i2"): ("I", "I;16S", []),
((1, 1), ">i2"): ("I", "I;16BS"), ((1, 1), ">i2"): ("I", "I;16BS", []),
((1, 1), "<u4"): ("I", "I;32"), ((1, 1), "<u4"): ("I", "I;32", []),
((1, 1), ">u4"): ("I", "I;32B"), ((1, 1), ">u4"): ("I", "I;32B", []),
((1, 1), "<i4"): ("I", "I;32S"), ((1, 1), "<i4"): ("I", "I;32S", []),
((1, 1), ">i4"): ("I", "I;32BS"), ((1, 1), ">i4"): ("I", "I;32BS", []),
((1, 1), "<f4"): ("F", "F;32F"), ((1, 1), "<f4"): ("F", "F;32F", []),
((1, 1), ">f4"): ("F", "F;32BF"), ((1, 1), ">f4"): ("F", "F;32BF", []),
((1, 1), "<f8"): ("F", "F;64F"), ((1, 1), "<f8"): ("F", "F;64F", []),
((1, 1), ">f8"): ("F", "F;64BF"), ((1, 1), ">f8"): ("F", "F;64BF", []),
((1, 1, 2), "|u1"): ("LA", "LA"), ((1, 1, 2), "|u1"): ("LA", "LA", ["La", "PA"]),
((1, 1, 3), "|u1"): ("RGB", "RGB"), ((1, 1, 3), "|u1"): ("RGB", "RGB", ["YCbCr", "LAB", "HSV"]),
((1, 1, 4), "|u1"): ("RGBA", "RGBA"), ((1, 1, 4), "|u1"): ("RGBA", "RGBA", ["RGBa", "RGBX", "CMYK"]),
# shortcuts: # shortcuts:
((1, 1), f"{_ENDIAN}i4"): ("I", "I"), ((1, 1), f"{_ENDIAN}i4"): ("I", "I", []),
((1, 1), f"{_ENDIAN}f4"): ("F", "F"), ((1, 1), f"{_ENDIAN}f4"): ("F", "F", []),
} }
@ -3577,9 +3570,8 @@ def alpha_composite(im1: Image, im2: Image) -> Image:
""" """
Alpha composite im2 over im1. Alpha composite im2 over im1.
:param im1: The first image. Must have mode RGBA. :param im1: The first image. Must have mode RGBA or LA.
:param im2: The second image. Must have mode RGBA, and the same size as :param im2: The second image. Must have the same mode and size as the first image.
the first image.
:returns: An :py:class:`~PIL.Image.Image` object. :returns: An :py:class:`~PIL.Image.Image` object.
""" """
@ -3805,6 +3797,7 @@ def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None:
def _show(image: Image, **options: Any) -> None: def _show(image: Image, **options: Any) -> None:
from . import ImageShow from . import ImageShow
deprecate("Image._show", 13, "ImageShow.show")
ImageShow.show(image, **options) ImageShow.show(image, **options)
@ -4226,6 +4219,8 @@ class Exif(_ExifBase):
del self._info[tag] del self._info[tag]
else: else:
del self._data[tag] del self._data[tag]
if tag in self._ifds:
del self._ifds[tag]
def __iter__(self) -> Iterator[int]: def __iter__(self) -> Iterator[int]:
keys = set(self._data) keys = set(self._data)

View File

@ -46,6 +46,18 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MAXBLOCK = 65536 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 SAFEBLOCK = 1024 * 1024

View File

@ -125,11 +125,16 @@ class ImageFont:
image.close() image.close()
def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None:
# check image
if image.mode not in ("1", "L"):
msg = "invalid font image mode"
raise TypeError(msg)
# read PILfont header # read PILfont header
if file.readline() != b"PILfont\n": if file.read(8) != b"PILfont\n":
msg = "Not a PILfont file" msg = "Not a PILfont file"
raise SyntaxError(msg) raise SyntaxError(msg)
file.readline().split(b";") file.readline()
self.info = [] # FIXME: should be a dictionary self.info = [] # FIXME: should be a dictionary
while True: while True:
s = file.readline() s = file.readline()
@ -140,11 +145,6 @@ class ImageFont:
# read PILfont metrics # read PILfont metrics
data = file.read(256 * 20) data = file.read(256 * 20)
# check image
if image.mode not in ("1", "L"):
msg = "invalid font image mode"
raise TypeError(msg)
image.load() image.load()
self.font = Image.core.font(image.im, data) self.font = Image.core.font(image.im, data)
@ -671,11 +671,7 @@ class FreeTypeFont:
:returns: A list of the named styles in a variation font. :returns: A list of the named styles in a variation font.
:exception OSError: If the font is not a variation font. :exception OSError: If the font is not a variation font.
""" """
try:
names = self.font.getvarnames() names = self.font.getvarnames()
except AttributeError as e:
msg = "FreeType 2.9.1 or greater is required"
raise NotImplementedError(msg) from e
return [name.replace(b"\x00", b"") for name in names] return [name.replace(b"\x00", b"") for name in names]
def set_variation_by_name(self, name: str | bytes) -> None: 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. :returns: A list of the axes in a variation font.
:exception OSError: If the font is not a variation font. :exception OSError: If the font is not a variation font.
""" """
try:
axes = self.font.getvaraxes() axes = self.font.getvaraxes()
except AttributeError as e:
msg = "FreeType 2.9.1 or greater is required"
raise NotImplementedError(msg) from e
for axis in axes: for axis in axes:
if axis["name"]: if axis["name"]:
axis["name"] = axis["name"].replace(b"\x00", b"") axis["name"] = axis["name"].replace(b"\x00", b"")
@ -717,11 +709,7 @@ class FreeTypeFont:
:param axes: A list of values for each axis. :param axes: A list of values for each axis.
:exception OSError: If the font is not a variation font. :exception OSError: If the font is not a variation font.
""" """
try:
self.font.setvaraxes(axes) self.font.setvaraxes(axes)
except AttributeError as e:
msg = "FreeType 2.9.1 or greater is required"
raise NotImplementedError(msg) from e
class TransposedFont: class TransposedFont:

View File

@ -150,7 +150,7 @@ class LutBuilder:
# Parse and create symmetries of the patterns strings # Parse and create symmetries of the patterns strings
for p in self.patterns: 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: if not m:
msg = 'Syntax error in pattern "' + p + '"' msg = 'Syntax error in pattern "' + p + '"'
raise Exception(msg) raise Exception(msg)

View File

@ -120,7 +120,7 @@ class Stat:
@cached_property @cached_property
def mean(self) -> list[float]: def mean(self) -> list[float]:
"""Average (arithmetic mean) pixel level for each band in the image.""" """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 @cached_property
def median(self) -> list[int]: def median(self) -> list[int]:
@ -141,13 +141,20 @@ class Stat:
@cached_property @cached_property
def rms(self) -> list[float]: def rms(self) -> list[float]:
"""RMS (root-mean-square) for each band in the image.""" """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 @cached_property
def var(self) -> list[float]: def var(self) -> list[float]:
"""Variance for each band in the image.""" """Variance for each band in the image."""
return [ 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 for i in self.bands
] ]

View File

@ -34,10 +34,6 @@ def _i(c: bytes) -> int:
return i32((b"\0\0\0\0" + c)[-4:]) return i32((b"\0\0\0\0" + c)[-4:])
def _i8(c: int | bytes) -> int:
return c if isinstance(c, int) else c[0]
## ##
# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields # Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields
# from TIFF and JPEG files, use the <b>getiptcinfo</b> function. # from TIFF and JPEG files, use the <b>getiptcinfo</b> function.
@ -100,16 +96,18 @@ class IptcImageFile(ImageFile.ImageFile):
# mode # mode
layers = self.info[(3, 60)][0] layers = self.info[(3, 60)][0]
component = self.info[(3, 60)][1] component = self.info[(3, 60)][1]
if (3, 65) in self.info:
id = self.info[(3, 65)][0] - 1
else:
id = 0
if layers == 1 and not component: if layers == 1 and not component:
self._mode = "L" self._mode = "L"
elif layers == 3 and component: band = None
self._mode = "RGB"[id] else:
if layers == 3 and component:
self._mode = "RGB"
elif layers == 4 and component: elif layers == 4 and component:
self._mode = "CMYK"[id] self._mode = "CMYK"
if (3, 65) in self.info:
band = self.info[(3, 65)][0] - 1
else:
band = 0
# size # size
self._size = self.getint((3, 20)), self.getint((3, 30)) self._size = self.getint((3, 20)), self.getint((3, 30))
@ -124,16 +122,16 @@ class IptcImageFile(ImageFile.ImageFile):
# tile # tile
if tag == (8, 10): if tag == (8, 10):
self.tile = [ self.tile = [
ImageFile._Tile("iptc", (0, 0) + self.size, offset, compression) ImageFile._Tile("iptc", (0, 0) + self.size, offset, (compression, band))
] ]
def load(self) -> Image.core.PixelAccess | None: def load(self) -> Image.core.PixelAccess | None:
if len(self.tile) != 1 or self.tile[0][0] != "iptc": if self.tile:
return ImageFile.ImageFile.load(self) args = self.tile[0].args
assert isinstance(args, tuple)
compression, band = args
offset, compression = self.tile[0][2:] self.fp.seek(self.tile[0].offset)
self.fp.seek(offset)
# Copy image data to temporary file # Copy image data to temporary file
o = BytesIO() o = BytesIO()
@ -153,10 +151,15 @@ class IptcImageFile(ImageFile.ImageFile):
size -= len(s) size -= len(s)
with Image.open(o) as _im: with Image.open(o) as _im:
if band is not None:
bands = [Image.new("L", _im.size)] * Image.getmodebands(self.mode)
bands[band] = _im
_im = Image.merge(self.mode, bands)
else:
_im.load() _im.load()
self.im = _im.im self.im = _im.im
self.tile = [] self.tile = []
return Image.Image.load(self) return ImageFile.ImageFile.load(self)
Image.register_open(IptcImageFile.format, IptcImageFile) Image.register_open(IptcImageFile.format, IptcImageFile)

View File

@ -32,7 +32,7 @@ class PcdImageFile(ImageFile.ImageFile):
assert self.fp is not None assert self.fp is not None
self.fp.seek(2048) self.fp.seek(2048)
s = self.fp.read(2048) s = self.fp.read(1539)
if not s.startswith(b"PCD_"): if not s.startswith(b"PCD_"):
msg = "not a PCD file" msg = "not a PCD file"
@ -46,14 +46,13 @@ class PcdImageFile(ImageFile.ImageFile):
self.tile_post_rotate = -90 self.tile_post_rotate = -90
self._mode = "RGB" 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)] self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)]
def load_end(self) -> None: def load_end(self) -> None:
if self.tile_post_rotate: if self.tile_post_rotate:
# Handle rotated PCDs # Handle rotated PCDs
self.im = self.im.rotate(self.tile_post_rotate) 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: 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: 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

@ -49,8 +49,7 @@ class WalImageFile(ImageFile.ImageFile):
# strings are null-terminated # strings are null-terminated
self.info["name"] = header[:32].split(b"\0", 1)[0] self.info["name"] = header[:32].split(b"\0", 1)[0]
next_name = header[56 : 56 + 32].split(b"\0", 1)[0] if next_name := header[56 : 56 + 32].split(b"\0", 1)[0]:
if next_name:
self.info["next_name"] = next_name self.info["next_name"] = next_name
def load(self) -> Image.core.PixelAccess | None: def load(self) -> Image.core.PixelAccess | None:

View File

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

View File

@ -870,8 +870,6 @@ PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args) {
if (strcmp(format, "j2k") == 0) { if (strcmp(format, "j2k") == 0) {
codec_format = OPJ_CODEC_J2K; codec_format = OPJ_CODEC_J2K;
} else if (strcmp(format, "jpt") == 0) {
codec_format = OPJ_CODEC_JPT;
} else if (strcmp(format, "jp2") == 0) { } else if (strcmp(format, "jp2") == 0) {
codec_format = OPJ_CODEC_JP2; codec_format = OPJ_CODEC_JP2;
} else { } else {

View File

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

View File

@ -1,7 +1,7 @@
The MIT License (MIT) The MIT License (MIT)
Copyright © 2015 Information Technology Authority (ITA) <foss@ita.gov.om> Copyright © 2015 Information Technology Authority (ITA) <foss@ita.gov.om>
Copyright © 2016-2023 Khaled Hosny <khaled@aliftype.com> Copyright © 2016-2025 Khaled Hosny <khaled@aliftype.com>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,3 +1,19 @@
Overview of changes leading to 0.10.3
Tuesday, August 5, 2025
====================================
Fix raqm_set_text_utf8/utf16 reading beyond len for multibyte.
Support building against SheenBidi 2.9.
Fix deprecation warning with latest HarfBuzz.
Overview of changes leading to 0.10.2
Sunday, September 22, 2024
====================================
Fix Unicode codepoint conversion from UTF-16.
Overview of changes leading to 0.10.1 Overview of changes leading to 0.10.1
Wednesday, April 12, 2023 Wednesday, April 12, 2023
==================================== ====================================

View File

@ -33,9 +33,9 @@
#define RAQM_VERSION_MAJOR 0 #define RAQM_VERSION_MAJOR 0
#define RAQM_VERSION_MINOR 10 #define RAQM_VERSION_MINOR 10
#define RAQM_VERSION_MICRO 1 #define RAQM_VERSION_MICRO 3
#define RAQM_VERSION_STRING "0.10.1" #define RAQM_VERSION_STRING "0.10.3"
#define RAQM_VERSION_ATLEAST(major,minor,micro) \ #define RAQM_VERSION_ATLEAST(major,minor,micro) \
((major)*10000+(minor)*100+(micro) <= \ ((major)*10000+(minor)*100+(micro) <= \

View File

@ -30,7 +30,11 @@
#include <string.h> #include <string.h>
#ifdef RAQM_SHEENBIDI #ifdef RAQM_SHEENBIDI
#ifdef RAQM_SHEENBIDI_GT_2_9
#include <SheenBidi/SheenBidi.h>
#else
#include <SheenBidi.h> #include <SheenBidi.h>
#endif
#else #else
#ifdef HAVE_FRIBIDI_SYSTEM #ifdef HAVE_FRIBIDI_SYSTEM
#include <fribidi.h> #include <fribidi.h>
@ -546,34 +550,32 @@ raqm_set_text (raqm_t *rq,
return true; return true;
} }
static void * static const char *
_raqm_get_utf8_codepoint (const void *str, _raqm_get_utf8_codepoint (const char *str,
uint32_t *out_codepoint) uint32_t *out_codepoint)
{ {
const char *s = (const char *)str; if (0xf0 == (0xf8 & str[0]))
if (0xf0 == (0xf8 & s[0]))
{ {
*out_codepoint = ((0x07 & s[0]) << 18) | ((0x3f & s[1]) << 12) | ((0x3f & s[2]) << 6) | (0x3f & s[3]); *out_codepoint = ((0x07 & str[0]) << 18) | ((0x3f & str[1]) << 12) | ((0x3f & str[2]) << 6) | (0x3f & str[3]);
s += 4; str += 4;
} }
else if (0xe0 == (0xf0 & s[0])) else if (0xe0 == (0xf0 & str[0]))
{ {
*out_codepoint = ((0x0f & s[0]) << 12) | ((0x3f & s[1]) << 6) | (0x3f & s[2]); *out_codepoint = ((0x0f & str[0]) << 12) | ((0x3f & str[1]) << 6) | (0x3f & str[2]);
s += 3; str += 3;
} }
else if (0xc0 == (0xe0 & s[0])) else if (0xc0 == (0xe0 & str[0]))
{ {
*out_codepoint = ((0x1f & s[0]) << 6) | (0x3f & s[1]); *out_codepoint = ((0x1f & str[0]) << 6) | (0x3f & str[1]);
s += 2; str += 2;
} }
else else
{ {
*out_codepoint = s[0]; *out_codepoint = str[0];
s += 1; str += 1;
} }
return (void *)s; return str;
} }
static size_t static size_t
@ -585,42 +587,41 @@ _raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode)
while ((*in_utf8 != '\0') && (in_len < len)) while ((*in_utf8 != '\0') && (in_len < len))
{ {
in_utf8 = _raqm_get_utf8_codepoint (in_utf8, out_utf32); const char *out_utf8 = _raqm_get_utf8_codepoint (in_utf8, out_utf32);
in_len += out_utf8 - in_utf8;
in_utf8 = out_utf8;
++out_utf32; ++out_utf32;
++in_len;
} }
return (out_utf32 - unicode); return (out_utf32 - unicode);
} }
static void * static const uint16_t *
_raqm_get_utf16_codepoint (const void *str, _raqm_get_utf16_codepoint (const uint16_t *str,
uint32_t *out_codepoint) uint32_t *out_codepoint)
{ {
const uint16_t *s = (const uint16_t *)str; if (str[0] >= 0xD800 && str[0] <= 0xDBFF)
if (s[0] > 0xD800 && s[0] < 0xDBFF)
{ {
if (s[1] > 0xDC00 && s[1] < 0xDFFF) if (str[1] >= 0xDC00 && str[1] <= 0xDFFF)
{ {
uint32_t X = ((s[0] & ((1 << 6) -1)) << 10) | (s[1] & ((1 << 10) -1)); uint32_t X = ((str[0] & ((1 << 6) -1)) << 10) | (str[1] & ((1 << 10) -1));
uint32_t W = (s[0] >> 6) & ((1 << 5) - 1); uint32_t W = (str[0] >> 6) & ((1 << 5) - 1);
*out_codepoint = (W+1) << 16 | X; *out_codepoint = (W+1) << 16 | X;
s += 2; str += 2;
} }
else else
{ {
/* A single high surrogate, this is an error. */ /* A single high surrogate, this is an error. */
*out_codepoint = s[0]; *out_codepoint = str[0];
s += 1; str += 1;
} }
} }
else else
{ {
*out_codepoint = s[0]; *out_codepoint = str[0];
s += 1; str += 1;
} }
return (void *)s; return str;
} }
static size_t static size_t
@ -632,9 +633,10 @@ _raqm_u16_to_u32 (const uint16_t *text, size_t len, uint32_t *unicode)
while ((*in_utf16 != '\0') && (in_len < len)) while ((*in_utf16 != '\0') && (in_len < len))
{ {
in_utf16 = _raqm_get_utf16_codepoint (in_utf16, out_utf32); const uint16_t *out_utf16 = _raqm_get_utf16_codepoint (in_utf16, out_utf32);
in_len += (out_utf16 - in_utf16);
in_utf16 = out_utf16;
++out_utf32; ++out_utf32;
++in_len;
} }
return (out_utf32 - unicode); return (out_utf32 - unicode);
@ -1114,12 +1116,12 @@ _raqm_set_spacing (raqm_t *rq,
{ {
if (_raqm_allowed_grapheme_boundary (rq->text[i], rq->text[i+1])) if (_raqm_allowed_grapheme_boundary (rq->text[i], rq->text[i+1]))
{ {
/* CSS word seperators, word spacing is only applied on these.*/ /* CSS word separators, word spacing is only applied on these.*/
if (rq->text[i] == 0x0020 || /* Space */ if (rq->text[i] == 0x0020 || /* Space */
rq->text[i] == 0x00A0 || /* No Break Space */ rq->text[i] == 0x00A0 || /* No Break Space */
rq->text[i] == 0x1361 || /* Ethiopic Word Space */ rq->text[i] == 0x1361 || /* Ethiopic Word Space */
rq->text[i] == 0x10100 || /* Aegean Word Seperator Line */ rq->text[i] == 0x10100 || /* Aegean Word Separator Line */
rq->text[i] == 0x10101 || /* Aegean Word Seperator Dot */ rq->text[i] == 0x10101 || /* Aegean Word Separator Dot */
rq->text[i] == 0x1039F || /* Ugaric Word Divider */ rq->text[i] == 0x1039F || /* Ugaric Word Divider */
rq->text[i] == 0x1091F) /* Phoenician Word Separator */ rq->text[i] == 0x1091F) /* Phoenician Word Separator */
{ {
@ -2167,6 +2169,10 @@ _raqm_ft_transform (int *x,
*y = vector.y; *y = vector.y;
} }
#if !HB_VERSION_ATLEAST (10, 4, 0)
# define hb_ft_font_get_ft_face hb_ft_font_get_face
#endif
static bool static bool
_raqm_shape (raqm_t *rq) _raqm_shape (raqm_t *rq)
{ {
@ -2199,7 +2205,7 @@ _raqm_shape (raqm_t *rq)
hb_glyph_position_t *pos; hb_glyph_position_t *pos;
unsigned int len; unsigned int len;
FT_Get_Transform (hb_ft_font_get_face (run->font), &matrix, NULL); FT_Get_Transform (hb_ft_font_get_ft_face (run->font), &matrix, NULL);
pos = hb_buffer_get_glyph_positions (run->buffer, &len); pos = hb_buffer_get_glyph_positions (run->buffer, &len);
info = hb_buffer_get_glyph_infos (run->buffer, &len); info = hb_buffer_get_glyph_infos (run->buffer, &len);

View File

@ -0,0 +1,30 @@
BSD License
For Zstandard software
Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name Facebook, nor Meta, nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

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

View File

@ -16,7 +16,7 @@ For more extensive info, see the [Windows build instructions](build.rst).
Here's an example script to build on Windows: Here's an example script to build on Windows:
``` ```
set PYTHON=C:\Python39\bin set PYTHON=C:\Python310\bin
cd /D C:\Pillow\winbuild cd /D C:\Pillow\winbuild
%PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends %PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends
build\build_dep_all.cmd build\build_dep_all.cmd

View File

@ -115,7 +115,7 @@ Example
Here's an example script to build on Windows:: Here's an example script to build on Windows::
set PYTHON=C:\Python39\bin set PYTHON=C:\Python310\bin
cd /D C:\Pillow\winbuild cd /D C:\Pillow\winbuild
%PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends %PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends
build\build_dep_all.cmd build\build_dep_all.cmd

View File

@ -114,19 +114,19 @@ ARCHITECTURES = {
V = { V = {
"BROTLI": "1.1.0", "BROTLI": "1.1.0",
"FREETYPE": "2.13.3", "FREETYPE": "2.14.1",
"FRIBIDI": "1.0.16", "FRIBIDI": "1.0.16",
"HARFBUZZ": "11.3.3", "HARFBUZZ": "11.5.0",
"JPEGTURBO": "3.1.1", "JPEGTURBO": "3.1.2",
"LCMS2": "2.17", "LCMS2": "2.17",
"LIBAVIF": "1.3.0", "LIBAVIF": "1.3.0",
"LIBIMAGEQUANT": "4.4.0", "LIBIMAGEQUANT": "4.4.0",
"LIBPNG": "1.6.50", "LIBPNG": "1.6.50",
"LIBWEBP": "1.6.0", "LIBWEBP": "1.6.0",
"OPENJPEG": "2.5.3", "OPENJPEG": "2.5.4",
"TIFF": "4.7.0", "TIFF": "4.7.1",
"XZ": "5.8.1", "XZ": "5.8.1",
"ZLIBNG": "2.2.4", "ZLIBNG": "2.2.5",
} }
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
@ -228,12 +228,6 @@ DEPS: dict[str, dict[str, Any]] = {
# link against libwebp.lib # link against libwebp.lib
"#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501 "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501
}, },
r"test\CMakeLists.txt": {
"add_executable(test_write_read_tags ../placeholder.h)": "",
"target_sources(test_write_read_tags PRIVATE test_write_read_tags.c)": "", # noqa: E501
"target_link_libraries(test_write_read_tags PRIVATE tiff)": "",
"list(APPEND simple_tests test_write_read_tags)": "",
},
}, },
"build": [ "build": [
*cmds_cmake( *cmds_cmake(
@ -241,7 +235,6 @@ DEPS: dict[str, dict[str, Any]] = {
"-DBUILD_SHARED_LIBS:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF",
"-DWebP_LIBRARY=libwebp", "-DWebP_LIBRARY=libwebp",
'-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"',
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
) )
], ],
"headers": [r"libtiff\tiff*.h"], "headers": [r"libtiff\tiff*.h"],