Merge branch 'main' into fp

This commit is contained in:
Andrew Murray 2025-09-04 21:17:50 +10:00 committed by GitHub
commit cbb1e36bea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 423 additions and 150 deletions

View File

@ -96,13 +96,13 @@ ARCHIVE_SDIR=pillow-depends-main
FREETYPE_VERSION=2.13.3 FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=11.3.3 HARFBUZZ_VERSION=11.3.3
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.3
XZ_VERSION=5.8.1 XZ_VERSION=5.8.1
ZSTD_VERSION=1.5.7
TIFF_VERSION=4.7.0 TIFF_VERSION=4.7.0
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 +254,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_zlib_ng
build_new_zlib
else
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 +289,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

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

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

@ -373,8 +373,7 @@ 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:
# issue #1765 # issue #1765

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

@ -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,
) )
@ -388,6 +389,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 +954,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 +1048,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

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

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

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

@ -2,7 +2,7 @@
# 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

View File

@ -61,6 +61,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

@ -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,10 @@ 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.
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: 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
) )

View File

@ -55,7 +55,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):
@ -72,7 +72,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"
@ -81,8 +81,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

@ -2632,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, ...]:
""" """
@ -3570,9 +3572,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.
""" """
@ -3798,6 +3799,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)
@ -4219,6 +4221,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

@ -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.
@ -102,16 +98,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:
elif layers == 4 and component: if layers == 3 and component:
self._mode = "CMYK"[id] self._mode = "RGB"
elif layers == 4 and component:
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))
@ -126,40 +124,45 @@ 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:] assert self.fp is not None
self.fp.seek(self.tile[0].offset)
assert self.fp is not None # Copy image data to temporary file
self.fp.seek(offset) o = BytesIO()
if compression == "raw":
# Copy image data to temporary file # To simplify access to the extracted file,
o = BytesIO() # prepend a PPM header
if compression == "raw": o.write(b"P5\n%d %d\n255\n" % self.size)
# To simplify access to the extracted file, while True:
# prepend a PPM header type, size = self.field()
o.write(b"P5\n%d %d\n255\n" % self.size) if type != (8, 10):
while True:
type, size = self.field()
if type != (8, 10):
break
while size > 0:
s = self.fp.read(min(size, 8192))
if not s:
break break
o.write(s) while size > 0:
size -= len(s) s = self.fp.read(min(size, 8192))
if not s:
break
o.write(s)
size -= len(s)
with Image.open(o) as _im: with Image.open(o) as _im:
_im.load() if band is not None:
self.im = _im.im bands = [Image.new("L", _im.size)] * Image.getmodebands(self.mode)
self.tile = [] bands[band] = _im
return Image.Image.load(self) _im = Image.merge(self.mode, bands)
else:
_im.load()
self.im = _im.im
self.tile = []
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

@ -50,8 +50,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

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

@ -148,7 +148,7 @@ ImagingPaletteDelete(ImagingPalette palette) {
#define BOX 8 #define BOX 8
#define BOXVOLUME BOX *BOX *BOX #define BOXVOLUME BOX * BOX * BOX
void void
ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) { ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) {

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

@ -117,7 +117,7 @@ V = {
"FREETYPE": "2.13.3", "FREETYPE": "2.13.3",
"FRIBIDI": "1.0.16", "FRIBIDI": "1.0.16",
"HARFBUZZ": "11.3.3", "HARFBUZZ": "11.3.3",
"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",
@ -126,7 +126,7 @@ V = {
"OPENJPEG": "2.5.3", "OPENJPEG": "2.5.3",
"TIFF": "4.7.0", "TIFF": "4.7.0",
"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])