Merge branch 'main' into ppc64le

This commit is contained in:
Andrew Murray 2025-09-16 00:41:04 +10:00 committed by GitHub
commit dd9f90fd7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 373 additions and 189 deletions

View File

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

View File

@ -37,7 +37,7 @@ jobs:
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

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

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

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

View File

@ -70,7 +70,7 @@ jobs:
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

@ -94,15 +94,15 @@ ARCHIVE_SDIR=pillow-depends-main
# 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.
FREETYPE_VERSION=2.13.3 FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=11.3.3 HARFBUZZ_VERSION=11.4.5
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_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
@ -289,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

View File

@ -136,7 +136,7 @@ jobs:
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"
@ -193,7 +193,7 @@ jobs:
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"
@ -268,7 +268,7 @@ jobs:
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

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

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

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

@ -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:
@ -53,8 +52,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

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

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

@ -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, ...]:
""" """
@ -3257,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.
@ -3280,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":
@ -3391,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", []),
} }
@ -3797,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)

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

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

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

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

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

View File

@ -116,8 +116,8 @@ V = {
"BROTLI": "1.1.0", "BROTLI": "1.1.0",
"FREETYPE": "2.13.3", "FREETYPE": "2.13.3",
"FRIBIDI": "1.0.16", "FRIBIDI": "1.0.16",
"HARFBUZZ": "11.3.3", "HARFBUZZ": "11.4.5",
"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])