From f6f3dab6724d72a60ebf0244843a82418f7d7740 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Dec 2023 17:42:17 +1100 Subject: [PATCH 0001/2374] Import UnidentifiedImageError directly --- Tests/oss-fuzz/test_fuzzers.py | 4 ++-- Tests/test_file_eps.py | 4 ++-- Tests/test_file_psd.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 68834045a52..186a0efd3fe 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -6,7 +6,7 @@ import packaging import pytest -from PIL import Image, features +from PIL import Image, UnidentifiedImageError, features from Tests.helper import skip_unless_feature if sys.platform.startswith("win32"): @@ -42,7 +42,7 @@ def test_fuzz_images(path): except ( Image.DecompressionBombError, Image.DecompressionBombWarning, - Image.UnidentifiedImageError, + UnidentifiedImageError, ): # Known Image.* exceptions assert True diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index c479c384a76..360ae11b515 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -3,7 +3,7 @@ import pytest -from PIL import EpsImagePlugin, Image, features +from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features from .helper import ( assert_image_similar, @@ -417,7 +417,7 @@ def test_emptyline(): ) def test_timeout(test_file): with open(test_file, "rb") as f: - with pytest.raises(Image.UnidentifiedImageError): + with pytest.raises(UnidentifiedImageError): with Image.open(f): pass diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 8b06ce2b163..d98f2335648 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -3,7 +3,7 @@ import pytest -from PIL import Image, PsdImagePlugin +from PIL import Image, PsdImagePlugin, UnidentifiedImageError from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy @@ -146,11 +146,11 @@ def test_combined_larger_than_size(): [ ( "Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd", - Image.UnidentifiedImageError, + UnidentifiedImageError, ), ( "Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd", - Image.UnidentifiedImageError, + UnidentifiedImageError, ), ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError), ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), From 30015f6236ee3165e06b78ff7dced956e21e24d5 Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 16:16:25 +0100 Subject: [PATCH 0002/2374] simplify decompression bomb check in FreeTypeFont.render --- src/PIL/ImageFont.py | 17 +++-------------- src/_imagingft.c | 14 ++++---------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 6db7cc4eccb..c8d834f91d4 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -582,22 +582,13 @@ def getmask2( _string_length_check(text) if start is None: start = (0, 0) - im = None - size = None def fill(width, height): - nonlocal im, size - size = (width, height) - if Image.MAX_IMAGE_PIXELS is not None: - pixels = max(1, width) * max(1, height) - if pixels > 2 * Image.MAX_IMAGE_PIXELS: - return - - im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - return im + Image._decompression_bomb_check(size) + return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - offset = self.font.render( + return self.font.render( text, fill, mode, @@ -610,8 +601,6 @@ def fill(width, height): start[0], start[1], ) - Image._decompression_bomb_check(size) - return im, offset def font_variant( self, font=None, size=None, index=None, encoding=None, layout_engine=None diff --git a/src/_imagingft.c b/src/_imagingft.c index 68c66ac2c60..6e24fcf95ed 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) { image = PyObject_CallFunction(fill, "ii", width, height); if (image == Py_None) { PyMem_Del(glyph_info); - return Py_BuildValue("ii", 0, 0); + return Py_BuildValue("N(ii)", image, 0, 0); } else if (image == NULL) { PyMem_Del(glyph_info); return NULL; @@ -894,7 +894,7 @@ font_render(FontObject *self, PyObject *args) { y_offset -= stroke_width; if (count == 0 || width == 0 || height == 0) { PyMem_Del(glyph_info); - return Py_BuildValue("ii", x_offset, y_offset); + return Py_BuildValue("N(ii)", image, x_offset, y_offset); } if (stroke_width) { @@ -1130,18 +1130,12 @@ font_render(FontObject *self, PyObject *args) { if (bitmap_converted_ready) { FT_Bitmap_Done(library, &bitmap_converted); } - Py_DECREF(image); FT_Stroker_Done(stroker); PyMem_Del(glyph_info); - return Py_BuildValue("ii", x_offset, y_offset); + return Py_BuildValue("N(ii)", image, x_offset, y_offset); glyph_error: - if (im->destroy) { - im->destroy(im); - } - if (im->image) { - free(im->image); - } + Py_DECREF(image); if (stroker != NULL) { FT_Done_Glyph(glyph); } From 90991428fa4ca39076efa6329792f30b962d7d49 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 20:05:16 +0100 Subject: [PATCH 0003/2374] add LCMS2 flags to ImageCms --- Tests/test_imagecms.py | 10 +++++++ src/PIL/ImageCms.py | 63 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 0dde82bd748..fec482f4393 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -90,6 +90,16 @@ def test_sanity(): hopper().point(t) +def test_flags(): + assert ImageCms.Flags.NONE == 0 + assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE + assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE + + assert ImageCms.Flags.GRIDPOINTS(255) == (255 << 16) + assert ImageCms.Flags.GRIDPOINTS(-1) == ImageCms.Flags.GRIDPOINTS(255) + assert ImageCms.Flags.GRIDPOINTS(511) == ImageCms.Flags.GRIDPOINTS(255) + + def test_name(): skip_missing() # get profile information for file diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 643fce830ba..eafafd58333 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -16,8 +16,10 @@ # below for the original description. from __future__ import annotations +import operator import sys -from enum import IntEnum +from enum import IntEnum, IntFlag +from functools import reduce from . import Image @@ -119,6 +121,48 @@ class Direction(IntEnum): # # flags + +class Flags(IntFlag): + # These are taken from lcms2.h (including comments) + NONE = 0 + NOCACHE = 0x0040 # Inhibit 1-pixel cache + NOOPTIMIZE = 0x0100 # Inhibit optimizations + NULLTRANSFORM = 0x0200 # Don't transform anyway + GAMUTCHECK = 0x1000 # Out of Gamut alarm + SOFTPROOFING = 0x4000 # Do softproofing + BLACKPOINTCOMPENSATION = 0x2000 + NOWHITEONWHITEFIXUP = 0x0004 # Don't fix scum dot + HIGHRESPRECALC = 0x0400 # Use more memory to give better accuracy + LOWRESPRECALC = 0x0800 # Use less memory to minimize resources + # this should be 8BITS_DEVICELINK, but that is not a valid name in Python: + USE_8BITS_DEVICELINK = 0x0008 # Create 8 bits devicelinks + GUESSDEVICECLASS = 0x0020 # Guess device class (for transform2devicelink) + KEEP_SEQUENCE = 0x0080 # Keep profile sequence for devicelink creation + FORCE_CLUT = 0x0002 # Force CLUT optimization + CLUT_POST_LINEARIZATION = 0x0001 # create postlinearization tables if possible + CLUT_PRE_LINEARIZATION = 0x0010 # create prelinearization tables if possible + NONEGATIVES = 0x8000 # Prevent negative numbers in floating point transforms + COPY_ALPHA = 0x04000000 # Alpha channels are copied on cmsDoTransform() + NODEFAULTRESOURCEDEF = 0x01000000 + + _GRIDPOINTS_1 = 1 << 16 + _GRIDPOINTS_2 = 2 << 16 + _GRIDPOINTS_4 = 4 << 16 + _GRIDPOINTS_8 = 8 << 16 + _GRIDPOINTS_16 = 16 << 16 + _GRIDPOINTS_32 = 32 << 16 + _GRIDPOINTS_64 = 64 << 16 + _GRIDPOINTS_128 = 128 << 16 + + @staticmethod + def GRIDPOINTS(n: int) -> Flags: + # Fine-tune control over number of gridpoints + return Flags.NONE | ((n & 0xFF) << 16) + + +_MAX_FLAG = reduce(operator.or_, Flags) + + FLAGS = { "MATRIXINPUT": 1, "MATRIXOUTPUT": 2, @@ -142,11 +186,6 @@ class Direction(IntEnum): "GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints } -_MAX_FLAG = 0 -for flag in FLAGS.values(): - if isinstance(flag, int): - _MAX_FLAG = _MAX_FLAG | flag - # --------------------------------------------------------------------. # Experimental PIL-level API @@ -218,7 +257,7 @@ def __init__( intent=Intent.PERCEPTUAL, proof=None, proof_intent=Intent.ABSOLUTE_COLORIMETRIC, - flags=0, + flags=Flags.NONE, ): if proof is None: self.transform = core.buildTransform( @@ -303,7 +342,7 @@ def profileToProfile( renderingIntent=Intent.PERCEPTUAL, outputMode=None, inPlace=False, - flags=0, + flags=Flags.NONE, ): """ (pyCMS) Applies an ICC transformation to a given image, mapping from @@ -420,7 +459,7 @@ def buildTransform( inMode, outMode, renderingIntent=Intent.PERCEPTUAL, - flags=0, + flags=Flags.NONE, ): """ (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the @@ -482,7 +521,7 @@ def buildTransform( raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" raise PyCMSError(msg) try: @@ -505,7 +544,7 @@ def buildProofTransform( outMode, renderingIntent=Intent.PERCEPTUAL, proofRenderingIntent=Intent.ABSOLUTE_COLORIMETRIC, - flags=FLAGS["SOFTPROOFING"], + flags=Flags.SOFTPROOFING, ): """ (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the @@ -586,7 +625,7 @@ def buildProofTransform( raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" raise PyCMSError(msg) try: From 26b2aa5165c62ee38664e291206f8ff28a30bb39 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 02:07:59 +0100 Subject: [PATCH 0004/2374] document ImageCms.{ImageCmsProfile,Intent,Direction}; fix ImageCms.core.CmsProfile references (cherry picked from commit f2b1bbcf65b327c14646d4113302e3df59555110) --- docs/deprecations.rst | 4 ++-- docs/reference/ImageCms.rst | 21 ++++++++++++++++++++- docs/releasenotes/8.0.0.rst | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a42dc555fea..0f9c75756ee 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -338,8 +338,8 @@ ImageCms.CmsProfile attributes .. deprecated:: 3.2.0 .. versionremoved:: 8.0.0 -Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0, -they issued a :py:exc:`DeprecationWarning`: +Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed. +From 6.0.0, they issued a :py:exc:`DeprecationWarning`: ======================== =================================================== Removed Use instead diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 9b9b5e7b29e..9b6be40b106 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -8,9 +8,26 @@ The :py:mod:`~PIL.ImageCms` module provides color profile management support using the LittleCMS2 color management engine, based on Kevin Cazabon's PyCMS library. +.. autoclass:: ImageCmsProfile + :members: + :special-members: __init__ .. autoclass:: ImageCmsTransform + :members: + :undoc-members: .. autoexception:: PyCMSError +Constants +--------- + +.. autoclass:: Intent + :members: + :member-order: bysource + :undoc-members: +.. autoclass:: Direction + :members: + :member-order: bysource + :undoc-members: + Functions --------- @@ -37,13 +54,15 @@ CmsProfile ---------- The ICC color profiles are wrapped in an instance of the class -:py:class:`CmsProfile`. The specification ICC.1:2010 contains more +:py:class:`~core.CmsProfile`. The specification ICC.1:2010 contains more information about the meaning of the values in ICC profiles. For convenience, all XYZ-values are also given as xyY-values (so they can be easily displayed in a chromaticity diagram, for example). +.. py:currentmodule:: PIL.ImageCms.core .. py:class:: CmsProfile + :canonical: PIL._imagingcms.CmsProfile .. py:attribute:: creation_date :type: Optional[datetime.datetime] diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index 2bf299dd3d8..1fc245c9a3c 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -30,7 +30,7 @@ Image.fromstring, im.fromstring and im.tostring ImageCms.CmsProfile attributes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed: +Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed: ======================== =================================================== Removed Use instead From 0b2e2b224fe430c74995664c0d8eec9df789727e Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 21:38:29 +0100 Subject: [PATCH 0005/2374] document ImageCms.Flags --- docs/reference/ImageCms.rst | 4 +++ src/PIL/ImageCms.py | 57 +++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 9b6be40b106..4ef5ac77456 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -27,6 +27,10 @@ Constants :members: :member-order: bysource :undoc-members: +.. autoclass:: Flags + :members: + :member-order: bysource + :undoc-members: Functions --------- diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index eafafd58333..9a7afe81f2b 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -123,26 +123,43 @@ class Direction(IntEnum): class Flags(IntFlag): - # These are taken from lcms2.h (including comments) + """Flags and documentation are taken from ``lcms2.h``.""" + NONE = 0 - NOCACHE = 0x0040 # Inhibit 1-pixel cache - NOOPTIMIZE = 0x0100 # Inhibit optimizations - NULLTRANSFORM = 0x0200 # Don't transform anyway - GAMUTCHECK = 0x1000 # Out of Gamut alarm - SOFTPROOFING = 0x4000 # Do softproofing + NOCACHE = 0x0040 + """Inhibit 1-pixel cache""" + NOOPTIMIZE = 0x0100 + """Inhibit optimizations""" + NULLTRANSFORM = 0x0200 + """Don't transform anyway""" + GAMUTCHECK = 0x1000 + """Out of Gamut alarm""" + SOFTPROOFING = 0x4000 + """Do softproofing""" BLACKPOINTCOMPENSATION = 0x2000 - NOWHITEONWHITEFIXUP = 0x0004 # Don't fix scum dot - HIGHRESPRECALC = 0x0400 # Use more memory to give better accuracy - LOWRESPRECALC = 0x0800 # Use less memory to minimize resources + NOWHITEONWHITEFIXUP = 0x0004 + """Don't fix scum dot""" + HIGHRESPRECALC = 0x0400 + """Use more memory to give better accuracy""" + LOWRESPRECALC = 0x0800 + """Use less memory to minimize resources""" # this should be 8BITS_DEVICELINK, but that is not a valid name in Python: - USE_8BITS_DEVICELINK = 0x0008 # Create 8 bits devicelinks - GUESSDEVICECLASS = 0x0020 # Guess device class (for transform2devicelink) - KEEP_SEQUENCE = 0x0080 # Keep profile sequence for devicelink creation - FORCE_CLUT = 0x0002 # Force CLUT optimization - CLUT_POST_LINEARIZATION = 0x0001 # create postlinearization tables if possible - CLUT_PRE_LINEARIZATION = 0x0010 # create prelinearization tables if possible - NONEGATIVES = 0x8000 # Prevent negative numbers in floating point transforms - COPY_ALPHA = 0x04000000 # Alpha channels are copied on cmsDoTransform() + USE_8BITS_DEVICELINK = 0x0008 + """Create 8 bits devicelinks""" + GUESSDEVICECLASS = 0x0020 + """Guess device class (for transform2devicelink)""" + KEEP_SEQUENCE = 0x0080 + """Keep profile sequence for devicelink creation""" + FORCE_CLUT = 0x0002 + """Force CLUT optimization""" + CLUT_POST_LINEARIZATION = 0x0001 + """create postlinearization tables if possible""" + CLUT_PRE_LINEARIZATION = 0x0010 + """create prelinearization tables if possible""" + NONEGATIVES = 0x8000 + """Prevent negative numbers in floating point transforms""" + COPY_ALPHA = 0x04000000 + """Alpha channels are copied on cmsDoTransform()""" NODEFAULTRESOURCEDEF = 0x01000000 _GRIDPOINTS_1 = 1 << 16 @@ -156,7 +173,11 @@ class Flags(IntFlag): @staticmethod def GRIDPOINTS(n: int) -> Flags: - # Fine-tune control over number of gridpoints + """ + Fine-tune control over number of gridpoints + + :param n: :py:class:`int` in range ``0 <= n <= 255`` + """ return Flags.NONE | ((n & 0xFF) << 16) From fd148246499bfd5c7d55f871e3f1f6e608a9bc3f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 19:15:13 +1100 Subject: [PATCH 0006/2374] bbox on macOS is not 2x on retina screens --- docs/reference/ImageGrab.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 0b94032d5f8..5c365132f5e 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -22,7 +22,10 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) :param bbox: What region to copy. Default is the entire screen. - Note that on Windows OS, the top-left point may be negative if ``all_screens=True`` is used. + On macOS, this is not increased to 2x for retina screens, so the full + width of a retina screen would be 1440, not 2880. + On Windows OS, the top-left point may be negative if + ``all_screens=True`` is used. :param include_layered_windows: Includes layered windows. Windows OS only. .. versionadded:: 6.1.0 From dacd92853094414ab6a4326e7f5805666b92996c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 20:37:59 +1100 Subject: [PATCH 0007/2374] 10.3.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 1018b96b525..0568943b52b 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "10.2.0" +__version__ = "10.3.0.dev0" From ec6a57f69d45c0444d7e38df57af6761524463fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 21:27:12 +1100 Subject: [PATCH 0008/2374] Updated description Co-authored-by: Hugo van Kemenade --- docs/reference/ImageGrab.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 5c365132f5e..e0e8d5a2f1b 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -22,10 +22,10 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) :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 - width of a retina screen would be 1440, not 2880. - On Windows OS, the top-left point may be negative if - ``all_screens=True`` is used. + On macOS, this is not increased to 2x for Retina screens, so the full + width of a Retina screen would be 1440, not 2880. + On Windows, the top-left point may be negative if ``all_screens=True`` + is used. :param include_layered_windows: Includes layered windows. Windows OS only. .. versionadded:: 6.1.0 From 75015e9859183e426012aa59309473c479b59a59 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 22:43:56 +1100 Subject: [PATCH 0009/2374] Skip PyPy3.8 Windows wheel --- .github/workflows/wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 060fc497ea7..5adff7ec1c4 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -143,6 +143,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" + CIBW_SKIP: pp38-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm -v {project}:C:\pillow From 5ddcf4d11493524627dbf4b65ad0ed76b7625168 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 20:33:48 +1100 Subject: [PATCH 0010/2374] Package name is now lowercase in wheel filenames --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index b3fd72a520e..97f4f8dcd18 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -87,7 +87,7 @@ Released as needed privately to individual vendors for critical security-related and copy into `dist`. Check and upload them e.g.: ```bash python3 -m twine check --strict dist/* - python3 -m twine upload dist/Pillow-5.2.0* + python3 -m twine upload dist/pillow-5.2.0* ``` ## Publicize Release From 81ea98e4941af0940b4725a7cc187c689a3726e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Tue, 2 Jan 2024 14:10:07 +0100 Subject: [PATCH 0011/2374] document ImageCmsTransform's base class Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/reference/ImageCms.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 4ef5ac77456..22ed516ce58 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -14,6 +14,7 @@ Cazabon's PyCMS library. .. autoclass:: ImageCmsTransform :members: :undoc-members: + :show-inheritance: .. autoexception:: PyCMSError Constants From fc7088a561554aec8b6a4cf6517fdcc11554cb6b Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 2 Jan 2024 14:52:12 +0100 Subject: [PATCH 0012/2374] improve ImageTransform documentation --- docs/PIL.rst | 8 ------- docs/reference/ImageTransform.rst | 35 +++++++++++++++++++++++++++++++ docs/reference/index.rst | 1 + src/PIL/Image.py | 4 ++++ src/PIL/ImageTransform.py | 13 +++++++----- 5 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 docs/reference/ImageTransform.rst diff --git a/docs/PIL.rst b/docs/PIL.rst index b6944e234a5..bdbf1373d88 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -77,14 +77,6 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.ImageTransform` Module ---------------------------------- - -.. automodule:: PIL.ImageTransform - :members: - :undoc-members: - :show-inheritance: - :mod:`~PIL.PaletteFile` Module ------------------------------ diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst new file mode 100644 index 00000000000..1278801821d --- /dev/null +++ b/docs/reference/ImageTransform.rst @@ -0,0 +1,35 @@ + +.. py:module:: PIL.ImageTransform +.. py:currentmodule:: PIL.ImageTransform + +:py:mod:`~PIL.ImageTransform` Module +==================================== + +The :py:mod:`~PIL.ImageTransform` module contains implementations of +:py:class:`~PIL.Image.ImageTransformHandler` for some of the builtin +:py:class:`.Image.Transform` methods. + +.. autoclass:: Transform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: AffineTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: ExtentTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: QuadTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: MeshTransform + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 5d6affa94ad..82c75e373ad 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -25,6 +25,7 @@ Reference ImageShow ImageStat ImageTk + ImageTransform ImageWin ExifTags TiffTags diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1bba9aad2c1..c56da545803 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2666,6 +2666,10 @@ class Example(Image.ImageTransformHandler): def transform(self, size, data, resample, fill=1): # Return result + Implementations of :py:class:`~PIL.Image.ImageTransformHandler` + for some of the :py:class:`Transform` methods are provided + in :py:mod:`~PIL.ImageTransform`. + It may also be an object with a ``method.getdata`` method that returns a tuple supplying new ``method`` and ``data`` values:: diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 84c81f1844f..4f79500e64e 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -20,12 +20,14 @@ class Transform(Image.ImageTransformHandler): + """Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`.""" + method: Image.Transform def __init__(self, data: Sequence[int]) -> None: self.data = data - def getdata(self) -> tuple[int, Sequence[int]]: + def getdata(self) -> tuple[Image.Transform, Sequence[int]]: return self.method, self.data def transform( @@ -34,6 +36,7 @@ def transform( image: Image.Image, **options: dict[str, str | int | tuple[int, ...] | list[int]], ) -> Image.Image: + """Perform the transform. Called from :py:meth:`.Image.transform`.""" # can be overridden method, data = self.getdata() return image.transform(size, method, data, **options) @@ -51,7 +54,7 @@ class AffineTransform(Transform): This function can be used to scale, translate, rotate, and shear the original image. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows from an affine transform matrix. @@ -73,7 +76,7 @@ class ExtentTransform(Transform): rectangle in the current image. It is slightly slower than crop, but about as fast as a corresponding resize operation. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the input image's coordinate system. See :ref:`coordinate-system`. @@ -89,7 +92,7 @@ class QuadTransform(Transform): Maps a quadrilateral (a region defined by four corners) from the image to a rectangle of the given size. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the upper left, lower left, lower right, and upper right corner of the @@ -104,7 +107,7 @@ class MeshTransform(Transform): Define a mesh image transform. A mesh transform consists of one or more individual quad transforms. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param data: A list of (bbox, quad) tuples. """ From 8070fe10f1ccc346539f87c88fb65ea55d001b9a Mon Sep 17 00:00:00 2001 From: Nulano Date: Sat, 23 Dec 2023 14:41:50 +0100 Subject: [PATCH 0013/2374] pass build config before setuptools command; add build_editable to custom build backend --- .github/workflows/wheels.yml | 2 ++ _custom_build/backend.py | 57 +++++++++++++----------------------- pyproject.toml | 2 +- setup.py | 34 ++++++++++++++++++--- 4 files changed, 54 insertions(+), 41 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 5adff7ec1c4..0c8a941de30 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -5,6 +5,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/workflows/wheel*" + - "setup.py" - "wheels/*" - "winbuild/build_prepare.py" - "winbuild/fribidi.cmake" @@ -14,6 +15,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/workflows/wheel*" + - "setup.py" - "wheels/*" - "winbuild/build_prepare.py" - "winbuild/fribidi.cmake" diff --git a/_custom_build/backend.py b/_custom_build/backend.py index d1537b80987..0b183a5870b 100644 --- a/_custom_build/backend.py +++ b/_custom_build/backend.py @@ -11,41 +11,16 @@ class _CustomBuildMetaBackend(backend_class): def run_setup(self, setup_script="setup.py"): if self.config_settings: - - def config_has(key, value): - settings = self.config_settings.get(key) - if settings: - if not isinstance(settings, list): - settings = [settings] - return value in settings - - flags = [] - for dependency in ( - "zlib", - "jpeg", - "tiff", - "freetype", - "raqm", - "lcms", - "webp", - "webpmux", - "jpeg2000", - "imagequant", - "xcb", - ): - if config_has(dependency, "enable"): - flags.append("--enable-" + dependency) - elif config_has(dependency, "disable"): - flags.append("--disable-" + dependency) - for dependency in ("raqm", "fribidi"): - if config_has(dependency, "vendor"): - flags.append("--vendor-" + dependency) - if self.config_settings.get("platform-guessing") == "disable": - flags.append("--disable-platform-guessing") - if self.config_settings.get("debug") == "true": - flags.append("--debug") - if flags: - sys.argv = sys.argv[:1] + ["build_ext"] + flags + sys.argv[1:] + params = [] + for k, v in self.config_settings.items(): + if isinstance(v, list): + msg = "Conflicting options: " + ", ".join( + f"'--config-setting {k}={v_}'" for v_ in v + ) + raise ValueError(msg) + params.append(f"--pillow-configuration={k}={v}") + + sys.argv = sys.argv[:1] + params + sys.argv[1:] return super().run_setup(setup_script) def build_wheel( @@ -54,5 +29,15 @@ def build_wheel( self.config_settings = config_settings return super().build_wheel(wheel_directory, config_settings, metadata_directory) + def build_editable( + self, wheel_directory, config_settings=None, metadata_directory=None + ): + self.config_settings = config_settings + return super().build_editable( + wheel_directory, config_settings, metadata_directory + ) + -build_wheel = _CustomBuildMetaBackend().build_wheel +_backend = _CustomBuildMetaBackend() +build_wheel = _backend.build_wheel +build_editable = _backend.build_editable diff --git a/pyproject.toml b/pyproject.toml index da2537b2137..d63e401af1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ version = {attr = "PIL.__version__"} [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 -config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" +config-settings = "raqm=vendor fribidi=vendor imagequant=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" diff --git a/setup.py b/setup.py index 1bf0bcff558..c74165fb73a 100755 --- a/setup.py +++ b/setup.py @@ -28,6 +28,9 @@ def get_version(): return locals()["__version__"] +configuration = {} + + PILLOW_VERSION = get_version() FREETYPE_ROOT = None HARFBUZZ_ROOT = None @@ -334,15 +337,24 @@ def __iter__(self): + [("add-imaging-libs=", None, "Add libs to _imaging build")] ) + @staticmethod + def check_configuration(option, value): + return True if configuration.get(option) == value else None + def initialize_options(self): - self.disable_platform_guessing = None + self.disable_platform_guessing = self.check_configuration( + "platform-guessing", "disable" + ) self.add_imaging_libs = "" build_ext.initialize_options(self) for x in self.feature: - setattr(self, f"disable_{x}", None) - setattr(self, f"enable_{x}", None) + setattr(self, f"disable_{x}", self.check_configuration(x, "disable")) + setattr(self, f"enable_{x}", self.check_configuration(x, "enable")) for x in ("raqm", "fribidi"): - setattr(self, f"vendor_{x}", None) + setattr(self, f"vendor_{x}", self.check_configuration(x, "vendor")) + if self.check_configuration("debug", "true"): + self.debug = True + self.parallel = configuration.get("parallel") def finalize_options(self): build_ext.finalize_options(self) @@ -390,6 +402,9 @@ def finalize_options(self): raise ValueError(msg) _dbg("Using vendored version of %s", x) self.feature.vendor.add(x) + if x == "raqm": + _dbg("--vendor-raqm implies --enable-raqm") + self.feature.required.add(x) def _update_extension(self, name, libraries, define_macros=None, sources=None): for extension in self.extensions: @@ -985,6 +1000,17 @@ def debug_build(): Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), ] + +# parse configuration from _custom_build/backend.py +while len(sys.argv[1]) >= 2 and sys.argv[1].startswith("--pillow-configuration="): + _, key, value = sys.argv[1].split("=", 2) + old = configuration.get(key) + if old is not None: + msg = f"Conflicting options: '-C {key}={old}' and '-C {key}={value}'" + raise ValueError(msg) + configuration[key] = value + del sys.argv[1] + try: setup( cmdclass={"build_ext": pil_build_ext}, From b4e690049d81dc2569d6f02c5b0c1b9eb29a07b2 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sun, 31 Dec 2023 02:22:05 +0100 Subject: [PATCH 0014/2374] document config setting "-C parallel=n" for number of CPUs to use for compilation --- docs/installation.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index fbcfbb90724..03011619ff7 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -390,9 +390,10 @@ After navigating to the Pillow directory, run:: Build Options """"""""""""" -* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use - multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` - sets the number of CPUs to use, or can disable parallel building by +* Config setting: ``-C parallel=n``. Can also be given + with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use + multiprocessing to build the extension. Setting ``-C parallel=n`` + sets the number of CPUs to use to ``n``, or can disable parallel building by using a setting of 1. By default, it uses 4 CPUs, or if 4 are not available, as many as are present. @@ -417,14 +418,13 @@ Build Options used to compile the standard Pillow wheels. Compiling libraqm requires a C99-compliant compiler. -* Build flag: ``-C platform-guessing=disable``. Skips all of the +* Config setting: ``-C platform-guessing=disable``. Skips all of the platform dependent guessing of include and library directories for automated build systems that configure the proper paths in the environment variables (e.g. Buildroot). -* Build flag: ``-C debug=true``. Adds a debugging flag to the include and - library search process to dump all paths searched for and found to - stdout. +* Config setting: ``-C debug=true``. Adds a debugging flag to the include and + library search process to dump all paths searched for and found to stdout. Sample usage:: From f27b838a451d0b20befea03da7b2d58dd1da7821 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 2 Jan 2024 15:47:47 +0100 Subject: [PATCH 0015/2374] support multiple --config-settings --- _custom_build/backend.py | 12 +++++------- pyproject.toml | 2 +- setup.py | 13 +++---------- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/_custom_build/backend.py b/_custom_build/backend.py index 0b183a5870b..2c670ff0ac3 100644 --- a/_custom_build/backend.py +++ b/_custom_build/backend.py @@ -12,13 +12,11 @@ class _CustomBuildMetaBackend(backend_class): def run_setup(self, setup_script="setup.py"): if self.config_settings: params = [] - for k, v in self.config_settings.items(): - if isinstance(v, list): - msg = "Conflicting options: " + ", ".join( - f"'--config-setting {k}={v_}'" for v_ in v - ) - raise ValueError(msg) - params.append(f"--pillow-configuration={k}={v}") + for key, values in self.config_settings.items(): + if not isinstance(values, list): + values = [values] + for value in values: + params.append(f"--pillow-configuration={key}={value}") sys.argv = sys.argv[:1] + params + sys.argv[1:] return super().run_setup(setup_script) diff --git a/pyproject.toml b/pyproject.toml index d63e401af1e..da2537b2137 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ version = {attr = "PIL.__version__"} [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 -config-settings = "raqm=vendor fribidi=vendor imagequant=disable" +config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" diff --git a/setup.py b/setup.py index c74165fb73a..686e8889fd9 100755 --- a/setup.py +++ b/setup.py @@ -339,7 +339,7 @@ def __iter__(self): @staticmethod def check_configuration(option, value): - return True if configuration.get(option) == value else None + return True if value in configuration.get(option, []) else None def initialize_options(self): self.disable_platform_guessing = self.check_configuration( @@ -354,7 +354,7 @@ def initialize_options(self): setattr(self, f"vendor_{x}", self.check_configuration(x, "vendor")) if self.check_configuration("debug", "true"): self.debug = True - self.parallel = configuration.get("parallel") + self.parallel = configuration.get("parallel", [None])[-1] def finalize_options(self): build_ext.finalize_options(self) @@ -402,9 +402,6 @@ def finalize_options(self): raise ValueError(msg) _dbg("Using vendored version of %s", x) self.feature.vendor.add(x) - if x == "raqm": - _dbg("--vendor-raqm implies --enable-raqm") - self.feature.required.add(x) def _update_extension(self, name, libraries, define_macros=None, sources=None): for extension in self.extensions: @@ -1004,11 +1001,7 @@ def debug_build(): # parse configuration from _custom_build/backend.py while len(sys.argv[1]) >= 2 and sys.argv[1].startswith("--pillow-configuration="): _, key, value = sys.argv[1].split("=", 2) - old = configuration.get(key) - if old is not None: - msg = f"Conflicting options: '-C {key}={old}' and '-C {key}={value}'" - raise ValueError(msg) - configuration[key] = value + configuration.setdefault(key, []).append(value) del sys.argv[1] try: From 01e5f06da055490f70b1294f39a2718297ee8067 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 2 Jan 2024 16:12:37 +0100 Subject: [PATCH 0016/2374] document editable mode installation in winbuild/build.rst --- winbuild/build.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/winbuild/build.rst b/winbuild/build.rst index a8e4ebaa6cc..26d0da0a35a 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -87,11 +87,18 @@ are set by running ``winbuild\build\build_env.cmd`` and install Pillow with pip: winbuild\build\build_env.cmd python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . +You can also install Pillow in `editable mode`_:: + + winbuild\build\build_env.cmd + python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor -e . + To build a wheel instead, run:: winbuild\build\build_env.cmd python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . +.. _editable mode: https://setuptools.pypa.io/en/latest/userguide/development_mode.html + Testing Pillow -------------- From b4a82712887e14b7ff1fc6302fdd9b1a48ac2280 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 2 Jan 2024 17:26:11 +0100 Subject: [PATCH 0017/2374] update Windows 11 tested versions --- docs/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index fbcfbb90724..4c58b4ebb27 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -581,9 +581,9 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+----------------------------+------------------+--------------+ | FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.1.0 |arm64 | +| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 | +----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 Pro | 3.11, 3.12 | 10.1.0 |x86-64 | +| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ | Windows 10 | 3.7 | 7.1.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ From d134110ace31194f5fab4a9c3a62563a5d2a91bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Jan 2024 09:01:35 +1100 Subject: [PATCH 0018/2374] If bbox is omitted, screenshot is taken at 2x on Retina screens --- docs/reference/ImageGrab.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index e0e8d5a2f1b..db2987eb081 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -11,9 +11,9 @@ or the clipboard to a PIL image memory. .. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None) - Take a snapshot of the screen. The pixels inside the bounding box are - returned as an "RGBA" on macOS, or an "RGB" image otherwise. - If the bounding box is omitted, the entire screen is copied. + Take a snapshot of the screen. The pixels inside the bounding box are returned as + an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, + the entire screen is copied, and on macOS, it will be at 2x if on a Retina screen. On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is From 424737ef4906b5005f53b1642e5b538cadf391ed Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Jan 2024 11:18:16 +1100 Subject: [PATCH 0019/2374] Updated macOS tested Pillow versions --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 4c58b4ebb27..922720b9d05 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -510,7 +510,7 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+============================+==================+==============+ -| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.1.0 |arm | +| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm | +----------------------------------+----------------------------+------------------+--------------+ | macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | | +----------------------------+------------------+ | From 05e73702f2651897efbdc0ad39551f1a99b371d4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 10:51:10 +1100 Subject: [PATCH 0020/2374] Updated matrix variable name on Linux and macOS to match Windows --- .github/workflows/wheels.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 5adff7ec1c4..85d9eba1c3e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -39,18 +39,18 @@ jobs: include: - name: "macOS x86_64" os: macos-latest - archs: x86_64 + cibw_arch: x86_64 macosx_deployment_target: "10.10" - name: "macOS arm64" os: macos-latest - archs: arm64 + cibw_arch: arm64 macosx_deployment_target: "11.0" - name: "manylinux2014 and musllinux x86_64" os: ubuntu-latest - archs: x86_64 + cibw_arch: x86_64 - name: "manylinux_2_28 x86_64" os: ubuntu-latest - archs: x86_64 + cibw_arch: x86_64 build: "*manylinux*" manylinux: "manylinux_2_28" steps: @@ -67,7 +67,7 @@ jobs: python3 -m pip install -r .ci/requirements-cibw.txt python3 -m cibuildwheel --output-dir wheelhouse env: - CIBW_ARCHS: ${{ matrix.archs }} + CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} @@ -77,7 +77,7 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: dist-${{ matrix.os }}-${{ matrix.archs }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} + name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} path: ./wheelhouse/*.whl windows: From 85c552934af5ed378b034560ace23107bb4ec497 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 3 Jan 2024 17:45:41 +0200 Subject: [PATCH 0021/2374] Goodbye Travis CI --- .github/workflows/test-cygwin.yml | 2 -- .github/workflows/test-docker.yml | 2 -- .github/workflows/test-mingw.yml | 2 -- .github/workflows/test-windows.yml | 2 -- .github/workflows/test.yml | 2 -- .travis.yml | 52 ------------------------------ README.md | 3 -- RELEASING.md | 8 +---- docs/about.rst | 3 +- docs/index.rst | 4 --- 10 files changed, 2 insertions(+), 78 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 32ac6f65e76..7244315ac15 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index eb27b4bf75b..3bb6856f6e8 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 115c2e9bebc..cdd51e2bb3f 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 86cd5b5fa1e..a0ef1c3f1ec 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -6,7 +6,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -14,7 +13,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7e112f4364..05f78704bcd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8f8250809d7..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,52 +0,0 @@ -if: tag IS present OR type = api - -env: - global: - - CIBW_ARCHS=aarch64 - - CIBW_SKIP=pp38-* - -language: python -# Default Python version is usually 3.6 -python: "3.12" -dist: jammy -services: docker - -jobs: - include: - - name: "manylinux2014 aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*manylinux*" - - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux2014 - - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux2014 - - name: "manylinux_2_28 aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*manylinux*" - - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux_2_28 - - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux_2_28 - - name: "musllinux aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*musllinux*" - -install: - - python3 -m pip install -r .ci/requirements-cibw.txt - -script: - - python3 -m cibuildwheel --output-dir wheelhouse - - ls -l "${TRAVIS_BUILD_DIR}/wheelhouse/" - -# Upload wheels to GitHub Releases -deploy: - provider: releases - api_key: $GITHUB_RELEASE_TOKEN - file_glob: true - file: "${TRAVIS_BUILD_DIR}/wheelhouse/*.whl" - on: - repo: python-pillow/Pillow - tags: true - skip_cleanup: true diff --git a/README.md b/README.md index e11bd2faa1d..6982676f518 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,6 @@ As of 2019, Pillow development is GitHub Actions build status (Wheels) - Travis CI wheels build status (aarch64) Code coverage diff --git a/RELEASING.md b/RELEASING.md index 97f4f8dcd18..62f3627de19 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -10,7 +10,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 * [ ] Develop and prepare release in `main` branch. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. -* [ ] Check that all of the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and [Travis CI](https://app.travis-ci.com/github/python-pillow/pillow) jobs by manually triggering them. +* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them. * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Update `CHANGES.rst`. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. @@ -83,12 +83,6 @@ Released as needed privately to individual vendors for critical security-related * [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) has passed, including the "Upload release to PyPI" job. This will have been triggered by the new tag. -* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases) - and copy into `dist`. Check and upload them e.g.: - ```bash - python3 -m twine check --strict dist/* - python3 -m twine upload dist/pillow-5.2.0* - ``` ## Publicize Release diff --git a/docs/about.rst b/docs/about.rst index 872ac0ea690..da351ce2c56 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -6,13 +6,12 @@ Goals The fork author's goal is to foster and support active development of PIL through: -- Continuous integration testing via `GitHub Actions`_, `AppVeyor`_ and `Travis CI`_ +- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_ - Publicized development activity on `GitHub`_ - Regular releases to the `Python Package Index`_ .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions .. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow -.. _Travis CI: https://app.travis-ci.com/github/python-pillow/Pillow .. _GitHub: https://github.com/python-pillow/Pillow .. _Python Package Index: https://pypi.org/project/Pillow/ diff --git a/docs/index.rst b/docs/index.rst index 4f577fe9c22..053d55c3c39 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,10 +41,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Tue, 2 Jan 2024 18:11:52 +0200 Subject: [PATCH 0022/2374] Build QEMU-emulated Linux aarch64 wheels on GitHub Actions --- .github/workflows/wheels.yml | 77 +++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 85d9eba1c3e..6ebf9a4c029 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -30,7 +30,80 @@ env: FORCE_COLOR: 1 jobs: - build: + build-1-QEMU-emulated-wheels: + name: QEMU ${{ matrix.python-version }} ${{ matrix.spec }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - pp39 + - pp310 + - cp38 + - cp39 + - cp310 + - cp311 + - cp312 + spec: + - manylinux2014 + - manylinux_2_28 + - musllinux + exclude: + - { python-version: pp39, spec: musllinux } + - { python-version: pp310, spec: musllinux } + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Install cibuildwheel + run: | + python3 -m pip install -r .ci/requirements-cibw.txt + + - name: Build wheels (manylinux) + if: matrix.spec != 'musllinux' + run: | + python3 -m cibuildwheel --output-dir wheelhouse + env: + # Build only the currently selected Linux architecture (so we can + # parallelise for speed). + CIBW_ARCHS_LINUX: "aarch64" + # Likewise, select only one Python version per job to speed this up. + CIBW_BUILD: "${{ matrix.python-version }}-manylinux*" + # Extra options for manylinux. + CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} + CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} + + - name: Build wheels (musllinux) + if: matrix.spec == 'musllinux' + run: | + python3 -m cibuildwheel --output-dir wheelhouse + env: + # Build only the currently selected Linux architecture (so we can + # parallelise for speed). + CIBW_ARCHS_LINUX: "aarch64" + # Likewise, select only one Python version per job to speed this up. + CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec }}*" + + - uses: actions/upload-artifact@v4 + with: + name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }} + path: ./wheelhouse/*.whl + + build-2-native-wheels: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} strategy: @@ -187,7 +260,7 @@ jobs: pypi-publish: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - needs: [build, windows, sdist] + needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] runs-on: ubuntu-latest name: Upload release to PyPI environment: From 55944860a5dd4a38d786df035346b9d5ddf3aa1e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 12:50:06 +0200 Subject: [PATCH 0023/2374] Remove unused docker/setup-buildx-action --- .github/workflows/wheels.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6ebf9a4c029..4de599b810b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -65,10 +65,6 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 - # https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Install cibuildwheel run: | python3 -m pip install -r .ci/requirements-cibw.txt From 32ae1bd08a6e3d8b9d266a2e7c8b265ec3d5ad18 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 12:52:22 +0200 Subject: [PATCH 0024/2374] Use aarch64 instead of QEMU in job name --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 4de599b810b..d7a93c70ce6 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -31,7 +31,7 @@ env: jobs: build-1-QEMU-emulated-wheels: - name: QEMU ${{ matrix.python-version }} ${{ matrix.spec }} + name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} runs-on: ubuntu-latest strategy: fail-fast: false From fd37d86accff55d366950b3f9d286c7174ebe9b4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 12:55:04 +0200 Subject: [PATCH 0025/2374] Skip non-wheel CI runs for tags --- .github/workflows/test-windows.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 86cd5b5fa1e..94c2d4d70b8 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -2,6 +2,8 @@ name: Test Windows on: push: + branches: + - "**" paths-ignore: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" From bc3cf97649d76bc231ec99d41dfd6eb2be72b175 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 11:00:11 +1100 Subject: [PATCH 0026/2374] Use general arch setting instead of platform-specific setting --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d7a93c70ce6..8a9f81dfd09 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -76,7 +76,7 @@ jobs: env: # Build only the currently selected Linux architecture (so we can # parallelise for speed). - CIBW_ARCHS_LINUX: "aarch64" + CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. CIBW_BUILD: "${{ matrix.python-version }}-manylinux*" # Extra options for manylinux. @@ -90,7 +90,7 @@ jobs: env: # Build only the currently selected Linux architecture (so we can # parallelise for speed). - CIBW_ARCHS_LINUX: "aarch64" + CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec }}*" From e84b0a401509325c7e5e553a9f1a4591256b4b45 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 11:38:41 +1100 Subject: [PATCH 0027/2374] Combine build steps --- .github/workflows/wheels.yml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 8a9f81dfd09..e2bc78b983c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -69,8 +69,7 @@ jobs: run: | python3 -m pip install -r .ci/requirements-cibw.txt - - name: Build wheels (manylinux) - if: matrix.spec != 'musllinux' + - name: Build wheels run: | python3 -m cibuildwheel --output-dir wheelhouse env: @@ -78,22 +77,11 @@ jobs: # parallelise for speed). CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. - CIBW_BUILD: "${{ matrix.python-version }}-manylinux*" + CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" # Extra options for manylinux. CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} - - name: Build wheels (musllinux) - if: matrix.spec == 'musllinux' - run: | - python3 -m cibuildwheel --output-dir wheelhouse - env: - # Build only the currently selected Linux architecture (so we can - # parallelise for speed). - CIBW_ARCHS: "aarch64" - # Likewise, select only one Python version per job to speed this up. - CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec }}*" - - uses: actions/upload-artifact@v4 with: name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }} From 60e82e5a3f21b25e1b54ebc1104e972290201a85 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 17:24:09 +1100 Subject: [PATCH 0028/2374] Separate cibuildwheel install --- .github/workflows/wheels.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e2bc78b983c..50e47f19853 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -119,9 +119,11 @@ jobs: with: python-version: "3.x" - - name: Build wheels + - name: Install cibuildwheel run: | python3 -m pip install -r .ci/requirements-cibw.txt + + - name: Build wheels python3 -m cibuildwheel --output-dir wheelhouse env: CIBW_ARCHS: ${{ matrix.cibw_arch }} @@ -163,6 +165,10 @@ jobs: with: python-version: "3.x" + - name: Install cibuildwheel + run: | + & python.exe -m pip install -r .ci/requirements-cibw.txt + - name: Prepare for build run: | choco install nasm --no-progress @@ -171,8 +177,6 @@ jobs: # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images - & python.exe -m pip install -r .ci/requirements-cibw.txt - & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.arch }} shell: pwsh From 46db79abe1b8ada6f980e01cee633f9c8f6fbbdf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 00:30:46 -0700 Subject: [PATCH 0029/2374] Fix syntax --- .github/workflows/wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 50e47f19853..77c1489dfcd 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -124,6 +124,7 @@ jobs: python3 -m pip install -r .ci/requirements-cibw.txt - name: Build wheels + run: | python3 -m cibuildwheel --output-dir wheelhouse env: CIBW_ARCHS: ${{ matrix.cibw_arch }} From f184775cd3a2410ad326572d2b1d9a75ac481790 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 4 Jan 2024 23:32:54 +1100 Subject: [PATCH 0030/2374] Removed leading ampersand Co-authored-by: Hugo van Kemenade --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 77c1489dfcd..be8f652447c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -168,7 +168,7 @@ jobs: - name: Install cibuildwheel run: | - & python.exe -m pip install -r .ci/requirements-cibw.txt + python.exe -m pip install -r .ci/requirements-cibw.txt - name: Prepare for build run: | From 2dd00de1f36d0e5dfd15f28048c2e2b7550bb232 Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 20:26:14 +0100 Subject: [PATCH 0031/2374] rename x64 to AMD64 in winbuild/build_prepare.py --- .appveyor.yml | 2 +- .github/workflows/wheels.yml | 17 +++++++---------- winbuild/build.rst | 6 +++--- winbuild/build_prepare.py | 4 ++-- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 0f5dea9c515..4c5a7f9ee47 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -14,7 +14,7 @@ environment: ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - PYTHON: C:/Python38-x64 - ARCHITECTURE: x64 + ARCHITECTURE: AMD64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 85d9eba1c3e..36e98aa554a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -81,18 +81,15 @@ jobs: path: ./wheelhouse/*.whl windows: - name: Windows ${{ matrix.arch }} + name: Windows ${{ matrix.cibw_arch }} runs-on: windows-latest strategy: fail-fast: false matrix: include: - - arch: x86 - cibw_arch: x86 - - arch: x64 - cibw_arch: AMD64 - - arch: ARM64 - cibw_arch: ARM64 + - cibw_arch: x86 + - cibw_arch: AMD64 + - cibw_arch: ARM64 steps: - uses: actions/checkout@v4 @@ -116,7 +113,7 @@ jobs: & python.exe -m pip install -r .ci/requirements-cibw.txt - & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.arch }} + & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} shell: pwsh - name: Build wheels @@ -157,13 +154,13 @@ jobs: - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: dist-windows-${{ matrix.arch }} + name: dist-windows-${{ matrix.cibw_arch }} path: ./wheelhouse/*.whl - name: Upload fribidi.dll uses: actions/upload-artifact@v4 with: - name: fribidi-windows-${{ matrix.arch }} + name: fribidi-windows-${{ matrix.cibw_arch }} path: winbuild\build\bin\fribidi* sdist: diff --git a/winbuild/build.rst b/winbuild/build.rst index a8e4ebaa6cc..cd3b559e7f0 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -27,7 +27,7 @@ Download and install: * `Ninja `_ (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) -* x86/x64: `Netwide Assembler (NASM) `_ +* x86/AMD64: `Netwide Assembler (NASM) `_ Any version of Visual Studio 2017 or newer should be supported, including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. @@ -42,7 +42,7 @@ Run ``build_prepare.py`` to configure the build:: usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] [--depends PILLOW_DEPS] - [--architecture {x86,x64,ARM64}] [--nmake] + [--architecture {x86,AMD64,ARM64}] [--nmake] [--no-imagequant] [--no-fribidi] Download and generate build scripts for Pillow dependencies. @@ -55,7 +55,7 @@ Run ``build_prepare.py`` to configure the build:: --depends PILLOW_DEPS directory used to store cached dependencies (default: 'winbuild\depends') - --architecture {x86,x64,ARM64} + --architecture {x86,AMD64,ARM64} build architecture (default: same as host Python) --nmake build dependencies using NMake instead of Ninja --no-imagequant skip GPL-licensed optional dependency libimagequant diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 8e3757ca89e..440e64d9851 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -105,7 +105,7 @@ def cmd_msbuild( ARCHITECTURES = { "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, - "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, + "AMD64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } @@ -651,7 +651,7 @@ def build_dep_all() -> None: ( "ARM64" if platform.machine() == "ARM64" - else ("x86" if struct.calcsize("P") == 4 else "x64") + else ("x86" if struct.calcsize("P") == 4 else "AMD64") ), ), help="build architecture (default: same as host Python)", From 5e2ebaface37ff71e1b8e4e9b5708ff3eec3a909 Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 21:00:06 +0100 Subject: [PATCH 0032/2374] winbuild: build libwebp using cmake --- winbuild/build_prepare.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 440e64d9851..1615abbdb82 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -175,22 +175,15 @@ def cmd_msbuild( "dir": "libwebp-1.3.2", "license": "COPYING", "build": [ - cmd_rmdir(r"output\release-static"), # clean - cmd_nmake( - "Makefile.vc", - "all", - [ - "CFG=release-static", - "RTLIBCFG=dynamic", - "OBJDIR=output", - "ARCH={architecture}", - "LIBWEBP_BASENAME=webp", - ], + *cmds_cmake( + "webp webpdemux webpmux", + "-DBUILD_SHARED_LIBS:BOOL=OFF", + "-DWEBP_LINK_STATIC:BOOL=OFF", ), cmd_mkdir(r"{inc_dir}\webp"), cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), ], - "libs": [r"output\release-static\{architecture}\lib\*.lib"], + "libs": [r"libwebp*.lib"], }, "libtiff": { "url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz", @@ -204,7 +197,7 @@ def cmd_msbuild( }, r"libtiff\tif_webp.c": { # link against webp.lib - "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.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)": "", @@ -217,6 +210,7 @@ def cmd_msbuild( *cmds_cmake( "tiff", "-DBUILD_SHARED_LIBS:BOOL=OFF", + "-DWebP_LIBRARY=libwebp", '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', ) ], From 4094edd12f32a432309d279d7c7b90d068f3bdb1 Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 21:26:47 +0100 Subject: [PATCH 0033/2374] winbuild: fix libwebp linking libsharpyuv --- winbuild/build_prepare.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1615abbdb82..84284ae10f4 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -174,6 +174,11 @@ def cmd_msbuild( "filename": "libwebp-1.3.2.tar.gz", "dir": "libwebp-1.3.2", "license": "COPYING", + "patch": { + r"src\enc\picture_csp_enc.c": { + '#include "sharpyuv/sharpyuv.h"': '#include "sharpyuv/sharpyuv.h"\n#pragma comment(lib, "libsharpyuv.lib")', # noqa: E501 + } + }, "build": [ *cmds_cmake( "webp webpdemux webpmux", @@ -183,7 +188,7 @@ def cmd_msbuild( cmd_mkdir(r"{inc_dir}\webp"), cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), ], - "libs": [r"libwebp*.lib"], + "libs": [r"libsharpyuv.lib", r"libwebp*.lib"], }, "libtiff": { "url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz", From eff9f06f0dac6521d89938aa5a93ab03a77fd50d Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 22:10:11 +0100 Subject: [PATCH 0034/2374] fix comments --- winbuild/build_prepare.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 84284ae10f4..df33ea493e6 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -176,6 +176,7 @@ def cmd_msbuild( "license": "COPYING", "patch": { r"src\enc\picture_csp_enc.c": { + # link against libsharpyuv.lib '#include "sharpyuv/sharpyuv.h"': '#include "sharpyuv/sharpyuv.h"\n#pragma comment(lib, "libsharpyuv.lib")', # noqa: E501 } }, @@ -201,7 +202,7 @@ def cmd_msbuild( "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 }, r"libtiff\tif_webp.c": { - # link against webp.lib + # link against libwebp.lib "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501 }, r"test\CMakeLists.txt": { From d329207e62125ee3dcda0b2a82112757e9ef6fbd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Jan 2024 07:03:40 +1100 Subject: [PATCH 0035/2374] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 85036f6425b..ac961a680a3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ Changelog (Pillow) ================== +10.3.0 (unreleased) +------------------- + +- Rename x64 to AMD64 in winbuild #7693 + [nulano] + 10.2.0 (2024-01-02) ------------------- From 2d6ad5868dac031a8b4eeda41116b3da3fd1be58 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Jan 2024 12:07:55 +1100 Subject: [PATCH 0036/2374] Use "non-zero" consistently --- Tests/test_file_eps.py | 4 ++-- Tests/test_image_resample.py | 2 +- src/libImaging/GifEncode.c | 2 +- src/libImaging/Jpeg.h | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index c479c384a76..8b48e83ad2c 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -270,7 +270,7 @@ def test_render_scale1(): image1_scale1_compare.load() assert_image_similar(image1_scale1, image1_scale1_compare, 5) - # Non-Zero bounding box + # Non-zero bounding box with Image.open(FILE2) as image2_scale1: image2_scale1.load() with Image.open(FILE2_COMPARE) as image2_scale1_compare: @@ -292,7 +292,7 @@ def test_render_scale2(): image1_scale2_compare.load() assert_image_similar(image1_scale2, image1_scale2_compare, 5) - # Non-Zero bounding box + # Non-zero bounding box with Image.open(FILE2) as image2_scale2: image2_scale2.load(scale=2) with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index b4bf6c8df3a..5a578dba5c4 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -403,7 +403,7 @@ def test_reduce(self): if px[2, 0] != test_color // 2: assert test_color // 2 == px[2, 0] - def test_nonzero_coefficients(self): + def test_non_zero_coefficients(self): # regression test for the wrong coefficients calculation # due to bug https://github.com/python-pillow/Pillow/issues/2161 im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c index f232454052a..e37301df765 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -105,7 +105,7 @@ static int glzwe(GIFENCODERSTATE *st, const UINT8 *in_ptr, UINT8 *out_ptr, st->head = st->codes[st->probe] >> 20; goto encode_loop; } else { - /* Reprobe decrement must be nonzero and relatively prime to table + /* Reprobe decrement must be non-zero and relatively prime to table * size. So, any odd positive number for power-of-2 size. */ if ((st->probe -= ((st->tail << 2) | 1)) < 0) { st->probe += TABLE_SIZE; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 98eaac28dd6..7cdba902281 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -74,7 +74,7 @@ typedef struct { /* Optimize Huffman tables (slow) */ int optimize; - /* Disable automatic conversion of RGB images to YCbCr if nonzero */ + /* Disable automatic conversion of RGB images to YCbCr if non-zero */ int keep_rgb; /* Stream type (0=full, 1=tables only, 2=image only) */ From 0d841aab9a51a0b9433bc5932dcd913c028301cf Mon Sep 17 00:00:00 2001 From: Nulano Date: Sat, 6 Jan 2024 13:37:43 +0100 Subject: [PATCH 0037/2374] add support for grayscale pfm image format --- Tests/images/hopper.pfm | Bin 0 -> 65552 bytes Tests/images/hopper_be.pfm | Bin 0 -> 65551 bytes Tests/test_file_ppm.py | 45 ++++++++++++++++++++++++++- docs/handbook/image-file-formats.rst | 18 +++++++++++ src/PIL/PpmImagePlugin.py | 26 +++++++++++++--- 5 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 Tests/images/hopper.pfm create mode 100644 Tests/images/hopper_be.pfm diff --git a/Tests/images/hopper.pfm b/Tests/images/hopper.pfm new file mode 100644 index 0000000000000000000000000000000000000000..b576615640136f3229ab5a3d29ab7956fd194dd0 GIT binary patch literal 65552 zcmb5#f2izNeeeHmjCyKM+ueR+o7S|Q#;7OBNiynDk9u0ic+^up*0H8-w5c66ZDX6( zX`A-u-kW>x5JK6Agh7IALXaVdY$Svs1lfcTh7e>UK{gV~MuH3>$VP%}L}VjDzOP5J z-rw(sVV`5~A5Y$Ez1MsF_^j7^t+PA#*bT7R2y0={4f9=}Z)$d#qy6E+E&p%2>l>_kbcIo?pZL7wrS8;JoM?pr@a`bJZaRP@XeZU z8Y@=JRu}~B5N{3hupJf+akHRJe`P*#*bCy9VJ&-Db#aTJerfZU?^x=G@lS#=YP^G1 z-i#Hy8>~yavUqKSFl<(P7ctWf6!cerDrI)ANap7MG_pMDsG;RVnJu@}PlHP0+%^%u?l_;ymB1mjeken%k=yMwfXj+e^Mh zI1IgvbtY_tt?(b=&R~wK8mD7#3zM)+pDSW73+`I)0`+sv?xlSv7+-lin2&EOSckr& z*!5tI{4A!;PGZsZZKe+6mH&0o{5Qi@jf0FmOnWcrU$&p};0VNyLba=pk3dZMmBksa z{OnruTbnWl7eN0}Fy71beKkB7_QRF&y)S$#Wqa z;j!?_#K{@!Tnqg$4(ihm>h=R&|90xyJjd^5EbBGD9Qft16@M@2S7Q%S9(JhuD86w+ zS*2AfAbI?az7h{aVBzTrW{U|)^UZBhE;m&X@3|imn zW1MLaKMV7)-B5=8(Y^>*WejKgXW@qI-CF`}A4nY!hF?nE`}GgQS5p6fV&5MACb%!; zz{!;9@Pm|T{cw0FSg$d*g7MT}lQ?^1t;1lQ#o%z>;y>TD?cCfDF z;h>ED)orce1jgR*wzMYHDP+X?407R|mB zJl7-Pg}~>`DtjL1%N;9UJm-JLJ{OLq{JmK5JK>?Q2=+Fy2`aK>z4_)Coe>G#; zcYj~ngWnHw&7E^yaAuU{V7-U5`@6*2F3lL`Gq<_M4P`!U=I=61-9BIzCgD`Zaz>xe zcn<_?IuUC;G5RQrRo)8o#B&Sw8huA3v;DA9kEEoHv@-f7t(x z`1K!$K`=Jd^&PIjr)@L+UXlLShtGsh1?&Au{I7{MkGs+O%|rWZ16|%0Xd@5p3H!$2 z9ypf%=B#r9YSuMN(k^%!Fw^qmF$`1Di9A{dv? z*y77-ulSRh=WD?^`u9M8`kL$MSkLT^zcf}pi2GJ7{hy5$dt=J(1A947{mJlPu!rKk zvmZ)*l6iY!8pc5#*3zZ?VywAhJ^ko*Q}|f$ z9Dg0$F~a*Zkcbfmz z8P7a&@^rA5w715uX5IR4g+uDa*3_trEE(=ZRqX06LSH-=I8%V3?C1bOLh7QYqj54#)8#b0P!1h%Mt z(-wCat85+BW`7^b8k{Sd<^VG>4R5oTdMj2mL0y?m3D zyV$2`Yxj&l%3RLBJKDaxm#+-&sv669%7bQC{p#oi-+A6^9|-?F{LkQToS(+;J>xw4 zt-(C*n|B1dzdO(eba79}&1uG5B(K(E#|_V;Y+dO3hpA7)PFMsQZK`K1cM9GAE!-ad zEXXn6U*1n=!>rTK7?Ut>*e_*c8oS2l(?)l5je~Ixg8tLsTyJIkM}j*5^jDV?-)d#= zef{(|CaXV`jo%L&!Pvbp2z>0YF^a{wgSJWYi(LeH{~Lie-b=La1^KG4IDM*Y42&98 zXZOQ;aL+pb|EpoYoqcoIZ`#wjbeDJTN&Vb`v~uT-Gu9y5@5Fju^}_ta4z-?He1kwE zWza?5-5CBMSl9muzRf-r%6dAN#Hx4x!nEEzccCXb@$WX zcenNTTD-pc3@^f3i+N@G`nGbfI#=F(-edCLGu{-g3bVvpk1@^TStre!&v^EBJ&Xch zp*{?QX4RFgxgYG!EF6UUGnevaEbN3qE9<)%`k~6|iy(Fs#(~cmla4ipe(dlF^erYSSAXU6c~lTi8Bz0q8G32CZMO*&5&2U5wig;$C{5j%Iw;Ithb@n6)r!^jdu$I}YNEuYKC; z#;9?`RA1#un1w;u4U4c7=E1wn-x#+Bx;y`LFMOl$eIoo}xHHeamwwBz9jw)u>c-^5 zR+u)F%{d8T)Mu^C8bhCrpx$rwQLHxbtFz-~=ds;({W0kzeuL(p#y*(m zboV>^-p#(b-T(CWchMy6HAZ*LnxlNfHZI$ZUyYlkeH!@1i61ojtJ#cVWNIZ!@*Fb=~o2&0C2`S^=T%IF8<`sSp+?{(I#n%!Tk-5_{AztQ zX0@++?bT+=dto;yW0>dhZS(0sYdO$;7bt%r{B5ug^VZKe)-nrQfq#|l)>iH6s5t(e z#-jN*V<*AbP{%ZEgmFWeul(v=j3Kth8K!&`+IuZlf4_gZ7a<3n|C8a$@cQ6e$T-GE zcl<%>IBFigie0tZR{M&%Sf3hWoVYIfR6qV{YZt!=_TO_VFT+-NI?wdsJX=@y%7wE3 zl=jaA?>}SG#5h%+rEDzB8|Ers*NQE~id!_)jXeqDR#u;OeEb{DXB=(nU8=uWj2gq( zF4_jI?J$<+@V9yCFYoE^@AMb*{KoE%S7o-w9;Dpw#C2oLS%?=~s8?*aT)tInoJnA{ z=~Mlx&HmMXv0Guc#i=iY_xoQ3dc*sG^$w61ejC!hpXc96ys?d=Kc>Mve3*AouQiB; z_--HZi^g{BtkbTqSp6mqbuq@S*c!Xy#%b%8N2w1RgIMsf>&kgH?|oguCFm9ILm}07}YsD4(6|awS#jcjsi;C%5ZN>oqIPguwyxB?YBJ2cZ zb}Q`W+0W>#j(`l?U=qWXy8n}lhw2RqI7W5ulpd!{T-yl0c^9}52V_uc=A z@W-jU+C}tKWu%BtACAA<#Bx4tifF6FUtpU=*Co= z*g>d1%3aE@KlCe~GGFZhyKKHm>`v$h{zb5NTb=qK<$3U1*Fmh`e)>VoEQn|IEuXsb zc0+%4c9n|JAL?S*RiAOO88#Zz*qTefYUhVoD333|Bz_oEjA!)D$FBr;zdU$b@cW;C zqw^cR95LTKEc1KLDE49u(+)A_Db%a2+PmZM4MM-!s`H`xRD1dO$H8;igLx3+H@|1X zB;{Gq#xHgl#E*h;SUz$5n76w2gRm7A!8qHkE@s^7`iLM_NT@W(+^vLzZdwXVHEhZd1m`` z5Y_|VPFM!Bi5}3G)je zuGnd1@y0=oS^djjb++QfniJEovBD(vaTs;rThDVi@9zD|0sjU}fA4?4{rLXz`~Ru7 zcg8Tj@d{(1>KC*6*BI4DeRu&XUi+Y--0zeth8;I%v7XO2+&_i-yUy>N;ylA5*h6Ef z&zoJw>JKse_SV>2>F+n8v$5r8#ZH5AS!I}GmEHKT)2MmG84Ig@R_kI)^RgT9O#)k} zkHe@@Wq;GS^S$rsE)RVF`4)8lUmdOs{<{$4&%<^wUUw{Y)VSJ|yHq?o4B{b1eU(A; ziAT3xy&uF>d)aY(n1-)s-G00AZ#sVG_cy&f*lo`vZa3@&&&Aqfe&y~w2dNLz&%YZP z$A0a^HxI0S%IvIJMa5OD@ixOWY=lV|H>+NB{mRCn^RYmG zdElAc2Yhb{Zw;>so_myd^DKh?>ZrN+l)L_JoqyOtTeWxngZR;Jw#KXeRo6$k#wdUJ zJfFP%Y}S8AaR2|eX8o@7(tn4Lm}S0Y_hYBQUhTBja}o{{QSyQ7vE)=`cVwxFSP4ZV|3$dtZJ+JB=Od=8?Mdz-TnU> zXeI~hw_aAB`)>-r9L|U1;UN7Mjh$HIT@n9BJALbSq94Xj4>8j)Yj&%Z)w}Q?#XSC_ zsMv0s@n~RP{+f$#8aBeDq0C=x{NslAnCGWIOLO;u9Plmpo**a8XI%Y8!F)HRZ;fM) zE?s|jK7G3kQpa!wzJBwGukn<{YA>p-6`1=jz_0a76*bm0zHwNF zN!V|86uSts@TKfcY0n;||B1v{zwyj92*yyqJbnB(>gtPpKkdYd?S)C0HLJXewPAPx z2Jxfc>}uR7e*Mc%THSv0&w};5Cu{uc;BWsrv+e+z(e*IvzasYP;CbZz3-Q?t@AaPt zIpA-Fzl$}-zX!U*SnTb=_py1W!5ZuZtZmfbSFbklTVWnbXJtNZUB7nq@-0&46Wbj_ zyT0~f62{>$h}{U&W}OYsSvlYytndFi|N0oqIQkBQ@$h?zKN*w{!Ynzk9=j9x^eOa3 z)mbsa3owWu7iuqmuf-X!<}mIk_@?{Itk3zUQJs6|_(a%C`)|koX7KKF5BUFR9u7YZ z+rfYTZ7wy zPkArwgu}4eLCiR7_4^L}i=*_n26GL@q6jRHw*r5@pnJ1>deyXgTe1Ve&e_@$a(j`^WlkbPuL0WHkv;g{JXdN zKt6mn<$npzzMPUz{}?_L?6Wo0dPgZ2UEetVNhmu_Szp>`?rUG3eKu`z%9Z5C-<6I&+I$bx%VymRQPt-2y~Vs z>}K#z_+jv!C&u>x&E%-;*vI9y?nI<$?0wN4xMY!*ttdCH*MqM=;y zErS?+cEVoZp9SM?2JMGolC}A^aqfNhz#i27uUy}MEQ|x=z?i+T7p_cuKin9U58@Le zel70;YZ^7iv8XybT&Y(Zf0se(Ma8gU`i)8KMqtNblxOwb|Jm?`;QW6g(C=r#-#v79 zXM4Xt6YHGsgf9lVt3MPLL2kGM#A!bW^rn4{|92_dTWjcrTHm0R*;=o*QJ4hlo`zYl zXY&r*DK9#x7yNszuFp=R`m2jMp0&#Pde70@*{5%5&(go{0By>~vcAnQ4wEnl>lx3# z2b>Px0q+iP3*HI#^)NBhFgk)^e8n$nH=me_FFS~@7pAREOh1f+wg<94=O4b=S@-&X z24}#zcc$HgcskJ8{XY%Q1$(v|o@{mB0DN-fE8({?hHndJ|L+6+y>IAa9o7Tu88+MX zSu;Otk7r>X^j~GseA>4H3w}E9HM+f85gZ%K`ch2|2vtb#;7~=;) zu4tDB^4}cvzpdH-h~GM_XV_4;#$H3YVoJ|xeD<22i(o(4c_Km1{{*T=dG=>O4Rf7XKa zS?j3vHHNbBrp@=#m?ze#J0laf6^uY%e&=!>r+-!Y2al>F0Ub z9|m{7_rP+c3_d%^wh4xqjMm!vL_J{3!Q?;Cf3ys}p7vcAF^tq;oO z7dH*G#4Ok^d(O|!1HZbqMOcQjnd|=G8T_5%Z(6#ubfr7Z;k=jL^mpHjsa&W#UmjEr z)LP6rY49C{s}grjpg((ca30jJ4fkbDH-~fKWH=u7(og?U=!Jf>gH~1_2lZhqtD_&* zgT9ki=BwDs4>5H8WcWhxzJEA)Uguw(c0Y=BPMmWZKi=4mmAlUedGJ(l=6@1sD^{EL z$P2N55KDL81M=-mumZ7 zG^Q!N<-k$eYcDDfN^`cp1C*^{BTPEXQ$7f9O8Yy)JA?b)I^GuShx2ki(0&+hX{etF z?uFfqu^(9ZH4dXN4gIj*=*4QY4tCn;#|{HK2-c`gSw8+l@O~O(E@%DqDa&hTn3jC* zf@N@D(Ep*Z6&?xS3!a1acrLsU#P0_=W*-kzcHjMDa1V@v=c%<=Bdn|6Q0B8{ohDvFW9HYG7o({N4?XXXE^8fUp~}1=Tj~!wrj1w&VOl7|3cnbi#1t~wG6{r z*5X^QAKnst3+Qj(ovV9;JUtU=ep8SG>>!*BSA)rm_DLd!#f%a^j z>FUGEfursM{>p*Uylbs(($=yWI}W|D6`swyPNx4Kh0@RB><4*rTd;SVL0@y*6MMECyu7Eij!}Gty;PnCvFEdnzX>NZhO=#Nt_XE!e>Xm7 z*cpE^%)(Zn{XOBX@UzS``!W9jbxH|7iB#USsUy+LsQo7Y%_wied)m=J~~o(XL+Y)kj;6T{%PhDtj(EPs2P|BRdMc zKtu1-w*>G0W8u;G=m%P#2=tW;e4}tK(7P8dYjrs?2y)|WaIW0X?&|&U#bEA!7=*QE zm4_XsDNn-3)BiKUx43gIKdsexKMv1_Z-u?E6TTkgocP;=_1qRV0-v_+@NC!(p3^(n z-w)R6+__`w8}O;Ljk1nfpE7E#RiDIHd%R5ff%J2q$W`wuS{0=gOI!CDTUbMBTRBly zA8l-nqfHKU_3oa1Woui6-Ovlp;vm#>?5Au`-23w7=9I^Q)^wBueCn{T^5I0Fe?N$$ zJHPyZXJ5wlV%;z9*QZmzn=e?$xUFLpYfaXtJP999{HFu$tjk{6S9%$jhVFp9;5<9$ zUG1+*{dka{C&Mj44!Q&Me>^P03&B2K63p@T@VUU>U7vDcja9a0&tp6~(49WY?i6R% z`0Dhd@ln1DY2Vela-nN!Y=7)Gl)E`F&-x$_?D;VCo3+ogunf0n{r3eLpAXIs-D#>m z3fgW8e5Jo1+pvIt?wu@=6O?*-Jd)*iAj8*OV zx*Vl>*Ju6nhCQ(#!{B-7J`37e&!=o3>F6BO=6s;L9PsS++FqUv{U9Is)x|v>emCnp zjCKFNKfED4m%6j}tw75k#5zCX^x4ha*47K+ZwQaXZ!G)Zxeroz?lywl7zgWI3v~9p z+hNh*Q=c|#z0=?xz*mA?u=h0o{b0>Mj87kHu`lCb|F+^U-Q4xGtMgkrvSO_F7wK=_ zuI}}mrEypP@|C`;cYXQ0c9u2I8`kexM#1yYUtPO6^=Yt&$Mc+{Fl^8s^pz8*gT2K0 zP7<}i-V}IW`%g!&I?JKRFpG$&w`hM!3MZ39sL7aQzN3q5*miW2YAV;m?j_~Dh zPml+v!d1b#`43{l#?8eCf=V{^g^sy^^a?z8KbRPfO!T%ARK&lxeP=NAuCpU829SUkJ~HN5kVm+xNm=*bUzfJHb0h9z77`_!YspKMu6>y2Ta(z@#Z6(z@p9wsp0)mWyex?WH@bJqK-RK5EdL*85=* z)Wz6;IpNuzvr}OhZVC2DPVmwHbfCrUfzRH0_n!#zlYVD|y(#^D7r6g_68q}dx(j@N z)8Lz7nYnLB{kq`%AIA4cm<8J6+29%LyxDK_8QZu&PrL67dn_02Yn>Tu`A+cda8K}D z8$lo68`_{q{|w68Yvt?FO*z=iBl`VaG5 zs65z78I$Jo-1^A{erNGaut)M>5Kaa9v*&|5^v+=4y+5xBzSrHS?+-Lmr}YbovtC+z zrsrdQ2iPm^?uJQx4+MGS`Draqo4zz*y@UPTPIvv?74q**!8xYW8-m|A_-K1our7Jx z?$`=X1i8S{P20JYjp?3JHy^z`i?VfG1yWzVOx{C#+Sz8vA>} z+}5M+Zk3bfRHlo0>%4j%`jjpGOEa2RPF_s^uHHwLySZVG|Jxj(EB$6+9@fJkIOF5M zKMC7`PrYlWsrQ3CsT^=FL;;sz#R=f`+X4K zz2Qs2m_LoBll#Lse6*)IO|ER1@7>{Sp!Ej>y-x-D4;o_n;Vr>9^pR)o7jwu%b9e{% zUf~lX_voQ*H6OYfYqu^`J{)EBp{KF5v*tkM6JJ+%XT8>FoyzhP+PY=q>_sjN? zKG4;9W@$-#cC&F+p6^)LNnKo7d9c-Dl%bz?cfUJdU%5al_whC1WVkN8HPGhjaAok$ zP+ysCzzo(v{xJAU8%~H_Sri!7ybx;0`z&=&$cqaPHq0 z`r(FFz9yCi_LVMg3G{c~-L3M#^VnbeFGu#{yE$0%ld*Ec9@-;wh_gT5^>yF$Ql_ux z)&J?>Z@uq?XM+1gS(_ZQ7IPV=?hZLpf3MmDH^vkJ6mIkdL0*O}EmPzH-1bU7EfJDbE9)OYeTli&p2GhEcP})9-Yk z|Eb_?INR>iuKw4hPJg(c<;NQX-JN|oQ0JEK&aj_3mgY*?dxG|2oKa)TCHYd{ee%J+ z(2PFbC)VTloW0;TfZgCd?t8;tT^X!N-pC=E8sC`mq4agv%!6FgSDdw&U(7Gkmabw} zwKq;tnwBk1FQmP=uGUqq{@uDczM6Fpv!DK^mjAmc55gk2SDg)YIWi05ppBIeb@t`V zMi6r>edvBNOacv@Z8<=r>jSNouMWq zuJVG`^kt`E-l_A6lLyZKnIH%3kG-OG<-p05OM`1uw#Uwy?=Nl6;`hU|!I?kIn3Es} zY&uePqf%KU5>bC&j%E9`0x=xg4;4(Eb1v&$+0ybz52Sa>+daebVBYmhH;k+$@_ zBvk%bpB$mF9FS}7pa(O?lffR#Wp@Ql^yjO+FCEK@sq%&FdK*W3H(t5MRWA5SfBQ&R zwsN4>V?6qw4JU(r+KG4cd_%S|CwOF>7k8g%5>fc_lE}>%8!Sq zgB;;!`R&7IxG%^d`C?pJ)AbJnjpej_@s9aoVjc~&a@W~ce*12pYtOqHDT^x&OJDx3 zPYk;4%7s|ZdlcqhMO=41?@s+>IG6R)buZAKzIVh&zemEd`FCUMju1z0cYwa^Jcu!# zy))l+!I^OfT+?`O?6tw%a)ADS6zEUaXVRDc&gXu^`8UruVqcXQnus;NJos7s-wX7j zh4#|n=kdQfb>Dy1L_=#bueSS#7gcW-$*NVH(D*eoOYm-Z_WveL3K)x(5n=duhMu@$O*%oHtnCdxG3? z_xjHET*h?A8_yY+BhRO8BRmXb~NsXL0F4#)O@=gTj!n?&rX_^5BjPfOMhp?{$CsFywhL(WO#G1pU$Fl zC?Bkc&g$-k(oBE*M04j}tiCkyOpElJ1iE=QxZ8{`PsCmwva>(6X*1Bnb zDm)atlj!FT@P44Zb;tv2ul)90a>Cx1KCHedy_K!G>a2QE^;Jzv+tPBi{)^(f!b4x$EIG5Lj_XK;apY_`t_cKjtKr3Uf1@DcW;QZ299=Hd@v(7pF>YKosrI-H3 zGQPg1WJfN@kR|VezhvC|koliMH8yfqYRC_Pn8J-Kq zFt0qIy?g$)_=mwa<*QRSr*;@io|@0|$X#=JCT(oxQt4HDPYdPJwzObZedeSS|53VL zNdJmuYrdMZ<}QTu#- zaF*GV!TZwt*g5x1_RfBa)%H~S)jNT{&M%$Z``YcfTyVZ=^6kX?W^+FMT|j4lKim`U z505ssV#PfX?hEr^Zr^mLf^%=*tV52-BW2^tEBOR_X?<(4az#GM+tRJ8L6!9>O-f_& zU7tDSK>78pd~)u)b$)$|ny2dBagB+2v{^K0Nz+*v1=>x6m}=7wb^U0qtdBCQ4?Ake z74=Euj_eb?ZV1-}XN8V~;JeIo(bOKkD_qmcp2r?v8Y>=R-2-&FI-CmLA##JJwA~F4 zhDX9dFowQ#rls;SKJN_QtYXiHhk`saKdhPNENz{8_Y`fc_lJphUs;29gfdO!11k>p zov!pK-Ag0w_{^|BSKu54erRQttjtz{8SN>4f}(`pz-t-Kw(7xp_Wn@=C*c|+f6 zU`L^=y_iYZ2=>-le`n|i&q@EY!QR6CU=Obi*9Z6RT6i&i?I~?&M-y@Mqz7G`PdYyt zXjkV}j+pPg;Y@g2Fg~ALfO+K3PXoUk^j?y$v^NLjoml&i``J#(|IclL+PikeHr!}$`F4L7Jo-{jZ zwV)a z($76}I@}n}2X~G6J`(ukpBQ8Ar{9&a=9M#V4{}v*`Oa{^n4kW%lnd81yl`Pui9q%0>B%p)3~4d>5knT`aC*XtrqV#?p5?j2rZ%8@-l6JI$evgJ2AC z(=ZAfVHhS2{f(tvUv0;;PnYI7?6>{2#~%!D4%Y_nRr=FTeJ%VbeZ2oscdRjJ;QcT5 z4}<-Gb&vzz<+PMX+CCgU-t5O>KN8H#|BhfD?*sR_?+Cw*na{f12ks^BCUe?HI>H?K zTbK8YoS-Pt1X?*O&bhsI?%c;O1Z#JG^jQz? z1UdBKFbt)w^K6`-r2WUiS$5~24<8NZg4jpB^%NBM0*{>U-8;OtwA z^G$PW@UC$Gy94O;>wy+D5bK#($j_tnFYQ?E72mCQ%hhg{Xd+tKD}^V z(EgENENJ(==={IEVLmyakM;PLQMT6)rYx`3-4X7p(pzrhSP)yw-P^ZnvLu!#MU!KD;H+_bb1vzF`q_K>&V#%BAa)~+18v>S;(V{|$7J`?1Tg7qrO%+KPWTd@qQX1H&N3c=~&%TjMC_K&_# z#b@k!aM#*XXOe#J2yYJZ#WTCJKh)~(0NOhHwEsxZ#~%7U!ad&$^ro?IJ$K2Qg1<*- zBZsaD;`nK8ZRT^{`73{{QO@XR-SE78rK7pKIZ$PNs@%2WjZu2C>RqbMJg9obRL=CW zzMbqnedvEIu%pyzbydptXcoIneZ95c(&~KrA2gKjNZX^~h2VUAEp{9~y=P%7Ok3Ui z-Z#E}kEKlO2g6=?zVS?~bL~C*gOugPW|)RM!@c1D+DL-JWJJO67ze`R^B zy>z&cUDaJ24e43h9;I*TY_4iAU)Mg6wcQoyOqWHF1J3Ux(Dz)%u>U9H-wn>$eymt$ z{?e44^?AxPo}{eZS$I0E#WxBxpJ@-q$8%|a>Dgny7VP~z_8Woj^1=C5wtxQ_`}H6n z?+afF?u{=8IpObtLS8vTa?t(k{jihv`ZoAj>dyRO^PNbUrq=Ufe9p77aekEgGSJul z(ayg9YH&ZksnzB4^MP)^9^@zejX`tkktY|^q^r+Mdz1LG+Dd!c7Bya-^RnfyF}r=O zqn>LwT9l5;^csf~S^K%f{cZ5RWSyDQ@oV1>`W?hN!^b406^v84_SIH5mbJ+dcMMJCiu{x#a@MzoHPT-mh@pY8t-(E@TsmFI zmTp}umae6JopHWmwXMdlezcd5*5M4SXDwF*cfhgO{n%wVO!-r3|9tqn@X7FeeB!l3 zynDd@&jL;C0d1cR#{;d$VHDgE=HdH!+N|v$<(pF857xdH>r9^s#-sl*Sl83B#y$v- zgma-6%p>2}QTR#_W4&~tue-n=xM$>Xzb8L^T_qIUmL9pJ-V(rbzU_I8Q{hNW# zJK@2`qp_aJ`^Phw%e#WL*DuB=Pvk>aqt!Bf>YmWItDm}f?HB4-`qsU3RII-8_Ai25 z_1sSeIlUcwYdD|sr^8=1oS75x(`(V73H{gNpU2uC`)E$@%~{Gc?+16j^0<``)Am9* zOr8Fl!QJuG@Ob!Qu&!y^mthjFO5HwsHuvneQXU6-oetXR;9YK?t;62aK6j%-;+4=4-*2a_zd{+dxiOD;=FXYf@J(JkKiP z(ABwo)vqjnm04rSwHmY9y5o75-yYoU&iOwC`D(9k&ze7&^52HfhEb3!PsD$Dpf%ma zvTO0thlc0F*2~V|Z6M72X*1HJ5exHlWK7Q#Q{P;bbuH{Xq>xazc5mm{eC zQ2tpcz3H}^Ct~+(f@{d+_~fJ&%R$U{6klet0hMp9;?Z zJUkw>$qD&qTspWntcR}3hoQ9Rqh}Ys(ysEN^sIi>-nA8LtSXlkClBiUe<9_+3G%>u zevv-T(|T+_R$ZI>&VH-~`{O%(5KDjBS1!<$&NOD_!cO1^jYomr^Kg(ctc{;fpM&5W z(H-{KUif|0-_>%!yWN`L9WOtgZ7gHmAM)zA(snYuJ^!rgx^yf3)R{Pz2X;QP>f z*7H3a9t#f!=lF|Z5^fE5hr7ak;r{S=cp}I@ch**L7uiQK^6-^`_B1yK4Lno9uS~O| z>J`IZIxIF$Zg~!7)!Si`9JIhxE_jw^7q&wk%&azFY3b zbo+K=l>a_!w*JGc^=#^%!+AKCaz8$LJLmKR-Lxqmw7MAmu?Tb=2l}e7wfMbOr$4`T zYx8~_wE8esUvt<;?_N0|Kj~ZAJAc;ccXs+Z)AUq!htY|qwC1P%N5cn$GcR5n?fp%A zCe)aJkg{`aK2|O{TNNc*&#d+d$4%Mrj zE#1mrR)18i{HYw`TkTupbo-UB#?;4N*lQYJ7Iw4ddt$BI`suV2XmZfX%UIBx56Tcf z3F7IiZfx<&#+o)}vG&Bb^)Tf@&}W%DU=pn7(Xbu95xmps@9%e7xr@Em>HO~CS!n&o z!TnBey2JbaPlC1^gFLABz(-Qn?oMz|XqQ*UlTZBSyE^!OF#nr_wa5kQmn%U*&p z!E!5|6WYg(&DeRkH0}J#yP?|oN77%} zI{?lx?e7fM?m4XAGucO4(2D+5rUmUv=T#lmOJDYdR#z^L@>N^e8dqJ}{N}Xp?*HAa zk3P#-Ix1flwo~^E)8H9qVJpykCsvLe##SHw4pP@|62?K_-C%5e*z#|tY|Kluf7Yen zC=9|d^c%g{MKF%J?hCI>pR2+(ffmYd2sZ@hmew?`Gfq1`TGn0f?iZ`h;>J*Sg*hBq}&4XC&%ImTE9S?GXf4{|fc1(jl#;CU4l;@!z_JTZg z4~ZEzKU+*%xfeSK;x148BVj#z|9hdE3w1W@UZ9;kp_y+z?`_}e?rz_W>c;4WHwJO; z7KnG}^ZO=n@00=(`)VtM`H!YzDdL zJey0OK^S$|NVy-@Lba(I%ieE>Zv{E@s*LlRa4h^%vwY5%JvP_VVG;CUza9AYf_CTm zmxJ#)f8QzpN|0;*_OMpEp9|Kz-cX)|e#0|Z?<_16Tl(8~nwqPd2i zU!fao-Y%7k`WS=0ht?SAc|6d6p1Nn^-wkxw31VnU&(dC>(!cuYI}Q44qxXIoHYTyg zvyS80Q+4r!pl`*^a<0@j!YGJCFRTZ?d2kn5cR!pDw}eqR7fyt$f_K(IVwb@)EAweP z6K)QZ@Z~TITj7yb{*PE=nP-}Q!-n|>t<0{svh|7)|9qgqet51~^~y;)bk8~c%GcHM zsIoE9)ws(1=7xE@RLr8Skv4RsY3c9Tb{m_`irWdS^LJ_ESgbuY-bNUQX)B9arfiIP z5TkuJcqZpdAMwMWjV-KW(BSK}@+@{5%GXa>EXH9Hv@2J=+O^d>t1Q+UXJISMTV3Dk zH%z%7dSTF5CP&v&)`oGIwDQS3U+pzLzTfJ!aOU|+Z-2gdD|OY5#R&$40e zD)Wn5WS#Umh^0N8iS70b^Vq$x8}`Fa(1)eJ{MP?iEAvl-=Vxz=U%c31Y%i!Ae=Ep) zcl1We`iL2aQ9}&>dKd)ys4uhUi@>L?AB+LMUKqBrdcjwIcH9`nqT2c?_gY)|w2QCU zvg`2;g1Pzh)dy$ueD?ggKwD*&7VPt(bW|^0yE#)DSDQXCX2n&#=F-;nneW-q%>l8C ztZ^-NKlXSmoqAy>^=Y6J{nf=?8svv~WgKf9#%H{ZFbVq8_U^RLQ{D~Q_-A1m{0(v# z)W=~M^i{@sm^ zB@fq9=hLpwBuvAtc{Y2F=K~GsSoXQ*KT1dXXs;OktFEou*_xyD?e@`bK2&@4r|(+) z^ep|aN}c9gVHtLUy*v!3(so;rC)(T>`pE(9+rhJsgXj0#nBPi_!DpWHt*`NYPq_#6 z(>@CN7gc8mLCi4pn_X+R+Qn3P8C`m^@ci72iK4-tR4b zYw^3v`S`?K8tZx_%>5E=T5MuVWStTZ4lPNcBnS7{V)yV zupGP`t85JUWexhw8vK*62pd70F}A`o80#SHwfYJD!yr_3HPucHqkH?zJ zo*xF~Q5c44v*Xz9ppD<&&jv5w8=!o9)o!G1$_-`N{|#V=BRZG59xYnCt5puMj>jKgDT zdnVAE?zB_xYPeb!Uwx|FwH0sAmD$RJvd_jR2db^=%lPnIW0EyqmhxA_Kje9S9=jjk zGHi!~a8-Ps*Kgv^0qy@ajDvgK@9U@1ZXb+CJI`+%Yw-Tp&%f3BP5pd)#<$OJNd4hh z`=<}B=w@!?@Ea5A>_)K1Vn#uqJA%EqDOd;10;zv3md=Z?p0d8`nDY-_-cx(q@AyxPO@Hs1 zQOf;TbJ1RT6uhV8wS1-9^ND*P9A=!GV`=k&`2SlFC;ryp-;CW`?l$+Be?#Q+Z-U14 zUNgTO?8mwvP6rwo*Rwqy>R?r`sMXFv|%I#zu4 z`FN~-w_m4&b6}73`{zL4KM3>!-3LLRn-fomX)OJW#~RZpu#!VDIQ^O>*ybX*b3s<@Vu;G1(1zy0v;Iuo?Z`ZqvKsc9;@#sW+TsOUyp}r%J%%Kly6GA_vAw8b)Cl9*=Js*1~RU>!ti+ zaHroE{xn<}yd(Zc`0v5p;d$H_?zMU1PXv99<=O3~m>S8XaXzPr6mRGODfg(_TK>i?JVAolCv+uW|WS^yr`D=`_ z)lXdMznAi}4SQ^_-=DSr&tRXOfA#wkLq9R|W-p7+-*HdG+9!K{Jk}m5!+2|9JJ5y> z+V_KTltF*@&poaGL40zFg?X*>cN611PP5wU-%t6Mg8d!`YcbC}&}}zX{(67k5}a4> zA!oicW4-g7-`9sv24|)h=*`|9eir1%r-D7Ie#&>Wx;wCY-l|;MUdYl=u4q>-9hGUP zUK&=t;?!wceOAX%FWt*pmpecX$j@2!$#Xi>H3jCYiAzDkWZ|f(BGYQBJtK|{#`b8?;Uvu@5eg#`tr#yarPF=PUj}+R&)(a7Z)^2?(?{M6!yx=l{N^(c3;LJO_oVy5 zep>6Z;mPpB;C)egUCdT~(5nznvu?j?V|QD-80A%}-)g&hSATnaS$y{Bo3ZO*5+ALm zt&KLruodjDJ_ms&;*SS0yI~&aJ`H^IrX~GLcYeOo|EBcUm+rC~8?}?wza@$=kj+IAs_I(#Xjy#+`)??kqxh%fY-Py0Q zJK-I{w;uiI@0;3v^Gu+<@~SRJ)#+)B(o9{MR?5{b#$8dd#;W#i-}14m^L6#NX1drj z&p}%nm4+KB+lTe|w_|B~BIV<)u1#P4SbaA`#TrKpE!nN0UED#KrQgZyq5ams^f$it z-SAY(b^iJFJr(Qwg!c5Hr5v#DhiNyL{{CM4qd+(3{MVZGuDdCm3-px_pA39o&lu)C z6I zSoQ8$t8=NZ>Oar3+Ebd@e|x7s3bQ~vd%6f>m0^$Awe)dc>bsTtW)Qa>jH911#A6!F z!FPZ9xc|qkudyx*;tpDz`}Fq`qi(F4Q{BGboHpl}{`5BIJ@Nk}JQRK$7U8~N&)$@n z&jj~@_dVYu;gu=>H1^AZ?|3*LoO3zm+ri%u>%qCU&os6MR_@i_vasLA>QWk)t$wAO zSmn~L{4{qKAP4AP<5qsG=0aC*>$GnB33nA}W?$#Q{?o=Dj)OM)OU!9m<>@0P2Xg43bjZ*lLx&6v49qzg{b^;kM`7-CxE`B9`%}SZ|6+l? zeEZQ@Tpw0XI`78HqMml@_{DGMf2Y}A$;#2owbY1P{}ykTL+hh_v{g@k{Vr#{1AfU~ zjJduZciVqm%=^JJ?7P}ZI0N20eyepB?41Yyi(IJ2nQ|uYhq-yD&)Uq))pIzrr4g3( z7o)+lHtI$naUAzeoL}wqnf-RYE?=)-vu`%@%S-Xot;O|SkImM!av%EX<6hj8 zYhDV^u6HxtKAKXltJ43u`kq z=V9i}@{N|SveWNncFxYL?D5Y6eO%kUx&L|A_gXKr-`)&H6Eibg?8Itpgc+H=ygMQO zBFtIrM9_1Bm4&EH-Rtn>vuluy4D-5|Bb)nW_%u(!?}TblJg)uGxy?QY==2po9&Z@{XM&^ ztr+b`|IvaUlY3n2(bsigKRK>D{)vf?Z#j3?kBjfcVLXk4mmu~m%!OV*hPl&cH?GG< zoG9mH$hi~Z<|&6?{B+P{D>fHazFaZ+V)iTT{|tZEcj4LoAuC6Z?a=qV&}%LGRyki~ z*V<1HdFS%c@YCYHr>%zHuYJGsyz6Eq-doeSnoj!6ve|bBh2fG*YPNt1#+Vex(YuS(EZa5SA zZY=Vzw)TF$nbmJ?(c@No&%5{jH{rXT-+}yo?epMbaXN3sA7N&<<3^};ZNAZR*5>&j z)R{NG`up)H%vX&)OTWou?I%y}XwEmeldt#k+J1b~OU=vbCU5%Bbsv}iB%a1$JYT~8 zAjDuz7rL#+ZkRFFV>4FDIUQp1jzWBAu@*ZUG^4p1dE3EvD%3bj_G0eOcWwXIzaPH4 ze-Q3foNoHAxBe{aeZH1;CdB><8hhrw_v!B0_qopZNn9%DT9^+l=}kv-qMJEA3UhYu z`0s_hnYS6srS&q-hnV`wl?VO!#9Z5Ya>UdPQ!{(<|6@64`p^A~sdK&E*pGuajAsk1 L`NZ}X?0@thg2ah6 literal 0 HcmV?d00001 diff --git a/Tests/images/hopper_be.pfm b/Tests/images/hopper_be.pfm new file mode 100644 index 0000000000000000000000000000000000000000..93c75e26fda2cd69362a58f61373b0d88075e51c GIT binary patch literal 65551 zcmb5Xf2icudH6rk)!bT}&otj{y0tc)th;*E)lAGL*`(X#edliC)@<5IV^f>jbefpN zwl)56OeS1ZA84LE+cf+;tyOh68T_236A3h=5NU4gf zm-R8e{>_rV7P|I+!Ma7Pub>Sr+Nm4IagRzI>xR$+{YAez+N@9d=z~J8iMCO(4Uzil zvjG)YFZw%%F>J!5px+3rv%R`D{bUF9pR$IHT3NrztlZgG?8VkuqI?bpV2q5nS9JT* zcN^A$I!a$78)^?PWZ%4o%8pn>JL?y#UsUz8Avy+0O{watk)W_?EM+x(s0i z`fR}l0_OC{jqf#$Tcvgtb_GKz+M^8`c|`DoYA_9;i`HUG{pXXk$PAo4x*{T%ge=oY^gvR}$QWMaKowtfKiq14`jCbUbr zDzb+ZeYDllKC0Qa&<9|g^t0Xh&`9~kBShmi*jEnAC zYMa10VzuBn;M>`NZ^3Ce0EY_4kaxp?HdCbM%x5ikt+-rog`RXTgrb)f~ePuWJV&V#-^$a>5D^Po@K z)%7!8+LeuBcWmlXT>#tnz?cKDjZ(6oy42tDIp&B@=PvXnjElWjq;V40%K9zXfeCCt zu}AKR(|M4!`z`n&+yDo#eILFD55q3DgTRD3&mvuO_Q2|&Q~nYB6#Y6NMu&bnkHb^& z2oO6YH6U;x&Qm|Fvya1`z+_6k|Y7(Ez(b4e_p59VqP zBQN(LPs0+nsuvpCzoVC~)u?*q^GLxqPa{{~zGe~j(B z$o~&M0AHf~Enp9<9)SbsuK;~?9)m~W`#_AiTZ~D0_3g+8{X5n!ILaX-fMl9(~qZjn7?9mje;oTo77Sm*zR{0`g$oU!@(jnM1sW*cq7 z970;JJ}J`vv~3aV6qvK;!Fl};vsThXeH6gf=*ievw0DeI5hKqFq1Ygl@cJvoXZ`-x2hKa4mNCfbne8g{q*Q z)NMWN#O~Y-wnK*cJ|f!x37Ld zJ;&$V>I^Bn4l;rPvA9n>qn(%GA$0e`GIhq&$2RKthQ5Iu7p6rTPum2n7xhhIxxb0q z*(UZ!;9sCk`Nv4^0Q0)~A*5?!-|mTJH;d=M6G-=k{ysb2gUfw!45f>N>2=>f7LE_&fL< zFy7AV$hU+2Tn&uhej@ibBF)SDf!xGBxLo96B=ycIqH^cDH>+4wOyK+NwGyHWZphI@tG=m`@PfJaL}mE_??02<(CS z86ZYy7aqp90lxncGtT{X(H}y78y*1qh-ZW`2lOM|B74Yw(Y3D?(r;MGj@h~qF`lMR zbK|pi84kiB?5|Ltv5YUqPyIZyn$phP`#s_x!Dr!%@JTRk6Rw3e90SihcVV1+%j(`6 zJ_Ec5xGUy)v2W?aprD>*=d%u77{L(6FrCR5m-kBtdhplaI4=SBC3kc9Zy;SC=WAt+!@`GUBKLX}Q%nSE~d(*L26J!(mg+Y-$q}1(Q%0t)! z+nJ+vVo{FwZS($ba1ZN-U%N;in;$GxF?>S=|1X z_p9#&bK`s1L1+Q@LA2FxP}3LVXIZ%mP1;@#p8x+-aJ{wL2kRZVH_w@O_Z|0Sv}>>q zTfm)X|CRkyj`>oa&!>mptzlV4P=Ps87xEJOu7mtXa9sZhzMFjxyra*+5Ad}w*{SKq z8rQHqDQMp)IKFAI>th`2w~4(Dcf&Vet;V;|zgfydVDr0+xcfc-o=NZCL(l@=yJP(5^Ze6h9yjCHWn&iC?+WtmeseQ4X(`y_UP|H#&Qh) z3cd#3OSw1y1D*RRK8Kyf*BE19yaDa!jX3lj^Dri{LRXe?g)}E(-t@O`v^eFpxRd7q};4oqPa9IG)c8&jKXz_6fhp99dx z@~G%aW7uX5EVoO!hqO+#TUPdq93$)XwwFPT3f*?vY$ui%v0YW{L+1G~^Y!jO06v?2 z=RO37;cDuQmoe%wvyZfOOI&5$f6;G5{SdV4r+-ywm$J5uky2ZttzXD`%L5of3pQY< zfW32^`S_0WDfoNv{G0pJMR!kF_Sv8h>ku)vk-Ol!sc*srwu&4h?Mu}4k*wD@soV4) zLLYiCDCIFytY0s-wIVIoZMG3(bfJP?!E)NPCj)e8199W;5a!?ab*0}qEyH0j|Mr)$ zl)VKOZRwkS{RMyHIUnumtE>uX%l0YLo^sKi7< zMW4mCmUFGv(RW}Q)S*ND>NW6rFxT#Z`m;d&pW(~EIz)amj$;_XMzN0<yt&+ykEfBXAIYpY?jV z#4+|_{0f_#uODr_3t-)1TYWFqCS&#St7DsNr+rxJ^`C(2?>ebZVS_oHVQ#lUJ@2^} zQht^Czr%Mx8)KRi+w=oX6y>RUGuB^*k_`@zKP}Zt?Ox9 zl*So=(mLB@yR38la=nxruwDEtPnnzVIe!D@TTU10Gr)au4^ZFP#eWNoZ5-Rn5bQ^r zjB8lVG3YD$*W2hnDQqG~wR+p?Yr8?gvOdO6-;AAped_9Zk8-zAAw`?A$=uN6x$oJ3 z2YCM<1;2|q4CLRxJw$(PWX3Xv^o!JPOntJhF7->ar@w8~(|1v~oTN`(T4xN=-Un?% z7#BG}PGAevl^e``o4N7XX0;FX_W*VI7x+AWKf)j1SXX>Qy2eAu{EyLvxNpgi{n6XaIGJz#t5#MrjUcG~PC{j49BdVL09U)d(hJ#5_?ZFFfdw=r}4P4N7if8UKh z2+QF2uiXEu4Scs}{;|eg^u*W~L%*!=ql@KiU$0lM7V0c7+H9X>8-275Us93bKuQowVCtY?n55ZMg=@X|WBEThIpW6L9S| zYUK)j%v@W@JtS+=(O;ht=&zhW+AOPY7HA)3m$eHawfwIywI`tdvX z)h3vGp8@9IGWBu)y9c-r;%~s4C02#({~EHN^cxiaw4YCQ@ky$huFZ0iK1$=%%YDj& z;=9YZnWLZM{H;u^PsY%v4I9PYLTVdAuh^_}&cuk^H<8-5U<&wm8c2OcFfP>fv@J^8 zOlnxx$F}QWKbF&OS+bpVmeW7w5ZfB`VNi6X)w^h>siuD@|4V`lrb zXIYtk`r4-qVQql{ifb_S%DyX+>dip5)g;ANG?{Hs*)8HO>mU{i1!vtJIV_6;-IYrVw&_}y# zOY9E*ekT;a4@kRG{~@TSR2TbLWE-1o6|x`wvhT$^rx|JQ}2BAQ{MsSrF4z8tJnM4qg_S&yGk6^D{?n3 z(st^~QIYC(v?p8W$pqd0v%PIBC)OwFm%heZhas%N0QyB*PU?1b;zXX8kmlcg;2!Wk z(DpvK1umP-JJ!N}CM9+cDcP4X)$8_pS$nsJby;7xSJ;A)JJ>b zy(Qlp7b%aa6Wc7JeNwZlC;D3NIJK#dU>zKPA9_Vv9v0LGzar*iuKlcM-o5`*dJi0f z>zLzxw5duvCwX0`Y|2kdD=cB@&SZ|w*QTNYSS(oJj{*Gmv@!tsE z{r?K)%sr6b^(vQ`-}m5tI0f8$F}Dd)wt#s4{yn}E$hLkjM6Axws5c+>IfPM>8%4KV zNBj90V?Q71Tdy;oIj~>t*_XB0Xwsx`UUvIB(wyU?*PX;iA5o0{S_&*DO59a>A zpyVy~fo1LPk1xU9@Ok(!v}k{lI1{9IM-S?4)xArIANypp$Pp6X$cf{ST>q5%$hgJ^ zx@4WQZc|Tx>oTq~$@ofbgCa9#+IrYwwE@2Cd+#?P&;Am+dx7tL;x``y{MVolU8t~6 zkv(WY_G`W9J1lzIQrafzqAyZmOZ2O^)7~zqH=*8sK-o6-zsVRMW)8fkMQ%O!=FI26 z?*X1;_l5ht1?HUd@85dk??D~pao~He)qeup7{fgy>cSp~?}bP7={h)G`i?PJ&UMr7 z_=Yt$&}~<@*+%=ghVd+?zf#gS?Q7VKm3DR8DP<43pg(%t_nhyL=G%SXesCWk{dY@m z`OdRPS%~G|3VnV=T<6s+I(5;%<^Bc8I9=)&sj$_tUjOv7E^R|}=iG-j3_#xrP#526 zevz?KKhIsh1A3-?=h+A5_;x80 zIX=fVSg0RU7W1))v8*%qN&2VVHu@zQYly86Qy9Q*kv-%DMy$UIMqWGTkXOU?z%h90!3t?>!K|^L+~WO<>*P-`}`akK^+;r1ydQ;S2CJ z@a(&%PQyRJt@P_UJBA!@4?U^d`q&4Ma)@qQ`kU{Ax<1BUXOpsQ!=#2b+v#iF6h>g| zQs+Bud>`ukGxy5O{}t$k`MuvSF^ts(`iS*CfxMaW9*m(4J+MDzzc4^bmX+3bi&Rgv zNp&95C#7*@07GcQ8uMFXT;BWlz*oVu>NDU@>VAxTJ2+?k_FYPo;=6Dzfm$5 zp9zuwA0hFH_W^V69-#mDZs9KKxCi16Y#|+EcY%tsSVy}!PeY`*W+T{uaSih}DX1sf zrl7BFwqOUek04`Oe~z&Y80Y1%AAIi-*C6kI^?d(RLc}rFdcl}2pr6%)(1y-Tk3FCt z{#66%n0kdiQnIYcd_Tl9VE%9Q4x{hG=Nt^7cOJUfl3!PEfBK|<$_iTxhNVuQHuQnI z_&3a+fAO6i8Ta~6&^-f7ls(gL7S13i;Qdd`_-_@kW}R(#x|H8QYGYmFzt{1*#Q7rJ z3;zJ-pXW{F$8od^j3>sgtlN6Uu5^t@Fow);T~3PK`iS!LTXn~# z>_V%Qn@H=8J%kEIpnU*+XcyG2Z$Yny^_D9b(f8ww!FRvEhp$ooIoyZ-c3_?z_Gf1g zeIN3t@H}`ous`Ct{{nIsUV+dnE$(H(*Jf#jKlGC3y#Y%whHRn zdawaw7=rmDZsc{XNb9uACTzo`R^IuQEpq;S#>T#m&ol2t`k6Md+=R@vdOim-@9qt` zg1C;s@kt-Lz_>fMI|6O^<05ZHdKZ}gPtDe+)4<>HId^{pU%Tl76)4AjN5-+TW)Ab-w1^7u2PcYgzU7Bc_Op+5oJV6J}* z)-i{;|GfjuJ?;H|pmPrSIk+3X3}3;v$5^QAo{dF85A7H9{z{m z1~C81>)}`_-^iFAfIHwMuvg-H^&Q&Uz6UL67g-hEavv;ri*8xk(1acgimokvUGri$ zukmj?|CxS&0gq93zHgtEBX_T%d;jl(IexORiQIyhVH=)-Gw?d>fxguF9EtA=-h)ow z<9*<($m3;fj;CA0aXMbfG565*+b9f6d4yyPu~ri#V~X6fe!gRRPeq>Ih4eXe0L-~L zHmBy>^7(Raeew?U`g7lXkY)53gX0_27^CmucP)GnJ_O6)INlGehv&unKXTti-dQ+_ z#JAHR#y0F0xsL2Z4~Ec&W}$_&Zmme`r48L8E93yQqsM;yN90H7i*lUx->1wTkI(kz zlJg&T!4x?G^ZzJpzz^X?a2{t0XOXXh{){{J80#2!z^5qx6Wk3w#^8L)Smt}6!seJo zy$ge)+hz>gK>snsMBtx0nK6?Af*e|^}w;V7zd8=o!r-n`vAIYb`@L#mtr%Ij`J*>fggZ5e+cde z+Qhkk2Ki*+1?11*6|mi#vsm$2*|qRJ!1v9&sDl;9(}t>s<+M3&$2%&hXL%DHV*ME3 zJkP&@?wLMNxmV9UP|JJnff(ChHkR1aee@P=Ft(Ry zOCDE03fI7u;CXlu?u0ww1awPT+bK8-R|Ec`A4j&~B-{`Bw-z)MtVORUH{V+rZ_1fRE*2b7zjoI;sf-0|m#m3KNmp&vlz88>g{(>n3I z=b6`TTYapvob}npy4(x7XWRp+lb`vyAD1yId(dKzuIJm~ec-cy3ESgvSHWB_gZaIq z*k~JhZX=DsevNV$S%G`w1hFnfdOyDbyYOxLv~SwQ`ZbEAO{^>JLk^Jx+J74Uj&|RI z*U+y4#v1W{j(i!Og&o*}M_?WFzXymHdvUGUtlNZ_U>lt0Pl&k>7;Bt6@0c&cSEzf2 zar7X^r!G0pEDx}4fa9Lh?jhP=0j{m@W9BN!Tq(`1_n9(bE#I;qnv@gU7)P0LtaA_4 z@}1{CWsGgY*tVestjBy#UG!bX%$mgWmi-cW?j!Gl6W|`Wqm;GLCicTJl6ajq=x5%K zLvKbrr>DflC+?TS*w4T>09%ZMequdo6Jye@KA_E~;q&;C&p4m1mFv0>+Z49p$FKv> z!w;aI`$LqS4{I8C$el>`V0=Gl`zK)nuM)Efmw$x`0BA$2V8GFtBre1pv#F-m2UiYZE??SuC0dfRW z#(xhy0C&MDxEsv-@lx&~t-HP0GXE{g#Eksg<^o9-{h~*foDgL(sd%)+A@1wDnK07bN_A)#UoZra3`OmR05raJu@4b7-ZR%K)&M`O* zkHHJz-r1b#v0k)`ym%Jv_Z)3|z}(_&4~nj=Uz3gN zoP;jus}FgN|L)39k%!^qpr3hq8lIrMZzel?NZQ9buOl57#GP@Jy04=f!?pGP{{@@@ zVg!#OzX$HS7Hx?a`OjE=%3IVe!7&(u`_R2JfhXZrVqXTi_Sd45Q?~7F-Hh9V0ef?V zf0z18!92VEw)3pJ2Qr2^v+iQKw@&}Wc-orqj>2N@)1LBwn*TB5cTM`h_+vh6NahoF zgp&EGyN;2^yO882?t|NrcK~f;tzAoV+J*|WTh{LkWB&u>IV7>-JH=}#pM@9TS#0L= z6=Va6pKan9MVuAe;P1a#@SQ&XJ7TT_bB_ITACj@hJ6<2@IEfQyegnA)lfqUh4`(vQ ziyrsDW619Udm(b^IsQYSe?KS0*+7qVp&!@J_rl1LIlCH^`wN*zrFk?*zb^mgyq@+=bMiL=Qr}qy2k#v6x$_m1?62NbFn_|sj~jw8}aUE3}fNn8H2w1IEH)S ze)uN1501hi=mK{~*zZ6tgLcQ>hi}6Ja2oCg_o8__00${g84EGY`3mc>GQze=oo9SM znAat!=T2Mhhuj1Ill<$`gv@VU=J`*X`DOlZdEQ&T0FHb93{W4G`LG6|ua$bGHhqRO zIe!+MV(xdqop3vxgkx|7j)QjV_4@{6aZNb?^XK8m(3wZv-RA$tl-~yAGwiP+T}$TZ zyJ&oFc0OMQ*U>Y{`gA5>|A)Z-%prG4d=KDr;Z=A69)~Bvx))&ww&6M0f~R2&4?z#E z2IKyme)a?J8;*6Q4f-{~JHlLCQKY(ifx0=F>$>gN>A8Q5XO7Kp=2}~lb*_u|lsFd4 z7a{iV#%vyu+sr@nj(iW1yTIDc^S_B++Qd0K%KW=G2Ik@((8k)v?~0a@ zj?w(#?|GT$-}eIV|JRY1L*4~fVIv3eTg)l;_rguU`H#EfhsY6_TX~5(=e{58$F)`x zJH99QC2}9Q#@s)#u8xT|F_!0%zB_yqZih9n&0(bZw|o~k&b(Lc!#w-EPn)?hPu3~3 zY`@wrMxOm_XWMMAEzxFw&bOZb)Gr{$yB6!LC3&DhLm3vK}4*AKud>@Q_<9q(I=H_jV#itpkZ)VogZ1@4B<03JeL2G$@x zx4%5AbN-PN=X^EzxjpmmUGZ+X7C6U|r)!XYzoCuXt{wu%^J}XYWVzgMZ}5JaKL!cg&%44&KC8a_%!A z&fjIod%?DzWzJFT75$vM=L(<5tP7=yuXaJk0r3*U%=0bO&|B3`LKU;Ro@TRC)QghxgX9a^*5i! zvR-K)lKV+pJ@20N9HZk@cYlg?b)7izS(rJ;Ip0H?=Qf{X8^~VyPJX@EDX)xRP?JOS zK5PKFig%?7$r^W#fV%tPG5YqrddHe?`Urg)XWv9AO7gm;VmcEH@TKjZ&3@G+!!!AIeb;DhiXa8HPPs0ohG zwNrO2oa=Z;r8#k}&9nX&V=?cxt=nvCIdgm=>7J0xvEx`OsAt~QvyAWj zTyLTepV8gE8P2&k^Ux+QptRXXd{N+P#C{3C41N*yqh) z|Mq8LRXg^Dwq^Qzq7ncE=%0H9gB|!!L5TQ*t*cvX5*5<@kKmMrX|<-%}*}VxIu(keTl`I_nl?ZA0kIWPJZ}jP{4%D0nt*0ra=z|0c@j zU%a2)6Fdvz{GUUz2lCv8?KJVpk>iZ>{~Gnp;^P@*OtF{TFZteQzFaeNW_}v8@xdbk!`+hgFK;ZF6O#`7K-e-qq?lx6UKd5C(-v3~9y z_m;7|v(2+P^z()1(6cWGz;}%gK-p{Yj?kj4zjsEidp;)|n``NQaD0wa^d)!kxp0hh zkJ}gHe@i~it$8+X=0IJWWPSQ%|0y%i+1{M8zA>h>Wn5)F_vg!h`r6h$z0+9J`0sy^ ztIT~>F(=Q)z1Y@Z8#Z86E~t>@PGzPp*5C2(%eUmy3M`84;A!9JJ4{V3WUle*Hg z?z-4U-8I!tzwzvHkJ+9*74NFwNB)GexpZ$|1DUQ{Hj~8tYe?f zJ8>;?AJqLa_TN%}5;!v}Q`mqtXfmh!;da?BJpB@ZKnS96d!@6Sc*p`@`W3WeT z*D-VN8r}frD)nV_&+v!gL$E|U`ib|eZz1o6JE0G>>9pWy_?cJq$fxbxQWQ{`?#XMMhui}>9^<*UPQVey&pO3B9%~$X zpo{IhaJJZ4vlX#^0#%Xe&w8)4U_=|&LBv|GEdvy=B*&{Sv?H zJ4m}%z;)e#ZS3SF{w8bwA?E0_*z;-oiIUub(SbEVtsu)AEA7NIh-u{+(CAs!gepb2|uJfEp~D-za#Wx-QxK_DRPX& zC(i$?Wjr?_Ju}_`HxxdGybq{QubK&Kb8{it4L&s#l-qmjd*V%UF!F!+u?*wuh_xpZy_rUeA4BioM zr;a-?_7H0qciJ^b$6$Z1b><+~J#%C~+LY!~xoFc~uS=h_8!O{4O2+oz>h#ZX80(Y3 z`Si2U$`saS`HSxX2H203`O#nGl{1y!_nC|NUf3);YvHpY+7U1ANzbTjeGGE#+;^V; zHkb>~63L10WVl-+?~fs0Ds{Ar?*l#4PhvAy%V4|~lpL9hc>i;bH-J9gHR{K~{(M&4 z3LS7CYS(rY920YnJbT9zH|~*#kmmDU;Cv2)cL2m#UqwFvuCMjB%k@v`m|SyxBy*`` zeg9jYwWW`3>!jT{^}J_nZQ1W)ea10Hu08GIb6&}xIrI()xrN+>Ns-o#imsF~ticfa zv$D_ZxQ{)D-uvzW&#L!8qTRK0z0Ai)fc1;J{wL^;?~mXZaQDW0w(~Nkcf9dDQxoZD^Y$A0y1^eaWpRdd|t;{GGrvYrbr6EaTJGyC%NJ{1);Iyo~))aBRkOuh`acvPWW%=9rzAW31=F zvgG<-EdS}7{`EMS%alvROthWPSDR$t#-;!GKFu7iGe7n~_s|Lso*oF~k-zidi z+BVVgpWpw)igx;Ri%#3PmtC_%&<6K`xfRb;1)g*D18@WApLeZo%(FRo2Y6=9<^KZr zf%&!mGWuc+opb0ng6GqGwSXLV_DVhF_oI3~C&h<1W-c8TIFVTG_kl(m}%~KO>OJA`UmXYKnKJPz-&iRbb1*egWBYw;C zAoB47V~uzBr;rc8m^Sns?{r6zXDGXFj>A3TK2rDGyRW=6gtd(EHPGEFhrxZDd8_9j zb@Q4z$sFrnx7nwAAnmrz+%KWm%i3+5WItK1$2BIznVukJ3$|edJs7|cSd)33^`ftB zSijJ1qpq}#vR81gSRTyqb7kb~y~vxuv(khLZiOY-53b>d;fA6!pIGC4l=T;Vy$8(8 z^+3+z8RGqIPT2#UZFm@d2z%f@vaNf}T%vcTumyL5zNg?(>V6ySUs_dPGeX^*xF3}IiED3O%}3@vb7Xzm)b-19%Jj9Y&bqvXtiPBnV~HHHK9NJ_ zKhJ*`y$2)MgdNzeF)cRRsE-T!4?)?3dhYcZz*<>b&-#a;4bHQI6X4p4_k(MABisz$ zyA9gDiF8fPO%u$Ce&*ABm>15cd5&j}xyy6w9+8(4aAG*^G%HX2>2ZRDBK3x zGVd*P&y#DOrty_sj>tz3Se2H~8$afAeoH-3vF&h`(DpCg)^cGT*NG5-8p8wl|;Vw&Y`; z_qxp(*6SBluixfw{BXpv`N@%L?YzePRrA@E*|jkHPi79Dnz~v*_m1 zePrEj@aZD|4Eag0U+o_R`mKZSA|xg5O*6Z(bO8|!kEGy+4nBOrB zp;wdIEGySx1o}vqb$XQXT@T*fuHDCB8O+rZc+OpG&RzW5-dE9`gXhOKP3%6;uZ7!y znDg9L=){Td4S$aASw0S@;8SoC+y|d2ka+XvdqCXXj_U|G9`*Nw`^P=zUhwQYmbU{r zk1_bH;Qo(0zx+zBZ;=bh zTu#vYv;0KvH%mQh6ZeC@X`3?F24&mYZUh5ZugTQ4>(>VTHfj4)#>ZUztQBXrLhdU% z=O>=S=B|nUCUOb9SA}(t^Z5q)G58BGpSQzbm9qK?{M;kv#&d4&MgKpC+z-ZR0OO1A zoM;zye}Z&xE`$5wdhotF0KW(3(R_IRX&e8J?HB0Y3H!mEx!#UlUvp!fZ6tG*vTk<| zrBB_iZ|(*4lFzr~+Wy>|qJADCXItb#ri`8Whu%kaVGJ8EfnC5Sa<5$*ro6I)t4w<@;M_f_^Dn2Ik#0XYJ!$n|sgL zGI+L6f_vbz;N5;JnB#YW`C&Yf+doBS{%=FKO$%-UHbFcSj3w6lzUcY?K*9dp1GZs2 zopVKhJNm=mzP8Rg!n-Q-y$@ZM0N;6TGk@kX^Q~Ubapu47tB>UVGNxQ!$Z}oUcOmjS zVf|&wxEWiV(KTeh*v;QIFvqz6w~BoS3DJhO;WI+^U;q^uV;!{H&N|D(8J+2DoUzBw z5$kpME;gUv=6@F^=+4ixU>=?C?_k&eS#;Oaz2Tf)FZaXyz#N}~3YPJG19=S0uY1Bf zpj-4?ki?AdAWkCXGex?ut_Ppz?*^YE?ge7TINg(;^Co?qg$K|b5AP%K9B7~~f#cRL z>W(#YUC(LS^bzao{+7*W#!Mf}+Fgh2M_uaquj!HN4RWy!W5&x^BgbpVK4sR{yE*dA zc;gP(L+Vdn;tt0r_QvL{yfQ#mpfoml3jb-b5jXO`PVARvV_l&xK65{hq+QI{HEmEP zZk&J1u8s3y4dS;qXV9IGd%!*8dAoF07co9g*=IKAI{tQK{!V~;T~yZ0DNtgCq=UgY2XsE>Pq{c29*9rYmkA@rwawuoWgE&=DT1ddxgchU~hD26W_nO=GTDhXMUdnV~@eR);0A^ zn!gXib>Mz+zTVll&dPBIm|M^O3HT(~#x?Z&2=Dn8m~V6Z5%4a#7W_P7Ufe@BfPUH! zf@7nUC@w<`7k+0I0^WpD+;yJK}G|mPL zfpR?mdB=AQ@mn4CN<2T=OY!&Imys_J$8+s-_7!knxW9-I-{0Sde4cvG-;Kb%8|V2z zvE7WU;ClR?LF#iW_{_fzmce|wZ+y3KuX)F7*R}#5fP@N&r9DlJRqH>G`LecEa=z90QM<794muH6Ii zj2%W8i2HX$Ed+t}nXU8ju$sUaR`D*I6 z(DU8k&(JyZzDI;@8R;2!JZ~cR&vfI&vu28X6`Sj4?p)V*fbR|0&dToR_&cF_`)%~o zVE%~_Id?qX0~g83TXM))-m*vR+dSKzb)S>*tgDlD+by!5Ah*9~6B^z4`Z?xaN1EUj^!~z)wJ* zN8ld#CY*rB;Xc?Zej*Rop?AQ${xFogCHA;ucHg?^ zo&n>G-~sqvDHAKk<{n{wZ+TalQuiYK92_J0kM~jY&icpm!M(se5PCgV=hJEPmi=H^ zKl8e;hUGeobs0O`xd$%+?<3DZ6F48<0df9!(VfdV>OTj64_|@5!uB%YZ+-YIA$MRK zT&ok{8j#z#tFOd9gg*3O4d_Q(d@g5fK0o5O#`NpFI>d`JeH>{#^WVjf`uJPCv2F81 zI0-GFANN~G`ibAK>hm&8iEEy{3tR(y<6PO-3+V3v^Yaqi4d%L{KjZ8N)+X*h$8$ee z|06KZTkvqy|>NN>rnC%`E>6d2XZ_gmvN7P{X0kVKP=LWPdmq?!p=Fz-3Cd=xH^I1ecF;_|2vRz8;sVj}uC{jJ^>hXA%uigWH3ZC3p|QE47S|?#u4uU?x~*v{mjq3{XPheRllp@PtZ>TG3U=D_bc;^`;T?iZ>a%Cy(@v?IMB9(U;JL(KFM3`g#*|Q!G5?BjzJ&1qgm@X_a~6D0^~NnAN&N;&xh<8 z@1cnQ9psOT?R6yiiuXNzEPHP_C;Ofv9nUg!;eL1&z5%C!F~;-%a-_L~n6q&#H%gg( z>psc#2>lnu=Mtnj!#4>({G)8$1&Fz4yYu;cfwunue+TS^SYy^OKBo^TkFb9c{t5jB zKDgDU?`Z=U_^R7)<2V3OV z`IY-3^O^0kzAop!#;Uc;wy{^-Q}WO7CHMk-9{&lsa9mBKbD$h`SEIWgj)gPndwQqB zX9vvvUeU>EdB&Z^JiUuz9r20#<5Fy|18W=4an54ooOy-B{@?b1b#K=E)a&`A zoOz$y)8BYvIc+6gw7G<~jA^w8eQ+Oq5&SIR*w?{%JcR9Y$lGB*wjEeQXHDZ=Z&PNy zooh(i#Ctw5Bd5DavE3d_fcEk3Fd~LI;`tlCT_of2Jt5xXzk_@dUI6AD?@NqfmGQ-W zzFegHp$R;<1LDR0xCVJ0m~V4k!Es;#;{Mn6QE*=v^XeI~m(1rgrGM@dn#|rS+Q`G; zo^dSigm(k`(YrqO^MgqI-1k2F@@|gz+Mi=%E|HVhk>-T8n^SW1-}XlOyjj{R^)*(; zOPjuL6wVhP|CkSZFh19P0bllf+=pL5f10sRp^xsJ_8R&G_KKZ(MoyL5cYr>leiKQ% zU>%ut`r3XAHeeg5cOCrP6?q?U|2NRru#=BCZx12Ia1KA;CFN?kuAqJZ%)=FM84xd? z3x1d5T~E;XUBRu$4w(1b-~`+Z?hpN1U_9GjGL!MHw}fupLE!UD$g7b03h$b+?`RkK zXU$@NcPZ0%y0G}qd1g-^jJ;G^IjV4b=5 zd!XZxG2erp`$_rR@NV$gW3Ih#)ZGv6F`payT*5ei1P$iUh7tNQxc=sP4_Kr5=UdOM z<*Zj)PO_Z!#z_6==#sYd&9*76PoK0Y^#|7?*7{21e#X3w{3bAN#~=CJLXwmD`JJK* zal*#DE}DO0hyMg=tYLxpu8Hr~k@pI@K6?kO2D9pKIUm1@vh28$H2j3s;|2l9i?ghs0 z-i&vEdzb@bk2$uGuJvp1Gk6)8@BBHDc~RE$X*q2vZ71ocZrnwC)}K#i+>-m)1K%YE z^YDDeXL{pV-hnk3&gvp>+vpSOn={OP?41U7<{3~Qd!>(DhcQ0;klNAXzOYV=MLW;U zJpXO<9@sZN@t*N8@;mU@Y@OrWdG^ga{_%G|^<&^ZsK7j$YxjWCTzmH2AJ+XDSf{W1 zLfix11%D2o0r!J==NtD3xZljb`c*UH`TG>(J6*<3%ou+cxr5zYm@D(2y1Cec|4Dvr z`zk2woX=LTSGNyI-Tv*9b@$m3=iU5FDK`K;?t#rRhv94vDT+d%BtKgzVPqZ8A2VaFHkddLd8&@Qx)6WA_&JwTgF;Sk&a=0g1%crSQv z&9yl;-|FU0X)g1wUqRQ`vQn7L{H{2G?)hgNt4HA$urKiru+MivgK^zg#@I&=pbP5G z(KU1Jcj0G+%-bGibCH-EZHeWy8N+sqq_0w+B>n6&b!Co0KVrujWDb%4eb{6_W%IZO z&S45;n9S~AF}pMcsH1f8zA#@1icM@w`uOIGiNu0&u!k-S55&yG8W(&e#4P zI0(z2@7oy1)8JaMPSv!G&v84>{>*M&ocmXhnVY?uo;glg_o@434j1#8_6teYeV%jm zM1SH(p2@*H|4Z0em-#u~MBjl8*rJa5*a!Ma6Rw2J{|5E_!VqcO?NV=wsY-zHP+DE(15J7Of1*FV~l5kJv4v!XI$|ftGtZ5*TMC5?asgiY@>V*v@y;Y z3;o333Vfd3Jwx~OfyaNVsKa5}lepvKxAx}bsc0=eEddRBi%4X3WFFrA@myxf)E}SjW za_&j;Fuxnr&0pH;xjdh4OflXf+U;BHw@&emv+SJoy%HFIoc}R4=eAu~FA_iV7XF^U zeT5}#uIU8Upbx{D9{Hc58^gHzSienM<`nP$w$Z;^>XeD&s0!LzMIRxDkhV5@uh2&h zz*rZW}R9UC&(CrTs9zz`j%0j$h<|AJVZj3!alra1LYGfo-6#$o*s{)Pn8QkORC^;gzuQm(-MwcFM<5WiiRBF}<(Racq|rMb#H zTHY;XZE4q*XtRwNGySrheOXtx+22c0-vjtYZW>7Ad;UGsEy`Ok1oLG6DR=OTT)RK? z-&a^d!duS&fHLjl`TGs@F>J%y%)T;0PHD$ykj^=D%YEpAZPld-L$F;7+Mrz@?djV^ zx2_5L^}xMS72W!}u8;o8#Ik<9g1U8<`%po%(Ee4q8lg8poAoUiz>vA!1+KZg4CY|B zCeIf8`SLiW&ieGReU`1udS&*J`K`CH-hR)QfAZUDFjmKu`9DP2oNoYaV=uUt=kPfS zcZ28IxXd@&EzRoZ_eP&G^N;Vwu0!7fZM*oNLK=62`b#PAp)-!C?;!^@vaGC%PZ!w+ zWuwTf*Qe;-5i6}yufKY)l+`Qfm$LPuP0~O8w5dxMDrgp6Uv0f2*N{WNC;nD6MY5i; z{y#>V#}^^*gR{k^U0-vU`8rR|=00^lf-|E|7z7`X)Q0sLYQG|;C&PJ=#jNZp(G|1I)ixEh`JYQHZDUt;=B9q*yrGyhoE zZi!JLjnjm6u&f+_cIiS78sNH9U+Fi57Fgdd=%e0)tm~s!(40y4OzazDY45_YJZo%& zE$S!ew&_zIAqUU`d}ADKWRJOFk9@v>GzXcdouX?qk7o<@oT~r7U@YTG#+@$2uxwwi zz%FC7%(;(q*+4%5=5-Usunp&+n#oR!oZW`cnOVEo2h26zy{QZT9&!lC`1d(qr40#+ z?^=xS`L}K#HoiM|j?tHUC)Yitak{V$^bwkKwb?`z@yaXS|U5t#os z(11?*Q2@(p)X(Ic=HW%x9JvS4VzQsPB(@{-@NRh5A0Q zZ0iVgW0qGeZaMew)#W-&98^Pgq{5|-)HSvOvfMh;tNRgZT`)jy7EHkpKVgF z%WTIwMGR%`gOr)`tjqEgUpYI&Jsf%c9`pG}^j|XeUBD;K8|xF_k4>oeJ8{p!Y4}%Q zesQngfX`8I9f%jX!{0dW!&RUEw)1aiH)A_R9r5G$!Pg)kqwM<8#$1`V=zG1`jVYFu zYv3AhLa(&B7wNj(4vZsm<~}ul@5Dy@`1b&`kKb-Hh7R%K_bm_O(**xE@E7n`z!>7U zT~j1$Wp3j>AWpo?7`FwE-~DnYKD#r&7$bV80&|_Y^B&l#*{qXoc)p-sFV}svXFTU> ztdx1q7xTYZ&U=A5#k`0Sx%dHd`8W6&_HV#laHQ0AvF``(@P7yM$$Cfr{{cV8V9xfy zc&tn8g%NTczb}F@{QlgvVvltiuuK`^-v@5dZi3!~C3MR&E_dkweF*Je+2gYazVX}L z9+JA~>uAxn@tlg^ExTX2Oyc?X5HZf7-vQ(%{#N)O#ZQ0c9)C0T-ueo#C*yC3{vG+F zVBFhidyF`nNc-h}Sfw0yt#Oy(NpNk-WG}#;@;5*kk;s!9DOPaIJnsJ@&XK?QO;iSHy3})bF6~ZXg$NKhh@N zk2bJ<41Ue6L0x0HtGj1m9bLjTt$2zq}Xh`!|6(#_!KV&pO8WzrWP&&iXL7o=;``e%f{Ao>^VOX8RtDsB`U}g=0XSzX^|L z&VcfRZ~&-}v$=xgZj0Y_nDYs+9`Uzfb7=jiurbDnXAI^Y?>po_#$p}omTO`k-v4nQ zeiiuz>KS+BF4x-p{W>zI>6hiqdFI;sw7I7w+vJ|fw%T1E{j?5? z?qkZn+xwh#UE}>}NZB<%M7??AIT`=X;bC;|#4cs@l`*hR`ue=~YXTEEC#)Ca< z_Q&7X=f9uWL2puzjJ;&}X?*Sg_vby7ofGAiJ`AA;U2IRn6dIJbXLX$xr5vS)Vp@ub=B7i+x$vUT>>yA^)y-{NB|yaSwbISciD8Ilwwj;arhh zv}aA?H!|#l`8gm)XEN(6a@(KPMSf@-p9@FW7=P@IB0B?O-AMhnvC}@|MW1`6OrP=D z!7;hkU&8+laQ?^fAx7*g`!t5-HR>8j+g=KX;WoGnz5ri`e+Tyf`#AQ6cLVoYXA|~8 zHH#5@pYcZipT*|}$oHWCi*Bs{1Fju?#`}e1bFDmQuK&d_hx$wUW&Z6~UAwxdXT37> zp7xB9GTZ5w`QJf*X@+a=S&QFC{TI@8{_?EsXV=KRK4Y-me(L-j_Y|;Bu6eFetUu*A zyR3!JjK~dZ5xL(*8iz4Sv&p+dg-&pb-kDTRN{~_hy!2f0FgJZFu zF}i(kBM;!?^ZQQlynYru^O-ZH&pglX@55iw#xv7`r6TV^{(?I9$LGK`%697amNIwX zTh5zt%t7Y%Led;E?(kF3JgPef%gQ9{({4`FW*_w!mNV}u9hY~2xnzH?jF8MX&U7F7 zHp<^e9zmKv{2~`)B;`08Zz2y+cHFMhl`|Q6?P4DmY(Irv*aOQOFu}h8_{I5W4P$SS z!+1xdF4`FXHGFuk`>qx5re8z4?*9g!<(mrht51CAV?Pk@cFZw8&wUbUuK%-O4DSG? zdxG|>hiJ1*TgDgtZ&POMK6m1byZ3xP=DD}6wmWBjk$=~B3c24aXq-j$y<#J#`SkNntkDiq zcCnGW$Zx;YokOn8>~S7l(=p&1@u~NI@jmVJun{NrR&yp}-)y3v#pfXP`ah0t{#^6Z zVBX|w@HfO6Wlr5m|bWAVoq5B}fl@$YGIE zLJ46BMV3;^;-UEYyq&lFy2SSI;my2x^JaeYotfSJr3F8?^&J}2<9)zB`qDl#jqe$7 zJjcr{0^e0_#AZ$cJyJ z|0#U*4gKrbG3up{=VJCbd^LRY*vr&Eg|l!Dz6JF_pZNO&W5%82UDCt)C%X5_9AZ8oXPyKJf->?B{Z`tR19O`L~)XchN zwX&S;#;_j4So3CkwQu^gqhnX~XU=(!<2fd&lYgk;g|ZIg*kx#tZwl(A-o?7!_QOWs z#nMJ>n9eVUzVhs;sU2pTK8Mz}r?Xf4;h&_mmTfa{>h7Kr*T8sl+m^A`tGRX?dsP1x&r81eykz}{ z_wyOb=yQ~3i8Wop8jB7AxwW3l_?)$G6Bfa^WiYS(%wfDVpbPlo_wxblJazg;F7$}F z-XPBV)PLh0_l>#An6ZC4w4DaeG5V{wW5)kqx`+K0?gIVd?=Y;{>~Z{apzeNmI0obRJ=dAYFb;1(7_GuO36y@u@$*3i2)+WPp_mi(b9^{W1%2bx9S zCT)vAu84Kr$181)(*|>4XNaGj!hT%*`X&a%JjfOA|6amQ60-#KiJ02J7w_LZqx*sH ziLuuhC%&_v#i!2fpE&;uSoeu%**n3rtgaux3HTg%5Ow$6cibP;{cRAR=ef3OCh9L) zuUgmL_^M8+t^UMkPSRJkna94YTbzIOx171PS+4fAkGY*!2h=}tAEf@vWo`S|F3=~x z1E_s`9_wpCuVCGH>O+lfS0k-CnPc4Rb*%BtV)fG=?_6KSo&;lh*fDIUv{R1vf3ILC z@i`vanm6~`%ryF(rv3{Yq-_ZtpB!SMc%`m$ZW{W4G1efCkR`7@vWa!z?q zjcD)ZRIJr9b`iP-`iA~Za*RQpI%7txF+FfyYS=z@FYKYrdWG&<gF?U9Y1q&%y=Jtf|z5J(Jf*clxu}a+9u#h>KC!R zGw}C*yf?7_ed^vXmw|bN{-?1kpw_8@+Pa>uuUKY2p-;QmTyL$u)bdd_^X+CG`*DnZ z7MRDeotyK|JAadMmD|*}STCM4G-J&k(tn=%5;3lkYj0e$#PqP{x2*>1j~(VR7LB}% z{IT^DGo|$W~2Fxw)UCZXYS?nL!s7;dDmClY_HZcHv4VRz6o8}g6&BVvTnU+j_-ky$=6?&vg#14q$&QG3HpOZr;}@lWV3y%*)s>s8f!d z}jlDUAohW@zQnsHy%ZK3yXoC8s{C`mU^7IW$byN zU+7CrtaX#PJupU{afWBON7*sl8??n)?c*~~tDyeI(J%H7_UI-M=fACpGtK!4ZRd!2 z2d)Bp!n{Y92JyZdiu?Hzb@MfV9C3Ge-jBi8umInH@2^kLeh}1|ImNqb*X(Dw2v@+f zt8Wc%RLahG73{ZO{C{Fsz&N$Fk9F-|ka1e;8CTs0`Z6}-9lu((p1#awpWXUpoMU8e z?FQV0E$G6X3f8y5HlaM!f1Yv&Tw_^;7BO`gr)(Q*66f6Z_MjY}*Av9lsIw-azj4kV zU+kSe^=65o&H2T5?PsyC!+v-ZKXY}A(9JOyDIcfJ`@BJYskGh2hsN>S%zIzm=fU^* zbMP*4hk$d+N)tJnm^S8}@j@N Date: Sat, 6 Jan 2024 14:33:11 +0100 Subject: [PATCH 0038/2374] Add code formatting to ImageCms.Flags docstrings Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/ImageCms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 9a7afe81f2b..62b010f4512 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -147,7 +147,7 @@ class Flags(IntFlag): USE_8BITS_DEVICELINK = 0x0008 """Create 8 bits devicelinks""" GUESSDEVICECLASS = 0x0020 - """Guess device class (for transform2devicelink)""" + """Guess device class (for ``transform2devicelink``)""" KEEP_SEQUENCE = 0x0080 """Keep profile sequence for devicelink creation""" FORCE_CLUT = 0x0002 @@ -159,7 +159,7 @@ class Flags(IntFlag): NONEGATIVES = 0x8000 """Prevent negative numbers in floating point transforms""" COPY_ALPHA = 0x04000000 - """Alpha channels are copied on cmsDoTransform()""" + """Alpha channels are copied on ``cmsDoTransform()``""" NODEFAULTRESOURCEDEF = 0x01000000 _GRIDPOINTS_1 = 1 << 16 From a786a0551b75ab3e85da1bfad54226faa2336022 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Jan 2024 16:17:57 +1100 Subject: [PATCH 0039/2374] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ac961a680a3..ebf731c5659 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Add LCMS2 flags to ImageCms #7676 + [nulano, radarhere, hugovk] + - Rename x64 to AMD64 in winbuild #7693 [nulano] From bb5527484536f6c8e90ffffd95906c352fecca18 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Jan 2024 18:49:01 +1100 Subject: [PATCH 0040/2374] Removed PPM loop to read header tokens --- src/PIL/PpmImagePlugin.py | 53 ++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 25dbfa5b0bc..82314214a7f 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -105,40 +105,31 @@ def _open(self): elif magic_number in (b"P3", b"P6"): self.custom_mimetype = "image/x-portable-pixmap" - maxval = None + self._size = int(self._read_token()), int(self._read_token()) + decoder_name = "raw" if magic_number in (b"P1", b"P2", b"P3"): decoder_name = "ppm_plain" - for ix in range(3): - token = int(self._read_token()) - if ix == 0: # token is the x size - xsize = token - elif ix == 1: # token is the y size - ysize = token - if mode == "1": - self._mode = "1" - rawmode = "1;I" - break - else: - self._mode = rawmode = mode - elif ix == 2: # token is maxval - maxval = token - if not 0 < maxval < 65536: - msg = "maxval must be greater than 0 and less than 65536" - raise ValueError(msg) - if maxval > 255 and mode == "L": - self._mode = "I" - - if decoder_name != "ppm_plain": - # If maxval matches a bit depth, use the raw decoder directly - if maxval == 65535 and mode == "L": - rawmode = "I;16B" - elif maxval != 255: - decoder_name = "ppm" - - args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval) - self._size = xsize, ysize - self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)] + if mode == "1": + self._mode = "1" + args = "1;I" + else: + maxval = int(self._read_token()) + if not 0 < maxval < 65536: + msg = "maxval must be greater than 0 and less than 65536" + raise ValueError(msg) + self._mode = "I" if maxval > 255 and mode == "L" else mode + + rawmode = mode + if decoder_name != "ppm_plain": + # If maxval matches a bit depth, use the raw decoder directly + if maxval == 65535 and mode == "L": + rawmode = "I;16B" + elif maxval != 255: + decoder_name = "ppm" + + args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval) + self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)] # From ba6399cad14d816cafc44c25782d6a3153082ae4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Jan 2024 19:34:27 +1100 Subject: [PATCH 0041/2374] Added PerspectiveTransform --- Tests/test_image_transform.py | 2 ++ docs/reference/ImageTransform.rst | 5 +++++ src/PIL/ImageTransform.py | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 15939ef647c..578a0a2967a 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -16,6 +16,8 @@ def test_sanity(self): transform = ImageTransform.AffineTransform(seq[:6]) im.transform((100, 100), transform) + transform = ImageTransform.PerspectiveTransform(seq[:8]) + im.transform((100, 100), transform) transform = ImageTransform.ExtentTransform(seq[:4]) im.transform((100, 100), transform) transform = ImageTransform.QuadTransform(seq[:8]) diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst index 1278801821d..5b0a5ce49dc 100644 --- a/docs/reference/ImageTransform.rst +++ b/docs/reference/ImageTransform.rst @@ -19,6 +19,11 @@ The :py:mod:`~PIL.ImageTransform` module contains implementations of :undoc-members: :show-inheritance: +.. autoclass:: PerspectiveTransform + :members: + :undoc-members: + :show-inheritance: + .. autoclass:: ExtentTransform :members: :undoc-members: diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 4f79500e64e..6aa82dadd9c 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -63,6 +63,26 @@ class AffineTransform(Transform): method = Image.Transform.AFFINE +class PerspectiveTransform(Transform): + """ + Define a perspective image transform. + + This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel + (x, y) in the output image, the new value is taken from a position + ((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in + the input image, rounded to nearest pixel. + + This function can be used to scale, translate, rotate, and shear the + original image. + + See :py:meth:`.Image.transform` + + :param matrix: An 8-tuple (a, b, c, d, e, f, g, h). + """ + + method = Image.Transform.PERSPECTIVE + + class ExtentTransform(Transform): """ Define a transform to extract a subregion from an image. From 6d99f9193f0abe211e611f2d9909a4eb5881d270 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Sun, 7 Jan 2024 16:00:58 -0500 Subject: [PATCH 0042/2374] Fix info for first frame of apng images getting clobbered when seeking to the first frame multiple times. --- src/PIL/PngImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index e4ed9388011..823f1249285 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -378,7 +378,7 @@ def save_rewind(self): } def rewind(self): - self.im_info = self.rewind_state["info"] + self.im_info = self.rewind_state["info"].copy() self.im_tile = self.rewind_state["tile"] self._seq_num = self.rewind_state["seq_num"] From 08f11c57a131b402f405db76dfd411b441df1ab5 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 20:52:47 +0100 Subject: [PATCH 0043/2374] deprecate ImageCms members: DESCRIPTION, VERSION, FLAGS, versions() --- Tests/test_imagecms.py | 13 +++++++++++-- src/PIL/ImageCms.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index fec482f4393..9575b026db2 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -49,8 +49,8 @@ def skip_missing(): def test_sanity(): # basic smoke test. # this mostly follows the cms_test outline. - - v = ImageCms.versions() # should return four strings + with pytest.warns(DeprecationWarning): + v = ImageCms.versions() # should return four strings assert v[0] == "1.0.0 pil" assert list(map(type, v)) == [str, str, str, str] @@ -637,3 +637,12 @@ def test_rgb_lab(mode): im = Image.new("LAB", (1, 1), (255, 0, 0)) converted_im = im.convert(mode) assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) + + +def test_deprecation(): + with pytest.warns(DeprecationWarning): + assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") + with pytest.warns(DeprecationWarning): + assert ImageCms.VERSION == "1.0.0 pil" + with pytest.warns(DeprecationWarning): + assert isinstance(ImageCms.FLAGS, dict) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 62b010f4512..827755bf672 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -20,8 +20,10 @@ import sys from enum import IntEnum, IntFlag from functools import reduce +from typing import Any from . import Image +from ._deprecate import deprecate try: from . import _imagingcms @@ -32,7 +34,7 @@ _imagingcms = DeferredError.new(ex) -DESCRIPTION = """ +_DESCRIPTION = """ pyCMS a Python / PIL interface to the littleCMS ICC Color Management System @@ -95,7 +97,22 @@ """ -VERSION = "1.0.0 pil" +_VERSION = "1.0.0 pil" + + +def __getattr__(name: str) -> Any: + if name == "DESCRIPTION": + deprecate("PIL.ImageCms.DESCRIPTION", 12) + return _DESCRIPTION + elif name == "VERSION": + deprecate("PIL.ImageCms.VERSION", 12) + return _VERSION + elif name == "FLAGS": + deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags") + return _FLAGS + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) + # --------------------------------------------------------------------. @@ -184,7 +201,7 @@ def GRIDPOINTS(n: int) -> Flags: _MAX_FLAG = reduce(operator.or_, Flags) -FLAGS = { +_FLAGS = { "MATRIXINPUT": 1, "MATRIXOUTPUT": 2, "MATRIXONLY": (1 | 2), @@ -1064,4 +1081,9 @@ def versions(): (pyCMS) Fetches versions. """ - return VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__ + deprecate( + "PIL.ImageCms.versions()", + 12, + '(PIL.features.version("littlecms2"), sys.version, PIL.__version__)', + ) + return _VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__ From ccdea48cf379fac585918001f304a7ab58448b96 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 Jan 2024 10:36:30 +1100 Subject: [PATCH 0044/2374] Added identity tests for Transform classes --- Tests/test_image_transform.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 578a0a2967a..f5d5ab70408 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -10,20 +10,25 @@ class TestImageTransform: def test_sanity(self): - im = Image.new("L", (100, 100)) - - seq = tuple(range(10)) - - transform = ImageTransform.AffineTransform(seq[:6]) - im.transform((100, 100), transform) - transform = ImageTransform.PerspectiveTransform(seq[:8]) - im.transform((100, 100), transform) - transform = ImageTransform.ExtentTransform(seq[:4]) - im.transform((100, 100), transform) - transform = ImageTransform.QuadTransform(seq[:8]) - im.transform((100, 100), transform) - transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])]) - im.transform((100, 100), transform) + im = hopper() + + for transform in ( + ImageTransform.AffineTransform((1, 0, 0, 0, 1, 0)), + ImageTransform.PerspectiveTransform((1, 0, 0, 0, 1, 0, 0, 0)), + ImageTransform.ExtentTransform((0, 0) + im.size), + ImageTransform.QuadTransform( + (0, 0, 0, im.height, im.width, im.height, im.width, 0) + ), + ImageTransform.MeshTransform( + [ + ( + (0, 0) + im.size, + (0, 0, 0, im.height, im.width, im.height, im.width, 0), + ) + ] + ), + ): + assert_image_equal(im, im.transform(im.size, transform)) def test_info(self): comment = b"File written by Adobe Photoshop\xa8 4.0" From edc46e223b1275f9b197c33011f305af2975afe3 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 01:27:41 +0100 Subject: [PATCH 0045/2374] document ImageCms deprecations --- docs/deprecations.rst | 39 ++++++++++++++++- docs/reference/ImageCms.rst | 3 ++ docs/releasenotes/10.3.0.rst | 83 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/9.1.0.rst | 2 +- docs/releasenotes/index.rst | 1 + 5 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 docs/releasenotes/10.3.0.rst diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 0f9c75756ee..7602f8b4e80 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -55,6 +55,43 @@ The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant for internal use, so there is no replacement. They can each be replaced by a single line of code using builtin functions in Python. +ImageCms constants and versions() function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.3.0 + +A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. +This includes a table of flags based on LittleCMS version 1 which has been +replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``ImageCms.DESCRIPTION`` +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +===================================================== ============================================================ + Removed features ---------------- @@ -118,7 +155,7 @@ Constants .. versionremoved:: 10.0.0 A number of constants have been removed. -Instead, ``enum.IntEnum`` classes have been added. +Instead, :py:class:`enum.IntEnum` classes have been added. .. note:: diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 22ed516ce58..c4484cbe22a 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -24,14 +24,17 @@ Constants :members: :member-order: bysource :undoc-members: + :show-inheritance: .. autoclass:: Direction :members: :member-order: bysource :undoc-members: + :show-inheritance: .. autoclass:: Flags :members: :member-order: bysource :undoc-members: + :show-inheritance: Functions --------- diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst new file mode 100644 index 00000000000..ddcb38aa161 --- /dev/null +++ b/docs/releasenotes/10.3.0.rst @@ -0,0 +1,83 @@ +10.3.0 +------ + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +ImageCms constants and versions() function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. +This includes a table of flags based on LittleCMS version 1 which has been replaced +with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``ImageCms.DESCRIPTION`` +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +===================================================== ============================================================ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 02da702a799..6400218f467 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -51,7 +51,7 @@ Constants ^^^^^^^^^ A number of constants have been deprecated and will be removed in Pillow 10.0.0 -(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. +(2023-07-01). Instead, :py:class:`enum.IntEnum` classes have been added. .. note:: diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index d8034853cc2..e86f8082b48 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 10.3.0 10.2.0 10.1.0 10.0.1 From bb855583ea3320b637e7f6511721a9e2655f2b99 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 01:55:32 +0100 Subject: [PATCH 0046/2374] Update PyPI links to use pillow (lowercase) --- README.md | 4 ++-- docs/about.rst | 2 +- docs/index.rst | 4 ++-- docs/installation.rst | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6982676f518..6ca870166a1 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,10 @@ As of 2019, Pillow development is Tidelift - Newest PyPI version - Number of PyPI downloads `_ and by direct URL access -eg. https://pypi.org/project/Pillow/1.0/. +`_ and by direct URL access +eg. https://pypi.org/project/pillow/1.0/. From 3515f997ce06664d8ec42cc21e39eef9897dbaeb Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Sun, 7 Jan 2024 20:42:52 -0500 Subject: [PATCH 0047/2374] Add test against info of apng images getting clobbered when seeking to the first frame multiple times. --- Tests/images/apng/issue_7700.png | Bin 0 -> 233 bytes Tests/test_file_apng.py | 10 ++++++++++ 2 files changed, 10 insertions(+) create mode 100644 Tests/images/apng/issue_7700.png diff --git a/Tests/images/apng/issue_7700.png b/Tests/images/apng/issue_7700.png new file mode 100644 index 0000000000000000000000000000000000000000..984254b8e5632f9f1bf148485a77bb6a141dcb4b GIT binary patch literal 233 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`9Er&xK0ulYh#5ZjPA&jaQfUw| zkPuh{hyel)U@p%GyA42&x~Gd{NX4Aw6SvYo%ojStp!80BRioiiwh7HElNnUodZY}p z4j33RFfd-v>sQUc#0b;^GZ@51GuQzr6w`P67?6@pOK}VV(o8_Z4dze!4m2ES)C$JM dY&_!34DTjb$p7CmBOj=M!PC{xWt~$(698zJHUj_v literal 0 HcmV?d00001 diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 60d951636e8..8069d4b08bb 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -689,3 +689,13 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat ) with Image.open(test_file) as reloaded: assert reloaded.mode == mode + + +def test_apng_issue_7700(): + # https://github.com/python-pillow/Pillow/issues/7700 + with Image.open("Tests/images/apng/issue_7700.png") as im: + for i in range(5): + im.seek(0) + assert im.info["duration"] == 4000.0 + im.seek(1) + assert im.info["duration"] == 1000.0 From bddfebc3315d85bf822192c57a33646d79e738c3 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 12:57:23 +0100 Subject: [PATCH 0048/2374] add license comment to ImageCms; explicitly say "no replacement" for deprecations without a replacement --- docs/deprecations.rst | 4 ++-- docs/releasenotes/10.3.0.rst | 4 ++-- src/PIL/ImageCms.py | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 7602f8b4e80..4c9abe19503 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -67,11 +67,11 @@ replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ -``ImageCms.DESCRIPTION`` +``ImageCms.DESCRIPTION`` No replacement ``ImageCms.VERSION`` ``PIL.__version__`` ``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` ``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement ``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` ``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` ``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index ddcb38aa161..8dfe34d95fa 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -20,11 +20,11 @@ with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ -``ImageCms.DESCRIPTION`` +``ImageCms.DESCRIPTION`` No replacement ``ImageCms.VERSION`` ``PIL.__version__`` ``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` ``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement ``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` ``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` ``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 827755bf672..3e40105e46a 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -4,6 +4,9 @@ # Optional color management support, based on Kevin Cazabon's PyCMS # library. +# Originally released under LGPL. Graciously donated to PIL in +# March 2009, for distribution under the standard PIL license + # History: # 2009-03-08 fl Added to PIL. From f044d53fd181446366ab78f570076a665933d4b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Mon, 8 Jan 2024 17:17:17 +0100 Subject: [PATCH 0049/2374] swap conditions Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/PpmImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 6be1278eb2e..d43e21e14db 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -115,7 +115,7 @@ def _open(self): for ix in range(3): if mode == "F" and ix == 2: scale = float(self._read_token()) - if not math.isfinite(scale) or scale == 0.0: + if scale == 0.0 or not math.isfinite(scale): msg = "scale must be finite and non-zero" raise ValueError(msg) rawmode = "F;32F" if scale < 0 else "F;32BF" From 5dd1652f2775bb916faa648fe48c7c6350d2c8a4 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 17:16:23 +0100 Subject: [PATCH 0050/2374] use filename instead of f --- Tests/test_file_ppm.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index efdb880de07..d8e259b1cf8 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -89,20 +89,20 @@ def test_16bit_pgm(): def test_16bit_pgm_write(tmp_path): with Image.open("Tests/images/16_bit_binary.pgm") as im: - f = str(tmp_path / "temp.pgm") - im.save(f, "PPM") + filename = str(tmp_path / "temp.pgm") + im.save(filename, "PPM") - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) def test_pnm(tmp_path): with Image.open("Tests/images/hopper.pnm") as im: assert_image_similar(im, hopper(), 0.0001) - f = str(tmp_path / "temp.pnm") - im.save(f) + filename = str(tmp_path / "temp.pnm") + im.save(filename) - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) def test_pfm(tmp_path): @@ -110,10 +110,10 @@ def test_pfm(tmp_path): assert im.info["scale"] == 1.0 assert_image_equal(im, hopper("F")) - f = str(tmp_path / "tmp.pfm") - im.save(f) + filename = str(tmp_path / "tmp.pfm") + im.save(filename) - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) def test_pfm_big_endian(tmp_path): @@ -121,10 +121,10 @@ def test_pfm_big_endian(tmp_path): assert im.info["scale"] == 2.5 assert_image_equal(im, hopper("F")) - f = str(tmp_path / "tmp.pfm") - im.save(f) + filename = str(tmp_path / "tmp.pfm") + im.save(filename) - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) @pytest.mark.parametrize( From 586e7740947933454589f3c60b22387e01fa5701 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 17:35:01 +0100 Subject: [PATCH 0051/2374] add PFM support to release notes --- docs/handbook/image-file-formats.rst | 3 +- docs/releasenotes/10.3.0.rst | 49 ++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 docs/releasenotes/10.3.0.rst diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 02a6a3af729..569ccb7691b 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -701,7 +701,8 @@ PFM .. versionadded:: 10.3.0 -Pillow reads and writes grayscale (Pf format) PFM files containing ``F`` data. +Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files +containing ``F`` data. Color (PF format) PFM files are not supported. diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst new file mode 100644 index 00000000000..34afbe4b844 --- /dev/null +++ b/docs/releasenotes/10.3.0.rst @@ -0,0 +1,49 @@ +10.3.0 +------ + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +Portable FloatMap (PFM) images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for reading and writing grayscale (Pf format) +Portable FloatMap (PFM) files containing ``F`` data. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index d8034853cc2..e86f8082b48 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 10.3.0 10.2.0 10.1.0 10.0.1 From a844871c5eec479275af20ae0f06e4204174e9a6 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Mon, 8 Jan 2024 15:18:49 -0500 Subject: [PATCH 0052/2374] Give apng repeated seeks test and image a more descriptive name. --- ...700.png => repeated_seeks_give_correct_info.png} | Bin Tests/test_file_apng.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename Tests/images/apng/{issue_7700.png => repeated_seeks_give_correct_info.png} (100%) diff --git a/Tests/images/apng/issue_7700.png b/Tests/images/apng/repeated_seeks_give_correct_info.png similarity index 100% rename from Tests/images/apng/issue_7700.png rename to Tests/images/apng/repeated_seeks_give_correct_info.png diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 8069d4b08bb..47d425f8b74 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -691,9 +691,9 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat assert reloaded.mode == mode -def test_apng_issue_7700(): +def test_apng_repeated_seeks_give_correct_info(): # https://github.com/python-pillow/Pillow/issues/7700 - with Image.open("Tests/images/apng/issue_7700.png") as im: + with Image.open("Tests/images/apng/repeated_seeks_give_correct_info.png") as im: for i in range(5): im.seek(0) assert im.info["duration"] == 4000.0 From a6051a4045354203d9fccafbe2ac620c505d9e00 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Mon, 8 Jan 2024 15:20:24 -0500 Subject: [PATCH 0053/2374] Add type hints and fix some formatting for the apng repeated seeks test. --- Tests/test_file_apng.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 47d425f8b74..ea5ab41ec60 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -691,11 +691,11 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat assert reloaded.mode == mode -def test_apng_repeated_seeks_give_correct_info(): +def test_apng_repeated_seeks_give_correct_info() -> None: # https://github.com/python-pillow/Pillow/issues/7700 with Image.open("Tests/images/apng/repeated_seeks_give_correct_info.png") as im: for i in range(5): im.seek(0) - assert im.info["duration"] == 4000.0 + assert im.info["duration"] == 4000 im.seek(1) - assert im.info["duration"] == 1000.0 + assert im.info["duration"] == 1000 From 931821688c0f9ef331097ac8b1358780eb82b21e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 12:22:25 +1100 Subject: [PATCH 0054/2374] Added release notes --- docs/releasenotes/10.3.0.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 34afbe4b844..391068769fb 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -26,10 +26,12 @@ TODO API Additions ============= -TODO -^^^^ +Added PerspectiveTransform +^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +:py:class:`~PIL.ImageTransform.PerspectiveTransform` has been added, meaning +that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding +subclass of :py:class:`~PIL.ImageTransform.Transform`. Security ======== From 6c320323b44df48a5162a2113ae2c34e7c9bf564 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 12:47:27 +1100 Subject: [PATCH 0055/2374] Only set row order when needed --- src/PIL/PpmImagePlugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 83e028718eb..9d37dcde099 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -311,7 +311,6 @@ def decode(self, buffer): def _save(im, fp, filename): - row_order = 1 if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": @@ -322,7 +321,6 @@ def _save(im, fp, filename): rawmode, head = "RGB", b"P6" elif im.mode == "F": rawmode, head = "F;32F", b"Pf" - row_order = -1 else: msg = f"cannot write mode {im.mode} as PPM" raise OSError(msg) @@ -336,6 +334,7 @@ def _save(im, fp, filename): fp.write(b"65535\n") elif head == b"Pf": fp.write(b"-1.0\n") + row_order = -1 if im.mode == "F" else 1 ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]) From ab262dbfd5b8076fd8d530e27c8e4896f03025e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 12:56:33 +1100 Subject: [PATCH 0056/2374] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ebf731c5659..30bbaec3a64 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Add support for reading and writing grayscale PFM images #7696 + [nulano, hugovk] + - Add LCMS2 flags to ImageCms #7676 [nulano, radarhere, hugovk] From 1e8a03cd2d938d3773dfdbe9d477276e96527494 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 18:08:40 +1100 Subject: [PATCH 0057/2374] Link to Python enum documentation --- docs/reference/ExifTags.rst | 5 +++-- docs/releasenotes/10.0.0.rst | 2 +- docs/releasenotes/9.3.0.rst | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 464ab77ea35..06965ead3f0 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -4,8 +4,9 @@ :py:mod:`~PIL.ExifTags` Module ============================== -The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes -which provide constants and clear-text names for various well-known EXIF tags. +The :py:mod:`~PIL.ExifTags` module exposes several :py:class:`enum.IntEnum` +classes which provide constants and clear-text names for various well-known +EXIF tags. .. py:data:: Base diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index a3f238119f0..705ca04152f 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -43,7 +43,7 @@ Constants ^^^^^^^^^ A number of constants have been removed. -Instead, ``enum.IntEnum`` classes have been added. +Instead, :py:class:`enum.IntEnum` classes have been added. ===================================================== ============================================================ Removed Use instead diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index fde2faae3a7..16075ce95ec 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -33,8 +33,9 @@ Added ExifTags enums ^^^^^^^^^^^^^^^^^^^^ The data from :py:data:`~PIL.ExifTags.TAGS` and -:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as ``enum.IntEnum`` -classes: :py:data:`~PIL.ExifTags.Base` and :py:data:`~PIL.ExifTags.GPS`. +:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as +:py:class:`enum.IntEnum` classes: :py:data:`~PIL.ExifTags.Base` and +:py:data:`~PIL.ExifTags.GPS`. Security From 71ba20bb19899f55761dae1ccd0bc662766cdc65 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 18:22:10 +1100 Subject: [PATCH 0058/2374] Shortened table description --- docs/deprecations.rst | 54 ++++++++++++++++++------------------ docs/releasenotes/10.3.0.rst | 54 ++++++++++++++++++------------------ 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4c9abe19503..205fcb9abcd 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -64,33 +64,33 @@ A number of constants and a function in :py:mod:`.ImageCms` have been deprecated This includes a table of flags based on LittleCMS version 1 which has been replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. -===================================================== ============================================================ -Deprecated Use instead -===================================================== ============================================================ -``ImageCms.DESCRIPTION`` No replacement -``ImageCms.VERSION`` ``PIL.__version__`` -``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` -``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` No replacement -``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` -``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` -``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` -``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` -``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` -``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` -``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` -``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` -``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` -``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` -``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` -``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` -``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` -``ImageCms.versions()`` :py:func:`PIL.features.version_module` with - ``feature="littlecms2"``, :py:data:`sys.version` or - :py:data:`sys.version_info`, and ``PIL.__version__`` -===================================================== ============================================================ +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== Removed features ---------------- diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 8ce6f4b9c9f..548f95df95c 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -17,33 +17,33 @@ A number of constants and a function in :py:mod:`.ImageCms` have been deprecated This includes a table of flags based on LittleCMS version 1 which has been replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. -===================================================== ============================================================ -Deprecated Use instead -===================================================== ============================================================ -``ImageCms.DESCRIPTION`` No replacement -``ImageCms.VERSION`` ``PIL.__version__`` -``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` -``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` No replacement -``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` -``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` -``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` -``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` -``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` -``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` -``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` -``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` -``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` -``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` -``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` -``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` -``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` -``ImageCms.versions()`` :py:func:`PIL.features.version_module` with - ``feature="littlecms2"``, :py:data:`sys.version` or - :py:data:`sys.version_info`, and ``PIL.__version__`` -===================================================== ============================================================ +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== API Changes =========== From dc6d7611e9196447aaa18305c79ee83f005f4ec3 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Tue, 9 Jan 2024 08:55:49 -0500 Subject: [PATCH 0059/2374] Test apng repeated seeks 3 times instead of 5. Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_apng.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index ea5ab41ec60..3403322585e 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -692,9 +692,8 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat def test_apng_repeated_seeks_give_correct_info() -> None: - # https://github.com/python-pillow/Pillow/issues/7700 with Image.open("Tests/images/apng/repeated_seeks_give_correct_info.png") as im: - for i in range(5): + for i in range(3): im.seek(0) assert im.info["duration"] == 4000 im.seek(1) From d7874e8a03dbf57b01c0fe41290603ddfe2875c1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 09:07:10 +1100 Subject: [PATCH 0060/2374] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 30bbaec3a64..c267ca472ce 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Added PerspectiveTransform #7699 + [radarhere] + - Add support for reading and writing grayscale PFM images #7696 [nulano, hugovk] From df99d48a0cb98cad2f6ee5ae65b0be5df8d84ef3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 09:26:56 +1100 Subject: [PATCH 0061/2374] Simplified code --- src/PIL/PsdImagePlugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 5cff564137d..9f7bf78290b 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -185,14 +185,13 @@ def read(size): # image info mode = [] ct_types = i16(read(2)) - types = list(range(ct_types)) - if len(types) > 4: - fp.seek(len(types) * 6 + 12, io.SEEK_CUR) + if ct_types > 4: + fp.seek(ct_types * 6 + 12, io.SEEK_CUR) size = i32(read(4)) fp.seek(size, io.SEEK_CUR) continue - for _ in types: + for _ in range(ct_types): type = i16(read(2)) if type == 65535: From 659098c6acc46ff353ba2a136805b463fb5cc97b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 22:05:26 +1100 Subject: [PATCH 0062/2374] Added type hints --- pyproject.toml | 1 - src/PIL/Image.py | 4 +- src/PIL/ImageMath.py | 136 +++++++++++++++++++++------------------ src/PIL/_imagingmath.pyi | 5 ++ 4 files changed, 80 insertions(+), 66 deletions(-) create mode 100644 src/PIL/_imagingmath.pyi diff --git a/pyproject.toml b/pyproject.toml index da2537b2137..54a4bcaec0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,7 +146,6 @@ exclude = [ '^src/PIL/DdsImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$', '^src/PIL/Image.py$', - '^src/PIL/ImageMath.py$', '^src/PIL/ImageMorph.py$', '^src/PIL/ImageQt.py$', '^src/PIL/ImageShow.py$', diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c56da545803..0fbbe5861a0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -873,7 +873,7 @@ def verify(self): def convert( self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256 - ): + ) -> Image: """ Returns a converted copy of this image. For the "P" mode, this method translates pixels through the palette. If mode is @@ -1305,7 +1305,7 @@ def getbands(self): """ return ImageMode.getmode(self.mode).bands - def getbbox(self, *, alpha_only=True): + def getbbox(self, *, alpha_only=True) -> tuple[int, int, int, int]: """ Calculates the bounding box of the non-zero regions in the image. diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index b77f4bce567..949fa45bb45 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -17,6 +17,7 @@ from __future__ import annotations import builtins +from typing import Any from . import Image, _imagingmath @@ -24,10 +25,10 @@ class _Operand: """Wraps an image operand, providing standard operators""" - def __init__(self, im): + def __init__(self, im: Image.Image): self.im = im - def __fixup(self, im1): + def __fixup(self, im1: _Operand | float) -> Image.Image: # convert image to suitable mode if isinstance(im1, _Operand): # argument was an image. @@ -45,122 +46,131 @@ def __fixup(self, im1): else: return Image.new("F", self.im.size, im1) - def apply(self, op, im1, im2=None, mode=None): - im1 = self.__fixup(im1) + def apply( + self, + op: str, + im1: _Operand | float, + im2: _Operand | float | None = None, + mode: str | None = None, + ) -> _Operand: + im_1 = self.__fixup(im1) if im2 is None: # unary operation - out = Image.new(mode or im1.mode, im1.size, None) - im1.load() + out = Image.new(mode or im_1.mode, im_1.size, None) + im_1.load() try: - op = getattr(_imagingmath, op + "_" + im1.mode) + op = getattr(_imagingmath, op + "_" + im_1.mode) except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.unop(op, out.im.id, im1.im.id) + _imagingmath.unop(op, out.im.id, im_1.im.id) else: # binary operation - im2 = self.__fixup(im2) - if im1.mode != im2.mode: + im_2 = self.__fixup(im2) + if im_1.mode != im_2.mode: # convert both arguments to floating point - if im1.mode != "F": - im1 = im1.convert("F") - if im2.mode != "F": - im2 = im2.convert("F") - if im1.size != im2.size: + if im_1.mode != "F": + im_1 = im_1.convert("F") + if im_2.mode != "F": + im_2 = im_2.convert("F") + if im_1.size != im_2.size: # crop both arguments to a common size - size = (min(im1.size[0], im2.size[0]), min(im1.size[1], im2.size[1])) - if im1.size != size: - im1 = im1.crop((0, 0) + size) - if im2.size != size: - im2 = im2.crop((0, 0) + size) - out = Image.new(mode or im1.mode, im1.size, None) - im1.load() - im2.load() + size = ( + min(im_1.size[0], im_2.size[0]), + min(im_1.size[1], im_2.size[1]), + ) + if im_1.size != size: + im_1 = im_1.crop((0, 0) + size) + if im_2.size != size: + im_2 = im_2.crop((0, 0) + size) + out = Image.new(mode or im_1.mode, im_1.size, None) + im_1.load() + im_2.load() try: - op = getattr(_imagingmath, op + "_" + im1.mode) + op = getattr(_imagingmath, op + "_" + im_1.mode) except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id) + _imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id) return _Operand(out) # unary operators - def __bool__(self): + def __bool__(self) -> bool: # an image is "true" if it contains at least one non-zero pixel return self.im.getbbox() is not None - def __abs__(self): + def __abs__(self) -> _Operand: return self.apply("abs", self) - def __pos__(self): + def __pos__(self) -> _Operand: return self - def __neg__(self): + def __neg__(self) -> _Operand: return self.apply("neg", self) # binary operators - def __add__(self, other): + def __add__(self, other: _Operand | float) -> _Operand: return self.apply("add", self, other) - def __radd__(self, other): + def __radd__(self, other: _Operand | float) -> _Operand: return self.apply("add", other, self) - def __sub__(self, other): + def __sub__(self, other: _Operand | float) -> _Operand: return self.apply("sub", self, other) - def __rsub__(self, other): + def __rsub__(self, other: _Operand | float) -> _Operand: return self.apply("sub", other, self) - def __mul__(self, other): + def __mul__(self, other: _Operand | float) -> _Operand: return self.apply("mul", self, other) - def __rmul__(self, other): + def __rmul__(self, other: _Operand | float) -> _Operand: return self.apply("mul", other, self) - def __truediv__(self, other): + def __truediv__(self, other: _Operand | float) -> _Operand: return self.apply("div", self, other) - def __rtruediv__(self, other): + def __rtruediv__(self, other: _Operand | float) -> _Operand: return self.apply("div", other, self) - def __mod__(self, other): + def __mod__(self, other: _Operand | float) -> _Operand: return self.apply("mod", self, other) - def __rmod__(self, other): + def __rmod__(self, other: _Operand | float) -> _Operand: return self.apply("mod", other, self) - def __pow__(self, other): + def __pow__(self, other: _Operand | float) -> _Operand: return self.apply("pow", self, other) - def __rpow__(self, other): + def __rpow__(self, other: _Operand | float) -> _Operand: return self.apply("pow", other, self) # bitwise - def __invert__(self): + def __invert__(self) -> _Operand: return self.apply("invert", self) - def __and__(self, other): + def __and__(self, other: _Operand | float) -> _Operand: return self.apply("and", self, other) - def __rand__(self, other): + def __rand__(self, other: _Operand | float) -> _Operand: return self.apply("and", other, self) - def __or__(self, other): + def __or__(self, other: _Operand | float) -> _Operand: return self.apply("or", self, other) - def __ror__(self, other): + def __ror__(self, other: _Operand | float) -> _Operand: return self.apply("or", other, self) - def __xor__(self, other): + def __xor__(self, other: _Operand | float) -> _Operand: return self.apply("xor", self, other) - def __rxor__(self, other): + def __rxor__(self, other: _Operand | float) -> _Operand: return self.apply("xor", other, self) - def __lshift__(self, other): + def __lshift__(self, other: _Operand | float) -> _Operand: return self.apply("lshift", self, other) - def __rshift__(self, other): + def __rshift__(self, other: _Operand | float) -> _Operand: return self.apply("rshift", self, other) # logical @@ -170,46 +180,46 @@ def __eq__(self, other): def __ne__(self, other): return self.apply("ne", self, other) - def __lt__(self, other): + def __lt__(self, other: _Operand | float) -> _Operand: return self.apply("lt", self, other) - def __le__(self, other): + def __le__(self, other: _Operand | float) -> _Operand: return self.apply("le", self, other) - def __gt__(self, other): + def __gt__(self, other: _Operand | float) -> _Operand: return self.apply("gt", self, other) - def __ge__(self, other): + def __ge__(self, other: _Operand | float) -> _Operand: return self.apply("ge", self, other) # conversions -def imagemath_int(self): +def imagemath_int(self: _Operand) -> _Operand: return _Operand(self.im.convert("I")) -def imagemath_float(self): +def imagemath_float(self: _Operand) -> _Operand: return _Operand(self.im.convert("F")) # logical -def imagemath_equal(self, other): +def imagemath_equal(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("eq", self, other, mode="I") -def imagemath_notequal(self, other): +def imagemath_notequal(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("ne", self, other, mode="I") -def imagemath_min(self, other): +def imagemath_min(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("min", self, other) -def imagemath_max(self, other): +def imagemath_max(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("max", self, other) -def imagemath_convert(self, mode): +def imagemath_convert(self: _Operand, mode: str) -> _Operand: return _Operand(self.im.convert(mode)) @@ -219,7 +229,7 @@ def imagemath_convert(self, mode): ops[k[10:]] = v -def eval(expression, _dict={}, **kw): +def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any: """ Evaluates an image expression. @@ -247,7 +257,7 @@ def eval(expression, _dict={}, **kw): compiled_code = compile(expression, "", "eval") - def scan(code): + def scan(code) -> None: for const in code.co_consts: if type(const) is type(compiled_code): scan(const) diff --git a/src/PIL/_imagingmath.pyi b/src/PIL/_imagingmath.pyi new file mode 100644 index 00000000000..b0235555dc5 --- /dev/null +++ b/src/PIL/_imagingmath.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... From 38bfe3cddf618bf5aaaf0d5c88011031fd0eaf45 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 10 Jan 2024 23:36:26 +1100 Subject: [PATCH 0063/2374] Added type hint Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 0fbbe5861a0..ac13c6c0c25 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1305,7 +1305,7 @@ def getbands(self): """ return ImageMode.getmode(self.mode).bands - def getbbox(self, *, alpha_only=True) -> tuple[int, int, int, int]: + def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]: """ Calculates the bounding box of the non-zero regions in the image. From 993bc6c2027926633321593d5c39a29cbe926e3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 23:41:09 +1100 Subject: [PATCH 0064/2374] Added type hint --- src/PIL/ImageMath.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 949fa45bb45..bc3318c04f4 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -17,6 +17,7 @@ from __future__ import annotations import builtins +from types import CodeType from typing import Any from . import Image, _imagingmath @@ -257,7 +258,7 @@ def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any: compiled_code = compile(expression, "", "eval") - def scan(code) -> None: + def scan(code: CodeType) -> None: for const in code.co_consts: if type(const) is type(compiled_code): scan(const) From c2907dc04967109391a77eea00f7d583a0a0395f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 23:53:45 +1100 Subject: [PATCH 0065/2374] Layer co-ordinates may be negative --- Tests/images/negative_top_left_layer.psd | Bin 0 -> 8220 bytes Tests/test_file_psd.py | 5 +++++ src/PIL/PsdImagePlugin.py | 9 +++++---- src/PIL/_binary.py | 10 ++++++++++ 4 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 Tests/images/negative_top_left_layer.psd diff --git a/Tests/images/negative_top_left_layer.psd b/Tests/images/negative_top_left_layer.psd new file mode 100644 index 0000000000000000000000000000000000000000..be9d9d6d03e357d4e3c31aecf0d3f90f9fc49974 GIT binary patch literal 8220 zcmeHLOKclO82+84w2B(Fg$kj{J{Py>O@s5)Tnk;DCfU!vP66^uVR05)uyyaj1IWz(WGT1y$ysS-%1)G!l)3h{_mtCT}}nG*az3o1HEcM}Oi^6FbX3af3SXscWnN8yQRS zkv;f{D6LjB3(OcF9sQ9%?~YCK@mQzlGb7Uxh;VkX$C1md)eI*XI|AUip+A3OS~z{? zDPjBfh(8Y7;a->|Mo>RmD$TJO8<*d|jC}e+WLO*9+)_JX(!fA>1hhC})cp|UXHZTz z!a9>*MR~8WNRX-6`zTNPxIvV^X8KAb|G?xbm(f9#{qp<_`nDHkq1q=``eb9#chE*d zn**lfBL2wfNH&@L9Fv{e67G!`7PXz!eS87?1K1vb6HtI6lt6$AEP)L*2tmZ&MNI(n zI6i%g`TO|%63(jNEE^1*F$*3zK=^!MF+dwEJ#GUlsd80p=QO~!?^>;29|YL`62Q%i zt=1oJwOTjd1{nPo;8V9Z-@A+v{|)sQd%6Q}0_=Jo;DakYUF8*k4=(`R_YLM7;p?!R z9I-5bU)}|nI0JCcHGuJ_Bi?X~jl(-%0hE!exvK!Ly$8HWg)-I`3h#FGOp7j4~@bc%4L8hTQ2-6pAH5O(=3Ana26Ds_C+#E3%+u zbv3EWGFrzj_jbgPxNj_zV8WU@+^iGh91VpEt=eur{_{tl#0A^T$Dd3U!Izr0>dN~Zz;^j<2WvK_UF`##V))XB=~LtY<8-MGLd} zINPsodWLQ4TuiEFI+IbdVkVukM9oU2#9W1@#foYeNjXI`ik0qzD!4&NT!S(w1W(wA zqgh&p=2TfsB~4A#vW6y-6g5RtlXGb$XXbKQtq`YT)&9jXQTZ1fa8)@**71c9Fmofwe|0a>Pzdx#Yc;WwsRrT7AAXKY7r zZQA_1aF)L?NBC*oxY&s>YDH<`}MGrhHY~b&bqgCV)xxx(PuRq~n z6vT%)30?EXB4rUm#za{W75T4>No|iM_TK2YJSF{&lqqh|_oBghkq%#A_&$`nFT^%! zxPv=c+QGP!O9;YnkxlVCq-#jmrWn|i@i4oFbZv@(O&Jff>p!DwymzIdF8&N^;Drg` zuFtStX|eJ-Yk2u$cWM>J*ncUpOYAm=pX_#b3GiANyTop3F8-Em|J8DYS+KHgj;v5W zy2aI&TpLxpnHuKs4!;5vI%7r+C4O-`rTwfiCIzSP* zx;hXQBQmxHbt}H9hJM^W1H*XSHUn!n)>?zmw&GXT+I8e=0NR#tbrrc9fVL%EwblUH L`ro%zxbyW->#0jd literal 0 HcmV?d00001 diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 8b06ce2b163..e3c1f447af2 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -112,6 +112,11 @@ def test_rgba(): assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png") +def test_negative_top_left_layer() -> None: + with Image.open("Tests/images/negative_top_left_layer.psd") as im: + assert im.layers[0][2] == (-50, -50, 50, 50) + + def test_layer_skip(): with Image.open("Tests/images/five_channels.psd") as im: assert im.n_frames == 1 diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 9f7bf78290b..d29bcf9970c 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -24,6 +24,7 @@ from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import si16be as si16 +from ._binary import si32be as si32 MODES = { # (photoshop mode, bits) -> (pil mode, required channels) @@ -177,10 +178,10 @@ def read(size): for _ in range(abs(ct)): # bounding box - y0 = i32(read(4)) - x0 = i32(read(4)) - y1 = i32(read(4)) - x1 = i32(read(4)) + y0 = si32(read(4)) + x0 = si32(read(4)) + y1 = si32(read(4)) + x1 = si32(read(4)) # image info mode = [] diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 0a07e8d0e12..4594ccce361 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -77,6 +77,16 @@ def si32le(c: bytes, o: int = 0) -> int: return unpack_from(" int: + """ + Converts a 4-bytes (32 bits) string to a signed integer, big endian. + + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string + """ + return unpack_from(">i", c, o)[0] + + def i16be(c: bytes, o: int = 0) -> int: return unpack_from(">H", c, o)[0] From 6f144d45b98d6c331da9fcd437542e224793b893 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Wed, 10 Jan 2024 16:03:42 -0500 Subject: [PATCH 0066/2374] Rename repeated seeks apng to reflect what it is rather than how it is used. --- ...ive_correct_info.png => different_durations.png} | Bin Tests/test_file_apng.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename Tests/images/apng/{repeated_seeks_give_correct_info.png => different_durations.png} (100%) diff --git a/Tests/images/apng/repeated_seeks_give_correct_info.png b/Tests/images/apng/different_durations.png similarity index 100% rename from Tests/images/apng/repeated_seeks_give_correct_info.png rename to Tests/images/apng/different_durations.png diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 3403322585e..e2c4569cee5 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -692,7 +692,7 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat def test_apng_repeated_seeks_give_correct_info() -> None: - with Image.open("Tests/images/apng/repeated_seeks_give_correct_info.png") as im: + with Image.open("Tests/images/apng/different_durations.png") as im: for i in range(3): im.seek(0) assert im.info["duration"] == 4000 From 5347b471c69c400c6199c8cd200b5844169f7988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Thu, 11 Jan 2024 02:08:46 +0100 Subject: [PATCH 0067/2374] Update Tests/test_imagecms.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_imagecms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 9575b026db2..810394e6f5f 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -639,7 +639,7 @@ def test_rgb_lab(mode): assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) -def test_deprecation(): +def test_deprecation() -> None: with pytest.warns(DeprecationWarning): assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") with pytest.warns(DeprecationWarning): From 08992cf6b18cc3ad1dc9e75088b505450c21388b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Jan 2024 20:01:25 +1100 Subject: [PATCH 0068/2374] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c267ca472ce..887319dabf8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Deprecate ImageCms constants and versions() function #7702 + [nulano, radarhere] + - Added PerspectiveTransform #7699 [radarhere] From bc192557b8de105da5942108d33d643f83513976 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Jan 2024 23:11:12 +1100 Subject: [PATCH 0069/2374] Added type hints --- pyproject.toml | 1 - src/PIL/ImageMorph.py | 50 +++++++++++++++++++++++---------------- src/PIL/_imagingmorph.pyi | 5 ++++ 3 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 src/PIL/_imagingmorph.pyi diff --git a/pyproject.toml b/pyproject.toml index 54a4bcaec0c..8acfc04204e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,7 +146,6 @@ exclude = [ '^src/PIL/DdsImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$', '^src/PIL/Image.py$', - '^src/PIL/ImageMorph.py$', '^src/PIL/ImageQt.py$', '^src/PIL/ImageShow.py$', '^src/PIL/ImImagePlugin.py$', diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 282e7d2a54e..534c6291a02 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -62,12 +62,14 @@ class LutBuilder: """ - def __init__(self, patterns=None, op_name=None): + def __init__( + self, patterns: list[str] | None = None, op_name: str | None = None + ) -> None: if patterns is not None: self.patterns = patterns else: self.patterns = [] - self.lut = None + self.lut: bytearray | None = None if op_name is not None: known_patterns = { "corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"], @@ -87,25 +89,27 @@ def __init__(self, patterns=None, op_name=None): self.patterns = known_patterns[op_name] - def add_patterns(self, patterns): + def add_patterns(self, patterns: list[str]) -> None: self.patterns += patterns - def build_default_lut(self): + def build_default_lut(self) -> None: symbols = [0, 1] m = 1 << 4 # pos of current pixel self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE)) - def get_lut(self): + def get_lut(self) -> bytearray | None: return self.lut - def _string_permute(self, pattern, permutation): + def _string_permute(self, pattern: str, permutation: list[int]) -> str: """string_permute takes a pattern and a permutation and returns the string permuted according to the permutation list. """ assert len(permutation) == 9 return "".join(pattern[p] for p in permutation) - def _pattern_permute(self, basic_pattern, options, basic_result): + def _pattern_permute( + self, basic_pattern: str, options: str, basic_result: int + ) -> list[tuple[str, int]]: """pattern_permute takes a basic pattern and its result and clones the pattern according to the modifications described in the $options parameter. It returns a list of all cloned patterns.""" @@ -135,12 +139,13 @@ def _pattern_permute(self, basic_pattern, options, basic_result): return patterns - def build_lut(self): + def build_lut(self) -> bytearray: """Compile all patterns into a morphology lut. TBD :Build based on (file) morphlut:modify_lut """ self.build_default_lut() + assert self.lut is not None patterns = [] # Parse and create symmetries of the patterns strings @@ -159,10 +164,10 @@ def build_lut(self): patterns += self._pattern_permute(pattern, options, result) # compile the patterns into regular expressions for speed - for i, pattern in enumerate(patterns): + compiled_patterns = [] + for pattern in patterns: p = pattern[0].replace(".", "X").replace("X", "[01]") - p = re.compile(p) - patterns[i] = (p, pattern[1]) + compiled_patterns.append((re.compile(p), pattern[1])) # Step through table and find patterns that match. # Note that all the patterns are searched. The last one @@ -172,8 +177,8 @@ def build_lut(self): bitpattern = bin(i)[2:] bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1] - for p, r in patterns: - if p.match(bitpattern): + for pattern, r in compiled_patterns: + if pattern.match(bitpattern): self.lut[i] = [0, 1][r] return self.lut @@ -182,7 +187,12 @@ def build_lut(self): class MorphOp: """A class for binary morphological operators""" - def __init__(self, lut=None, op_name=None, patterns=None): + def __init__( + self, + lut: bytearray | None = None, + op_name: str | None = None, + patterns: list[str] | None = None, + ) -> None: """Create a binary morphological operator""" self.lut = lut if op_name is not None: @@ -190,7 +200,7 @@ def __init__(self, lut=None, op_name=None, patterns=None): elif patterns is not None: self.lut = LutBuilder(patterns=patterns).build_lut() - def apply(self, image): + def apply(self, image: Image.Image): """Run a single morphological operation on an image Returns a tuple of the number of changed pixels and the @@ -206,7 +216,7 @@ def apply(self, image): count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage - def match(self, image): + def match(self, image: Image.Image): """Get a list of coordinates matching the morphological operation on an image. @@ -221,7 +231,7 @@ def match(self, image): raise ValueError(msg) return _imagingmorph.match(bytes(self.lut), image.im.id) - def get_on_pixels(self, image): + def get_on_pixels(self, image: Image.Image): """Get a list of all turned on pixels in a binary image Returns a list of tuples of (x,y) coordinates @@ -232,7 +242,7 @@ def get_on_pixels(self, image): raise ValueError(msg) return _imagingmorph.get_on_pixels(image.im.id) - def load_lut(self, filename): + def load_lut(self, filename: str) -> None: """Load an operator from an mrl file""" with open(filename, "rb") as f: self.lut = bytearray(f.read()) @@ -242,7 +252,7 @@ def load_lut(self, filename): msg = "Wrong size operator file!" raise Exception(msg) - def save_lut(self, filename): + def save_lut(self, filename: str) -> None: """Save an operator to an mrl file""" if self.lut is None: msg = "No operator loaded" @@ -250,6 +260,6 @@ def save_lut(self, filename): with open(filename, "wb") as f: f.write(self.lut) - def set_lut(self, lut): + def set_lut(self, lut: bytearray | None) -> None: """Set the lut from an external source""" self.lut = lut diff --git a/src/PIL/_imagingmorph.pyi b/src/PIL/_imagingmorph.pyi new file mode 100644 index 00000000000..b0235555dc5 --- /dev/null +++ b/src/PIL/_imagingmorph.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... From 067c5f4123c7cab7507fca02605bfd9762861d1f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Jan 2024 23:13:29 +1100 Subject: [PATCH 0070/2374] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 887319dabf8..62ae2a68bbc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Fix APNG info after seeking backwards more than twice #7701 + [esoma, radarhere] + - Deprecate ImageCms constants and versions() function #7702 [nulano, radarhere] From 10cf2f2651eee87c127bbb6d090138506c86fbc6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jan 2024 14:40:59 +1100 Subject: [PATCH 0071/2374] Added type hints --- pyproject.toml | 1 - src/PIL/Image.py | 6 +++-- src/PIL/ImageShow.py | 60 ++++++++++++++++++++++++++------------------ tox.ini | 1 + 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8acfc04204e..789df6f5e67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,7 +147,6 @@ exclude = [ '^src/PIL/FpxImagePlugin.py$', '^src/PIL/Image.py$', '^src/PIL/ImageQt.py$', - '^src/PIL/ImageShow.py$', '^src/PIL/ImImagePlugin.py$', '^src/PIL/MicImagePlugin.py$', '^src/PIL/PdfParser.py$', diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ac13c6c0c25..5ab27c35954 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -242,7 +242,7 @@ def _conv_type_shape(im): _MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") -def getmodebase(mode): +def getmodebase(mode: str) -> str: """ Gets the "base" mode for given mode. This function returns "L" for images that contain grayscale data, and "RGB" for images that @@ -583,7 +583,9 @@ def _ensure_mutable(self): else: self.load() - def _dump(self, file=None, format=None, **options): + def _dump( + self, file: str | None = None, format: str | None = None, **options + ) -> str: suffix = "" if format: suffix = "." + format diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index fad3e098003..d90545e92ec 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -13,18 +13,20 @@ # from __future__ import annotations +import abc import os import shutil import subprocess import sys from shlex import quote +from typing import Any from . import Image _viewers = [] -def register(viewer, order=1): +def register(viewer, order: int = 1) -> None: """ The :py:func:`register` function is used to register additional viewers:: @@ -49,7 +51,7 @@ def register(viewer, order=1): _viewers.insert(0, viewer) -def show(image, title=None, **options): +def show(image: Image.Image, title: str | None = None, **options: Any) -> bool: r""" Display a given image. @@ -69,7 +71,7 @@ class Viewer: # main api - def show(self, image, **options): + def show(self, image: Image.Image, **options: Any) -> int: """ The main function for displaying an image. Converts the given image to the target format and displays it. @@ -87,16 +89,16 @@ def show(self, image, **options): # hook methods - format = None + format: str | None = None """The format to convert the image into.""" - options = {} + options: dict[str, Any] = {} """Additional options used to convert the image.""" - def get_format(self, image): + def get_format(self, image: Image.Image) -> str | None: """Return format name, or ``None`` to save as PGM/PPM.""" return self.format - def get_command(self, file, **options): + def get_command(self, file: str, **options: Any) -> str: """ Returns the command used to display the file. Not implemented in the base class. @@ -104,15 +106,15 @@ def get_command(self, file, **options): msg = "unavailable in base viewer" raise NotImplementedError(msg) - def save_image(self, image): + def save_image(self, image: Image.Image) -> str: """Save to temporary file and return filename.""" return image._dump(format=self.get_format(image), **self.options) - def show_image(self, image, **options): + def show_image(self, image: Image.Image, **options: Any) -> int: """Display the given image.""" return self.show_file(self.save_image(image), **options) - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -129,7 +131,7 @@ class WindowsViewer(Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} - def get_command(self, file, **options): + def get_command(self, file: str, **options: Any) -> str: return ( f'start "Pillow" /WAIT "{file}" ' "&& ping -n 4 127.0.0.1 >NUL " @@ -147,14 +149,14 @@ class MacViewer(Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} - def get_command(self, file, **options): + def get_command(self, file: str, **options: Any) -> str: # on darwin open returns immediately resulting in the temp # file removal while app is opening command = "open -a Preview.app" command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" return command - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -180,7 +182,11 @@ class UnixViewer(Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} - def get_command(self, file, **options): + @abc.abstractmethod + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: + pass + + def get_command(self, file: str, **options: Any) -> str: command = self.get_command_ex(file, **options)[0] return f"({command} {quote(file)}" @@ -190,11 +196,11 @@ class XDGViewer(UnixViewer): The freedesktop.org ``xdg-open`` command. """ - def get_command_ex(self, file, **options): + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: command = executable = "xdg-open" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -208,13 +214,15 @@ class DisplayViewer(UnixViewer): This viewer supports the ``title`` parameter. """ - def get_command_ex(self, file, title=None, **options): + def get_command_ex( + self, file: str, title: str | None = None, **options: Any + ) -> tuple[str, str]: command = executable = "display" if title: command += f" -title {quote(title)}" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -231,12 +239,12 @@ def show_file(self, path, **options): class GmDisplayViewer(UnixViewer): """The GraphicsMagick ``gm display`` command.""" - def get_command_ex(self, file, **options): + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: executable = "gm" command = "gm display" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -247,12 +255,12 @@ def show_file(self, path, **options): class EogViewer(UnixViewer): """The GNOME Image Viewer ``eog`` command.""" - def get_command_ex(self, file, **options): + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: executable = "eog" command = "eog -n" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -266,7 +274,9 @@ class XVViewer(UnixViewer): This viewer supports the ``title`` parameter. """ - def get_command_ex(self, file, title=None, **options): + def get_command_ex( + self, file: str, title: str | None = None, **options: Any + ) -> tuple[str, str]: # note: xv is pretty outdated. most modern systems have # imagemagick's display command instead. command = executable = "xv" @@ -274,7 +284,7 @@ def get_command_ex(self, file, title=None, **options): command += f" -name {quote(title)}" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -304,7 +314,7 @@ def show_file(self, path, **options): class IPythonViewer(Viewer): """The viewer for IPython frontends.""" - def show_image(self, image, **options): + def show_image(self, image: Image.Image, **options: Any) -> int: ipython_display(image) return 1 diff --git a/tox.ini b/tox.ini index d89d017e45c..fb6746ce7bf 100644 --- a/tox.ini +++ b/tox.ini @@ -33,6 +33,7 @@ commands = [testenv:mypy] skip_install = true deps = + ipython mypy==1.7.1 numpy extras = From ffd0363b65ca05870f9306e8a6e999d58d9725ae Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jan 2024 16:26:15 +1100 Subject: [PATCH 0072/2374] Added type hints --- src/PIL/FitsImagePlugin.py | 8 +++++--- src/PIL/Image.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 7dce2d60f76..e69890babcc 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -15,7 +15,7 @@ from . import Image, ImageFile -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:6] == b"SIMPLE" @@ -23,8 +23,10 @@ class FitsImageFile(ImageFile.ImageFile): format = "FITS" format_description = "FITS" - def _open(self): - headers = {} + def _open(self) -> None: + assert self.fp is not None + + headers: dict[bytes, bytes] = {} while True: header = self.fp.read(80) if not header: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ac13c6c0c25..b2520d57c5a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3484,7 +3484,7 @@ def register_extension(id, extension) -> None: EXTENSION[extension.lower()] = id.upper() -def register_extensions(id, extensions): +def register_extensions(id, extensions) -> None: """ Registers image extensions. This function should not be used in application code. From 2fbd7dda839fb1cb4d9ba0a1e9abecf34e4786ec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jan 2024 21:10:02 +1100 Subject: [PATCH 0073/2374] Use consistent arguments for load_seek --- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/IcoImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index d2e60aa0759..690fb3586a7 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -408,7 +408,7 @@ def load(self, scale=1, transparency=False): self.tile = [] return Image.Image.load(self) - def load_seek(self, *args, **kwargs): + def load_seek(self, pos): # we can't incrementally load, so force ImageFile.parser to # use our custom load method by defining this method. pass diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 1b22f8645dc..2b21d957fd2 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -339,7 +339,7 @@ def load(self): self.size = im.size - def load_seek(self): + def load_seek(self, pos): # Flag the ImageFile.Parser so that it # just does all the decode at the end. pass From 543b5a674160c86d8b128e0af6c706a2e96c16d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jan 2024 21:11:16 +1100 Subject: [PATCH 0074/2374] Use consistent arguments for load_read --- src/PIL/ImageFile.py | 2 +- src/PIL/XpmImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 0923979af8b..17b1b3203e6 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -328,7 +328,7 @@ def load_end(self): # pass # may be defined for blocked formats (e.g. PNG) - # def load_read(self, bytes): + # def load_read(self, read_bytes): # pass def _seek_check(self, frame): diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index bf73c9bef06..3125f8d52c6 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -103,7 +103,7 @@ def _open(self): self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] - def load_read(self, bytes): + def load_read(self, read_bytes): # # load all image data in one chunk From c97b5c6f7a221ed534d93e943992e44c2a7ec5fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 14 Jan 2024 22:29:56 +1100 Subject: [PATCH 0075/2374] Exclude abstract method code from coverage --- src/PIL/ImageShow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index d90545e92ec..c03122c11aa 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -184,7 +184,7 @@ class UnixViewer(Viewer): @abc.abstractmethod def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - pass + pass # pragma: no cover def get_command(self, file: str, **options: Any) -> str: command = self.get_command_ex(file, **options)[0] From c75a93b9a35e0835cc44465d5a73a9a05be0d166 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Jan 2024 16:11:48 +1100 Subject: [PATCH 0076/2374] Added type hints --- src/PIL/Image.py | 4 ++-- src/PIL/ImageFile.py | 2 +- src/PIL/ImagePalette.py | 2 +- src/PIL/McIdasImagePlugin.py | 8 +++++--- src/PIL/PcdImagePlugin.py | 8 ++++++-- src/PIL/PcxImagePlugin.py | 10 +++++++--- src/PIL/PixarImagePlugin.py | 6 ++++-- src/PIL/SunImagePlugin.py | 6 ++++-- src/PIL/XVThumbImagePlugin.py | 6 ++++-- src/PIL/XbmImagePlugin.py | 9 ++++++--- 10 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c1f89af4616..e32f254de8e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3437,7 +3437,7 @@ def register_open(id, factory, accept=None) -> None: OPEN[id] = factory, accept -def register_mime(id, mimetype): +def register_mime(id, mimetype) -> None: """ Registers an image MIME type by populating ``Image.MIME``. This function should not be used in application code. @@ -3452,7 +3452,7 @@ def register_mime(id, mimetype): MIME[id.upper()] = mimetype -def register_save(id, driver): +def register_save(id, driver) -> None: """ Registers an image save function. This function should not be used in application code. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 0923979af8b..72c3c03c51a 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -514,7 +514,7 @@ def close(self): # -------------------------------------------------------------------- -def _save(im, fp, tile, bufsize=0): +def _save(im, fp, tile, bufsize=0) -> None: """Helper to save image based on tile list :param im: Image object. diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index fbcfa309d29..2b6cecc6105 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -192,7 +192,7 @@ def save(self, fp): # Internal -def raw(rawmode, data): +def raw(rawmode, data) -> ImagePalette: palette = ImagePalette() palette.rawmode = rawmode palette.palette = data diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 9a85c0d15b0..27972236c0a 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -22,8 +22,8 @@ from . import Image, ImageFile -def _accept(s): - return s[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" +def _accept(prefix: bytes) -> bool: + return prefix[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" ## @@ -34,8 +34,10 @@ class McIdasImageFile(ImageFile.ImageFile): format = "MCIDAS" format_description = "McIdas area file" - def _open(self): + def _open(self) -> None: # parse area file directory + assert self.fp is not None + s = self.fp.read(256) if not _accept(s) or len(s) != 256: msg = "not an McIdas area file" diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index a0515b302eb..1cd5c4a9dbe 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -27,8 +27,10 @@ class PcdImageFile(ImageFile.ImageFile): format = "PCD" format_description = "Kodak PhotoCD" - def _open(self): + def _open(self) -> None: # rough + assert self.fp is not None + self.fp.seek(2048) s = self.fp.read(2048) @@ -47,9 +49,11 @@ def _open(self): self._size = 768, 512 # FIXME: not correct for rotated images! self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)] - def load_end(self): + def load_end(self) -> None: if self.tile_post_rotate: # Handle rotated PCDs + assert self.im is not None + self.im = self.im.rotate(self.tile_post_rotate) self._size = self.im.size diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 98ecefd0514..3e0968a8386 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -37,7 +37,7 @@ logger = logging.getLogger(__name__) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] @@ -49,8 +49,10 @@ class PcxImageFile(ImageFile.ImageFile): format = "PCX" format_description = "Paintbrush" - def _open(self): + def _open(self) -> None: # header + assert self.fp is not None + s = self.fp.read(128) if not _accept(s): msg = "not a PCX file" @@ -141,7 +143,7 @@ def _open(self): } -def _save(im, fp, filename): +def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None: try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: @@ -199,6 +201,8 @@ def _save(im, fp, filename): if im.mode == "P": # colour palette + assert im.im is not None + fp.write(o8(12)) palette = im.im.getpalette("RGB", "RGB") palette += b"\x00" * (768 - len(palette)) diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index af866feb362..887b6568bf7 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -27,7 +27,7 @@ # helpers -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"\200\350\000\000" @@ -39,8 +39,10 @@ class PixarImageFile(ImageFile.ImageFile): format = "PIXAR" format_description = "PIXAR raster image" - def _open(self): + def _open(self) -> None: # assuming a 4-byte magic label + assert self.fp is not None + s = self.fp.read(4) if not _accept(s): msg = "not a PIXAR file" diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index 11ce3dfefd0..4e098474ab9 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -21,7 +21,7 @@ from ._binary import i32be as i32 -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return len(prefix) >= 4 and i32(prefix) == 0x59A66A95 @@ -33,7 +33,7 @@ class SunImageFile(ImageFile.ImageFile): format = "SUN" format_description = "Sun Raster File" - def _open(self): + def _open(self) -> None: # The Sun Raster file header is 32 bytes in length # and has the following format: @@ -49,6 +49,8 @@ def _open(self): # DWORD ColorMapLength; /* Size of the color map in bytes */ # } SUNRASTER; + assert self.fp is not None + # HEAD s = self.fp.read(32) if not _accept(s): diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index 47ba1c54803..c84adaca215 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -33,7 +33,7 @@ ) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:6] == _MAGIC @@ -45,8 +45,10 @@ class XVThumbImageFile(ImageFile.ImageFile): format = "XVThumb" format_description = "XV thumbnail image" - def _open(self): + def _open(self) -> None: # check magic + assert self.fp is not None + if not _accept(self.fp.read(6)): msg = "not an XV thumbnail file" raise SyntaxError(msg) diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 566acbfe5af..0291e2858ac 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -21,6 +21,7 @@ from __future__ import annotations import re +from io import BytesIO from . import Image, ImageFile @@ -36,7 +37,7 @@ ) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix.lstrip()[:7] == b"#define" @@ -48,7 +49,9 @@ class XbmImageFile(ImageFile.ImageFile): format = "XBM" format_description = "X11 Bitmap" - def _open(self): + def _open(self) -> None: + assert self.fp is not None + m = xbm_head.match(self.fp.read(512)) if not m: @@ -67,7 +70,7 @@ def _open(self): self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] -def _save(im, fp, filename): +def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as XBM" raise OSError(msg) From 575edbefe49641107b7315b3113e35c6b74f9bca Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 15 Jan 2024 18:25:31 +1100 Subject: [PATCH 0077/2374] Added type hints Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e32f254de8e..3f35bf50e1e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3437,7 +3437,7 @@ def register_open(id, factory, accept=None) -> None: OPEN[id] = factory, accept -def register_mime(id, mimetype) -> None: +def register_mime(id: str, mimetype: str) -> None: """ Registers an image MIME type by populating ``Image.MIME``. This function should not be used in application code. @@ -3452,7 +3452,7 @@ def register_mime(id, mimetype) -> None: MIME[id.upper()] = mimetype -def register_save(id, driver) -> None: +def register_save(id: str, driver) -> None: """ Registers an image save function. This function should not be used in application code. From 4a6cb0f8447869ce886db1cde088723b55df32a2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Jan 2024 20:04:51 +1100 Subject: [PATCH 0078/2374] Added type hints --- src/PIL/ImtImagePlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 7469c592dd2..abb3fb762e7 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -33,10 +33,12 @@ class ImtImageFile(ImageFile.ImageFile): format = "IMT" format_description = "IM Tools" - def _open(self): + def _open(self) -> None: # Quick rejection: if there's not a LF among the first # 100 bytes, this is (probably) not a text header. + assert self.fp is not None + buffer = self.fp.read(100) if b"\n" not in buffer: msg = "not an IM file" From 6a2bdb6feb921fae4160c26e63e2ff541c4a7738 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 09:00:40 +1100 Subject: [PATCH 0079/2374] Added type hints --- src/PIL/Image.py | 2 +- src/PIL/ImageFile.py | 2 +- src/PIL/MspImagePlugin.py | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3f35bf50e1e..b7bb514acc5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3507,7 +3507,7 @@ def registered_extensions(): return EXTENSION -def register_decoder(name, decoder): +def register_decoder(name: str, decoder) -> None: """ Registers an image decoder. This function should not be used in application code. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 72c3c03c51a..b79f2707b5a 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -713,7 +713,7 @@ def decode(self, buffer): msg = "unavailable in base decoder" raise NotImplementedError(msg) - def set_as_raw(self, data, rawmode=None): + def set_as_raw(self, data: bytes, rawmode = None) -> None: """ Convenience method to set the internal image from a stream of raw data diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 77dac65b6b3..bb7e466a790 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -35,7 +35,7 @@ # read MSP files -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] in [b"DanM", b"LinS"] @@ -48,8 +48,10 @@ class MspImageFile(ImageFile.ImageFile): format = "MSP" format_description = "Windows Paint" - def _open(self): + def _open(self) -> None: # Header + assert self.fp is not None + s = self.fp.read(32) if not _accept(s): msg = "not an MSP file" @@ -109,7 +111,9 @@ class MspDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + img = io.BytesIO() blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) try: @@ -159,7 +163,7 @@ def decode(self, buffer): # write MSP files (uncompressed only) -def _save(im, fp, filename): +def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as MSP" raise OSError(msg) From edaf7acdb3581e20cc7e77b631db736782cd8a15 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:01:17 +0000 Subject: [PATCH 0080/2374] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/ImageFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index b79f2707b5a..40353da677b 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -713,7 +713,7 @@ def decode(self, buffer): msg = "unavailable in base decoder" raise NotImplementedError(msg) - def set_as_raw(self, data: bytes, rawmode = None) -> None: + def set_as_raw(self, data: bytes, rawmode=None) -> None: """ Convenience method to set the internal image from a stream of raw data From 5a587193c7ed360cc0705ae81c4628e990420384 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 12:22:59 +1100 Subject: [PATCH 0081/2374] Added type hints --- src/PIL/Image.py | 8 ++++---- src/PIL/SgiImagePlugin.py | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b7bb514acc5..ec1cff89636 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -710,7 +710,7 @@ def __setstate__(self, state): self.putpalette(palette) self.frombytes(data) - def tobytes(self, encoder_name="raw", *args): + def tobytes(self, encoder_name: str = "raw", *args) -> bytes: """ Return image as a bytes object. @@ -788,7 +788,7 @@ def tobitmap(self, name="image"): ] ) - def frombytes(self, data, decoder_name="raw", *args): + def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None: """ Loads this image with pixel data from a bytes object. @@ -1297,7 +1297,7 @@ def filter(self, filter): ] return merge(self.mode, ims) - def getbands(self): + def getbands(self) -> tuple[str, ...]: """ Returns a tuple containing the name of each band in this image. For example, ``getbands`` on an RGB image returns ("R", "G", "B"). @@ -2495,7 +2495,7 @@ def show(self, title=None): _show(self, title=title) - def split(self): + def split(self) -> tuple[Image, ...]: """ Split this image into individual bands. This method returns a tuple of individual image bands from an image. For example, diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index f9a10f6109c..ccf661ff1f3 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -24,13 +24,14 @@ import os import struct +from io import BytesIO from . import Image, ImageFile from ._binary import i16be as i16 from ._binary import o8 -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return len(prefix) >= 2 and i16(prefix) == 474 @@ -52,8 +53,10 @@ class SgiImageFile(ImageFile.ImageFile): format = "SGI" format_description = "SGI Image File Format" - def _open(self): + def _open(self) -> None: # HEAD + assert self.fp is not None + headlen = 512 s = self.fp.read(headlen) @@ -122,7 +125,7 @@ def _open(self): ] -def _save(im, fp, filename): +def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: if im.mode not in {"RGB", "RGBA", "L"}: msg = "Unsupported SGI image mode" raise ValueError(msg) @@ -168,8 +171,8 @@ def _save(im, fp, filename): # Maximum Byte value (255 = 8bits per pixel) pinmax = 255 # Image name (79 characters max, truncated below in write) - img_name = os.path.splitext(os.path.basename(filename))[0] - img_name = img_name.encode("ascii", "ignore") + filename = os.path.basename(filename) + img_name = os.path.splitext(filename)[0].encode("ascii", "ignore") # Standard representation of pixel in the file colormap = 0 fp.write(struct.pack(">h", magic_number)) @@ -201,7 +204,10 @@ def _save(im, fp, filename): class SGI16Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + assert self.im is not None + rawmode, stride, orientation = self.args pagesize = self.state.xsize * self.state.ysize zsize = len(self.mode) From e2aa0fd4996b85334df3831c2df9bf2d5f68dadd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 12:55:48 +1100 Subject: [PATCH 0082/2374] Changed ops to be static --- src/PIL/ImageMath.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index bc3318c04f4..a7652f237ed 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -224,10 +224,15 @@ def imagemath_convert(self: _Operand, mode: str) -> _Operand: return _Operand(self.im.convert(mode)) -ops = {} -for k, v in list(globals().items()): - if k[:10] == "imagemath_": - ops[k[10:]] = v +ops = { + "int": imagemath_int, + "float": imagemath_float, + "equal": imagemath_equal, + "notequal": imagemath_notequal, + "min": imagemath_min, + "max": imagemath_max, + "convert": imagemath_convert, +} def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any: @@ -244,7 +249,7 @@ def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any: """ # build execution namespace - args = ops.copy() + args: dict[str, Any] = ops.copy() for k in list(_dict.keys()) + list(kw.keys()): if "__" in k or hasattr(builtins, k): msg = f"'{k}' not allowed" From 44e77a22b572a3ffe158a8b4b63c218ae549b8ac Mon Sep 17 00:00:00 2001 From: FangFuxin <38530078+lajiyuan@users.noreply.github.com> Date: Fri, 12 Jan 2024 15:46:49 +0800 Subject: [PATCH 0083/2374] Fix png image plugin load_end func handle truncated file. --- Tests/images/end_trunc_file.png | Bin 0 -> 30339 bytes Tests/test_file_png.py | 9 +++++++++ src/PIL/PngImagePlugin.py | 8 +++++++- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 Tests/images/end_trunc_file.png diff --git a/Tests/images/end_trunc_file.png b/Tests/images/end_trunc_file.png new file mode 100644 index 0000000000000000000000000000000000000000..5e88c5e4fa2a6ac010219a2469b83efa21102f0e GIT binary patch literal 30339 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 mOz9jWq7NYmRv5v_Ot0CrAEL_t(ca^tlC literal 0 HcmV?d00001 diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ff386211061..9b6a4c5e6ba 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -777,6 +777,15 @@ class MyStdOut: mystdout = mystdout.buffer with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) + + def test_end_truncated_file(self): + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + with Image.open("Tests/images/end_trunc_file.png") as im: + assert_image_equal_tofile(im, "Tests/images/end_trunc_file.png") + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 823f1249285..1248fb78529 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -981,7 +981,13 @@ def load_end(self): except EOFError: if cid == b"fdAT": length -= 4 - ImageFile._safe_read(self.fp, length) + try: + ImageFile._safe_read(self.fp, length) + except OSError as e: + if ImageFile.LOAD_TRUNCATED_IMAGES: + break + else: + raise e except AttributeError: logger.debug("%r %s %s (unknown)", cid, pos, length) s = ImageFile._safe_read(self.fp, length) From b2711c3e8b019d0d069d58069912abd4ecb99e61 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Jan 2024 07:36:57 +0000 Subject: [PATCH 0084/2374] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_png.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 9b6a4c5e6ba..3c285b0779e 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -777,7 +777,7 @@ class MyStdOut: mystdout = mystdout.buffer with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) - + def test_end_truncated_file(self): ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -787,7 +787,6 @@ def test_end_truncated_file(self): ImageFile.LOAD_TRUNCATED_IMAGES = False - @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @skip_unless_feature("zlib") class TestTruncatedPngPLeaks(PillowLeakTestCase): From fe7b6d9e80ae8ee65d2b4e615971a00690b876fe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 18:39:33 +1100 Subject: [PATCH 0085/2374] Corrected expected image path --- Tests/test_file_png.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 3c285b0779e..aa2aac906f7 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -782,7 +782,7 @@ def test_end_truncated_file(self): ImageFile.LOAD_TRUNCATED_IMAGES = True try: with Image.open("Tests/images/end_trunc_file.png") as im: - assert_image_equal_tofile(im, "Tests/images/end_trunc_file.png") + assert_image_equal_tofile(im, "Tests/images/hopper.png") finally: ImageFile.LOAD_TRUNCATED_IMAGES = False From 62e6d62518f21333fbde364d7b7a57e25d39061b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 18:49:25 +1100 Subject: [PATCH 0086/2374] Test error is raised without LOAD_TRUNCATED_IMAGES --- .../{end_trunc_file.png => truncated_end_chunk.png} | Bin Tests/test_file_png.py | 8 ++++++-- 2 files changed, 6 insertions(+), 2 deletions(-) rename Tests/images/{end_trunc_file.png => truncated_end_chunk.png} (100%) diff --git a/Tests/images/end_trunc_file.png b/Tests/images/truncated_end_chunk.png similarity index 100% rename from Tests/images/end_trunc_file.png rename to Tests/images/truncated_end_chunk.png diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index aa2aac906f7..0884ddcc35d 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -778,10 +778,14 @@ class MyStdOut: with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) - def test_end_truncated_file(self): + def test_truncated_end_chunk(self): + with Image.open("Tests/images/truncated_end_chunk.png") as im: + with pytest.raises(OSError): + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = True try: - with Image.open("Tests/images/end_trunc_file.png") as im: + with Image.open("Tests/images/truncated_end_chunk.png") as im: assert_image_equal_tofile(im, "Tests/images/hopper.png") finally: ImageFile.LOAD_TRUNCATED_IMAGES = False From 54c96df9d6e370d16960ee8a2aee932b41f213f3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jan 2024 08:03:09 +1100 Subject: [PATCH 0087/2374] Added type hints --- src/PIL/TgaImagePlugin.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 65c7484f756..584932d2c7d 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -18,6 +18,7 @@ from __future__ import annotations import warnings +from io import BytesIO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -49,8 +50,10 @@ class TgaImageFile(ImageFile.ImageFile): format = "TGA" format_description = "Targa" - def _open(self): + def _open(self) -> None: # process header + assert self.fp is not None + s = self.fp.read(18) id_len = s[0] @@ -151,8 +154,9 @@ def _open(self): except KeyError: pass # cannot decode - def load_end(self): + def load_end(self) -> None: if self._flip_horizontally: + assert self.im is not None self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) @@ -171,7 +175,7 @@ def load_end(self): } -def _save(im, fp, filename): +def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: @@ -194,6 +198,7 @@ def _save(im, fp, filename): warnings.warn("id_section has been trimmed to 255 characters") if colormaptype: + assert im.im is not None palette = im.im.getpalette("RGB", "BGR") colormaplength, colormapentry = len(palette) // 3, 24 else: From 7972332bc59800aa64c23664645fe9f8277cdbf4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jan 2024 19:22:45 +1100 Subject: [PATCH 0088/2374] Added type hints --- src/PIL/Image.py | 2 +- src/PIL/ImageFile.py | 2 ++ src/PIL/PpmImagePlugin.py | 46 +++++++++++++++++++++++++-------------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ec1cff89636..553f36703b3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -282,7 +282,7 @@ def getmodebandnames(mode): return ImageMode.getmode(mode).bands -def getmodebands(mode): +def getmodebands(mode: str) -> int: """ Gets the number of individual bands for this mode. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 40353da677b..5ba5a6f8277 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -616,6 +616,8 @@ def extents(self): class PyCodec: + fd: io.BytesIO | None + def __init__(self, mode, *args): self.im = None self.state = PyCodecState() diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 9d37dcde099..3e45ba95c84 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -16,6 +16,7 @@ from __future__ import annotations import math +from io import BytesIO from . import Image, ImageFile from ._binary import i16be as i16 @@ -45,7 +46,7 @@ } -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[0:1] == b"P" and prefix[1] in b"0123456fy" @@ -57,7 +58,9 @@ class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" - def _read_magic(self): + def _read_magic(self) -> bytes: + assert self.fp is not None + magic = b"" # read until whitespace or longest available magic number for _ in range(6): @@ -67,7 +70,9 @@ def _read_magic(self): magic += c return magic - def _read_token(self): + def _read_token(self) -> bytes: + assert self.fp is not None + token = b"" while len(token) <= 10: # read until next whitespace or limit of 10 characters c = self.fp.read(1) @@ -93,7 +98,9 @@ def _read_token(self): raise ValueError(msg) return token - def _open(self): + def _open(self) -> None: + assert self.fp is not None + magic_number = self._read_magic() try: mode = MODES[magic_number] @@ -114,6 +121,8 @@ def _open(self): decoder_name = "raw" if magic_number in (b"P1", b"P2", b"P3"): decoder_name = "ppm_plain" + + args: str | tuple[str | int, ...] if mode == "1": args = "1;I" elif mode == "F": @@ -151,16 +160,19 @@ def _open(self): class PpmPlainDecoder(ImageFile.PyDecoder): _pulls_fd = True + _comment_spans: bool + + def _read_block(self) -> bytes: + assert self.fd is not None - def _read_block(self): return self.fd.read(ImageFile.SAFEBLOCK) - def _find_comment_end(self, block, start=0): + def _find_comment_end(self, block: bytes, start: int = 0) -> int: a = block.find(b"\n", start) b = block.find(b"\r", start) return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1) - def _ignore_comments(self, block): + def _ignore_comments(self, block: bytes) -> bytes: if self._comment_spans: # Finish current comment while block: @@ -194,7 +206,7 @@ def _ignore_comments(self, block): break return block - def _decode_bitonal(self): + def _decode_bitonal(self) -> bytearray: """ This is a separate method because in the plain PBM format, all data tokens are exactly one byte, so the inter-token whitespace is optional. @@ -219,7 +231,7 @@ def _decode_bitonal(self): invert = bytes.maketrans(b"01", b"\xFF\x00") return data.translate(invert) - def _decode_blocks(self, maxval): + def _decode_blocks(self, maxval: int) -> bytearray: data = bytearray() max_len = 10 out_byte_count = 4 if self.mode == "I" else 1 @@ -227,7 +239,7 @@ def _decode_blocks(self, maxval): bands = Image.getmodebands(self.mode) total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count - half_token = False + half_token = b"" while len(data) != total_bytes: block = self._read_block() # read next block if not block: @@ -241,7 +253,7 @@ def _decode_blocks(self, maxval): if half_token: block = half_token + block # stitch half_token to new block - half_token = False + half_token = b"" tokens = block.split() @@ -259,15 +271,15 @@ def _decode_blocks(self, maxval): raise ValueError(msg) value = int(token) if value > maxval: - msg = f"Channel value too large for this mode: {value}" - raise ValueError(msg) + msg_str = f"Channel value too large for this mode: {value}" + raise ValueError(msg_str) value = round(value / maxval * out_max) data += o32(value) if self.mode == "I" else o8(value) if len(data) == total_bytes: # finished! break return data - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: self._comment_spans = False if self.mode == "1": data = self._decode_bitonal() @@ -283,7 +295,9 @@ def decode(self, buffer): class PpmDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + data = bytearray() maxval = self.args[-1] in_byte_count = 1 if maxval < 256 else 2 @@ -310,7 +324,7 @@ def decode(self, buffer): # -------------------------------------------------------------------- -def _save(im, fp, filename): +def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": From d8c7af0157269f82106216788e9073b4379f786d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jan 2024 23:10:37 +1100 Subject: [PATCH 0089/2374] Added type hints to GdImageFile --- src/PIL/GdImageFile.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index d84876eb6b6..7bb4736af13 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -27,6 +27,8 @@ class is not registered for use with :py:func:`PIL.Image.open()`. To open a """ from __future__ import annotations +from io import BytesIO + from . import ImageFile, ImagePalette, UnidentifiedImageError from ._binary import i16be as i16 from ._binary import i32be as i32 @@ -43,8 +45,10 @@ class GdImageFile(ImageFile.ImageFile): format = "GD" format_description = "GD uncompressed images" - def _open(self): + def _open(self) -> None: # Header + assert self.fp is not None + s = self.fp.read(1037) if i16(s) not in [65534, 65535]: @@ -76,7 +80,7 @@ def _open(self): ] -def open(fp, mode="r"): +def open(fp: BytesIO, mode: str = "r") -> GdImageFile: """ Load texture from a GD image file. From 6a85653cc373fd78fdf04e53d3fc38b6d83a7e1b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 Jan 2024 12:05:54 +1100 Subject: [PATCH 0090/2374] Added type hints --- src/PIL/MpegImagePlugin.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index f4e598ca3a0..b9e9243e59f 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -14,6 +14,8 @@ # from __future__ import annotations +from io import BytesIO + from . import Image, ImageFile from ._binary import i8 @@ -22,15 +24,15 @@ class BitStream: - def __init__(self, fp): + def __init__(self, fp: BytesIO) -> None: self.fp = fp self.bits = 0 self.bitbuffer = 0 - def next(self): + def next(self) -> int: return i8(self.fp.read(1)) - def peek(self, bits): + def peek(self, bits: int) -> int: while self.bits < bits: c = self.next() if c < 0: @@ -40,13 +42,13 @@ def peek(self, bits): self.bits += 8 return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 - def skip(self, bits): + def skip(self, bits: int) -> None: while self.bits < bits: self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1)) self.bits += 8 self.bits = self.bits - bits - def read(self, bits): + def read(self, bits: int) -> int: v = self.peek(bits) self.bits = self.bits - bits return v @@ -61,9 +63,10 @@ class MpegImageFile(ImageFile.ImageFile): format = "MPEG" format_description = "MPEG" - def _open(self): - s = BitStream(self.fp) + def _open(self) -> None: + assert self.fp is not None + s = BitStream(self.fp) if s.read(32) != 0x1B3: msg = "not an MPEG file" raise SyntaxError(msg) From 81b5c5dc68be449478442364a86e391906a61228 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 19 Jan 2024 08:37:58 +1100 Subject: [PATCH 0091/2374] Added type hints --- Tests/helper.py | 2 +- Tests/oss-fuzz/fuzz_font.py | 4 ++-- Tests/oss-fuzz/fuzz_pillow.py | 4 ++-- Tests/oss-fuzz/fuzzers.py | 8 ++++---- Tests/oss-fuzz/test_fuzzers.py | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index b333c2fd4ef..88c1f02a803 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -158,7 +158,7 @@ def assert_tuple_approx_equal(actuals, targets, threshold, msg): assert value, msg + ": " + repr(actuals) + " != " + repr(targets) -def skip_unless_feature(feature): +def skip_unless_feature(feature: str) -> pytest.MarkDecorator: reason = f"{feature} not available" return pytest.mark.skipif(not features.check(feature), reason=reason) diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py index bc2ba9a7e27..8788d7021d3 100755 --- a/Tests/oss-fuzz/fuzz_font.py +++ b/Tests/oss-fuzz/fuzz_font.py @@ -23,7 +23,7 @@ import fuzzers -def TestOneInput(data): +def TestOneInput(data: bytes) -> None: try: fuzzers.fuzz_font(data) except Exception: @@ -32,7 +32,7 @@ def TestOneInput(data): pass -def main(): +def main() -> None: fuzzers.enable_decompressionbomb_error() atheris.Setup(sys.argv, TestOneInput) atheris.Fuzz() diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index 545daccb680..e6e99d415a6 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -23,7 +23,7 @@ import fuzzers -def TestOneInput(data): +def TestOneInput(data: bytes) -> None: try: fuzzers.fuzz_image(data) except Exception: @@ -32,7 +32,7 @@ def TestOneInput(data): pass -def main(): +def main() -> None: fuzzers.enable_decompressionbomb_error() atheris.Setup(sys.argv, TestOneInput) atheris.Fuzz() diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py index 3f3c1e38833..3afa952157b 100644 --- a/Tests/oss-fuzz/fuzzers.py +++ b/Tests/oss-fuzz/fuzzers.py @@ -5,18 +5,18 @@ from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont -def enable_decompressionbomb_error(): +def enable_decompressionbomb_error() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = True warnings.filterwarnings("ignore") warnings.simplefilter("error", Image.DecompressionBombWarning) -def disable_decompressionbomb_error(): +def disable_decompressionbomb_error() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = False warnings.resetwarnings() -def fuzz_image(data): +def fuzz_image(data: bytes) -> None: # This will fail on some images in the corpus, as we have many # invalid images in the test suite. with Image.open(io.BytesIO(data)) as im: @@ -25,7 +25,7 @@ def fuzz_image(data): im.save(io.BytesIO(), "BMP") -def fuzz_font(data): +def fuzz_font(data: bytes) -> None: wrapper = io.BytesIO(data) try: font = ImageFont.truetype(wrapper) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 68834045a52..028ee71eec8 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -23,7 +23,7 @@ "path", subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"), ) -def test_fuzz_images(path): +def test_fuzz_images(path: str) -> None: fuzzers.enable_decompressionbomb_error() try: with open(path, "rb") as f: @@ -54,7 +54,7 @@ def test_fuzz_images(path): @pytest.mark.parametrize( "path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n") ) -def test_fuzz_fonts(path): +def test_fuzz_fonts(path: str) -> None: if not path: return with open(path, "rb") as f: From 1d63cffdadbf649442804dae288992cdd854a849 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 19 Jan 2024 21:50:27 +1100 Subject: [PATCH 0092/2374] Added type hints --- Tests/check_fli_overflow.py | 2 +- Tests/check_imaging_leaks.py | 15 ++++++++++----- Tests/check_j2k_leaks.py | 4 ++-- Tests/check_j2k_overflow.py | 3 ++- Tests/check_jpeg_leaks.py | 6 +++--- Tests/check_large_memory.py | 12 ++++++++---- Tests/check_large_memory_numpy.py | 7 ++++--- Tests/check_libtiff_segfault.py | 2 +- Tests/check_png_dos.py | 8 ++++---- Tests/check_wheel.py | 6 +++--- Tests/helper.py | 4 ++-- 11 files changed, 40 insertions(+), 29 deletions(-) diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 0fabcb5d35f..109f8fb5498 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -4,7 +4,7 @@ TEST_FILE = "Tests/images/fli_overflow.fli" -def test_fli_overflow(): +def test_fli_overflow() -> None: # this should not crash with a malloc error or access violation with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 8c17c051d4b..b0c4f620b93 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -2,6 +2,8 @@ from __future__ import annotations import pytest +from typing import Any, Callable + from PIL import Image from .helper import is_win32 @@ -12,31 +14,34 @@ pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") -def _get_mem_usage(): +def _get_mem_usage() -> float: from resource import RUSAGE_SELF, getpagesize, getrusage mem = getrusage(RUSAGE_SELF).ru_maxrss return mem * getpagesize() / 1024 / 1024 -def _test_leak(min_iterations, max_iterations, fn, *args, **kwargs): +def _test_leak( + min_iterations: int, max_iterations: int, fn: Callable[..., None], *args: Any +) -> None: mem_limit = None for i in range(max_iterations): - fn(*args, **kwargs) + fn(*args) mem = _get_mem_usage() if i < min_iterations: mem_limit = mem + 1 continue msg = f"memory usage limit exceeded after {i + 1} iterations" + assert mem_limit is not None assert mem <= mem_limit, msg -def test_leak_putdata(): +def test_leak_putdata() -> None: im = Image.new("RGB", (25, 25)) _test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) -def test_leak_getlist(): +def test_leak_getlist() -> None: im = Image.new("P", (25, 25)) _test_leak( min_iterations, diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py index 83a12e2c29f..0d0d3a57c2d 100644 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -19,7 +19,7 @@ ] -def test_leak_load(): +def test_leak_load() -> None: from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit setrlimit(RLIMIT_STACK, (stack_size, stack_size)) @@ -29,7 +29,7 @@ def test_leak_load(): im.load() -def test_leak_save(): +def test_leak_save() -> None: from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit setrlimit(RLIMIT_STACK, (stack_size, stack_size)) diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index 982f6ea74d2..ba14964b55f 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,10 +1,11 @@ from __future__ import annotations +from pathlib import PosixPath import pytest from PIL import Image -def test_j2k_overflow(tmp_path): +def test_j2k_overflow(tmp_path: PosixPath) -> None: im = Image.new("RGBA", (1024, 131584)) target = str(tmp_path / "temp.jpc") with pytest.raises(OSError): diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 3cd37c7af7b..e91709a9604 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -110,14 +110,14 @@ [standard_l_qtable, standard_chrominance_qtable], ), ) -def test_qtables_leak(qtables): +def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None: im = hopper("RGB") for _ in range(iterations): test_output = BytesIO() im.save(test_output, "JPEG", qtables=qtables) -def test_exif_leak(): +def test_exif_leak() -> None: """ pre patch: @@ -180,7 +180,7 @@ def test_exif_leak(): im.save(test_output, "JPEG", exif=exif) -def test_base_save(): +def test_base_save() -> None: """ base case: MB diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 9b83798d5e7..4d67270765c 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import PosixPath import sys +from types import ModuleType import pytest @@ -15,6 +17,7 @@ # 2.7 and 3.2. +numpy: ModuleType | None try: import numpy except ImportError: @@ -27,23 +30,24 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") -def _write_png(tmp_path, xdim, ydim): +def _write_png(tmp_path: PosixPath, xdim: int, ydim: int) -> None: f = str(tmp_path / "temp.png") im = Image.new("L", (xdim, ydim), 0) im.save(f) -def test_large(tmp_path): +def test_large(tmp_path: PosixPath) -> None: """succeeded prepatch""" _write_png(tmp_path, XDIM, YDIM) -def test_2gpx(tmp_path): +def test_2gpx(tmp_path: PosixPath) -> None: """failed prepatch""" _write_png(tmp_path, XDIM, XDIM) @pytest.mark.skipif(numpy is None, reason="Numpy is not installed") -def test_size_greater_than_int(): +def test_size_greater_than_int() -> None: + assert numpy is not None arr = numpy.ndarray(shape=(16394, 16394)) Image.fromarray(arr) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 0ff3de8dcc7..d1cbad887aa 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,4 +1,5 @@ from __future__ import annotations +from pathlib import PosixPath import sys import pytest @@ -23,7 +24,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") -def _write_png(tmp_path, xdim, ydim): +def _write_png(tmp_path: PosixPath, xdim: int, ydim: int) -> None: dtype = np.uint8 a = np.zeros((xdim, ydim), dtype=dtype) f = str(tmp_path / "temp.png") @@ -31,11 +32,11 @@ def _write_png(tmp_path, xdim, ydim): im.save(f) -def test_large(tmp_path): +def test_large(tmp_path: PosixPath) -> None: """succeeded prepatch""" _write_png(tmp_path, XDIM, YDIM) -def test_2gpx(tmp_path): +def test_2gpx(tmp_path: PosixPath) -> None: """failed prepatch""" _write_png(tmp_path, XDIM, XDIM) diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index ee1d7d11f0c..f1c77efc16b 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -6,7 +6,7 @@ TEST_FILE = "Tests/images/libtiff_segfault.tif" -def test_libtiff_segfault(): +def test_libtiff_segfault() -> None: """This test should not segfault. It will on Pillow <= 3.1.0 and libtiff >= 4.0.0 """ diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 292fe4b7f09..a3d50fa57e5 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -7,7 +7,7 @@ TEST_FILE = "Tests/images/png_decompression_dos.png" -def test_ignore_dos_text(): +def test_ignore_dos_text() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -23,7 +23,7 @@ def test_ignore_dos_text(): assert len(s) < 1024 * 1024, "Text chunk larger than 1M" -def test_dos_text(): +def test_dos_text() -> None: try: im = Image.open(TEST_FILE) im.load() @@ -35,7 +35,7 @@ def test_dos_text(): assert len(s) < 1024 * 1024, "Text chunk larger than 1M" -def test_dos_total_memory(): +def test_dos_total_memory() -> None: im = Image.new("L", (1, 1)) compressed_data = zlib.compress(b"a" * 1024 * 1023) @@ -52,7 +52,7 @@ def test_dos_total_memory(): try: im2 = Image.open(b) except ValueError as msg: - assert "Too much memory" in msg + assert "Too much memory" in str(msg) return total_len = 0 diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index afe4cc3eeaa..969e596b45a 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -4,7 +4,7 @@ from PIL import features -def test_wheel_modules(): +def test_wheel_modules() -> None: expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} # tkinter is not available in cibuildwheel installed CPython on Windows @@ -18,13 +18,13 @@ def test_wheel_modules(): assert set(features.get_supported_modules()) == expected_modules -def test_wheel_codecs(): +def test_wheel_codecs() -> None: expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"} assert set(features.get_supported_codecs()) == expected_codecs -def test_wheel_features(): +def test_wheel_features() -> None: expected_features = { "webp_anim", "webp_mux", diff --git a/Tests/helper.py b/Tests/helper.py index 88c1f02a803..34839422e2f 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -238,7 +238,7 @@ def tostring(im, string_format, **options): return out.getvalue() -def hopper(mode=None, cache={}): +def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image: if mode is None: # Always return fresh not-yet-loaded version of image. # Operations on not-yet-loaded images is separate class of errors @@ -323,7 +323,7 @@ def is_ppc64le(): return platform.machine() == "ppc64le" -def is_win32(): +def is_win32() -> bool: return sys.platform.startswith("win32") From 9b6c1e3763000329e80173b99507e10aa0569b43 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jan 2024 11:13:06 +1100 Subject: [PATCH 0093/2374] Added type hints --- Tests/helper.py | 157 +++++++++++++++++++++++++++--------------------- 1 file changed, 89 insertions(+), 68 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 88c1f02a803..12a4fed3811 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -14,47 +14,46 @@ import pytest from packaging.version import parse as parse_version +from typing import Any, Callable, Sequence from PIL import Image, ImageMath, features logger = logging.getLogger(__name__) - -HAS_UPLOADER = False - +uploader = None if os.environ.get("SHOW_ERRORS"): - # local img.show for errors. - HAS_UPLOADER = True - - class test_image_results: - @staticmethod - def upload(a, b): - a.show() - b.show() - + uploader = "show" elif "GITHUB_ACTIONS" in os.environ: - HAS_UPLOADER = True - - class test_image_results: - @staticmethod - def upload(a, b): - dir_errors = os.path.join(os.path.dirname(__file__), "errors") - os.makedirs(dir_errors, exist_ok=True) - tmpdir = tempfile.mkdtemp(dir=dir_errors) - a.save(os.path.join(tmpdir, "a.png")) - b.save(os.path.join(tmpdir, "b.png")) - return tmpdir - + uploader = "github_actions" else: try: import test_image_results - HAS_UPLOADER = True + uploader = "aws" except ImportError: pass -def convert_to_comparable(a, b): +def upload(a: Image.Image, b: Image.Image) -> str | None: + if uploader == "show": + # local img.show for errors. + a.show() + b.show() + elif uploader == "github_actions": + dir_errors = os.path.join(os.path.dirname(__file__), "errors") + os.makedirs(dir_errors, exist_ok=True) + tmpdir = tempfile.mkdtemp(dir=dir_errors) + a.save(os.path.join(tmpdir, "a.png")) + b.save(os.path.join(tmpdir, "b.png")) + return tmpdir + elif uploader == "aws": + return test_image_results.upload(a, b) + return None + + +def convert_to_comparable( + a: Image.Image, b: Image.Image +) -> tuple[Image.Image, Image.Image]: new_a, new_b = a, b if a.mode == "P": new_a = Image.new("L", a.size) @@ -67,14 +66,16 @@ def convert_to_comparable(a, b): return new_a, new_b -def assert_deep_equal(a, b, msg=None): +def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None: try: assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" except Exception: assert a == b, msg -def assert_image(im, mode, size, msg=None): +def assert_image( + im: Image.Image, mode: str, size: tuple[int, int], msg: str | None = None +) -> None: if mode is not None: assert im.mode == mode, ( msg or f"got mode {repr(im.mode)}, expected {repr(mode)}" @@ -86,13 +87,13 @@ def assert_image(im, mode, size, msg=None): ) -def assert_image_equal(a, b, msg=None): +def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -> None: assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" if a.tobytes() != b.tobytes(): - if HAS_UPLOADER: + if uploader: try: - url = test_image_results.upload(a, b) + url = upload(a, b) logger.error("URL for test images: %s", url) except Exception: pass @@ -100,14 +101,18 @@ def assert_image_equal(a, b, msg=None): pytest.fail(msg or "got different content") -def assert_image_equal_tofile(a, filename, msg=None, mode=None): +def assert_image_equal_tofile( + a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None +) -> None: with Image.open(filename) as img: if mode: img = img.convert(mode) assert_image_equal(a, img, msg) -def assert_image_similar(a, b, epsilon, msg=None): +def assert_image_similar( + a: Image.Image, b: Image.Image, epsilon: float, msg: str | None = None +) -> None: assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" @@ -125,37 +130,43 @@ def assert_image_similar(a, b, epsilon, msg=None): + f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}" ) except Exception as e: - if HAS_UPLOADER: + if uploader: try: - url = test_image_results.upload(a, b) + url = upload(a, b) logger.exception("URL for test images: %s", url) except Exception: pass raise e -def assert_image_similar_tofile(a, filename, epsilon, msg=None, mode=None): +def assert_image_similar_tofile( + a: Image.Image, + filename: str, + epsilon: float, + msg: str | None = None, + mode: str | None = None, +) -> None: with Image.open(filename) as img: if mode: img = img.convert(mode) assert_image_similar(a, img, epsilon, msg) -def assert_all_same(items, msg=None): +def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None: assert items.count(items[0]) == len(items), msg -def assert_not_all_same(items, msg=None): +def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None: assert items.count(items[0]) != len(items), msg -def assert_tuple_approx_equal(actuals, targets, threshold, msg): +def assert_tuple_approx_equal( + actuals: Sequence[int], targets: tuple[int, ...], threshold: int, msg: str +) -> None: """Tests if actuals has values within threshold from targets""" - value = True for i, target in enumerate(targets): - value *= target - threshold <= actuals[i] <= target + threshold - - assert value, msg + ": " + repr(actuals) + " != " + repr(targets) + if not (target - threshold <= actuals[i] <= target + threshold): + pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets)) def skip_unless_feature(feature: str) -> pytest.MarkDecorator: @@ -163,17 +174,24 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator: return pytest.mark.skipif(not features.check(feature), reason=reason) -def skip_unless_feature_version(feature, version_required, reason=None): +def skip_unless_feature_version( + feature: str, required: str, reason: str | None = None +) -> pytest.MarkDecorator: if not features.check(feature): return pytest.mark.skip(f"{feature} not available") if reason is None: - reason = f"{feature} is older than {version_required}" - version_required = parse_version(version_required) + reason = f"{feature} is older than {required}" + version_required = parse_version(required) version_available = parse_version(features.version(feature)) return pytest.mark.skipif(version_available < version_required, reason=reason) -def mark_if_feature_version(mark, feature, version_blacklist, reason=None): +def mark_if_feature_version( + mark: pytest.MarkDecorator, + feature: str, + version_blacklist: str, + reason: str | None = None, +) -> pytest.MarkDecorator: if not features.check(feature): return pytest.mark.pil_noop_mark() if reason is None: @@ -194,7 +212,7 @@ class PillowLeakTestCase: iterations = 100 # count mem_limit = 512 # k - def _get_mem_usage(self): + def _get_mem_usage(self) -> float: """ Gets the RUSAGE memory usage, returns in K. Encapsulates the difference between macOS and Linux rss reporting @@ -216,7 +234,7 @@ def _get_mem_usage(self): # This is the maximum resident set size used (in kilobytes). return mem # Kb - def _test_leak(self, core): + def _test_leak(self, core: Callable[[], None]) -> None: start_mem = self._get_mem_usage() for cycle in range(self.iterations): core() @@ -228,17 +246,17 @@ def _test_leak(self, core): # helpers -def fromstring(data): +def fromstring(data: bytes) -> Image.Image: return Image.open(BytesIO(data)) -def tostring(im, string_format, **options): +def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes: out = BytesIO() im.save(out, string_format, **options) return out.getvalue() -def hopper(mode=None, cache={}): +def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image: if mode is None: # Always return fresh not-yet-loaded version of image. # Operations on not-yet-loaded images is separate class of errors @@ -259,29 +277,31 @@ def hopper(mode=None, cache={}): return im.copy() -def djpeg_available(): +def djpeg_available() -> bool: if shutil.which("djpeg"): try: subprocess.check_call(["djpeg", "-version"]) return True except subprocess.CalledProcessError: # pragma: no cover - return False + pass + return False -def cjpeg_available(): +def cjpeg_available() -> bool: if shutil.which("cjpeg"): try: subprocess.check_call(["cjpeg", "-version"]) return True except subprocess.CalledProcessError: # pragma: no cover - return False + pass + return False -def netpbm_available(): +def netpbm_available() -> bool: return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) -def magick_command(): +def magick_command() -> list[str] | None: if sys.platform == "win32": magickhome = os.environ.get("MAGICK_HOME") if magickhome: @@ -298,47 +318,48 @@ def magick_command(): return imagemagick if graphicsmagick and shutil.which(graphicsmagick[0]): return graphicsmagick + return None -def on_appveyor(): +def on_appveyor() -> bool: return "APPVEYOR" in os.environ -def on_github_actions(): +def on_github_actions() -> bool: return "GITHUB_ACTIONS" in os.environ -def on_ci(): +def on_ci() -> bool: # GitHub Actions and AppVeyor have "CI" return "CI" in os.environ -def is_big_endian(): +def is_big_endian() -> bool: return sys.byteorder == "big" -def is_ppc64le(): +def is_ppc64le() -> bool: import platform return platform.machine() == "ppc64le" -def is_win32(): +def is_win32() -> bool: return sys.platform.startswith("win32") -def is_pypy(): +def is_pypy() -> bool: return hasattr(sys, "pypy_translation_info") -def is_mingw(): +def is_mingw() -> bool: return sysconfig.get_platform() == "mingw" class CachedProperty: - def __init__(self, func): + def __init__(self, func: Callable[[Any], None]) -> None: self.func = func - def __get__(self, instance, cls=None): + def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any: result = instance.__dict__[self.func.__name__] = self.func(instance) return result From 5dc3de7974833bc8a0e2ecb389c6c183dd1a1d21 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 06:07:48 +0000 Subject: [PATCH 0094/2374] Update actions/cache action to v4 --- .github/workflows/lint.yml | 2 +- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-windows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9069fc615f3..cc4760288e5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - name: pre-commit cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pre-commit key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7244315ac15..9c3eb092417 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -95,7 +95,7 @@ jobs: python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT - name: pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: 'C:\cygwin\home\runneradmin\.cache\pip' key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 372f97fd667..8cad7a8b281 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -89,7 +89,7 @@ jobs: - name: Cache build id: build-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: winbuild\build key: From 74af933a9f0441e1285c398675dc3eca1de5406c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Sat, 20 Jan 2024 10:08:14 +0100 Subject: [PATCH 0095/2374] Link to stable setuptools documentation Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- winbuild/build.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build.rst b/winbuild/build.rst index 26d0da0a35a..f40982cd546 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -97,7 +97,7 @@ To build a wheel instead, run:: winbuild\build\build_env.cmd python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . -.. _editable mode: https://setuptools.pypa.io/en/latest/userguide/development_mode.html +.. _editable mode: https://setuptools.pypa.io/en/stable/userguide/development_mode.html Testing Pillow -------------- From f7701e6596c9a2e7c9ed886621f01b3861d70990 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jan 2024 21:04:57 +1100 Subject: [PATCH 0096/2374] Do not log URL of test images if there is no URL --- Tests/helper.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 12a4fed3811..670c93633d7 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -91,12 +91,12 @@ def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) - assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" if a.tobytes() != b.tobytes(): - if uploader: - try: - url = upload(a, b) + try: + url = upload(a, b) + if url: logger.error("URL for test images: %s", url) - except Exception: - pass + except Exception: + pass pytest.fail(msg or "got different content") @@ -130,12 +130,12 @@ def assert_image_similar( + f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}" ) except Exception as e: - if uploader: - try: - url = upload(a, b) + try: + url = upload(a, b) + if url: logger.exception("URL for test images: %s", url) - except Exception: - pass + except Exception: + pass raise e From 99d851957fec5394bbc9d61ceff57f534bd1b873 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:23:08 +1100 Subject: [PATCH 0097/2374] Return early Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 670c93633d7..e4ed9c551ae 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -283,7 +283,7 @@ def djpeg_available() -> bool: subprocess.check_call(["djpeg", "-version"]) return True except subprocess.CalledProcessError: # pragma: no cover - pass + return False return False @@ -293,7 +293,7 @@ def cjpeg_available() -> bool: subprocess.check_call(["cjpeg", "-version"]) return True except subprocess.CalledProcessError: # pragma: no cover - pass + return False return False From 970bd102ba0304adeb5351cc09c616ab23535d2c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:24:34 +1100 Subject: [PATCH 0098/2374] Updated type hint Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/helper.py b/Tests/helper.py index e4ed9c551ae..4d2d27226ae 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -66,7 +66,7 @@ def convert_to_comparable( return new_a, new_b -def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None: +def assert_deep_equal(a: Sequence[Any], b: Sequence[Any], msg: str | None = None) -> None: try: assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" except Exception: From 9454c28f0f8363d1316b1dfec0f23404f9e36082 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 10:24:54 +0000 Subject: [PATCH 0099/2374] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/helper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/helper.py b/Tests/helper.py index 4d2d27226ae..203d20053c3 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -66,7 +66,9 @@ def convert_to_comparable( return new_a, new_b -def assert_deep_equal(a: Sequence[Any], b: Sequence[Any], msg: str | None = None) -> None: +def assert_deep_equal( + a: Sequence[Any], b: Sequence[Any], msg: str | None = None +) -> None: try: assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" except Exception: From a18cee35ff4362c834d0fa43277ee5b02ab9c693 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:26:31 +1100 Subject: [PATCH 0100/2374] Updated import order Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/helper.py b/Tests/helper.py index 203d20053c3..99170c765dd 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -11,10 +11,10 @@ import sysconfig import tempfile from io import BytesIO +from typing import Any, Callable, Sequence import pytest from packaging.version import parse as parse_version -from typing import Any, Callable, Sequence from PIL import Image, ImageMath, features From eba0be98ecea7150c691e05bf6faa3c73c121689 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:22:04 +0200 Subject: [PATCH 0101/2374] isort Tests --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 789df6f5e67..b1ce9cf1d07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,7 +119,6 @@ extend-ignore = [ ] [tool.ruff.per-file-ignores] -"Tests/*.py" = ["I001"] "Tests/oss-fuzz/fuzz_font.py" = ["I002"] "Tests/oss-fuzz/fuzz_pillow.py" = ["I002"] From 53c3cd9f8e91afc038b64b14cd63d1eb592f717a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:23:03 +0200 Subject: [PATCH 0102/2374] isort Tests --- Tests/bench_cffi_access.py | 1 + Tests/check_fli_overflow.py | 1 + Tests/check_imaging_leaks.py | 1 + Tests/check_j2k_leaks.py | 1 + Tests/check_j2k_overflow.py | 1 + Tests/check_jp2_overflow.py | 1 - Tests/check_jpeg_leaks.py | 1 + Tests/check_large_memory.py | 1 + Tests/check_large_memory_numpy.py | 1 + Tests/check_libtiff_segfault.py | 1 + Tests/check_png_dos.py | 1 + Tests/check_release_notes.py | 1 + Tests/check_wheel.py | 1 + Tests/conftest.py | 1 + Tests/createfontdatachunk.py | 1 + Tests/oss-fuzz/fuzzers.py | 1 + Tests/oss-fuzz/test_fuzzers.py | 1 + Tests/test_000_sanity.py | 1 + Tests/test_binary.py | 1 + Tests/test_bmp_reference.py | 1 + Tests/test_box_blur.py | 1 + Tests/test_color_lut.py | 1 + Tests/test_core_resources.py | 1 + Tests/test_decompression_bomb.py | 1 + Tests/test_deprecate.py | 1 + Tests/test_features.py | 1 + Tests/test_file_apng.py | 1 + Tests/test_file_blp.py | 1 + Tests/test_file_bmp.py | 1 + Tests/test_file_bufrstub.py | 1 + Tests/test_file_container.py | 1 + Tests/test_file_cur.py | 1 + Tests/test_file_dcx.py | 1 + Tests/test_file_dds.py | 1 + Tests/test_file_eps.py | 1 + Tests/test_file_fits.py | 1 + Tests/test_file_fli.py | 1 + Tests/test_file_fpx.py | 1 + Tests/test_file_ftex.py | 1 + Tests/test_file_gbr.py | 1 + Tests/test_file_gd.py | 1 + Tests/test_file_gif.py | 1 + Tests/test_file_gimpgradient.py | 1 + Tests/test_file_gimppalette.py | 1 + Tests/test_file_gribstub.py | 1 + Tests/test_file_hdf5stub.py | 1 + Tests/test_file_icns.py | 1 + Tests/test_file_ico.py | 1 + Tests/test_file_im.py | 1 + Tests/test_file_imt.py | 1 + Tests/test_file_iptc.py | 1 + Tests/test_file_jpeg.py | 1 + Tests/test_file_jpeg2k.py | 1 + Tests/test_file_libtiff.py | 1 + Tests/test_file_libtiff_small.py | 1 + Tests/test_file_mcidas.py | 1 + Tests/test_file_mic.py | 1 + Tests/test_file_mpo.py | 1 + Tests/test_file_msp.py | 1 + Tests/test_file_palm.py | 1 + Tests/test_file_pcd.py | 1 + Tests/test_file_pcx.py | 1 + Tests/test_file_pdf.py | 1 + Tests/test_file_pixar.py | 1 + Tests/test_file_png.py | 1 + Tests/test_file_ppm.py | 1 + Tests/test_file_psd.py | 1 + Tests/test_file_qoi.py | 1 + Tests/test_file_sgi.py | 1 + Tests/test_file_spider.py | 1 + Tests/test_file_sun.py | 1 + Tests/test_file_tar.py | 1 + Tests/test_file_tga.py | 1 + Tests/test_file_tiff.py | 1 + Tests/test_file_tiff_metadata.py | 1 + Tests/test_file_wal.py | 1 + Tests/test_file_webp.py | 1 + Tests/test_file_webp_alpha.py | 1 + Tests/test_file_webp_animated.py | 1 + Tests/test_file_webp_lossless.py | 1 + Tests/test_file_webp_metadata.py | 1 + Tests/test_file_wmf.py | 1 + Tests/test_file_xbm.py | 1 + Tests/test_file_xpm.py | 1 + Tests/test_file_xvthumb.py | 1 + Tests/test_font_bdf.py | 1 + Tests/test_font_crash.py | 1 + Tests/test_font_leaks.py | 1 + Tests/test_font_pcf.py | 1 + Tests/test_font_pcf_charsets.py | 1 + Tests/test_fontfile.py | 1 + Tests/test_format_hsv.py | 1 + Tests/test_format_lab.py | 1 + Tests/test_image.py | 1 + Tests/test_image_access.py | 1 + Tests/test_image_array.py | 1 + Tests/test_image_convert.py | 1 + Tests/test_image_copy.py | 1 + Tests/test_image_crop.py | 1 + Tests/test_image_draft.py | 1 + Tests/test_image_entropy.py | 1 + Tests/test_image_filter.py | 1 + Tests/test_image_frombytes.py | 1 + Tests/test_image_fromqimage.py | 1 + Tests/test_image_getbands.py | 1 + Tests/test_image_getbbox.py | 1 + Tests/test_image_getcolors.py | 1 + Tests/test_image_getdata.py | 1 + Tests/test_image_getextrema.py | 1 + Tests/test_image_getim.py | 1 + Tests/test_image_getpalette.py | 1 + Tests/test_image_getprojection.py | 1 + Tests/test_image_histogram.py | 1 + Tests/test_image_load.py | 1 + Tests/test_image_mode.py | 1 + Tests/test_image_paste.py | 1 + Tests/test_image_point.py | 1 + Tests/test_image_putalpha.py | 1 + Tests/test_image_putdata.py | 1 + Tests/test_image_putpalette.py | 1 + Tests/test_image_quantize.py | 1 + Tests/test_image_reduce.py | 1 + Tests/test_image_resample.py | 1 + Tests/test_image_resize.py | 1 + Tests/test_image_rotate.py | 1 + Tests/test_image_split.py | 1 + Tests/test_image_thumbnail.py | 1 + Tests/test_image_tobitmap.py | 1 + Tests/test_image_tobytes.py | 1 + Tests/test_image_transform.py | 1 + Tests/test_image_transpose.py | 1 + Tests/test_imagechops.py | 1 + Tests/test_imagecms.py | 1 + Tests/test_imagecolor.py | 1 + Tests/test_imagedraw.py | 1 + Tests/test_imagedraw2.py | 1 + Tests/test_imageenhance.py | 1 + Tests/test_imagefile.py | 1 + Tests/test_imagefont.py | 1 + Tests/test_imagefontctl.py | 1 + Tests/test_imagefontpil.py | 6 ++++-- Tests/test_imagegrab.py | 1 + Tests/test_imagemath.py | 1 + Tests/test_imagemorph.py | 1 + Tests/test_imageops.py | 1 + Tests/test_imageops_usm.py | 1 + Tests/test_imagepalette.py | 1 + Tests/test_imagepath.py | 1 + Tests/test_imageqt.py | 1 + Tests/test_imagesequence.py | 1 + Tests/test_imageshow.py | 1 + Tests/test_imagestat.py | 1 + Tests/test_imagetk.py | 1 + Tests/test_imagewin.py | 1 + Tests/test_imagewin_pointers.py | 1 + Tests/test_lib_image.py | 1 + Tests/test_lib_pack.py | 1 + Tests/test_locale.py | 1 + Tests/test_main.py | 1 + Tests/test_map.py | 1 + Tests/test_mode_i16.py | 1 + Tests/test_numpy.py | 1 + Tests/test_pdfparser.py | 1 + Tests/test_pickle.py | 1 + Tests/test_psdraw.py | 1 + Tests/test_pyroma.py | 1 + Tests/test_qt_image_qapplication.py | 1 + Tests/test_qt_image_toqimage.py | 1 + Tests/test_sgi_crash.py | 1 + Tests/test_shell_injection.py | 1 + Tests/test_tiff_ifdrational.py | 1 + Tests/test_uploader.py | 1 + Tests/test_util.py | 1 + Tests/test_webp_leaks.py | 1 + 174 files changed, 176 insertions(+), 3 deletions(-) diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 8a37c7d5157..ad15a9739b2 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -1,4 +1,5 @@ from __future__ import annotations + import time from PIL import PyAccess diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 0fabcb5d35f..8cb6ac0a6a8 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image TEST_FILE = "Tests/images/fli_overflow.fli" diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 8c17c051d4b..eed326a4ca8 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py index 83a12e2c29f..249c92cef86 100644 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index 982f6ea74d2..6f3aa437ea1 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py index 9afbff112b0..5adbb84b69c 100755 --- a/Tests/check_jp2_overflow.py +++ b/Tests/check_jp2_overflow.py @@ -14,7 +14,6 @@ # version. from __future__ import annotations - from PIL import Image repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2") diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 3cd37c7af7b..147deb28593 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 9b83798d5e7..fb336f6e142 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 0ff3de8dcc7..d91d615f8f8 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index ee1d7d11f0c..17b5336286f 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 292fe4b7f09..7b0ed242435 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -1,4 +1,5 @@ from __future__ import annotations + import zlib from io import BytesIO diff --git a/Tests/check_release_notes.py b/Tests/check_release_notes.py index ebfaffa47e1..cf414d7fff0 100644 --- a/Tests/check_release_notes.py +++ b/Tests/check_release_notes.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from pathlib import Path diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index afe4cc3eeaa..daf9e621148 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from PIL import features diff --git a/Tests/conftest.py b/Tests/conftest.py index cd64bd755f0..ac618c5b945 100644 --- a/Tests/conftest.py +++ b/Tests/conftest.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py index 2e990b70916..41c76f87eac 100755 --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 from __future__ import annotations + import base64 import os diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py index 3f3c1e38833..5c00e9716cb 100644 --- a/Tests/oss-fuzz/fuzzers.py +++ b/Tests/oss-fuzz/fuzzers.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import warnings diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 68834045a52..bf4fb45ff98 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -1,4 +1,5 @@ from __future__ import annotations + import subprocess import sys diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index c582dfad3e4..f64216bca8e 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_binary.py b/Tests/test_binary.py index 62da2663668..41fb93fcf48 100644 --- a/Tests/test_binary.py +++ b/Tests/test_binary.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import _binary diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index bed8dc3a899..0da41e85845 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import warnings diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index e798cba3d4f..461e6aaacb3 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageFilter diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 448ba2fac80..fcd1169ef81 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,4 +1,5 @@ from __future__ import annotations + from array import array import pytest diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 5275652f66a..d3f76fdb1b9 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 391948d40c7..d3049eff124 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index d45a6603c54..6c7f509a795 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import _deprecate diff --git a/Tests/test_features.py b/Tests/test_features.py index 8f0e4b4184a..b90c1d25f2b 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import re diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index e2c4569cee5..23263b5d459 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageSequence, PngImagePlugin diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 4c1e38d1ddf..27ff7ab6640 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 4cc92c5f684..225fb28ba51 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import pytest diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 5780232a2d6..45081832e68 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import BufrStubImagePlugin, Image diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 0da5d3824b0..95a5b2337d8 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import ContainerIO, Image diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 08c3257f9d1..27b2bc91489 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import CurImagePlugin, Image diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 25e4badbc92..cba7c10bf76 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index a605c8399d8..7064b74c07b 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -1,5 +1,6 @@ """Test DdsImagePlugin""" from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 8b48e83ad2c..8def9a43511 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import pytest diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 1383f9c5ca3..7444eb673cb 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 10bf36cc290..00377e0c92d 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index af3b7981561..d710070c01b 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index a494c8029c9..0f9154e3d09 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import FtexImagePlugin, Image diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index 7dfe0539673..d84004e1483 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import GbrImagePlugin, Image diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index ec80c54a122..e7db54fb44c 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import GdImageFile, UnidentifiedImageError diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 78b77e9743a..3e19940aab9 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings from io import BytesIO diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index d5be46dc39e..ceea1edd34c 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import GimpGradientFile, ImagePalette diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index 775d3b7cdae..28855c28acc 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL.GimpPaletteFile import GimpPaletteFile diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index d962e85a436..a4ce6dde674 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import GribStubImagePlugin, Image diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 9c776b712ee..72764461730 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Hdf5StubImagePlugin, Image diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index c62fffc5be8..314fa800868 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import os import warnings diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index de9fa353adb..99b3048d1e0 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import os diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 0cb26d06a51..a031b3e887c 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,4 +1,5 @@ from __future__ import annotations + import filecmp import warnings diff --git a/Tests/test_file_imt.py b/Tests/test_file_imt.py index 3db4885586a..aa13d4407bc 100644 --- a/Tests/test_file_imt.py +++ b/Tests/test_file_imt.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import pytest diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index c44a08f526a..a2c50ecefdf 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from io import BytesIO, StringIO diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 979c7e33d00..232e51f9126 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import re import warnings diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index aaa4104e57c..94b02c9ff52 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import re from io import BytesIO diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 65adf449dcc..494253c87c1 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,4 +1,5 @@ from __future__ import annotations + import base64 import io import itertools diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 9501c55a6b8..171e4a3f866 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO from PIL import Image diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index 4b31aaa7857..73eba5cc861 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, McIdasImagePlugin diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index e7ea39ea918..8c43f7d7ab7 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImagePalette diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index da62bc6d429..c7121ea28c9 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings from io import BytesIO diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index f4e357ae0fd..9037ea33b54 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import pytest diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 735840de4cb..eba69415395 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os.path import subprocess diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 596a3414f79..1a37c6ab31d 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index f42ec4a6894..2565e0b6ddc 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageFile, PcxImagePlugin diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 9e07d9ed014..30c54c963cb 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import os import os.path diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py index 63779f202cb..c6ddc54e714 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, PixarImagePlugin diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ff386211061..ae2a4772b60 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,4 +1,5 @@ from __future__ import annotations + import re import sys import warnings diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index d8e259b1cf8..32de42ed45d 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from io import BytesIO diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 8b06ce2b163..16f049602bc 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py index b7c9457294d..6dc468754ef 100644 --- a/Tests/test_file_qoi.py +++ b/Tests/test_file_qoi.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, QoiImagePlugin diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 13698276b8c..bc45bbfd34d 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, SgiImagePlugin diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index f2109875478..42d833fb25c 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,4 +1,5 @@ from __future__ import annotations + import tempfile import warnings from io import BytesIO diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 874b37b5285..41f3b7d98f3 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import pytest diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 4470823cdbd..58226c33062 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index d0f228573bb..eafb61d30af 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os from glob import glob from itertools import product diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a50f50e5e96..f0995679b1f 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import warnings from io import BytesIO diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index ee69681854e..06689bc9092 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import struct diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index 0b84d0320aa..7acec975942 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import WalImageFile from .helper import assert_image_equal_tofile diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index c91818ef64e..c49418ce3c5 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import re import sys diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 79d01a4446a..cfda35a0962 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 22acb4be68f..426fe7a0293 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from packaging.version import parse as parse_version diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 6acf58ac3f5..08c80973a74 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index a7b7bbcf6ee..deaf5e380db 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 596dc8ba181..6e1d4c1361f 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, WmfImagePlugin diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index b086ffd683f..69a0a1b38d8 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 265feab4294..529a4558073 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, XpmImagePlugin diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index 5848995c1aa..b87494eba18 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, XVThumbImagePlugin diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 1e5eff2f15d..e5e85618651 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import BdfFontFile, FontFile diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index 388ee711861..e3c72c1aebd 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageDraw, ImageFont diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 6a038bb4038..4e29a856bc9 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image, ImageDraw, ImageFont from .helper import PillowLeakTestCase, skip_unless_feature diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 4365b931066..e6abede0787 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import pytest diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 950e5029ff5..4c2d7185e3c 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import pytest diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py index ce1e02f63e0..eda8fb81283 100644 --- a/Tests/test_fontfile.py +++ b/Tests/test_fontfile.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import FontFile diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index fd47fae39ba..6395ae4aad8 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -1,4 +1,5 @@ from __future__ import annotations + import colorsys import itertools diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index c7610ce8a6c..a55620e09ab 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_image.py b/Tests/test_image.py index 80f6583d8d9..dd989ad99b5 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import logging import os diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 4a794371d67..4ae56fae0cc 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import subprocess import sys diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index b3e5d9e3e08..0dacb3157a2 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from packaging.version import parse as parse_version diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 7c17040d30b..d4ddc2a31b2 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index abf5f846f89..3a26ef96e74 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -1,4 +1,5 @@ from __future__ import annotations + import copy import pytest diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 0bb54e5d811..5e02a3b0dfc 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 774272dd1f7..08c40af1f16 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import fromstring, skip_unless_feature, tostring diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py index 031fceda3fa..fce16122409 100644 --- a/Tests/test_image_entropy.py +++ b/Tests/test_image_entropy.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 5bd7ee0d273..3fa5dd2423a 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageFilter diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 017da499d2c..5d5e9f2dfc9 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index b3ca43bde6d..76b576da56b 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py index e7701dbc460..64339e2cd85 100644 --- a/Tests/test_image_getbands.py +++ b/Tests/test_image_getbands.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index 9e792cfdf7c..b18a7202e38 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index dea3a60a112..17460fa93f5 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index 873cc65bf91..ace64279b8c 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import hopper diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index b17c8a786dd..6bbc4da9a01 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import hopper diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index e969c8164a2..bc8a7485eb0 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index a5be972d3ed..4340f46f619 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import hopper diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py index aa47be3b2e0..e90f5f5056f 100644 --- a/Tests/test_image_getprojection.py +++ b/Tests/test_image_getprojection.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import hopper diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index 7ba2f10b765..3ac6649e074 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 17847c4fd9c..36f8ba575d8 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -1,4 +1,5 @@ from __future__ import annotations + import logging import os diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index ad90d1250dc..3c1d494fab2 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageMode diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 0b87f607286..fd117f9dbc9 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index fce45ec4f0e..2232b94429d 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from .helper import assert_image_equal, hopper diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py index 0ba7e5919a5..c44b048d523 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index d3cb13e2e97..2648af8fa2e 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from array import array diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index de2d9024213..43b65be2b17 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImagePalette diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 54c567aae6c..1475b027bd8 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from packaging.version import parse as parse_version diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index a4d0f510761..ba9100415a2 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageMath, ImageMode diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 5a578dba5c4..af730dce13d 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,4 +1,5 @@ from __future__ import annotations + from contextlib import contextmanager import pytest diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 0d3b43ee29f..aedcf4a09b5 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -2,6 +2,7 @@ Tests for resize functionality. """ from __future__ import annotations + from itertools import permutations import pytest diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 0931aa32d98..e63fef2c152 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 707508250f6..c39a100e777 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, features diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 9e6796ca299..7fa5692aa7d 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 156b9919d27..89a41cf8ec2 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from .helper import assert_image_equal, fromstring, hopper diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index f6042bca527..8f15adac065 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index f5d5ab70408..0fe9fd1d5f5 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,4 +1,5 @@ from __future__ import annotations + import math import pytest diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index 66a2d9e2955..01bf5a83918 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL.Image import Transpose diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 8e3a738d708..2f0614385aa 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image, ImageChops from .helper import assert_image_equal, hopper diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 810394e6f5f..03332699a1d 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,4 +1,5 @@ from __future__ import annotations + import datetime import os import re diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index c0ffd2ebf0b..b602172b642 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageColor diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 379fe78cd8a..69aab48912b 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,4 +1,5 @@ from __future__ import annotations + import contextlib import os.path diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index d729af14d3a..004c2d768fd 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os.path import pytest diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index f4e4d59be32..e3d8a7ab251 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageEnhance diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 4804a554f83..99731f35208 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 807d581edf0..d2c87d42aaa 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,4 +1,5 @@ from __future__ import annotations + import copy import os import re diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index bea532b051e..09e68ea488a 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageDraw, ImageFont diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 9e085510149..be4be1c546e 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -1,9 +1,11 @@ from __future__ import annotations + import struct -import pytest from io import BytesIO -from PIL import Image, ImageDraw, ImageFont, features, _util +import pytest + +from PIL import Image, ImageDraw, ImageFont, _util, features from .helper import assert_image_equal_tofile diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index b7683ec1817..9d3d40398f6 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import shutil import subprocess diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 9281de6f66a..622ad27eacb 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageMath diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 64a1785ea97..0708ee63905 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,5 +1,6 @@ # Test the ImageMorphology functionality from __future__ import annotations + import pytest from PIL import Image, ImageMorph, _imagingmorph diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 7980bead0a4..636b99dbe8f 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageDraw, ImageOps, ImageStat, features diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 84d3a69507a..8ffb9bff7a2 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageFilter diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index e5b59b74a84..be21464b4ae 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImagePalette diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index ac3ea3281f5..5c6393e237f 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -1,4 +1,5 @@ from __future__ import annotations + import array import math import struct diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 41d247f429f..d55d980d9be 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 6d71e4d87af..66d553bcbc7 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageSequence, TiffImagePlugin diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 761d28d3019..0996ad41d4d 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageShow diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 7b56b89cc4b..01687db353e 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageStat diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index bb20fbb6f92..c06fc58235b 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 6927eedcf86..f93eabcb4ff 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import ImageWin diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index bd154335af7..63d6b903ca6 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO from PIL import Image, ImageWin diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 92cad4ac1b1..1c642e4c981 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 1293f7628be..e2024abbf2e 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 49b052fa485..db9557d7ba2 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,4 +1,5 @@ from __future__ import annotations + import locale import pytest diff --git a/Tests/test_main.py b/Tests/test_main.py index a84e61a7b7d..9f61a0c8169 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import subprocess import sys diff --git a/Tests/test_map.py b/Tests/test_map.py index 76444f33d1c..9c79fe35906 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index 3e17d8dccde..d3ee511b7c2 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 6f0e99b3f93..24dff36a610 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index aeeafb6f1df..a89d75b59d3 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -1,4 +1,5 @@ from __future__ import annotations + import time import pytest diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index eb687b57b62..c445e349447 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pickle import pytest diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 77c7952e912..7f618d0f53b 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import sys from io import BytesIO diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 08133b6c30c..c2cea08ca61 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import __version__ diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 49ca016771f..ad2b5ad9bf5 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import ImageQt diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 396bd9080ca..b26787ce668 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import ImageQt diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index 37d72d451de..dee6258ecb5 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index d93b0390416..9f3e86a32a8 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,4 +1,5 @@ from __future__ import annotations + import shutil import pytest diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index e7b41fb4738..c07e7f7d395 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,4 +1,5 @@ from __future__ import annotations + from fractions import Fraction from PIL import Image, TiffImagePlugin, features diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py index 6b693f7cd5d..75326288f97 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import assert_image_equal, assert_image_similar, hopper diff --git a/Tests/test_util.py b/Tests/test_util.py index 4a312beb440..3395ef753d7 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import _util diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 28ebc7d79f1..0f51abc9574 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO from PIL import Image From 420150f0e251c9519a5aea1c24546a44d489828c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Sat, 20 Jan 2024 14:56:20 +0100 Subject: [PATCH 0103/2374] Update winbuild/build.rst --- winbuild/build.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build.rst b/winbuild/build.rst index f40982cd546..c980d9c7567 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -92,7 +92,7 @@ You can also install Pillow in `editable mode`_:: winbuild\build\build_env.cmd python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor -e . -To build a wheel instead, run:: +To build a binary wheel instead, run:: winbuild\build\build_env.cmd python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . From 46741953215a764d18fb18fe4a16cadb82d40f9c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Jan 2024 15:01:12 +1100 Subject: [PATCH 0104/2374] Removed support for test-image-results --- Tests/helper.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 99170c765dd..b2e7d43dd67 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -25,13 +25,6 @@ uploader = "show" elif "GITHUB_ACTIONS" in os.environ: uploader = "github_actions" -else: - try: - import test_image_results - - uploader = "aws" - except ImportError: - pass def upload(a: Image.Image, b: Image.Image) -> str | None: @@ -46,8 +39,6 @@ def upload(a: Image.Image, b: Image.Image) -> str | None: a.save(os.path.join(tmpdir, "a.png")) b.save(os.path.join(tmpdir, "b.png")) return tmpdir - elif uploader == "aws": - return test_image_results.upload(a, b) return None From d331eb9c528920bf6c10d56b5a5a8149b8a92801 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Jan 2024 16:47:38 +1100 Subject: [PATCH 0105/2374] Added type hints --- Tests/conftest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/conftest.py b/Tests/conftest.py index ac618c5b945..e00d1f019f5 100644 --- a/Tests/conftest.py +++ b/Tests/conftest.py @@ -2,8 +2,10 @@ import io +import pytest -def pytest_report_header(config): + +def pytest_report_header(config: pytest.Config) -> str: try: from PIL import features @@ -14,7 +16,7 @@ def pytest_report_header(config): return f"pytest_report_header failed: {e}" -def pytest_configure(config): +def pytest_configure(config: pytest.Config) -> None: config.addinivalue_line( "markers", "pil_noop_mark: A conditional mark where nothing special happens", From 16ea9bd102757faa7cd02ebfc74b5c9e3d1dac1f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 20 Jan 2024 19:08:51 +0200 Subject: [PATCH 0106/2374] Include pyproject.toml in pip cache key --- .github/workflows/docs.yml | 4 +++- .github/workflows/test.yml | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9fe345c8a3d..6853462255f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -37,7 +37,9 @@ jobs: with: python-version: "3.x" cache: pip - cache-dependency-path: ".ci/*.sh" + cache-dependency-path: | + ".ci/*.sh" + "pyproject.toml" - name: Build system information run: python3 .github/workflows/system-info.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 05f78704bcd..2044620aa8a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,6 +26,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: @@ -65,7 +68,9 @@ jobs: python-version: ${{ matrix.python-version }} allow-prereleases: true cache: pip - cache-dependency-path: ".ci/*.sh" + cache-dependency-path: | + ".ci/*.sh" + "pyproject.toml" - name: Build system information run: python3 .github/workflows/system-info.py From 0b6c7ba49e80dbba02705e40274854b8b4b5b09b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 20 Jan 2024 19:09:41 +0200 Subject: [PATCH 0107/2374] Disable wget progress bar but not all output --- depends/download-and-extract.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index a318bfafd9f..04bfbc7556b 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -5,7 +5,7 @@ archive=$1 url=$2 if [ ! -f $archive.tar.gz ]; then - wget -O $archive.tar.gz $url + wget --no-verbose -O $archive.tar.gz $url fi rmdir $archive From 97d24f14a539115c82ecdd0a9801e37dc83636b5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 21 Jan 2024 01:15:59 +0200 Subject: [PATCH 0108/2374] Cache libimagequant --- .github/workflows/docs.yml | 8 ++++++++ .github/workflows/test.yml | 9 +++++++++ depends/install_imagequant.sh | 38 +++++++++++++++++++++++++++-------- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6853462255f..4319cc8ff28 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -44,11 +44,19 @@ jobs: - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libimagequant + uses: actions/cache@v4 + id: cache-libimagequant + with: + path: ~/cache-libimagequant + key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Install Linux dependencies run: | .ci/install.sh env: GHA_PYTHON_VERSION: "3.x" + GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} - name: Build run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2044620aa8a..4e23f5c5b12 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,12 +75,21 @@ jobs: - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libimagequant + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@v4 + id: cache-libimagequant + with: + path: ~/cache-libimagequant + key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Install Linux dependencies if: startsWith(matrix.os, 'ubuntu') run: | .ci/install.sh env: GHA_PYTHON_VERSION: ${{ matrix.python-version }} + GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} - name: Install macOS dependencies if: startsWith(matrix.os, 'macOS') diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index b7cebbdbf60..0fe8cbdba8e 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,15 +1,37 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.2.2 +archive_name=libimagequant +archive_version=4.2.2 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz +archive=$archive_name-$archive_version -pushd $archive/imagequant-sys +if [[ "$GHA_LIBIMAGEQUANT_CACHE_HIT" == "true" ]]; then -cargo install cargo-c -cargo cinstall --prefix=/usr --destdir=. -sudo cp usr/lib/libimagequant.so* /usr/lib/ -sudo cp usr/include/libimagequant.h /usr/include/ + # Copy cached files into place + sudo cp ~/cache-$archive_name/libimagequant.so* /usr/lib/ + sudo cp ~/cache-$archive_name/libimagequant.h /usr/include/ -popd +else + + # Build from source + ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz + + pushd $archive/imagequant-sys + + time cargo install cargo-c + time cargo cinstall --prefix=/usr --destdir=. + + # Copy into place + sudo cp usr/lib/libimagequant.so* /usr/lib/ + sudo cp usr/include/libimagequant.h /usr/include/ + + # Copy to cache + rm -rf ~/cache-$archive_name + mkdir ~/cache-$archive_name + cp usr/lib/libimagequant.so* ~/cache-$archive_name/ + cp usr/include/libimagequant.h ~/cache-$archive_name/ + + popd + +fi From a09e056a4ca9ab5283b14bfd9ccec9aeb0757643 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Jan 2024 18:42:43 +1100 Subject: [PATCH 0109/2374] Added type hints --- Tests/test_font_bdf.py | 4 +-- Tests/test_font_crash.py | 4 +-- Tests/test_font_leaks.py | 6 ++-- Tests/test_font_pcf.py | 21 +++++++----- Tests/test_font_pcf_charsets.py | 59 +++++++++++++++++++-------------- 5 files changed, 53 insertions(+), 41 deletions(-) diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index e5e85618651..136070f9e7d 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -7,7 +7,7 @@ filename = "Tests/images/courB08.bdf" -def test_sanity(): +def test_sanity() -> None: with open(filename, "rb") as test_file: font = BdfFontFile.BdfFontFile(test_file) @@ -15,7 +15,7 @@ def test_sanity(): assert len([_f for _f in font.glyph if _f]) == 190 -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): BdfFontFile.BdfFontFile(fp) diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index e3c72c1aebd..b82340ef70b 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -8,7 +8,7 @@ class TestFontCrash: - def _fuzz_font(self, font): + def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: # from fuzzers.fuzz_font font.getbbox("ABC") font.getmask("test text") @@ -18,7 +18,7 @@ def _fuzz_font(self, font): draw.text((10, 10), "Test Text", font=font, fill="#000") @skip_unless_feature("freetype2") - def test_segfault(self): + def test_segfault(self) -> None: with pytest.raises(OSError): font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") self._fuzz_font(font) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 4e29a856bc9..241f455b813 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -10,7 +10,7 @@ class TestTTypeFontLeak(PillowLeakTestCase): iterations = 10 mem_limit = 4096 # k - def _test_font(self, font): + def _test_font(self, font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (255, 255), "white") draw = ImageDraw.ImageDraw(im) self._test_leak( @@ -20,7 +20,7 @@ def _test_font(self, font): ) @skip_unless_feature("freetype2") - def test_leak(self): + def test_leak(self) -> None: ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) self._test_font(ttype) @@ -30,6 +30,6 @@ class TestDefaultFontLeak(TestTTypeFontLeak): iterations = 100 mem_limit = 1024 # k - def test_leak(self): + def test_leak(self) -> None: default_font = ImageFont.load_default() self._test_font(default_font) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index e6abede0787..0f1eabdce39 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from pathlib import PosixPath import pytest @@ -20,7 +21,7 @@ pytestmark = skip_unless_feature("zlib") -def save_font(request, tmp_path): +def save_font(request: pytest.FixtureRequest, tmp_path: PosixPath) -> str: with open(fontname, "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file) assert isinstance(font, FontFile.FontFile) @@ -29,7 +30,7 @@ def save_font(request, tmp_path): tempname = str(tmp_path / "temp.pil") - def delete_tempfile(): + def delete_tempfile() -> None: try: os.remove(tempname[:-4] + ".pbm") except OSError: @@ -47,11 +48,11 @@ def delete_tempfile(): return tempname -def test_sanity(request, tmp_path): +def test_sanity(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: save_font(request, tmp_path) -def test_less_than_256_characters(): +def test_less_than_256_characters() -> None: with open("Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf", "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file) assert isinstance(font, FontFile.FontFile) @@ -59,13 +60,13 @@ def test_less_than_256_characters(): assert len([_f for _f in font.glyph if _f]) == 127 -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): PcfFontFile.PcfFontFile(fp) -def test_draw(request, tmp_path): +def test_draw(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) im = Image.new("L", (130, 30), "white") @@ -74,7 +75,7 @@ def test_draw(request, tmp_path): assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) -def test_textsize(request, tmp_path): +def test_textsize(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) for i in range(255): @@ -90,7 +91,9 @@ def test_textsize(request, tmp_path): assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) -def _test_high_characters(request, tmp_path, message): +def _test_high_characters( + request: pytest.FixtureRequest, tmp_path: PosixPath, message: str | bytes +) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) im = Image.new("L", (750, 30), "white") @@ -99,7 +102,7 @@ def _test_high_characters(request, tmp_path, message): assert_image_similar_tofile(im, "Tests/images/high_ascii_chars.png", 0) -def test_high_characters(request, tmp_path): +def test_high_characters(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: message = "".join(chr(i + 1) for i in range(140, 232)) _test_high_characters(request, tmp_path, message) # accept bytes instances. diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 4c2d7185e3c..9dfaa404e28 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from pathlib import PosixPath import pytest @@ -14,38 +15,40 @@ fontname = "Tests/fonts/ter-x20b.pcf" -charsets = { - "iso8859-1": { - "glyph_count": 223, - "message": "hello, world", - "image1": "Tests/images/test_draw_pbm_ter_en_target.png", - }, - "iso8859-2": { - "glyph_count": 223, - "message": "witaj świecie", - "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", - }, - "cp1250": { - "glyph_count": 250, - "message": "witaj świecie", - "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", - }, +charsets: dict[str, tuple[int, str, str]] = { + "iso8859-1": ( + 223, + "hello, world", + "Tests/images/test_draw_pbm_ter_en_target.png", + ), + "iso8859-2": ( + 223, + "witaj świecie", + "Tests/images/test_draw_pbm_ter_pl_target.png", + ), + "cp1250": ( + 250, + "witaj świecie", + "Tests/images/test_draw_pbm_ter_pl_target.png", + ), } pytestmark = skip_unless_feature("zlib") -def save_font(request, tmp_path, encoding): +def save_font( + request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str +) -> str: with open(fontname, "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file, encoding) assert isinstance(font, FontFile.FontFile) # check the number of characters in the font - assert len([_f for _f in font.glyph if _f]) == charsets[encoding]["glyph_count"] + assert len([_f for _f in font.glyph if _f]) == charsets[encoding][0] tempname = str(tmp_path / "temp.pil") - def delete_tempfile(): + def delete_tempfile() -> None: try: os.remove(tempname[:-4] + ".pbm") except OSError: @@ -64,23 +67,29 @@ def delete_tempfile(): @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_sanity(request, tmp_path, encoding): +def test_sanity( + request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str +) -> None: save_font(request, tmp_path, encoding) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_draw(request, tmp_path, encoding): +def test_draw( + request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str +) -> None: tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) im = Image.new("L", (150, 30), "white") draw = ImageDraw.Draw(im) - message = charsets[encoding]["message"].encode(encoding) + message = charsets[encoding][1].encode(encoding) draw.text((0, 0), message, "black", font=font) - assert_image_similar_tofile(im, charsets[encoding]["image1"], 0) + assert_image_similar_tofile(im, charsets[encoding][2], 0) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_textsize(request, tmp_path, encoding): +def test_textsize( + request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str +) -> None: tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) for i in range(255): @@ -90,7 +99,7 @@ def test_textsize(request, tmp_path, encoding): assert dy == 20 assert dx in (0, 10) assert font.getlength(bytearray([i])) == dx - message = charsets[encoding]["message"].encode(encoding) + message = charsets[encoding][1].encode(encoding) for i in range(len(message)): msg = message[: i + 1] assert font.getlength(msg) == len(msg) * 10 From d96c196c48a27fc327eb1fcb7296ee47b7f25e0f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 22 Jan 2024 12:42:03 +0200 Subject: [PATCH 0110/2374] Only cache on GHA, remove debugging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- depends/install_imagequant.sh | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 0fe8cbdba8e..3adae91a525 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -19,18 +19,20 @@ else pushd $archive/imagequant-sys - time cargo install cargo-c - time cargo cinstall --prefix=/usr --destdir=. + cargo install cargo-c + cargo cinstall --prefix=/usr --destdir=. # Copy into place sudo cp usr/lib/libimagequant.so* /usr/lib/ sudo cp usr/include/libimagequant.h /usr/include/ - # Copy to cache - rm -rf ~/cache-$archive_name - mkdir ~/cache-$archive_name - cp usr/lib/libimagequant.so* ~/cache-$archive_name/ - cp usr/include/libimagequant.h ~/cache-$archive_name/ + if [ -n "$GITHUB_ACTIONS" ]; then + # Copy to cache + rm -rf ~/cache-$archive_name + mkdir ~/cache-$archive_name + cp usr/lib/libimagequant.so* ~/cache-$archive_name/ + cp usr/include/libimagequant.h ~/cache-$archive_name/ + fi popd From 2521ec4732311ffd85444253d755e3acee89c10f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Jan 2024 22:08:45 +1100 Subject: [PATCH 0111/2374] Restored charsets dictionary --- Tests/test_font_pcf_charsets.py | 53 +++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 9dfaa404e28..cb77128eff7 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -15,22 +15,22 @@ fontname = "Tests/fonts/ter-x20b.pcf" -charsets: dict[str, tuple[int, str, str]] = { - "iso8859-1": ( - 223, - "hello, world", - "Tests/images/test_draw_pbm_ter_en_target.png", - ), - "iso8859-2": ( - 223, - "witaj świecie", - "Tests/images/test_draw_pbm_ter_pl_target.png", - ), - "cp1250": ( - 250, - "witaj świecie", - "Tests/images/test_draw_pbm_ter_pl_target.png", - ), +charsets: dict[str, dict[str, int | str]] = { + "iso8859-1": { + "glyph_count": 223, + "message": "hello, world", + "image1": "Tests/images/test_draw_pbm_ter_en_target.png", + }, + "iso8859-2": { + "glyph_count": 223, + "message": "witaj świecie", + "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", + }, + "cp1250": { + "glyph_count": 250, + "message": "witaj świecie", + "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", + }, } @@ -44,7 +44,7 @@ def save_font( font = PcfFontFile.PcfFontFile(test_file, encoding) assert isinstance(font, FontFile.FontFile) # check the number of characters in the font - assert len([_f for _f in font.glyph if _f]) == charsets[encoding][0] + assert len([_f for _f in font.glyph if _f]) == charsets[encoding]["glyph_count"] tempname = str(tmp_path / "temp.pil") @@ -81,9 +81,14 @@ def test_draw( font = ImageFont.load(tempname) im = Image.new("L", (150, 30), "white") draw = ImageDraw.Draw(im) - message = charsets[encoding][1].encode(encoding) - draw.text((0, 0), message, "black", font=font) - assert_image_similar_tofile(im, charsets[encoding][2], 0) + + message = charsets[encoding]["message"] + assert isinstance(message, str) + draw.text((0, 0), message.encode(encoding), "black", font=font) + + expected_path = charsets[encoding]["image1"] + assert isinstance(expected_path, str) + assert_image_similar_tofile(im, expected_path, 0) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) @@ -99,8 +104,10 @@ def test_textsize( assert dy == 20 assert dx in (0, 10) assert font.getlength(bytearray([i])) == dx - message = charsets[encoding][1].encode(encoding) - for i in range(len(message)): - msg = message[: i + 1] + message = charsets[encoding]["message"] + assert isinstance(message, str) + message_bytes = message.encode(encoding) + for i in range(len(message_bytes)): + msg = message_bytes[: i + 1] assert font.getlength(msg) == len(msg) * 10 assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) From 231d54b9df90dcce1cdbb9928f8d21899eed8153 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 22 Jan 2024 21:37:37 +0200 Subject: [PATCH 0112/2374] Replace io.BytesIO in type hints --- docs/reference/internal_design.rst | 4 ++-- docs/reference/internal_modules.rst | 12 ++++++++++++ src/PIL/GdImageFile.py | 7 +++++-- src/PIL/MpegImagePlugin.py | 5 ++--- src/PIL/_typing.py | 16 +++++++++++++++- src/PIL/_util.py | 4 ++-- 6 files changed, 38 insertions(+), 10 deletions(-) diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index 2e2d3322f75..99a18e9ea99 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -1,5 +1,5 @@ -Internal Reference Docs -======================= +Internal Reference +================== .. toctree:: :maxdepth: 2 diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index f2932c32200..c3cc700607f 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -33,6 +33,18 @@ Internal Modules Provides a convenient way to import type hints that are not available on some Python versions. +.. py:class:: FileDescriptor + + Typing alias. + +.. py:class:: StrOrBytesPath + + Typing alias. + +.. py:class:: SupportsRead + + An object that supports the read method. + .. py:data:: TypeGuard :value: typing.TypeGuard diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 7bb4736af13..315ac6d6c1a 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -27,11 +27,12 @@ class is not registered for use with :py:func:`PIL.Image.open()`. To open a """ from __future__ import annotations -from io import BytesIO +from typing import IO from . import ImageFile, ImagePalette, UnidentifiedImageError from ._binary import i16be as i16 from ._binary import i32be as i32 +from ._typing import FileDescriptor, StrOrBytesPath class GdImageFile(ImageFile.ImageFile): @@ -80,7 +81,9 @@ def _open(self) -> None: ] -def open(fp: BytesIO, mode: str = "r") -> GdImageFile: +def open( + fp: StrOrBytesPath | FileDescriptor | IO[bytes], mode: str = "r" +) -> GdImageFile: """ Load texture from a GD image file. diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index b9e9243e59f..1565612f869 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -14,17 +14,16 @@ # from __future__ import annotations -from io import BytesIO - from . import Image, ImageFile from ._binary import i8 +from ._typing import SupportsRead # # Bitstream parser class BitStream: - def __init__(self, fp: BytesIO) -> None: + def __init__(self, fp: SupportsRead[bytes]) -> None: self.fp = fp self.bits = 0 self.bitbuffer = 0 diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 608b2b41fa8..6eb25c1c171 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -1,6 +1,8 @@ from __future__ import annotations +import os import sys +from typing import Protocol, TypeVar, Union if sys.version_info >= (3, 10): from typing import TypeGuard @@ -15,4 +17,16 @@ def __class_getitem__(cls, item: Any) -> type[bool]: return bool -__all__ = ["TypeGuard"] +_T_co = TypeVar("_T_co", covariant=True) + + +class SupportsRead(Protocol[_T_co]): + def read(self, __length: int = ...) -> _T_co: + ... + + +FileDescriptor = int +StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] + + +__all__ = ["FileDescriptor", "TypeGuard", "StrOrBytesPath", "SupportsRead"] diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 13f369cca1d..4ecdc4bd307 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -4,10 +4,10 @@ from pathlib import Path from typing import Any, NoReturn -from ._typing import TypeGuard +from ._typing import StrOrBytesPath, TypeGuard -def is_path(f: Any) -> TypeGuard[bytes | str | Path]: +def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: return isinstance(f, (bytes, str, Path)) From 474411b52a3619f01bb626ce8e56100094886d1a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jan 2024 08:52:14 +1100 Subject: [PATCH 0113/2374] Updated zlib to 1.3.1 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3ec3148734f..a30d0468ccb 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -29,7 +29,7 @@ else GIFLIB_VERSION=5.2.1 fi if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then - ZLIB_VERSION=1.3 + ZLIB_VERSION=1.3.1 else ZLIB_VERSION=1.2.8 fi diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index df33ea493e6..3117065e8f7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -143,9 +143,9 @@ def cmd_msbuild( "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": "https://zlib.net/zlib13.zip", - "filename": "zlib13.zip", - "dir": "zlib-1.3", + "url": "https://zlib.net/zlib131.zip", + "filename": "zlib131.zip", + "dir": "zlib-1.3.1", "license": "README", "license_pattern": "Copyright notice:\n\n(.+)$", "build": [ From 16fd934b007d7090fc32ff4a1ad13182a32bf612 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jan 2024 09:55:25 +1100 Subject: [PATCH 0114/2374] Use TypedDict --- Tests/test_font_pcf_charsets.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index cb77128eff7..894d4eb56bf 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -2,6 +2,7 @@ import os from pathlib import PosixPath +from typing import TypedDict import pytest @@ -15,7 +16,14 @@ fontname = "Tests/fonts/ter-x20b.pcf" -charsets: dict[str, dict[str, int | str]] = { + +class Charset(TypedDict): + glyph_count: int + message: str + image1: str + + +charsets: dict[str, Charset] = { "iso8859-1": { "glyph_count": 223, "message": "hello, world", @@ -81,14 +89,9 @@ def test_draw( font = ImageFont.load(tempname) im = Image.new("L", (150, 30), "white") draw = ImageDraw.Draw(im) - - message = charsets[encoding]["message"] - assert isinstance(message, str) - draw.text((0, 0), message.encode(encoding), "black", font=font) - - expected_path = charsets[encoding]["image1"] - assert isinstance(expected_path, str) - assert_image_similar_tofile(im, expected_path, 0) + message = charsets[encoding]["message"].encode(encoding) + draw.text((0, 0), message, "black", font=font) + assert_image_similar_tofile(im, charsets[encoding]["image1"], 0) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) @@ -104,10 +107,8 @@ def test_textsize( assert dy == 20 assert dx in (0, 10) assert font.getlength(bytearray([i])) == dx - message = charsets[encoding]["message"] - assert isinstance(message, str) - message_bytes = message.encode(encoding) - for i in range(len(message_bytes)): - msg = message_bytes[: i + 1] + message = charsets[encoding]["message"].encode(encoding) + for i in range(len(message)): + msg = message[: i + 1] assert font.getlength(msg) == len(msg) * 10 assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) From 8caae8739f16cfddade0848ba686b1a40a0c10b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jan 2024 18:51:41 +1100 Subject: [PATCH 0115/2374] Restored testing of non-TrueType default font --- Tests/test_font_leaks.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 4e29a856bc9..d29e9bcfc9a 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,8 +1,10 @@ from __future__ import annotations -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw, ImageFont, _util -from .helper import PillowLeakTestCase, skip_unless_feature +from .helper import PillowLeakTestCase, features, skip_unless_feature + +original_core = ImageFont.core class TestTTypeFontLeak(PillowLeakTestCase): @@ -31,5 +33,9 @@ class TestDefaultFontLeak(TestTTypeFontLeak): mem_limit = 1024 # k def test_leak(self): + if features.check_module("freetype2"): + ImageFont.core = _util.DeferredError(ImportError) default_font = ImageFont.load_default() + ImageFont.core = original_core + self._test_font(default_font) From 4814bee6c0b99b4e00fa08a5d15663f8238f063a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jan 2024 21:42:36 +1100 Subject: [PATCH 0116/2374] Use Path instead of PosixPath --- Tests/check_j2k_overflow.py | 4 ++-- Tests/check_large_memory.py | 8 ++++---- Tests/check_large_memory_numpy.py | 8 ++++---- Tests/test_font_pcf.py | 14 +++++++------- Tests/test_font_pcf_charsets.py | 16 +++++----------- 5 files changed, 22 insertions(+), 28 deletions(-) diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index 8a85783fccb..dbdd5a4f557 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,13 +1,13 @@ from __future__ import annotations -from pathlib import PosixPath +from pathlib import Path import pytest from PIL import Image -def test_j2k_overflow(tmp_path: PosixPath) -> None: +def test_j2k_overflow(tmp_path: Path) -> None: im = Image.new("RGBA", (1024, 131584)) target = str(tmp_path / "temp.jpc") with pytest.raises(OSError): diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 2c8c77800bb..a9ce79e57e6 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from pathlib import PosixPath +from pathlib import Path from types import ModuleType import pytest @@ -31,18 +31,18 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") -def _write_png(tmp_path: PosixPath, xdim: int, ydim: int) -> None: +def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: f = str(tmp_path / "temp.png") im = Image.new("L", (xdim, ydim), 0) im.save(f) -def test_large(tmp_path: PosixPath) -> None: +def test_large(tmp_path: Path) -> None: """succeeded prepatch""" _write_png(tmp_path, XDIM, YDIM) -def test_2gpx(tmp_path: PosixPath) -> None: +def test_2gpx(tmp_path: Path) -> None: """failed prepatch""" _write_png(tmp_path, XDIM, XDIM) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 8609fe6d07b..f4ca8d0aa68 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from pathlib import PosixPath +from pathlib import Path import pytest @@ -25,7 +25,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") -def _write_png(tmp_path: PosixPath, xdim: int, ydim: int) -> None: +def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: dtype = np.uint8 a = np.zeros((xdim, ydim), dtype=dtype) f = str(tmp_path / "temp.png") @@ -33,11 +33,11 @@ def _write_png(tmp_path: PosixPath, xdim: int, ydim: int) -> None: im.save(f) -def test_large(tmp_path: PosixPath) -> None: +def test_large(tmp_path: Path) -> None: """succeeded prepatch""" _write_png(tmp_path, XDIM, YDIM) -def test_2gpx(tmp_path: PosixPath) -> None: +def test_2gpx(tmp_path: Path) -> None: """failed prepatch""" _write_png(tmp_path, XDIM, XDIM) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 0f1eabdce39..997809e463b 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from pathlib import PosixPath +from pathlib import Path import pytest @@ -21,7 +21,7 @@ pytestmark = skip_unless_feature("zlib") -def save_font(request: pytest.FixtureRequest, tmp_path: PosixPath) -> str: +def save_font(request: pytest.FixtureRequest, tmp_path: Path) -> str: with open(fontname, "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file) assert isinstance(font, FontFile.FontFile) @@ -48,7 +48,7 @@ def delete_tempfile() -> None: return tempname -def test_sanity(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: +def test_sanity(request: pytest.FixtureRequest, tmp_path: Path) -> None: save_font(request, tmp_path) @@ -66,7 +66,7 @@ def test_invalid_file() -> None: PcfFontFile.PcfFontFile(fp) -def test_draw(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: +def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) im = Image.new("L", (130, 30), "white") @@ -75,7 +75,7 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) -def test_textsize(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: +def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) for i in range(255): @@ -92,7 +92,7 @@ def test_textsize(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: def _test_high_characters( - request: pytest.FixtureRequest, tmp_path: PosixPath, message: str | bytes + request: pytest.FixtureRequest, tmp_path: Path, message: str | bytes ) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) @@ -102,7 +102,7 @@ def _test_high_characters( assert_image_similar_tofile(im, "Tests/images/high_ascii_chars.png", 0) -def test_high_characters(request: pytest.FixtureRequest, tmp_path: PosixPath) -> None: +def test_high_characters(request: pytest.FixtureRequest, tmp_path: Path) -> None: message = "".join(chr(i + 1) for i in range(140, 232)) _test_high_characters(request, tmp_path, message) # accept bytes instances. diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 894d4eb56bf..895458d9d82 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from pathlib import PosixPath +from pathlib import Path from typing import TypedDict import pytest @@ -45,9 +45,7 @@ class Charset(TypedDict): pytestmark = skip_unless_feature("zlib") -def save_font( - request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str -) -> str: +def save_font(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> str: with open(fontname, "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file, encoding) assert isinstance(font, FontFile.FontFile) @@ -75,16 +73,12 @@ def delete_tempfile() -> None: @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_sanity( - request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str -) -> None: +def test_sanity(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None: save_font(request, tmp_path, encoding) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_draw( - request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str -) -> None: +def test_draw(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None: tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) im = Image.new("L", (150, 30), "white") @@ -96,7 +90,7 @@ def test_draw( @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) def test_textsize( - request: pytest.FixtureRequest, tmp_path: PosixPath, encoding: str + request: pytest.FixtureRequest, tmp_path: Path, encoding: str ) -> None: tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) From e2d1b2663d2b392089bfe59c7a17d2afbf74ce7f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 24 Jan 2024 08:12:06 +1100 Subject: [PATCH 0117/2374] Restored original state using finally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- Tests/test_font_leaks.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index d29e9bcfc9a..5eea0c34d5f 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -35,7 +35,9 @@ class TestDefaultFontLeak(TestTTypeFontLeak): def test_leak(self): if features.check_module("freetype2"): ImageFont.core = _util.DeferredError(ImportError) - default_font = ImageFont.load_default() - ImageFont.core = original_core + try: + default_font = ImageFont.load_default() + finally: + ImageFont.core = original_core self._test_font(default_font) From e3932b7dbaf6aff8ef4b7a24007f4de07477ec91 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 23 Jan 2024 23:58:41 +0200 Subject: [PATCH 0118/2374] Exclude from coverage: empty bodies in protocols or abstract methods --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 46df3f90d27..5678e45661a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,9 @@ exclude_also = if DEBUG: # Don't complain about compatibility code for missing optional dependencies except ImportError + # Empty bodies in protocols or abstract methods + ^\s*def [a-zA-Z0-9_]+\(.*\)(\s*->.*)?:\s*\.\.\.(\s*#.*)?$ + ^\s*\.\.\.(\s*#.*)?$ [run] omit = From b3a7ae065c4f34b345ecaa3b019bbd1d24e7922c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Jan 2024 06:40:03 +1100 Subject: [PATCH 0119/2374] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 62ae2a68bbc..7d80eec0345 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Do not support using test-image-results to upload images after test failures #7739 + [radarhere] + +- Changed ImageMath.ops to be static #7721 + [radarhere] + - Fix APNG info after seeking backwards more than twice #7701 [esoma, radarhere] From cf9e6ff2563be6d0458856e2aa35e54973709752 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Jan 2024 08:10:07 +1100 Subject: [PATCH 0120/2374] Updated libjpeg-turbo to 3.0.2 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3ec3148734f..9013e8ae2c8 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -18,7 +18,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.13.2 HARFBUZZ_VERSION=8.3.0 LIBPNG_VERSION=1.6.40 -JPEGTURBO_VERSION=3.0.1 +JPEGTURBO_VERSION=3.0.2 OPENJPEG_VERSION=2.5.0 XZ_VERSION=5.4.5 TIFF_VERSION=4.6.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index df33ea493e6..92cbcdf7a99 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,9 +113,9 @@ def cmd_msbuild( DEPS = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/3.0.1/libjpeg-turbo-3.0.1.tar.gz/download", - "filename": "libjpeg-turbo-3.0.1.tar.gz", - "dir": "libjpeg-turbo-3.0.1", + + "/libjpeg-turbo/files/3.0.2/libjpeg-turbo-3.0.2.tar.gz/download", + "filename": "libjpeg-turbo-3.0.2.tar.gz", + "dir": "libjpeg-turbo-3.0.2", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" From 5721577e4e6f5d64a5c50ab8732eea9e5a437120 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Jan 2024 20:20:53 +1100 Subject: [PATCH 0121/2374] Stop reading EPS at EOF marker --- Tests/test_file_eps.py | 8 ++++++++ src/PIL/EpsImagePlugin.py | 8 ++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 8def9a43511..5ba3a0c1449 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -436,3 +436,11 @@ def test_eof_before_bounding_box(): with pytest.raises(OSError): with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"): pass + + +def test_invalid_data_after_eof() -> None: + with open("Tests/images/illuCS6_preview.eps", "rb") as f: + img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255)) + + with Image.open(img_bytes) as img: + assert img.mode == "RGB" diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index d2e60aa0759..94b163bc4d6 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -356,14 +356,10 @@ def _read_comment(s): self._size = columns, rows return + elif bytes_mv[:5] == b"%%EOF": + break elif trailer_reached and reading_trailer_comments: # Load EPS trailer - - # if this line starts with "%%EOF", - # then we've reached the end of the file - if bytes_mv[:5] == b"%%EOF": - break - s = str(bytes_mv[:bytes_read], "latin-1") _read_comment(s) elif bytes_mv[:9] == b"%%Trailer": From ddb7df0ec6b5852e509dbf00675a3866ca00bd66 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Jan 2024 22:18:46 +1100 Subject: [PATCH 0122/2374] Added type hints --- Tests/test_image_convert.py | 50 +++++++++++----------- Tests/test_image_copy.py | 6 +-- Tests/test_image_crop.py | 14 +++---- Tests/test_image_frombytes.py | 2 +- Tests/test_image_fromqimage.py | 15 +++---- Tests/test_image_getbands.py | 2 +- Tests/test_image_getbbox.py | 12 +++--- Tests/test_image_getcolors.py | 6 +-- Tests/test_image_getdata.py | 6 +-- Tests/test_image_getim.py | 2 +- Tests/test_image_getprojection.py | 2 +- Tests/test_image_histogram.py | 4 +- Tests/test_image_point.py | 8 ++-- Tests/test_image_putalpha.py | 6 +-- Tests/test_image_quantize.py | 24 +++++------ Tests/test_image_resize.py | 70 +++++++++++++++++++++---------- Tests/test_image_split.py | 12 +++--- Tests/test_image_tobitmap.py | 2 +- Tests/test_image_tobytes.py | 2 +- Tests/test_image_transpose.py | 19 +++++---- 20 files changed, 149 insertions(+), 115 deletions(-) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index d4ddc2a31b2..f154de123bb 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -7,8 +9,8 @@ from .helper import assert_image, assert_image_equal, assert_image_similar, hopper -def test_sanity(): - def convert(im, mode): +def test_sanity() -> None: + def convert(im: Image.Image, mode: str) -> None: out = im.convert(mode) assert out.mode == mode assert out.size == im.size @@ -40,13 +42,13 @@ def convert(im, mode): convert(im, output_mode) -def test_unsupported_conversion(): +def test_unsupported_conversion() -> None: im = hopper() with pytest.raises(ValueError): im.convert("INVALID") -def test_default(): +def test_default() -> None: im = hopper("P") assert im.mode == "P" converted_im = im.convert() @@ -62,18 +64,18 @@ def test_default(): # ref https://github.com/python-pillow/Pillow/issues/274 -def _test_float_conversion(im): +def _test_float_conversion(im: Image.Image) -> None: orig = im.getpixel((5, 5)) converted = im.convert("F").getpixel((5, 5)) assert orig == converted -def test_8bit(): +def test_8bit() -> None: with Image.open("Tests/images/hopper.jpg") as im: _test_float_conversion(im.convert("L")) -def test_16bit(): +def test_16bit() -> None: with Image.open("Tests/images/16bit.cropped.tif") as im: _test_float_conversion(im) @@ -83,19 +85,19 @@ def test_16bit(): assert im_i16.getpixel((0, 0)) == 65535 -def test_16bit_workaround(): +def test_16bit_workaround() -> None: with Image.open("Tests/images/16bit.cropped.tif") as im: _test_float_conversion(im.convert("I")) -def test_opaque(): +def test_opaque() -> None: alpha = hopper("P").convert("PA").getchannel("A") solid = Image.new("L", (128, 128), 255) assert_image_equal(alpha, solid) -def test_rgba_p(): +def test_rgba_p() -> None: im = hopper("RGBA") im.putalpha(hopper("L")) @@ -105,14 +107,14 @@ def test_rgba_p(): assert_image_similar(im, comparable, 20) -def test_rgba(): +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_trns_p(tmp_path): +def test_trns_p(tmp_path: Path) -> None: im = hopper("P") im.info["transparency"] = 0 @@ -131,7 +133,7 @@ def test_trns_p(tmp_path): @pytest.mark.parametrize("mode", ("LA", "PA", "RGBA")) -def test_trns_p_transparency(mode): +def test_trns_p_transparency(mode: str) -> None: # Arrange im = hopper("P") im.info["transparency"] = 128 @@ -148,7 +150,7 @@ def test_trns_p_transparency(mode): assert converted_im.palette is None -def test_trns_l(tmp_path): +def test_trns_l(tmp_path: Path) -> None: im = hopper("L") im.info["transparency"] = 128 @@ -171,7 +173,7 @@ def test_trns_l(tmp_path): im_p.save(f) -def test_trns_RGB(tmp_path): +def test_trns_RGB(tmp_path: Path) -> None: im = hopper("RGB") im.info["transparency"] = im.getpixel((0, 0)) @@ -201,7 +203,7 @@ def test_trns_RGB(tmp_path): @pytest.mark.parametrize("convert_mode", ("L", "LA", "I")) -def test_l_macro_rounding(convert_mode): +def test_l_macro_rounding(convert_mode: str) -> None: for mode in ("P", "PA"): im = Image.new(mode, (1, 1)) im.palette.getcolor((0, 1, 2)) @@ -214,7 +216,7 @@ def test_l_macro_rounding(convert_mode): assert converted_color == 1 -def test_gif_with_rgba_palette_to_p(): +def test_gif_with_rgba_palette_to_p() -> None: # See https://github.com/python-pillow/Pillow/issues/2433 with Image.open("Tests/images/hopper.gif") as im: im.info["transparency"] = 255 @@ -226,7 +228,7 @@ def test_gif_with_rgba_palette_to_p(): im_p.load() -def test_p_la(): +def test_p_la() -> None: im = hopper("RGBA") alpha = hopper("L") im.putalpha(alpha) @@ -236,7 +238,7 @@ def test_p_la(): assert_image_similar(alpha, comparable, 5) -def test_p2pa_alpha(): +def test_p2pa_alpha() -> None: with Image.open("Tests/images/tiny.png") as im: assert im.mode == "P" @@ -250,13 +252,13 @@ def test_p2pa_alpha(): assert im_a.getpixel((x, y)) == alpha -def test_p2pa_palette(): +def test_p2pa_palette() -> None: with Image.open("Tests/images/tiny.png") as im: im_pa = im.convert("PA") assert im_pa.getpalette() == im.getpalette() -def test_matrix_illegal_conversion(): +def test_matrix_illegal_conversion() -> None: # Arrange im = hopper("CMYK") # fmt: off @@ -272,7 +274,7 @@ def test_matrix_illegal_conversion(): im.convert(mode="CMYK", matrix=matrix) -def test_matrix_wrong_mode(): +def test_matrix_wrong_mode() -> None: # Arrange im = hopper("L") # fmt: off @@ -289,7 +291,7 @@ def test_matrix_wrong_mode(): @pytest.mark.parametrize("mode", ("RGB", "L")) -def test_matrix_xyz(mode): +def test_matrix_xyz(mode: str) -> None: # Arrange im = hopper("RGB") im.info["transparency"] = (255, 0, 0) @@ -317,7 +319,7 @@ def test_matrix_xyz(mode): assert converted_im.info["transparency"] == 105 -def test_matrix_identity(): +def test_matrix_identity() -> None: # Arrange im = hopper("RGB") # fmt: off diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 3a26ef96e74..027e5338b7a 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -10,7 +10,7 @@ @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_copy(mode): +def test_copy(mode: str) -> None: cropped_coordinates = (10, 10, 20, 20) cropped_size = (10, 10) @@ -39,7 +39,7 @@ def test_copy(mode): assert out.size == cropped_size -def test_copy_zero(): +def test_copy_zero() -> None: im = Image.new("RGB", (0, 0)) out = im.copy() assert out.mode == im.mode @@ -47,7 +47,7 @@ def test_copy_zero(): @skip_unless_feature("libtiff") -def test_deepcopy(): +def test_deepcopy() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: out = copy.deepcopy(im) assert out.size == (590, 88) diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 5e02a3b0dfc..d095364ba75 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -8,7 +8,7 @@ @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_crop(mode): +def test_crop(mode: str) -> None: im = hopper(mode) assert_image_equal(im.crop(), im) @@ -17,8 +17,8 @@ def test_crop(mode): assert cropped.size == (50, 50) -def test_wide_crop(): - def crop(*bbox): +def test_wide_crop() -> None: + def crop(*bbox: int) -> tuple[int, ...]: i = im.crop(bbox) h = i.histogram() while h and not h[-1]: @@ -47,14 +47,14 @@ def crop(*bbox): @pytest.mark.parametrize("box", ((8, 2, 2, 8), (2, 8, 8, 2), (8, 8, 2, 2))) -def test_negative_crop(box): +def test_negative_crop(box: tuple[int, int, int, int]) -> None: im = Image.new("RGB", (10, 10)) with pytest.raises(ValueError): im.crop(box) -def test_crop_float(): +def test_crop_float() -> None: # Check cropping floats are rounded to nearest integer # https://github.com/python-pillow/Pillow/issues/1744 @@ -69,7 +69,7 @@ def test_crop_float(): assert cropped.size == (3, 5) -def test_crop_crash(): +def test_crop_crash() -> None: # Image.crop crashes prepatch with an access violation # apparently a use after free on Windows, see # https://github.com/python-pillow/Pillow/issues/1077 @@ -87,7 +87,7 @@ def test_crop_crash(): img.load() -def test_crop_zero(): +def test_crop_zero() -> None: im = Image.new("RGB", (0, 0), "white") cropped = im.crop((0, 0, 0, 0)) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 5d5e9f2dfc9..6474daba108 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -8,7 +8,7 @@ @pytest.mark.parametrize("data_type", ("bytes", "memoryview")) -def test_sanity(data_type): +def test_sanity(data_type) -> None: im1 = hopper() data = im1.tobytes() diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index 76b576da56b..ea31a9de913 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -1,6 +1,7 @@ from __future__ import annotations import warnings +from typing import Generator import pytest @@ -18,7 +19,7 @@ @pytest.fixture -def test_images(): +def test_images() -> Generator[Image.Image, None, None]: ims = [ hopper(), Image.open("Tests/images/transparent.png"), @@ -31,7 +32,7 @@ def test_images(): im.close() -def roundtrip(expected): +def roundtrip(expected: Image.Image) -> None: # PIL -> Qt intermediate = expected.toqimage() # Qt -> PIL @@ -43,26 +44,26 @@ def roundtrip(expected): assert_image_equal(result, expected.convert("RGB")) -def test_sanity_1(test_images): +def test_sanity_1(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("1")) -def test_sanity_rgb(test_images): +def test_sanity_rgb(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("RGB")) -def test_sanity_rgba(test_images): +def test_sanity_rgba(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("RGBA")) -def test_sanity_l(test_images): +def test_sanity_l(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("L")) -def test_sanity_p(test_images): +def test_sanity_p(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("P")) diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py index 64339e2cd85..887553fc042 100644 --- a/Tests/test_image_getbands.py +++ b/Tests/test_image_getbands.py @@ -3,7 +3,7 @@ from PIL import Image -def test_getbands(): +def test_getbands() -> None: assert Image.new("1", (1, 1)).getbands() == ("1",) assert Image.new("L", (1, 1)).getbands() == ("L",) assert Image.new("I", (1, 1)).getbands() == ("I",) diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index b18a7202e38..18c6f657925 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -7,13 +7,13 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: bbox = hopper().getbbox() assert isinstance(bbox, tuple) -def test_bbox(): - def check(im, fill_color): +def test_bbox() -> None: + def check(im: Image.Image, fill_color: int | tuple[int, ...]) -> None: assert im.getbbox() is None im.paste(fill_color, (10, 25, 90, 75)) @@ -34,8 +34,8 @@ def check(im, fill_color): check(im, 255) for mode in ("RGBA", "RGBa"): - for color in ((0, 0, 0, 0), (127, 127, 127, 0), (255, 255, 255, 0)): - im = Image.new(mode, (100, 100), color) + for rgba_color in ((0, 0, 0, 0), (127, 127, 127, 0), (255, 255, 255, 0)): + im = Image.new(mode, (100, 100), rgba_color) check(im, (255, 255, 255, 255)) for mode in ("La", "LA", "PA"): @@ -45,7 +45,7 @@ def check(im, fill_color): @pytest.mark.parametrize("mode", ("RGBA", "RGBa", "La", "LA", "PA")) -def test_bbox_alpha_only_false(mode): +def test_bbox_alpha_only_false(mode: str) -> None: im = Image.new(mode, (100, 100)) assert im.getbbox(alpha_only=False) is None diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index 17460fa93f5..8f8870f4fec 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -3,8 +3,8 @@ from .helper import hopper -def test_getcolors(): - def getcolors(mode, limit=None): +def test_getcolors() -> None: + def getcolors(mode: str, limit: int | None = None) -> int | None: im = hopper(mode) if limit: colors = im.getcolors(limit) @@ -39,7 +39,7 @@ def getcolors(mode, limit=None): # -------------------------------------------------------------------- -def test_pack(): +def test_pack() -> None: # Pack problems for small tables (@PIL209) im = hopper().quantize(3).convert("RGB") diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index ace64279b8c..ac27400be94 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -5,7 +5,7 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: data = hopper().getdata() len(data) @@ -14,8 +14,8 @@ def test_sanity(): assert data[0] == (20, 20, 70) -def test_roundtrip(): - def getdata(mode): +def test_roundtrip() -> None: + def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]: im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) data = im.getdata() return data[0], len(data), len(list(data)) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index bc8a7485eb0..9afa02b0a8b 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -3,7 +3,7 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() type_repr = repr(type(im.getim())) diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py index e90f5f5056f..2b5a758ed29 100644 --- a/Tests/test_image_getprojection.py +++ b/Tests/test_image_getprojection.py @@ -5,7 +5,7 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() projection = im.getprojection() diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index 3ac6649e074..dbd55d4c2d3 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -3,8 +3,8 @@ from .helper import hopper -def test_histogram(): - def histogram(mode): +def test_histogram() -> None: + def histogram(mode: str) -> tuple[int, int, int]: h = hopper(mode).histogram() return len(h), min(h), max(h) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 2232b94429d..05f209351d2 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -5,7 +5,7 @@ from .helper import assert_image_equal, hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() with pytest.raises(ValueError): @@ -39,7 +39,7 @@ def test_sanity(): im.point(lambda x: x // 2) -def test_16bit_lut(): +def test_16bit_lut() -> None: """Tests for 16 bit -> 8 bit lut for converting I->L images see https://github.com/python-pillow/Pillow/issues/440 """ @@ -47,7 +47,7 @@ def test_16bit_lut(): im.point(list(range(256)) * 256, "L") -def test_f_lut(): +def test_f_lut() -> None: """Tests for floating point lut of 8bit gray image""" im = hopper("L") lut = [0.5 * float(x) for x in range(256)] @@ -58,7 +58,7 @@ def test_f_lut(): assert_image_equal(out.convert("L"), im.point(int_lut, "L")) -def test_f_mode(): +def test_f_mode() -> None: im = hopper("F") with pytest.raises(ValueError): im.point(None) diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py index c44b048d523..2c92911d1fb 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -3,7 +3,7 @@ from PIL import Image -def test_interface(): +def test_interface() -> None: im = Image.new("RGBA", (1, 1), (1, 2, 3, 0)) assert im.getpixel((0, 0)) == (1, 2, 3, 0) @@ -17,7 +17,7 @@ def test_interface(): assert im.getpixel((0, 0)) == (1, 2, 3, 5) -def test_promote(): +def test_promote() -> None: im = Image.new("L", (1, 1), 1) assert im.getpixel((0, 0)) == 1 @@ -40,7 +40,7 @@ def test_promote(): assert im.getpixel((0, 0)) == (1, 2, 3, 4) -def test_readonly(): +def test_readonly() -> None: im = Image.new("RGB", (1, 1), (1, 2, 3)) im.readonly = 1 diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 1475b027bd8..873a9bb5dcb 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -8,7 +8,7 @@ from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature -def test_sanity(): +def test_sanity() -> None: image = hopper() converted = image.quantize() assert converted.mode == "P" @@ -21,7 +21,7 @@ def test_sanity(): @skip_unless_feature("libimagequant") -def test_libimagequant_quantize(): +def test_libimagequant_quantize() -> None: image = hopper() if is_ppc64le(): libimagequant = parse_version(features.version_feature("libimagequant")) @@ -33,7 +33,7 @@ def test_libimagequant_quantize(): assert len(converted.getcolors()) == 100 -def test_octree_quantize(): +def test_octree_quantize() -> None: image = hopper() converted = image.quantize(100, Image.Quantize.FASTOCTREE) assert converted.mode == "P" @@ -41,7 +41,7 @@ def test_octree_quantize(): assert len(converted.getcolors()) == 100 -def test_rgba_quantize(): +def test_rgba_quantize() -> None: image = hopper("RGBA") with pytest.raises(ValueError): image.quantize(method=0) @@ -49,7 +49,7 @@ def test_rgba_quantize(): assert image.quantize().convert().mode == "RGBA" -def test_quantize(): +def test_quantize() -> None: with Image.open("Tests/images/caption_6_33_22.png") as image: image = image.convert("RGB") converted = image.quantize() @@ -57,7 +57,7 @@ def test_quantize(): assert_image_similar(converted.convert("RGB"), image, 1) -def test_quantize_no_dither(): +def test_quantize_no_dither() -> None: image = hopper() with Image.open("Tests/images/caption_6_33_22.png") as palette: palette = palette.convert("P") @@ -67,7 +67,7 @@ def test_quantize_no_dither(): assert converted.palette.palette == palette.palette.palette -def test_quantize_no_dither2(): +def test_quantize_no_dither2() -> None: im = Image.new("RGB", (9, 1)) im.putdata([(p,) * 3 for p in range(0, 36, 4)]) @@ -83,7 +83,7 @@ def test_quantize_no_dither2(): assert px[x, 0] == (0 if x < 5 else 1) -def test_quantize_dither_diff(): +def test_quantize_dither_diff() -> None: image = hopper() with Image.open("Tests/images/caption_6_33_22.png") as palette: palette = palette.convert("P") @@ -94,14 +94,14 @@ def test_quantize_dither_diff(): assert dither.tobytes() != nodither.tobytes() -def test_colors(): +def test_colors() -> None: im = hopper() colors = 2 converted = im.quantize(colors) assert len(converted.palette.palette) == colors * len("RGB") -def test_transparent_colors_equal(): +def test_transparent_colors_equal() -> None: im = Image.new("RGBA", (1, 2), (0, 0, 0, 0)) px = im.load() px[0, 1] = (255, 255, 255, 0) @@ -120,7 +120,7 @@ def test_transparent_colors_equal(): (Image.Quantize.FASTOCTREE, (0, 0, 0, 0)), ), ) -def test_palette(method, color): +def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None: im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color) converted = im.quantize(method=method) @@ -128,7 +128,7 @@ def test_palette(method, color): assert converted_px[0, 0] == converted.palette.colors[color] -def test_small_palette(): +def test_small_palette() -> None: # Arrange im = hopper() diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index aedcf4a09b5..bd45ee893ad 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -4,6 +4,8 @@ from __future__ import annotations from itertools import permutations +from pathlib import Path +from typing import Generator import pytest @@ -19,7 +21,9 @@ class TestImagingCoreResize: - def resize(self, im, size, f): + def resize( + self, im: Image.Image, size: tuple[int, int], f: Image.Resampling + ) -> Image.Image: # Image class independent version of resize. im.load() return im._new(im.im.resize(size, f)) @@ -27,14 +31,14 @@ def resize(self, im, size, f): @pytest.mark.parametrize( "mode", ("1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", "I;16") ) - def test_nearest_mode(self, mode): + def test_nearest_mode(self, mode: str) -> None: im = hopper(mode) r = self.resize(im, (15, 12), Image.Resampling.NEAREST) assert r.mode == mode assert r.size == (15, 12) assert r.im.bands == im.im.bands - def test_convolution_modes(self): + def test_convolution_modes(self) -> None: with pytest.raises(ValueError): self.resize(hopper("1"), (15, 12), Image.Resampling.BILINEAR) with pytest.raises(ValueError): @@ -59,7 +63,7 @@ def test_convolution_modes(self): Image.Resampling.LANCZOS, ), ) - def test_reduce_filters(self, resample): + def test_reduce_filters(self, resample: Image.Resampling) -> None: r = self.resize(hopper("RGB"), (15, 12), resample) assert r.mode == "RGB" assert r.size == (15, 12) @@ -75,7 +79,7 @@ def test_reduce_filters(self, resample): Image.Resampling.LANCZOS, ), ) - def test_enlarge_filters(self, resample): + def test_enlarge_filters(self, resample: Image.Resampling) -> None: r = self.resize(hopper("RGB"), (212, 195), resample) assert r.mode == "RGB" assert r.size == (212, 195) @@ -99,7 +103,9 @@ def test_enlarge_filters(self, resample): ("LA", ("filled", "dirty")), ), ) - def test_endianness(self, resample, mode, channels_set): + def test_endianness( + self, resample: Image.Resampling, mode: str, channels_set: tuple[str, ...] + ) -> None: # Make an image with one colored pixel, in one channel. # When resized, that channel should be the same as a GS image. # Other channels should be unaffected. @@ -139,17 +145,17 @@ def test_endianness(self, resample, mode, channels_set): Image.Resampling.LANCZOS, ), ) - def test_enlarge_zero(self, resample): + def test_enlarge_zero(self, resample: Image.Resampling) -> None: r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample) assert r.mode == "RGB" assert r.size == (212, 195) assert r.getdata()[0] == (0, 0, 0) - def test_unknown_filter(self): + def test_unknown_filter(self) -> None: with pytest.raises(ValueError): self.resize(hopper(), (10, 10), 9) - def test_cross_platform(self, tmp_path): + def test_cross_platform(self, tmp_path: Path) -> None: # This test is intended for only check for consistent behaviour across # platforms. So if a future Pillow change requires that the test file # be updated, that is okay. @@ -162,7 +168,7 @@ def test_cross_platform(self, tmp_path): @pytest.fixture -def gradients_image(): +def gradients_image() -> Generator[Image.Image, None, None]: with Image.open("Tests/images/radial_gradients.png") as im: im.load() try: @@ -172,7 +178,7 @@ def gradients_image(): class TestReducingGapResize: - def test_reducing_gap_values(self, gradients_image): + def test_reducing_gap_values(self, gradients_image: Image.Image) -> None: ref = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, reducing_gap=None ) @@ -191,7 +197,12 @@ def test_reducing_gap_values(self, gradients_image): "box, epsilon", ((None, 4), ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10)), ) - def test_reducing_gap_1(self, gradients_image, box, epsilon): + def test_reducing_gap_1( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 @@ -206,7 +217,12 @@ def test_reducing_gap_1(self, gradients_image, box, epsilon): "box, epsilon", ((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)), ) - def test_reducing_gap_2(self, gradients_image, box, epsilon): + def test_reducing_gap_2( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 @@ -221,7 +237,12 @@ def test_reducing_gap_2(self, gradients_image, box, epsilon): "box, epsilon", ((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)), ) - def test_reducing_gap_3(self, gradients_image, box, epsilon): + def test_reducing_gap_3( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 @@ -233,7 +254,9 @@ def test_reducing_gap_3(self, gradients_image, box, epsilon): assert_image_similar(ref, im, epsilon) @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256))) - def test_reducing_gap_8(self, gradients_image, box): + def test_reducing_gap_8( + self, gradients_image: Image.Image, box: tuple[float, float, float, float] + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 @@ -245,7 +268,12 @@ def test_reducing_gap_8(self, gradients_image, box): "box, epsilon", (((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5)), ) - def test_box_filter(self, gradients_image, box, epsilon): + def test_box_filter( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 @@ -255,8 +283,8 @@ def test_box_filter(self, gradients_image, box, epsilon): class TestImageResize: - def test_resize(self): - def resize(mode, size): + def test_resize(self) -> None: + def resize(mode: str, size: tuple[int, int]) -> None: out = hopper(mode).resize(size) assert out.mode == mode assert out.size == size @@ -271,7 +299,7 @@ def resize(mode, size): im.resize((10, 10), "unknown") @skip_unless_feature("libtiff") - def test_load_first(self): + def test_load_first(self) -> None: # load() may change the size of the image # Test that resize() is calling it before getting the size with Image.open("Tests/images/g4_orientation_5.tif") as im: @@ -279,13 +307,13 @@ def test_load_first(self): assert im.size == (64, 64) @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F")) - def test_default_filter_bicubic(self, mode): + def test_default_filter_bicubic(self, mode: str) -> None: im = hopper(mode) assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) @pytest.mark.parametrize( "mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16") ) - def test_default_filter_nearest(self, mode): + def test_default_filter_nearest(self, mode: str) -> None: im = hopper(mode) assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index c39a100e777..3385f81f527 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, features @@ -7,8 +9,8 @@ from .helper import assert_image_equal, hopper -def test_split(): - def split(mode): +def test_split() -> None: + def split(mode: str) -> list[tuple[str, int, int]]: layers = hopper(mode).split() return [(i.mode, i.size[0], i.size[1]) for i in layers] @@ -36,18 +38,18 @@ def split(mode): @pytest.mark.parametrize( "mode", ("1", "L", "I", "F", "P", "RGB", "RGBA", "CMYK", "YCbCr") ) -def test_split_merge(mode): +def test_split_merge(mode: str) -> None: expected = Image.merge(mode, hopper(mode).split()) assert_image_equal(hopper(mode), expected) -def test_split_open(tmp_path): +def test_split_open(tmp_path: Path) -> None: if features.check("zlib"): test_file = str(tmp_path / "temp.png") else: test_file = str(tmp_path / "temp.pcx") - def split_open(mode): + def split_open(mode: str) -> int: hopper(mode).save(test_file) with Image.open(test_file) as im: return len(im.split()) diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 89a41cf8ec2..f7a3cc41d90 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -5,7 +5,7 @@ from .helper import assert_image_equal, fromstring, hopper -def test_sanity(): +def test_sanity() -> None: with pytest.raises(ValueError): hopper().tobitmap() diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index 8f15adac065..d32b6c09ba0 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -3,6 +3,6 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: data = hopper().tobytes() assert isinstance(data, bytes) diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index 01bf5a83918..d384d81414a 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -2,6 +2,7 @@ import pytest +from PIL import Image from PIL.Image import Transpose from . import helper @@ -14,7 +15,7 @@ @pytest.mark.parametrize("mode", HOPPER) -def test_flip_left_right(mode): +def test_flip_left_right(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.FLIP_LEFT_RIGHT) assert out.mode == mode @@ -28,7 +29,7 @@ def test_flip_left_right(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_flip_top_bottom(mode): +def test_flip_top_bottom(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.FLIP_TOP_BOTTOM) assert out.mode == mode @@ -42,7 +43,7 @@ def test_flip_top_bottom(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_rotate_90(mode): +def test_rotate_90(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.ROTATE_90) assert out.mode == mode @@ -56,7 +57,7 @@ def test_rotate_90(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_rotate_180(mode): +def test_rotate_180(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.ROTATE_180) assert out.mode == mode @@ -70,7 +71,7 @@ def test_rotate_180(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_rotate_270(mode): +def test_rotate_270(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.ROTATE_270) assert out.mode == mode @@ -84,7 +85,7 @@ def test_rotate_270(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_transpose(mode): +def test_transpose(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.TRANSPOSE) assert out.mode == mode @@ -98,7 +99,7 @@ def test_transpose(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_tranverse(mode): +def test_tranverse(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.TRANSVERSE) assert out.mode == mode @@ -112,10 +113,10 @@ def test_tranverse(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_roundtrip(mode): +def test_roundtrip(mode: str) -> None: im = HOPPER[mode] - def transpose(first, second): + def transpose(first: Transpose, second: Transpose) -> Image.Image: return im.transpose(first).transpose(second) assert_image_equal( From 945253672a74415807b5f685f54ebb1533ff468e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:11:18 +0200 Subject: [PATCH 0123/2374] Handle os.PathLike in is_path --- src/PIL/ImageFont.py | 2 +- src/PIL/_util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a63b73b33f5..9eecad1ca3a 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -230,7 +230,7 @@ def load_from_bytes(f): ) if is_path(font): - if isinstance(font, Path): + if isinstance(font, os.PathLike): font = str(font) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 4ecdc4bd307..b649500abb5 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -8,7 +8,7 @@ def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: - return isinstance(f, (bytes, str, Path)) + return isinstance(f, (bytes, str, os.PathLike)) def is_directory(f: Any) -> TypeGuard[bytes | str | Path]: From f613a9213f4edc7b58ac84a4793223c8e4fd9191 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:15:19 +0200 Subject: [PATCH 0124/2374] Parameterise test --- Tests/test_util.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/Tests/test_util.py b/Tests/test_util.py index 3395ef753d7..71a862569b7 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,27 +1,14 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import _util -def test_is_path(): - # Arrange - fp = "filename.ext" - - # Act - it_is = _util.is_path(fp) - - # Assert - assert it_is - - -def test_path_obj_is_path(): - # Arrange - from pathlib import Path - - test_path = Path("filename.ext") - +@pytest.mark.parametrize("test_path", ["filename.ext", Path("filename.ext")]) +def test_is_path(test_path): # Act it_is = _util.is_path(test_path) From 16d4068b42f0a6069e14b2327302df713dabbfed Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:17:13 +0200 Subject: [PATCH 0125/2374] Test os.PathLike that's not pathlib.Path --- Tests/test_util.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_util.py b/Tests/test_util.py index 71a862569b7..617e5f7c6da 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,13 +1,15 @@ from __future__ import annotations -from pathlib import Path +from pathlib import Path, PurePath import pytest from PIL import _util -@pytest.mark.parametrize("test_path", ["filename.ext", Path("filename.ext")]) +@pytest.mark.parametrize( + "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] +) def test_is_path(test_path): # Act it_is = _util.is_path(test_path) From d631afc266c3c1214e12373d3ad0d16978867a7f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 26 Jan 2024 20:46:58 +0200 Subject: [PATCH 0126/2374] Use os.fspath instead of isinstance and str --- src/PIL/ImageFont.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 9eecad1ca3a..1feaf447a17 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -230,8 +230,7 @@ def load_from_bytes(f): ) if is_path(font): - if isinstance(font, os.PathLike): - font = str(font) + font = os.fspath(font) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() try: From 737314923fd1abe8cea2b1986626302215436481 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Jan 2024 15:19:43 +1100 Subject: [PATCH 0127/2374] Added type hints --- Tests/test_000_sanity.py | 2 +- Tests/test_binary.py | 6 +++--- Tests/test_file_cur.py | 4 ++-- Tests/test_file_ftex.py | 6 +++--- Tests/test_file_gbr.py | 8 ++++---- Tests/test_file_gd.py | 6 +++--- Tests/test_file_gimppalette.py | 4 ++-- Tests/test_file_imt.py | 4 ++-- Tests/test_file_mcidas.py | 4 ++-- Tests/test_file_pcd.py | 2 +- Tests/test_file_pixar.py | 4 ++-- Tests/test_file_qoi.py | 4 ++-- Tests/test_file_wal.py | 4 ++-- Tests/test_file_webp_lossless.py | 4 +++- Tests/test_file_xpm.py | 6 +++--- Tests/test_file_xvthumb.py | 6 +++--- Tests/test_fontfile.py | 4 +++- Tests/test_format_lab.py | 6 +++--- Tests/test_lib_image.py | 2 +- Tests/test_locale.py | 2 +- Tests/test_main.py | 2 +- Tests/test_pyroma.py | 2 +- Tests/test_uploader.py | 4 ++-- Tests/test_util.py | 14 ++++++++------ Tests/test_webp_leaks.py | 4 ++-- 25 files changed, 60 insertions(+), 54 deletions(-) diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index f64216bca8e..c3926250f7b 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -3,7 +3,7 @@ from PIL import Image -def test_sanity(): +def test_sanity() -> None: # Make sure we have the binary extension Image.core.new("L", (100, 100)) diff --git a/Tests/test_binary.py b/Tests/test_binary.py index 41fb93fcf48..d19799a095c 100644 --- a/Tests/test_binary.py +++ b/Tests/test_binary.py @@ -3,12 +3,12 @@ from PIL import _binary -def test_standard(): +def test_standard() -> None: assert _binary.i8(b"*") == 42 assert _binary.o8(42) == b"*" -def test_little_endian(): +def test_little_endian() -> None: assert _binary.i16le(b"\xff\xff\x00\x00") == 65535 assert _binary.i32le(b"\xff\xff\x00\x00") == 65535 @@ -16,7 +16,7 @@ def test_little_endian(): assert _binary.o32le(65535) == b"\xff\xff\x00\x00" -def test_big_endian(): +def test_big_endian() -> None: assert _binary.i16be(b"\x00\x00\xff\xff") == 0 assert _binary.i32be(b"\x00\x00\xff\xff") == 65535 diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 27b2bc91489..dbf1b866d7f 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -7,7 +7,7 @@ TEST_FILE = "Tests/images/deerstalker.cur" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: assert im.size == (32, 32) assert isinstance(im, CurImagePlugin.CurImageFile) @@ -17,7 +17,7 @@ def test_sanity(): assert im.getpixel((16, 16)) == (84, 87, 86, 255) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index 0f9154e3d09..0c544245a9f 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -7,18 +7,18 @@ from .helper import assert_image_equal_tofile, assert_image_similar -def test_load_raw(): +def test_load_raw() -> None: with Image.open("Tests/images/ftex_uncompressed.ftu") as im: assert_image_equal_tofile(im, "Tests/images/ftex_uncompressed.png") -def test_load_dxt1(): +def test_load_dxt1() -> None: with Image.open("Tests/images/ftex_dxt1.ftc") as im: with Image.open("Tests/images/ftex_dxt1.png") as target: assert_image_similar(im, target.convert("RGBA"), 15) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index d84004e1483..be98b08f2ad 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -7,12 +7,12 @@ from .helper import assert_image_equal_tofile -def test_gbr_file(): +def test_gbr_file() -> None: with Image.open("Tests/images/gbr.gbr") as im: assert_image_equal_tofile(im, "Tests/images/gbr.png") -def test_load(): +def test_load() -> None: with Image.open("Tests/images/gbr.gbr") as im: assert im.load()[0, 0] == (0, 0, 0, 0) @@ -20,14 +20,14 @@ def test_load(): assert im.load()[0, 0] == (0, 0, 0, 0) -def test_multiple_load_operations(): +def test_multiple_load_operations() -> None: with Image.open("Tests/images/gbr.gbr") as im: im.load() im.load() assert_image_equal_tofile(im, "Tests/images/gbr.png") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index e7db54fb44c..d512df284e1 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -7,18 +7,18 @@ TEST_GD_FILE = "Tests/images/hopper.gd" -def test_sanity(): +def test_sanity() -> None: with GdImageFile.open(TEST_GD_FILE) as im: assert im.size == (128, 128) assert im.format == "GD" -def test_bad_mode(): +def test_bad_mode() -> None: with pytest.raises(ValueError): GdImageFile.open(TEST_GD_FILE, "bad mode") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(UnidentifiedImageError): diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index 28855c28acc..e8d5f170506 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -5,7 +5,7 @@ from PIL.GimpPaletteFile import GimpPaletteFile -def test_sanity(): +def test_sanity() -> None: with open("Tests/images/test.gpl", "rb") as fp: GimpPaletteFile(fp) @@ -22,7 +22,7 @@ def test_sanity(): GimpPaletteFile(fp) -def test_get_palette(): +def test_get_palette() -> None: # Arrange with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: palette_file = GimpPaletteFile(fp) diff --git a/Tests/test_file_imt.py b/Tests/test_file_imt.py index aa13d4407bc..6957dfa0ac5 100644 --- a/Tests/test_file_imt.py +++ b/Tests/test_file_imt.py @@ -9,13 +9,13 @@ from .helper import assert_image_equal_tofile -def test_sanity(): +def test_sanity() -> None: with Image.open("Tests/images/bw_gradient.imt") as im: assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") @pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n")) -def test_invalid_file(data): +def test_invalid_file(data: bytes) -> None: with io.BytesIO(data) as fp: with pytest.raises(SyntaxError): ImtImagePlugin.ImtImageFile(fp) diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index 73eba5cc861..2c94fdc3911 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -7,14 +7,14 @@ from .helper import assert_image_equal_tofile -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): McIdasImagePlugin.McIdasImageFile(invalid_file) -def test_valid_file(): +def test_valid_file() -> None: # Arrange # https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8 # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 1a37c6ab31d..81a316fc14a 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -3,7 +3,7 @@ from PIL import Image -def test_load_raw(): +def test_load_raw() -> None: with Image.open("Tests/images/hopper.pcd") as im: im.load() # should not segfault. diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py index c6ddc54e714..8f208cfbf07 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -9,7 +9,7 @@ TEST_FILE = "Tests/images/hopper.pxr" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "RGB" @@ -21,7 +21,7 @@ def test_sanity(): assert_image_similar(im, im2, 4.8) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py index 6dc468754ef..fd4b981ce93 100644 --- a/Tests/test_file_qoi.py +++ b/Tests/test_file_qoi.py @@ -7,7 +7,7 @@ from .helper import assert_image_equal_tofile -def test_sanity(): +def test_sanity() -> None: with Image.open("Tests/images/hopper.qoi") as im: assert im.mode == "RGB" assert im.size == (128, 128) @@ -23,7 +23,7 @@ def test_sanity(): assert_image_equal_tofile(im, "Tests/images/pil123rgba.png") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index 7acec975942..b34975e8380 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -7,7 +7,7 @@ TEST_FILE = "Tests/images/hopper.wal" -def test_open(): +def test_open() -> None: with WalImageFile.open(TEST_FILE) as im: assert im.format == "WAL" assert im.format_description == "Quake2 Texture" @@ -19,7 +19,7 @@ def test_open(): assert_image_equal_tofile(im, "Tests/images/hopper_wal.png") -def test_load(): +def test_load() -> None: with WalImageFile.open(TEST_FILE) as im: assert im.load()[0, 0] == 122 diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 08c80973a74..32e29de56ad 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -10,7 +12,7 @@ RGB_MODE = "RGB" -def test_write_lossless_rgb(tmp_path): +def test_write_lossless_rgb(tmp_path: Path) -> None: if _webp.WebPDecoderVersion() < 0x0200: pytest.skip("lossless not included") diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 529a4558073..26afe93f450 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -9,7 +9,7 @@ TEST_FILE = "Tests/images/hopper.xpm" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "P" @@ -20,14 +20,14 @@ def test_sanity(): assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): XpmImagePlugin.XpmImageFile(invalid_file) -def test_load_read(): +def test_load_read() -> None: # Arrange with Image.open(TEST_FILE) as im: dummy_bytes = 1 diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index b87494eba18..6b81159303e 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -9,7 +9,7 @@ TEST_FILE = "Tests/images/hopper.p7" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -20,7 +20,7 @@ def test_open(): assert_image_similar(im, im_hopper, 9) -def test_unexpected_eof(): +def test_unexpected_eof() -> None: # Test unexpected EOF reading XV thumbnail file # Arrange bad_file = "Tests/images/hopper_bad.p7" @@ -30,7 +30,7 @@ def test_unexpected_eof(): XVThumbImagePlugin.XVThumbImageFile(bad_file) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py index eda8fb81283..206499a047f 100644 --- a/Tests/test_fontfile.py +++ b/Tests/test_fontfile.py @@ -1,11 +1,13 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import FontFile -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: tempname = str(tmp_path / "temp.pil") font = FontFile.FontFile() diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index a55620e09ab..4fcc37e88cc 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -3,7 +3,7 @@ from PIL import Image -def test_white(): +def test_white() -> None: with Image.open("Tests/images/lab.tif") as i: i.load() @@ -24,7 +24,7 @@ def test_white(): assert list(b) == [128] * 100 -def test_green(): +def test_green() -> None: # l= 50 (/100), a = -100 (-128 .. 128) b=0 in PS # == RGB: 0, 152, 117 with Image.open("Tests/images/lab-green.tif") as i: @@ -32,7 +32,7 @@ def test_green(): assert k == (128, 28, 128) -def test_red(): +def test_red() -> None: # l= 50 (/100), a = 100 (-128 .. 128) b=0 in PS # == RGB: 255, 0, 124 with Image.open("Tests/images/lab-red.tif") as i: diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 1c642e4c981..31548bbc91f 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -5,7 +5,7 @@ from PIL import Image -def test_setmode(): +def test_setmode() -> None: im = Image.new("L", (1, 1), 255) im.im.setmode("1") assert im.im.getpixel((0, 0)) == 255 diff --git a/Tests/test_locale.py b/Tests/test_locale.py index db9557d7ba2..1c8b84a2b41 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -24,7 +24,7 @@ path = "Tests/images/hopper.jpg" -def test_sanity(): +def test_sanity() -> None: with Image.open(path): pass try: diff --git a/Tests/test_main.py b/Tests/test_main.py index 9f61a0c8169..46259f1dc52 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -5,7 +5,7 @@ import sys -def test_main(): +def test_main() -> None: out = subprocess.check_output([sys.executable, "-m", "PIL"]).decode("utf-8") lines = out.splitlines() assert lines[0] == "-" * 68 diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index c2cea08ca61..c2f7fe22ecb 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -7,7 +7,7 @@ pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") -def test_pyroma(): +def test_pyroma() -> None: # Arrange data = pyroma.projectdata.get_data(".") diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py index 75326288f97..d55ceb4be17 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -3,13 +3,13 @@ from .helper import assert_image_equal, assert_image_similar, hopper -def check_upload_equal(): +def check_upload_equal() -> None: result = hopper("P").convert("RGB") target = hopper("RGB") assert_image_equal(result, target) -def check_upload_similar(): +def check_upload_similar() -> None: result = hopper("P").convert("RGB") target = hopper("RGB") assert_image_similar(result, target, 0) diff --git a/Tests/test_util.py b/Tests/test_util.py index 3395ef753d7..b47ca88271c 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,11 +1,13 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import _util -def test_is_path(): +def test_is_path() -> None: # Arrange fp = "filename.ext" @@ -16,7 +18,7 @@ def test_is_path(): assert it_is -def test_path_obj_is_path(): +def test_path_obj_is_path() -> None: # Arrange from pathlib import Path @@ -29,7 +31,7 @@ def test_path_obj_is_path(): assert it_is -def test_is_not_path(tmp_path): +def test_is_not_path(tmp_path: Path) -> None: # Arrange with (tmp_path / "temp.ext").open("w") as fp: pass @@ -41,7 +43,7 @@ def test_is_not_path(tmp_path): assert not it_is_not -def test_is_directory(): +def test_is_directory() -> None: # Arrange directory = "Tests" @@ -52,7 +54,7 @@ def test_is_directory(): assert it_is -def test_is_not_directory(): +def test_is_not_directory() -> None: # Arrange text = "abc" @@ -63,7 +65,7 @@ def test_is_not_directory(): assert not it_is_not -def test_deferred_error(): +def test_deferred_error() -> None: # Arrange # Act diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 0f51abc9574..626fe427cab 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -14,11 +14,11 @@ class TestWebPLeaks(PillowLeakTestCase): mem_limit = 3 * 1024 # kb iterations = 100 - def test_leak_load(self): + def test_leak_load(self) -> None: with open(test_file, "rb") as f: im_data = f.read() - def core(): + def core() -> None: with Image.open(BytesIO(im_data)) as im: im.load() From cd640e5df27c6c3c58d8cc63c16cba71c237b9a4 Mon Sep 17 00:00:00 2001 From: Nicola Guerrera Date: Mon, 22 Jan 2024 15:19:59 +0100 Subject: [PATCH 0128/2374] Refactor grabclipboard() for x11 and wayland MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simpified logic and made it more robust against edge cases ( see the `allowed_errors` list ). Doing error checking this way, makes the behaviour of this function for x11 and wayland platforms more silimar to darwin and windows systems. fix typo src/PIL/ImageGrab.py Co-authored-by: Ondrej Baranovič fix typo src/PIL/ImageGrab.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> ImageGrab: \added debian edge case to comment --- src/PIL/ImageGrab.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index a4993d3d4b8..1cb02f5f93c 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -149,18 +149,7 @@ def grabclipboard(): session_type = None if shutil.which("wl-paste") and session_type in ("wayland", None): - output = subprocess.check_output(["wl-paste", "-l"]).decode() - mimetypes = output.splitlines() - if "image/png" in mimetypes: - mimetype = "image/png" - elif mimetypes: - mimetype = mimetypes[0] - else: - mimetype = None - - args = ["wl-paste"] - if mimetype: - args.extend(["-t", mimetype]) + args = ["wl-paste", "-t", "image"] elif shutil.which("xclip") and session_type in ("x11", None): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: @@ -168,10 +157,19 @@ def grabclipboard(): raise NotImplementedError(msg) p = subprocess.run(args, capture_output=True) - err = p.stderr - if err: - msg = f"{args[0]} error: {err.strip().decode()}" + err = p.stderr.decode() + if p.returncode != 0: + allowed_errors = [ + "Nothing is copied", # wl-paste, when the clipboard is empty + "not available", # wl-paste/debian xclip, when an image isn't available + "cannot convert", # xclip, when an image isn't available + "There is no owner", # xclip, when the clipboard isn't initialized + ] + if any(e in err for e in allowed_errors): + return None + msg = f"{args[0]} error: {err.strip() if err else 'Unknown error'}" raise ChildProcessError(msg) + data = io.BytesIO(p.stdout) im = Image.open(data) im.load() From b81341ae7e62a246adabc40982d2b81ed3b7542d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Jan 2024 20:15:10 +1100 Subject: [PATCH 0129/2374] Only decode stderr when necessary --- src/PIL/ImageGrab.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 1cb02f5f93c..730351c0d68 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -157,17 +157,21 @@ def grabclipboard(): raise NotImplementedError(msg) p = subprocess.run(args, capture_output=True) - err = p.stderr.decode() + err = p.stderr if p.returncode != 0: allowed_errors = [ - "Nothing is copied", # wl-paste, when the clipboard is empty - "not available", # wl-paste/debian xclip, when an image isn't available - "cannot convert", # xclip, when an image isn't available - "There is no owner", # xclip, when the clipboard isn't initialized + # wl-paste, when the clipboard is empty + b"Nothing is copied", + # wl-paste/debian xclip, when an image isn't available + b"not available", + # xclip, when an image isn't available + b"cannot convert", + # xclip, when the clipboard isn't initialized + b"There is no owner", ] if any(e in err for e in allowed_errors): return None - msg = f"{args[0]} error: {err.strip() if err else 'Unknown error'}" + msg = f"{args[0]} error: {err.strip().decode() if err else 'Unknown error'}" raise ChildProcessError(msg) data = io.BytesIO(p.stdout) From d2d9240de4cafee650f11c085a7ec321240a8e3e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Jan 2024 19:26:55 +1100 Subject: [PATCH 0130/2374] Do not declare variable until necessary --- src/PIL/ImageGrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 730351c0d68..ca27b520caa 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -157,7 +157,6 @@ def grabclipboard(): raise NotImplementedError(msg) p = subprocess.run(args, capture_output=True) - err = p.stderr if p.returncode != 0: allowed_errors = [ # wl-paste, when the clipboard is empty @@ -169,6 +168,7 @@ def grabclipboard(): # xclip, when the clipboard isn't initialized b"There is no owner", ] + err = p.stderr if any(e in err for e in allowed_errors): return None msg = f"{args[0]} error: {err.strip().decode() if err else 'Unknown error'}" From 6998f3476843e2f8da00eb23545aab55dc280006 Mon Sep 17 00:00:00 2001 From: Nicola Guerrera Date: Sat, 27 Jan 2024 12:08:16 +0100 Subject: [PATCH 0131/2374] Rearrange error handling Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageGrab.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index ca27b520caa..a2c7a935103 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -171,7 +171,9 @@ def grabclipboard(): err = p.stderr if any(e in err for e in allowed_errors): return None - msg = f"{args[0]} error: {err.strip().decode() if err else 'Unknown error'}" + msg = f"{args[0]} error" + if err: + msg += f": {err.strip().decode()}" raise ChildProcessError(msg) data = io.BytesIO(p.stdout) From d3205fae192ec10497326aacb7325f5880d07b04 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Jan 2024 22:54:01 +1100 Subject: [PATCH 0132/2374] Simplified code --- src/PIL/ImageGrab.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index a2c7a935103..c04be521f91 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -158,7 +158,8 @@ def grabclipboard(): p = subprocess.run(args, capture_output=True) if p.returncode != 0: - allowed_errors = [ + err = p.stderr + for silent_error in [ # wl-paste, when the clipboard is empty b"Nothing is copied", # wl-paste/debian xclip, when an image isn't available @@ -167,10 +168,9 @@ def grabclipboard(): b"cannot convert", # xclip, when the clipboard isn't initialized b"There is no owner", - ] - err = p.stderr - if any(e in err for e in allowed_errors): - return None + ]: + if err in silent_error: + return None msg = f"{args[0]} error" if err: msg += f": {err.strip().decode()}" From 61d47c3dfa200b186ecacd7b9a5090cedb5523b6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:06:06 +0200 Subject: [PATCH 0133/2374] More support for arbitrary os.PathLike --- Tests/test_image.py | 3 +-- docs/reference/open_files.rst | 2 +- src/PIL/Image.py | 16 ++++++---------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index dd989ad99b5..84189df5477 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -7,6 +7,7 @@ import sys import tempfile import warnings +from pathlib import Path import pytest @@ -161,8 +162,6 @@ def test_stringio(self): pass def test_pathlib(self, tmp_path): - from PIL.Image import Path - with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: assert im.mode == "P" assert im.size == (10, 10) diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index f31941c9abb..730c8da5b80 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -3,7 +3,7 @@ File Handling in Pillow ======================= -When opening a file as an image, Pillow requires a filename, ``pathlib.Path`` +When opening a file as an image, Pillow requires a filename, ``os.PathLike`` object, or a file-like object. Pillow uses the filename or ``Path`` to open a file, so for the rest of this article, they will all be treated as a file-like object. diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 553f36703b3..48125b3173b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -39,7 +39,6 @@ import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum -from pathlib import Path try: from defusedxml import ElementTree @@ -2370,7 +2369,7 @@ def save(self, fp, format=None, **params) -> None: implement the ``seek``, ``tell``, and ``write`` methods, and be opened in binary mode. - :param fp: A filename (string), pathlib.Path object or file object. + :param fp: A filename (string), os.PathLike object or file object. :param format: Optional format override. If omitted, the format to use is determined from the filename extension. If a file object was used instead of a filename, this @@ -2385,11 +2384,8 @@ def save(self, fp, format=None, **params) -> None: filename = "" open_fp = False - if isinstance(fp, Path): - filename = str(fp) - open_fp = True - elif is_path(fp): - filename = fp + if is_path(fp): + filename = os.fspath(fp) open_fp = True elif fp == sys.stdout: try: @@ -3206,7 +3202,7 @@ def open(fp, mode="r", formats=None) -> Image: :py:meth:`~PIL.Image.Image.load` method). See :py:func:`~PIL.Image.new`. See :ref:`file-handling`. - :param fp: A filename (string), pathlib.Path object or a file object. + :param fp: A filename (string), os.PathLike object or a file object. The file object must implement ``file.read``, ``file.seek``, and ``file.tell`` methods, and be opened in binary mode. The file object will also seek to zero @@ -3244,8 +3240,8 @@ def open(fp, mode="r", formats=None) -> Image: exclusive_fp = False filename = "" - if isinstance(fp, Path): - filename = str(fp.resolve()) + if isinstance(fp, os.PathLike): + filename = os.path.realpath(os.fspath(fp)) elif is_path(fp): filename = fp From 52e51e12b950aac7a5bd5593ea9fc2981490c2d3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 28 Jan 2024 21:20:28 +0000 Subject: [PATCH 0134/2374] Update dependency cibuildwheel to v2.16.4 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index dd61634cd31..867543ebd84 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.16.2 +cibuildwheel==2.16.4 From 529487c244c64ee93ed7601eac9a9bbc3194827f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:48:39 +0200 Subject: [PATCH 0135/2374] Remove execute bit from setup.py --- setup.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 setup.py diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 From 866c26957d521ec02c6dc86c686800fe0a18a4d6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:37:24 +0200 Subject: [PATCH 0136/2374] Add check-shebang-scripts-are-executable to pre-commit --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6adc75b4902..5ce0c9a1792 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,6 +32,7 @@ repos: rev: v4.5.0 hooks: - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable - id: check-merge-conflict - id: check-json - id: check-toml From 0669532898c9cbb45ceebbeefb317dfcc97eaac1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:43:03 +0200 Subject: [PATCH 0137/2374] Remove shebangs --- Tests/check_fli_oob.py | 1 - Tests/images/create_eps.gnuplot | 2 -- Tests/oss-fuzz/fuzz_pillow.py | 2 -- setup.py | 1 - 4 files changed, 6 deletions(-) diff --git a/Tests/check_fli_oob.py b/Tests/check_fli_oob.py index ac46ff1ebc0..e0057a2c2ad 100644 --- a/Tests/check_fli_oob.py +++ b/Tests/check_fli_oob.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 from __future__ import annotations from PIL import Image diff --git a/Tests/images/create_eps.gnuplot b/Tests/images/create_eps.gnuplot index 4d7e2987769..57a3c8c9725 100644 --- a/Tests/images/create_eps.gnuplot +++ b/Tests/images/create_eps.gnuplot @@ -1,5 +1,3 @@ -#!/usr/bin/gnuplot - #This is the script that was used to create our sample EPS files #We used the following version of the gnuplot program #G N U P L O T diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index e6e99d415a6..9137391b656 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/setup.py b/setup.py index 1bf0bcff558..1bbd2c05cc9 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # > pyroma . # ------------------------------ # Checking . From 76955bbaf7ee718c743da8ba1866e5c98b69f272 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:43:51 +0200 Subject: [PATCH 0138/2374] Remove shebang and execute bit --- Tests/check_jp2_overflow.py | 2 -- 1 file changed, 2 deletions(-) mode change 100755 => 100644 Tests/check_jp2_overflow.py diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py old mode 100755 new mode 100644 index 5adbb84b69c..954d68bf7e3 --- a/Tests/check_jp2_overflow.py +++ b/Tests/check_jp2_overflow.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Reproductions/tests for OOB read errors in FliDecode.c # When run in python, all of these images should fail for From 139320be3a121dbc38d51baefda8d0b97441314d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Jan 2024 19:44:42 +1100 Subject: [PATCH 0139/2374] Pin to Python 3.9.16-1 --- .github/workflows/test-cygwin.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 9c3eb092417..b5c8c39aaef 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -47,7 +47,7 @@ jobs: uses: actions/checkout@v4 - name: Install Cygwin - uses: cygwin/cygwin-install-action@v4 + uses: egor-tensin/setup-cygwin@v4 with: platform: x86_64 packages: > @@ -69,6 +69,7 @@ jobs: make netpbm perl + python39=3.9.16-1 python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel @@ -86,7 +87,7 @@ jobs: - name: Select Python version run: | - ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 + ln -sf c:/tools/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/tools/cygwin/bin/python3 - name: Get latest NumPy version id: latest-numpy From 40fceedfba5d79cde4891ec70e69aee961cd3165 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Jan 2024 22:22:13 +1100 Subject: [PATCH 0140/2374] brew remove libxau --- .github/workflows/wheels-dependencies.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3ec3148734f..50ac2e18e8b 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -131,13 +131,13 @@ untar pillow-depends-main.zip if [[ -n "$IS_MACOS" ]]; then # webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb - # libxdmcp causes an issue on macOS < 11 + # libxau and libxdmcp cause an issue on macOS < 11 # if php is installed, brew tries to reinstall these after installing openblas # remove cairo to fix building harfbuzz on arm64 # remove lcms2 and libpng to fix building openjpeg on arm64 # remove zstd to avoid inclusion on x86_64 # curl from brew requires zstd, use system curl - brew remove --ignore-dependencies webp libpng libtiff libxcb libxdmcp curl php cairo lcms2 ghostscript zstd + brew remove --ignore-dependencies webp libpng libtiff libxcb libxau libxdmcp curl php cairo lcms2 ghostscript zstd brew install pkg-config fi From b374f2679c4a0b102a1bc59b177a1a6b5cd0e1be Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Jan 2024 22:57:17 +1100 Subject: [PATCH 0141/2374] Build libxcb dependencies on macOS x86-64 --- .github/workflows/wheels-dependencies.sh | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 50ac2e18e8b..26bf2f6d655 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -72,13 +72,11 @@ function build { build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then - if [[ "$CIBW_ARCHS" == "arm64" ]]; then - build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto - build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib - build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist - if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then - cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc - fi + build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto + build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib + build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist + if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then + cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc fi else sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc From 7c6d8066452621f1adc54ca21305563e5fef99d5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 30 Jan 2024 18:26:20 +0200 Subject: [PATCH 0142/2374] Test on macOS M1 where available --- .github/workflows/test.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e23f5c5b12..ae84a4d8fd5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,7 @@ jobs: fail-fast: false matrix: os: [ - "macos-latest", + "macos-14", "ubuntu-latest", ] python-version: [ @@ -50,11 +50,21 @@ jobs: "3.8", ] include: - - python-version: "3.9" + - python-version: "3.11" PYTHONOPTIMIZE: 1 REVERSE: "--reverse" - - python-version: "3.8" + - python-version: "3.10" PYTHONOPTIMIZE: 2 + # M1 only available for 3.10+ + - os: "macos-latest" + python-version: "3.9" + - os: "macos-latest" + python-version: "3.8" + exclude: + - os: "macos-14" + python-version: "3.9" + - os: "macos-14" + python-version: "3.8" runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} @@ -141,7 +151,7 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v3 with: - flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} + flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} gcov: true From d65f7b5ef7ef32905078e10af4471ce8bfa54c9f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 30 Jan 2024 22:10:22 +0200 Subject: [PATCH 0143/2374] brew install ghostscript --- .github/workflows/macos-install.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index f41324c4ba6..28124d7f759 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,7 +2,16 @@ set -e -brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm +brew install \ + freetype \ + ghostscript \ + libimagequant \ + libjpeg \ + libraqm \ + libtiff \ + little-cms2 \ + openjpeg \ + webp export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" # TODO Update condition when cffi supports 3.13 From 1dad1b87ed77e283d8fb10eee1e61e66c5173eba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 00:17:32 +0000 Subject: [PATCH 0144/2374] Update dependency cibuildwheel to v2.16.5 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 867543ebd84..ccd6d87edc6 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.16.4 +cibuildwheel==2.16.5 From 39cbd4f0f1bf4f40229f50aa5480b4b25eaae1a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Jan 2024 16:31:03 +1100 Subject: [PATCH 0145/2374] Expanded error message strings --- src/PIL/ImageGrab.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index c04be521f91..b888e66f1c1 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -163,11 +163,11 @@ def grabclipboard(): # wl-paste, when the clipboard is empty b"Nothing is copied", # wl-paste/debian xclip, when an image isn't available - b"not available", + b" not available", # xclip, when an image isn't available - b"cannot convert", + b"cannot convert ", # xclip, when the clipboard isn't initialized - b"There is no owner", + b"xclip: Error: There is no owner for the ", ]: if err in silent_error: return None From 5efa2ade222785979c1b085be09eff5ee738c42c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Jan 2024 16:53:27 +1100 Subject: [PATCH 0146/2374] Added test --- Tests/test_imagegrab.py | 12 ++++++++++++ src/PIL/ImageGrab.py | 6 +++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 9d3d40398f6..efef4d90807 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -119,3 +119,15 @@ def test_grabclipboard_wl_clipboard(self, ext): subprocess.call(["wl-copy"], stdin=fp) im = ImageGrab.grabclipboard() assert_image_equal_tofile(im, image_path) + + @pytest.mark.skipif( + ( + sys.platform != "linux" + or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy")) + ), + reason="Linux with wl-clipboard only", + ) + @pytest.mark.parametrize("arg", ("text", "--clear")) + def test_grabclipboard_wl_clipboard_errors(self, arg): + subprocess.call(["wl-copy", arg]) + assert ImageGrab.grabclipboard() is None diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index b888e66f1c1..17f5750b1f3 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -162,7 +162,11 @@ def grabclipboard(): for silent_error in [ # wl-paste, when the clipboard is empty b"Nothing is copied", - # wl-paste/debian xclip, when an image isn't available + # Ubuntu/Debian wl-paste, when the clipboard is empty + b"No selection", + # Ubuntu/Debian wl-paste, when an image isn't available + b"No suitable type of content copied", + # wl-paste or Ubuntu/Debian xclip, when an image isn't available b" not available", # xclip, when an image isn't available b"cannot convert ", From d57b5e827cfd0e9850a074a4ba27e9f5ad0c9910 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Jan 2024 16:49:44 +1100 Subject: [PATCH 0147/2374] Corrected check --- src/PIL/ImageGrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 17f5750b1f3..3f3be706d96 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -173,7 +173,7 @@ def grabclipboard(): # xclip, when the clipboard isn't initialized b"xclip: Error: There is no owner for the ", ]: - if err in silent_error: + if silent_error in err: return None msg = f"{args[0]} error" if err: From 4a4b90c3652d5036ab8d7d140763fa1eeef62985 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:12:58 +0200 Subject: [PATCH 0148/2374] Autotype tests (#7756) * autotyping: --none-return * autotyping: --scalar-return * autotyping: --int-param * autotyping: --float-param * autotyping: --str-param * autotyping: --annotate-named-param tmp_path:pathlib.Path --- Tests/bench_cffi_access.py | 8 +- Tests/test_bmp_reference.py | 6 +- Tests/test_box_blur.py | 34 +++--- Tests/test_color_lut.py | 52 ++++----- Tests/test_core_resources.py | 32 ++--- Tests/test_decompression_bomb.py | 28 ++--- Tests/test_deprecate.py | 14 +-- Tests/test_features.py | 30 ++--- Tests/test_file_apng.py | 52 +++++---- Tests/test_file_blp.py | 14 ++- Tests/test_file_bmp.py | 35 +++--- Tests/test_file_bufrstub.py | 16 +-- Tests/test_file_container.py | 20 ++-- Tests/test_file_dcx.py | 20 ++-- Tests/test_file_dds.py | 53 ++++----- Tests/test_file_eps.py | 71 ++++++------ Tests/test_file_fits.py | 10 +- Tests/test_file_fli.py | 30 ++--- Tests/test_file_fpx.py | 8 +- Tests/test_file_gif.py | 173 +++++++++++++-------------- Tests/test_file_gimpgradient.py | 20 ++-- Tests/test_file_gribstub.py | 16 +-- Tests/test_file_hdf5stub.py | 16 +-- Tests/test_file_icns.py | 23 ++-- Tests/test_file_ico.py | 33 +++--- Tests/test_file_im.py | 29 ++--- Tests/test_file_iptc.py | 18 +-- Tests/test_file_jpeg.py | 155 +++++++++++++------------ Tests/test_file_jpeg2k.py | 77 ++++++------ Tests/test_file_libtiff.py | 167 +++++++++++++------------- Tests/test_file_libtiff_small.py | 7 +- Tests/test_file_mic.py | 14 +-- Tests/test_file_mpo.py | 44 +++---- Tests/test_file_msp.py | 17 +-- Tests/test_file_palm.py | 13 ++- Tests/test_file_pcx.py | 32 ++--- Tests/test_file_pdf.py | 35 +++--- Tests/test_file_png.py | 109 ++++++++--------- Tests/test_file_ppm.py | 51 ++++---- Tests/test_file_psd.py | 34 +++--- Tests/test_file_sgi.py | 24 ++-- Tests/test_file_spider.py | 35 +++--- Tests/test_file_sun.py | 6 +- Tests/test_file_tar.py | 8 +- Tests/test_file_tga.py | 31 ++--- Tests/test_file_tiff.py | 151 ++++++++++++------------ Tests/test_file_tiff_metadata.py | 49 ++++---- Tests/test_file_webp.py | 41 +++---- Tests/test_file_webp_alpha.py | 14 ++- Tests/test_file_webp_animated.py | 18 +-- Tests/test_file_webp_metadata.py | 17 +-- Tests/test_file_wmf.py | 16 +-- Tests/test_file_xbm.py | 13 ++- Tests/test_format_hsv.py | 8 +- Tests/test_image.py | 155 +++++++++++++------------ Tests/test_image_access.py | 44 +++---- Tests/test_image_array.py | 10 +- Tests/test_image_draft.py | 6 +- Tests/test_image_entropy.py | 2 +- Tests/test_image_filter.py | 24 ++-- Tests/test_image_getextrema.py | 4 +- Tests/test_image_getpalette.py | 4 +- Tests/test_image_load.py | 10 +- Tests/test_image_mode.py | 4 +- Tests/test_image_paste.py | 28 ++--- Tests/test_image_putdata.py | 20 ++-- Tests/test_image_putpalette.py | 12 +- Tests/test_image_reduce.py | 40 ++++--- Tests/test_image_resample.py | 80 ++++++------- Tests/test_image_rotate.py | 30 ++--- Tests/test_image_thumbnail.py | 20 ++-- Tests/test_image_transform.py | 38 +++--- Tests/test_imagechops.py | 60 +++++----- Tests/test_imagecms.py | 71 ++++++------ Tests/test_imagecolor.py | 12 +- Tests/test_imagedraw.py | 174 ++++++++++++++-------------- Tests/test_imagedraw2.py | 24 ++-- Tests/test_imageenhance.py | 8 +- Tests/test_imagefile.py | 58 +++++----- Tests/test_imagefont.py | 122 +++++++++---------- Tests/test_imagefontctl.py | 40 +++---- Tests/test_imagefontpil.py | 16 +-- Tests/test_imagegrab.py | 16 +-- Tests/test_imagemath.py | 50 ++++---- Tests/test_imagemorph.py | 40 ++++--- Tests/test_imageops.py | 50 ++++---- Tests/test_imageops_usm.py | 10 +- Tests/test_imagepalette.py | 30 ++--- Tests/test_imagepath.py | 24 ++-- Tests/test_imageqt.py | 8 +- Tests/test_imagesequence.py | 20 ++-- Tests/test_imageshow.py | 18 +-- Tests/test_imagestat.py | 6 +- Tests/test_imagetk.py | 12 +- Tests/test_imagewin.py | 16 +-- Tests/test_imagewin_pointers.py | 3 +- Tests/test_lib_pack.py | 80 ++++++------- Tests/test_map.py | 6 +- Tests/test_mode_i16.py | 10 +- Tests/test_numpy.py | 32 ++--- Tests/test_pdfparser.py | 10 +- Tests/test_pickle.py | 17 +-- Tests/test_psdraw.py | 7 +- Tests/test_qt_image_qapplication.py | 8 +- Tests/test_qt_image_toqimage.py | 4 +- Tests/test_sgi_crash.py | 2 +- Tests/test_shell_injection.py | 11 +- Tests/test_tiff_ifdrational.py | 11 +- 108 files changed, 1866 insertions(+), 1798 deletions(-) diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index ad15a9739b2..c4ab3bdccb3 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -9,21 +9,21 @@ # Not running this test by default. No DOS against CI. -def iterate_get(size, access): +def iterate_get(size, access) -> None: (w, h) = size for x in range(w): for y in range(h): access[(x, y)] -def iterate_set(size, access): +def iterate_set(size, access) -> None: (w, h) = size for x in range(w): for y in range(h): access[(x, y)] = (x % 256, y % 256, 0) -def timer(func, label, *args): +def timer(func, label, *args) -> None: iterations = 5000 starttime = time.time() for x in range(iterations): @@ -38,7 +38,7 @@ def timer(func, label, *args): ) -def test_direct(): +def test_direct() -> None: im = hopper() im.load() # im = Image.new("RGB", (2000, 2000), (1, 3, 2)) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 0da41e85845..22ac9443e86 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -10,13 +10,13 @@ base = os.path.join("Tests", "images", "bmp") -def get_files(d, ext=".bmp"): +def get_files(d, ext: str = ".bmp"): return [ os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f ] -def test_bad(): +def test_bad() -> None: """These shouldn't crash/dos, but they shouldn't return anything either""" for f in get_files("b"): @@ -56,7 +56,7 @@ def test_questionable(): raise -def test_good(): +def test_good() -> None: """These should all work. There's a set of target files in the html directory that we can compare against.""" diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 461e6aaacb3..dfedb48d911 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -16,18 +16,18 @@ # fmt: on -def test_imageops_box_blur(): +def test_imageops_box_blur() -> None: i = sample.filter(ImageFilter.BoxBlur(1)) assert i.mode == sample.mode assert i.size == sample.size assert isinstance(i, Image.Image) -def box_blur(image, radius=1, n=1): +def box_blur(image, radius: int = 1, n: int = 1): return image._new(image.im.box_blur((radius, radius), n)) -def assert_image(im, data, delta=0): +def assert_image(im, data, delta: int = 0) -> None: it = iter(im.getdata()) for data_row in data: im_row = [next(it) for _ in range(im.size[0])] @@ -37,7 +37,7 @@ def assert_image(im, data, delta=0): next(it) -def assert_blur(im, radius, data, passes=1, delta=0): +def assert_blur(im, radius, data, passes: int = 1, delta: int = 0) -> None: # check grayscale image assert_image(box_blur(im, radius, passes), data, delta) rgba = Image.merge("RGBA", (im, im, im, im)) @@ -45,7 +45,7 @@ def assert_blur(im, radius, data, passes=1, delta=0): assert_image(band, data, delta) -def test_color_modes(): +def test_color_modes() -> None: with pytest.raises(ValueError): box_blur(sample.convert("1")) with pytest.raises(ValueError): @@ -65,7 +65,7 @@ def test_color_modes(): box_blur(sample.convert("YCbCr")) -def test_radius_0(): +def test_radius_0() -> None: assert_blur( sample, 0, @@ -81,7 +81,7 @@ def test_radius_0(): ) -def test_radius_0_02(): +def test_radius_0_02() -> None: assert_blur( sample, 0.02, @@ -98,7 +98,7 @@ def test_radius_0_02(): ) -def test_radius_0_05(): +def test_radius_0_05() -> None: assert_blur( sample, 0.05, @@ -115,7 +115,7 @@ def test_radius_0_05(): ) -def test_radius_0_1(): +def test_radius_0_1() -> None: assert_blur( sample, 0.1, @@ -132,7 +132,7 @@ def test_radius_0_1(): ) -def test_radius_0_5(): +def test_radius_0_5() -> None: assert_blur( sample, 0.5, @@ -149,7 +149,7 @@ def test_radius_0_5(): ) -def test_radius_1(): +def test_radius_1() -> None: assert_blur( sample, 1, @@ -166,7 +166,7 @@ def test_radius_1(): ) -def test_radius_1_5(): +def test_radius_1_5() -> None: assert_blur( sample, 1.5, @@ -183,7 +183,7 @@ def test_radius_1_5(): ) -def test_radius_bigger_then_half(): +def test_radius_bigger_then_half() -> None: assert_blur( sample, 3, @@ -200,7 +200,7 @@ def test_radius_bigger_then_half(): ) -def test_radius_bigger_then_width(): +def test_radius_bigger_then_width() -> None: assert_blur( sample, 10, @@ -215,7 +215,7 @@ def test_radius_bigger_then_width(): ) -def test_extreme_large_radius(): +def test_extreme_large_radius() -> None: assert_blur( sample, 600, @@ -230,7 +230,7 @@ def test_extreme_large_radius(): ) -def test_two_passes(): +def test_two_passes() -> None: assert_blur( sample, 1, @@ -248,7 +248,7 @@ def test_two_passes(): ) -def test_three_passes(): +def test_three_passes() -> None: assert_blur( sample, 1, diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index fcd1169ef81..e6c8d7819a6 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -41,7 +41,7 @@ def generate_identity_table(self, channels, size): [item for sublist in table for item in sublist], ) - def test_wrong_args(self): + def test_wrong_args(self) -> None: im = Image.new("RGB", (10, 10), 0) with pytest.raises(ValueError, match="filter"): @@ -101,7 +101,7 @@ def test_wrong_args(self): with pytest.raises(TypeError): im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) - def test_correct_args(self): + def test_correct_args(self) -> None: im = Image.new("RGB", (10, 10), 0) im.im.color_lut_3d( @@ -136,7 +136,7 @@ def test_correct_args(self): *self.generate_identity_table(3, (3, 3, 65)), ) - def test_wrong_mode(self): + def test_wrong_mode(self) -> None: with pytest.raises(ValueError, match="wrong mode"): im = Image.new("L", (10, 10), 0) im.im.color_lut_3d( @@ -167,7 +167,7 @@ def test_wrong_mode(self): "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) ) - def test_correct_mode(self): + def test_correct_mode(self) -> None: im = Image.new("RGBA", (10, 10), 0) im.im.color_lut_3d( "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) @@ -188,7 +188,7 @@ def test_correct_mode(self): "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) ) - def test_identities(self): + def test_identities(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -224,7 +224,7 @@ def test_identities(self): ), ) - def test_identities_4_channels(self): + def test_identities_4_channels(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -247,7 +247,7 @@ def test_identities_4_channels(self): ), ) - def test_copy_alpha_channel(self): + def test_copy_alpha_channel(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGBA", @@ -270,7 +270,7 @@ def test_copy_alpha_channel(self): ), ) - def test_channels_order(self): + def test_channels_order(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -295,7 +295,7 @@ def test_channels_order(self): ]))) # fmt: on - def test_overflow(self): + def test_overflow(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -348,7 +348,7 @@ def test_overflow(self): class TestColorLut3DFilter: - def test_wrong_args(self): + def test_wrong_args(self) -> None: with pytest.raises(ValueError, match="should be either an integer"): ImageFilter.Color3DLUT("small", [1]) @@ -376,7 +376,7 @@ def test_wrong_args(self): with pytest.raises(ValueError, match="Only 3 or 4 output"): ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8, channels=2) - def test_convert_table(self): + def test_convert_table(self) -> None: lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) assert tuple(lut.size) == (2, 2, 2) assert lut.name == "Color 3D LUT" @@ -394,7 +394,7 @@ def test_convert_table(self): assert lut.table == list(range(4)) * 8 @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy_sources(self): + def test_numpy_sources(self) -> None: table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16) with pytest.raises(ValueError, match="should have either channels"): lut = ImageFilter.Color3DLUT((5, 6, 7), table) @@ -427,7 +427,7 @@ def test_numpy_sources(self): assert lut.table[0] == 33 @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy_formats(self): + def test_numpy_formats(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -466,7 +466,7 @@ def test_numpy_formats(self): lut.table = numpy.array(lut.table, dtype=numpy.int8) im.filter(lut) - def test_repr(self): + def test_repr(self) -> None: lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) assert repr(lut) == "" @@ -484,7 +484,7 @@ def test_repr(self): class TestGenerateColorLut3D: - def test_wrong_channels_count(self): + def test_wrong_channels_count(self) -> None: with pytest.raises(ValueError, match="3 or 4 output channels"): ImageFilter.Color3DLUT.generate( 5, channels=2, callback=lambda r, g, b: (r, g, b) @@ -498,7 +498,7 @@ def test_wrong_channels_count(self): 5, channels=4, callback=lambda r, g, b: (r, g, b) ) - def test_3_channels(self): + def test_3_channels(self) -> None: lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) assert tuple(lut.size) == (5, 5, 5) assert lut.name == "Color 3D LUT" @@ -508,7 +508,7 @@ def test_3_channels(self): 1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.25, 0.25, 0.0, 0.5, 0.25, 0.0] # fmt: on - def test_4_channels(self): + def test_4_channels(self) -> None: lut = ImageFilter.Color3DLUT.generate( 5, channels=4, callback=lambda r, g, b: (b, r, g, (r + g + b) / 2) ) @@ -521,7 +521,7 @@ def test_4_channels(self): ] # fmt: on - def test_apply(self): + def test_apply(self) -> None: lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) g = Image.linear_gradient("L") @@ -537,7 +537,7 @@ def test_apply(self): class TestTransformColorLut3D: - def test_wrong_args(self): + def test_wrong_args(self) -> None: source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) with pytest.raises(ValueError, match="Only 3 or 4 output"): @@ -552,7 +552,7 @@ def test_wrong_args(self): with pytest.raises(TypeError): source.transform(lambda r, g, b, a: (r, g, b)) - def test_target_mode(self): + def test_target_mode(self) -> None: source = ImageFilter.Color3DLUT.generate( 2, lambda r, g, b: (r, g, b), target_mode="HSV" ) @@ -563,7 +563,7 @@ def test_target_mode(self): lut = source.transform(lambda r, g, b: (r, g, b), target_mode="RGB") assert lut.mode == "RGB" - def test_3_to_3_channels(self): + def test_3_to_3_channels(self) -> None: source = ImageFilter.Color3DLUT.generate((3, 4, 5), lambda r, g, b: (r, g, b)) lut = source.transform(lambda r, g, b: (r * r, g * g, b * b)) assert tuple(lut.size) == tuple(source.size) @@ -571,7 +571,7 @@ def test_3_to_3_channels(self): assert lut.table != source.table assert lut.table[:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0] - def test_3_to_4_channels(self): + def test_3_to_4_channels(self) -> None: source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b)) lut = source.transform(lambda r, g, b: (r * r, g * g, b * b, 1), channels=4) assert tuple(lut.size) == tuple(source.size) @@ -583,7 +583,7 @@ def test_3_to_4_channels(self): 0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1] # fmt: on - def test_4_to_3_channels(self): + def test_4_to_3_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (3, 6, 5), lambda r, g, b: (r, g, b, 1), channels=4 ) @@ -599,7 +599,7 @@ def test_4_to_3_channels(self): 1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0] # fmt: on - def test_4_to_4_channels(self): + def test_4_to_4_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4 ) @@ -613,7 +613,7 @@ def test_4_to_4_channels(self): 0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5] # fmt: on - def test_with_normals_3_channels(self): + def test_with_normals_3_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (6, 5, 4), lambda r, g, b: (r * r, g * g, b * b) ) @@ -629,7 +629,7 @@ def test_with_normals_3_channels(self): 0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0] # fmt: on - def test_with_normals_4_channels(self): + def test_with_normals_4_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (3, 6, 5), lambda r, g, b: (r * r, g * g, b * b, 1), channels=4 ) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index d3f76fdb1b9..5eabe8f11ba 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -9,7 +9,7 @@ from .helper import is_pypy -def test_get_stats(): +def test_get_stats() -> None: # Create at least one image Image.new("RGB", (10, 10)) @@ -22,7 +22,7 @@ def test_get_stats(): assert "blocks_cached" in stats -def test_reset_stats(): +def test_reset_stats() -> None: Image.core.reset_stats() stats = Image.core.get_stats() @@ -35,19 +35,19 @@ def test_reset_stats(): class TestCoreMemory: - def teardown_method(self): + def teardown_method(self) -> None: # Restore default values Image.core.set_alignment(1) Image.core.set_block_size(1024 * 1024) Image.core.set_blocks_max(0) Image.core.clear_cache() - def test_get_alignment(self): + def test_get_alignment(self) -> None: alignment = Image.core.get_alignment() assert alignment > 0 - def test_set_alignment(self): + def test_set_alignment(self) -> None: for i in [1, 2, 4, 8, 16, 32]: Image.core.set_alignment(i) alignment = Image.core.get_alignment() @@ -63,12 +63,12 @@ def test_set_alignment(self): with pytest.raises(ValueError): Image.core.set_alignment(3) - def test_get_block_size(self): + def test_get_block_size(self) -> None: block_size = Image.core.get_block_size() assert block_size >= 4096 - def test_set_block_size(self): + def test_set_block_size(self) -> None: for i in [4096, 2 * 4096, 3 * 4096]: Image.core.set_block_size(i) block_size = Image.core.get_block_size() @@ -84,7 +84,7 @@ def test_set_block_size(self): with pytest.raises(ValueError): Image.core.set_block_size(4000) - def test_set_block_size_stats(self): + def test_set_block_size_stats(self) -> None: Image.core.reset_stats() Image.core.set_blocks_max(0) Image.core.set_block_size(4096) @@ -96,12 +96,12 @@ def test_set_block_size_stats(self): if not is_pypy(): assert stats["freed_blocks"] >= 64 - def test_get_blocks_max(self): + def test_get_blocks_max(self) -> None: blocks_max = Image.core.get_blocks_max() assert blocks_max >= 0 - def test_set_blocks_max(self): + def test_set_blocks_max(self) -> None: for i in [0, 1, 10]: Image.core.set_blocks_max(i) blocks_max = Image.core.get_blocks_max() @@ -117,7 +117,7 @@ def test_set_blocks_max(self): Image.core.set_blocks_max(2**29) @pytest.mark.skipif(is_pypy(), reason="Images not collected") - def test_set_blocks_max_stats(self): + def test_set_blocks_max_stats(self) -> None: Image.core.reset_stats() Image.core.set_blocks_max(128) Image.core.set_block_size(4096) @@ -132,7 +132,7 @@ def test_set_blocks_max_stats(self): assert stats["blocks_cached"] == 64 @pytest.mark.skipif(is_pypy(), reason="Images not collected") - def test_clear_cache_stats(self): + def test_clear_cache_stats(self) -> None: Image.core.reset_stats() Image.core.clear_cache() Image.core.set_blocks_max(128) @@ -149,7 +149,7 @@ def test_clear_cache_stats(self): assert stats["freed_blocks"] >= 48 assert stats["blocks_cached"] == 16 - def test_large_images(self): + def test_large_images(self) -> None: Image.core.reset_stats() Image.core.set_blocks_max(0) Image.core.set_block_size(4096) @@ -166,14 +166,14 @@ def test_large_images(self): class TestEnvVars: - def teardown_method(self): + def teardown_method(self) -> None: # Restore default values Image.core.set_alignment(1) Image.core.set_block_size(1024 * 1024) Image.core.set_blocks_max(0) Image.core.clear_cache() - def test_units(self): + def test_units(self) -> None: Image._apply_env_variables({"PILLOW_BLOCKS_MAX": "2K"}) assert Image.core.get_blocks_max() == 2 * 1024 Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) @@ -187,6 +187,6 @@ def test_units(self): {"PILLOW_BLOCKS_MAX": "wat"}, ), ) - def test_warnings(self, var): + def test_warnings(self, var) -> None: with pytest.warns(UserWarning): Image._apply_env_variables(var) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index d3049eff124..9c21efa45f7 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -12,16 +12,16 @@ class TestDecompressionBomb: - def teardown_method(self, method): + def teardown_method(self, method) -> None: Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - def test_no_warning_small_file(self): + def test_no_warning_small_file(self) -> None: # Implicit assert: no warning. # A warning would cause a failure. with Image.open(TEST_FILE): pass - def test_no_warning_no_limit(self): + def test_no_warning_no_limit(self) -> None: # Arrange # Turn limit off Image.MAX_IMAGE_PIXELS = None @@ -33,7 +33,7 @@ def test_no_warning_no_limit(self): with Image.open(TEST_FILE): pass - def test_warning(self): + def test_warning(self) -> None: # Set limit to trigger warning on the test file Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 @@ -42,7 +42,7 @@ def test_warning(self): with Image.open(TEST_FILE): pass - def test_exception(self): + def test_exception(self) -> None: # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1 @@ -51,22 +51,22 @@ def test_exception(self): with Image.open(TEST_FILE): pass - def test_exception_ico(self): + def test_exception_ico(self) -> None: with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/decompression_bomb.ico"): pass - def test_exception_gif(self): + def test_exception_gif(self) -> None: with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/decompression_bomb.gif"): pass - def test_exception_gif_extents(self): + def test_exception_gif_extents(self) -> None: with Image.open("Tests/images/decompression_bomb_extents.gif") as im: with pytest.raises(Image.DecompressionBombError): im.seek(1) - def test_exception_gif_zero_width(self): + def test_exception_gif_zero_width(self) -> None: # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 4 * 64 * 128 assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128 @@ -75,7 +75,7 @@ def test_exception_gif_zero_width(self): with Image.open("Tests/images/zero_width.gif"): pass - def test_exception_bmp(self): + def test_exception_bmp(self) -> None: with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/bmp/b/reallybig.bmp"): pass @@ -83,15 +83,15 @@ def test_exception_bmp(self): class TestDecompressionCrop: @classmethod - def setup_class(cls): + def setup_class(cls) -> None: width, height = 128, 128 Image.MAX_IMAGE_PIXELS = height * width * 4 - 1 @classmethod - def teardown_class(cls): + def teardown_class(cls) -> None: Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - def test_enlarge_crop(self): + def test_enlarge_crop(self) -> None: # Crops can extend the extents, therefore we should have the # same decompression bomb warnings on them. with hopper() as src: @@ -99,7 +99,7 @@ def test_enlarge_crop(self): with pytest.warns(Image.DecompressionBombWarning): src.crop(box) - def test_crop_decompression_checks(self): + def test_crop_decompression_checks(self) -> None: im = Image.new("RGB", (100, 100)) for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)): diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 6c7f509a795..6ffc8f6f589 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -20,12 +20,12 @@ ), ], ) -def test_version(version, expected): +def test_version(version, expected) -> None: with pytest.warns(DeprecationWarning, match=expected): _deprecate.deprecate("Old thing", version, "new thing") -def test_unknown_version(): +def test_unknown_version() -> None: expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?" with pytest.raises(ValueError, match=expected): _deprecate.deprecate("Old thing", 12345, "new thing") @@ -46,13 +46,13 @@ def test_unknown_version(): ), ], ) -def test_old_version(deprecated, plural, expected): +def test_old_version(deprecated, plural, expected) -> None: expected = r"" with pytest.raises(RuntimeError, match=expected): _deprecate.deprecate(deprecated, 1, plural=plural) -def test_plural(): +def test_plural() -> None: expected = ( r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Use new thing instead\." @@ -61,7 +61,7 @@ def test_plural(): _deprecate.deprecate("Old things", 11, "new thing", plural=True) -def test_replacement_and_action(): +def test_replacement_and_action() -> None: expected = "Use only one of 'replacement' and 'action'" with pytest.raises(ValueError, match=expected): _deprecate.deprecate( @@ -76,7 +76,7 @@ def test_replacement_and_action(): "Upgrade to new thing.", ], ) -def test_action(action): +def test_action(action) -> None: expected = ( r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Upgrade to new thing\." @@ -85,7 +85,7 @@ def test_action(action): _deprecate.deprecate("Old thing", 11, action=action) -def test_no_replacement_or_action(): +def test_no_replacement_or_action() -> None: expected = ( r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)" ) diff --git a/Tests/test_features.py b/Tests/test_features.py index b90c1d25f2b..de74e9c1829 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -15,7 +15,7 @@ pass -def test_check(): +def test_check() -> None: # Check the correctness of the convenience function for module in features.modules: assert features.check_module(module) == features.check(module) @@ -25,11 +25,11 @@ def test_check(): assert features.check_feature(feature) == features.check(feature) -def test_version(): +def test_version() -> None: # Check the correctness of the convenience function # and the format of version numbers - def test(name, function): + def test(name, function) -> None: version = features.version(name) if not features.check(name): assert version is None @@ -47,56 +47,56 @@ def test(name, function): @skip_unless_feature("webp") -def test_webp_transparency(): +def test_webp_transparency() -> None: assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY @skip_unless_feature("webp") -def test_webp_mux(): +def test_webp_mux() -> None: assert features.check("webp_mux") == _webp.HAVE_WEBPMUX @skip_unless_feature("webp") -def test_webp_anim(): +def test_webp_anim() -> None: assert features.check("webp_anim") == _webp.HAVE_WEBPANIM @skip_unless_feature("libjpeg_turbo") -def test_libjpeg_turbo_version(): +def test_libjpeg_turbo_version() -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo")) @skip_unless_feature("libimagequant") -def test_libimagequant_version(): +def test_libimagequant_version() -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant")) @pytest.mark.parametrize("feature", features.modules) -def test_check_modules(feature): +def test_check_modules(feature) -> None: assert features.check_module(feature) in [True, False] @pytest.mark.parametrize("feature", features.codecs) -def test_check_codecs(feature): +def test_check_codecs(feature) -> None: assert features.check_codec(feature) in [True, False] -def test_check_warns_on_nonexistent(): +def test_check_warns_on_nonexistent() -> None: with pytest.warns(UserWarning) as cm: has_feature = features.check("typo") assert has_feature is False assert str(cm[-1].message) == "Unknown feature 'typo'." -def test_supported_modules(): +def test_supported_modules() -> None: assert isinstance(features.get_supported_modules(), list) assert isinstance(features.get_supported_codecs(), list) assert isinstance(features.get_supported_features(), list) assert isinstance(features.get_supported(), list) -def test_unsupported_codec(): +def test_unsupported_codec() -> None: # Arrange codec = "unsupported_codec" # Act / Assert @@ -106,7 +106,7 @@ def test_unsupported_codec(): features.version_codec(codec) -def test_unsupported_module(): +def test_unsupported_module() -> None: # Arrange module = "unsupported_module" # Act / Assert @@ -116,7 +116,7 @@ def test_unsupported_module(): features.version_module(module) -def test_pilinfo(): +def test_pilinfo() -> None: buf = io.StringIO() features.pilinfo(buf) out = buf.getvalue() diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 23263b5d459..f9edf6e9877 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImageSequence, PngImagePlugin @@ -8,7 +10,7 @@ # APNG browser support tests and fixtures via: # https://philip.html5.org/tests/apng/tests.html # (referenced from https://wiki.mozilla.org/APNG_Specification) -def test_apng_basic(): +def test_apng_basic() -> None: with Image.open("Tests/images/apng/single_frame.png") as im: assert not im.is_animated assert im.n_frames == 1 @@ -45,14 +47,14 @@ def test_apng_basic(): "filename", ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"), ) -def test_apng_fdat(filename): +def test_apng_fdat(filename) -> None: with Image.open(filename) as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_dispose(): +def test_apng_dispose() -> None: with Image.open("Tests/images/apng/dispose_op_none.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -84,7 +86,7 @@ def test_apng_dispose(): assert im.getpixel((64, 32)) == (0, 0, 0, 0) -def test_apng_dispose_region(): +def test_apng_dispose_region() -> None: with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -106,7 +108,7 @@ def test_apng_dispose_region(): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_dispose_op_previous_frame(): +def test_apng_dispose_op_previous_frame() -> None: # Test that the dispose settings being used are from the previous frame # # Image created with: @@ -131,14 +133,14 @@ def test_apng_dispose_op_previous_frame(): assert im.getpixel((0, 0)) == (255, 0, 0, 255) -def test_apng_dispose_op_background_p_mode(): +def test_apng_dispose_op_background_p_mode() -> None: with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im: im.seek(1) im.load() assert im.size == (128, 64) -def test_apng_blend(): +def test_apng_blend() -> None: with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -165,20 +167,20 @@ def test_apng_blend(): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_blend_transparency(): +def test_apng_blend_transparency() -> None: with Image.open("Tests/images/blend_transparency.png") as im: im.seek(1) assert im.getpixel((0, 0)) == (255, 0, 0) -def test_apng_chunk_order(): +def test_apng_chunk_order() -> None: with Image.open("Tests/images/apng/fctl_actl.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_delay(): +def test_apng_delay() -> None: with Image.open("Tests/images/apng/delay.png") as im: im.seek(1) assert im.info.get("duration") == 500.0 @@ -218,7 +220,7 @@ def test_apng_delay(): assert im.info.get("duration") == 1000.0 -def test_apng_num_plays(): +def test_apng_num_plays() -> None: with Image.open("Tests/images/apng/num_plays.png") as im: assert im.info.get("loop") == 0 @@ -226,7 +228,7 @@ def test_apng_num_plays(): assert im.info.get("loop") == 1 -def test_apng_mode(): +def test_apng_mode() -> None: with Image.open("Tests/images/apng/mode_16bit.png") as im: assert im.mode == "RGBA" im.seek(im.n_frames - 1) @@ -267,7 +269,7 @@ def test_apng_mode(): assert im.getpixel((64, 32)) == (0, 0, 255, 128) -def test_apng_chunk_errors(): +def test_apng_chunk_errors() -> None: with Image.open("Tests/images/apng/chunk_no_actl.png") as im: assert not im.is_animated @@ -292,7 +294,7 @@ def test_apng_chunk_errors(): im.seek(im.n_frames - 1) -def test_apng_syntax_errors(): +def test_apng_syntax_errors() -> None: with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: assert not im.is_animated @@ -336,14 +338,14 @@ def test_apng_syntax_errors(): "sequence_fdat_fctl.png", ), ) -def test_apng_sequence_errors(test_file): +def test_apng_sequence_errors(test_file) -> None: with pytest.raises(SyntaxError): with Image.open(f"Tests/images/apng/{test_file}") as im: im.seek(im.n_frames - 1) im.load() -def test_apng_save(tmp_path): +def test_apng_save(tmp_path: Path) -> None: with Image.open("Tests/images/apng/single_frame.png") as im: test_file = str(tmp_path / "temp.png") im.save(test_file, save_all=True) @@ -374,7 +376,7 @@ def test_apng_save(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_save_alpha(tmp_path): +def test_apng_save_alpha(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) @@ -388,7 +390,7 @@ def test_apng_save_alpha(tmp_path): assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127) -def test_apng_save_split_fdat(tmp_path): +def test_apng_save_split_fdat(tmp_path: Path) -> None: # test to make sure we do not generate sequence errors when writing # frames with image data spanning multiple fdAT chunks (in this case # both the default image and first animation frame will span multiple @@ -412,7 +414,7 @@ def test_apng_save_split_fdat(tmp_path): assert exception is None -def test_apng_save_duration_loop(tmp_path): +def test_apng_save_duration_loop(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") with Image.open("Tests/images/apng/delay.png") as im: frames = [] @@ -475,7 +477,7 @@ def test_apng_save_duration_loop(tmp_path): assert im.info["duration"] == 600 -def test_apng_save_disposal(tmp_path): +def test_apng_save_disposal(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") size = (128, 64) red = Image.new("RGBA", size, (255, 0, 0, 255)) @@ -576,7 +578,7 @@ def test_apng_save_disposal(tmp_path): assert im.getpixel((64, 32)) == (0, 0, 0, 0) -def test_apng_save_disposal_previous(tmp_path): +def test_apng_save_disposal_previous(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") size = (128, 64) blue = Image.new("RGBA", size, (0, 0, 255, 255)) @@ -598,7 +600,7 @@ def test_apng_save_disposal_previous(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_save_blend(tmp_path): +def test_apng_save_blend(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") size = (128, 64) red = Image.new("RGBA", size, (255, 0, 0, 255)) @@ -666,7 +668,7 @@ def test_apng_save_blend(tmp_path): assert im.getpixel((0, 0)) == (0, 255, 0, 255) -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open("Tests/images/apng/delay.png") im.seek(1) im.close() @@ -678,7 +680,9 @@ def test_seek_after_close(): @pytest.mark.parametrize("mode", ("RGBA", "RGB", "P")) @pytest.mark.parametrize("default_image", (True, False)) @pytest.mark.parametrize("duplicate", (True, False)) -def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path): +def test_different_modes_in_later_frames( + mode, default_image, duplicate, tmp_path: Path +) -> None: test_file = str(tmp_path / "temp.png") im = Image.new("L", (1, 1)) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 27ff7ab6640..3904d3bc5b5 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -12,7 +14,7 @@ ) -def test_load_blp1(): +def test_load_blp1() -> None: with Image.open("Tests/images/blp/blp1_jpeg.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") @@ -20,22 +22,22 @@ def test_load_blp1(): im.load() -def test_load_blp2_raw(): +def test_load_blp2_raw() -> None: with Image.open("Tests/images/blp/blp2_raw.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_raw.png") -def test_load_blp2_dxt1(): +def test_load_blp2_dxt1() -> None: with Image.open("Tests/images/blp/blp2_dxt1.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1.png") -def test_load_blp2_dxt1a(): +def test_load_blp2_dxt1a() -> None: with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: f = str(tmp_path / "temp.blp") for version in ("BLP1", "BLP2"): @@ -69,7 +71,7 @@ def test_save(tmp_path): "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", ], ) -def test_crashes(test_file): +def test_crashes(test_file) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 225fb28ba51..c36466e0269 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,6 +1,7 @@ from __future__ import annotations import io +from pathlib import Path import pytest @@ -14,8 +15,8 @@ ) -def test_sanity(tmp_path): - def roundtrip(im): +def test_sanity(tmp_path: Path) -> None: + def roundtrip(im) -> None: outfile = str(tmp_path / "temp.bmp") im.save(outfile, "BMP") @@ -35,20 +36,20 @@ def roundtrip(im): roundtrip(hopper("RGB")) -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): BmpImagePlugin.BmpImageFile(fp) -def test_fallback_if_mmap_errors(): +def test_fallback_if_mmap_errors() -> None: # This image has been truncated, # so that the buffer is not large enough when using mmap with Image.open("Tests/images/mmap_error.bmp") as im: assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp") -def test_save_to_bytes(): +def test_save_to_bytes() -> None: output = io.BytesIO() im = hopper() im.save(output, "BMP") @@ -60,7 +61,7 @@ def test_save_to_bytes(): assert reloaded.format == "BMP" -def test_small_palette(tmp_path): +def test_small_palette(tmp_path: Path) -> None: im = Image.new("P", (1, 1)) colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] im.putpalette(colors) @@ -72,7 +73,7 @@ def test_small_palette(tmp_path): assert reloaded.getpalette() == colors -def test_save_too_large(tmp_path): +def test_save_too_large(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.bmp") with Image.new("RGB", (1, 1)) as im: im._size = (37838, 37838) @@ -80,7 +81,7 @@ def test_save_too_large(tmp_path): im.save(outfile) -def test_dpi(): +def test_dpi() -> None: dpi = (72, 72) output = io.BytesIO() @@ -92,7 +93,7 @@ def test_dpi(): assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161) -def test_save_bmp_with_dpi(tmp_path): +def test_save_bmp_with_dpi(tmp_path: Path) -> None: # Test for #1301 # Arrange outfile = str(tmp_path / "temp.jpg") @@ -110,7 +111,7 @@ def test_save_bmp_with_dpi(tmp_path): assert reloaded.format == "JPEG" -def test_save_float_dpi(tmp_path): +def test_save_float_dpi(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.bmp") with Image.open("Tests/images/hopper.bmp") as im: im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) @@ -118,7 +119,7 @@ def test_save_float_dpi(tmp_path): assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306) -def test_load_dib(): +def test_load_dib() -> None: # test for #1293, Imagegrab returning Unsupported Bitfields Format with Image.open("Tests/images/clipboard.dib") as im: assert im.format == "DIB" @@ -127,7 +128,7 @@ def test_load_dib(): assert_image_equal_tofile(im, "Tests/images/clipboard_target.png") -def test_save_dib(tmp_path): +def test_save_dib(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.dib") with Image.open("Tests/images/clipboard.dib") as im: @@ -139,7 +140,7 @@ def test_save_dib(tmp_path): assert_image_equal(im, reloaded) -def test_rgba_bitfields(): +def test_rgba_bitfields() -> None: # This test image has been manually hexedited # to change the bitfield compression in the header from XBGR to RGBA with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: @@ -157,7 +158,7 @@ def test_rgba_bitfields(): ) -def test_rle8(): +def test_rle8() -> None: with Image.open("Tests/images/hopper_rle8.bmp") as im: assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) @@ -177,7 +178,7 @@ def test_rle8(): im.load() -def test_rle4(): +def test_rle4() -> None: with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im: assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12) @@ -193,7 +194,7 @@ def test_rle4(): ("Tests/images/bmp/g/pal8rle.bmp", 1064), ), ) -def test_rle8_eof(file_name, length): +def test_rle8_eof(file_name, length) -> None: with open(file_name, "rb") as fp: data = fp.read(length) with Image.open(io.BytesIO(data)) as im: @@ -201,7 +202,7 @@ def test_rle8_eof(file_name, length): im.load() -def test_offset(): +def test_offset() -> None: # This image has been hexedited # to exclude the palette size from the pixel data offset with Image.open("Tests/images/pal8_offset.bmp") as im: diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 45081832e68..3dd24533aae 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import BufrStubImagePlugin, Image @@ -9,7 +11,7 @@ TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -20,7 +22,7 @@ def test_open(): assert im.size == (1, 1) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -29,7 +31,7 @@ def test_invalid_file(): BufrStubImagePlugin.BufrStubImageFile(invalid_file) -def test_load(): +def test_load() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler @@ -37,7 +39,7 @@ def test_load(): im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: # Arrange im = hopper() tmpfile = str(tmp_path / "temp.bufr") @@ -47,13 +49,13 @@ def test_save(tmp_path): im.save(tmpfile) -def test_handler(tmp_path): +def test_handler(tmp_path: Path) -> None: class TestHandler: opened = False loaded = False saved = False - def open(self, im): + def open(self, im) -> None: self.opened = True def load(self, im): @@ -61,7 +63,7 @@ def load(self, im): im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename): + def save(self, im, fp, filename) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 95a5b2337d8..4dba4be5d5c 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -9,19 +9,19 @@ TEST_FILE = "Tests/images/dummy.container" -def test_sanity(): +def test_sanity() -> None: dir(Image) dir(ContainerIO) -def test_isatty(): +def test_isatty() -> None: with hopper() as im: container = ContainerIO.ContainerIO(im, 0, 0) assert container.isatty() is False -def test_seek_mode_0(): +def test_seek_mode_0() -> None: # Arrange mode = 0 with open(TEST_FILE, "rb") as fh: @@ -35,7 +35,7 @@ def test_seek_mode_0(): assert container.tell() == 33 -def test_seek_mode_1(): +def test_seek_mode_1() -> None: # Arrange mode = 1 with open(TEST_FILE, "rb") as fh: @@ -49,7 +49,7 @@ def test_seek_mode_1(): assert container.tell() == 66 -def test_seek_mode_2(): +def test_seek_mode_2() -> None: # Arrange mode = 2 with open(TEST_FILE, "rb") as fh: @@ -64,7 +64,7 @@ def test_seek_mode_2(): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n0(bytesmode): +def test_read_n0(bytesmode) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -80,7 +80,7 @@ def test_read_n0(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n(bytesmode): +def test_read_n(bytesmode) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -96,7 +96,7 @@ def test_read_n(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_eof(bytesmode): +def test_read_eof(bytesmode) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -112,7 +112,7 @@ def test_read_eof(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_readline(bytesmode): +def test_readline(bytesmode) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 0, 120) @@ -127,7 +127,7 @@ def test_readline(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_readlines(bytesmode): +def test_readlines(bytesmode) -> None: # Arrange expected = [ "This is line 1\n", diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index cba7c10bf76..65337cad9b1 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -12,7 +12,7 @@ TEST_FILE = "Tests/images/hopper.dcx" -def test_sanity(): +def test_sanity() -> None: # Arrange # Act @@ -25,8 +25,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_FILE) im.load() @@ -34,26 +34,26 @@ def open(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_FILE) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_FILE) as im: im.load() -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): DcxImagePlugin.DcxImageFile(fp) -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act @@ -63,13 +63,13 @@ def test_tell(): assert frame == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_FILE) as im: assert im.n_frames == 1 assert not im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(TEST_FILE) as im: n_frames = im.n_frames @@ -82,7 +82,7 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_seek_too_far(): +def test_seek_too_far() -> None: # Arrange with Image.open(TEST_FILE) as im: frame = 999 # too big on purpose diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 7064b74c07b..09ee8986aca 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -2,6 +2,7 @@ from __future__ import annotations from io import BytesIO +from pathlib import Path import pytest @@ -46,7 +47,7 @@ TEST_FILE_DX10_BC1_TYPELESS, ), ) -def test_sanity_dxt1_bc1(image_path): +def test_sanity_dxt1_bc1(image_path) -> None: """Check DXT1 and BC1 images can be opened""" with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: target = target.convert("RGBA") @@ -60,7 +61,7 @@ def test_sanity_dxt1_bc1(image_path): assert_image_equal(im, target) -def test_sanity_dxt3(): +def test_sanity_dxt3() -> None: """Check DXT3 images can be opened""" with Image.open(TEST_FILE_DXT3) as im: @@ -73,7 +74,7 @@ def test_sanity_dxt3(): assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png")) -def test_sanity_dxt5(): +def test_sanity_dxt5() -> None: """Check DXT5 images can be opened""" with Image.open(TEST_FILE_DXT5) as im: @@ -94,7 +95,7 @@ def test_sanity_dxt5(): TEST_FILE_BC4U, ), ) -def test_sanity_ati1_bc4u(image_path): +def test_sanity_ati1_bc4u(image_path) -> None: """Check ATI1 and BC4U images can be opened""" with Image.open(image_path) as im: @@ -115,7 +116,7 @@ def test_sanity_ati1_bc4u(image_path): TEST_FILE_DX10_BC4_TYPELESS, ), ) -def test_dx10_bc4(image_path): +def test_dx10_bc4(image_path) -> None: """Check DX10 BC4 images can be opened""" with Image.open(image_path) as im: @@ -136,7 +137,7 @@ def test_dx10_bc4(image_path): TEST_FILE_BC5U, ), ) -def test_sanity_ati2_bc5u(image_path): +def test_sanity_ati2_bc5u(image_path) -> None: """Check ATI2 and BC5U images can be opened""" with Image.open(image_path) as im: @@ -160,7 +161,7 @@ def test_sanity_ati2_bc5u(image_path): (TEST_FILE_BC5S, TEST_FILE_BC5S), ), ) -def test_dx10_bc5(image_path, expected_path): +def test_dx10_bc5(image_path, expected_path) -> None: """Check DX10 BC5 images can be opened""" with Image.open(image_path) as im: @@ -174,7 +175,7 @@ def test_dx10_bc5(image_path, expected_path): @pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS)) -def test_dx10_bc6h(image_path): +def test_dx10_bc6h(image_path) -> None: """Check DX10 BC6H/BC6HS images can be opened""" with Image.open(image_path) as im: @@ -187,7 +188,7 @@ def test_dx10_bc6h(image_path): assert_image_equal_tofile(im, image_path.replace(".dds", ".png")) -def test_dx10_bc7(): +def test_dx10_bc7() -> None: """Check DX10 images can be opened""" with Image.open(TEST_FILE_DX10_BC7) as im: @@ -200,7 +201,7 @@ def test_dx10_bc7(): assert_image_equal_tofile(im, TEST_FILE_DX10_BC7.replace(".dds", ".png")) -def test_dx10_bc7_unorm_srgb(): +def test_dx10_bc7_unorm_srgb() -> None: """Check DX10 unsigned normalized integer images can be opened""" with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) as im: @@ -216,7 +217,7 @@ def test_dx10_bc7_unorm_srgb(): ) -def test_dx10_r8g8b8a8(): +def test_dx10_r8g8b8a8() -> None: """Check DX10 images can be opened""" with Image.open(TEST_FILE_DX10_R8G8B8A8) as im: @@ -229,7 +230,7 @@ def test_dx10_r8g8b8a8(): assert_image_equal_tofile(im, TEST_FILE_DX10_R8G8B8A8.replace(".dds", ".png")) -def test_dx10_r8g8b8a8_unorm_srgb(): +def test_dx10_r8g8b8a8_unorm_srgb() -> None: """Check DX10 unsigned normalized integer images can be opened""" with Image.open(TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB) as im: @@ -255,7 +256,7 @@ def test_dx10_r8g8b8a8_unorm_srgb(): ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA), ], ) -def test_uncompressed(mode, size, test_file): +def test_uncompressed(mode, size, test_file) -> None: """Check uncompressed images can be opened""" with Image.open(test_file) as im: @@ -266,7 +267,7 @@ def test_uncompressed(mode, size, test_file): assert_image_equal_tofile(im, test_file.replace(".dds", ".png")) -def test__accept_true(): +def test__accept_true() -> None: """Check valid prefix""" # Arrange prefix = b"DDS etc" @@ -278,7 +279,7 @@ def test__accept_true(): assert output -def test__accept_false(): +def test__accept_false() -> None: """Check invalid prefix""" # Arrange prefix = b"something invalid" @@ -290,19 +291,19 @@ def test__accept_false(): assert not output -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): DdsImagePlugin.DdsImageFile(invalid_file) -def test_short_header(): +def test_short_header() -> None: """Check a short header""" with open(TEST_FILE_DXT5, "rb") as f: img_file = f.read() - def short_header(): + def short_header() -> None: with Image.open(BytesIO(img_file[:119])): pass # pragma: no cover @@ -310,13 +311,13 @@ def short_header(): short_header() -def test_short_file(): +def test_short_file() -> None: """Check that the appropriate error is thrown for a short file""" with open(TEST_FILE_DXT5, "rb") as f: img_file = f.read() - def short_file(): + def short_file() -> None: with Image.open(BytesIO(img_file[:-100])) as im: im.load() @@ -324,7 +325,7 @@ def short_file(): short_file() -def test_dxt5_colorblock_alpha_issue_4142(): +def test_dxt5_colorblock_alpha_issue_4142() -> None: """Check that colorblocks are decoded correctly in DXT5""" with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: @@ -339,12 +340,12 @@ def test_dxt5_colorblock_alpha_issue_4142(): assert px[2] != 0 -def test_palette(): +def test_palette() -> None: with Image.open("Tests/images/palette.dds") as im: assert_image_equal_tofile(im, "Tests/images/transparent.gif") -def test_unsupported_bitcount(): +def test_unsupported_bitcount() -> None: with pytest.raises(OSError): with Image.open("Tests/images/unsupported_bitcount.dds"): pass @@ -357,13 +358,13 @@ def test_unsupported_bitcount(): "Tests/images/unimplemented_pfflags.dds", ), ) -def test_not_implemented(test_file): +def test_not_implemented(test_file) -> None: with pytest.raises(NotImplementedError): with Image.open(test_file): pass -def test_save_unsupported_mode(tmp_path): +def test_save_unsupported_mode(tmp_path: Path) -> None: out = str(tmp_path / "temp.dds") im = hopper("HSV") with pytest.raises(OSError): @@ -379,7 +380,7 @@ def test_save_unsupported_mode(tmp_path): ("RGBA", "Tests/images/pil123rgba.png"), ], ) -def test_save(mode, test_file, tmp_path): +def test_save(mode, test_file, tmp_path: Path) -> None: out = str(tmp_path / "temp.dds") with Image.open(test_file) as im: assert im.mode == mode diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 8def9a43511..06f927c7bb2 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,6 +1,7 @@ from __future__ import annotations import io +from pathlib import Path import pytest @@ -83,7 +84,7 @@ ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) ) @pytest.mark.parametrize("scale", (1, 2)) -def test_sanity(filename, size, scale): +def test_sanity(filename, size, scale) -> None: expected_size = tuple(s * scale for s in size) with Image.open(filename) as image: image.load(scale=scale) @@ -93,7 +94,7 @@ def test_sanity(filename, size, scale): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_load(): +def test_load() -> None: with Image.open(FILE1) as im: assert im.load()[0, 0] == (255, 255, 255) @@ -101,7 +102,7 @@ def test_load(): assert im.load()[0, 0] == (255, 255, 255) -def test_binary(): +def test_binary() -> None: if HAS_GHOSTSCRIPT: assert EpsImagePlugin.gs_binary is not None else: @@ -115,41 +116,41 @@ def test_binary(): assert EpsImagePlugin.gs_windows_binary is not None -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(invalid_file) -def test_binary_header_only(): +def test_binary_header_only() -> None: data = io.BytesIO(simple_binary_header) with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_version_comment(prefix): +def test_missing_version_comment(prefix) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_boundingbox_comment(prefix): +def test_missing_boundingbox_comment(prefix) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox)) with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment(prefix): +def test_invalid_boundingbox_comment(prefix) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) with pytest.raises(OSError, match="cannot determine EPS bounding box"): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix): +def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix) -> None: data = io.BytesIO( prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) ) @@ -160,21 +161,21 @@ def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix): @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_ascii_comment_too_long(prefix): +def test_ascii_comment_too_long(prefix) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) with pytest.raises(SyntaxError, match="not an EPS file"): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_long_binary_data(prefix): +def test_long_binary_data(prefix) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) EpsImagePlugin.EpsImageFile(data) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_load_long_binary_data(prefix): +def test_load_long_binary_data(prefix) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) with Image.open(data) as img: img.load() @@ -187,7 +188,7 @@ def test_load_long_binary_data(prefix): pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_cmyk(): +def test_cmyk() -> None: with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: assert cmyk_image.mode == "CMYK" assert cmyk_image.size == (100, 100) @@ -203,7 +204,7 @@ def test_cmyk(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_showpage(): +def test_showpage() -> None: # See https://github.com/python-pillow/Pillow/issues/2615 with Image.open("Tests/images/reqd_showpage.eps") as plot_image: with Image.open("Tests/images/reqd_showpage.png") as target: @@ -214,7 +215,7 @@ def test_showpage(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_transparency(): +def test_transparency() -> None: with Image.open("Tests/images/reqd_showpage.eps") as plot_image: plot_image.load(transparency=True) assert plot_image.mode == "RGBA" @@ -225,7 +226,7 @@ def test_transparency(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_file_object(tmp_path): +def test_file_object(tmp_path: Path) -> None: # issue 479 with Image.open(FILE1) as image1: with open(str(tmp_path / "temp.eps"), "wb") as fh: @@ -233,7 +234,7 @@ def test_file_object(tmp_path): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_bytesio_object(): +def test_bytesio_object() -> None: with open(FILE1, "rb") as f: img_bytes = io.BytesIO(f.read()) @@ -246,12 +247,12 @@ def test_bytesio_object(): assert_image_similar(img, image1_scale1_compare, 5) -def test_1_mode(): +def test_1_mode() -> None: with Image.open("Tests/images/1.eps") as im: assert im.mode == "1" -def test_image_mode_not_supported(tmp_path): +def test_image_mode_not_supported(tmp_path: Path) -> None: im = hopper("RGBA") tmpfile = str(tmp_path / "temp.eps") with pytest.raises(ValueError): @@ -260,7 +261,7 @@ def test_image_mode_not_supported(tmp_path): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @skip_unless_feature("zlib") -def test_render_scale1(): +def test_render_scale1() -> None: # We need png support for these render test # Zero bounding box @@ -282,7 +283,7 @@ def test_render_scale1(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @skip_unless_feature("zlib") -def test_render_scale2(): +def test_render_scale2() -> None: # We need png support for these render test # Zero bounding box @@ -304,7 +305,7 @@ def test_render_scale2(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) -def test_resize(filename): +def test_resize(filename) -> None: with Image.open(filename) as im: new_size = (100, 100) im = im.resize(new_size) @@ -313,7 +314,7 @@ def test_resize(filename): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("filename", (FILE1, FILE2)) -def test_thumbnail(filename): +def test_thumbnail(filename) -> None: # Issue #619 with Image.open(filename) as im: new_size = (100, 100) @@ -321,20 +322,20 @@ def test_thumbnail(filename): assert max(im.size) == max(new_size) -def test_read_binary_preview(): +def test_read_binary_preview() -> None: # Issue 302 # open image with binary preview with Image.open(FILE3): pass -def test_readline_psfile(tmp_path): +def test_readline_psfile(tmp_path: Path) -> None: # check all the freaking line endings possible from the spec # test_string = u'something\r\nelse\n\rbaz\rbif\n' line_endings = ["\r\n", "\n", "\n\r", "\r"] strings = ["something", "else", "baz", "bif"] - def _test_readline(t, ending): + def _test_readline(t, ending) -> None: ending = "Failure with line ending: %s" % ( "".join("%s" % ord(s) for s in ending) ) @@ -343,13 +344,13 @@ def _test_readline(t, ending): assert t.readline().strip("\r\n") == "baz", ending assert t.readline().strip("\r\n") == "bif", ending - def _test_readline_io_psfile(test_string, ending): + def _test_readline_io_psfile(test_string, ending) -> None: f = io.BytesIO(test_string.encode("latin-1")) with pytest.warns(DeprecationWarning): t = EpsImagePlugin.PSFile(f) _test_readline(t, ending) - def _test_readline_file_psfile(test_string, ending): + def _test_readline_file_psfile(test_string, ending) -> None: f = str(tmp_path / "temp.txt") with open(f, "wb") as w: w.write(test_string.encode("latin-1")) @@ -365,7 +366,7 @@ def _test_readline_file_psfile(test_string, ending): _test_readline_file_psfile(s, ending) -def test_psfile_deprecation(): +def test_psfile_deprecation() -> None: with pytest.warns(DeprecationWarning): EpsImagePlugin.PSFile(None) @@ -375,7 +376,7 @@ def test_psfile_deprecation(): "line_ending", (b"\r\n", b"\n", b"\n\r", b"\r"), ) -def test_readline(prefix, line_ending): +def test_readline(prefix, line_ending) -> None: simple_file = prefix + line_ending.join(simple_eps_file_with_comments) data = io.BytesIO(simple_file) test_file = EpsImagePlugin.EpsImageFile(data) @@ -393,14 +394,14 @@ def test_readline(prefix, line_ending): "Tests/images/illuCS6_preview.eps", ), ) -def test_open_eps(filename): +def test_open_eps(filename) -> None: # https://github.com/python-pillow/Pillow/issues/1104 with Image.open(filename) as img: assert img.mode == "RGB" @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_emptyline(): +def test_emptyline() -> None: # Test file includes an empty line in the header data emptyline_file = "Tests/images/zero_bb_emptyline.eps" @@ -416,14 +417,14 @@ def test_emptyline(): "test_file", ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], ) -def test_timeout(test_file): +def test_timeout(test_file) -> None: with open(test_file, "rb") as f: with pytest.raises(Image.UnidentifiedImageError): with Image.open(f): pass -def test_bounding_box_in_trailer(): +def test_bounding_box_in_trailer() -> None: # Check bounding boxes are parsed in the same way # when specified in the header and the trailer with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open( @@ -432,7 +433,7 @@ def test_bounding_box_in_trailer(): assert trailer_image.size == header_image.size -def test_eof_before_bounding_box(): +def test_eof_before_bounding_box() -> None: with pytest.raises(OSError): with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"): pass diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 7444eb673cb..cce0b05cda8 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -11,7 +11,7 @@ TEST_FILE = "Tests/images/hopper.fits" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -22,7 +22,7 @@ def test_open(): assert_image_equal(im, hopper("L")) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -31,14 +31,14 @@ def test_invalid_file(): FitsImagePlugin.FitsImageFile(invalid_file) -def test_truncated_fits(): +def test_truncated_fits() -> None: # No END to headers image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE" with pytest.raises(OSError): FitsImagePlugin.FitsImageFile(BytesIO(image_data)) -def test_naxis_zero(): +def test_naxis_zero() -> None: # This test image has been manually hexedited # to set the number of data axes to zero with pytest.raises(ValueError): @@ -46,7 +46,7 @@ def test_naxis_zero(): pass -def test_comment(): +def test_comment() -> None: image_data = b"SIMPLE = T / comment string" with pytest.raises(OSError): FitsImagePlugin.FitsImageFile(BytesIO(image_data)) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 00377e0c92d..a673d4af851 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -16,7 +16,7 @@ animated_test_file = "Tests/images/a.fli" -def test_sanity(): +def test_sanity() -> None: with Image.open(static_test_file) as im: im.load() assert im.mode == "P" @@ -33,8 +33,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(static_test_file) im.load() @@ -42,14 +42,14 @@ def open(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(static_test_file) im.load() im.close() -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open(animated_test_file) im.seek(1) im.close() @@ -58,13 +58,13 @@ def test_seek_after_close(): im.seek(0) -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(static_test_file) as im: im.load() -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(static_test_file) as im: # Act @@ -74,20 +74,20 @@ def test_tell(): assert frame == 0 -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): FliImagePlugin.FliImageFile(invalid_file) -def test_palette_chunk_second(): +def test_palette_chunk_second() -> None: with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im: with Image.open(static_test_file) as expected: assert_image_equal(im.convert("RGB"), expected.convert("RGB")) -def test_n_frames(): +def test_n_frames() -> None: with Image.open(static_test_file) as im: assert im.n_frames == 1 assert not im.is_animated @@ -97,7 +97,7 @@ def test_n_frames(): assert im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(animated_test_file) as im: n_frames = im.n_frames @@ -110,7 +110,7 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_seek_tell(): +def test_seek_tell() -> None: with Image.open(animated_test_file) as im: layer_number = im.tell() assert layer_number == 0 @@ -132,7 +132,7 @@ def test_seek_tell(): assert layer_number == 1 -def test_seek(): +def test_seek() -> None: with Image.open(animated_test_file) as im: im.seek(50) @@ -147,7 +147,7 @@ def test_seek(): ], ) @pytest.mark.timeout(timeout=3) -def test_timeouts(test_file): +def test_timeouts(test_file) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): @@ -160,7 +160,7 @@ def test_timeouts(test_file): "Tests/images/crash-5762152299364352.fli", ], ) -def test_crash(test_file): +def test_crash(test_file) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index d710070c01b..e32f30a0123 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -11,7 +11,7 @@ ) -def test_sanity(): +def test_sanity() -> None: with Image.open("Tests/images/input_bw_one_band.fpx") as im: assert im.mode == "L" assert im.size == (70, 46) @@ -20,7 +20,7 @@ def test_sanity(): assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") -def test_close(): +def test_close() -> None: with Image.open("Tests/images/input_bw_one_band.fpx") as im: pass assert im.ole.fp.closed @@ -30,7 +30,7 @@ def test_close(): assert im.ole.fp.closed -def test_invalid_file(): +def test_invalid_file() -> None: # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): @@ -42,7 +42,7 @@ def test_invalid_file(): FpxImagePlugin.FpxImageFile(ole_file) -def test_fpx_invalid_number_of_bands(): +def test_fpx_invalid_number_of_bands() -> None: with pytest.raises(OSError, match="Invalid number of bands"): with Image.open("Tests/images/input_bw_five_bands.fpx"): pass diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 3e19940aab9..3f550fd1109 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -2,6 +2,7 @@ import warnings from io import BytesIO +from pathlib import Path import pytest @@ -23,7 +24,7 @@ data = f.read() -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_GIF) as im: im.load() assert im.mode == "P" @@ -33,8 +34,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_GIF) im.load() @@ -42,14 +43,14 @@ def open(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_GIF) im.load() im.close() -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open("Tests/images/iss634.gif") im.load() im.close() @@ -62,20 +63,20 @@ def test_seek_after_close(): im.seek(1) -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_GIF) as im: im.load() -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): GifImagePlugin.GifImageFile(invalid_file) -def test_l_mode_transparency(): +def test_l_mode_transparency() -> None: with Image.open("Tests/images/no_palette_with_transparency.gif") as im: assert im.mode == "L" assert im.load()[0, 0] == 128 @@ -86,7 +87,7 @@ def test_l_mode_transparency(): assert im.load()[0, 0] == 128 -def test_l_mode_after_rgb(): +def test_l_mode_after_rgb() -> None: with Image.open("Tests/images/no_palette_after_rgb.gif") as im: im.seek(1) assert im.mode == "RGB" @@ -95,13 +96,13 @@ def test_l_mode_after_rgb(): assert im.mode == "RGB" -def test_palette_not_needed_for_second_frame(): +def test_palette_not_needed_for_second_frame() -> None: with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im: im.seek(1) assert_image_similar(im, hopper("L").convert("RGB"), 8) -def test_strategy(): +def test_strategy() -> None: with Image.open("Tests/images/iss634.gif") as im: expected_rgb_always = im.convert("RGB") @@ -142,7 +143,7 @@ def test_strategy(): GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST -def test_optimize(): +def test_optimize() -> None: def test_grayscale(optimize): im = Image.new("L", (1, 1), 0) filename = BytesIO() @@ -177,7 +178,7 @@ def test_bilevel(optimize): (4, 513, 256), ), ) -def test_optimize_correctness(colors, size, expected_palette_length): +def test_optimize_correctness(colors, size, expected_palette_length) -> None: # 256 color Palette image, posterize to > 128 and < 128 levels. # Size bigger and smaller than 512x512. # Check the palette for number of colors allocated. @@ -199,14 +200,14 @@ def test_optimize_correctness(colors, size, expected_palette_length): assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) -def test_optimize_full_l(): +def test_optimize_full_l() -> None: im = Image.frombytes("L", (16, 16), bytes(range(256))) test_file = BytesIO() im.save(test_file, "GIF", optimize=True) assert im.mode == "L" -def test_optimize_if_palette_can_be_reduced_by_half(): +def test_optimize_if_palette_can_be_reduced_by_half() -> None: im = Image.new("P", (8, 1)) im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150)) for i in range(8): @@ -219,7 +220,7 @@ def test_optimize_if_palette_can_be_reduced_by_half(): assert len(reloaded.palette.palette) // 3 == colors -def test_full_palette_second_frame(tmp_path): +def test_full_palette_second_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("P", (1, 256)) @@ -240,7 +241,7 @@ def test_full_palette_second_frame(tmp_path): reloaded.getpixel((0, i)) == i -def test_roundtrip(tmp_path): +def test_roundtrip(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = hopper() im.save(out) @@ -248,7 +249,7 @@ def test_roundtrip(tmp_path): assert_image_similar(reread.convert("RGB"), im, 50) -def test_roundtrip2(tmp_path): +def test_roundtrip2(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/403 out = str(tmp_path / "temp.gif") with Image.open(TEST_GIF) as im: @@ -258,7 +259,7 @@ def test_roundtrip2(tmp_path): assert_image_similar(reread.convert("RGB"), hopper(), 50) -def test_roundtrip_save_all(tmp_path): +def test_roundtrip_save_all(tmp_path: Path) -> None: # Single frame image out = str(tmp_path / "temp.gif") im = hopper() @@ -275,7 +276,7 @@ def test_roundtrip_save_all(tmp_path): assert reread.n_frames == 5 -def test_roundtrip_save_all_1(tmp_path): +def test_roundtrip_save_all_1(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("1", (1, 1)) im2 = Image.new("1", (1, 1), 1) @@ -296,7 +297,7 @@ def test_roundtrip_save_all_1(tmp_path): ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"), ), ) -def test_loading_multiple_palettes(path, mode): +def test_loading_multiple_palettes(path, mode) -> None: with Image.open(path) as im: assert im.mode == "P" first_frame_colors = im.palette.colors.keys() @@ -314,7 +315,7 @@ def test_loading_multiple_palettes(path, mode): assert im.load()[24, 24] not in first_frame_colors -def test_headers_saving_for_animated_gifs(tmp_path): +def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: important_headers = ["background", "version", "duration", "loop"] # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: @@ -327,7 +328,7 @@ def test_headers_saving_for_animated_gifs(tmp_path): assert info[header] == reread.info[header] -def test_palette_handling(tmp_path): +def test_palette_handling(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/513 with Image.open(TEST_GIF) as im: @@ -343,7 +344,7 @@ def test_palette_handling(tmp_path): assert_image_similar(im, reloaded.convert("RGB"), 10) -def test_palette_434(tmp_path): +def test_palette_434(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/434 def roundtrip(im, *args, **kwargs): @@ -368,7 +369,7 @@ def roundtrip(im, *args, **kwargs): @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") -def test_save_netpbm_bmp_mode(tmp_path): +def test_save_netpbm_bmp_mode(tmp_path: Path) -> None: with Image.open(TEST_GIF) as img: img = img.convert("RGB") @@ -379,7 +380,7 @@ def test_save_netpbm_bmp_mode(tmp_path): @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") -def test_save_netpbm_l_mode(tmp_path): +def test_save_netpbm_l_mode(tmp_path: Path) -> None: with Image.open(TEST_GIF) as img: img = img.convert("L") @@ -389,7 +390,7 @@ def test_save_netpbm_l_mode(tmp_path): assert_image_similar(img, reloaded.convert("L"), 0) -def test_seek(): +def test_seek() -> None: with Image.open("Tests/images/dispose_none.gif") as img: frame_count = 0 try: @@ -400,7 +401,7 @@ def test_seek(): assert frame_count == 5 -def test_seek_info(): +def test_seek_info() -> None: with Image.open("Tests/images/iss634.gif") as im: info = im.info.copy() @@ -410,7 +411,7 @@ def test_seek_info(): assert im.info == info -def test_seek_rewind(): +def test_seek_rewind() -> None: with Image.open("Tests/images/iss634.gif") as im: im.seek(2) im.seek(1) @@ -428,7 +429,7 @@ def test_seek_rewind(): ("Tests/images/iss634.gif", 42), ), ) -def test_n_frames(path, n_frames): +def test_n_frames(path, n_frames) -> None: # Test is_animated before n_frames with Image.open(path) as im: assert im.is_animated == (n_frames != 1) @@ -439,7 +440,7 @@ def test_n_frames(path, n_frames): assert im.is_animated == (n_frames != 1) -def test_no_change(): +def test_no_change() -> None: # Test n_frames does not change the image with Image.open("Tests/images/dispose_bgnd.gif") as im: im.seek(1) @@ -460,7 +461,7 @@ def test_no_change(): assert_image_equal(im, expected) -def test_eoferror(): +def test_eoferror() -> None: with Image.open(TEST_GIF) as im: n_frames = im.n_frames @@ -473,13 +474,13 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_first_frame_transparency(): +def test_first_frame_transparency() -> None: with Image.open("Tests/images/first_frame_transparency.gif") as im: px = im.load() assert px[0, 0] == im.info["transparency"] -def test_dispose_none(): +def test_dispose_none() -> None: with Image.open("Tests/images/dispose_none.gif") as img: try: while True: @@ -489,7 +490,7 @@ def test_dispose_none(): pass -def test_dispose_none_load_end(): +def test_dispose_none_load_end() -> None: # Test image created with: # # im = Image.open("transparent.gif") @@ -502,7 +503,7 @@ def test_dispose_none_load_end(): assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png") -def test_dispose_background(): +def test_dispose_background() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as img: try: while True: @@ -512,7 +513,7 @@ def test_dispose_background(): pass -def test_dispose_background_transparency(): +def test_dispose_background_transparency() -> None: with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img: img.seek(2) px = img.load() @@ -540,7 +541,7 @@ def test_dispose_background_transparency(): ), ), ) -def test_transparent_dispose(loading_strategy, expected_colors): +def test_transparent_dispose(loading_strategy, expected_colors) -> None: GifImagePlugin.LOADING_STRATEGY = loading_strategy try: with Image.open("Tests/images/transparent_dispose.gif") as img: @@ -553,7 +554,7 @@ def test_transparent_dispose(loading_strategy, expected_colors): GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST -def test_dispose_previous(): +def test_dispose_previous() -> None: with Image.open("Tests/images/dispose_prev.gif") as img: try: while True: @@ -563,7 +564,7 @@ def test_dispose_previous(): pass -def test_dispose_previous_first_frame(): +def test_dispose_previous_first_frame() -> None: with Image.open("Tests/images/dispose_prev_first_frame.gif") as im: im.seek(1) assert_image_equal_tofile( @@ -571,7 +572,7 @@ def test_dispose_previous_first_frame(): ) -def test_previous_frame_loaded(): +def test_previous_frame_loaded() -> None: with Image.open("Tests/images/dispose_none.gif") as img: img.load() img.seek(1) @@ -582,7 +583,7 @@ def test_previous_frame_loaded(): assert_image_equal(img_skipped, img) -def test_save_dispose(tmp_path): +def test_save_dispose(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im_list = [ Image.new("L", (100, 100), "#000"), @@ -610,7 +611,7 @@ def test_save_dispose(tmp_path): assert img.disposal_method == i + 1 -def test_dispose2_palette(tmp_path): +def test_dispose2_palette(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Four colors: white, gray, black, red @@ -641,7 +642,7 @@ def test_dispose2_palette(tmp_path): assert rgb_img.getpixel((50, 50)) == circle -def test_dispose2_diff(tmp_path): +def test_dispose2_diff(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # 4 frames: red/blue, red/red, blue/blue, red/blue @@ -683,7 +684,7 @@ def test_dispose2_diff(tmp_path): assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0) -def test_dispose2_background(tmp_path): +def test_dispose2_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im_list = [] @@ -709,7 +710,7 @@ def test_dispose2_background(tmp_path): assert im.getpixel((0, 0)) == (255, 0, 0) -def test_dispose2_background_frame(tmp_path): +def test_dispose2_background_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im_list = [Image.new("RGBA", (1, 20))] @@ -727,7 +728,7 @@ def test_dispose2_background_frame(tmp_path): assert im.n_frames == 3 -def test_transparency_in_second_frame(tmp_path): +def test_transparency_in_second_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/different_transparency.gif") as im: assert im.info["transparency"] == 0 @@ -747,7 +748,7 @@ def test_transparency_in_second_frame(tmp_path): ) -def test_no_transparency_in_second_frame(): +def test_no_transparency_in_second_frame() -> None: with Image.open("Tests/images/iss634.gif") as img: # Seek to the second frame img.seek(img.tell() + 1) @@ -757,7 +758,7 @@ def test_no_transparency_in_second_frame(): assert img.histogram()[255] == 0 -def test_remapped_transparency(tmp_path): +def test_remapped_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("P", (1, 2)) @@ -773,7 +774,7 @@ def test_remapped_transparency(tmp_path): assert reloaded.info["transparency"] == reloaded.getpixel((0, 1)) -def test_duration(tmp_path): +def test_duration(tmp_path: Path) -> None: duration = 1000 out = str(tmp_path / "temp.gif") @@ -787,7 +788,7 @@ def test_duration(tmp_path): assert reread.info["duration"] == duration -def test_multiple_duration(tmp_path): +def test_multiple_duration(tmp_path: Path) -> None: duration_list = [1000, 2000, 3000] out = str(tmp_path / "temp.gif") @@ -822,7 +823,7 @@ def test_multiple_duration(tmp_path): pass -def test_roundtrip_info_duration(tmp_path): +def test_roundtrip_info_duration(tmp_path: Path) -> None: duration_list = [100, 500, 500] out = str(tmp_path / "temp.gif") @@ -839,7 +840,7 @@ def test_roundtrip_info_duration(tmp_path): ] == duration_list -def test_roundtrip_info_duration_combined(tmp_path): +def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/duplicate_frame.gif") as im: assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ @@ -855,7 +856,7 @@ def test_roundtrip_info_duration_combined(tmp_path): ] == [1000, 2000] -def test_identical_frames(tmp_path): +def test_identical_frames(tmp_path: Path) -> None: duration_list = [1000, 1500, 2000, 4000] out = str(tmp_path / "temp.gif") @@ -888,7 +889,7 @@ def test_identical_frames(tmp_path): 1500, ), ) -def test_identical_frames_to_single_frame(duration, tmp_path): +def test_identical_frames_to_single_frame(duration, tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im_list = [ Image.new("L", (100, 100), "#000"), @@ -905,7 +906,7 @@ def test_identical_frames_to_single_frame(duration, tmp_path): assert reread.info["duration"] == 4500 -def test_loop_none(tmp_path): +def test_loop_none(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("L", (100, 100), "#000") im.save(out, loop=None) @@ -913,7 +914,7 @@ def test_loop_none(tmp_path): assert "loop" not in reread.info -def test_number_of_loops(tmp_path): +def test_number_of_loops(tmp_path: Path) -> None: number_of_loops = 2 out = str(tmp_path / "temp.gif") @@ -931,7 +932,7 @@ def test_number_of_loops(tmp_path): assert im.info["loop"] == 2 -def test_background(tmp_path): +def test_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("L", (100, 100), "#000") im.info["background"] = 1 @@ -940,7 +941,7 @@ def test_background(tmp_path): assert reread.info["background"] == im.info["background"] -def test_webp_background(tmp_path): +def test_webp_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Test opaque WebP background @@ -955,7 +956,7 @@ def test_webp_background(tmp_path): im.save(out) -def test_comment(tmp_path): +def test_comment(tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" @@ -975,7 +976,7 @@ def test_comment(tmp_path): assert reread.info["version"] == b"GIF89a" -def test_comment_over_255(tmp_path): +def test_comment_over_255(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("L", (100, 100), "#000") comment = b"Test comment text" @@ -990,18 +991,18 @@ def test_comment_over_255(tmp_path): assert reread.info["version"] == b"GIF89a" -def test_zero_comment_subblocks(): +def test_zero_comment_subblocks() -> None: with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: assert_image_equal_tofile(im, TEST_GIF) -def test_read_multiple_comment_blocks(): +def test_read_multiple_comment_blocks() -> None: with Image.open("Tests/images/multiple_comments.gif") as im: # Multiple comment blocks in a frame are separated not concatenated assert im.info["comment"] == b"Test comment 1\nTest comment 2" -def test_empty_string_comment(tmp_path): +def test_empty_string_comment(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/chi.gif") as im: assert "comment" in im.info @@ -1014,7 +1015,7 @@ def test_empty_string_comment(tmp_path): assert "comment" not in frame.info -def test_retain_comment_in_subsequent_frames(tmp_path): +def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: # Test that a comment block at the beginning is kept with Image.open("Tests/images/chi.gif") as im: for frame in ImageSequence.Iterator(im): @@ -1045,10 +1046,10 @@ def test_retain_comment_in_subsequent_frames(tmp_path): assert frame.info["comment"] == b"Test" -def test_version(tmp_path): +def test_version(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") - def assert_version_after_save(im, version): + def assert_version_after_save(im, version) -> None: im.save(out) with Image.open(out) as reread: assert reread.info["version"] == version @@ -1075,7 +1076,7 @@ def assert_version_after_save(im, version): assert_version_after_save(im, b"GIF87a") -def test_append_images(tmp_path): +def test_append_images(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Test appending single frame images @@ -1104,7 +1105,7 @@ def im_generator(ims): assert reread.n_frames == 10 -def test_transparent_optimize(tmp_path): +def test_transparent_optimize(tmp_path: Path) -> None: # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses # transparency. # Need a palette that isn't using the 0 color, @@ -1124,7 +1125,7 @@ def test_transparent_optimize(tmp_path): assert reloaded.info["transparency"] == reloaded.getpixel((252, 0)) -def test_removed_transparency(tmp_path): +def test_removed_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("RGB", (256, 1)) @@ -1139,7 +1140,7 @@ def test_removed_transparency(tmp_path): assert "transparency" not in reloaded.info -def test_rgb_transparency(tmp_path): +def test_rgb_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Single frame @@ -1161,7 +1162,7 @@ def test_rgb_transparency(tmp_path): assert "transparency" not in reloaded.info -def test_rgba_transparency(tmp_path): +def test_rgba_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = hopper("P") @@ -1172,13 +1173,13 @@ def test_rgba_transparency(tmp_path): assert_image_equal(hopper("P").convert("RGB"), reloaded) -def test_background_outside_palettte(tmp_path): +def test_background_outside_palettte(tmp_path: Path) -> None: with Image.open("Tests/images/background_outside_palette.gif") as im: im.seek(1) assert im.info["background"] == 255 -def test_bbox(tmp_path): +def test_bbox(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("RGB", (100, 100), "#fff") @@ -1189,7 +1190,7 @@ def test_bbox(tmp_path): assert reread.n_frames == 2 -def test_bbox_alpha(tmp_path): +def test_bbox_alpha(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) @@ -1201,7 +1202,7 @@ def test_bbox_alpha(tmp_path): assert reread.n_frames == 2 -def test_palette_save_L(tmp_path): +def test_palette_save_L(tmp_path: Path) -> None: # Generate an L mode image with a separate palette im = hopper("P") @@ -1215,7 +1216,7 @@ def test_palette_save_L(tmp_path): assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) -def test_palette_save_P(tmp_path): +def test_palette_save_P(tmp_path: Path) -> None: im = Image.new("P", (1, 2)) im.putpixel((0, 1), 1) @@ -1229,7 +1230,7 @@ def test_palette_save_P(tmp_path): assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6) -def test_palette_save_duplicate_entries(tmp_path): +def test_palette_save_duplicate_entries(tmp_path: Path) -> None: im = Image.new("P", (1, 2)) im.putpixel((0, 1), 1) @@ -1242,7 +1243,7 @@ def test_palette_save_duplicate_entries(tmp_path): assert reloaded.convert("RGB").getpixel((0, 1)) == (0, 0, 0) -def test_palette_save_all_P(tmp_path): +def test_palette_save_all_P(tmp_path: Path) -> None: frames = [] colors = ((255, 0, 0), (0, 255, 0)) for color in colors: @@ -1265,7 +1266,7 @@ def test_palette_save_all_P(tmp_path): assert im.palette.palette == im.global_palette.palette -def test_palette_save_ImagePalette(tmp_path): +def test_palette_save_ImagePalette(tmp_path: Path) -> None: # Pass in a different palette, as an ImagePalette.ImagePalette # effectively the same as test_palette_save_P @@ -1280,7 +1281,7 @@ def test_palette_save_ImagePalette(tmp_path): assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) -def test_save_I(tmp_path): +def test_save_I(tmp_path: Path) -> None: # Test saving something that would trigger the auto-convert to 'L' im = hopper("I") @@ -1292,7 +1293,7 @@ def test_save_I(tmp_path): assert_image_equal(reloaded.convert("L"), im.convert("L")) -def test_getdata(): +def test_getdata() -> None: # Test getheader/getdata against legacy values. # Create a 'P' image with holes in the palette. im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) @@ -1320,7 +1321,7 @@ def test_getdata(): GifImagePlugin._FORCE_OPTIMIZE = False -def test_lzw_bits(): +def test_lzw_bits() -> None: # see https://github.com/python-pillow/Pillow/issues/2811 with Image.open("Tests/images/issue_2811.gif") as im: assert im.tile[0][3][0] == 11 # LZW bits @@ -1328,7 +1329,7 @@ def test_lzw_bits(): im.load() -def test_extents(): +def test_extents() -> None: with Image.open("Tests/images/test_extents.gif") as im: assert im.size == (100, 100) @@ -1340,7 +1341,7 @@ def test_extents(): assert im.size == (150, 150) -def test_missing_background(): +def test_missing_background() -> None: # The Global Color Table Flag isn't set, so there is no background color index, # but the disposal method is "Restore to background color" with Image.open("Tests/images/missing_background.gif") as im: @@ -1348,7 +1349,7 @@ def test_missing_background(): assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png") -def test_saving_rgba(tmp_path): +def test_saving_rgba(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/transparent.png") as im: im.save(out) diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index ceea1edd34c..006ee952d43 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -3,7 +3,7 @@ from PIL import GimpGradientFile, ImagePalette -def test_linear_pos_le_middle(): +def test_linear_pos_le_middle() -> None: # Arrange middle = 0.5 pos = 0.25 @@ -15,7 +15,7 @@ def test_linear_pos_le_middle(): assert ret == 0.25 -def test_linear_pos_le_small_middle(): +def test_linear_pos_le_small_middle() -> None: # Arrange middle = 1e-11 pos = 1e-12 @@ -27,7 +27,7 @@ def test_linear_pos_le_small_middle(): assert ret == 0.0 -def test_linear_pos_gt_middle(): +def test_linear_pos_gt_middle() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -39,7 +39,7 @@ def test_linear_pos_gt_middle(): assert ret == 0.75 -def test_linear_pos_gt_small_middle(): +def test_linear_pos_gt_small_middle() -> None: # Arrange middle = 1 - 1e-11 pos = 1 - 1e-12 @@ -51,7 +51,7 @@ def test_linear_pos_gt_small_middle(): assert ret == 1.0 -def test_curved(): +def test_curved() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -63,7 +63,7 @@ def test_curved(): assert ret == 0.75 -def test_sine(): +def test_sine() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -75,7 +75,7 @@ def test_sine(): assert ret == 0.8535533905932737 -def test_sphere_increasing(): +def test_sphere_increasing() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -87,7 +87,7 @@ def test_sphere_increasing(): assert round(abs(ret - 0.9682458365518543), 7) == 0 -def test_sphere_decreasing(): +def test_sphere_decreasing() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -99,7 +99,7 @@ def test_sphere_decreasing(): assert ret == 0.3385621722338523 -def test_load_via_imagepalette(): +def test_load_via_imagepalette() -> None: # Arrange test_file = "Tests/images/gimp_gradient.ggr" @@ -112,7 +112,7 @@ def test_load_via_imagepalette(): assert palette[1] == "RGBA" -def test_load_1_3_via_imagepalette(): +def test_load_1_3_via_imagepalette() -> None: # Arrange # GIMP 1.3 gradient files contain a name field test_file = "Tests/images/gimp_gradient_with_name.ggr" diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index a4ce6dde674..4945468be40 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import GribStubImagePlugin, Image @@ -9,7 +11,7 @@ TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -20,7 +22,7 @@ def test_open(): assert im.size == (1, 1) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -29,7 +31,7 @@ def test_invalid_file(): GribStubImagePlugin.GribStubImageFile(invalid_file) -def test_load(): +def test_load() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler @@ -37,7 +39,7 @@ def test_load(): im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: # Arrange im = hopper() tmpfile = str(tmp_path / "temp.grib") @@ -47,13 +49,13 @@ def test_save(tmp_path): im.save(tmpfile) -def test_handler(tmp_path): +def test_handler(tmp_path: Path) -> None: class TestHandler: opened = False loaded = False saved = False - def open(self, im): + def open(self, im) -> None: self.opened = True def load(self, im): @@ -61,7 +63,7 @@ def load(self, im): im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename): + def save(self, im, fp, filename) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 72764461730..ac3d40bf287 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Hdf5StubImagePlugin, Image @@ -7,7 +9,7 @@ TEST_FILE = "Tests/images/hdf5.h5" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -18,7 +20,7 @@ def test_open(): assert im.size == (1, 1) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -27,7 +29,7 @@ def test_invalid_file(): Hdf5StubImagePlugin.HDF5StubImageFile(invalid_file) -def test_load(): +def test_load() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler @@ -35,7 +37,7 @@ def test_load(): im.load() -def test_save(): +def test_save() -> None: # Arrange with Image.open(TEST_FILE) as im: dummy_fp = None @@ -48,13 +50,13 @@ def test_save(): Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename) -def test_handler(tmp_path): +def test_handler(tmp_path: Path) -> None: class TestHandler: opened = False loaded = False saved = False - def open(self, im): + def open(self, im) -> None: self.opened = True def load(self, im): @@ -62,7 +64,7 @@ def load(self, im): im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename): + def save(self, im, fp, filename) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 314fa800868..488984aef70 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -3,6 +3,7 @@ import io import os import warnings +from pathlib import Path import pytest @@ -14,7 +15,7 @@ TEST_FILE = "Tests/images/pillow.icns" -def test_sanity(): +def test_sanity() -> None: # Loading this icon by default should result in the largest size # (512x512@2x) being loaded with Image.open(TEST_FILE) as im: @@ -27,7 +28,7 @@ def test_sanity(): assert im.format == "ICNS" -def test_load(): +def test_load() -> None: with Image.open(TEST_FILE) as im: assert im.load()[0, 0] == (0, 0, 0, 0) @@ -35,7 +36,7 @@ def test_load(): assert im.load()[0, 0] == (0, 0, 0, 0) -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.icns") with Image.open(TEST_FILE) as im: @@ -52,7 +53,7 @@ def test_save(tmp_path): assert _binary.i32be(fp.read(4)) == file_length -def test_save_append_images(tmp_path): +def test_save_append_images(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.icns") provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) @@ -67,7 +68,7 @@ def test_save_append_images(tmp_path): assert_image_equal(reread, provided_im) -def test_save_fp(): +def test_save_fp() -> None: fp = io.BytesIO() with Image.open(TEST_FILE) as im: @@ -79,7 +80,7 @@ def test_save_fp(): assert reread.format == "ICNS" -def test_sizes(): +def test_sizes() -> None: # Check that we can load all of the sizes, and that the final pixel # dimensions are as expected with Image.open(TEST_FILE) as im: @@ -96,7 +97,7 @@ def test_sizes(): im.size = (1, 1) -def test_older_icon(): +def test_older_icon() -> None: # This icon was made with Icon Composer rather than iconutil; it still # uses PNG rather than JP2, however (since it was made on 10.9). with Image.open("Tests/images/pillow2.icns") as im: @@ -111,7 +112,7 @@ def test_older_icon(): @skip_unless_feature("jpg_2000") -def test_jp2_icon(): +def test_jp2_icon() -> None: # This icon uses JPEG 2000 images instead of the PNG images. # The advantage of doing this is that OS X 10.5 supports JPEG 2000 # but not PNG; some commercial software therefore does just this. @@ -127,7 +128,7 @@ def test_jp2_icon(): assert im2.size == (wr, hr) -def test_getimage(): +def test_getimage() -> None: with open(TEST_FILE, "rb") as fp: icns_file = IcnsImagePlugin.IcnsFile(fp) @@ -140,14 +141,14 @@ def test_getimage(): assert im.size == (512, 512) -def test_not_an_icns_file(): +def test_not_an_icns_file() -> None: with io.BytesIO(b"invalid\n") as fp: with pytest.raises(SyntaxError): IcnsImagePlugin.IcnsFile(fp) @skip_unless_feature("jpg_2000") -def test_icns_decompression_bomb(): +def test_icns_decompression_bomb() -> None: with Image.open( "Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns" ) as im: diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 99b3048d1e0..65f090931b4 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -2,6 +2,7 @@ import io import os +from pathlib import Path import pytest @@ -12,7 +13,7 @@ TEST_ICO_FILE = "Tests/images/hopper.ico" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_ICO_FILE) as im: im.load() assert im.mode == "RGBA" @@ -21,29 +22,29 @@ def test_sanity(): assert im.get_format_mimetype() == "image/x-icon" -def test_load(): +def test_load() -> None: with Image.open(TEST_ICO_FILE) as im: assert im.load()[0, 0] == (1, 1, 9, 255) -def test_mask(): +def test_mask() -> None: with Image.open("Tests/images/hopper_mask.ico") as im: assert_image_equal_tofile(im, "Tests/images/hopper_mask.png") -def test_black_and_white(): +def test_black_and_white() -> None: with Image.open("Tests/images/black_and_white.ico") as im: assert im.mode == "RGBA" assert im.size == (16, 16) -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): IcoImagePlugin.IcoImageFile(fp) -def test_save_to_bytes(): +def test_save_to_bytes() -> None: output = io.BytesIO() im = hopper() im.save(output, "ico", sizes=[(32, 32), (64, 64)]) @@ -73,7 +74,7 @@ def test_save_to_bytes(): ) -def test_getpixel(tmp_path): +def test_getpixel(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.ico") im = hopper() @@ -86,7 +87,7 @@ def test_getpixel(tmp_path): assert reloaded.getpixel((0, 0)) == (18, 20, 62) -def test_no_duplicates(tmp_path): +def test_no_duplicates(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.ico") temp_file2 = str(tmp_path / "temp2.ico") @@ -100,7 +101,7 @@ def test_no_duplicates(tmp_path): assert os.path.getsize(temp_file) == os.path.getsize(temp_file2) -def test_different_bit_depths(tmp_path): +def test_different_bit_depths(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.ico") temp_file2 = str(tmp_path / "temp2.ico") @@ -134,7 +135,7 @@ def test_different_bit_depths(tmp_path): @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) -def test_save_to_bytes_bmp(mode): +def test_save_to_bytes_bmp(mode) -> None: output = io.BytesIO() im = hopper(mode) im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) @@ -162,13 +163,13 @@ def test_save_to_bytes_bmp(mode): assert_image_equal(reloaded, im) -def test_incorrect_size(): +def test_incorrect_size() -> None: with Image.open(TEST_ICO_FILE) as im: with pytest.raises(ValueError): im.size = (1, 1) -def test_save_256x256(tmp_path): +def test_save_256x256(tmp_path: Path) -> None: """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" # Arrange with Image.open("Tests/images/hopper_256x256.ico") as im: @@ -181,7 +182,7 @@ def test_save_256x256(tmp_path): assert im_saved.size == (256, 256) -def test_only_save_relevant_sizes(tmp_path): +def test_only_save_relevant_sizes(tmp_path: Path) -> None: """Issue #2266 https://github.com/python-pillow/Pillow/issues/2266 Should save in 16x16, 24x24, 32x32, 48x48 sizes and not in 16x16, 24x24, 32x32, 48x48, 48x48, 48x48, 48x48 sizes @@ -197,7 +198,7 @@ def test_only_save_relevant_sizes(tmp_path): assert im_saved.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)} -def test_save_append_images(tmp_path): +def test_save_append_images(tmp_path: Path) -> None: # append_images should be used for scaled down versions of the image im = hopper("RGBA") provided_im = Image.new("RGBA", (32, 32), (255, 0, 0)) @@ -211,7 +212,7 @@ def test_save_append_images(tmp_path): assert_image_equal(reread, provided_im) -def test_unexpected_size(): +def test_unexpected_size() -> None: # This image has been manually hexedited to state that it is 16x32 # while the image within is still 16x16 with pytest.warns(UserWarning): @@ -219,7 +220,7 @@ def test_unexpected_size(): assert im.size == (16, 16) -def test_draw_reloaded(tmp_path): +def test_draw_reloaded(tmp_path: Path) -> None: with Image.open(TEST_ICO_FILE) as im: outfile = str(tmp_path / "temp_saved_hopper_draw.ico") diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index a031b3e887c..f932069b9c3 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -2,6 +2,7 @@ import filecmp import warnings +from pathlib import Path import pytest @@ -13,7 +14,7 @@ TEST_IM = "Tests/images/hopper.im" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_IM) as im: im.load() assert im.mode == "RGB" @@ -21,7 +22,7 @@ def test_sanity(): assert im.format == "IM" -def test_name_limit(tmp_path): +def test_name_limit(tmp_path: Path) -> None: out = str(tmp_path / ("name_limit_test" * 7 + ".im")) with Image.open(TEST_IM) as im: im.save(out) @@ -29,8 +30,8 @@ def test_name_limit(tmp_path): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_IM) im.load() @@ -38,20 +39,20 @@ def open(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_IM) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_IM) as im: im.load() -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(TEST_IM) as im: # Act @@ -61,13 +62,13 @@ def test_tell(): assert frame == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_IM) as im: assert im.n_frames == 1 assert not im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(TEST_IM) as im: n_frames = im.n_frames @@ -81,14 +82,14 @@ def test_eoferror(): @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) -def test_roundtrip(mode, tmp_path): +def test_roundtrip(mode, tmp_path: Path) -> None: out = str(tmp_path / "temp.im") im = hopper(mode) im.save(out) assert_image_equal_tofile(im, out) -def test_small_palette(tmp_path): +def test_small_palette(tmp_path: Path) -> None: im = Image.new("P", (1, 1)) colors = [0, 1, 2] im.putpalette(colors) @@ -100,19 +101,19 @@ def test_small_palette(tmp_path): assert reloaded.getpalette() == colors + [0] * 765 -def test_save_unsupported_mode(tmp_path): +def test_save_unsupported_mode(tmp_path: Path) -> None: out = str(tmp_path / "temp.im") im = hopper("HSV") with pytest.raises(ValueError): im.save(out) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): ImImagePlugin.ImImageFile(invalid_file) -def test_number(): +def test_number() -> None: assert ImImagePlugin.number("1.2") == 1.2 diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index a2c50ecefdf..9c0969437ea 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -12,7 +12,7 @@ TEST_FILE = "Tests/images/iptc.jpg" -def test_open(): +def test_open() -> None: expected = Image.new("L", (1, 1)) f = BytesIO( @@ -24,7 +24,7 @@ def test_open(): assert_image_equal(im, expected) -def test_getiptcinfo_jpg_none(): +def test_getiptcinfo_jpg_none() -> None: # Arrange with hopper() as im: # Act @@ -34,7 +34,7 @@ def test_getiptcinfo_jpg_none(): assert iptc is None -def test_getiptcinfo_jpg_found(): +def test_getiptcinfo_jpg_found() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act @@ -46,7 +46,7 @@ def test_getiptcinfo_jpg_found(): assert iptc[(2, 101)] == b"Hungary" -def test_getiptcinfo_fotostation(): +def test_getiptcinfo_fotostation() -> None: # Arrange with open(TEST_FILE, "rb") as fp: data = bytearray(fp.read()) @@ -63,7 +63,7 @@ def test_getiptcinfo_fotostation(): pytest.fail("FotoStation tag not found") -def test_getiptcinfo_zero_padding(): +def test_getiptcinfo_zero_padding() -> None: # Arrange with Image.open(TEST_FILE) as im: im.info["photoshop"][0x0404] += b"\x00\x00\x00" @@ -76,7 +76,7 @@ def test_getiptcinfo_zero_padding(): assert len(iptc) == 3 -def test_getiptcinfo_tiff_none(): +def test_getiptcinfo_tiff_none() -> None: # Arrange with Image.open("Tests/images/hopper.tif") as im: # Act @@ -86,7 +86,7 @@ def test_getiptcinfo_tiff_none(): assert iptc is None -def test_i(): +def test_i() -> None: # Arrange c = b"a" @@ -98,7 +98,7 @@ def test_i(): assert ret == 97 -def test_dump(monkeypatch): +def test_dump(monkeypatch) -> None: # Arrange c = b"abc" # Temporarily redirect stdout @@ -113,6 +113,6 @@ def test_dump(monkeypatch): assert mystdout.getvalue() == "61 62 63 \n" -def test_pad_deprecation(): +def test_pad_deprecation() -> None: with pytest.warns(DeprecationWarning): assert IptcImagePlugin.PAD == b"\0\0\0\0" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 232e51f9126..ff278d4c1bf 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -4,6 +4,7 @@ import re import warnings from io import BytesIO +from pathlib import Path import pytest @@ -50,7 +51,7 @@ def roundtrip(self, im, **options): im.bytes = test_bytes # for testing only return im - def gen_random_image(self, size, mode="RGB"): + def gen_random_image(self, size, mode: str = "RGB"): """Generates a very hard to compress file :param size: tuple :param mode: optional image mode @@ -58,7 +59,7 @@ def gen_random_image(self, size, mode="RGB"): """ return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) - def test_sanity(self): + def test_sanity(self) -> None: # internal version number assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) @@ -70,13 +71,13 @@ def test_sanity(self): assert im.get_format_mimetype() == "image/jpeg" @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero(self, size, tmp_path): + def test_zero(self, size, tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") im = Image.new("RGB", size) with pytest.raises(ValueError): im.save(f) - def test_app(self): + def test_app(self) -> None: # Test APP/COM reader (@PIL135) with Image.open(TEST_FILE) as im: assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") @@ -89,7 +90,7 @@ def test_app(self): assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" assert im.app["COM"] == im.info["comment"] - def test_comment_write(self): + def test_comment_write(self) -> None: with Image.open(TEST_FILE) as im: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" @@ -115,7 +116,7 @@ def test_comment_write(self): comment = comment.encode() assert reloaded.info["comment"] == comment - def test_cmyk(self): + def test_cmyk(self) -> None: # Test CMYK handling. Thanks to Tim and Charlie for test data, # Michael for getting me to look one more time. f = "Tests/images/pil_sample_cmyk.jpg" @@ -143,7 +144,7 @@ def test_cmyk(self): ) assert k > 0.9 - def test_rgb(self): + def test_rgb(self) -> None: def getchannels(im): return tuple(v[0] for v in im.layer) @@ -160,7 +161,7 @@ def getchannels(im): "test_image_path", [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], ) - def test_dpi(self, test_image_path): + def test_dpi(self, test_image_path) -> None: def test(xdpi, ydpi=None): with Image.open(test_image_path) as im: im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) @@ -174,7 +175,7 @@ def test(xdpi, ydpi=None): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_icc(self, tmp_path): + def test_icc(self, tmp_path: Path) -> None: # Test ICC support with Image.open("Tests/images/rgb.jpg") as im1: icc_profile = im1.info["icc_profile"] @@ -206,7 +207,7 @@ def test_icc(self, tmp_path): ImageFile.MAXBLOCK * 4 + 3, # large block ), ) - def test_icc_big(self, n): + def test_icc_big(self, n) -> None: # Make sure that the "extra" support handles large blocks # The ICC APP marker can store 65519 bytes per marker, so # using a 4-byte test code should allow us to detect out of @@ -219,7 +220,7 @@ def test_icc_big(self, n): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_large_icc_meta(self, tmp_path): + def test_large_icc_meta(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 # Sometimes the meta data on the icc_profile block is bigger than # Image.MAXBLOCK or the image size. @@ -243,7 +244,7 @@ def test_large_icc_meta(self, tmp_path): f = str(tmp_path / "temp3.jpg") im.save(f, progressive=True, quality=94, exif=b" " * 43668) - def test_optimize(self): + def test_optimize(self) -> None: im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), optimize=0) im3 = self.roundtrip(hopper(), optimize=1) @@ -252,14 +253,14 @@ def test_optimize(self): assert im1.bytes >= im2.bytes assert im1.bytes >= im3.bytes - def test_optimize_large_buffer(self, tmp_path): + def test_optimize_large_buffer(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 f = str(tmp_path / "temp.jpg") # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", optimize=True) - def test_progressive(self): + def test_progressive(self) -> None: im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), progressive=False) im3 = self.roundtrip(hopper(), progressive=True) @@ -270,25 +271,25 @@ def test_progressive(self): assert_image_equal(im1, im3) assert im1.bytes >= im3.bytes - def test_progressive_large_buffer(self, tmp_path): + def test_progressive_large_buffer(self, tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", progressive=True) - def test_progressive_large_buffer_highest_quality(self, tmp_path): + def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") im = self.gen_random_image((255, 255)) # this requires more bytes than pixels in the image im.save(f, format="JPEG", progressive=True, quality=100) - def test_progressive_cmyk_buffer(self): + def test_progressive_cmyk_buffer(self) -> None: # Issue 2272, quality 90 cmyk image is tripping the large buffer bug. f = BytesIO() im = self.gen_random_image((256, 256), "CMYK") im.save(f, format="JPEG", progressive=True, quality=94) - def test_large_exif(self, tmp_path): + def test_large_exif(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 f = str(tmp_path / "temp.jpg") im = hopper() @@ -297,12 +298,12 @@ def test_large_exif(self, tmp_path): with pytest.raises(ValueError): im.save(f, "JPEG", quality=90, exif=b"1" * 65534) - def test_exif_typeerror(self): + def test_exif_typeerror(self) -> None: with Image.open("Tests/images/exif_typeerror.jpg") as im: # Should not raise a TypeError im._getexif() - def test_exif_gps(self, tmp_path): + def test_exif_gps(self, tmp_path: Path) -> None: expected_exif_gps = { 0: b"\x00\x00\x00\x01", 2: 4294967295, @@ -327,7 +328,7 @@ def test_exif_gps(self, tmp_path): exif = reloaded._getexif() assert exif[gps_index] == expected_exif_gps - def test_empty_exif_gps(self): + def test_empty_exif_gps(self) -> None: with Image.open("Tests/images/empty_gps_ifd.jpg") as im: exif = im.getexif() del exif[0x8769] @@ -345,7 +346,7 @@ def test_empty_exif_gps(self): # Assert that it was transposed assert 0x0112 not in exif - def test_exif_equality(self): + def test_exif_equality(self) -> None: # In 7.2.0, Exif rationals were changed to be read as # TiffImagePlugin.IFDRational. This class had a bug in __eq__, # breaking the self-equality of Exif data @@ -355,7 +356,7 @@ def test_exif_equality(self): exifs.append(im._getexif()) assert exifs[0] == exifs[1] - def test_exif_rollback(self): + def test_exif_rollback(self) -> None: # rolling back exif support in 3.1 to pre-3.0 formatting. # expected from 2.9, with b/u qualifiers switched for 3.2 compatibility # this test passes on 2.9 and 3.1, but not 3.0 @@ -390,12 +391,12 @@ def test_exif_rollback(self): for tag, value in expected_exif.items(): assert value == exif[tag] - def test_exif_gps_typeerror(self): + def test_exif_gps_typeerror(self) -> None: with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: # Should not raise a TypeError im._getexif() - def test_progressive_compat(self): + def test_progressive_compat(self) -> None: im1 = self.roundtrip(hopper()) assert not im1.info.get("progressive") assert not im1.info.get("progression") @@ -416,7 +417,7 @@ def test_progressive_compat(self): assert im3.info.get("progressive") assert im3.info.get("progression") - def test_quality(self): + def test_quality(self) -> None: im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), quality=50) assert_image(im1, im2.mode, im2.size) @@ -426,12 +427,12 @@ def test_quality(self): assert_image(im1, im3.mode, im3.size) assert im2.bytes > im3.bytes - def test_smooth(self): + def test_smooth(self) -> None: im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), smooth=100) assert_image(im1, im2.mode, im2.size) - def test_subsampling(self): + def test_subsampling(self) -> None: def getsampling(im): layer = im.layer return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] @@ -463,23 +464,23 @@ def getsampling(im): with pytest.raises(TypeError): self.roundtrip(hopper(), subsampling="1:1:1") - def test_exif(self): + def test_exif(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: info = im._getexif() assert info[305] == "Adobe Photoshop CS Macintosh" - def test_get_child_images(self): + def test_get_child_images(self) -> None: with Image.open("Tests/images/flower.jpg") as im: ims = im.get_child_images() assert len(ims) == 1 assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1) - def test_mp(self): + def test_mp(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: assert im._getmp() is None - def test_quality_keep(self, tmp_path): + def test_quality_keep(self, tmp_path: Path) -> None: # RGB with Image.open("Tests/images/hopper.jpg") as im: f = str(tmp_path / "temp.jpg") @@ -493,13 +494,13 @@ def test_quality_keep(self, tmp_path): f = str(tmp_path / "temp.jpg") im.save(f, quality="keep") - def test_junk_jpeg_header(self): + def test_junk_jpeg_header(self) -> None: # https://github.com/python-pillow/Pillow/issues/630 filename = "Tests/images/junk_jpeg_header.jpg" with Image.open(filename): pass - def test_ff00_jpeg_header(self): + def test_ff00_jpeg_header(self) -> None: filename = "Tests/images/jpeg_ff00_header.jpg" with Image.open(filename): pass @@ -507,7 +508,7 @@ def test_ff00_jpeg_header(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_truncated_jpeg_should_read_all_the_data(self): + def test_truncated_jpeg_should_read_all_the_data(self) -> None: filename = "Tests/images/truncated_jpeg.jpg" ImageFile.LOAD_TRUNCATED_IMAGES = True with Image.open(filename) as im: @@ -515,7 +516,7 @@ def test_truncated_jpeg_should_read_all_the_data(self): ImageFile.LOAD_TRUNCATED_IMAGES = False assert im.getbbox() is not None - def test_truncated_jpeg_throws_oserror(self): + def test_truncated_jpeg_throws_oserror(self) -> None: filename = "Tests/images/truncated_jpeg.jpg" with Image.open(filename) as im: with pytest.raises(OSError): @@ -528,8 +529,8 @@ def test_truncated_jpeg_throws_oserror(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_qtables(self, tmp_path): - def _n_qtables_helper(n, test_file): + def test_qtables(self, tmp_path: Path) -> None: + def _n_qtables_helper(n, test_file) -> None: with Image.open(test_file) as im: f = str(tmp_path / "temp.jpg") im.save(f, qtables=[[n] * 64] * n) @@ -637,24 +638,24 @@ def _n_qtables_helper(n, test_file): with pytest.raises(ValueError): self.roundtrip(im, qtables=[[1, 2, 3, 4]]) - def test_load_16bit_qtables(self): + def test_load_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: assert len(im.quantization) == 2 assert len(im.quantization[0]) == 64 assert max(im.quantization[0]) > 255 - def test_save_multiple_16bit_qtables(self): + def test_save_multiple_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: im2 = self.roundtrip(im, qtables="keep") assert im.quantization == im2.quantization - def test_save_single_16bit_qtable(self): + def test_save_single_16bit_qtable(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: im2 = self.roundtrip(im, qtables={0: im.quantization[0]}) assert len(im2.quantization) == 1 assert im2.quantization[0] == im.quantization[0] - def test_save_low_quality_baseline_qtables(self): + def test_save_low_quality_baseline_qtables(self) -> None: with Image.open(TEST_FILE) as im: im2 = self.roundtrip(im, quality=10) assert len(im2.quantization) == 2 @@ -665,7 +666,7 @@ def test_save_low_quality_baseline_qtables(self): "blocks, rows, markers", ((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)), ) - def test_restart_markers(self, blocks, rows, markers): + def test_restart_markers(self, blocks, rows, markers) -> None: im = Image.new("RGB", (32, 32)) # 16 MCUs out = BytesIO() im.save( @@ -679,20 +680,20 @@ def test_restart_markers(self, blocks, rows, markers): assert len(re.findall(b"\xff[\xd0-\xd7]", out.getvalue())) == markers @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") - def test_load_djpeg(self): + def test_load_djpeg(self) -> None: with Image.open(TEST_FILE) as img: img.load_djpeg() assert_image_similar_tofile(img, TEST_FILE, 5) @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") - def test_save_cjpeg(self, tmp_path): + def test_save_cjpeg(self, tmp_path: Path) -> None: with Image.open(TEST_FILE) as img: tempfile = str(tmp_path / "temp.jpg") JpegImagePlugin._save_cjpeg(img, 0, tempfile) # Default save quality is 75%, so a tiny bit of difference is alright assert_image_similar_tofile(img, tempfile, 17) - def test_no_duplicate_0x1001_tag(self): + def test_no_duplicate_0x1001_tag(self) -> None: # Arrange tag_ids = {v: k for k, v in ExifTags.TAGS.items()} @@ -700,7 +701,7 @@ def test_no_duplicate_0x1001_tag(self): assert tag_ids["RelatedImageWidth"] == 0x1001 assert tag_ids["RelatedImageLength"] == 0x1002 - def test_MAXBLOCK_scaling(self, tmp_path): + def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None: im = self.gen_random_image((512, 512)) f = str(tmp_path / "temp.jpeg") im.save(f, quality=100, optimize=True) @@ -711,7 +712,7 @@ def test_MAXBLOCK_scaling(self, tmp_path): reloaded.save(f, quality="keep", progressive=True) reloaded.save(f, quality="keep", optimize=True) - def test_bad_mpo_header(self): + def test_bad_mpo_header(self) -> None: """Treat unknown MPO as JPEG""" # Arrange @@ -723,20 +724,20 @@ def test_bad_mpo_header(self): assert im.format == "JPEG" @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) - def test_save_correct_modes(self, mode): + def test_save_correct_modes(self, mode) -> None: out = BytesIO() img = Image.new(mode, (20, 20)) img.save(out, "JPEG") @pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P")) - def test_save_wrong_modes(self, mode): + def test_save_wrong_modes(self, mode) -> None: # ref https://github.com/python-pillow/Pillow/issues/2005 out = BytesIO() img = Image.new(mode, (20, 20)) with pytest.raises(OSError): img.save(out, "JPEG") - def test_save_tiff_with_dpi(self, tmp_path): + def test_save_tiff_with_dpi(self, tmp_path: Path) -> None: # Arrange outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: @@ -748,7 +749,7 @@ def test_save_tiff_with_dpi(self, tmp_path): reloaded.load() assert im.info["dpi"] == reloaded.info["dpi"] - def test_save_dpi_rounding(self, tmp_path): + def test_save_dpi_rounding(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.jpg") as im: im.save(outfile, dpi=(72.2, 72.2)) @@ -761,7 +762,7 @@ def test_save_dpi_rounding(self, tmp_path): with Image.open(outfile) as reloaded: assert reloaded.info["dpi"] == (73, 73) - def test_dpi_tuple_from_exif(self): + def test_dpi_tuple_from_exif(self) -> None: # Arrange # This Photoshop CC 2017 image has DPI in EXIF not metadata # EXIF XResolution is (2000000, 10000) @@ -769,7 +770,7 @@ def test_dpi_tuple_from_exif(self): # Act / Assert assert im.info.get("dpi") == (200, 200) - def test_dpi_int_from_exif(self): + def test_dpi_int_from_exif(self) -> None: # Arrange # This image has DPI in EXIF not metadata # EXIF XResolution is 72 @@ -777,7 +778,7 @@ def test_dpi_int_from_exif(self): # Act / Assert assert im.info.get("dpi") == (72, 72) - def test_dpi_from_dpcm_exif(self): + def test_dpi_from_dpcm_exif(self) -> None: # Arrange # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg @@ -785,7 +786,7 @@ def test_dpi_from_dpcm_exif(self): # Act / Assert assert im.info.get("dpi") == (508, 508) - def test_dpi_exif_zero_division(self): + def test_dpi_exif_zero_division(self) -> None: # Arrange # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg @@ -794,7 +795,7 @@ def test_dpi_exif_zero_division(self): # This should return the default, and not raise a ZeroDivisionError assert im.info.get("dpi") == (72, 72) - def test_dpi_exif_string(self): + def test_dpi_exif_string(self) -> None: # Arrange # 0x011A tag in this exif contains string '300300\x02' with Image.open("Tests/images/broken_exif_dpi.jpg") as im: @@ -802,14 +803,14 @@ def test_dpi_exif_string(self): # This should return the default assert im.info.get("dpi") == (72, 72) - def test_dpi_exif_truncated(self): + def test_dpi_exif_truncated(self) -> None: # Arrange with Image.open("Tests/images/truncated_exif_dpi.jpg") as im: # Act / Assert # This should return the default assert im.info.get("dpi") == (72, 72) - def test_no_dpi_in_exif(self): + def test_no_dpi_in_exif(self) -> None: # Arrange # This is photoshop-200dpi.jpg with resolution removed from EXIF: # exiftool "-*resolution*"= photoshop-200dpi.jpg @@ -819,7 +820,7 @@ def test_no_dpi_in_exif(self): # https://exiv2.org/tags.html assert im.info.get("dpi") == (72, 72) - def test_invalid_exif(self): + def test_invalid_exif(self) -> None: # This is no-dpi-in-exif with the tiff header of the exif block # hexedited from MM * to FF FF FF FF with Image.open("Tests/images/invalid-exif.jpg") as im: @@ -830,7 +831,7 @@ def test_invalid_exif(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_exif_x_resolution(self, tmp_path): + def test_exif_x_resolution(self, tmp_path: Path) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() assert exif[282] == 180 @@ -842,14 +843,14 @@ def test_exif_x_resolution(self, tmp_path): with Image.open(out) as reloaded: assert reloaded.getexif()[282] == 180 - def test_invalid_exif_x_resolution(self): + def test_invalid_exif_x_resolution(self) -> None: # When no x or y resolution is defined in EXIF with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im: # This should return the default, and not a ValueError or # OSError for an unidentified image. assert im.info.get("dpi") == (72, 72) - def test_ifd_offset_exif(self): + def test_ifd_offset_exif(self) -> None: # Arrange # This image has been manually hexedited to have an IFD offset of 10, # in contrast to normal 8 @@ -857,14 +858,14 @@ def test_ifd_offset_exif(self): # Act / Assert assert im._getexif()[306] == "2017:03:13 23:03:09" - def test_multiple_exif(self): + def test_multiple_exif(self) -> None: with Image.open("Tests/images/multiple_exif.jpg") as im: assert im.info["exif"] == b"Exif\x00\x00firstsecond" @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_photoshop(self): + def test_photoshop(self) -> None: with Image.open("Tests/images/photoshop-200dpi.jpg") as im: assert im.info["photoshop"][0x03ED] == { "XResolution": 200.0, @@ -881,14 +882,14 @@ def test_photoshop(self): with Image.open("Tests/images/app13.jpg") as im: assert "photoshop" not in im.info - def test_photoshop_malformed_and_multiple(self): + def test_photoshop_malformed_and_multiple(self) -> None: with Image.open("Tests/images/app13-multiple.jpg") as im: assert "photoshop" in im.info assert 24 == len(im.info["photoshop"]) apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] assert [65504, 24] == apps_13_lengths - def test_adobe_transform(self): + def test_adobe_transform(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: assert im.info["adobe_transform"] == 1 @@ -902,11 +903,11 @@ def test_adobe_transform(self): assert "adobe" in im.info assert "adobe_transform" not in im.info - def test_icc_after_SOF(self): + def test_icc_after_SOF(self) -> None: with Image.open("Tests/images/icc-after-SOF.jpg") as im: assert im.info["icc_profile"] == b"profile" - def test_jpeg_magic_number(self): + def test_jpeg_magic_number(self) -> None: size = 4097 buffer = BytesIO(b"\xFF" * size) # Many xFF bytes buffer.max_pos = 0 @@ -925,7 +926,7 @@ def read(n=-1): # Assert the entire file has not been read assert 0 < buffer.max_pos < size - def test_getxmp(self): + def test_getxmp(self) -> None: with Image.open("Tests/images/xmp_test.jpg") as im: if ElementTree is None: with pytest.warns( @@ -954,7 +955,7 @@ def test_getxmp(self): with Image.open("Tests/images/hopper.jpg") as im: assert im.getxmp() == {} - def test_getxmp_no_prefix(self): + def test_getxmp_no_prefix(self) -> None: with Image.open("Tests/images/xmp_no_prefix.jpg") as im: if ElementTree is None: with pytest.warns( @@ -965,7 +966,7 @@ def test_getxmp_no_prefix(self): else: assert im.getxmp() == {"xmpmeta": {"key": "value"}} - def test_getxmp_padded(self): + def test_getxmp_padded(self) -> None: with Image.open("Tests/images/xmp_padded.jpg") as im: if ElementTree is None: with pytest.warns( @@ -977,7 +978,7 @@ def test_getxmp_padded(self): assert im.getxmp() == {"xmpmeta": None} @pytest.mark.timeout(timeout=1) - def test_eof(self): + def test_eof(self) -> None: # Even though this decoder never says that it is finished # the image should still end when there is no new data class InfiniteMockPyDecoder(ImageFile.PyDecoder): @@ -1000,7 +1001,7 @@ def closure(mode, *args): im.load() ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_separate_tables(self): + def test_separate_tables(self) -> None: im = hopper() data = [] # [interchange, tables-only, image-only] for streamtype in range(3): @@ -1022,14 +1023,14 @@ def test_separate_tables(self): with Image.open(BytesIO(data[1] + data[2])) as combined_im: assert_image_equal(interchange_im, combined_im) - def test_repr_jpeg(self): + def test_repr_jpeg(self) -> None: im = hopper() with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg: assert repr_jpeg.format == "JPEG" assert_image_similar(im, repr_jpeg, 17) - def test_repr_jpeg_error_returns_none(self): + def test_repr_jpeg_error_returns_none(self) -> None: im = hopper("F") assert im._repr_jpeg_() is None @@ -1038,7 +1039,7 @@ def test_repr_jpeg_error_returns_none(self): @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") class TestFileCloseW32: - def test_fd_leak(self, tmp_path): + def test_fd_leak(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.jpg") as im: diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 94b02c9ff52..e3f1fa8fde1 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -3,6 +3,7 @@ import os import re from io import BytesIO +from pathlib import Path import pytest @@ -46,7 +47,7 @@ def roundtrip(im, **options): return im -def test_sanity(): +def test_sanity() -> None: # Internal version number assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000")) @@ -59,20 +60,20 @@ def test_sanity(): assert im.get_format_mimetype() == "image/jp2" -def test_jpf(): +def test_jpf() -> None: with Image.open("Tests/images/balloon.jpf") as im: assert im.format == "JPEG2000" assert im.get_format_mimetype() == "image/jpx" -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) -def test_bytesio(): +def test_bytesio() -> None: with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = BytesIO(f.read()) assert_image_similar_tofile(test_card, data, 1.0e-3) @@ -82,7 +83,7 @@ def test_bytesio(): # PIL (they were made using Adobe Photoshop) -def test_lossless(tmp_path): +def test_lossless(tmp_path: Path) -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: im.load() outfile = str(tmp_path / "temp_test-card.png") @@ -90,54 +91,54 @@ def test_lossless(tmp_path): assert_image_similar(im, test_card, 1.0e-3) -def test_lossy_tiled(): +def test_lossy_tiled() -> None: assert_image_similar_tofile( test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0 ) -def test_lossless_rt(): +def test_lossless_rt() -> None: im = roundtrip(test_card) assert_image_equal(im, test_card) -def test_lossy_rt(): +def test_lossy_rt() -> None: im = roundtrip(test_card, quality_layers=[20]) assert_image_similar(im, test_card, 2.0) -def test_tiled_rt(): +def test_tiled_rt() -> None: im = roundtrip(test_card, tile_size=(128, 128)) assert_image_equal(im, test_card) -def test_tiled_offset_rt(): +def test_tiled_offset_rt() -> None: im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) assert_image_equal(im, test_card) -def test_tiled_offset_too_small(): +def test_tiled_offset_too_small() -> None: with pytest.raises(ValueError): roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) -def test_irreversible_rt(): +def test_irreversible_rt() -> None: im = roundtrip(test_card, irreversible=True, quality_layers=[20]) assert_image_similar(im, test_card, 2.0) -def test_prog_qual_rt(): +def test_prog_qual_rt() -> None: im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") assert_image_similar(im, test_card, 2.0) -def test_prog_res_rt(): +def test_prog_res_rt() -> None: im = roundtrip(test_card, num_resolutions=8, progression="RLCP") assert_image_equal(im, test_card) @pytest.mark.parametrize("num_resolutions", range(2, 6)) -def test_default_num_resolutions(num_resolutions): +def test_default_num_resolutions(num_resolutions) -> None: d = 1 << (num_resolutions - 1) im = test_card.resize((d - 1, d - 1)) with pytest.raises(OSError): @@ -146,7 +147,7 @@ def test_default_num_resolutions(num_resolutions): assert_image_equal(im, reloaded) -def test_reduce(): +def test_reduce() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert callable(im.reduce) @@ -160,7 +161,7 @@ def test_reduce(): assert im.size == (40, 30) -def test_load_dpi(): +def test_load_dpi() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert im.info["dpi"] == (71.9836, 71.9836) @@ -168,7 +169,7 @@ def test_load_dpi(): assert "dpi" not in im.info -def test_restricted_icc_profile(): +def test_restricted_icc_profile() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = True try: # JPEG2000 image with a restricted ICC profile and a known colorspace @@ -178,7 +179,7 @@ def test_restricted_icc_profile(): ImageFile.LOAD_TRUNCATED_IMAGES = False -def test_header_errors(): +def test_header_errors() -> None: for path in ( "Tests/images/invalid_header_length.jp2", "Tests/images/not_enough_data.jp2", @@ -192,7 +193,7 @@ def test_header_errors(): pass -def test_layers_type(tmp_path): +def test_layers_type(tmp_path: Path) -> None: outfile = str(tmp_path / "temp_layers.jp2") for quality_layers in [[100, 50, 10], (100, 50, 10), None]: test_card.save(outfile, quality_layers=quality_layers) @@ -202,7 +203,7 @@ def test_layers_type(tmp_path): test_card.save(outfile, quality_layers=quality_layers) -def test_layers(): +def test_layers() -> None: out = BytesIO() test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") out.seek(0) @@ -232,7 +233,7 @@ def test_layers(): ("foo.jp2", {"no_jp2": False}, 4, b"jP"), ), ) -def test_no_jp2(name, args, offset, data): +def test_no_jp2(name, args, offset, data) -> None: out = BytesIO() if name: out.name = name @@ -241,7 +242,7 @@ def test_no_jp2(name, args, offset, data): assert out.read(2) == data -def test_mct(): +def test_mct() -> None: # Three component for val in (0, 1): out = BytesIO() @@ -262,7 +263,7 @@ def test_mct(): assert_image_similar(im, jp2, 1.0e-3) -def test_sgnd(tmp_path): +def test_sgnd(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.jp2") im = Image.new("L", (1, 1)) @@ -277,7 +278,7 @@ def test_sgnd(tmp_path): @pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_rgba(ext): +def test_rgba(ext) -> None: # Arrange with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im: # Act @@ -288,47 +289,47 @@ def test_rgba(ext): @pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_16bit_monochrome_has_correct_mode(ext): +def test_16bit_monochrome_has_correct_mode(ext) -> None: with Image.open("Tests/images/16bit.cropped" + ext) as im: im.load() assert im.mode == "I;16" -def test_16bit_monochrome_jp2_like_tiff(): +def test_16bit_monochrome_jp2_like_tiff() -> None: with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.jp2", 1e-3) -def test_16bit_monochrome_j2k_like_tiff(): +def test_16bit_monochrome_j2k_like_tiff() -> None: with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.j2k", 1e-3) -def test_16bit_j2k_roundtrips(): +def test_16bit_j2k_roundtrips() -> None: with Image.open("Tests/images/16bit.cropped.j2k") as j2k: im = roundtrip(j2k) assert_image_equal(im, j2k) -def test_16bit_jp2_roundtrips(): +def test_16bit_jp2_roundtrips() -> None: with Image.open("Tests/images/16bit.cropped.jp2") as jp2: im = roundtrip(jp2) assert_image_equal(im, jp2) -def test_issue_6194(): +def test_issue_6194() -> None: with Image.open("Tests/images/issue_6194.j2k") as im: assert im.getpixel((5, 5)) == 31 -def test_unbound_local(): +def test_unbound_local() -> None: # prepatch, a malformed jp2 file could cause an UnboundLocalError exception. with pytest.raises(OSError): with Image.open("Tests/images/unbound_variable.jp2"): pass -def test_parser_feed(): +def test_parser_feed() -> None: # Arrange with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = f.read() @@ -345,7 +346,7 @@ def test_parser_feed(): not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) @pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2")) -def test_subsampling_decode(name): +def test_subsampling_decode(name) -> None: test = f"{EXTRA_DIR}/{name}.jp2" reference = f"{EXTRA_DIR}/{name}.ppm" @@ -361,7 +362,7 @@ def test_subsampling_decode(name): assert_image_similar(im, expected, epsilon) -def test_comment(): +def test_comment() -> None: with Image.open("Tests/images/comment.jp2") as im: assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" @@ -372,7 +373,7 @@ def test_comment(): pass -def test_save_comment(): +def test_save_comment() -> None: for comment in ("Created by Pillow", b"Created by Pillow"): out = BytesIO() test_card.save(out, "JPEG2000", comment=comment) @@ -399,7 +400,7 @@ def test_save_comment(): "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", ], ) -def test_crashes(test_file): +def test_crashes(test_file) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: # Valgrind should not complain here @@ -410,7 +411,7 @@ def test_crashes(test_file): @skip_unless_feature_version("jpg_2000", "2.4.0") -def test_plt_marker(): +def test_plt_marker() -> None: # Search the start of the codesteam for PLT out = BytesIO() test_card.save(out, "JPEG2000", no_jp2=True, plt=True) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 494253c87c1..1386034e5a3 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -7,6 +7,7 @@ import re import sys from collections import namedtuple +from pathlib import Path import pytest @@ -26,7 +27,7 @@ @skip_unless_feature("libtiff") class LibTiffTestCase: - def _assert_noerr(self, tmp_path, im): + def _assert_noerr(self, tmp_path: Path, im) -> None: """Helper tests that assert basic sanity about the g4 tiff reading""" # 1 bit assert im.mode == "1" @@ -50,10 +51,10 @@ def _assert_noerr(self, tmp_path, im): class TestFileLibTiff(LibTiffTestCase): - def test_version(self): + def test_version(self) -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff")) - def test_g4_tiff(self, tmp_path): + def test_g4_tiff(self, tmp_path: Path) -> None: """Test the ordinary file path load path""" test_file = "Tests/images/hopper_g4_500.tif" @@ -61,12 +62,12 @@ def test_g4_tiff(self, tmp_path): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_large(self, tmp_path): + def test_g4_large(self, tmp_path: Path) -> None: test_file = "Tests/images/pport_g4.tif" with Image.open(test_file) as im: self._assert_noerr(tmp_path, im) - def test_g4_tiff_file(self, tmp_path): + def test_g4_tiff_file(self, tmp_path: Path) -> None: """Testing the string load path""" test_file = "Tests/images/hopper_g4_500.tif" @@ -75,7 +76,7 @@ def test_g4_tiff_file(self, tmp_path): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_tiff_bytesio(self, tmp_path): + def test_g4_tiff_bytesio(self, tmp_path: Path) -> None: """Testing the stringio loading code path""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() @@ -86,7 +87,7 @@ def test_g4_tiff_bytesio(self, tmp_path): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_non_disk_file_object(self, tmp_path): + def test_g4_non_disk_file_object(self, tmp_path: Path) -> None: """Testing loading from non-disk non-BytesIO file object""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() @@ -98,18 +99,18 @@ def test_g4_non_disk_file_object(self, tmp_path): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_eq_png(self): + def test_g4_eq_png(self) -> None: """Checking that we're actually getting the data that we expect""" with Image.open("Tests/images/hopper_bw_500.png") as png: assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif") # see https://github.com/python-pillow/Pillow/issues/279 - def test_g4_fillorder_eq_png(self): + def test_g4_fillorder_eq_png(self) -> None: """Checking that we're actually getting the data that we expect""" with Image.open("Tests/images/g4-fillorder-test.tif") as g4: assert_image_equal_tofile(g4, "Tests/images/g4-fillorder-test.png") - def test_g4_write(self, tmp_path): + def test_g4_write(self, tmp_path: Path) -> None: """Checking to see that the saved image is the same as what we wrote""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: @@ -128,7 +129,7 @@ def test_g4_write(self, tmp_path): assert orig.tobytes() != reread.tobytes() - def test_adobe_deflate_tiff(self): + def test_adobe_deflate_tiff(self) -> None: test_file = "Tests/images/tiff_adobe_deflate.tif" with Image.open(test_file) as im: assert im.mode == "RGB" @@ -139,7 +140,7 @@ def test_adobe_deflate_tiff(self): assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") @pytest.mark.parametrize("legacy_api", (False, True)) - def test_write_metadata(self, legacy_api, tmp_path): + def test_write_metadata(self, legacy_api, tmp_path: Path) -> None: """Test metadata writing through libtiff""" f = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper_g4.tif") as img: @@ -184,7 +185,7 @@ def test_write_metadata(self, legacy_api, tmp_path): assert field in reloaded, f"{field} not in metadata" @pytest.mark.valgrind_known_error(reason="Known invalid metadata") - def test_additional_metadata(self, tmp_path): + def test_additional_metadata(self, tmp_path: Path) -> None: # these should not crash. Seriously dummy data, most of it doesn't make # any sense, so we're running up against limits where we're asking # libtiff to do stupid things. @@ -241,7 +242,7 @@ def test_additional_metadata(self, tmp_path): TiffImagePlugin.WRITE_LIBTIFF = False - def test_custom_metadata(self, tmp_path): + def test_custom_metadata(self, tmp_path: Path) -> None: tc = namedtuple("test_case", "value,type,supported_by_default") custom = { 37000 + k: v @@ -283,7 +284,7 @@ def test_custom_metadata(self, tmp_path): for libtiff in libtiffs: TiffImagePlugin.WRITE_LIBTIFF = libtiff - def check_tags(tiffinfo): + def check_tags(tiffinfo) -> None: im = hopper() out = str(tmp_path / "temp.tif") @@ -322,7 +323,7 @@ def check_tags(tiffinfo): ) TiffImagePlugin.WRITE_LIBTIFF = False - def test_subifd(self, tmp_path): + def test_subifd(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/g4_orientation_6.tif") as im: im.tag_v2[SUBIFD] = 10000 @@ -330,7 +331,7 @@ def test_subifd(self, tmp_path): # Should not segfault im.save(outfile) - def test_xmlpacket_tag(self, tmp_path): + def test_xmlpacket_tag(self, tmp_path: Path) -> None: TiffImagePlugin.WRITE_LIBTIFF = True out = str(tmp_path / "temp.tif") @@ -341,7 +342,7 @@ def test_xmlpacket_tag(self, tmp_path): if 700 in reloaded.tag_v2: assert reloaded.tag_v2[700] == b"xmlpacket tag" - def test_int_dpi(self, tmp_path): + def test_int_dpi(self, tmp_path: Path) -> None: # issue #1765 im = hopper("RGB") out = str(tmp_path / "temp.tif") @@ -351,7 +352,7 @@ def test_int_dpi(self, tmp_path): with Image.open(out) as reloaded: assert reloaded.info["dpi"] == (72.0, 72.0) - def test_g3_compression(self, tmp_path): + def test_g3_compression(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper_g4_500.tif") as i: out = str(tmp_path / "temp.tif") i.save(out, compression="group3") @@ -360,7 +361,7 @@ def test_g3_compression(self, tmp_path): assert reread.info["compression"] == "group3" assert_image_equal(reread, i) - def test_little_endian(self, tmp_path): + def test_little_endian(self, tmp_path: Path) -> None: with Image.open("Tests/images/16bit.deflate.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16" @@ -379,7 +380,7 @@ def test_little_endian(self, tmp_path): # UNDONE - libtiff defaults to writing in native endian, so # on big endian, we'll get back mode = 'I;16B' here. - def test_big_endian(self, tmp_path): + def test_big_endian(self, tmp_path: Path) -> None: with Image.open("Tests/images/16bit.MM.deflate.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16B" @@ -396,7 +397,7 @@ def test_big_endian(self, tmp_path): assert reread.info["compression"] == im.info["compression"] assert reread.getpixel((0, 0)) == 480 - def test_g4_string_info(self, tmp_path): + def test_g4_string_info(self, tmp_path: Path) -> None: """Tests String data in info directory""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: @@ -409,7 +410,7 @@ def test_g4_string_info(self, tmp_path): assert "temp.tif" == reread.tag_v2[269] assert "temp.tif" == reread.tag[269][0] - def test_12bit_rawmode(self): + def test_12bit_rawmode(self) -> None: """Are we generating the same interpretation of the image as Imagemagick is?""" TiffImagePlugin.READ_LIBTIFF = True @@ -424,7 +425,7 @@ def test_12bit_rawmode(self): assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") - def test_blur(self, tmp_path): + def test_blur(self, tmp_path: Path) -> None: # test case from irc, how to do blur on b/w image # and save to compressed tif. out = str(tmp_path / "temp.tif") @@ -436,7 +437,7 @@ def test_blur(self, tmp_path): assert_image_equal_tofile(im, out) - def test_compressions(self, tmp_path): + def test_compressions(self, tmp_path: Path) -> None: # Test various tiff compressions and assert similar image content but reduced # file sizes. im = hopper("RGB") @@ -462,7 +463,7 @@ def test_compressions(self, tmp_path): assert size_compressed > size_jpeg assert size_jpeg > size_jpeg_30 - def test_tiff_jpeg_compression(self, tmp_path): + def test_tiff_jpeg_compression(self, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") im.save(out, compression="tiff_jpeg") @@ -470,7 +471,7 @@ def test_tiff_jpeg_compression(self, tmp_path): with Image.open(out) as reloaded: assert reloaded.info["compression"] == "jpeg" - def test_tiff_deflate_compression(self, tmp_path): + def test_tiff_deflate_compression(self, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") im.save(out, compression="tiff_deflate") @@ -478,7 +479,7 @@ def test_tiff_deflate_compression(self, tmp_path): with Image.open(out) as reloaded: assert reloaded.info["compression"] == "tiff_adobe_deflate" - def test_quality(self, tmp_path): + def test_quality(self, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") @@ -493,7 +494,7 @@ def test_quality(self, tmp_path): im.save(out, compression="jpeg", quality=0) im.save(out, compression="jpeg", quality=100) - def test_cmyk_save(self, tmp_path): + def test_cmyk_save(self, tmp_path: Path) -> None: im = hopper("CMYK") out = str(tmp_path / "temp.tif") @@ -501,7 +502,7 @@ def test_cmyk_save(self, tmp_path): assert_image_equal_tofile(im, out) @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) - def test_palette_save(self, im, tmp_path): + def test_palette_save(self, im, tmp_path: Path) -> None: out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True @@ -513,14 +514,14 @@ def test_palette_save(self, im, tmp_path): assert len(reloaded.tag_v2[320]) == 768 @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) - def test_bw_compression_w_rgb(self, compression, tmp_path): + def test_bw_compression_w_rgb(self, compression, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") with pytest.raises(OSError): im.save(out, compression=compression) - def test_fp_leak(self): + def test_fp_leak(self) -> None: im = Image.open("Tests/images/hopper_g4_500.tif") fn = im.fp.fileno() @@ -534,7 +535,7 @@ def test_fp_leak(self): with pytest.raises(OSError): os.close(fn) - def test_multipage(self): + def test_multipage(self) -> None: # issue #862 TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/multipage.tiff") as im: @@ -557,7 +558,7 @@ def test_multipage(self): TiffImagePlugin.READ_LIBTIFF = False - def test_multipage_nframes(self): + def test_multipage_nframes(self) -> None: # issue #862 TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/multipage.tiff") as im: @@ -570,7 +571,7 @@ def test_multipage_nframes(self): TiffImagePlugin.READ_LIBTIFF = False - def test_multipage_seek_backwards(self): + def test_multipage_seek_backwards(self) -> None: TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/multipage.tiff") as im: im.seek(1) @@ -581,14 +582,14 @@ def test_multipage_seek_backwards(self): TiffImagePlugin.READ_LIBTIFF = False - def test__next(self): + def test__next(self) -> None: TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/hopper.tif") as im: assert not im.tag.next im.load() assert not im.tag.next - def test_4bit(self): + def test_4bit(self) -> None: # Arrange test_file = "Tests/images/hopper_gray_4bpp.tif" original = hopper("L") @@ -603,7 +604,7 @@ def test_4bit(self): assert im.mode == "L" assert_image_similar(im, original, 7.3) - def test_gray_semibyte_per_pixel(self): + def test_gray_semibyte_per_pixel(self) -> None: test_files = ( ( 24.8, # epsilon @@ -636,7 +637,7 @@ def test_gray_semibyte_per_pixel(self): assert im2.mode == "L" assert_image_equal(im, im2) - def test_save_bytesio(self): + def test_save_bytesio(self) -> None: # PR 1011 # Test TIFF saving to io.BytesIO() object. @@ -646,7 +647,7 @@ def test_save_bytesio(self): # Generate test image pilim = hopper() - def save_bytesio(compression=None): + def save_bytesio(compression=None) -> None: buffer_io = io.BytesIO() pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) @@ -661,7 +662,7 @@ def save_bytesio(compression=None): TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False - def test_save_ycbcr(self, tmp_path): + def test_save_ycbcr(self, tmp_path: Path) -> None: im = hopper("YCbCr") outfile = str(tmp_path / "temp.tif") im.save(outfile, compression="jpeg") @@ -670,7 +671,7 @@ def test_save_ycbcr(self, tmp_path): assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) - def test_exif_ifd(self, tmp_path): + def test_exif_ifd(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: assert im.tag_v2[34665] == 125456 @@ -680,7 +681,7 @@ def test_exif_ifd(self, tmp_path): if Image.core.libtiff_support_custom_tags: assert reloaded.tag_v2[34665] == 125456 - def test_crashing_metadata(self, tmp_path): + def test_crashing_metadata(self, tmp_path: Path) -> None: # issue 1597 with Image.open("Tests/images/rdf.tif") as im: out = str(tmp_path / "temp.tif") @@ -690,7 +691,7 @@ def test_crashing_metadata(self, tmp_path): im.save(out, format="TIFF") TiffImagePlugin.WRITE_LIBTIFF = False - def test_page_number_x_0(self, tmp_path): + def test_page_number_x_0(self, tmp_path: Path) -> None: # Issue 973 # Test TIFF with tag 297 (Page Number) having value of 0 0. # The first number is the current page number. @@ -704,7 +705,7 @@ def test_page_number_x_0(self, tmp_path): # Should not divide by zero im.save(outfile) - def test_fd_duplication(self, tmp_path): + def test_fd_duplication(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/1651 tmpfile = str(tmp_path / "temp.tif") @@ -718,7 +719,7 @@ def test_fd_duplication(self, tmp_path): # Should not raise PermissionError. os.remove(tmpfile) - def test_read_icc(self): + def test_read_icc(self) -> None: with Image.open("Tests/images/hopper.iccprofile.tif") as img: icc = img.info.get("icc_profile") assert icc is not None @@ -729,8 +730,8 @@ def test_read_icc(self): TiffImagePlugin.READ_LIBTIFF = False assert icc == icc_libtiff - def test_write_icc(self, tmp_path): - def check_write(libtiff): + def test_write_icc(self, tmp_path: Path) -> None: + def check_write(libtiff) -> None: TiffImagePlugin.WRITE_LIBTIFF = libtiff with Image.open("Tests/images/hopper.iccprofile.tif") as img: @@ -749,7 +750,7 @@ def check_write(libtiff): for libtiff in libtiffs: check_write(libtiff) - def test_multipage_compression(self): + def test_multipage_compression(self) -> None: with Image.open("Tests/images/compression.tif") as im: im.seek(0) assert im._compression == "tiff_ccitt" @@ -765,7 +766,7 @@ def test_multipage_compression(self): assert im.size == (10, 10) im.load() - def test_save_tiff_with_jpegtables(self, tmp_path): + def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None: # Arrange outfile = str(tmp_path / "temp.tif") @@ -777,7 +778,7 @@ def test_save_tiff_with_jpegtables(self, tmp_path): # Should not raise UnicodeDecodeError or anything else im.save(outfile) - def test_16bit_RGB_tiff(self): + def test_16bit_RGB_tiff(self) -> None: with Image.open("Tests/images/tiff_16bit_RGB.tiff") as im: assert im.mode == "RGB" assert im.size == (100, 40) @@ -793,7 +794,7 @@ def test_16bit_RGB_tiff(self): assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - def test_16bit_RGBa_tiff(self): + def test_16bit_RGBa_tiff(self) -> None: with Image.open("Tests/images/tiff_16bit_RGBa.tiff") as im: assert im.mode == "RGBA" assert im.size == (100, 40) @@ -805,7 +806,7 @@ def test_16bit_RGBa_tiff(self): assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") @skip_unless_feature("jpg") - def test_gimp_tiff(self): + def test_gimp_tiff(self) -> None: # Read TIFF JPEG images from GIMP [@PIL168] filename = "Tests/images/pil168.tif" with Image.open(filename) as im: @@ -818,14 +819,14 @@ def test_gimp_tiff(self): assert_image_equal_tofile(im, "Tests/images/pil168.png") - def test_sampleformat(self): + def test_sampleformat(self) -> None: # https://github.com/python-pillow/Pillow/issues/1466 with Image.open("Tests/images/copyleft.tiff") as im: assert im.mode == "RGB" assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB") - def test_sampleformat_write(self, tmp_path): + def test_sampleformat_write(self, tmp_path: Path) -> None: im = Image.new("F", (1, 1)) out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True @@ -874,7 +875,7 @@ def test_webp(self, capfd): sys.stderr.write(captured.err) raise - def test_lzw(self): + def test_lzw(self) -> None: with Image.open("Tests/images/hopper_lzw.tif") as im: assert im.mode == "RGB" assert im.size == (128, 128) @@ -882,12 +883,12 @@ def test_lzw(self): im2 = hopper() assert_image_similar(im, im2, 5) - def test_strip_cmyk_jpeg(self): + def test_strip_cmyk_jpeg(self) -> None: infile = "Tests/images/tiff_strip_cmyk_jpeg.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - def test_strip_cmyk_16l_jpeg(self): + def test_strip_cmyk_16l_jpeg(self) -> None: infile = "Tests/images/tiff_strip_cmyk_16l_jpeg.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) @@ -895,7 +896,7 @@ def test_strip_cmyk_16l_jpeg(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_strip_ycbcr_jpeg_2x2_sampling(self): + def test_strip_ycbcr_jpeg_2x2_sampling(self) -> None: infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2) @@ -903,12 +904,12 @@ def test_strip_ycbcr_jpeg_2x2_sampling(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_strip_ycbcr_jpeg_1x1_sampling(self): + def test_strip_ycbcr_jpeg_1x1_sampling(self) -> None: infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) - def test_tiled_cmyk_jpeg(self): + def test_tiled_cmyk_jpeg(self) -> None: infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) @@ -916,7 +917,7 @@ def test_tiled_cmyk_jpeg(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_tiled_ycbcr_jpeg_1x1_sampling(self): + def test_tiled_ycbcr_jpeg_1x1_sampling(self) -> None: infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) @@ -924,45 +925,45 @@ def test_tiled_ycbcr_jpeg_1x1_sampling(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_tiled_ycbcr_jpeg_2x2_sampling(self): + def test_tiled_ycbcr_jpeg_2x2_sampling(self) -> None: infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.5) - def test_strip_planar_rgb(self): + def test_strip_planar_rgb(self) -> None: # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_strip_raw.tif tiff_strip_planar_lzw.tiff infile = "Tests/images/tiff_strip_planar_lzw.tiff" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_tiled_planar_rgb(self): + def test_tiled_planar_rgb(self) -> None: # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_tiled_raw.tif tiff_tiled_planar_lzw.tiff infile = "Tests/images/tiff_tiled_planar_lzw.tiff" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_tiled_planar_16bit_RGB(self): + def test_tiled_planar_16bit_RGB(self) -> None: # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_16bit_RGB.tiff tiff_tiled_planar_16bit_RGB.tiff with Image.open("Tests/images/tiff_tiled_planar_16bit_RGB.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - def test_strip_planar_16bit_RGB(self): + def test_strip_planar_16bit_RGB(self) -> None: # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_16bit_RGB.tiff tiff_strip_planar_16bit_RGB.tiff with Image.open("Tests/images/tiff_strip_planar_16bit_RGB.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - def test_tiled_planar_16bit_RGBa(self): + def test_tiled_planar_16bit_RGBa(self) -> None: # gdal_translate -co TILED=yes \ # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ # tiff_16bit_RGBa.tiff tiff_tiled_planar_16bit_RGBa.tiff with Image.open("Tests/images/tiff_tiled_planar_16bit_RGBa.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") - def test_strip_planar_16bit_RGBa(self): + def test_strip_planar_16bit_RGBa(self) -> None: # gdal_translate -co TILED=no \ # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ # tiff_16bit_RGBa.tiff tiff_strip_planar_16bit_RGBa.tiff @@ -970,7 +971,7 @@ def test_strip_planar_16bit_RGBa(self): assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") @pytest.mark.parametrize("compression", (None, "jpeg")) - def test_block_tile_tags(self, compression, tmp_path): + def test_block_tile_tags(self, compression, tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.tif") @@ -986,11 +987,11 @@ def test_block_tile_tags(self, compression, tmp_path): for tag in tags: assert tag not in reloaded.getexif() - def test_old_style_jpeg(self): + def test_old_style_jpeg(self) -> None: with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") - def test_open_missing_samplesperpixel(self): + def test_open_missing_samplesperpixel(self) -> None: with Image.open( "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" ) as im: @@ -1019,21 +1020,21 @@ def test_open_missing_samplesperpixel(self): ), ], ) - def test_wrong_bits_per_sample(self, file_name, mode, size, tile): + def test_wrong_bits_per_sample(self, file_name, mode, size, tile) -> None: with Image.open("Tests/images/" + file_name) as im: assert im.mode == mode assert im.size == size assert im.tile == tile im.load() - def test_no_rows_per_strip(self): + def test_no_rows_per_strip(self) -> None: # This image does not have a RowsPerStrip TIFF tag infile = "Tests/images/no_rows_per_strip.tif" with Image.open(infile) as im: im.load() assert im.size == (950, 975) - def test_orientation(self): + def test_orientation(self) -> None: with Image.open("Tests/images/g4_orientation_1.tif") as base_im: for i in range(2, 9): with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: @@ -1044,7 +1045,7 @@ def test_orientation(self): assert_image_similar(base_im, im, 0.7) - def test_exif_transpose(self): + def test_exif_transpose(self) -> None: with Image.open("Tests/images/g4_orientation_1.tif") as base_im: for i in range(2, 9): with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: @@ -1053,7 +1054,7 @@ def test_exif_transpose(self): assert_image_similar(base_im, im, 0.7) @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") - def test_sampleformat_not_corrupted(self): + def test_sampleformat_not_corrupted(self) -> None: # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted # when saving to a new file. # Pillow 6.0 fails with "OSError: cannot identify image file". @@ -1074,7 +1075,7 @@ def test_sampleformat_not_corrupted(self): with Image.open(out) as im: im.load() - def test_realloc_overflow(self): + def test_realloc_overflow(self) -> None: TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: with pytest.raises(OSError) as e: @@ -1085,7 +1086,7 @@ def test_realloc_overflow(self): TiffImagePlugin.READ_LIBTIFF = False @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) - def test_save_multistrip(self, compression, tmp_path): + def test_save_multistrip(self, compression, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") im.save(out, compression=compression) @@ -1095,7 +1096,7 @@ def test_save_multistrip(self, compression, tmp_path): assert len(im.tag_v2[STRIPOFFSETS]) > 1 @pytest.mark.parametrize("argument", (True, False)) - def test_save_single_strip(self, argument, tmp_path): + def test_save_single_strip(self, argument, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") @@ -1113,13 +1114,13 @@ def test_save_single_strip(self, argument, tmp_path): TiffImagePlugin.STRIP_SIZE = 65536 @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) - def test_save_zero(self, compression, tmp_path): + def test_save_zero(self, compression, tmp_path: Path) -> None: im = Image.new("RGB", (0, 0)) out = str(tmp_path / "temp.tif") with pytest.raises(SystemError): im.save(out, compression=compression) - def test_save_many_compressed(self, tmp_path): + def test_save_many_compressed(self, tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.tif") for _ in range(10000): @@ -1133,7 +1134,7 @@ def test_save_many_compressed(self, tmp_path): ("Tests/images/child_ifd_jpeg.tiff", (20,)), ), ) - def test_get_child_images(self, path, sizes): + def test_get_child_images(self, path, sizes) -> None: with Image.open(path) as im: ims = im.get_child_images() diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 171e4a3f866..ac5270eac30 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from pathlib import Path from PIL import Image @@ -17,7 +18,7 @@ class TestFileLibTiffSmall(LibTiffTestCase): file just before reading in libtiff. These tests remain to ensure that it stays fixed.""" - def test_g4_hopper_file(self, tmp_path): + def test_g4_hopper_file(self, tmp_path: Path) -> None: """Testing the open file load path""" test_file = "Tests/images/hopper_g4.tif" @@ -26,7 +27,7 @@ def test_g4_hopper_file(self, tmp_path): assert im.size == (128, 128) self._assert_noerr(tmp_path, im) - def test_g4_hopper_bytesio(self, tmp_path): + def test_g4_hopper_bytesio(self, tmp_path: Path) -> None: """Testing the bytesio loading code path""" test_file = "Tests/images/hopper_g4.tif" s = BytesIO() @@ -37,7 +38,7 @@ def test_g4_hopper_bytesio(self, tmp_path): assert im.size == (128, 128) self._assert_noerr(tmp_path, im) - def test_g4_hopper(self, tmp_path): + def test_g4_hopper(self, tmp_path: Path) -> None: """The 128x128 lena image failed for some reason.""" test_file = "Tests/images/hopper_g4.tif" diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 8c43f7d7ab7..9a6f13ea366 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -13,7 +13,7 @@ TEST_FILE = "Tests/images/hopper.mic" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "RGBA" @@ -28,22 +28,22 @@ def test_sanity(): assert_image_similar(im, im2, 10) -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_FILE) as im: assert im.n_frames == 1 -def test_is_animated(): +def test_is_animated() -> None: with Image.open(TEST_FILE) as im: assert not im.is_animated -def test_tell(): +def test_tell() -> None: with Image.open(TEST_FILE) as im: assert im.tell() == 0 -def test_seek(): +def test_seek() -> None: with Image.open(TEST_FILE) as im: im.seek(0) assert im.tell() == 0 @@ -53,7 +53,7 @@ def test_seek(): assert im.tell() == 0 -def test_close(): +def test_close() -> None: with Image.open(TEST_FILE) as im: pass assert im.ole.fp.closed @@ -63,7 +63,7 @@ def test_close(): assert im.ole.fp.closed -def test_invalid_file(): +def test_invalid_file() -> None: # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index c7121ea28c9..55b04a1e076 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -30,7 +30,7 @@ def roundtrip(im, **options): @pytest.mark.parametrize("test_file", test_files) -def test_sanity(test_file): +def test_sanity(test_file) -> None: with Image.open(test_file) as im: im.load() assert im.mode == "RGB" @@ -39,8 +39,8 @@ def test_sanity(test_file): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(test_files[0]) im.load() @@ -48,14 +48,14 @@ def open(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(test_files[0]) im.load() im.close() -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open(test_files[0]) im.close() @@ -63,14 +63,14 @@ def test_seek_after_close(): im.seek(1) -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(test_files[0]) as im: im.load() @pytest.mark.parametrize("test_file", test_files) -def test_app(test_file): +def test_app(test_file) -> None: # Test APP/COM reader (@PIL135) with Image.open(test_file) as im: assert im.applist[0][0] == "APP1" @@ -82,7 +82,7 @@ def test_app(test_file): @pytest.mark.parametrize("test_file", test_files) -def test_exif(test_file): +def test_exif(test_file) -> None: with Image.open(test_file) as im_original: im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) @@ -93,7 +93,7 @@ def test_exif(test_file): assert info[34665] == 188 -def test_frame_size(): +def test_frame_size() -> None: # This image has been hexedited to contain a different size # in the EXIF data of the second frame with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: @@ -106,7 +106,7 @@ def test_frame_size(): assert im.size == (640, 480) -def test_ignore_frame_size(): +def test_ignore_frame_size() -> None: # Ignore the different size of the second frame # since this is not a "Large Thumbnail" image with Image.open("Tests/images/ignore_frame_size.mpo") as im: @@ -120,7 +120,7 @@ def test_ignore_frame_size(): assert im.size == (64, 64) -def test_parallax(): +def test_parallax() -> None: # Nintendo with Image.open("Tests/images/sugarshack.mpo") as im: exif = im.getexif() @@ -133,7 +133,7 @@ def test_parallax(): assert exif.get_ifd(0x927C)[0xB211] == -3.125 -def test_reload_exif_after_seek(): +def test_reload_exif_after_seek() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: exif = im.getexif() del exif[296] @@ -143,14 +143,14 @@ def test_reload_exif_after_seek(): @pytest.mark.parametrize("test_file", test_files) -def test_mp(test_file): +def test_mp(test_file) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 -def test_mp_offset(): +def test_mp_offset() -> None: # This image has been manually hexedited to have an IFD offset of 10 # in APP2 data, in contrast to normal 8 with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: @@ -159,7 +159,7 @@ def test_mp_offset(): assert mpinfo[45057] == 2 -def test_mp_no_data(): +def test_mp_no_data() -> None: # This image has been manually hexedited to have the second frame # beyond the end of the file with Image.open("Tests/images/sugarshack_no_data.mpo") as im: @@ -168,7 +168,7 @@ def test_mp_no_data(): @pytest.mark.parametrize("test_file", test_files) -def test_mp_attribute(test_file): +def test_mp_attribute(test_file) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() for frame_number, mpentry in enumerate(mpinfo[0xB002]): @@ -185,7 +185,7 @@ def test_mp_attribute(test_file): @pytest.mark.parametrize("test_file", test_files) -def test_seek(test_file): +def test_seek(test_file) -> None: with Image.open(test_file) as im: assert im.tell() == 0 # prior to first image raises an error, both blatant and borderline @@ -209,13 +209,13 @@ def test_seek(test_file): assert im.tell() == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: assert im.n_frames == 2 assert im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: n_frames = im.n_frames @@ -229,7 +229,7 @@ def test_eoferror(): @pytest.mark.parametrize("test_file", test_files) -def test_image_grab(test_file): +def test_image_grab(test_file) -> None: with Image.open(test_file) as im: assert im.tell() == 0 im0 = im.tobytes() @@ -244,7 +244,7 @@ def test_image_grab(test_file): @pytest.mark.parametrize("test_file", test_files) -def test_save(test_file): +def test_save(test_file) -> None: with Image.open(test_file) as im: assert im.tell() == 0 jpg0 = roundtrip(im) @@ -255,7 +255,7 @@ def test_save(test_file): assert_image_similar(im, jpg1, 30) -def test_save_all(): +def test_save_all() -> None: for test_file in test_files: with Image.open(test_file) as im: im_reloaded = roundtrip(im, save_all=True) diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 9037ea33b54..f9f81d11413 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from pathlib import Path import pytest @@ -13,7 +14,7 @@ YA_EXTRA_DIR = "Tests/images/msp" -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.msp") hopper("1").save(test_file) @@ -25,14 +26,14 @@ def test_sanity(tmp_path): assert im.format == "MSP" -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): MspImagePlugin.MspImageFile(invalid_file) -def test_bad_checksum(): +def test_bad_checksum() -> None: # Arrange # This was created by forcing Pillow to save with checksum=0 bad_checksum = "Tests/images/hopper_bad_checksum.msp" @@ -42,7 +43,7 @@ def test_bad_checksum(): MspImagePlugin.MspImageFile(bad_checksum) -def test_open_windows_v1(): +def test_open_windows_v1() -> None: # Arrange # Act with Image.open(TEST_FILE) as im: @@ -51,7 +52,7 @@ def test_open_windows_v1(): assert isinstance(im, MspImagePlugin.MspImageFile) -def _assert_file_image_equal(source_path, target_path): +def _assert_file_image_equal(source_path, target_path) -> None: with Image.open(source_path) as im: assert_image_equal_tofile(im, target_path) @@ -59,7 +60,7 @@ def _assert_file_image_equal(source_path, target_path): @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) -def test_open_windows_v2(): +def test_open_windows_v2() -> None: files = ( os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) @@ -72,7 +73,7 @@ def test_open_windows_v2(): @pytest.mark.skipif( not os.path.exists(YA_EXTRA_DIR), reason="Even More Extra image files not installed" ) -def test_msp_v2(): +def test_msp_v2() -> None: for f in os.listdir(YA_EXTRA_DIR): if ".MSP" not in f: continue @@ -80,7 +81,7 @@ def test_msp_v2(): _assert_file_image_equal(path, path.replace(".MSP", ".png")) -def test_cannot_save_wrong_mode(tmp_path): +def test_cannot_save_wrong_mode(tmp_path: Path) -> None: # Arrange im = hopper() filename = str(tmp_path / "temp.msp") diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index eba69415395..55041a4b202 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -2,6 +2,7 @@ import os.path import subprocess +from pathlib import Path import pytest @@ -10,7 +11,7 @@ from .helper import assert_image_equal, hopper, magick_command -def helper_save_as_palm(tmp_path, mode): +def helper_save_as_palm(tmp_path: Path, mode) -> None: # Arrange im = hopper(mode) outfile = str(tmp_path / ("temp_" + mode + ".palm")) @@ -23,7 +24,7 @@ def helper_save_as_palm(tmp_path, mode): assert os.path.getsize(outfile) > 0 -def open_with_magick(magick, tmp_path, f): +def open_with_magick(magick, tmp_path: Path, f): outfile = str(tmp_path / "temp.png") rc = subprocess.call( magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT @@ -32,7 +33,7 @@ def open_with_magick(magick, tmp_path, f): return Image.open(outfile) -def roundtrip(tmp_path, mode): +def roundtrip(tmp_path: Path, mode) -> None: magick = magick_command() if not magick: return @@ -45,7 +46,7 @@ def roundtrip(tmp_path, mode): assert_image_equal(converted, im) -def test_monochrome(tmp_path): +def test_monochrome(tmp_path: Path) -> None: # Arrange mode = "1" @@ -55,7 +56,7 @@ def test_monochrome(tmp_path): @pytest.mark.xfail(reason="Palm P image is wrong") -def test_p_mode(tmp_path): +def test_p_mode(tmp_path: Path) -> None: # Arrange mode = "P" @@ -65,6 +66,6 @@ def test_p_mode(tmp_path): @pytest.mark.parametrize("mode", ("L", "RGB")) -def test_oserror(tmp_path, mode): +def test_oserror(tmp_path: Path, mode) -> None: with pytest.raises(OSError): helper_save_as_palm(tmp_path, mode) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 2565e0b6ddc..a2486be40c5 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImageFile, PcxImagePlugin @@ -7,7 +9,7 @@ from .helper import assert_image_equal, hopper -def _roundtrip(tmp_path, im): +def _roundtrip(tmp_path: Path, im) -> None: f = str(tmp_path / "temp.pcx") im.save(f) with Image.open(f) as im2: @@ -18,7 +20,7 @@ def _roundtrip(tmp_path, im): assert_image_equal(im2, im) -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: for mode in ("1", "L", "P", "RGB"): _roundtrip(tmp_path, hopper(mode)) @@ -34,7 +36,7 @@ def test_sanity(tmp_path): im.save(f) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): @@ -42,7 +44,7 @@ def test_invalid_file(): @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) -def test_odd(tmp_path, mode): +def test_odd(tmp_path: Path, mode) -> None: # See issue #523, odd sized images should have a stride that's even. # Not that ImageMagick or GIMP write PCX that way. # We were not handling properly. @@ -51,7 +53,7 @@ def test_odd(tmp_path, mode): _roundtrip(tmp_path, hopper(mode).resize((511, 511))) -def test_odd_read(): +def test_odd_read() -> None: # Reading an image with an odd stride, making it malformed with Image.open("Tests/images/odd_stride.pcx") as im: im.load() @@ -59,7 +61,7 @@ def test_odd_read(): assert im.size == (371, 150) -def test_pil184(): +def test_pil184() -> None: # Check reading of files where xmin/xmax is not zero. test_file = "Tests/images/pil184.pcx" @@ -71,7 +73,7 @@ def test_pil184(): assert im.histogram()[0] + im.histogram()[255] == 447 * 144 -def test_1px_width(tmp_path): +def test_1px_width(tmp_path: Path) -> None: im = Image.new("L", (1, 256)) px = im.load() for y in range(256): @@ -79,7 +81,7 @@ def test_1px_width(tmp_path): _roundtrip(tmp_path, im) -def test_large_count(tmp_path): +def test_large_count(tmp_path: Path) -> None: im = Image.new("L", (256, 1)) px = im.load() for x in range(256): @@ -87,7 +89,7 @@ def test_large_count(tmp_path): _roundtrip(tmp_path, im) -def _test_buffer_overflow(tmp_path, im, size=1024): +def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None: _last = ImageFile.MAXBLOCK ImageFile.MAXBLOCK = size try: @@ -96,7 +98,7 @@ def _test_buffer_overflow(tmp_path, im, size=1024): ImageFile.MAXBLOCK = _last -def test_break_in_count_overflow(tmp_path): +def test_break_in_count_overflow(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(4): @@ -105,7 +107,7 @@ def test_break_in_count_overflow(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_one_in_loop(tmp_path): +def test_break_one_in_loop(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(5): @@ -114,7 +116,7 @@ def test_break_one_in_loop(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_many_in_loop(tmp_path): +def test_break_many_in_loop(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(4): @@ -125,7 +127,7 @@ def test_break_many_in_loop(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_one_at_end(tmp_path): +def test_break_one_at_end(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(5): @@ -135,7 +137,7 @@ def test_break_one_at_end(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_many_at_end(tmp_path): +def test_break_many_at_end(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(5): @@ -147,7 +149,7 @@ def test_break_many_at_end(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_padding(tmp_path): +def test_break_padding(tmp_path: Path) -> None: im = Image.new("L", (257, 5)) px = im.load() for y in range(5): diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 30c54c963cb..65a93c138e9 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -5,6 +5,7 @@ import os.path import tempfile import time +from pathlib import Path import pytest @@ -13,7 +14,7 @@ from .helper import hopper, mark_if_feature_version, skip_unless_feature -def helper_save_as_pdf(tmp_path, mode, **kwargs): +def helper_save_as_pdf(tmp_path: Path, mode, **kwargs): # Arrange im = hopper(mode) outfile = str(tmp_path / ("temp_" + mode + ".pdf")) @@ -40,17 +41,17 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs): @pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) -def test_save(tmp_path, mode): +def test_save(tmp_path: Path, mode) -> None: helper_save_as_pdf(tmp_path, mode) @skip_unless_feature("jpg_2000") @pytest.mark.parametrize("mode", ("LA", "RGBA")) -def test_save_alpha(tmp_path, mode): +def test_save_alpha(tmp_path: Path, mode) -> None: helper_save_as_pdf(tmp_path, mode) -def test_p_alpha(tmp_path): +def test_p_alpha(tmp_path: Path) -> None: # Arrange outfile = str(tmp_path / "temp.pdf") with Image.open("Tests/images/pil123p.png") as im: @@ -66,7 +67,7 @@ def test_p_alpha(tmp_path): assert b"\n/SMask " in contents -def test_monochrome(tmp_path): +def test_monochrome(tmp_path: Path) -> None: # Arrange mode = "1" @@ -75,7 +76,7 @@ def test_monochrome(tmp_path): assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000) -def test_unsupported_mode(tmp_path): +def test_unsupported_mode(tmp_path: Path) -> None: im = hopper("PA") outfile = str(tmp_path / "temp_PA.pdf") @@ -83,7 +84,7 @@ def test_unsupported_mode(tmp_path): im.save(outfile) -def test_resolution(tmp_path): +def test_resolution(tmp_path: Path) -> None: im = hopper() outfile = str(tmp_path / "temp.pdf") @@ -111,7 +112,7 @@ def test_resolution(tmp_path): {"dpi": (75, 150), "resolution": 200}, ), ) -def test_dpi(params, tmp_path): +def test_dpi(params, tmp_path: Path) -> None: im = hopper() outfile = str(tmp_path / "temp.pdf") @@ -135,7 +136,7 @@ def test_dpi(params, tmp_path): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_save_all(tmp_path): +def test_save_all(tmp_path: Path) -> None: # Single frame image helper_save_as_pdf(tmp_path, "RGB", save_all=True) @@ -171,7 +172,7 @@ def im_generator(ims): assert os.path.getsize(outfile) > 0 -def test_multiframe_normal_save(tmp_path): +def test_multiframe_normal_save(tmp_path: Path) -> None: # Test saving a multiframe image without save_all with Image.open("Tests/images/dispose_bgnd.gif") as im: outfile = str(tmp_path / "temp.pdf") @@ -181,7 +182,7 @@ def test_multiframe_normal_save(tmp_path): assert os.path.getsize(outfile) > 0 -def test_pdf_open(tmp_path): +def test_pdf_open(tmp_path: Path) -> None: # fail on a buffer full of null bytes with pytest.raises(PdfParser.PdfFormatError): PdfParser.PdfParser(buf=bytearray(65536)) @@ -218,14 +219,14 @@ def test_pdf_open(tmp_path): assert not hopper_pdf.should_close_file -def test_pdf_append_fails_on_nonexistent_file(): +def test_pdf_append_fails_on_nonexistent_file() -> None: im = hopper("RGB") with tempfile.TemporaryDirectory() as temp_dir: with pytest.raises(OSError): im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) -def check_pdf_pages_consistency(pdf): +def check_pdf_pages_consistency(pdf) -> None: pages_info = pdf.read_indirect(pdf.pages_ref) assert b"Parent" not in pages_info assert b"Kids" in pages_info @@ -243,7 +244,7 @@ def check_pdf_pages_consistency(pdf): assert kids_not_used == [] -def test_pdf_append(tmp_path): +def test_pdf_append(tmp_path: Path) -> None: # make a PDF file pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser") @@ -294,7 +295,7 @@ def test_pdf_append(tmp_path): check_pdf_pages_consistency(pdf) -def test_pdf_info(tmp_path): +def test_pdf_info(tmp_path: Path) -> None: # make a PDF file pdf_filename = helper_save_as_pdf( tmp_path, @@ -323,7 +324,7 @@ def test_pdf_info(tmp_path): check_pdf_pages_consistency(pdf) -def test_pdf_append_to_bytesio(): +def test_pdf_append_to_bytesio() -> None: im = hopper("RGB") f = io.BytesIO() im.save(f, format="PDF") @@ -338,7 +339,7 @@ def test_pdf_append_to_bytesio(): @pytest.mark.timeout(1) @pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") @pytest.mark.parametrize("newline", (b"\r", b"\n")) -def test_redos(newline): +def test_redos(newline) -> None: malicious = b" trailer<<>>" + newline * 3456 # This particular exception isn't relevant here. diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ae2a4772b60..0f1d96365ea 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -5,6 +5,7 @@ import warnings import zlib from io import BytesIO +from pathlib import Path import pytest @@ -79,7 +80,7 @@ def get_chunks(self, filename): png.crc(cid, s) return chunks - def test_sanity(self, tmp_path): + def test_sanity(self, tmp_path: Path) -> None: # internal version number assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib")) @@ -102,13 +103,13 @@ def test_sanity(self, tmp_path): reloaded = reloaded.convert(mode) assert_image_equal(reloaded, im) - def test_invalid_file(self): + def test_invalid_file(self) -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): PngImagePlugin.PngImageFile(invalid_file) - def test_broken(self): + def test_broken(self) -> None: # Check reading of totally broken files. In this case, the test # file was checked into Subversion as a text file. @@ -117,7 +118,7 @@ def test_broken(self): with Image.open(test_file): pass - def test_bad_text(self): + def test_bad_text(self) -> None: # Make sure PIL can read malformed tEXt chunks (@PIL152) im = load(HEAD + chunk(b"tEXt") + TAIL) @@ -135,7 +136,7 @@ def test_bad_text(self): im = load(HEAD + chunk(b"tEXt", b"spam\0egg\0") + TAIL) assert im.info == {"spam": "egg\x00"} - def test_bad_ztxt(self): + def test_bad_ztxt(self) -> None: # Test reading malformed zTXt chunks (python-pillow/Pillow#318) im = load(HEAD + chunk(b"zTXt") + TAIL) @@ -156,7 +157,7 @@ def test_bad_ztxt(self): im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")) + TAIL) assert im.info == {"spam": "egg"} - def test_bad_itxt(self): + def test_bad_itxt(self) -> None: im = load(HEAD + chunk(b"iTXt") + TAIL) assert im.info == {} @@ -200,7 +201,7 @@ def test_bad_itxt(self): assert im.info["spam"].lang == "en" assert im.info["spam"].tkey == "Spam" - def test_interlace(self): + def test_interlace(self) -> None: test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) @@ -215,7 +216,7 @@ def test_interlace(self): im.load() - def test_load_transparent_p(self): + def test_load_transparent_p(self) -> None: test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) @@ -225,7 +226,7 @@ def test_load_transparent_p(self): # image has 124 unique alpha values assert len(im.getchannel("A").getcolors()) == 124 - def test_load_transparent_rgb(self): + def test_load_transparent_rgb(self) -> None: test_file = "Tests/images/rgb_trns.png" with Image.open(test_file) as im: assert im.info["transparency"] == (0, 255, 52) @@ -237,7 +238,7 @@ def test_load_transparent_rgb(self): # image has 876 transparent pixels assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_p_transparent_palette(self, tmp_path): + def test_save_p_transparent_palette(self, tmp_path: Path) -> None: in_file = "Tests/images/pil123p.png" with Image.open(in_file) as im: # 'transparency' contains a byte string with the opacity for @@ -258,7 +259,7 @@ def test_save_p_transparent_palette(self, tmp_path): # image has 124 unique alpha values assert len(im.getchannel("A").getcolors()) == 124 - def test_save_p_single_transparency(self, tmp_path): + def test_save_p_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/p_trns_single.png" with Image.open(in_file) as im: # pixel value 164 is full transparent @@ -281,7 +282,7 @@ def test_save_p_single_transparency(self, tmp_path): # image has 876 transparent pixels assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_p_transparent_black(self, tmp_path): + def test_save_p_transparent_black(self, tmp_path: Path) -> None: # check if solid black image with full transparency # is supported (check for #1838) im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) @@ -299,7 +300,7 @@ def test_save_p_transparent_black(self, tmp_path): assert_image(im, "RGBA", (10, 10)) assert im.getcolors() == [(100, (0, 0, 0, 0))] - def test_save_grayscale_transparency(self, tmp_path): + def test_save_grayscale_transparency(self, tmp_path: Path) -> None: for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items(): in_file = "Tests/images/" + mode.lower() + "_trns.png" with Image.open(in_file) as im: @@ -320,13 +321,13 @@ def test_save_grayscale_transparency(self, tmp_path): test_im_rgba = test_im.convert("RGBA") assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent - def test_save_rgb_single_transparency(self, tmp_path): + def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/caption_6_33_22.png" with Image.open(in_file) as im: test_file = str(tmp_path / "temp.png") im.save(test_file) - def test_load_verify(self): + def test_load_verify(self) -> None: # Check open/load/verify exception (@PIL150) with Image.open(TEST_PNG_FILE) as im: @@ -339,7 +340,7 @@ def test_load_verify(self): with pytest.raises(RuntimeError): im.verify() - def test_verify_struct_error(self): + def test_verify_struct_error(self) -> None: # Check open/load/verify exception (#1755) # offsets to test, -10: breaks in i32() in read. (OSError) @@ -355,7 +356,7 @@ def test_verify_struct_error(self): with pytest.raises((OSError, SyntaxError)): im.verify() - def test_verify_ignores_crc_error(self): + def test_verify_ignores_crc_error(self) -> None: # check ignores crc errors in ancillary chunks chunk_data = chunk(b"tEXt", b"spam") @@ -372,7 +373,7 @@ def test_verify_ignores_crc_error(self): finally: ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_verify_not_ignores_crc_error_in_required_chunk(self): + def test_verify_not_ignores_crc_error_in_required_chunk(self) -> None: # check does not ignore crc errors in required chunks image_data = MAGIC + IHDR[:-1] + b"q" + TAIL @@ -384,18 +385,18 @@ def test_verify_not_ignores_crc_error_in_required_chunk(self): finally: ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_roundtrip_dpi(self): + def test_roundtrip_dpi(self) -> None: # Check dpi roundtripping with Image.open(TEST_PNG_FILE) as im: im = roundtrip(im, dpi=(100.33, 100.33)) assert im.info["dpi"] == (100.33, 100.33) - def test_load_float_dpi(self): + def test_load_float_dpi(self) -> None: with Image.open(TEST_PNG_FILE) as im: assert im.info["dpi"] == (95.9866, 95.9866) - def test_roundtrip_text(self): + def test_roundtrip_text(self) -> None: # Check text roundtripping with Image.open(TEST_PNG_FILE) as im: @@ -407,7 +408,7 @@ def test_roundtrip_text(self): assert im.info == {"TXT": "VALUE", "ZIP": "VALUE"} assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} - def test_roundtrip_itxt(self): + def test_roundtrip_itxt(self) -> None: # Check iTXt roundtripping im = Image.new("RGB", (32, 32)) @@ -423,7 +424,7 @@ def test_roundtrip_itxt(self): assert im.text["eggs"].lang == "en" assert im.text["eggs"].tkey == "Eggs" - def test_nonunicode_text(self): + def test_nonunicode_text(self) -> None: # Check so that non-Unicode text is saved as a tEXt rather than iTXt im = Image.new("RGB", (32, 32)) @@ -432,10 +433,10 @@ def test_nonunicode_text(self): im = roundtrip(im, pnginfo=info) assert isinstance(im.info["Text"], str) - def test_unicode_text(self): + def test_unicode_text(self) -> None: # Check preservation of non-ASCII characters - def rt_text(value): + def rt_text(value) -> None: im = Image.new("RGB", (32, 32)) info = PngImagePlugin.PngInfo() info.add_text("Text", value) @@ -448,7 +449,7 @@ def rt_text(value): rt_text(chr(0x4E00) + chr(0x66F0) + chr(0x9FBA) + chr(0x3042) + chr(0xAC00)) rt_text("A" + chr(0xC4) + chr(0x472) + chr(0x3042)) # Combined - def test_scary(self): + def test_scary(self) -> None: # Check reading of evil PNG file. For information, see: # http://scary.beasts.org/security/CESA-2004-001.txt # The first byte is removed from pngtest_bad.png @@ -462,7 +463,7 @@ def test_scary(self): with Image.open(pngfile): pass - def test_trns_rgb(self): + def test_trns_rgb(self) -> None: # Check writing and reading of tRNS chunks for RGB images. # Independent file sample provided by Sebastian Spaeth. @@ -477,7 +478,7 @@ def test_trns_rgb(self): im = roundtrip(im, transparency=(0, 1, 2)) assert im.info["transparency"] == (0, 1, 2) - def test_trns_p(self, tmp_path): + def test_trns_p(self, tmp_path: Path) -> None: # Check writing a transparency of 0, issue #528 im = hopper("P") im.info["transparency"] = 0 @@ -490,13 +491,13 @@ def test_trns_p(self, tmp_path): assert_image_equal(im2.convert("RGBA"), im.convert("RGBA")) - def test_trns_null(self): + def test_trns_null(self) -> None: # Check reading images with null tRNS value, issue #1239 test_file = "Tests/images/tRNS_null_1x1.png" with Image.open(test_file) as im: assert im.info["transparency"] == 0 - def test_save_icc_profile(self): + def test_save_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile_none.png") as im: assert im.info["icc_profile"] is None @@ -506,40 +507,40 @@ def test_save_icc_profile(self): im = roundtrip(im, icc_profile=expected_icc) assert im.info["icc_profile"] == expected_icc - def test_discard_icc_profile(self): + def test_discard_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile.png") as im: assert "icc_profile" in im.info im = roundtrip(im, icc_profile=None) assert "icc_profile" not in im.info - def test_roundtrip_icc_profile(self): + def test_roundtrip_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile.png") as im: expected_icc = im.info["icc_profile"] im = roundtrip(im) assert im.info["icc_profile"] == expected_icc - def test_roundtrip_no_icc_profile(self): + def test_roundtrip_no_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile_none.png") as im: assert im.info["icc_profile"] is None im = roundtrip(im) assert "icc_profile" not in im.info - def test_repr_png(self): + def test_repr_png(self) -> None: im = hopper() with Image.open(BytesIO(im._repr_png_())) as repr_png: assert repr_png.format == "PNG" assert_image_equal(im, repr_png) - def test_repr_png_error_returns_none(self): + def test_repr_png_error_returns_none(self) -> None: im = hopper("F") assert im._repr_png_() is None - def test_chunk_order(self, tmp_path): + def test_chunk_order(self, tmp_path: Path) -> None: with Image.open("Tests/images/icc_profile.png") as im: test_file = str(tmp_path / "temp.png") im.convert("P").save(test_file, dpi=(100, 100)) @@ -560,17 +561,17 @@ def test_chunk_order(self, tmp_path): # pHYs - before IDAT assert chunks.index(b"pHYs") < chunks.index(b"IDAT") - def test_getchunks(self): + def test_getchunks(self) -> None: im = hopper() chunks = PngImagePlugin.getchunks(im) assert len(chunks) == 3 - def test_read_private_chunks(self): + def test_read_private_chunks(self) -> None: with Image.open("Tests/images/exif.png") as im: assert im.private_chunks == [(b"orNT", b"\x01")] - def test_roundtrip_private_chunk(self): + def test_roundtrip_private_chunk(self) -> None: # Check private chunk roundtripping with Image.open(TEST_PNG_FILE) as im: @@ -588,7 +589,7 @@ def test_roundtrip_private_chunk(self): (b"prIV", b"VALUE3", True), ] - def test_textual_chunks_after_idat(self): + def test_textual_chunks_after_idat(self) -> None: with Image.open("Tests/images/hopper.png") as im: assert "comment" in im.text for k, v in { @@ -615,7 +616,7 @@ def test_textual_chunks_after_idat(self): with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} - def test_padded_idat(self): + def test_padded_idat(self) -> None: # This image has been manually hexedited # so that the IDAT chunk has padding at the end # Set MAXBLOCK to the length of the actual data @@ -635,7 +636,7 @@ def test_padded_idat(self): @pytest.mark.parametrize( "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") ) - def test_truncated_chunks(self, cid): + def test_truncated_chunks(self, cid) -> None: fp = BytesIO() with PngImagePlugin.PngStream(fp) as png: with pytest.raises(ValueError): @@ -645,7 +646,7 @@ def test_truncated_chunks(self, cid): png.call(cid, 0, 0) ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_specify_bits(self, tmp_path): + def test_specify_bits(self, tmp_path: Path) -> None: im = hopper("P") out = str(tmp_path / "temp.png") @@ -654,7 +655,7 @@ def test_specify_bits(self, tmp_path): with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 48 - def test_plte_length(self, tmp_path): + def test_plte_length(self, tmp_path: Path) -> None: im = Image.new("P", (1, 1)) im.putpalette((1, 1, 1)) @@ -664,7 +665,7 @@ def test_plte_length(self, tmp_path): with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 3 - def test_getxmp(self): + def test_getxmp(self) -> None: with Image.open("Tests/images/color_snakes.png") as im: if ElementTree is None: with pytest.warns( @@ -679,7 +680,7 @@ def test_getxmp(self): assert description["PixelXDimension"] == "10" assert description["subject"]["Seq"] is None - def test_exif(self): + def test_exif(self) -> None: # With an EXIF chunk with Image.open("Tests/images/exif.png") as im: exif = im._getexif() @@ -705,7 +706,7 @@ def test_exif(self): exif = im.getexif() assert exif[274] == 3 - def test_exif_save(self, tmp_path): + def test_exif_save(self, tmp_path: Path) -> None: # Test exif is not saved from info test_file = str(tmp_path / "temp.png") with Image.open("Tests/images/exif.png") as im: @@ -725,7 +726,7 @@ def test_exif_save(self, tmp_path): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_exif_from_jpg(self, tmp_path): + def test_exif_from_jpg(self, tmp_path: Path) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: test_file = str(tmp_path / "temp.png") im.save(test_file, exif=im.getexif()) @@ -734,7 +735,7 @@ def test_exif_from_jpg(self, tmp_path): exif = reloaded._getexif() assert exif[305] == "Adobe Photoshop CS Macintosh" - def test_exif_argument(self, tmp_path): + def test_exif_argument(self, tmp_path: Path) -> None: with Image.open(TEST_PNG_FILE) as im: test_file = str(tmp_path / "temp.png") im.save(test_file, exif=b"exifstring") @@ -742,11 +743,11 @@ def test_exif_argument(self, tmp_path): with Image.open(test_file) as reloaded: assert reloaded.info["exif"] == b"Exif\x00\x00exifstring" - def test_tell(self): + def test_tell(self) -> None: with Image.open(TEST_PNG_FILE) as im: assert im.tell() == 0 - def test_seek(self): + def test_seek(self) -> None: with Image.open(TEST_PNG_FILE) as im: im.seek(0) @@ -754,7 +755,7 @@ def test_seek(self): im.seek(1) @pytest.mark.parametrize("buffer", (True, False)) - def test_save_stdout(self, buffer): + def test_save_stdout(self, buffer) -> None: old_stdout = sys.stdout if buffer: @@ -786,7 +787,7 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): mem_limit = 2 * 1024 # max increase in K iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs - def test_leak_load(self): + def test_leak_load(self) -> None: with open("Tests/images/hopper.png", "rb") as f: DATA = BytesIO(f.read(16 * 1024)) @@ -794,7 +795,7 @@ def test_leak_load(self): with Image.open(DATA) as im: im.load() - def core(): + def core() -> None: with Image.open(DATA) as im: im.load() diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 32de42ed45d..94f66ee7d28 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -2,6 +2,7 @@ import sys from io import BytesIO +from pathlib import Path import pytest @@ -18,7 +19,7 @@ TEST_FILE = "Tests/images/hopper.ppm" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: assert im.mode == "RGB" assert im.size == (128, 128) @@ -69,7 +70,7 @@ def test_sanity(): ), ), ) -def test_arbitrary_maxval(data, mode, pixels): +def test_arbitrary_maxval(data, mode, pixels) -> None: fp = BytesIO(data) with Image.open(fp) as im: assert im.size == (3, 1) @@ -79,7 +80,7 @@ def test_arbitrary_maxval(data, mode, pixels): assert tuple(px[x, 0] for x in range(3)) == pixels -def test_16bit_pgm(): +def test_16bit_pgm() -> None: with Image.open("Tests/images/16_bit_binary.pgm") as im: assert im.mode == "I" assert im.size == (20, 100) @@ -88,7 +89,7 @@ def test_16bit_pgm(): assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.png") -def test_16bit_pgm_write(tmp_path): +def test_16bit_pgm_write(tmp_path: Path) -> None: with Image.open("Tests/images/16_bit_binary.pgm") as im: filename = str(tmp_path / "temp.pgm") im.save(filename, "PPM") @@ -96,7 +97,7 @@ def test_16bit_pgm_write(tmp_path): assert_image_equal_tofile(im, filename) -def test_pnm(tmp_path): +def test_pnm(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.pnm") as im: assert_image_similar(im, hopper(), 0.0001) @@ -106,7 +107,7 @@ def test_pnm(tmp_path): assert_image_equal_tofile(im, filename) -def test_pfm(tmp_path): +def test_pfm(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.pfm") as im: assert im.info["scale"] == 1.0 assert_image_equal(im, hopper("F")) @@ -117,7 +118,7 @@ def test_pfm(tmp_path): assert_image_equal_tofile(im, filename) -def test_pfm_big_endian(tmp_path): +def test_pfm_big_endian(tmp_path: Path) -> None: with Image.open("Tests/images/hopper_be.pfm") as im: assert im.info["scale"] == 2.5 assert_image_equal(im, hopper("F")) @@ -138,7 +139,7 @@ def test_pfm_big_endian(tmp_path): b"Pf 1 1 -0.0 \0\0\0\0", ], ) -def test_pfm_invalid(data): +def test_pfm_invalid(data) -> None: with pytest.raises(ValueError): with Image.open(BytesIO(data)): pass @@ -161,12 +162,12 @@ def test_pfm_invalid(data): ), ), ) -def test_plain(plain_path, raw_path): +def test_plain(plain_path, raw_path) -> None: with Image.open(plain_path) as im: assert_image_equal_tofile(im, raw_path) -def test_16bit_plain_pgm(): +def test_16bit_plain_pgm() -> None: # P2 with maxval 2 ** 16 - 1 with Image.open("Tests/images/hopper_16bit_plain.pgm") as im: assert im.mode == "I" @@ -185,7 +186,7 @@ def test_16bit_plain_pgm(): (b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6), ), ) -def test_plain_data_with_comment(tmp_path, header, data, comment_count): +def test_plain_data_with_comment(tmp_path: Path, header, data, comment_count) -> None: path1 = str(tmp_path / "temp1.ppm") path2 = str(tmp_path / "temp2.ppm") comment = b"# comment" * comment_count @@ -198,7 +199,7 @@ def test_plain_data_with_comment(tmp_path, header, data, comment_count): @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) -def test_plain_truncated_data(tmp_path, data): +def test_plain_truncated_data(tmp_path: Path, data) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -209,7 +210,7 @@ def test_plain_truncated_data(tmp_path, data): @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) -def test_plain_invalid_data(tmp_path, data): +def test_plain_invalid_data(tmp_path: Path, data) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -226,7 +227,7 @@ def test_plain_invalid_data(tmp_path, data): b"P3\n128 128\n255\n012345678910 0", # token too long ), ) -def test_plain_ppm_token_too_long(tmp_path, data): +def test_plain_ppm_token_too_long(tmp_path: Path, data) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -236,7 +237,7 @@ def test_plain_ppm_token_too_long(tmp_path, data): im.load() -def test_plain_ppm_value_too_large(tmp_path): +def test_plain_ppm_value_too_large(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P3\n128 128\n255\n256") @@ -246,12 +247,12 @@ def test_plain_ppm_value_too_large(tmp_path): im.load() -def test_magic(): +def test_magic() -> None: with pytest.raises(SyntaxError): PpmImagePlugin.PpmImageFile(fp=BytesIO(b"PyInvalid")) -def test_header_with_comments(tmp_path): +def test_header_with_comments(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") @@ -260,7 +261,7 @@ def test_header_with_comments(tmp_path): assert im.size == (128, 128) -def test_non_integer_token(tmp_path): +def test_non_integer_token(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\nTEST") @@ -270,7 +271,7 @@ def test_non_integer_token(tmp_path): pass -def test_header_token_too_long(tmp_path): +def test_header_token_too_long(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n 01234567890") @@ -282,7 +283,7 @@ def test_header_token_too_long(tmp_path): assert str(e.value) == "Token too long in file header: 01234567890" -def test_truncated_file(tmp_path): +def test_truncated_file(tmp_path: Path) -> None: # Test EOF in header path = str(tmp_path / "temp.pgm") with open(path, "wb") as f: @@ -301,7 +302,7 @@ def test_truncated_file(tmp_path): im.load() -def test_not_enough_image_data(tmp_path): +def test_not_enough_image_data(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P2 1 2 255 255") @@ -312,7 +313,7 @@ def test_not_enough_image_data(tmp_path): @pytest.mark.parametrize("maxval", (b"0", b"65536")) -def test_invalid_maxval(maxval, tmp_path): +def test_invalid_maxval(maxval, tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n3 1 " + maxval) @@ -324,7 +325,7 @@ def test_invalid_maxval(maxval, tmp_path): assert str(e.value) == "maxval must be greater than 0 and less than 65536" -def test_neg_ppm(): +def test_neg_ppm() -> None: # Storage.c accepted negative values for xsize, ysize. the # internal open_ppm function didn't check for sanity but it # has been removed. The default opener doesn't accept negative @@ -335,7 +336,7 @@ def test_neg_ppm(): pass -def test_mimetypes(tmp_path): +def test_mimetypes(tmp_path: Path) -> None: path = str(tmp_path / "temp.pgm") with open(path, "wb") as f: @@ -350,7 +351,7 @@ def test_mimetypes(tmp_path): @pytest.mark.parametrize("buffer", (True, False)) -def test_save_stdout(buffer): +def test_save_stdout(buffer) -> None: old_stdout = sys.stdout if buffer: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 16f049602bc..7eca8d9b151 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -11,7 +11,7 @@ test_file = "Tests/images/hopper.psd" -def test_sanity(): +def test_sanity() -> None: with Image.open(test_file) as im: im.load() assert im.mode == "RGB" @@ -24,8 +24,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(test_file) im.load() @@ -33,27 +33,27 @@ def open(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(test_file) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(test_file) as im: im.load() -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): PsdImagePlugin.PsdImageFile(invalid_file) -def test_n_frames(): +def test_n_frames() -> None: with Image.open("Tests/images/hopper_merged.psd") as im: assert im.n_frames == 1 assert not im.is_animated @@ -64,7 +64,7 @@ def test_n_frames(): assert im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(test_file) as im: # PSD seek index starts at 1 rather than 0 n_frames = im.n_frames + 1 @@ -78,7 +78,7 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_seek_tell(): +def test_seek_tell() -> None: with Image.open(test_file) as im: layer_number = im.tell() assert layer_number == 1 @@ -95,30 +95,30 @@ def test_seek_tell(): assert layer_number == 2 -def test_seek_eoferror(): +def test_seek_eoferror() -> None: with Image.open(test_file) as im: with pytest.raises(EOFError): im.seek(-1) -def test_open_after_exclusive_load(): +def test_open_after_exclusive_load() -> None: with Image.open(test_file) as im: im.load() im.seek(im.tell() + 1) im.load() -def test_rgba(): +def test_rgba() -> None: with Image.open("Tests/images/rgba.psd") as im: assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png") -def test_layer_skip(): +def test_layer_skip() -> None: with Image.open("Tests/images/five_channels.psd") as im: assert im.n_frames == 1 -def test_icc_profile(): +def test_icc_profile() -> None: with Image.open(test_file) as im: assert "icc_profile" in im.info @@ -126,12 +126,12 @@ def test_icc_profile(): assert len(icc_profile) == 3144 -def test_no_icc_profile(): +def test_no_icc_profile() -> None: with Image.open("Tests/images/hopper_merged.psd") as im: assert "icc_profile" not in im.info -def test_combined_larger_than_size(): +def test_combined_larger_than_size() -> None: # The combined size of the individual parts is larger than the # declared 'size' of the extra data field, resulting in a backwards seek. @@ -157,7 +157,7 @@ def test_combined_larger_than_size(): ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), ], ) -def test_crashes(test_file, raises): +def test_crashes(test_file, raises) -> None: with open(test_file, "rb") as f: with pytest.raises(raises): with Image.open(f): diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index bc45bbfd34d..92aea07350e 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, SgiImagePlugin @@ -12,7 +14,7 @@ ) -def test_rgb(): +def test_rgb() -> None: # Created with ImageMagick then renamed: # convert hopper.ppm -compress None sgi:hopper.rgb test_file = "Tests/images/hopper.rgb" @@ -22,11 +24,11 @@ def test_rgb(): assert im.get_format_mimetype() == "image/rgb" -def test_rgb16(): +def test_rgb16() -> None: assert_image_equal_tofile(hopper(), "Tests/images/hopper16.rgb") -def test_l(): +def test_l() -> None: # Created with ImageMagick # convert hopper.ppm -monochrome -compress None sgi:hopper.bw test_file = "Tests/images/hopper.bw" @@ -36,7 +38,7 @@ def test_l(): assert im.get_format_mimetype() == "image/sgi" -def test_rgba(): +def test_rgba() -> None: # Created with ImageMagick: # convert transparent.png -compress None transparent.sgi test_file = "Tests/images/transparent.sgi" @@ -46,7 +48,7 @@ def test_rgba(): assert im.get_format_mimetype() == "image/sgi" -def test_rle(): +def test_rle() -> None: # Created with ImageMagick: # convert hopper.ppm hopper.sgi test_file = "Tests/images/hopper.sgi" @@ -55,22 +57,22 @@ def test_rle(): assert_image_equal_tofile(im, "Tests/images/hopper.rgb") -def test_rle16(): +def test_rle16() -> None: test_file = "Tests/images/tv16.sgi" with Image.open(test_file) as im: assert_image_equal_tofile(im, "Tests/images/tv.rgb") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(ValueError): SgiImagePlugin.SgiImageFile(invalid_file) -def test_write(tmp_path): - def roundtrip(img): +def test_write(tmp_path: Path) -> None: + def roundtrip(img) -> None: out = str(tmp_path / "temp.sgi") img.save(out, format="sgi") assert_image_equal_tofile(img, out) @@ -89,7 +91,7 @@ def roundtrip(img): roundtrip(Image.new("L", (10, 1))) -def test_write16(tmp_path): +def test_write16(tmp_path: Path) -> None: test_file = "Tests/images/hopper16.rgb" with Image.open(test_file) as im: @@ -99,7 +101,7 @@ def test_write16(tmp_path): assert_image_equal_tofile(im, out) -def test_unsupported_mode(tmp_path): +def test_unsupported_mode(tmp_path: Path) -> None: im = hopper("LA") out = str(tmp_path / "temp.sgi") diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 42d833fb25c..75fef1dc6b6 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -3,6 +3,7 @@ import tempfile import warnings from io import BytesIO +from pathlib import Path import pytest @@ -13,7 +14,7 @@ TEST_FILE = "Tests/images/hopper.spider" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "F" @@ -22,8 +23,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_FILE) im.load() @@ -31,20 +32,20 @@ def open(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_FILE) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_FILE) as im: im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: # Arrange temp = str(tmp_path / "temp.spider") im = hopper() @@ -59,7 +60,7 @@ def test_save(tmp_path): assert im2.format == "SPIDER" -def test_tempfile(): +def test_tempfile() -> None: # Arrange im = hopper() @@ -75,11 +76,11 @@ def test_tempfile(): assert reloaded.format == "SPIDER" -def test_is_spider_image(): +def test_is_spider_image() -> None: assert SpiderImagePlugin.isSpiderImage(TEST_FILE) -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act @@ -89,13 +90,13 @@ def test_tell(): assert index == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_FILE) as im: assert im.n_frames == 1 assert not im.is_animated -def test_load_image_series(): +def test_load_image_series() -> None: # Arrange not_spider_file = "Tests/images/hopper.ppm" file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] @@ -109,7 +110,7 @@ def test_load_image_series(): assert img_list[0].size == (128, 128) -def test_load_image_series_no_input(): +def test_load_image_series_no_input() -> None: # Arrange file_list = None @@ -120,7 +121,7 @@ def test_load_image_series_no_input(): assert img_list is None -def test_is_int_not_a_number(): +def test_is_int_not_a_number() -> None: # Arrange not_a_number = "a" @@ -131,7 +132,7 @@ def test_is_int_not_a_number(): assert ret == 0 -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/invalid.spider" with pytest.raises(OSError): @@ -139,20 +140,20 @@ def test_invalid_file(): pass -def test_nonstack_file(): +def test_nonstack_file() -> None: with Image.open(TEST_FILE) as im: with pytest.raises(EOFError): im.seek(0) -def test_nonstack_dos(): +def test_nonstack_dos() -> None: with Image.open(TEST_FILE) as im: for i, frame in enumerate(ImageSequence.Iterator(im)): assert i <= 1, "Non-stack DOS file test failed" # for issue #4093 -def test_odd_size(): +def test_odd_size() -> None: data = BytesIO() width = 100 im = Image.new("F", (width, 64)) diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 41f3b7d98f3..6cfff87309c 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -11,7 +11,7 @@ EXTRA_DIR = "Tests/images/sunraster" -def test_sanity(): +def test_sanity() -> None: # Arrange # Created with ImageMagick: convert hopper.jpg hopper.ras test_file = "Tests/images/hopper.ras" @@ -28,7 +28,7 @@ def test_sanity(): SunImagePlugin.SunImageFile(invalid_file) -def test_im1(): +def test_im1() -> None: with Image.open("Tests/images/sunraster.im1") as im: assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png") @@ -36,7 +36,7 @@ def test_im1(): @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) -def test_others(): +def test_others() -> None: files = ( os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 58226c33062..44e78e972dc 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -19,7 +19,7 @@ ("jpg", "hopper.jpg", "JPEG"), ), ) -def test_sanity(codec, test_path, format): +def test_sanity(codec, test_path, format) -> None: if features.check(codec): with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: with Image.open(tar) as im: @@ -30,18 +30,18 @@ def test_sanity(codec, test_path, format): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): +def test_unclosed_file() -> None: with pytest.warns(ResourceWarning): TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") -def test_close(): +def test_close() -> None: with warnings.catch_warnings(): tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar.close() -def test_contextmanager(): +def test_contextmanager() -> None: with warnings.catch_warnings(): with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): pass diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index eafb61d30af..bd8e522c7b5 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -3,6 +3,7 @@ import os from glob import glob from itertools import product +from pathlib import Path import pytest @@ -21,8 +22,8 @@ @pytest.mark.parametrize("mode", _MODES) -def test_sanity(mode, tmp_path): - def roundtrip(original_im): +def test_sanity(mode, tmp_path: Path) -> None: + def roundtrip(original_im) -> None: out = str(tmp_path / "temp.tga") original_im.save(out, rle=rle) @@ -64,7 +65,7 @@ def roundtrip(original_im): roundtrip(original_im) -def test_palette_depth_16(tmp_path): +def test_palette_depth_16(tmp_path: Path) -> None: with Image.open("Tests/images/p_16.tga") as im: assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png") @@ -74,7 +75,7 @@ def test_palette_depth_16(tmp_path): assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png") -def test_id_field(): +def test_id_field() -> None: # tga file with id field test_file = "Tests/images/tga_id_field.tga" @@ -84,7 +85,7 @@ def test_id_field(): assert im.size == (100, 100) -def test_id_field_rle(): +def test_id_field_rle() -> None: # tga file with id field test_file = "Tests/images/rgb32rle.tga" @@ -94,7 +95,7 @@ def test_id_field_rle(): assert im.size == (199, 199) -def test_cross_scan_line(): +def test_cross_scan_line() -> None: with Image.open("Tests/images/cross_scan_line.tga") as im: assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png") @@ -103,7 +104,7 @@ def test_cross_scan_line(): im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: out = str(tmp_path / "temp.tga") @@ -120,7 +121,7 @@ def test_save(tmp_path): assert test_im.size == (100, 100) -def test_small_palette(tmp_path): +def test_small_palette(tmp_path: Path) -> None: im = Image.new("P", (1, 1)) colors = [0, 0, 0] im.putpalette(colors) @@ -132,7 +133,7 @@ def test_small_palette(tmp_path): assert reloaded.getpalette() == colors -def test_save_wrong_mode(tmp_path): +def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper("PA") out = str(tmp_path / "temp.tga") @@ -140,7 +141,7 @@ def test_save_wrong_mode(tmp_path): im.save(out) -def test_save_mapdepth(): +def test_save_mapdepth() -> None: # This image has been manually hexedited from 200x32_p_bl_raw.tga # to include an origin test_file = "Tests/images/200x32_p_bl_raw_origin.tga" @@ -148,7 +149,7 @@ def test_save_mapdepth(): assert_image_equal_tofile(im, "Tests/images/tga/common/200x32_p.png") -def test_save_id_section(tmp_path): +def test_save_id_section(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: out = str(tmp_path / "temp.tga") @@ -179,7 +180,7 @@ def test_save_id_section(tmp_path): assert "id_section" not in test_im.info -def test_save_orientation(tmp_path): +def test_save_orientation(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" out = str(tmp_path / "temp.tga") with Image.open(test_file) as im: @@ -190,7 +191,7 @@ def test_save_orientation(tmp_path): assert test_im.info["orientation"] == 1 -def test_horizontal_orientations(): +def test_horizontal_orientations() -> None: # These images have been manually hexedited to have the relevant orientations with Image.open("Tests/images/rgb32rle_top_right.tga") as im: assert im.load()[90, 90][:3] == (0, 0, 0) @@ -199,7 +200,7 @@ def test_horizontal_orientations(): assert im.load()[90, 90][:3] == (0, 255, 0) -def test_save_rle(tmp_path): +def test_save_rle(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: assert im.info["compression"] == "tga_rle" @@ -232,7 +233,7 @@ def test_save_rle(tmp_path): assert test_im.info["compression"] == "tga_rle" -def test_save_l_transparency(tmp_path): +def test_save_l_transparency(tmp_path: Path) -> None: # There are 559 transparent pixels in la.tga. num_transparent = 559 diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index f0995679b1f..a16b76e19f7 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -3,6 +3,7 @@ import os import warnings from io import BytesIO +from pathlib import Path import pytest @@ -26,7 +27,7 @@ class TestFileTiff: - def test_sanity(self, tmp_path): + def test_sanity(self, tmp_path: Path) -> None: filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename) @@ -58,21 +59,21 @@ def test_sanity(self, tmp_path): pass @pytest.mark.skipif(is_pypy(), reason="Requires CPython") - def test_unclosed_file(self): - def open(): + def test_unclosed_file(self) -> None: + def open() -> None: im = Image.open("Tests/images/multipage.tiff") im.load() with pytest.warns(ResourceWarning): open() - def test_closed_file(self): + def test_closed_file(self) -> None: with warnings.catch_warnings(): im = Image.open("Tests/images/multipage.tiff") im.load() im.close() - def test_seek_after_close(self): + def test_seek_after_close(self) -> None: im = Image.open("Tests/images/multipage.tiff") im.close() @@ -81,12 +82,12 @@ def test_seek_after_close(self): with pytest.raises(ValueError): im.seek(1) - def test_context_manager(self): + def test_context_manager(self) -> None: with warnings.catch_warnings(): with Image.open("Tests/images/multipage.tiff") as im: im.load() - def test_mac_tiff(self): + def test_mac_tiff(self) -> None: # Read RGBa images from macOS [@PIL136] filename = "Tests/images/pil136.tiff" @@ -98,7 +99,7 @@ def test_mac_tiff(self): assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) - def test_bigtiff(self, tmp_path): + def test_bigtiff(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper_bigtiff.tif") as im: assert_image_equal_tofile(im, "Tests/images/hopper.tif") @@ -109,13 +110,13 @@ def test_bigtiff(self, tmp_path): outfile = str(tmp_path / "temp.tif") im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) - def test_set_legacy_api(self): + def test_set_legacy_api(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() with pytest.raises(Exception) as e: ifd.legacy_api = None assert str(e.value) == "Not allowing setting of legacy api" - def test_xyres_tiff(self): + def test_xyres_tiff(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: # legacy api @@ -128,7 +129,7 @@ def test_xyres_tiff(self): assert im.info["dpi"] == (72.0, 72.0) - def test_xyres_fallback_tiff(self): + def test_xyres_fallback_tiff(self) -> None: filename = "Tests/images/compression.tif" with Image.open(filename) as im: # v2 api @@ -142,7 +143,7 @@ def test_xyres_fallback_tiff(self): # Fallback "inch". assert im.info["dpi"] == (100.0, 100.0) - def test_int_resolution(self): + def test_int_resolution(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: # Try to read a file where X,Y_RESOLUTION are ints @@ -155,14 +156,14 @@ def test_int_resolution(self): "resolution_unit, dpi", [(None, 72.8), (2, 72.8), (3, 184.912)], ) - def test_load_float_dpi(self, resolution_unit, dpi): + def test_load_float_dpi(self, resolution_unit, dpi) -> None: with Image.open( "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" ) as im: assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit assert im.info["dpi"] == (dpi, dpi) - def test_save_float_dpi(self, tmp_path): + def test_save_float_dpi(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: dpi = (72.2, 72.2) @@ -171,7 +172,7 @@ def test_save_float_dpi(self, tmp_path): with Image.open(outfile) as reloaded: assert reloaded.info["dpi"] == dpi - def test_save_setting_missing_resolution(self): + def test_save_setting_missing_resolution(self) -> None: b = BytesIO() with Image.open("Tests/images/10ct_32bit_128.tiff") as im: im.save(b, format="tiff", resolution=123.45) @@ -179,7 +180,7 @@ def test_save_setting_missing_resolution(self): assert im.tag_v2[X_RESOLUTION] == 123.45 assert im.tag_v2[Y_RESOLUTION] == 123.45 - def test_invalid_file(self): + def test_invalid_file(self) -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): @@ -190,30 +191,30 @@ def test_invalid_file(self): TiffImagePlugin.TiffImageFile(invalid_file) TiffImagePlugin.PREFIXES.pop() - def test_bad_exif(self): + def test_bad_exif(self) -> None: with Image.open("Tests/images/hopper_bad_exif.jpg") as i: # Should not raise struct.error. with pytest.warns(UserWarning): i._getexif() - def test_save_rgba(self, tmp_path): + def test_save_rgba(self, tmp_path: Path) -> None: im = hopper("RGBA") outfile = str(tmp_path / "temp.tif") im.save(outfile) - def test_save_unsupported_mode(self, tmp_path): + def test_save_unsupported_mode(self, tmp_path: Path) -> None: im = hopper("HSV") outfile = str(tmp_path / "temp.tif") with pytest.raises(OSError): im.save(outfile) - def test_8bit_s(self): + def test_8bit_s(self) -> None: with Image.open("Tests/images/8bit.s.tif") as im: im.load() assert im.mode == "L" assert im.getpixel((50, 50)) == 184 - def test_little_endian(self): + def test_little_endian(self) -> None: with Image.open("Tests/images/16bit.cropped.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16" @@ -223,7 +224,7 @@ def test_little_endian(self): assert b[0] == ord(b"\xe0") assert b[1] == ord(b"\x01") - def test_big_endian(self): + def test_big_endian(self) -> None: with Image.open("Tests/images/16bit.MM.cropped.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16B" @@ -233,7 +234,7 @@ def test_big_endian(self): assert b[0] == ord(b"\x01") assert b[1] == ord(b"\xe0") - def test_16bit_r(self): + def test_16bit_r(self) -> None: with Image.open("Tests/images/16bit.r.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16" @@ -242,14 +243,14 @@ def test_16bit_r(self): assert b[0] == ord(b"\xe0") assert b[1] == ord(b"\x01") - def test_16bit_s(self): + def test_16bit_s(self) -> None: with Image.open("Tests/images/16bit.s.tif") as im: im.load() assert im.mode == "I" assert im.getpixel((0, 0)) == 32767 assert im.getpixel((0, 1)) == 0 - def test_12bit_rawmode(self): + def test_12bit_rawmode(self) -> None: """Are we generating the same interpretation of the image as Imagemagick is?""" @@ -262,7 +263,7 @@ def test_12bit_rawmode(self): assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") - def test_32bit_float(self): + def test_32bit_float(self) -> None: # Issue 614, specific 32-bit float format path = "Tests/images/10ct_32bit_128.tiff" with Image.open(path) as im: @@ -271,7 +272,7 @@ def test_32bit_float(self): assert im.getpixel((0, 0)) == -0.4526388943195343 assert im.getextrema() == (-3.140936851501465, 3.140684127807617) - def test_unknown_pixel_mode(self): + def test_unknown_pixel_mode(self) -> None: with pytest.raises(OSError): with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"): pass @@ -283,12 +284,12 @@ def test_unknown_pixel_mode(self): ("Tests/images/multipage.tiff", 3), ), ) - def test_n_frames(self, path, n_frames): + def test_n_frames(self, path, n_frames) -> None: with Image.open(path) as im: assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) - def test_eoferror(self): + def test_eoferror(self) -> None: with Image.open("Tests/images/multipage-lastframe.tif") as im: n_frames = im.n_frames @@ -300,7 +301,7 @@ def test_eoferror(self): # Test that seeking to the last frame does not raise an error im.seek(n_frames - 1) - def test_multipage(self): + def test_multipage(self) -> None: # issue #862 with Image.open("Tests/images/multipage.tiff") as im: # file is a multipage tiff: 10x10 green, 10x10 red, 20x20 blue @@ -324,13 +325,13 @@ def test_multipage(self): assert im.size == (20, 20) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) - def test_multipage_last_frame(self): + def test_multipage_last_frame(self) -> None: with Image.open("Tests/images/multipage-lastframe.tif") as im: im.load() assert im.size == (20, 20) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) - def test_frame_order(self): + def test_frame_order(self) -> None: # A frame can't progress to itself after reading with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: assert im.n_frames == 1 @@ -343,7 +344,7 @@ def test_frame_order(self): with Image.open("Tests/images/multipage_out_of_order.tiff") as im: assert im.n_frames == 3 - def test___str__(self): + def test___str__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: # Act @@ -352,7 +353,7 @@ def test___str__(self): # Assert assert isinstance(ret, str) - def test_dict(self): + def test_dict(self) -> None: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: @@ -392,7 +393,7 @@ def test_dict(self): } assert dict(im.tag) == legacy_tags - def test__delitem__(self): + def test__delitem__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: len_before = len(dict(im.ifd)) @@ -401,36 +402,36 @@ def test__delitem__(self): assert len_before == len_after + 1 @pytest.mark.parametrize("legacy_api", (False, True)) - def test_load_byte(self, legacy_api): + def test_load_byte(self, legacy_api) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc" ret = ifd.load_byte(data, legacy_api) assert ret == b"abc" - def test_load_string(self): + def test_load_string(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc\0" ret = ifd.load_string(data, False) assert ret == "abc" - def test_load_float(self): + def test_load_float(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdabcd" ret = ifd.load_float(data, False) assert ret == (1.6777999408082104e22, 1.6777999408082104e22) - def test_load_double(self): + def test_load_double(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdefghabcdefgh" ret = ifd.load_double(data, False) assert ret == (8.540883223036124e194, 8.540883223036124e194) - def test_ifd_tag_type(self): + def test_ifd_tag_type(self) -> None: with Image.open("Tests/images/ifd_tag_type.tiff") as im: assert 0x8825 in im.tag_v2 - def test_exif(self, tmp_path): - def check_exif(exif): + def test_exif(self, tmp_path: Path) -> None: + def check_exif(exif) -> None: assert sorted(exif.keys()) == [ 256, 257, @@ -481,7 +482,7 @@ def check_exif(exif): exif = im.getexif() check_exif(exif) - def test_modify_exif(self, tmp_path): + def test_modify_exif(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/ifd_tag_type.tiff") as im: exif = im.getexif() @@ -493,7 +494,7 @@ def test_modify_exif(self, tmp_path): exif = im.getexif() assert exif[264] == 100 - def test_reload_exif_after_seek(self): + def test_reload_exif_after_seek(self) -> None: with Image.open("Tests/images/multipage.tiff") as im: exif = im.getexif() del exif[256] @@ -501,7 +502,7 @@ def test_reload_exif_after_seek(self): assert 256 in exif - def test_exif_frames(self): + def test_exif_frames(self) -> None: # Test that EXIF data can change across frames with Image.open("Tests/images/g4-multi.tiff") as im: assert im.getexif()[273] == (328, 815) @@ -510,7 +511,7 @@ def test_exif_frames(self): assert im.getexif()[273] == (1408, 1907) @pytest.mark.parametrize("mode", ("1", "L")) - def test_photometric(self, mode, tmp_path): + def test_photometric(self, mode, tmp_path: Path) -> None: filename = str(tmp_path / "temp.tif") im = hopper(mode) im.save(filename, tiffinfo={262: 0}) @@ -518,13 +519,13 @@ def test_photometric(self, mode, tmp_path): assert reloaded.tag_v2[262] == 0 assert_image_equal(im, reloaded) - def test_seek(self): + def test_seek(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: im.seek(0) assert im.tell() == 0 - def test_seek_eof(self): + def test_seek_eof(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: assert im.tell() == 0 @@ -533,21 +534,21 @@ def test_seek_eof(self): with pytest.raises(EOFError): im.seek(1) - def test__limit_rational_int(self): + def test__limit_rational_int(self) -> None: from PIL.TiffImagePlugin import _limit_rational value = 34 ret = _limit_rational(value, 65536) assert ret == (34, 1) - def test__limit_rational_float(self): + def test__limit_rational_float(self) -> None: from PIL.TiffImagePlugin import _limit_rational value = 22.3 ret = _limit_rational(value, 65536) assert ret == (223, 10) - def test_4bit(self): + def test_4bit(self) -> None: test_file = "Tests/images/hopper_gray_4bpp.tif" original = hopper("L") with Image.open(test_file) as im: @@ -555,7 +556,7 @@ def test_4bit(self): assert im.mode == "L" assert_image_similar(im, original, 7.3) - def test_gray_semibyte_per_pixel(self): + def test_gray_semibyte_per_pixel(self) -> None: test_files = ( ( 24.8, # epsilon @@ -588,7 +589,7 @@ def test_gray_semibyte_per_pixel(self): assert im2.mode == "L" assert_image_equal(im, im2) - def test_with_underscores(self, tmp_path): + def test_with_underscores(self, tmp_path: Path) -> None: kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename, **kwargs) @@ -601,7 +602,7 @@ def test_with_underscores(self, tmp_path): assert im.tag_v2[X_RESOLUTION] == 72 assert im.tag_v2[Y_RESOLUTION] == 36 - def test_roundtrip_tiff_uint16(self, tmp_path): + def test_roundtrip_tiff_uint16(self, tmp_path: Path) -> None: # Test an image of all '0' values pixel_value = 0x1234 infile = "Tests/images/uint16_1_4660.tif" @@ -613,7 +614,7 @@ def test_roundtrip_tiff_uint16(self, tmp_path): assert_image_equal_tofile(im, tmpfile) - def test_rowsperstrip(self, tmp_path): + def test_rowsperstrip(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") im = hopper() im.save(outfile, tiffinfo={278: 256}) @@ -621,25 +622,25 @@ def test_rowsperstrip(self, tmp_path): with Image.open(outfile) as im: assert im.tag_v2[278] == 256 - def test_strip_raw(self): + def test_strip_raw(self) -> None: infile = "Tests/images/tiff_strip_raw.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_strip_planar_raw(self): + def test_strip_planar_raw(self) -> None: # gdal_translate -of GTiff -co INTERLEAVE=BAND \ # tiff_strip_raw.tif tiff_strip_planar_raw.tiff infile = "Tests/images/tiff_strip_planar_raw.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_strip_planar_raw_with_overviews(self): + def test_strip_planar_raw_with_overviews(self) -> None: # gdaladdo tiff_strip_planar_raw2.tif 2 4 8 16 infile = "Tests/images/tiff_strip_planar_raw_with_overviews.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_tiled_planar_raw(self): + def test_tiled_planar_raw(self) -> None: # gdal_translate -of GTiff -co TILED=YES -co BLOCKXSIZE=32 \ # -co BLOCKYSIZE=32 -co INTERLEAVE=BAND \ # tiff_tiled_raw.tif tiff_tiled_planar_raw.tiff @@ -647,7 +648,7 @@ def test_tiled_planar_raw(self): with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_planar_configuration_save(self, tmp_path): + def test_planar_configuration_save(self, tmp_path: Path) -> None: infile = "Tests/images/tiff_tiled_planar_raw.tif" with Image.open(infile) as im: assert im._planar_configuration == 2 @@ -659,7 +660,7 @@ def test_planar_configuration_save(self, tmp_path): assert_image_equal_tofile(reloaded, infile) @pytest.mark.parametrize("mode", ("P", "PA")) - def test_palette(self, mode, tmp_path): + def test_palette(self, mode, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") im = hopper(mode) @@ -668,7 +669,7 @@ def test_palette(self, mode, tmp_path): with Image.open(outfile) as reloaded: assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) - def test_tiff_save_all(self): + def test_tiff_save_all(self) -> None: mp = BytesIO() with Image.open("Tests/images/multipage.tiff") as im: im.save(mp, format="tiff", save_all=True) @@ -698,7 +699,7 @@ def im_generator(ims): with Image.open(mp) as reread: assert reread.n_frames == 3 - def test_saving_icc_profile(self, tmp_path): + def test_saving_icc_profile(self, tmp_path: Path) -> None: # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs # as libtiff does not support embedded ICC profiles, @@ -712,7 +713,7 @@ def test_saving_icc_profile(self, tmp_path): with Image.open(tmpfile) as reloaded: assert b"Dummy value" == reloaded.info["icc_profile"] - def test_save_icc_profile(self, tmp_path): + def test_save_icc_profile(self, tmp_path: Path) -> None: im = hopper() assert "icc_profile" not in im.info @@ -723,14 +724,14 @@ def test_save_icc_profile(self, tmp_path): with Image.open(outfile) as reloaded: assert reloaded.info["icc_profile"] == icc_profile - def test_save_bmp_compression(self, tmp_path): + def test_save_bmp_compression(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.bmp") as im: assert im.info["compression"] == 0 outfile = str(tmp_path / "temp.tif") im.save(outfile) - def test_discard_icc_profile(self, tmp_path): + def test_discard_icc_profile(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/icc_profile.png") as im: @@ -741,7 +742,7 @@ def test_discard_icc_profile(self, tmp_path): with Image.open(outfile) as reloaded: assert "icc_profile" not in reloaded.info - def test_getxmp(self): + def test_getxmp(self) -> None: with Image.open("Tests/images/lab.tif") as im: if ElementTree is None: with pytest.warns( @@ -756,7 +757,7 @@ def test_getxmp(self): assert description[0]["format"] == "image/tiff" assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] - def test_get_photoshop_blocks(self): + def test_get_photoshop_blocks(self) -> None: with Image.open("Tests/images/lab.tif") as im: assert list(im.get_photoshop_blocks().keys()) == [ 1061, @@ -782,7 +783,7 @@ def test_get_photoshop_blocks(self): 4001, ] - def test_tiff_chunks(self, tmp_path): + def test_tiff_chunks(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.tif") im = hopper() @@ -803,7 +804,7 @@ def test_tiff_chunks(self, tmp_path): assert_image_equal_tofile(im, tmpfile) - def test_close_on_load_exclusive(self, tmp_path): + def test_close_on_load_exclusive(self, tmp_path: Path) -> None: # similar to test_fd_leak, but runs on unixlike os tmpfile = str(tmp_path / "temp.tif") @@ -816,7 +817,7 @@ def test_close_on_load_exclusive(self, tmp_path): im.load() assert fp.closed - def test_close_on_load_nonexclusive(self, tmp_path): + def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/uint16_1_4660.tif") as im: @@ -838,7 +839,7 @@ def test_close_on_load_nonexclusive(self, tmp_path): not os.path.exists("Tests/images/string_dimension.tiff"), reason="Extra image files not installed", ) - def test_string_dimension(self): + def test_string_dimension(self) -> None: # Assert that an error is raised if one of the dimensions is a string with Image.open("Tests/images/string_dimension.tiff") as im: with pytest.raises(OSError): @@ -846,7 +847,7 @@ def test_string_dimension(self): @pytest.mark.timeout(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") - def test_timeout(self): + def test_timeout(self) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: ImageFile.LOAD_TRUNCATED_IMAGES = True im.load() @@ -859,7 +860,7 @@ def test_timeout(self): ], ) @pytest.mark.timeout(2) - def test_oom(self, test_file): + def test_oom(self, test_file) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): with Image.open(test_file): @@ -868,7 +869,7 @@ def test_oom(self, test_file): @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestFileTiffW32: - def test_fd_leak(self, tmp_path): + def test_fd_leak(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.tif") # this is an mmaped file. diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 06689bc9092..bb6225d075b 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -2,6 +2,7 @@ import io import struct +from pathlib import Path import pytest @@ -13,7 +14,7 @@ TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()} -def test_rt_metadata(tmp_path): +def test_rt_metadata(tmp_path: Path) -> None: """Test writing arbitrary metadata into the tiff image directory Use case is ImageJ private tags, one numeric, one arbitrary data. https://github.com/python-pillow/Pillow/issues/291 @@ -79,7 +80,7 @@ def test_rt_metadata(tmp_path): assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) -def test_read_metadata(): +def test_read_metadata() -> None: with Image.open("Tests/images/hopper_g4.tif") as img: assert { "YResolution": IFDRational(4294967295, 113653537), @@ -120,7 +121,7 @@ def test_read_metadata(): } == img.tag.named() -def test_write_metadata(tmp_path): +def test_write_metadata(tmp_path: Path) -> None: """Test metadata writing through the python code""" with Image.open("Tests/images/hopper.tif") as img: f = str(tmp_path / "temp.tiff") @@ -157,7 +158,7 @@ def test_write_metadata(tmp_path): assert value == reloaded[tag], f"{tag} didn't roundtrip" -def test_change_stripbytecounts_tag_type(tmp_path): +def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.tif") as im: info = im.tag_v2 @@ -176,19 +177,19 @@ def test_change_stripbytecounts_tag_type(tmp_path): assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG -def test_no_duplicate_50741_tag(): +def test_no_duplicate_50741_tag() -> None: assert TAG_IDS["MakerNoteSafety"] == 50741 assert TAG_IDS["BestQualityScale"] == 50780 -def test_iptc(tmp_path): +def test_iptc(tmp_path: Path) -> None: out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.Lab.tif") as im: im.save(out) @pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) -def test_writing_other_types_to_ascii(value, expected, tmp_path): +def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None: info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[271] @@ -205,7 +206,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path): @pytest.mark.parametrize("value", (1, IFDRational(1))) -def test_writing_other_types_to_bytes(value, tmp_path): +def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -221,7 +222,7 @@ def test_writing_other_types_to_bytes(value, tmp_path): assert reloaded.tag_v2[700] == b"\x01" -def test_writing_other_types_to_undefined(tmp_path): +def test_writing_other_types_to_undefined(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -237,7 +238,7 @@ def test_writing_other_types_to_undefined(tmp_path): assert reloaded.tag_v2[33723] == b"1" -def test_undefined_zero(tmp_path): +def test_undefined_zero(tmp_path: Path) -> None: # Check that the tag has not been changed since this test was created tag = TiffTags.TAGS_V2[45059] assert tag.type == TiffTags.UNDEFINED @@ -252,7 +253,7 @@ def test_undefined_zero(tmp_path): assert info[45059] == original -def test_empty_metadata(): +def test_empty_metadata() -> None: f = io.BytesIO(b"II*\x00\x08\x00\x00\x00") head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) @@ -261,7 +262,7 @@ def test_empty_metadata(): info.load(f) -def test_iccprofile(tmp_path): +def test_iccprofile(tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/1462 out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.iccprofile.tif") as im: @@ -272,7 +273,7 @@ def test_iccprofile(tmp_path): assert im.info["icc_profile"] == reloaded.info["icc_profile"] -def test_iccprofile_binary(): +def test_iccprofile_binary() -> None: # https://github.com/python-pillow/Pillow/issues/1526 # We should be able to load this, # but probably won't be able to save it. @@ -282,19 +283,19 @@ def test_iccprofile_binary(): assert im.info["icc_profile"] -def test_iccprofile_save_png(tmp_path): +def test_iccprofile_save_png(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.iccprofile.tif") as im: outfile = str(tmp_path / "temp.png") im.save(outfile) -def test_iccprofile_binary_save_png(tmp_path): +def test_iccprofile_binary_save_png(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: outfile = str(tmp_path / "temp.png") im.save(outfile) -def test_exif_div_zero(tmp_path): +def test_exif_div_zero(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() info[41988] = TiffImagePlugin.IFDRational(0, 0) @@ -307,7 +308,7 @@ def test_exif_div_zero(tmp_path): assert 0 == reloaded.tag_v2[41988].denominator -def test_ifd_unsigned_rational(tmp_path): +def test_ifd_unsigned_rational(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -338,7 +339,7 @@ def test_ifd_unsigned_rational(tmp_path): assert 1 == reloaded.tag_v2[41493].denominator -def test_ifd_signed_rational(tmp_path): +def test_ifd_signed_rational(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -381,7 +382,7 @@ def test_ifd_signed_rational(tmp_path): assert -1 == reloaded.tag_v2[37380].denominator -def test_ifd_signed_long(tmp_path): +def test_ifd_signed_long(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -394,7 +395,7 @@ def test_ifd_signed_long(tmp_path): assert reloaded.tag_v2[37000] == -60000 -def test_empty_values(): +def test_empty_values() -> None: data = io.BytesIO( b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00" @@ -409,7 +410,7 @@ def test_empty_values(): assert 33432 in info -def test_photoshop_info(tmp_path): +def test_photoshop_info(tmp_path: Path) -> None: with Image.open("Tests/images/issue_2278.tif") as im: assert len(im.tag_v2[34377]) == 70 assert isinstance(im.tag_v2[34377], bytes) @@ -420,7 +421,7 @@ def test_photoshop_info(tmp_path): assert isinstance(reloaded.tag_v2[34377], bytes) -def test_too_many_entries(): +def test_too_many_entries() -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() # 277: ("SamplesPerPixel", SHORT, 1), @@ -432,7 +433,7 @@ def test_too_many_entries(): assert ifd[277] == 4 -def test_tag_group_data(): +def test_tag_group_data() -> None: base_ifd = TiffImagePlugin.ImageFileDirectory_v2() interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965) for ifd in (base_ifd, interop_ifd): @@ -446,7 +447,7 @@ def test_tag_group_data(): assert base_ifd.tagtype[2] != interop_ifd.tagtype[256] -def test_empty_subifd(tmp_path): +def test_empty_subifd(tmp_path: Path) -> None: out = str(tmp_path / "temp.jpg") im = hopper() diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index c49418ce3c5..249846da481 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -4,6 +4,7 @@ import re import sys import warnings +from pathlib import Path import pytest @@ -26,7 +27,7 @@ class TestUnsupportedWebp: - def test_unsupported(self): + def test_unsupported(self) -> None: if HAVE_WEBP: WebPImagePlugin.SUPPORTED = False @@ -42,15 +43,15 @@ def test_unsupported(self): @skip_unless_feature("webp") class TestFileWebp: - def setup_method(self): + def setup_method(self) -> None: self.rgb_mode = "RGB" - def test_version(self): + def test_version(self) -> None: _webp.WebPDecoderVersion() _webp.WebPDecoderBuggyAlpha() assert re.search(r"\d+\.\d+\.\d+$", features.version_module("webp")) - def test_read_rgb(self): + def test_read_rgb(self) -> None: """ Can we read a RGB mode WebP file without error? Does it have the bits we expect? @@ -67,7 +68,7 @@ def test_read_rgb(self): # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0) - def _roundtrip(self, tmp_path, mode, epsilon, args={}): + def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None: temp_file = str(tmp_path / "temp.webp") hopper(mode).save(temp_file, **args) @@ -93,7 +94,7 @@ def _roundtrip(self, tmp_path, mode, epsilon, args={}): target = target.convert(self.rgb_mode) assert_image_similar(image, target, epsilon) - def test_write_rgb(self, tmp_path): + def test_write_rgb(self, tmp_path: Path) -> None: """ Can we write a RGB mode file to webp without error? Does it have the bits we expect? @@ -101,7 +102,7 @@ def test_write_rgb(self, tmp_path): self._roundtrip(tmp_path, self.rgb_mode, 12.5) - def test_write_method(self, tmp_path): + def test_write_method(self, tmp_path: Path) -> None: self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6}) buffer_no_args = io.BytesIO() @@ -112,7 +113,7 @@ def test_write_method(self, tmp_path): assert buffer_no_args.getbuffer() != buffer_method.getbuffer() @skip_unless_feature("webp_anim") - def test_save_all(self, tmp_path): + def test_save_all(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (1, 1)) im2 = Image.new("RGB", (1, 1), "#f00") @@ -124,14 +125,14 @@ def test_save_all(self, tmp_path): reloaded.seek(1) assert_image_similar(im2, reloaded, 1) - def test_icc_profile(self, tmp_path): + def test_icc_profile(self, tmp_path: Path) -> None: self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) if _webp.HAVE_WEBPANIM: self._roundtrip( tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} ) - def test_write_unsupported_mode_L(self, tmp_path): + def test_write_unsupported_mode_L(self, tmp_path: Path) -> None: """ Saving a black-and-white file to WebP format should work, and be similar to the original file. @@ -139,7 +140,7 @@ def test_write_unsupported_mode_L(self, tmp_path): self._roundtrip(tmp_path, "L", 10.0) - def test_write_unsupported_mode_P(self, tmp_path): + def test_write_unsupported_mode_P(self, tmp_path: Path) -> None: """ Saving a palette-based file to WebP format should work, and be similar to the original file. @@ -148,14 +149,14 @@ def test_write_unsupported_mode_P(self, tmp_path): self._roundtrip(tmp_path, "P", 50.0) @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") - def test_write_encoding_error_message(self, tmp_path): + def test_write_encoding_error_message(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (15000, 15000)) with pytest.raises(ValueError) as e: im.save(temp_file, method=0) assert str(e.value) == "encoding error 6" - def test_WebPEncode_with_invalid_args(self): + def test_WebPEncode_with_invalid_args(self) -> None: """ Calling encoder functions with no arguments should result in an error. """ @@ -166,7 +167,7 @@ def test_WebPEncode_with_invalid_args(self): with pytest.raises(TypeError): _webp.WebPEncode() - def test_WebPDecode_with_invalid_args(self): + def test_WebPDecode_with_invalid_args(self) -> None: """ Calling decoder functions with no arguments should result in an error. """ @@ -177,14 +178,14 @@ def test_WebPDecode_with_invalid_args(self): with pytest.raises(TypeError): _webp.WebPDecode() - def test_no_resource_warning(self, tmp_path): + def test_no_resource_warning(self, tmp_path: Path) -> None: file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: temp_file = str(tmp_path / "temp.webp") with warnings.catch_warnings(): image.save(temp_file) - def test_file_pointer_could_be_reused(self): + def test_file_pointer_could_be_reused(self) -> None: file_path = "Tests/images/hopper.webp" with open(file_path, "rb") as blob: Image.open(blob).load() @@ -195,14 +196,14 @@ def test_file_pointer_could_be_reused(self): (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), ) @skip_unless_feature("webp_anim") - def test_invalid_background(self, background, tmp_path): + def test_invalid_background(self, background, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = hopper() with pytest.raises(OSError): im.save(temp_file, save_all=True, append_images=[im], background=background) @skip_unless_feature("webp_anim") - def test_background_from_gif(self, tmp_path): + def test_background_from_gif(self, tmp_path: Path) -> None: # Save L mode GIF with background with Image.open("Tests/images/no_palette_with_background.gif") as im: out_webp = str(tmp_path / "temp.webp") @@ -227,7 +228,7 @@ def test_background_from_gif(self, tmp_path): assert difference < 5 @skip_unless_feature("webp_anim") - def test_duration(self, tmp_path): + def test_duration(self, tmp_path: Path) -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: assert im.info["duration"] == 1000 @@ -238,7 +239,7 @@ def test_duration(self, tmp_path): reloaded.load() assert reloaded.info["duration"] == 1000 - def test_roundtrip_rgba_palette(self, tmp_path): + def test_roundtrip_rgba_palette(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGBA", (1, 1)).convert("P") assert im.mode == "P" diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index cfda35a0962..a95434624f5 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -14,12 +16,12 @@ _webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") -def setup_module(): +def setup_module() -> None: if _webp.WebPDecoderBuggyAlpha(): pytest.skip("Buggy early version of WebP installed, not testing transparency") -def test_read_rgba(): +def test_read_rgba() -> None: """ Can we read an RGBA mode file without error? Does it have the bits we expect? @@ -39,7 +41,7 @@ def test_read_rgba(): assert_image_similar_tofile(image, "Tests/images/transparent.png", 20.0) -def test_write_lossless_rgb(tmp_path): +def test_write_lossless_rgb(tmp_path: Path) -> None: """ Can we write an RGBA mode file with lossless compression without error? Does it have the bits we expect? @@ -68,7 +70,7 @@ def test_write_lossless_rgb(tmp_path): assert_image_equal(image, pil_image) -def test_write_rgba(tmp_path): +def test_write_rgba(tmp_path: Path) -> None: """ Can we write a RGBA mode file to WebP without error. Does it have the bits we expect? @@ -99,7 +101,7 @@ def test_write_rgba(tmp_path): assert_image_similar(image, pil_image, 1.0) -def test_keep_rgb_values_when_transparent(tmp_path): +def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: """ Saving transparent pixels should retain their original RGB values when using the "exact" parameter. @@ -128,7 +130,7 @@ def test_keep_rgb_values_when_transparent(tmp_path): assert_image_equal(reloaded.convert("RGB"), image) -def test_write_unsupported_mode_PA(tmp_path): +def test_write_unsupported_mode_PA(tmp_path: Path) -> None: """ Saving a palette-based file with transparency to WebP format should work, and be similar to the original file. diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 426fe7a0293..9a730f1f9bd 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from packaging.version import parse as parse_version @@ -18,7 +20,7 @@ ] -def test_n_frames(): +def test_n_frames() -> None: """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" with Image.open("Tests/images/hopper.webp") as im: @@ -30,7 +32,7 @@ def test_n_frames(): assert im.is_animated -def test_write_animation_L(tmp_path): +def test_write_animation_L(tmp_path: Path) -> None: """ Convert an animated GIF to animated WebP, then compare the frame count, and first and last frames to ensure they're visually similar. @@ -60,13 +62,13 @@ def test_write_animation_L(tmp_path): assert_image_similar(im, orig.convert("RGBA"), 32.9) -def test_write_animation_RGB(tmp_path): +def test_write_animation_RGB(tmp_path: Path) -> None: """ Write an animated WebP from RGB frames, and ensure the frames are visually similar to the originals. """ - def check(temp_file): + def check(temp_file) -> None: with Image.open(temp_file) as im: assert im.n_frames == 2 @@ -105,7 +107,7 @@ def im_generator(ims): check(temp_file2) -def test_timestamp_and_duration(tmp_path): +def test_timestamp_and_duration(tmp_path: Path) -> None: """ Try passing a list of durations, and make sure the encoded timestamps and durations are correct. @@ -136,7 +138,7 @@ def test_timestamp_and_duration(tmp_path): ts += durations[frame] -def test_float_duration(tmp_path): +def test_float_duration(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") with Image.open("Tests/images/iss634.apng") as im: assert im.info["duration"] == 70.0 @@ -148,7 +150,7 @@ def test_float_duration(tmp_path): assert reloaded.info["duration"] == 70 -def test_seeking(tmp_path): +def test_seeking(tmp_path: Path) -> None: """ Create an animated WebP file, and then try seeking through frames in reverse-order, verifying the timestamps and durations are correct. @@ -179,7 +181,7 @@ def test_seeking(tmp_path): ts -= dur -def test_seek_errors(): +def test_seek_errors() -> None: with Image.open("Tests/images/iss634.webp") as im: with pytest.raises(EOFError): im.seek(-1) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index deaf5e380db..fea19694109 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from pathlib import Path import pytest @@ -19,7 +20,7 @@ ElementTree = None -def test_read_exif_metadata(): +def test_read_exif_metadata() -> None: file_path = "Tests/images/flower.webp" with Image.open(file_path) as image: assert image.format == "WEBP" @@ -37,7 +38,7 @@ def test_read_exif_metadata(): assert exif_data == expected_exif -def test_read_exif_metadata_without_prefix(): +def test_read_exif_metadata_without_prefix() -> None: with Image.open("Tests/images/flower2.webp") as im: # Assert prefix is not present assert im.info["exif"][:6] != b"Exif\x00\x00" @@ -49,7 +50,7 @@ def test_read_exif_metadata_without_prefix(): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_write_exif_metadata(): +def test_write_exif_metadata() -> None: file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() with Image.open(file_path) as image: @@ -63,7 +64,7 @@ def test_write_exif_metadata(): assert webp_exif == expected_exif[6:], "WebP EXIF didn't match" -def test_read_icc_profile(): +def test_read_icc_profile() -> None: file_path = "Tests/images/flower2.webp" with Image.open(file_path) as image: assert image.format == "WEBP" @@ -80,7 +81,7 @@ def test_read_icc_profile(): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_write_icc_metadata(): +def test_write_icc_metadata() -> None: file_path = "Tests/images/flower2.jpg" test_buffer = BytesIO() with Image.open(file_path) as image: @@ -100,7 +101,7 @@ def test_write_icc_metadata(): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_read_no_exif(): +def test_read_no_exif() -> None: file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() with Image.open(file_path) as image: @@ -113,7 +114,7 @@ def test_read_no_exif(): assert not webp_image._getexif() -def test_getxmp(): +def test_getxmp() -> None: with Image.open("Tests/images/flower.webp") as im: assert "xmp" not in im.info assert im.getxmp() == {} @@ -133,7 +134,7 @@ def test_getxmp(): @skip_unless_feature("webp_anim") -def test_write_animated_metadata(tmp_path): +def test_write_animated_metadata(tmp_path: Path) -> None: iccp_data = b"" exif_data = b"" xmp_data = b"" diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 6e1d4c1361f..b43e3f2965f 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, WmfImagePlugin @@ -7,7 +9,7 @@ from .helper import assert_image_similar_tofile, hopper -def test_load_raw(): +def test_load_raw() -> None: # Test basic EMF open and rendering with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): @@ -25,17 +27,17 @@ def test_load_raw(): assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref.png", 2.0) -def test_load(): +def test_load() -> None: with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): assert im.load()[0, 0] == (255, 255, 255) -def test_register_handler(tmp_path): +def test_register_handler(tmp_path: Path) -> None: class TestHandler: methodCalled = False - def save(self, im, fp, filename): + def save(self, im, fp, filename) -> None: self.methodCalled = True handler = TestHandler() @@ -51,12 +53,12 @@ def save(self, im, fp, filename): WmfImagePlugin.register_handler(original_handler) -def test_load_float_dpi(): +def test_load_float_dpi() -> None: with Image.open("Tests/images/drawing.emf") as im: assert im.info["dpi"] == 1423.7668161434979 -def test_load_set_dpi(): +def test_load_set_dpi() -> None: with Image.open("Tests/images/drawing.wmf") as im: assert im.size == (82, 82) @@ -68,7 +70,7 @@ def test_load_set_dpi(): @pytest.mark.parametrize("ext", (".wmf", ".emf")) -def test_save(ext, tmp_path): +def test_save(ext, tmp_path: Path) -> None: im = hopper() tmpfile = str(tmp_path / ("temp" + ext)) diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 69a0a1b38d8..44dd2541fb5 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from pathlib import Path import pytest @@ -32,14 +33,14 @@ """ -def test_pil151(): +def test_pil151() -> None: with Image.open(BytesIO(PIL151)) as im: im.load() assert im.mode == "1" assert im.size == (32, 32) -def test_open(): +def test_open() -> None: # Arrange # Created with `convert hopper.png hopper.xbm` filename = "Tests/images/hopper.xbm" @@ -51,7 +52,7 @@ def test_open(): assert im.size == (128, 128) -def test_open_filename_with_underscore(): +def test_open_filename_with_underscore() -> None: # Arrange # Created with `convert hopper.png hopper_underscore.xbm` filename = "Tests/images/hopper_underscore.xbm" @@ -63,14 +64,14 @@ def test_open_filename_with_underscore(): assert im.size == (128, 128) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): XbmImagePlugin.XbmImageFile(invalid_file) -def test_save_wrong_mode(tmp_path): +def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.xbm") @@ -78,7 +79,7 @@ def test_save_wrong_mode(tmp_path): im.save(out) -def test_hotspot(tmp_path): +def test_hotspot(tmp_path: Path) -> None: im = hopper("1") out = str(tmp_path / "temp.xbm") diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 6395ae4aad8..73aaae6e757 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -21,7 +21,7 @@ def tuple_to_ints(tp): return int(x * 255.0), int(y * 255.0), int(z * 255.0) -def test_sanity(): +def test_sanity() -> None: Image.new("HSV", (100, 100)) @@ -78,7 +78,7 @@ def to_rgb_colorsys(im): return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") -def test_wedge(): +def test_wedge() -> None: src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR) im = src.convert("HSV") comparable = to_hsv_colorsys(src) @@ -110,7 +110,7 @@ def test_wedge(): ) -def test_convert(): +def test_convert() -> None: im = hopper("RGB").convert("HSV") comparable = to_hsv_colorsys(hopper("RGB")) @@ -128,7 +128,7 @@ def test_convert(): ) -def test_hsv_to_rgb(): +def test_hsv_to_rgb() -> None: comparable = to_hsv_colorsys(hopper("RGB")) converted = comparable.convert("RGB") comparable = to_rgb_colorsys(comparable) diff --git a/Tests/test_image.py b/Tests/test_image.py index dd989ad99b5..67a7d7eca54 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -7,6 +7,7 @@ import sys import tempfile import warnings +from pathlib import Path import pytest @@ -60,19 +61,19 @@ class TestImage: "HSV", ), ) - def test_image_modes_success(self, mode): + def test_image_modes_success(self, mode) -> None: Image.new(mode, (1, 1)) @pytest.mark.parametrize("mode", ("", "bad", "very very long")) - def test_image_modes_fail(self, mode): + def test_image_modes_fail(self, mode) -> None: with pytest.raises(ValueError) as e: Image.new(mode, (1, 1)) assert str(e.value) == "unrecognized image mode" - def test_exception_inheritance(self): + def test_exception_inheritance(self) -> None: assert issubclass(UnidentifiedImageError, OSError) - def test_sanity(self): + def test_sanity(self) -> None: im = Image.new("L", (100, 100)) assert repr(im)[:45] == " None: class Pretty: - def text(self, text): + def text(self, text) -> None: self.pretty_output = text im = Image.new("L", (100, 100)) @@ -108,7 +109,7 @@ def text(self, text): im._repr_pretty_(p, None) assert p.pretty_output == "" - def test_open_formats(self): + def test_open_formats(self) -> None: PNGFILE = "Tests/images/hopper.png" JPGFILE = "Tests/images/hopper.jpg" @@ -130,7 +131,7 @@ def test_open_formats(self): assert im.mode == "RGB" assert im.size == (128, 128) - def test_width_height(self): + def test_width_height(self) -> None: im = Image.new("RGB", (1, 2)) assert im.width == 1 assert im.height == 2 @@ -138,29 +139,29 @@ def test_width_height(self): with pytest.raises(AttributeError): im.size = (3, 4) - def test_set_mode(self): + def test_set_mode(self) -> None: im = Image.new("RGB", (1, 1)) with pytest.raises(AttributeError): im.mode = "P" - def test_invalid_image(self): + def test_invalid_image(self) -> None: im = io.BytesIO(b"") with pytest.raises(UnidentifiedImageError): with Image.open(im): pass - def test_bad_mode(self): + def test_bad_mode(self) -> None: with pytest.raises(ValueError): with Image.open("filename", "bad mode"): pass - def test_stringio(self): + def test_stringio(self) -> None: with pytest.raises(ValueError): with Image.open(io.StringIO()): pass - def test_pathlib(self, tmp_path): + def test_pathlib(self, tmp_path: Path) -> None: from PIL.Image import Path with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: @@ -179,11 +180,11 @@ def test_pathlib(self, tmp_path): os.remove(temp_file) im.save(Path(temp_file)) - def test_fp_name(self, tmp_path): + def test_fp_name(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.jpg") class FP: - def write(self, b): + def write(self, b) -> None: pass fp = FP() @@ -192,7 +193,7 @@ def write(self, b): im = hopper() im.save(fp) - def test_tempfile(self): + def test_tempfile(self) -> None: # see #1460, pathlib support breaks tempfile.TemporaryFile on py27 # Will error out on save on 3.0.0 im = hopper() @@ -201,13 +202,13 @@ def test_tempfile(self): fp.seek(0) assert_image_similar_tofile(im, fp, 20) - def test_unknown_extension(self, tmp_path): + def test_unknown_extension(self, tmp_path: Path) -> None: im = hopper() temp_file = str(tmp_path / "temp.unknown") with pytest.raises(ValueError): im.save(temp_file) - def test_internals(self): + def test_internals(self) -> None: im = Image.new("L", (100, 100)) im.readonly = 1 im._copy() @@ -222,7 +223,7 @@ def test_internals(self): sys.platform == "cygwin", reason="Test requires opening an mmaped file for writing", ) - def test_readonly_save(self, tmp_path): + def test_readonly_save(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.bmp") shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) @@ -230,7 +231,7 @@ def test_readonly_save(self, tmp_path): assert im.readonly im.save(temp_file) - def test_dump(self, tmp_path): + def test_dump(self, tmp_path: Path) -> None: im = Image.new("L", (10, 10)) im._dump(str(tmp_path / "temp_L.ppm")) @@ -241,7 +242,7 @@ def test_dump(self, tmp_path): with pytest.raises(ValueError): im._dump(str(tmp_path / "temp_HSV.ppm")) - def test_comparison_with_other_type(self): + def test_comparison_with_other_type(self) -> None: # Arrange item = Image.new("RGB", (25, 25), "#000") num = 12 @@ -251,7 +252,7 @@ def test_comparison_with_other_type(self): assert item is not None assert item != num - def test_expand_x(self): + def test_expand_x(self) -> None: # Arrange im = hopper() orig_size = im.size @@ -264,7 +265,7 @@ def test_expand_x(self): assert im.size[0] == orig_size[0] + 2 * xmargin assert im.size[1] == orig_size[1] + 2 * xmargin - def test_expand_xy(self): + def test_expand_xy(self) -> None: # Arrange im = hopper() orig_size = im.size @@ -278,12 +279,12 @@ def test_expand_xy(self): assert im.size[0] == orig_size[0] + 2 * xmargin assert im.size[1] == orig_size[1] + 2 * ymargin - def test_getbands(self): + def test_getbands(self) -> None: # Assert assert hopper("RGB").getbands() == ("R", "G", "B") assert hopper("YCbCr").getbands() == ("Y", "Cb", "Cr") - def test_getchannel_wrong_params(self): + def test_getchannel_wrong_params(self) -> None: im = hopper() with pytest.raises(ValueError): @@ -295,7 +296,7 @@ def test_getchannel_wrong_params(self): with pytest.raises(ValueError): im.getchannel("1") - def test_getchannel(self): + def test_getchannel(self) -> None: im = hopper("YCbCr") Y, Cb, Cr = im.split() @@ -306,7 +307,7 @@ def test_getchannel(self): assert_image_equal(Cr, im.getchannel(2)) assert_image_equal(Cr, im.getchannel("Cr")) - def test_getbbox(self): + def test_getbbox(self) -> None: # Arrange im = hopper() @@ -316,7 +317,7 @@ def test_getbbox(self): # Assert assert bbox == (0, 0, 128, 128) - def test_ne(self): + def test_ne(self) -> None: # Arrange im1 = Image.new("RGB", (25, 25), "black") im2 = Image.new("RGB", (25, 25), "white") @@ -324,7 +325,7 @@ def test_ne(self): # Act / Assert assert im1 != im2 - def test_alpha_composite(self): + def test_alpha_composite(self) -> None: # https://stackoverflow.com/questions/3374878 # Arrange expected_colors = sorted( @@ -355,7 +356,7 @@ def test_alpha_composite(self): img_colors = sorted(img.getcolors()) assert img_colors == expected_colors - def test_alpha_inplace(self): + def test_alpha_inplace(self) -> None: src = Image.new("RGBA", (128, 128), "blue") over = Image.new("RGBA", (128, 128), "red") @@ -407,7 +408,7 @@ def test_alpha_inplace(self): with pytest.raises(ValueError): source.alpha_composite(over, (0, 0), (0, -1)) - def test_register_open_duplicates(self): + def test_register_open_duplicates(self) -> None: # Arrange factory, accept = Image.OPEN["JPEG"] id_length = len(Image.ID) @@ -418,7 +419,7 @@ def test_register_open_duplicates(self): # Assert assert len(Image.ID) == id_length - def test_registered_extensions_uninitialized(self): + def test_registered_extensions_uninitialized(self) -> None: # Arrange Image._initialized = 0 @@ -428,7 +429,7 @@ def test_registered_extensions_uninitialized(self): # Assert assert Image._initialized == 2 - def test_registered_extensions(self): + def test_registered_extensions(self) -> None: # Arrange # Open an image to trigger plugin registration with Image.open("Tests/images/rgb.jpg"): @@ -442,7 +443,7 @@ def test_registered_extensions(self): for ext in [".cur", ".icns", ".tif", ".tiff"]: assert ext in extensions - def test_effect_mandelbrot(self): + def test_effect_mandelbrot(self) -> None: # Arrange size = (512, 512) extent = (-3, -2.5, 2, 2.5) @@ -455,7 +456,7 @@ def test_effect_mandelbrot(self): assert im.size == (512, 512) assert_image_equal_tofile(im, "Tests/images/effect_mandelbrot.png") - def test_effect_mandelbrot_bad_arguments(self): + def test_effect_mandelbrot_bad_arguments(self) -> None: # Arrange size = (512, 512) # Get coordinates the wrong way round: @@ -467,7 +468,7 @@ def test_effect_mandelbrot_bad_arguments(self): with pytest.raises(ValueError): Image.effect_mandelbrot(size, extent, quality) - def test_effect_noise(self): + def test_effect_noise(self) -> None: # Arrange size = (100, 100) sigma = 128 @@ -485,7 +486,7 @@ def test_effect_noise(self): p4 = im.getpixel((0, 4)) assert_not_all_same([p0, p1, p2, p3, p4]) - def test_effect_spread(self): + def test_effect_spread(self) -> None: # Arrange im = hopper() distance = 10 @@ -497,7 +498,7 @@ def test_effect_spread(self): assert im.size == (128, 128) assert_image_similar_tofile(im2, "Tests/images/effect_spread.png", 110) - def test_effect_spread_zero(self): + def test_effect_spread_zero(self) -> None: # Arrange im = hopper() distance = 0 @@ -508,7 +509,7 @@ def test_effect_spread_zero(self): # Assert assert_image_equal(im, im2) - def test_check_size(self): + def test_check_size(self) -> None: # Checking that the _check_size function throws value errors when we want it to with pytest.raises(ValueError): Image.new("RGB", 0) # not a tuple @@ -537,10 +538,10 @@ def test_check_size(self): "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) - def test_empty_image(self, size): + def test_empty_image(self, size) -> None: Image.new("RGB", size) - def test_storage_neg(self): + def test_storage_neg(self) -> None: # Storage.c accepted negative values for xsize, ysize. Was # test_neg_ppm, but the core function for that has been # removed Calling directly into core to test the error in @@ -549,13 +550,13 @@ def test_storage_neg(self): with pytest.raises(ValueError): Image.core.fill("RGB", (2, -2), (0, 0, 0)) - def test_one_item_tuple(self): + def test_one_item_tuple(self) -> None: for mode in ("I", "F", "L"): im = Image.new(mode, (100, 100), (5,)) px = im.load() assert px[0, 0] == 5 - def test_linear_gradient_wrong_mode(self): + def test_linear_gradient_wrong_mode(self) -> None: # Arrange wrong_mode = "RGB" @@ -564,7 +565,7 @@ def test_linear_gradient_wrong_mode(self): Image.linear_gradient(wrong_mode) @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) - def test_linear_gradient(self, mode): + def test_linear_gradient(self, mode) -> None: # Arrange target_file = "Tests/images/linear_gradient.png" @@ -580,7 +581,7 @@ def test_linear_gradient(self, mode): target = target.convert(mode) assert_image_equal(im, target) - def test_radial_gradient_wrong_mode(self): + def test_radial_gradient_wrong_mode(self) -> None: # Arrange wrong_mode = "RGB" @@ -589,7 +590,7 @@ def test_radial_gradient_wrong_mode(self): Image.radial_gradient(wrong_mode) @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) - def test_radial_gradient(self, mode): + def test_radial_gradient(self, mode) -> None: # Arrange target_file = "Tests/images/radial_gradient.png" @@ -605,7 +606,7 @@ def test_radial_gradient(self, mode): target = target.convert(mode) assert_image_equal(im, target) - def test_register_extensions(self): + def test_register_extensions(self) -> None: test_format = "a" exts = ["b", "c"] for ext in exts: @@ -621,7 +622,7 @@ def test_register_extensions(self): assert ext_individual == ext_multiple - def test_remap_palette(self): + def test_remap_palette(self) -> None: # Test identity transform with Image.open("Tests/images/hopper.gif") as im: assert_image_equal(im, im.remap_palette(list(range(256)))) @@ -640,7 +641,7 @@ def test_remap_palette(self): with pytest.raises(ValueError): im.remap_palette(None) - def test_remap_palette_transparency(self): + def test_remap_palette_transparency(self) -> None: im = Image.new("P", (1, 2), (0, 0, 0)) im.putpixel((0, 1), (255, 0, 0)) im.info["transparency"] = 0 @@ -655,7 +656,7 @@ def test_remap_palette_transparency(self): im_remapped = im.remap_palette([1, 0]) assert "transparency" not in im_remapped.info - def test__new(self): + def test__new(self) -> None: im = hopper("RGB") im_p = hopper("P") @@ -664,7 +665,7 @@ def test__new(self): blank_p.palette = None blank_pa.palette = None - def _make_new(base_image, image, palette_result=None): + def _make_new(base_image, image, palette_result=None) -> None: new_image = base_image._new(image.im) assert new_image.mode == image.mode assert new_image.size == image.size @@ -679,7 +680,7 @@ def _make_new(base_image, image, palette_result=None): _make_new(im, blank_p, ImagePalette.ImagePalette()) _make_new(im, blank_pa, ImagePalette.ImagePalette()) - def test_p_from_rgb_rgba(self): + def test_p_from_rgb_rgba(self) -> None: for mode, color in [ ("RGB", "#DDEEFF"), ("RGB", (221, 238, 255)), @@ -689,7 +690,7 @@ def test_p_from_rgb_rgba(self): expected = Image.new(mode, (100, 100), color) assert_image_equal(im.convert(mode), expected) - def test_no_resource_warning_on_save(self, tmp_path): + def test_no_resource_warning_on_save(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/835 # Arrange test_file = "Tests/images/hopper.png" @@ -700,7 +701,7 @@ def test_no_resource_warning_on_save(self, tmp_path): with warnings.catch_warnings(): im.save(temp_file) - def test_no_new_file_on_error(self, tmp_path): + def test_no_new_file_on_error(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.jpg") im = Image.new("RGB", (0, 0)) @@ -709,10 +710,10 @@ def test_no_new_file_on_error(self, tmp_path): assert not os.path.exists(temp_file) - def test_load_on_nonexclusive_multiframe(self): + def test_load_on_nonexclusive_multiframe(self) -> None: with open("Tests/images/frozenpond.mpo", "rb") as fp: - def act(fp): + def act(fp) -> None: im = Image.open(fp) im.load() @@ -723,7 +724,7 @@ def act(fp): assert not fp.closed - def test_empty_exif(self): + def test_empty_exif(self) -> None: with Image.open("Tests/images/exif.png") as im: exif = im.getexif() assert dict(exif) @@ -739,7 +740,7 @@ def test_empty_exif(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_exif_jpeg(self, tmp_path): + def test_exif_jpeg(self, tmp_path: Path) -> None: with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian exif = im.getexif() assert 258 not in exif @@ -785,7 +786,7 @@ def test_exif_jpeg(self, tmp_path): @skip_unless_feature("webp") @skip_unless_feature("webp_anim") - def test_exif_webp(self, tmp_path): + def test_exif_webp(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.webp") as im: exif = im.getexif() assert exif == {} @@ -795,7 +796,7 @@ def test_exif_webp(self, tmp_path): exif[40963] = 455 exif[305] = "Pillow test" - def check_exif(): + def check_exif() -> None: with Image.open(out) as reloaded: reloaded_exif = reloaded.getexif() assert reloaded_exif[258] == 8 @@ -807,7 +808,7 @@ def check_exif(): im.save(out, exif=exif, save_all=True) check_exif() - def test_exif_png(self, tmp_path): + def test_exif_png(self, tmp_path: Path) -> None: with Image.open("Tests/images/exif.png") as im: exif = im.getexif() assert exif == {274: 1} @@ -823,7 +824,7 @@ def test_exif_png(self, tmp_path): reloaded_exif = reloaded.getexif() assert reloaded_exif == {258: 8, 40963: 455, 305: "Pillow test"} - def test_exif_interop(self): + def test_exif_interop(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() assert exif.get_ifd(0xA005) == { @@ -837,7 +838,7 @@ def test_exif_interop(self): reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005) - def test_exif_ifd1(self): + def test_exif_ifd1(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() assert exif.get_ifd(ExifTags.IFD.IFD1) == { @@ -849,7 +850,7 @@ def test_exif_ifd1(self): 283: 180.0, } - def test_exif_ifd(self): + def test_exif_ifd(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() del exif.get_ifd(0x8769)[0xA005] @@ -858,7 +859,7 @@ def test_exif_ifd(self): reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) - def test_exif_load_from_fp(self): + def test_exif_load_from_fp(self) -> None: with Image.open("Tests/images/flower.jpg") as im: data = im.info["exif"] if data.startswith(b"Exif\x00\x00"): @@ -879,7 +880,7 @@ def test_exif_load_from_fp(self): 34665: 196, } - def test_exif_hide_offsets(self): + def test_exif_hide_offsets(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() @@ -905,18 +906,18 @@ def test_exif_hide_offsets(self): assert exif.get_ifd(0xA005) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero_tobytes(self, size): + def test_zero_tobytes(self, size) -> None: im = Image.new("RGB", size) assert im.tobytes() == b"" @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero_frombytes(self, size): + def test_zero_frombytes(self, size) -> None: Image.frombytes("RGB", size, b"") im = Image.new("RGB", size) im.frombytes(b"") - def test_has_transparency_data(self): + def test_has_transparency_data(self) -> None: for mode in ("1", "L", "P", "RGB"): im = Image.new(mode, (1, 1)) assert not im.has_transparency_data @@ -941,7 +942,7 @@ def test_has_transparency_data(self): assert im.palette.mode == "RGBA" assert im.has_transparency_data - def test_apply_transparency(self): + def test_apply_transparency(self) -> None: im = Image.new("P", (1, 1)) im.putpalette((0, 0, 0, 1, 1, 1)) assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1} @@ -970,7 +971,7 @@ def test_apply_transparency(self): im.apply_transparency() assert im.palette.colors[(27, 35, 6, 214)] == 24 - def test_constants(self): + def test_constants(self) -> None: for enum in ( Image.Transpose, Image.Transform, @@ -995,7 +996,7 @@ def test_constants(self): "01r_00.pcx", ], ) - def test_overrun(self, path): + def test_overrun(self, path) -> None: """For overrun completeness, test as: valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c """ @@ -1009,7 +1010,7 @@ def test_overrun(self, path): assert buffer_overrun or truncated - def test_fli_overrun2(self): + def test_fli_overrun2(self) -> None: with Image.open("Tests/images/fli_overrun2.bin") as im: try: im.seek(1) @@ -1017,12 +1018,12 @@ def test_fli_overrun2(self): except OSError as e: assert str(e) == "buffer overrun when reading image file" - def test_exit_fp(self): + def test_exit_fp(self) -> None: with Image.new("L", (1, 1)) as im: pass assert not hasattr(im, "fp") - def test_close_graceful(self, caplog): + def test_close_graceful(self, caplog) -> None: with Image.open("Tests/images/hopper.jpg") as im: copy = im.copy() with caplog.at_level(logging.DEBUG): @@ -1043,7 +1044,7 @@ def mock_encode(*args): class TestRegistry: - def test_encode_registry(self): + def test_encode_registry(self) -> None: Image.register_encoder("MOCK", mock_encode) assert "MOCK" in Image.ENCODERS @@ -1052,6 +1053,6 @@ def test_encode_registry(self): assert isinstance(enc, MockEncoder) assert enc.args == ("RGB", "args", "extra") - def test_encode_registry_fail(self): + def test_encode_registry_fail(self) -> None: with pytest.raises(OSError): Image._getencoder("RGB", "DoesNotExist", ("args",), extra=("extra",)) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 4ae56fae0cc..00cd4e7a9a1 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -35,16 +35,16 @@ class AccessTest: _need_cffi_access = False @classmethod - def setup_class(cls): + def setup_class(cls) -> None: Image.USE_CFFI_ACCESS = cls._need_cffi_access @classmethod - def teardown_class(cls): + def teardown_class(cls) -> None: Image.USE_CFFI_ACCESS = cls._init_cffi_access class TestImagePutPixel(AccessTest): - def test_sanity(self): + def test_sanity(self) -> None: im1 = hopper() im2 = Image.new(im1.mode, im1.size, 0) @@ -81,7 +81,7 @@ def test_sanity(self): assert_image_equal(im1, im2) - def test_sanity_negative_index(self): + def test_sanity_negative_index(self) -> None: im1 = hopper() im2 = Image.new(im1.mode, im1.size, 0) @@ -119,7 +119,7 @@ def test_sanity_negative_index(self): assert_image_equal(im1, im2) @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy(self): + def test_numpy(self) -> None: im = hopper() pix = im.load() @@ -138,7 +138,7 @@ def color(mode): return (16, 32, 49) return tuple(range(1, bands + 1)) - def check(self, mode, expected_color=None): + def check(self, mode, expected_color=None) -> None: if self._need_cffi_access and mode.startswith("BGR;"): pytest.skip("Support not added to deprecated module for BGR;* modes") @@ -222,10 +222,10 @@ def check(self, mode, expected_color=None): "YCbCr", ), ) - def test_basic(self, mode): + def test_basic(self, mode) -> None: self.check(mode) - def test_list(self): + def test_list(self) -> None: im = hopper() assert im.getpixel([0, 0]) == (20, 20, 70) @@ -233,14 +233,14 @@ def test_list(self): @pytest.mark.parametrize( "expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1) ) - def test_signedness(self, mode, expected_color): + def test_signedness(self, mode, expected_color) -> None: # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* self.check(mode, expected_color) @pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) - def test_p_putpixel_rgb_rgba(self, mode, color): + def test_p_putpixel_rgb_rgba(self, mode, color) -> None: im = Image.new(mode, (1, 1)) im.putpixel((0, 0), color) @@ -264,7 +264,7 @@ class TestCffiGetPixel(TestImageGetPixel): class TestCffi(AccessTest): _need_cffi_access = True - def _test_get_access(self, im): + def _test_get_access(self, im) -> None: """Do we get the same thing as the old pixel access Using private interfaces, forcing a capi access and @@ -282,7 +282,7 @@ def _test_get_access(self, im): with pytest.raises(ValueError): access[(access.xsize + 1, access.ysize + 1)] - def test_get_vs_c(self): + def test_get_vs_c(self) -> None: with pytest.warns(DeprecationWarning): rgb = hopper("RGB") rgb.load() @@ -301,7 +301,7 @@ def test_get_vs_c(self): # im = Image.new('I;32B', (10, 10), 2**10) # self._test_get_access(im) - def _test_set_access(self, im, color): + def _test_set_access(self, im, color) -> None: """Are we writing the correct bits into the image? Using private interfaces, forcing a capi access and @@ -322,7 +322,7 @@ def _test_set_access(self, im, color): with pytest.raises(ValueError): access[(0, 0)] = color - def test_set_vs_c(self): + def test_set_vs_c(self) -> None: rgb = hopper("RGB") with pytest.warns(DeprecationWarning): rgb.load() @@ -345,11 +345,11 @@ def test_set_vs_c(self): # self._test_set_access(im, 2**13-1) @pytest.mark.filterwarnings("ignore::DeprecationWarning") - def test_not_implemented(self): + def test_not_implemented(self) -> None: assert PyAccess.new(hopper("BGR;15")) is None # ref https://github.com/python-pillow/Pillow/pull/2009 - def test_reference_counting(self): + def test_reference_counting(self) -> None: size = 10 for _ in range(10): @@ -361,7 +361,7 @@ def test_reference_counting(self): assert px[i, 0] == 0 @pytest.mark.parametrize("mode", ("P", "PA")) - def test_p_putpixel_rgb_rgba(self, mode): + def test_p_putpixel_rgb_rgba(self, mode) -> None: for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): im = Image.new(mode, (1, 1)) with pytest.warns(DeprecationWarning): @@ -379,7 +379,7 @@ class TestImagePutPixelError(AccessTest): INVALID_TYPES = ["foo", 1.0, None] @pytest.mark.parametrize("mode", IMAGE_MODES1) - def test_putpixel_type_error1(self, mode): + def test_putpixel_type_error1(self, mode) -> None: im = hopper(mode) for v in self.INVALID_TYPES: with pytest.raises(TypeError, match="color must be int or tuple"): @@ -402,14 +402,14 @@ def test_putpixel_type_error1(self, mode): ), ), ) - def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match): + def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match) -> None: im = hopper(mode) for band_number in band_numbers: with pytest.raises(TypeError, match=match): im.putpixel((0, 0), (0,) * band_number) @pytest.mark.parametrize("mode", IMAGE_MODES2) - def test_putpixel_type_error2(self, mode): + def test_putpixel_type_error2(self, mode) -> None: im = hopper(mode) for v in self.INVALID_TYPES: with pytest.raises( @@ -418,7 +418,7 @@ def test_putpixel_type_error2(self, mode): im.putpixel((0, 0), v) @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) - def test_putpixel_overflow_error(self, mode): + def test_putpixel_overflow_error(self, mode) -> None: im = hopper(mode) with pytest.raises(OverflowError): im.putpixel((0, 0), 2**80) @@ -427,7 +427,7 @@ def test_putpixel_overflow_error(self, mode): class TestEmbeddable: @pytest.mark.xfail(reason="failing test") @pytest.mark.skipif(not is_win32(), reason="requires Windows") - def test_embeddable(self): + def test_embeddable(self) -> None: import ctypes from setuptools.command.build_ext import new_compiler diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 0dacb3157a2..0125ab56af9 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -12,12 +12,12 @@ im = hopper().resize((128, 100)) -def test_toarray(): +def test_toarray() -> None: def test(mode): ai = numpy.array(im.convert(mode)) return ai.shape, ai.dtype.str, ai.nbytes - def test_with_dtype(dtype): + def test_with_dtype(dtype) -> None: ai = numpy.array(im, dtype=dtype) assert ai.dtype == dtype @@ -46,11 +46,11 @@ def test_with_dtype(dtype): numpy.array(im_truncated) -def test_fromarray(): +def test_fromarray() -> None: class Wrapper: """Class with API matching Image.fromarray""" - def __init__(self, img, arr_params): + def __init__(self, img, arr_params) -> None: self.img = img self.__array_interface__ = arr_params @@ -89,7 +89,7 @@ def test(mode): Image.fromarray(wrapped) -def test_fromarray_palette(): +def test_fromarray_palette() -> None: # Arrange i = im.convert("L") a = numpy.array(i) diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 08c40af1f16..54474311a09 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -19,7 +19,7 @@ def draft_roundtrip(in_mode, in_size, req_mode, req_size): return im -def test_size(): +def test_size() -> None: for in_size, req_size, out_size in [ ((435, 361), (2048, 2048), (435, 361)), # bigger ((435, 361), (435, 361), (435, 361)), # same @@ -48,7 +48,7 @@ def test_size(): assert im.size == out_size -def test_mode(): +def test_mode() -> None: for in_mode, req_mode, out_mode in [ ("RGB", "1", "RGB"), ("RGB", "L", "L"), @@ -68,7 +68,7 @@ def test_mode(): assert im.mode == out_mode -def test_several_drafts(): +def test_several_drafts() -> None: im = draft_roundtrip("L", (128, 128), None, (64, 64)) im.draft(None, (64, 64)) im.load() diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py index fce16122409..01107ae6b88 100644 --- a/Tests/test_image_entropy.py +++ b/Tests/test_image_entropy.py @@ -3,7 +3,7 @@ from .helper import hopper -def test_entropy(): +def test_entropy() -> None: def entropy(mode): return hopper(mode).entropy() diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 3fa5dd2423a..2b6787933cd 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -36,7 +36,7 @@ ), ) @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity(filter_to_apply, mode): +def test_sanity(filter_to_apply, mode) -> None: im = hopper(mode) if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): out = im.filter(filter_to_apply) @@ -45,7 +45,7 @@ def test_sanity(filter_to_apply, mode): @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity_error(mode): +def test_sanity_error(mode) -> None: with pytest.raises(TypeError): im = hopper(mode) im.filter("hello") @@ -53,7 +53,7 @@ def test_sanity_error(mode): # crashes on small images @pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3))) -def test_crash(size): +def test_crash(size) -> None: im = Image.new("RGB", size) im.filter(ImageFilter.SMOOTH) @@ -67,7 +67,7 @@ def test_crash(size): ("RGB", ((4, 0, 0), (0, 0, 0))), ), ) -def test_modefilter(mode, expected): +def test_modefilter(mode, expected) -> None: im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) # image is: @@ -90,7 +90,7 @@ def test_modefilter(mode, expected): ("F", (0.0, 4.0, 8.0)), ), ) -def test_rankfilter(mode, expected): +def test_rankfilter(mode, expected) -> None: im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) # image is: @@ -106,7 +106,7 @@ def test_rankfilter(mode, expected): @pytest.mark.parametrize( "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter) ) -def test_rankfilter_error(filter): +def test_rankfilter_error(filter) -> None: with pytest.raises(ValueError): im = Image.new("P", (3, 3), None) im.putdata(list(range(9))) @@ -117,27 +117,27 @@ def test_rankfilter_error(filter): im.filter(filter).getpixel((1, 1)) -def test_rankfilter_properties(): +def test_rankfilter_properties() -> None: rankfilter = ImageFilter.RankFilter(1, 2) assert rankfilter.size == 1 assert rankfilter.rank == 2 -def test_builtinfilter_p(): +def test_builtinfilter_p() -> None: builtin_filter = ImageFilter.BuiltinFilter() with pytest.raises(ValueError): builtin_filter.filter(hopper("P")) -def test_kernel_not_enough_coefficients(): +def test_kernel_not_enough_coefficients() -> None: with pytest.raises(ValueError): ImageFilter.Kernel((3, 3), (0, 0)) @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_3x3(mode): +def test_consistency_3x3(mode) -> None: with Image.open("Tests/images/hopper.bmp") as source: reference_name = "hopper_emboss" reference_name += "_I.png" if mode == "I" else ".bmp" @@ -163,7 +163,7 @@ def test_consistency_3x3(mode): @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_5x5(mode): +def test_consistency_5x5(mode) -> None: with Image.open("Tests/images/hopper.bmp") as source: reference_name = "hopper_emboss_more" reference_name += "_I.png" if mode == "I" else ".bmp" @@ -199,7 +199,7 @@ def test_consistency_5x5(mode): (2, -2), ), ) -def test_invalid_box_blur_filter(radius): +def test_invalid_box_blur_filter(radius) -> None: with pytest.raises(ValueError): ImageFilter.BoxBlur(radius) diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index 6bbc4da9a01..0107fdcc426 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -5,7 +5,7 @@ from .helper import hopper -def test_extrema(): +def test_extrema() -> None: def extrema(mode): return hopper(mode).getextrema() @@ -20,7 +20,7 @@ def extrema(mode): assert extrema("I;16") == (1, 255) -def test_true_16(): +def test_true_16() -> None: with Image.open("Tests/images/16_bit_noise.tif") as im: assert im.mode == "I;16" extrema = im.getextrema() diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index 4340f46f619..e7304c98f30 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -5,7 +5,7 @@ from .helper import hopper -def test_palette(): +def test_palette() -> None: def palette(mode): p = hopper(mode).getpalette() if p: @@ -23,7 +23,7 @@ def palette(mode): assert palette("YCbCr") is None -def test_palette_rawmode(): +def test_palette_rawmode() -> None: im = Image.new("P", (1, 1)) im.putpalette((1, 2, 3)) diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 36f8ba575d8..5b1a9ee2dda 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -10,14 +10,14 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() pix = im.load() assert pix[0, 0] == (20, 20, 70) -def test_close(): +def test_close() -> None: im = Image.open("Tests/images/hopper.gif") im.close() with pytest.raises(ValueError): @@ -26,7 +26,7 @@ def test_close(): im.getpixel((0, 0)) -def test_close_after_load(caplog): +def test_close_after_load(caplog) -> None: im = Image.open("Tests/images/hopper.gif") im.load() with caplog.at_level(logging.DEBUG): @@ -34,7 +34,7 @@ def test_close_after_load(caplog): assert len(caplog.records) == 0 -def test_contextmanager(): +def test_contextmanager() -> None: fn = None with Image.open("Tests/images/hopper.gif") as im: fn = im.fp.fileno() @@ -44,7 +44,7 @@ def test_contextmanager(): os.fstat(fn) -def test_contextmanager_non_exclusive_fp(): +def test_contextmanager_non_exclusive_fp() -> None: with open("Tests/images/hopper.gif", "rb") as fp: with Image.open(fp): pass diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 3c1d494fab2..8e94aafc598 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -7,7 +7,7 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: with hopper() as im: im.mode @@ -69,7 +69,7 @@ def test_sanity(): ) def test_properties( mode, expected_base, expected_type, expected_bands, expected_band_names -): +) -> None: assert Image.getmodebase(mode) == expected_base assert Image.getmodetype(mode) == expected_type assert Image.getmodebands(mode) == expected_bands diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index fd117f9dbc9..34a2f8f3db5 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -11,7 +11,7 @@ class TestImagingPaste: masks = {} size = 128 - def assert_9points_image(self, im, expected): + def assert_9points_image(self, im, expected) -> None: expected = [ point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected ] @@ -29,7 +29,7 @@ def assert_9points_image(self, im, expected): ] assert actual == expected - def assert_9points_paste(self, im, im2, mask, expected): + def assert_9points_paste(self, im, im2, mask, expected) -> None: im3 = im.copy() im3.paste(im2, (0, 0), mask) self.assert_9points_image(im3, expected) @@ -106,7 +106,7 @@ def gradient_RGBa(self): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_solid(self, mode): + def test_image_solid(self, mode) -> None: im = Image.new(mode, (200, 200), "red") im2 = getattr(self, "gradient_" + mode) @@ -116,7 +116,7 @@ def test_image_solid(self, mode): assert_image_equal(im, im2) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_1(self, mode): + def test_image_mask_1(self, mode) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -138,7 +138,7 @@ def test_image_mask_1(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_L(self, mode): + def test_image_mask_L(self, mode) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -160,7 +160,7 @@ def test_image_mask_L(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_LA(self, mode): + def test_image_mask_LA(self, mode) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -182,7 +182,7 @@ def test_image_mask_LA(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBA(self, mode): + def test_image_mask_RGBA(self, mode) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -204,7 +204,7 @@ def test_image_mask_RGBA(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBa(self, mode): + def test_image_mask_RGBa(self, mode) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -226,7 +226,7 @@ def test_image_mask_RGBa(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_solid(self, mode): + def test_color_solid(self, mode) -> None: im = Image.new(mode, (200, 200), "black") rect = (12, 23, 128 + 12, 128 + 23) @@ -239,7 +239,7 @@ def test_color_solid(self, mode): assert sum(head[:255]) == 0 @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_1(self, mode): + def test_color_mask_1(self, mode) -> None: im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) color = (10, 20, 30, 40)[: len(mode)] @@ -261,7 +261,7 @@ def test_color_mask_1(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_L(self, mode): + def test_color_mask_L(self, mode) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -283,7 +283,7 @@ def test_color_mask_L(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBA(self, mode): + def test_color_mask_RGBA(self, mode) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -305,7 +305,7 @@ def test_color_mask_RGBA(self, mode): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBa(self, mode): + def test_color_mask_RGBa(self, mode) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -326,7 +326,7 @@ def test_color_mask_RGBa(self, mode): ], ) - def test_different_sizes(self): + def test_different_sizes(self) -> None: im = Image.new("RGB", (100, 100)) im2 = Image.new("RGB", (50, 50)) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 2648af8fa2e..10301991624 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -10,7 +10,7 @@ from .helper import assert_image_equal, hopper -def test_sanity(): +def test_sanity() -> None: im1 = hopper() data = list(im1.getdata()) @@ -29,7 +29,7 @@ def test_sanity(): assert_image_equal(im1, im2) -def test_long_integers(): +def test_long_integers() -> None: # see bug-200802-systemerror def put(value): im = Image.new("RGBA", (1, 1)) @@ -46,19 +46,19 @@ def put(value): assert put(sys.maxsize) == (255, 255, 255, 127) -def test_pypy_performance(): +def test_pypy_performance() -> None: im = Image.new("L", (256, 256)) im.putdata(list(range(256)) * 256) -def test_mode_with_L_with_float(): +def test_mode_with_L_with_float() -> None: im = Image.new("L", (1, 1), 0) im.putdata([2.0]) assert im.getpixel((0, 0)) == 2 @pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) -def test_mode_i(mode): +def test_mode_i(mode) -> None: src = hopper("L") data = list(src.getdata()) im = Image.new(mode, src.size, 0) @@ -68,7 +68,7 @@ def test_mode_i(mode): assert list(im.getdata()) == target -def test_mode_F(): +def test_mode_F() -> None: src = hopper("L") data = list(src.getdata()) im = Image.new("F", src.size, 0) @@ -79,7 +79,7 @@ def test_mode_F(): @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) -def test_mode_BGR(mode): +def test_mode_BGR(mode) -> None: data = [(16, 32, 49), (32, 32, 98)] im = Image.new(mode, (1, 2)) im.putdata(data) @@ -87,7 +87,7 @@ def test_mode_BGR(mode): assert list(im.getdata()) == data -def test_array_B(): +def test_array_B() -> None: # shouldn't segfault # see https://github.com/python-pillow/Pillow/issues/1008 @@ -98,7 +98,7 @@ def test_array_B(): assert len(im.getdata()) == len(arr) -def test_array_F(): +def test_array_F() -> None: # shouldn't segfault # see https://github.com/python-pillow/Pillow/issues/1008 @@ -109,7 +109,7 @@ def test_array_F(): assert len(im.getdata()) == len(arr) -def test_not_flattened(): +def test_not_flattened() -> None: im = Image.new("L", (1, 1)) with pytest.raises(TypeError): im.putdata([[0]]) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 43b65be2b17..ffe2551d239 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -7,7 +7,7 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper -def test_putpalette(): +def test_putpalette() -> None: def palette(mode): im = hopper(mode).copy() im.putpalette(list(range(256)) * 3) @@ -43,7 +43,7 @@ def palette(mode): im.putpalette(list(range(256)) * 3) -def test_imagepalette(): +def test_imagepalette() -> None: im = hopper("P") im.putpalette(ImagePalette.negative()) assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png") @@ -57,7 +57,7 @@ def test_imagepalette(): assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png") -def test_putpalette_with_alpha_values(): +def test_putpalette_with_alpha_values() -> None: with Image.open("Tests/images/transparent.gif") as im: expected = im.convert("RGBA") @@ -81,19 +81,19 @@ def test_putpalette_with_alpha_values(): ("RGBAX", (1, 2, 3, 4, 0)), ), ) -def test_rgba_palette(mode, palette): +def test_rgba_palette(mode, palette) -> None: im = Image.new("P", (1, 1)) im.putpalette(palette, mode) assert im.getpalette() == [1, 2, 3] assert im.palette.colors == {(1, 2, 3, 4): 0} -def test_empty_palette(): +def test_empty_palette() -> None: im = Image.new("P", (1, 1)) assert im.getpalette() == [] -def test_undefined_palette_index(): +def test_undefined_palette_index() -> None: im = Image.new("P", (1, 1), 3) im.putpalette((1, 2, 3)) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0) diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index ba9100415a2..c29830a7e5f 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -48,7 +48,7 @@ ((1, 3), (10, 4)), ), ) -def test_args_factor(size, expected): +def test_args_factor(size, expected) -> None: im = Image.new("L", (10, 10)) assert expected == im.reduce(size).size @@ -56,7 +56,7 @@ def test_args_factor(size, expected): @pytest.mark.parametrize( "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) ) -def test_args_factor_error(size, expected_error): +def test_args_factor_error(size, expected_error) -> None: im = Image.new("L", (10, 10)) with pytest.raises(expected_error): im.reduce(size) @@ -69,7 +69,7 @@ def test_args_factor_error(size, expected_error): ((5, 5, 6, 6), (1, 1)), ), ) -def test_args_box(size, expected): +def test_args_box(size, expected) -> None: im = Image.new("L", (10, 10)) assert expected == im.reduce(2, size).size @@ -86,14 +86,14 @@ def test_args_box(size, expected): ((5, 0, 5, 10), ValueError), ), ) -def test_args_box_error(size, expected_error): +def test_args_box_error(size, expected_error) -> None: im = Image.new("L", (10, 10)) with pytest.raises(expected_error): im.reduce(2, size).size @pytest.mark.parametrize("mode", ("P", "1", "I;16")) -def test_unsupported_modes(mode): +def test_unsupported_modes(mode) -> None: im = Image.new("P", (10, 10)) with pytest.raises(ValueError): im.reduce(3) @@ -119,14 +119,16 @@ def get_image(mode): return im.crop((0, 0, im.width, im.height - 5)) -def compare_reduce_with_box(im, factor): +def compare_reduce_with_box(im, factor) -> None: box = (11, 13, 146, 164) reduced = im.reduce(factor, box=box) reference = im.crop(box).reduce(factor) assert reduced == reference -def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1): +def compare_reduce_with_reference( + im, factor, average_diff: float = 0.4, max_diff: int = 1 +) -> None: """Image.reduce() should look very similar to Image.resize(BOX). A reference image is compiled from a large source area @@ -171,7 +173,7 @@ def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1): assert_compare_images(reduced, reference, average_diff, max_diff) -def assert_compare_images(a, b, max_average_diff, max_diff=255): +def assert_compare_images(a, b, max_average_diff, max_diff: int = 255) -> None: assert a.mode == b.mode, f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, f"got size {repr(a.size)}, expected {repr(b.size)}" @@ -199,20 +201,20 @@ def assert_compare_images(a, b, max_average_diff, max_diff=255): @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_L(factor): +def test_mode_L(factor) -> None: im = get_image("L") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA(factor): +def test_mode_LA(factor) -> None: im = get_image("LA") compare_reduce_with_reference(im, factor, 0.8, 5) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA_opaque(factor): +def test_mode_LA_opaque(factor) -> None: im = get_image("LA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) @@ -221,27 +223,27 @@ def test_mode_LA_opaque(factor): @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_La(factor): +def test_mode_La(factor) -> None: im = get_image("La") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGB(factor): +def test_mode_RGB(factor) -> None: im = get_image("RGB") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA(factor): +def test_mode_RGBA(factor) -> None: im = get_image("RGBA") compare_reduce_with_reference(im, factor, 0.8, 5) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA_opaque(factor): +def test_mode_RGBA_opaque(factor) -> None: im = get_image("RGBA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) @@ -250,27 +252,27 @@ def test_mode_RGBA_opaque(factor): @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBa(factor): +def test_mode_RGBa(factor) -> None: im = get_image("RGBa") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_I(factor): +def test_mode_I(factor) -> None: im = get_image("I") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_F(factor): +def test_mode_F(factor) -> None: im = get_image("F") compare_reduce_with_reference(im, factor, 0, 0) compare_reduce_with_box(im, factor) @skip_unless_feature("jpg_2000") -def test_jpeg2k(): +def test_jpeg2k() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert im.reduce(2).size == (320, 240) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index af730dce13d..f4c9eb0e6fa 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -16,7 +16,7 @@ class TestImagingResampleVulnerability: # see https://github.com/python-pillow/Pillow/issues/1710 - def test_overflow(self): + def test_overflow(self) -> None: im = hopper("L") size_too_large = 0x100000008 // 4 size_normal = 1000 # unimportant @@ -28,7 +28,7 @@ def test_overflow(self): # any resampling filter will do here im.im.resize((xsize, ysize), Image.Resampling.BILINEAR) - def test_invalid_size(self): + def test_invalid_size(self) -> None: im = hopper() # Should not crash @@ -40,7 +40,7 @@ def test_invalid_size(self): with pytest.raises(ValueError): im.resize((100, -100)) - def test_modify_after_resizing(self): + def test_modify_after_resizing(self) -> None: im = hopper("RGB") # get copy with same size copy = im.resize(im.size) @@ -83,7 +83,7 @@ def make_sample(self, data, size): s_px[size[0] - x - 1, y] = 255 - val return sample - def check_case(self, case, sample): + def check_case(self, case, sample) -> None: s_px = sample.load() c_px = case.load() for y in range(case.size[1]): @@ -103,7 +103,7 @@ def serialize_image(self, image): ) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_box(self, mode): + def test_reduce_box(self, mode) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off @@ -114,7 +114,7 @@ def test_reduce_box(self, mode): self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bilinear(self, mode): + def test_reduce_bilinear(self, mode) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off @@ -125,7 +125,7 @@ def test_reduce_bilinear(self, mode): self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_hamming(self, mode): + def test_reduce_hamming(self, mode) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off @@ -136,7 +136,7 @@ def test_reduce_hamming(self, mode): self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bicubic(self, mode): + def test_reduce_bicubic(self, mode) -> None: case = self.make_case(mode, (12, 12), 0xE1) case = case.resize((6, 6), Image.Resampling.BICUBIC) # fmt: off @@ -148,7 +148,7 @@ def test_reduce_bicubic(self, mode): self.check_case(channel, self.make_sample(data, (6, 6))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_lanczos(self, mode): + def test_reduce_lanczos(self, mode) -> None: case = self.make_case(mode, (16, 16), 0xE1) case = case.resize((8, 8), Image.Resampling.LANCZOS) # fmt: off @@ -161,7 +161,7 @@ def test_reduce_lanczos(self, mode): self.check_case(channel, self.make_sample(data, (8, 8))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_box(self, mode): + def test_enlarge_box(self, mode) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off @@ -172,7 +172,7 @@ def test_enlarge_box(self, mode): self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bilinear(self, mode): + def test_enlarge_bilinear(self, mode) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off @@ -183,7 +183,7 @@ def test_enlarge_bilinear(self, mode): self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_hamming(self, mode): + def test_enlarge_hamming(self, mode) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off @@ -194,7 +194,7 @@ def test_enlarge_hamming(self, mode): self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bicubic(self, mode): + def test_enlarge_bicubic(self, mode) -> None: case = self.make_case(mode, (4, 4), 0xE1) case = case.resize((8, 8), Image.Resampling.BICUBIC) # fmt: off @@ -207,7 +207,7 @@ def test_enlarge_bicubic(self, mode): self.check_case(channel, self.make_sample(data, (8, 8))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_lanczos(self, mode): + def test_enlarge_lanczos(self, mode) -> None: case = self.make_case(mode, (6, 6), 0xE1) case = case.resize((12, 12), Image.Resampling.LANCZOS) data = ( @@ -221,7 +221,7 @@ def test_enlarge_lanczos(self, mode): for channel in case.split(): self.check_case(channel, self.make_sample(data, (12, 12))) - def test_box_filter_correct_range(self): + def test_box_filter_correct_range(self) -> None: im = Image.new("RGB", (8, 8), "#1688ff").resize( (100, 100), Image.Resampling.BOX ) @@ -234,7 +234,7 @@ def make_case(self, mode, fill): im = Image.new(mode, (512, 9), fill) return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] - def run_case(self, case): + def run_case(self, case) -> None: channel, color = case px = channel.load() for x in range(channel.size[0]): @@ -243,7 +243,7 @@ def run_case(self, case): message = f"{px[x, y]} != {color} for pixel {(x, y)}" assert px[x, y] == color, message - def test_8u(self): + def test_8u(self) -> None: im, color = self.make_case("RGB", (0, 64, 255)) r, g, b = im.split() self.run_case((r, color[0])) @@ -251,13 +251,13 @@ def test_8u(self): self.run_case((b, color[2])) self.run_case(self.make_case("L", 12)) - def test_32i(self): + def test_32i(self) -> None: self.run_case(self.make_case("I", 12)) self.run_case(self.make_case("I", 0x7FFFFFFF)) self.run_case(self.make_case("I", -12)) self.run_case(self.make_case("I", -1 << 31)) - def test_32f(self): + def test_32f(self) -> None: self.run_case(self.make_case("F", 1)) self.run_case(self.make_case("F", 3.40282306074e38)) self.run_case(self.make_case("F", 1.175494e-38)) @@ -275,7 +275,7 @@ def make_levels_case(self, mode): px[x, y] = tuple(pix) return i - def run_levels_case(self, i): + def run_levels_case(self, i) -> None: px = i.load() for y in range(i.size[1]): used_colors = {px[x, y][0] for x in range(i.size[0])} @@ -285,7 +285,7 @@ def run_levels_case(self, i): ) @pytest.mark.xfail(reason="Current implementation isn't precise enough") - def test_levels_rgba(self): + def test_levels_rgba(self) -> None: case = self.make_levels_case("RGBA") self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) @@ -294,7 +294,7 @@ def test_levels_rgba(self): self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) @pytest.mark.xfail(reason="Current implementation isn't precise enough") - def test_levels_la(self): + def test_levels_la(self) -> None: case = self.make_levels_case("LA") self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) @@ -312,7 +312,7 @@ def make_dirty_case(self, mode, clean_pixel, dirty_pixel): px[x + xdiv4, y + ydiv4] = clean_pixel return i - def run_dirty_case(self, i, clean_pixel): + def run_dirty_case(self, i, clean_pixel) -> None: px = i.load() for y in range(i.size[1]): for x in range(i.size[0]): @@ -323,7 +323,7 @@ def run_dirty_case(self, i, clean_pixel): ) assert px[x, y][:3] == clean_pixel, message - def test_dirty_pixels_rgba(self): + def test_dirty_pixels_rgba(self) -> None: case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255, 255, 0)) self.run_dirty_case( @@ -339,7 +339,7 @@ def test_dirty_pixels_rgba(self): case.resize((20, 20), Image.Resampling.LANCZOS), (255, 255, 0) ) - def test_dirty_pixels_la(self): + def test_dirty_pixels_la(self) -> None: case = self.make_dirty_case("LA", (255, 128), (0, 0)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255,)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BILINEAR), (255,)) @@ -355,22 +355,22 @@ def count(self, diff): yield assert Image.core.get_stats()["new_count"] - count == diff - def test_horizontal(self): + def test_horizontal(self) -> None: im = hopper("L") with self.count(1): im.resize((im.size[0] - 10, im.size[1]), Image.Resampling.BILINEAR) - def test_vertical(self): + def test_vertical(self) -> None: im = hopper("L") with self.count(1): im.resize((im.size[0], im.size[1] - 10), Image.Resampling.BILINEAR) - def test_both(self): + def test_both(self) -> None: im = hopper("L") with self.count(2): im.resize((im.size[0] - 10, im.size[1] - 10), Image.Resampling.BILINEAR) - def test_box_horizontal(self): + def test_box_horizontal(self) -> None: im = hopper("L") box = (20, 0, im.size[0] - 20, im.size[1]) with self.count(1): @@ -380,7 +380,7 @@ def test_box_horizontal(self): cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR) assert_image_similar(with_box, cropped, 0.1) - def test_box_vertical(self): + def test_box_vertical(self) -> None: im = hopper("L") box = (0, 20, im.size[0], im.size[1] - 20) with self.count(1): @@ -392,7 +392,7 @@ def test_box_vertical(self): class TestCoreResampleCoefficients: - def test_reduce(self): + def test_reduce(self) -> None: test_color = 254 for size in range(400000, 400010, 2): @@ -404,7 +404,7 @@ def test_reduce(self): if px[2, 0] != test_color // 2: assert test_color // 2 == px[2, 0] - def test_non_zero_coefficients(self): + def test_non_zero_coefficients(self) -> None: # regression test for the wrong coefficients calculation # due to bug https://github.com/python-pillow/Pillow/issues/2161 im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) @@ -432,7 +432,7 @@ class TestCoreResampleBox: Image.Resampling.LANCZOS, ), ) - def test_wrong_arguments(self, resample): + def test_wrong_arguments(self, resample) -> None: im = hopper() im.resize((32, 32), resample, (0, 0, im.width, im.height)) im.resize((32, 32), resample, (20, 20, im.width, im.height)) @@ -478,7 +478,7 @@ def split_range(size, tiles): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_tiles(self): + def test_tiles(self) -> None: with Image.open("Tests/images/flower.jpg") as im: assert im.size == (480, 360) dst_size = (251, 188) @@ -491,7 +491,7 @@ def test_tiles(self): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_subsample(self): + def test_subsample(self) -> None: # This test shows advantages of the subpixel resizing # after supersampling (e.g. during JPEG decoding). with Image.open("Tests/images/flower.jpg") as im: @@ -518,14 +518,14 @@ def test_subsample(self): @pytest.mark.parametrize( "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR) ) - def test_formats(self, mode, resample): + def test_formats(self, mode, resample) -> None: im = hopper(mode) box = (20, 20, im.size[0] - 20, im.size[1] - 20) with_box = im.resize((32, 32), resample, box) cropped = im.crop(box).resize((32, 32), resample) assert_image_similar(cropped, with_box, 0.4) - def test_passthrough(self): + def test_passthrough(self) -> None: # When no resize is required im = hopper() @@ -539,7 +539,7 @@ def test_passthrough(self): assert res.size == size assert_image_equal(res, im.crop(box), f">>> {size} {box}") - def test_no_passthrough(self): + def test_no_passthrough(self) -> None: # When resize is required im = hopper() @@ -558,7 +558,7 @@ def test_no_passthrough(self): @pytest.mark.parametrize( "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) ) - def test_skip_horizontal(self, flt): + def test_skip_horizontal(self, flt) -> None: # Can skip resize for one dimension im = hopper() @@ -581,7 +581,7 @@ def test_skip_horizontal(self, flt): @pytest.mark.parametrize( "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) ) - def test_skip_vertical(self, flt): + def test_skip_vertical(self, flt) -> None: # Can skip resize for one dimension im = hopper() diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index e63fef2c152..51e0f585421 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -12,7 +12,7 @@ ) -def rotate(im, mode, angle, center=None, translate=None): +def rotate(im, mode, angle, center=None, translate=None) -> None: out = im.rotate(angle, center=center, translate=translate) assert out.mode == mode assert out.size == im.size # default rotate clips output @@ -27,13 +27,13 @@ def rotate(im, mode, angle, center=None, translate=None): @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_mode(mode): +def test_mode(mode) -> None: im = hopper(mode) rotate(im, mode, 45) @pytest.mark.parametrize("angle", (0, 90, 180, 270)) -def test_angle(angle): +def test_angle(angle) -> None: with Image.open("Tests/images/test-card.png") as im: rotate(im, im.mode, angle) @@ -42,12 +42,12 @@ def test_angle(angle): @pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) -def test_zero(angle): +def test_zero(angle) -> None: im = Image.new("RGB", (0, 0)) rotate(im, im.mode, angle) -def test_resample(): +def test_resample() -> None: # Target image creation, inspected by eye. # >>> im = Image.open('Tests/images/hopper.ppm') # >>> im = im.rotate(45, resample=Image.Resampling.BICUBIC, expand=True) @@ -64,7 +64,7 @@ def test_resample(): assert_image_similar(im, target, epsilon) -def test_center_0(): +def test_center_0() -> None: im = hopper() im = im.rotate(45, center=(0, 0), resample=Image.Resampling.BICUBIC) @@ -75,7 +75,7 @@ def test_center_0(): assert_image_similar(im, target, 15) -def test_center_14(): +def test_center_14() -> None: im = hopper() im = im.rotate(45, center=(14, 14), resample=Image.Resampling.BICUBIC) @@ -86,7 +86,7 @@ def test_center_14(): assert_image_similar(im, target, 10) -def test_translate(): +def test_translate() -> None: im = hopper() with Image.open("Tests/images/hopper_45.png") as target: target_origin = (target.size[1] / 2 - 64) - 5 @@ -99,7 +99,7 @@ def test_translate(): assert_image_similar(im, target, 1) -def test_fastpath_center(): +def test_fastpath_center() -> None: # if the center is -1,-1 and we rotate by 90<=x<=270 the # resulting image should be black for angle in (90, 180, 270): @@ -107,7 +107,7 @@ def test_fastpath_center(): assert_image_equal(im, Image.new("RGB", im.size, "black")) -def test_fastpath_translate(): +def test_fastpath_translate() -> None: # if we post-translate by -128 # resulting image should be black for angle in (0, 90, 180, 270): @@ -115,26 +115,26 @@ def test_fastpath_translate(): assert_image_equal(im, Image.new("RGB", im.size, "black")) -def test_center(): +def test_center() -> None: im = hopper() rotate(im, im.mode, 45, center=(0, 0)) rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0)) rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0)) -def test_rotate_no_fill(): +def test_rotate_no_fill() -> None: im = Image.new("RGB", (100, 100), "green") im = im.rotate(45) assert_image_equal_tofile(im, "Tests/images/rotate_45_no_fill.png") -def test_rotate_with_fill(): +def test_rotate_with_fill() -> None: im = Image.new("RGB", (100, 100), "green") im = im.rotate(45, fillcolor="white") assert_image_equal_tofile(im, "Tests/images/rotate_45_with_fill.png") -def test_alpha_rotate_no_fill(): +def test_alpha_rotate_no_fill() -> None: # Alpha images are handled differently internally im = Image.new("RGBA", (10, 10), "green") im = im.rotate(45, expand=1) @@ -142,7 +142,7 @@ def test_alpha_rotate_no_fill(): assert corner == (0, 0, 0, 0) -def test_alpha_rotate_with_fill(): +def test_alpha_rotate_with_fill() -> None: # Alpha images are handled differently internally im = Image.new("RGBA", (10, 10), "green") im = im.rotate(45, expand=1, fillcolor=(255, 0, 0, 255)) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 7fa5692aa7d..6aeeea2ed0d 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -14,14 +14,14 @@ ) -def test_sanity(): +def test_sanity() -> None: im = hopper() assert im.thumbnail((100, 100)) is None assert im.size == (100, 100) -def test_aspect(): +def test_aspect() -> None: im = Image.new("L", (128, 128)) im.thumbnail((100, 100)) assert im.size == (100, 100) @@ -67,19 +67,19 @@ def test_aspect(): assert im.size == (75, 23) # ratio is 3.260869565217 -def test_division_by_zero(): +def test_division_by_zero() -> None: im = Image.new("L", (200, 2)) im.thumbnail((75, 75)) assert im.size == (75, 1) -def test_float(): +def test_float() -> None: im = Image.new("L", (128, 128)) im.thumbnail((99.9, 99.9)) assert im.size == (99, 99) -def test_no_resize(): +def test_no_resize() -> None: # Check that draft() can resize the image to the destination size with Image.open("Tests/images/hopper.jpg") as im: im.draft(None, (64, 64)) @@ -92,7 +92,7 @@ def test_no_resize(): @skip_unless_feature("libtiff") -def test_load_first(): +def test_load_first() -> None: # load() may change the size of the image # Test that thumbnail() is calling it before performing size calculations with Image.open("Tests/images/g4_orientation_5.tif") as im: @@ -106,7 +106,7 @@ def test_load_first(): assert im.size == (590, 88) -def test_load_first_unless_jpeg(): +def test_load_first_unless_jpeg() -> None: # Test that thumbnail() still uses draft() for JPEG with Image.open("Tests/images/hopper.jpg") as im: draft = im.draft @@ -124,7 +124,7 @@ def im_draft(mode, size): # valgrind test is failing with memory allocated in libjpeg @pytest.mark.valgrind_known_error(reason="Known Failing") -def test_DCT_scaling_edges(): +def test_DCT_scaling_edges() -> None: # Make an image with red borders and size (N * 8) + 1 to cross DCT grid im = Image.new("RGB", (257, 257), "red") im.paste(Image.new("RGB", (235, 235)), (11, 11)) @@ -138,7 +138,7 @@ def test_DCT_scaling_edges(): assert_image_similar(thumb, ref, 1.5) -def test_reducing_gap_values(): +def test_reducing_gap_values() -> None: im = hopper() im.thumbnail((18, 18), Image.Resampling.BICUBIC) @@ -155,7 +155,7 @@ def test_reducing_gap_values(): assert_image_similar(ref, im, 3.5) -def test_reducing_gap_for_DCT_scaling(): +def test_reducing_gap_for_DCT_scaling() -> None: with Image.open("Tests/images/hopper.jpg") as ref: # thumbnail should call draft with reducing_gap scale ref.draft(None, (18 * 3, 18 * 3)) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 0fe9fd1d5f5..1067dd563c1 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -10,7 +10,7 @@ class TestImageTransform: - def test_sanity(self): + def test_sanity(self) -> None: im = hopper() for transform in ( @@ -31,7 +31,7 @@ def test_sanity(self): ): assert_image_equal(im, im.transform(im.size, transform)) - def test_info(self): + def test_info(self) -> None: comment = b"File written by Adobe Photoshop\xa8 4.0" with Image.open("Tests/images/hopper.gif") as im: @@ -41,14 +41,14 @@ def test_info(self): new_im = im.transform((100, 100), transform) assert new_im.info["comment"] == comment - def test_palette(self): + def test_palette(self) -> None: with Image.open("Tests/images/hopper.gif") as im: transformed = im.transform( im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] ) assert im.palette.palette == transformed.palette.palette - def test_extent(self): + def test_extent(self) -> None: im = hopper("RGB") (w, h) = im.size transformed = im.transform( @@ -63,7 +63,7 @@ def test_extent(self): # undone -- precision? assert_image_similar(transformed, scaled, 23) - def test_quad(self): + def test_quad(self) -> None: # one simple quad transform, equivalent to scale & crop upper left quad im = hopper("RGB") (w, h) = im.size @@ -91,7 +91,7 @@ def test_quad(self): ("LA", (76, 0)), ), ) - def test_fill(self, mode, expected_pixel): + def test_fill(self, mode, expected_pixel) -> None: im = hopper(mode) (w, h) = im.size transformed = im.transform( @@ -103,7 +103,7 @@ def test_fill(self, mode, expected_pixel): ) assert transformed.getpixel((w - 1, h - 1)) == expected_pixel - def test_mesh(self): + def test_mesh(self) -> None: # this should be a checkerboard of halfsized hoppers in ul, lr im = hopper("RGBA") (w, h) = im.size @@ -142,7 +142,7 @@ def test_mesh(self): assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2))) assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h))) - def _test_alpha_premult(self, op): + def _test_alpha_premult(self, op) -> None: # create image with half white, half black, # with the black half transparent. # do op, @@ -158,13 +158,13 @@ def _test_alpha_premult(self, op): hist = im_background.histogram() assert 40 * 10 == hist[-1] - def test_alpha_premult_resize(self): + def test_alpha_premult_resize(self) -> None: def op(im, sz): return im.resize(sz, Image.Resampling.BILINEAR) self._test_alpha_premult(op) - def test_alpha_premult_transform(self): + def test_alpha_premult_transform(self) -> None: def op(im, sz): (w, h) = im.size return im.transform( @@ -173,7 +173,7 @@ def op(im, sz): self._test_alpha_premult(op) - def _test_nearest(self, op, mode): + def _test_nearest(self, op, mode) -> None: # create white image with half transparent, # do op, # the image should remain white with half transparent @@ -196,14 +196,14 @@ def _test_nearest(self, op, mode): ) @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_resize(self, mode): + def test_nearest_resize(self, mode) -> None: def op(im, sz): return im.resize(sz, Image.Resampling.NEAREST) self._test_nearest(op, mode) @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_transform(self, mode): + def test_nearest_transform(self, mode) -> None: def op(im, sz): (w, h) = im.size return im.transform( @@ -212,7 +212,7 @@ def op(im, sz): self._test_nearest(op, mode) - def test_blank_fill(self): + def test_blank_fill(self) -> None: # attempting to hit # https://github.com/python-pillow/Pillow/issues/254 reported # @@ -234,13 +234,13 @@ def test_blank_fill(self): self.test_mesh() - def test_missing_method_data(self): + def test_missing_method_data(self) -> None: with hopper() as im: with pytest.raises(ValueError): im.transform((100, 100), None) @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) - def test_unknown_resampling_filter(self, resample): + def test_unknown_resampling_filter(self, resample) -> None: with hopper() as im: (w, h) = im.size with pytest.raises(ValueError): @@ -263,7 +263,7 @@ def _test_image(self): (270, Image.Transpose.ROTATE_270), ), ) - def test_rotate(self, deg, transpose): + def test_rotate(self, deg, transpose) -> None: im = self._test_image() angle = -math.radians(deg) @@ -313,7 +313,7 @@ def test_rotate(self, deg, transpose): (Image.Resampling.BICUBIC, 1), ), ) - def test_resize(self, scale, epsilon_scale, resample, epsilon): + def test_resize(self, scale, epsilon_scale, resample, epsilon) -> None: im = self._test_image() size_up = int(round(im.width * scale)), int(round(im.height * scale)) @@ -342,7 +342,7 @@ def test_resize(self, scale, epsilon_scale, resample, epsilon): (Image.Resampling.BICUBIC, 1), ), ) - def test_translate(self, x, y, epsilon_scale, resample, epsilon): + def test_translate(self, x, y, epsilon_scale, resample, epsilon) -> None: im = self._test_image() size_up = int(round(im.width + x)), int(round(im.height + y)) diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 2f0614385aa..94f57e06690 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -15,7 +15,7 @@ GRAY = 128 -def test_sanity(): +def test_sanity() -> None: im = hopper("L") ImageChops.constant(im, 128) @@ -48,7 +48,7 @@ def test_sanity(): ImageChops.offset(im, 10, 20) -def test_add(): +def test_add() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -60,7 +60,7 @@ def test_add(): assert new.getpixel((50, 50)) == ORANGE -def test_add_scale_offset(): +def test_add_scale_offset() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -72,7 +72,7 @@ def test_add_scale_offset(): assert new.getpixel((50, 50)) == (202, 151, 100) -def test_add_clip(): +def test_add_clip() -> None: # Arrange im = hopper() @@ -83,7 +83,7 @@ def test_add_clip(): assert new.getpixel((50, 50)) == (255, 255, 254) -def test_add_modulo(): +def test_add_modulo() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -95,7 +95,7 @@ def test_add_modulo(): assert new.getpixel((50, 50)) == ORANGE -def test_add_modulo_no_clip(): +def test_add_modulo_no_clip() -> None: # Arrange im = hopper() @@ -106,7 +106,7 @@ def test_add_modulo_no_clip(): assert new.getpixel((50, 50)) == (224, 76, 254) -def test_blend(): +def test_blend() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -118,7 +118,7 @@ def test_blend(): assert new.getpixel((50, 50)) == BROWN -def test_constant(): +def test_constant() -> None: # Arrange im = Image.new("RGB", (20, 10)) @@ -131,7 +131,7 @@ def test_constant(): assert new.getpixel((19, 9)) == GRAY -def test_darker_image(): +def test_darker_image() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -142,7 +142,7 @@ def test_darker_image(): assert_image_equal(new, im2) -def test_darker_pixel(): +def test_darker_pixel() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -153,7 +153,7 @@ def test_darker_pixel(): assert new.getpixel((50, 50)) == (240, 166, 0) -def test_difference(): +def test_difference() -> None: # Arrange with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1: with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2: @@ -164,7 +164,7 @@ def test_difference(): assert new.getbbox() == (25, 25, 76, 76) -def test_difference_pixel(): +def test_difference_pixel() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: @@ -175,7 +175,7 @@ def test_difference_pixel(): assert new.getpixel((50, 50)) == (240, 166, 128) -def test_duplicate(): +def test_duplicate() -> None: # Arrange im = hopper() @@ -186,7 +186,7 @@ def test_duplicate(): assert_image_equal(new, im) -def test_invert(): +def test_invert() -> None: # Arrange with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: # Act @@ -198,7 +198,7 @@ def test_invert(): assert new.getpixel((50, 50)) == CYAN -def test_lighter_image(): +def test_lighter_image() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -209,7 +209,7 @@ def test_lighter_image(): assert_image_equal(new, im1) -def test_lighter_pixel(): +def test_lighter_pixel() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -220,7 +220,7 @@ def test_lighter_pixel(): assert new.getpixel((50, 50)) == (255, 255, 127) -def test_multiply_black(): +def test_multiply_black() -> None: """If you multiply an image with a solid black image, the result is black.""" # Arrange @@ -234,7 +234,7 @@ def test_multiply_black(): assert_image_equal(new, black) -def test_multiply_green(): +def test_multiply_green() -> None: # Arrange with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: green = Image.new("RGB", im.size, "green") @@ -248,7 +248,7 @@ def test_multiply_green(): assert new.getpixel((50, 50)) == BLACK -def test_multiply_white(): +def test_multiply_white() -> None: """If you multiply with a solid white image, the image is unaffected.""" # Arrange im1 = hopper() @@ -261,7 +261,7 @@ def test_multiply_white(): assert_image_equal(new, im1) -def test_offset(): +def test_offset() -> None: # Arrange xoffset = 45 yoffset = 20 @@ -278,7 +278,7 @@ def test_offset(): assert ImageChops.offset(im, xoffset) == ImageChops.offset(im, xoffset, xoffset) -def test_screen(): +def test_screen() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -290,7 +290,7 @@ def test_screen(): assert new.getpixel((50, 50)) == ORANGE -def test_subtract(): +def test_subtract() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -303,7 +303,7 @@ def test_subtract(): assert new.getpixel((50, 52)) == BLACK -def test_subtract_scale_offset(): +def test_subtract_scale_offset() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -315,7 +315,7 @@ def test_subtract_scale_offset(): assert new.getpixel((50, 50)) == (100, 202, 100) -def test_subtract_clip(): +def test_subtract_clip() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -326,7 +326,7 @@ def test_subtract_clip(): assert new.getpixel((50, 50)) == (0, 0, 127) -def test_subtract_modulo(): +def test_subtract_modulo() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -339,7 +339,7 @@ def test_subtract_modulo(): assert new.getpixel((50, 52)) == BLACK -def test_subtract_modulo_no_clip(): +def test_subtract_modulo_no_clip() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -350,7 +350,7 @@ def test_subtract_modulo_no_clip(): assert new.getpixel((50, 50)) == (241, 167, 127) -def test_soft_light(): +def test_soft_light() -> None: # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: @@ -362,7 +362,7 @@ def test_soft_light(): assert new.getpixel((15, 100)) == (1, 1, 3) -def test_hard_light(): +def test_hard_light() -> None: # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: @@ -374,7 +374,7 @@ def test_hard_light(): assert new.getpixel((15, 100)) == (1, 1, 2) -def test_overlay(): +def test_overlay() -> None: # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: @@ -386,7 +386,7 @@ def test_overlay(): assert new.getpixel((15, 100)) == (1, 1, 2) -def test_logical(): +def test_logical() -> None: def table(op, a, b): out = [] for x in (a, b): diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 03332699a1d..7f652715566 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -5,6 +5,7 @@ import re import shutil from io import BytesIO +from pathlib import Path import pytest @@ -32,7 +33,7 @@ HAVE_PROFILE = os.path.exists(SRGB) -def setup_module(): +def setup_module() -> None: try: from PIL import ImageCms @@ -42,12 +43,12 @@ def setup_module(): pytest.skip(str(v)) -def skip_missing(): +def skip_missing() -> None: if not HAVE_PROFILE: pytest.skip("SRGB profile not available") -def test_sanity(): +def test_sanity() -> None: # basic smoke test. # this mostly follows the cms_test outline. with pytest.warns(DeprecationWarning): @@ -91,7 +92,7 @@ def test_sanity(): hopper().point(t) -def test_flags(): +def test_flags() -> None: assert ImageCms.Flags.NONE == 0 assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE @@ -101,7 +102,7 @@ def test_flags(): assert ImageCms.Flags.GRIDPOINTS(511) == ImageCms.Flags.GRIDPOINTS(255) -def test_name(): +def test_name() -> None: skip_missing() # get profile information for file assert ( @@ -110,7 +111,7 @@ def test_name(): ) -def test_info(): +def test_info() -> None: skip_missing() assert ImageCms.getProfileInfo(SRGB).splitlines() == [ "sRGB IEC61966-2-1 black scaled", @@ -120,7 +121,7 @@ def test_info(): ] -def test_copyright(): +def test_copyright() -> None: skip_missing() assert ( ImageCms.getProfileCopyright(SRGB).strip() @@ -128,12 +129,12 @@ def test_copyright(): ) -def test_manufacturer(): +def test_manufacturer() -> None: skip_missing() assert ImageCms.getProfileManufacturer(SRGB).strip() == "" -def test_model(): +def test_model() -> None: skip_missing() assert ( ImageCms.getProfileModel(SRGB).strip() @@ -141,14 +142,14 @@ def test_model(): ) -def test_description(): +def test_description() -> None: skip_missing() assert ( ImageCms.getProfileDescription(SRGB).strip() == "sRGB IEC61966-2-1 black scaled" ) -def test_intent(): +def test_intent() -> None: skip_missing() assert ImageCms.getDefaultIntent(SRGB) == 0 support = ImageCms.isIntentSupported( @@ -157,7 +158,7 @@ def test_intent(): assert support == 1 -def test_profile_object(): +def test_profile_object() -> None: # same, using profile object p = ImageCms.createProfile("sRGB") # assert ImageCms.getProfileName(p).strip() == "sRGB built-in - (lcms internal)" @@ -170,7 +171,7 @@ def test_profile_object(): assert support == 1 -def test_extensions(): +def test_extensions() -> None: # extensions with Image.open("Tests/images/rgb.jpg") as i: @@ -181,7 +182,7 @@ def test_extensions(): ) -def test_exceptions(): +def test_exceptions() -> None: # Test mode mismatch psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") @@ -207,17 +208,17 @@ def test_exceptions(): ImageCms.isIntentSupported(SRGB, None, None) -def test_display_profile(): +def test_display_profile() -> None: # try fetching the profile for the current display device ImageCms.get_display_profile() -def test_lab_color_profile(): +def test_lab_color_profile() -> None: ImageCms.createProfile("LAB", 5000) ImageCms.createProfile("LAB", 6500) -def test_unsupported_color_space(): +def test_unsupported_color_space() -> None: with pytest.raises( ImageCms.PyCMSError, match=re.escape( @@ -227,7 +228,7 @@ def test_unsupported_color_space(): ImageCms.createProfile("unsupported") -def test_invalid_color_temperature(): +def test_invalid_color_temperature() -> None: with pytest.raises( ImageCms.PyCMSError, match='Color temperature must be numeric, "invalid" not valid', @@ -236,7 +237,7 @@ def test_invalid_color_temperature(): @pytest.mark.parametrize("flag", ("my string", -1)) -def test_invalid_flag(flag): +def test_invalid_flag(flag) -> None: with hopper() as im: with pytest.raises( ImageCms.PyCMSError, match="flags must be an integer between 0 and " @@ -244,7 +245,7 @@ def test_invalid_flag(flag): ImageCms.profileToProfile(im, "foo", "bar", flags=flag) -def test_simple_lab(): +def test_simple_lab() -> None: i = Image.new("RGB", (10, 10), (128, 128, 128)) psRGB = ImageCms.createProfile("sRGB") @@ -268,7 +269,7 @@ def test_simple_lab(): assert list(b_data) == [128] * 100 -def test_lab_color(): +def test_lab_color() -> None: psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") @@ -283,7 +284,7 @@ def test_lab_color(): assert_image_similar_tofile(i, "Tests/images/hopper.Lab.tif", 3.5) -def test_lab_srgb(): +def test_lab_srgb() -> None: psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") @@ -300,7 +301,7 @@ def test_lab_srgb(): assert "sRGB" in ImageCms.getProfileDescription(profile) -def test_lab_roundtrip(): +def test_lab_roundtrip() -> None: # check to see if we're at least internally consistent. psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") @@ -317,7 +318,7 @@ def test_lab_roundtrip(): assert_image_similar(hopper(), out, 2) -def test_profile_tobytes(): +def test_profile_tobytes() -> None: with Image.open("Tests/images/rgb.jpg") as i: p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) @@ -329,12 +330,12 @@ def test_profile_tobytes(): assert ImageCms.getProfileDescription(p) == ImageCms.getProfileDescription(p2) -def test_extended_information(): +def test_extended_information() -> None: skip_missing() o = ImageCms.getOpenProfile(SRGB) p = o.profile - def assert_truncated_tuple_equal(tup1, tup2, digits=10): + def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None: # Helper function to reduce precision of tuples of floats # recursively and then check equality. power = 10**digits @@ -476,7 +477,7 @@ def truncate_tuple(tuple_or_float): assert p.xcolor_space == "RGB " -def test_non_ascii_path(tmp_path): +def test_non_ascii_path(tmp_path: Path) -> None: skip_missing() tempfile = str(tmp_path / ("temp_" + chr(128) + ".icc")) try: @@ -489,7 +490,7 @@ def test_non_ascii_path(tmp_path): assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB" -def test_profile_typesafety(): +def test_profile_typesafety() -> None: """Profile init type safety prepatch, these would segfault, postpatch they should emit a typeerror @@ -501,7 +502,7 @@ def test_profile_typesafety(): ImageCms.ImageCmsProfile(1).tobytes() -def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel): +def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None: def create_test_image(): # set up test image with something interesting in the tested aux channel. # fmt: off @@ -556,31 +557,31 @@ def create_test_image(): assert_image_equal(source_image_aux, result_image_aux) -def test_preserve_auxiliary_channels_rgba(): +def test_preserve_auxiliary_channels_rgba() -> None: assert_aux_channel_preserved( mode="RGBA", transform_in_place=False, preserved_channel="A" ) -def test_preserve_auxiliary_channels_rgba_in_place(): +def test_preserve_auxiliary_channels_rgba_in_place() -> None: assert_aux_channel_preserved( mode="RGBA", transform_in_place=True, preserved_channel="A" ) -def test_preserve_auxiliary_channels_rgbx(): +def test_preserve_auxiliary_channels_rgbx() -> None: assert_aux_channel_preserved( mode="RGBX", transform_in_place=False, preserved_channel="X" ) -def test_preserve_auxiliary_channels_rgbx_in_place(): +def test_preserve_auxiliary_channels_rgbx_in_place() -> None: assert_aux_channel_preserved( mode="RGBX", transform_in_place=True, preserved_channel="X" ) -def test_auxiliary_channels_isolated(): +def test_auxiliary_channels_isolated() -> None: # test data in aux channels does not affect non-aux channels aux_channel_formats = [ # format, profile, color-only format, source test image @@ -630,7 +631,7 @@ def test_auxiliary_channels_isolated(): @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) -def test_rgb_lab(mode): +def test_rgb_lab(mode) -> None: im = Image.new(mode, (1, 1)) converted_im = im.convert("LAB") assert converted_im.getpixel((0, 0)) == (0, 128, 128) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index b602172b642..6eea7886dd8 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -5,7 +5,7 @@ from PIL import Image, ImageColor -def test_hash(): +def test_hash() -> None: # short 3 components assert (255, 0, 0) == ImageColor.getrgb("#f00") assert (0, 255, 0) == ImageColor.getrgb("#0f0") @@ -57,7 +57,7 @@ def test_hash(): ImageColor.getrgb("#f00000 ") -def test_colormap(): +def test_colormap() -> None: assert (0, 0, 0) == ImageColor.getrgb("black") assert (255, 255, 255) == ImageColor.getrgb("white") assert (255, 255, 255) == ImageColor.getrgb("WHITE") @@ -66,7 +66,7 @@ def test_colormap(): ImageColor.getrgb("black ") -def test_functions(): +def test_functions() -> None: # rgb numbers assert (255, 0, 0) == ImageColor.getrgb("rgb(255,0,0)") assert (0, 255, 0) == ImageColor.getrgb("rgb(0,255,0)") @@ -160,7 +160,7 @@ def test_functions(): # look for rounding errors (based on code by Tim Hatch) -def test_rounding_errors(): +def test_rounding_errors() -> None: for color in ImageColor.colormap: expected = Image.new("RGB", (1, 1), color).convert("L").getpixel((0, 0)) actual = ImageColor.getcolor(color, "L") @@ -195,11 +195,11 @@ def test_rounding_errors(): Image.new("LA", (1, 1), "white") -def test_color_hsv(): +def test_color_hsv() -> None: assert (170, 255, 255) == ImageColor.getcolor("hsv(240, 100%, 100%)", "HSV") -def test_color_too_long(): +def test_color_too_long() -> None: # Arrange color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)" diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 69aab48912b..86d25b1ebc7 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -47,7 +47,7 @@ ) -def test_sanity(): +def test_sanity() -> None: im = hopper("RGB").copy() draw = ImageDraw.ImageDraw(im) @@ -59,13 +59,13 @@ def test_sanity(): draw.rectangle(list(range(4))) -def test_valueerror(): +def test_valueerror() -> None: with Image.open("Tests/images/chi.gif") as im: draw = ImageDraw.Draw(im) draw.line((0, 0), fill=(0, 0, 0)) -def test_mode_mismatch(): +def test_mode_mismatch() -> None: im = hopper("RGB").copy() with pytest.raises(ValueError): @@ -74,7 +74,7 @@ def test_mode_mismatch(): @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) -def test_arc(bbox, start, end): +def test_arc(bbox, start, end) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -87,7 +87,7 @@ def test_arc(bbox, start, end): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_end_le_start(bbox): +def test_arc_end_le_start(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -102,7 +102,7 @@ def test_arc_end_le_start(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_no_loops(bbox): +def test_arc_no_loops(bbox) -> None: # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -118,7 +118,7 @@ def test_arc_no_loops(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width(bbox): +def test_arc_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -131,7 +131,7 @@ def test_arc_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_pieslice_large(bbox): +def test_arc_width_pieslice_large(bbox) -> None: # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) @@ -145,7 +145,7 @@ def test_arc_width_pieslice_large(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_fill(bbox): +def test_arc_width_fill(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -158,7 +158,7 @@ def test_arc_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_non_whole_angle(bbox): +def test_arc_width_non_whole_angle(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -171,7 +171,7 @@ def test_arc_width_non_whole_angle(bbox): assert_image_similar_tofile(im, expected, 1) -def test_arc_high(): +def test_arc_high() -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -184,7 +184,7 @@ def test_arc_high(): assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_high.png") -def test_bitmap(): +def test_bitmap() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -200,7 +200,7 @@ def test_bitmap(): @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_chord(mode, bbox): +def test_chord(mode, bbox) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -214,7 +214,7 @@ def test_chord(mode, bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width(bbox): +def test_chord_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -227,7 +227,7 @@ def test_chord_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width_fill(bbox): +def test_chord_width_fill(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -240,7 +240,7 @@ def test_chord_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_chord_zero_width(bbox): +def test_chord_zero_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -252,7 +252,7 @@ def test_chord_zero_width(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png") -def test_chord_too_fat(): +def test_chord_too_fat() -> None: # Arrange im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) @@ -266,7 +266,7 @@ def test_chord_too_fat(): @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(mode, bbox): +def test_ellipse(mode, bbox) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -280,7 +280,7 @@ def test_ellipse(mode, bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_translucent(bbox): +def test_ellipse_translucent(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -293,7 +293,7 @@ def test_ellipse_translucent(bbox): assert_image_similar_tofile(im, expected, 1) -def test_ellipse_edge(): +def test_ellipse_edge() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -305,7 +305,7 @@ def test_ellipse_edge(): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) -def test_ellipse_symmetric(): +def test_ellipse_symmetric() -> None: for width, bbox in ( (100, (24, 24, 75, 75)), (101, (25, 25, 75, 75)), @@ -317,7 +317,7 @@ def test_ellipse_symmetric(): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width(bbox): +def test_ellipse_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -329,7 +329,7 @@ def test_ellipse_width(bbox): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) -def test_ellipse_width_large(): +def test_ellipse_width_large() -> None: # Arrange im = Image.new("RGB", (500, 500)) draw = ImageDraw.Draw(im) @@ -342,7 +342,7 @@ def test_ellipse_width_large(): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width_fill(bbox): +def test_ellipse_width_fill(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -355,7 +355,7 @@ def test_ellipse_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_zero_width(bbox): +def test_ellipse_zero_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -394,13 +394,13 @@ def ellipse_various_sizes_helper(filled): return im -def test_ellipse_various_sizes(): +def test_ellipse_various_sizes() -> None: im = ellipse_various_sizes_helper(False) assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_various_sizes.png") -def test_ellipse_various_sizes_filled(): +def test_ellipse_various_sizes_filled() -> None: im = ellipse_various_sizes_helper(True) assert_image_equal_tofile( @@ -409,7 +409,7 @@ def test_ellipse_various_sizes_filled(): @pytest.mark.parametrize("points", POINTS) -def test_line(points): +def test_line(points) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -421,7 +421,7 @@ def test_line(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -def test_shape1(): +def test_shape1() -> None: # Arrange im = Image.new("RGB", (100, 100), "white") draw = ImageDraw.Draw(im) @@ -442,7 +442,7 @@ def test_shape1(): assert_image_equal_tofile(im, "Tests/images/imagedraw_shape1.png") -def test_shape2(): +def test_shape2() -> None: # Arrange im = Image.new("RGB", (100, 100), "white") draw = ImageDraw.Draw(im) @@ -463,7 +463,7 @@ def test_shape2(): assert_image_equal_tofile(im, "Tests/images/imagedraw_shape2.png") -def test_transform(): +def test_transform() -> None: # Arrange im = Image.new("RGB", (100, 100), "white") expected = im.copy() @@ -482,7 +482,7 @@ def test_transform(): @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) -def test_pieslice(bbox, start, end): +def test_pieslice(bbox, start, end) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -495,7 +495,7 @@ def test_pieslice(bbox, start, end): @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width(bbox): +def test_pieslice_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -508,7 +508,7 @@ def test_pieslice_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width_fill(bbox): +def test_pieslice_width_fill(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -522,7 +522,7 @@ def test_pieslice_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_zero_width(bbox): +def test_pieslice_zero_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -534,7 +534,7 @@ def test_pieslice_zero_width(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png") -def test_pieslice_wide(): +def test_pieslice_wide() -> None: # Arrange im = Image.new("RGB", (200, 100)) draw = ImageDraw.Draw(im) @@ -546,7 +546,7 @@ def test_pieslice_wide(): assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_wide.png") -def test_pieslice_no_spikes(): +def test_pieslice_no_spikes() -> None: im = Image.new("RGB", (161, 161), "white") draw = ImageDraw.Draw(im) cxs = ( @@ -577,7 +577,7 @@ def test_pieslice_no_spikes(): @pytest.mark.parametrize("points", POINTS) -def test_point(points): +def test_point(points) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -589,7 +589,7 @@ def test_point(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") -def test_point_I16(): +def test_point_I16() -> None: # Arrange im = Image.new("I;16", (1, 1)) draw = ImageDraw.Draw(im) @@ -602,7 +602,7 @@ def test_point_I16(): @pytest.mark.parametrize("points", POINTS) -def test_polygon(points): +def test_polygon(points) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -616,7 +616,7 @@ def test_polygon(points): @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("kite_points", KITE_POINTS) -def test_polygon_kite(mode, kite_points): +def test_polygon_kite(mode, kite_points) -> None: # Test drawing lines of different gradients (dx>dy, dy>dx) and # vertical (dx==0) and horizontal (dy==0) lines # Arrange @@ -631,7 +631,7 @@ def test_polygon_kite(mode, kite_points): assert_image_equal_tofile(im, expected) -def test_polygon_1px_high(): +def test_polygon_1px_high() -> None: # Test drawing a 1px high polygon # Arrange im = Image.new("RGB", (3, 3)) @@ -645,7 +645,7 @@ def test_polygon_1px_high(): assert_image_equal_tofile(im, expected) -def test_polygon_1px_high_translucent(): +def test_polygon_1px_high_translucent() -> None: # Test drawing a translucent 1px high polygon # Arrange im = Image.new("RGB", (4, 3)) @@ -659,7 +659,7 @@ def test_polygon_1px_high_translucent(): assert_image_equal_tofile(im, expected) -def test_polygon_translucent(): +def test_polygon_translucent() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -673,7 +673,7 @@ def test_polygon_translucent(): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox): +def test_rectangle(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -685,7 +685,7 @@ def test_rectangle(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") -def test_big_rectangle(): +def test_big_rectangle() -> None: # Test drawing a rectangle bigger than the image # Arrange im = Image.new("RGB", (W, H)) @@ -700,7 +700,7 @@ def test_big_rectangle(): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width(bbox): +def test_rectangle_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -714,7 +714,7 @@ def test_rectangle_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width_fill(bbox): +def test_rectangle_width_fill(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -728,7 +728,7 @@ def test_rectangle_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_zero_width(bbox): +def test_rectangle_zero_width(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -741,7 +741,7 @@ def test_rectangle_zero_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16(bbox): +def test_rectangle_I16(bbox) -> None: # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) @@ -754,7 +754,7 @@ def test_rectangle_I16(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_translucent_outline(bbox): +def test_rectangle_translucent_outline(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -772,7 +772,7 @@ def test_rectangle_translucent_outline(bbox): "xy", [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], ) -def test_rounded_rectangle(xy): +def test_rounded_rectangle(xy) -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -788,7 +788,9 @@ def test_rounded_rectangle(xy): @pytest.mark.parametrize("top_right", (True, False)) @pytest.mark.parametrize("bottom_right", (True, False)) @pytest.mark.parametrize("bottom_left", (True, False)) -def test_rounded_rectangle_corners(top_left, top_right, bottom_right, bottom_left): +def test_rounded_rectangle_corners( + top_left, top_right, bottom_right, bottom_left +) -> None: corners = (top_left, top_right, bottom_right, bottom_left) # Arrange @@ -822,7 +824,7 @@ def test_rounded_rectangle_corners(top_left, top_right, bottom_right, bottom_lef ((10, 20, 190, 181), 85, "height"), ], ) -def test_rounded_rectangle_non_integer_radius(xy, radius, type): +def test_rounded_rectangle_non_integer_radius(xy, radius, type) -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -838,7 +840,7 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type): @pytest.mark.parametrize("bbox", BBOX) -def test_rounded_rectangle_zero_radius(bbox): +def test_rounded_rectangle_zero_radius(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -860,7 +862,7 @@ def test_rounded_rectangle_zero_radius(bbox): ((20, 20, 80, 80), "both"), ], ) -def test_rounded_rectangle_translucent(xy, suffix): +def test_rounded_rectangle_translucent(xy, suffix) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -877,7 +879,7 @@ def test_rounded_rectangle_translucent(xy, suffix): @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill(bbox): +def test_floodfill(bbox) -> None: red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: @@ -910,7 +912,7 @@ def test_floodfill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_border(bbox): +def test_floodfill_border(bbox) -> None: # floodfill() is experimental # Arrange @@ -932,7 +934,7 @@ def test_floodfill_border(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_thresh(bbox): +def test_floodfill_thresh(bbox) -> None: # floodfill() is experimental # Arrange @@ -948,7 +950,7 @@ def test_floodfill_thresh(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") -def test_floodfill_not_negative(): +def test_floodfill_not_negative() -> None: # floodfill() is experimental # Test that floodfill does not extend into negative coordinates @@ -976,7 +978,7 @@ def create_base_image_draw( return img, ImageDraw.Draw(img) -def test_square(): +def test_square() -> None: expected = os.path.join(IMAGES_PATH, "square.png") img, draw = create_base_image_draw((10, 10)) draw.polygon([(2, 2), (2, 7), (7, 7), (7, 2)], BLACK) @@ -989,7 +991,7 @@ def test_square(): assert_image_equal_tofile(img, expected, "square as normal rectangle failed") -def test_triangle_right(): +def test_triangle_right() -> None: img, draw = create_base_image_draw((20, 20)) draw.polygon([(3, 5), (17, 5), (10, 12)], BLACK) assert_image_equal_tofile( @@ -1001,7 +1003,7 @@ def test_triangle_right(): "fill, suffix", ((BLACK, "width"), (None, "width_no_fill")), ) -def test_triangle_right_width(fill, suffix): +def test_triangle_right_width(fill, suffix) -> None: img, draw = create_base_image_draw((100, 100)) draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5) assert_image_equal_tofile( @@ -1009,7 +1011,7 @@ def test_triangle_right_width(fill, suffix): ) -def test_line_horizontal(): +def test_line_horizontal() -> None: img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 5), BLACK, 2) assert_image_equal_tofile( @@ -1047,7 +1049,7 @@ def test_line_horizontal(): ) -def test_line_h_s1_w2(): +def test_line_h_s1_w2() -> None: pytest.skip("failing") img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 6), BLACK, 2) @@ -1058,7 +1060,7 @@ def test_line_h_s1_w2(): ) -def test_line_vertical(): +def test_line_vertical() -> None: img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 5, 14), BLACK, 2) assert_image_equal_tofile( @@ -1104,7 +1106,7 @@ def test_line_vertical(): ) -def test_line_oblique_45(): +def test_line_oblique_45() -> None: expected = os.path.join(IMAGES_PATH, "line_oblique_45_w3px_a.png") img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 14), BLACK, 3) @@ -1126,7 +1128,7 @@ def test_line_oblique_45(): ) -def test_wide_line_dot(): +def test_wide_line_dot() -> None: # Test drawing a wide "line" from one point to another just draws a single point # Arrange im = Image.new("RGB", (W, H)) @@ -1139,7 +1141,7 @@ def test_wide_line_dot(): assert_image_similar_tofile(im, "Tests/images/imagedraw_wide_line_dot.png", 1) -def test_wide_line_larger_than_int(): +def test_wide_line_larger_than_int() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -1233,7 +1235,7 @@ def test_wide_line_larger_than_int(): ], ], ) -def test_line_joint(xy): +def test_line_joint(xy) -> None: im = Image.new("RGB", (500, 325)) draw = ImageDraw.Draw(im) @@ -1244,7 +1246,7 @@ def test_line_joint(xy): assert_image_similar_tofile(im, "Tests/images/imagedraw_line_joint_curve.png", 3) -def test_textsize_empty_string(): +def test_textsize_empty_string() -> None: # https://github.com/python-pillow/Pillow/issues/2783 # Arrange im = Image.new("RGB", (W, H)) @@ -1260,7 +1262,7 @@ def test_textsize_empty_string(): @skip_unless_feature("freetype2") -def test_textbbox_stroke(): +def test_textbbox_stroke() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -1274,7 +1276,7 @@ def test_textbbox_stroke(): @skip_unless_feature("freetype2") -def test_stroke(): +def test_stroke() -> None: for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): # Arrange im = Image.new("RGB", (120, 130)) @@ -1291,7 +1293,7 @@ def test_stroke(): @skip_unless_feature("freetype2") -def test_stroke_descender(): +def test_stroke_descender() -> None: # Arrange im = Image.new("RGB", (120, 130)) draw = ImageDraw.Draw(im) @@ -1305,7 +1307,7 @@ def test_stroke_descender(): @skip_unless_feature("freetype2") -def test_split_word(): +def test_split_word() -> None: # Arrange im = Image.new("RGB", (230, 55)) expected = im.copy() @@ -1326,7 +1328,7 @@ def test_split_word(): @skip_unless_feature("freetype2") -def test_stroke_multiline(): +def test_stroke_multiline() -> None: # Arrange im = Image.new("RGB", (100, 250)) draw = ImageDraw.Draw(im) @@ -1342,7 +1344,7 @@ def test_stroke_multiline(): @skip_unless_feature("freetype2") -def test_setting_default_font(): +def test_setting_default_font() -> None: # Arrange im = Image.new("RGB", (100, 250)) draw = ImageDraw.Draw(im) @@ -1359,7 +1361,7 @@ def test_setting_default_font(): assert isinstance(draw.getfont(), ImageFont.load_default().__class__) -def test_default_font_size(): +def test_default_font_size() -> None: freetype_support = features.check_module("freetype2") text = "Default font at a specific size." @@ -1386,7 +1388,7 @@ def test_default_font_size(): @pytest.mark.parametrize("bbox", BBOX) -def test_same_color_outline(bbox): +def test_same_color_outline(bbox) -> None: # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 @@ -1432,7 +1434,7 @@ def test_same_color_outline(bbox): (3, "triangle_width", {"width": 5, "outline": "yellow"}), ], ) -def test_draw_regular_polygon(n_sides, polygon_name, args): +def test_draw_regular_polygon(n_sides, polygon_name, args) -> None: im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) filename = f"Tests/images/imagedraw_{polygon_name}.png" draw = ImageDraw.Draw(im) @@ -1469,7 +1471,7 @@ def test_draw_regular_polygon(n_sides, polygon_name, args): ), ], ) -def test_compute_regular_polygon_vertices(n_sides, expected_vertices): +def test_compute_regular_polygon_vertices(n_sides, expected_vertices) -> None: bounding_circle = (W // 2, H // 2, 25) vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0) assert vertices == expected_vertices @@ -1521,13 +1523,13 @@ def test_compute_regular_polygon_vertices(n_sides, expected_vertices): ) def test_compute_regular_polygon_vertices_input_error_handling( n_sides, bounding_circle, rotation, expected_error, error_message -): +) -> None: with pytest.raises(expected_error) as e: ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) assert str(e.value) == error_message -def test_continuous_horizontal_edges_polygon(): +def test_continuous_horizontal_edges_polygon() -> None: xy = [ (2, 6), (6, 6), @@ -1546,7 +1548,7 @@ def test_continuous_horizontal_edges_polygon(): ) -def test_discontiguous_corners_polygon(): +def test_discontiguous_corners_polygon() -> None: img, draw = create_base_image_draw((84, 68)) draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK) @@ -1558,7 +1560,7 @@ def test_discontiguous_corners_polygon(): assert_image_similar_tofile(img, expected, 1) -def test_polygon2(): +def test_polygon2() -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") @@ -1567,7 +1569,7 @@ def test_polygon2(): @pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) -def test_incorrectly_ordered_coordinates(xy): +def test_incorrectly_ordered_coordinates(xy) -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) with pytest.raises(ValueError): diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 004c2d768fd..07a25b84b4e 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -43,7 +43,7 @@ FONT_PATH = "Tests/fonts/FreeMono.ttf" -def test_sanity(): +def test_sanity() -> None: im = hopper("RGB").copy() draw = ImageDraw2.Draw(im) @@ -56,7 +56,7 @@ def test_sanity(): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(bbox): +def test_ellipse(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -70,7 +70,7 @@ def test_ellipse(bbox): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_RGB.png", 1) -def test_ellipse_edge(): +def test_ellipse_edge() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -84,7 +84,7 @@ def test_ellipse_edge(): @pytest.mark.parametrize("points", POINTS) -def test_line(points): +def test_line(points) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -98,7 +98,7 @@ def test_line(points): @pytest.mark.parametrize("points", POINTS) -def test_line_pen_as_brush(points): +def test_line_pen_as_brush(points) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -114,7 +114,7 @@ def test_line_pen_as_brush(points): @pytest.mark.parametrize("points", POINTS) -def test_polygon(points): +def test_polygon(points) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -129,7 +129,7 @@ def test_polygon(points): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox): +def test_rectangle(bbox) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -143,7 +143,7 @@ def test_rectangle(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") -def test_big_rectangle(): +def test_big_rectangle() -> None: # Test drawing a rectangle bigger than the image # Arrange im = Image.new("RGB", (W, H)) @@ -160,7 +160,7 @@ def test_big_rectangle(): @skip_unless_feature("freetype2") -def test_text(): +def test_text() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -175,7 +175,7 @@ def test_text(): @skip_unless_feature("freetype2") -def test_textbbox(): +def test_textbbox() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -190,7 +190,7 @@ def test_textbbox(): @skip_unless_feature("freetype2") -def test_textsize_empty_string(): +def test_textsize_empty_string() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -206,7 +206,7 @@ def test_textsize_empty_string(): @skip_unless_feature("freetype2") -def test_flush(): +def test_flush() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index e3d8a7ab251..9ce9cda8267 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -7,7 +7,7 @@ from .helper import assert_image_equal, hopper -def test_sanity(): +def test_sanity() -> None: # FIXME: assert_image # Implicit asserts no exception: ImageEnhance.Color(hopper()).enhance(0.5) @@ -16,7 +16,7 @@ def test_sanity(): ImageEnhance.Sharpness(hopper()).enhance(0.5) -def test_crash(): +def test_crash() -> None: # crashes on small images im = Image.new("RGB", (1, 1)) ImageEnhance.Sharpness(im).enhance(0.5) @@ -34,7 +34,7 @@ def _half_transparent_image(): return im -def _check_alpha(im, original, op, amount): +def _check_alpha(im, original, op, amount) -> None: assert im.getbands() == original.getbands() assert_image_equal( im.getchannel("A"), @@ -44,7 +44,7 @@ def _check_alpha(im, original, op, amount): @pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) -def test_alpha(op): +def test_alpha(op) -> None: # Issue https://github.com/python-pillow/Pillow/issues/899 # Is alpha preserved through image enhancement? diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 99731f35208..49140978168 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -30,7 +30,7 @@ class TestImageFile: - def test_parser(self): + def test_parser(self) -> None: def roundtrip(format): im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): @@ -84,7 +84,7 @@ def roundtrip(format): with pytest.raises(OSError): roundtrip("PDF") - def test_ico(self): + def test_ico(self) -> None: with open("Tests/images/python.ico", "rb") as f: data = f.read() with ImageFile.Parser() as p: @@ -93,7 +93,7 @@ def test_ico(self): @skip_unless_feature("webp") @skip_unless_feature("webp_anim") - def test_incremental_webp(self): + def test_incremental_webp(self) -> None: with ImageFile.Parser() as p: with open("Tests/images/hopper.webp", "rb") as f: p.feed(f.read(1024)) @@ -105,7 +105,7 @@ def test_incremental_webp(self): assert (128, 128) == p.image.size @skip_unless_feature("zlib") - def test_safeblock(self): + def test_safeblock(self) -> None: im1 = hopper() try: @@ -116,17 +116,17 @@ def test_safeblock(self): assert_image_equal(im1, im2) - def test_raise_oserror(self): + def test_raise_oserror(self) -> None: with pytest.warns(DeprecationWarning): with pytest.raises(OSError): ImageFile.raise_oserror(1) - def test_raise_typeerror(self): + def test_raise_typeerror(self) -> None: with pytest.raises(TypeError): parser = ImageFile.Parser() parser.feed(1) - def test_negative_stride(self): + def test_negative_stride(self) -> None: with open("Tests/images/raw_negative_stride.bin", "rb") as f: input = f.read() p = ImageFile.Parser() @@ -134,11 +134,11 @@ def test_negative_stride(self): with pytest.raises(OSError): p.close() - def test_no_format(self): + def test_no_format(self) -> None: buf = BytesIO(b"\x00" * 255) class DummyImageFile(ImageFile.ImageFile): - def _open(self): + def _open(self) -> None: self._mode = "RGB" self._size = (1, 1) @@ -146,12 +146,12 @@ def _open(self): assert im.format is None assert im.get_format_mimetype() is None - def test_oserror(self): + def test_oserror(self) -> None: im = Image.new("RGB", (1, 1)) with pytest.raises(OSError): im.save(BytesIO(), "JPEG2000", num_resolutions=2) - def test_truncated(self): + def test_truncated(self) -> None: b = BytesIO( b"BM000000000000" # head_data + _binary.o32le( @@ -166,7 +166,7 @@ def test_truncated(self): assert str(e.value) == "Truncated File Read" @skip_unless_feature("zlib") - def test_truncated_with_errors(self): + def test_truncated_with_errors(self) -> None: with Image.open("Tests/images/truncated_image.png") as im: with pytest.raises(OSError): im.load() @@ -176,7 +176,7 @@ def test_truncated_with_errors(self): im.load() @skip_unless_feature("zlib") - def test_truncated_without_errors(self): + def test_truncated_without_errors(self) -> None: with Image.open("Tests/images/truncated_image.png") as im: ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -185,13 +185,13 @@ def test_truncated_without_errors(self): ImageFile.LOAD_TRUNCATED_IMAGES = False @skip_unless_feature("zlib") - def test_broken_datastream_with_errors(self): + def test_broken_datastream_with_errors(self) -> None: with Image.open("Tests/images/broken_data_stream.png") as im: with pytest.raises(OSError): im.load() @skip_unless_feature("zlib") - def test_broken_datastream_without_errors(self): + def test_broken_datastream_without_errors(self) -> None: with Image.open("Tests/images/broken_data_stream.png") as im: ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -210,7 +210,7 @@ class MockPyEncoder(ImageFile.PyEncoder): def encode(self, buffer): return 1, 1, b"" - def cleanup(self): + def cleanup(self) -> None: self.cleanup_called = True @@ -218,7 +218,7 @@ def cleanup(self): class MockImageFile(ImageFile.ImageFile): - def _open(self): + def _open(self) -> None: self.rawmode = "RGBA" self._mode = "RGBA" self._size = (200, 200) @@ -227,7 +227,7 @@ def _open(self): class CodecsTest: @classmethod - def setup_class(cls): + def setup_class(cls) -> None: cls.decoder = MockPyDecoder(None) cls.encoder = MockPyEncoder(None) @@ -244,7 +244,7 @@ def encoder_closure(mode, *args): class TestPyDecoder(CodecsTest): - def test_setimage(self): + def test_setimage(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -259,7 +259,7 @@ def test_setimage(self): with pytest.raises(ValueError): self.decoder.set_as_raw(b"\x00") - def test_extents_none(self): + def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -272,7 +272,7 @@ def test_extents_none(self): assert self.decoder.state.xsize == 200 assert self.decoder.state.ysize == 200 - def test_negsize(self): + def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -285,7 +285,7 @@ def test_negsize(self): with pytest.raises(ValueError): im.load() - def test_oversize(self): + def test_oversize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -298,14 +298,14 @@ def test_oversize(self): with pytest.raises(ValueError): im.load() - def test_decode(self): + def test_decode(self) -> None: decoder = ImageFile.PyDecoder(None) with pytest.raises(NotImplementedError): decoder.decode(None) class TestPyEncoder(CodecsTest): - def test_setimage(self): + def test_setimage(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -320,7 +320,7 @@ def test_setimage(self): assert self.encoder.state.xsize == xsize assert self.encoder.state.ysize == ysize - def test_extents_none(self): + def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -334,7 +334,7 @@ def test_extents_none(self): assert self.encoder.state.xsize == 200 assert self.encoder.state.ysize == 200 - def test_negsize(self): + def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -352,7 +352,7 @@ def test_negsize(self): im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] ) - def test_oversize(self): + def test_oversize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -372,7 +372,7 @@ def test_oversize(self): [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], ) - def test_encode(self): + def test_encode(self) -> None: encoder = ImageFile.PyEncoder(None) with pytest.raises(NotImplementedError): encoder.encode(None) @@ -388,6 +388,6 @@ def test_encode(self): with pytest.raises(NotImplementedError): encoder.encode_to_file(None, None) - def test_zero_height(self): + def test_zero_height(self) -> None: with pytest.raises(UnidentifiedImageError): Image.open("Tests/images/zero_height.j2k") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index d2c87d42aaa..909026dc8c6 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -31,7 +31,7 @@ pytestmark = skip_unless_feature("freetype2") -def test_sanity(): +def test_sanity() -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2")) @@ -51,7 +51,7 @@ def font(layout_engine): return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine) -def test_font_properties(font): +def test_font_properties(font) -> None: assert font.path == FONT_PATH assert font.size == FONT_SIZE @@ -80,11 +80,11 @@ def _render(font, layout_engine): @pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH))) -def test_font_with_name(layout_engine, font): +def test_font_with_name(layout_engine, font) -> None: _render(font, layout_engine) -def test_font_with_filelike(layout_engine): +def test_font_with_filelike(layout_engine) -> None: def _font_as_bytes(): with open(FONT_PATH, "rb") as f: font_bytes = BytesIO(f.read()) @@ -102,12 +102,12 @@ def _font_as_bytes(): # _render(shared_bytes) -def test_font_with_open_file(layout_engine): +def test_font_with_open_file(layout_engine) -> None: with open(FONT_PATH, "rb") as f: _render(f, layout_engine) -def test_render_equal(layout_engine): +def test_render_equal(layout_engine) -> None: img_path = _render(FONT_PATH, layout_engine) with open(FONT_PATH, "rb") as f: font_filelike = BytesIO(f.read()) @@ -116,7 +116,7 @@ def test_render_equal(layout_engine): assert_image_equal(img_path, img_filelike) -def test_non_ascii_path(tmp_path, layout_engine): +def test_non_ascii_path(tmp_path: Path, layout_engine) -> None: tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) try: shutil.copy(FONT_PATH, tempfile) @@ -126,7 +126,7 @@ def test_non_ascii_path(tmp_path, layout_engine): ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine) -def test_transparent_background(font): +def test_transparent_background(font) -> None: im = Image.new(mode="RGBA", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -140,7 +140,7 @@ def test_transparent_background(font): assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_I16(font): +def test_I16(font) -> None: im = Image.new(mode="I;16", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -153,7 +153,7 @@ def test_I16(font): assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_textbbox_equal(font): +def test_textbbox_equal(font) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -182,7 +182,7 @@ def test_textbbox_equal(font): ) def test_getlength( text, mode, fontname, size, layout_engine, length_basic, length_raqm -): +) -> None: f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) im = Image.new(mode, (1, 1), 0) @@ -197,7 +197,7 @@ def test_getlength( assert length == length_raqm -def test_float_size(): +def test_float_size() -> None: lengths = [] for size in (48, 48.5, 49): f = ImageFont.truetype( @@ -207,7 +207,7 @@ def test_float_size(): assert lengths[0] != lengths[1] != lengths[2] -def test_render_multiline(font): +def test_render_multiline(font) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) line_spacing = font.getbbox("A")[3] + 4 @@ -223,7 +223,7 @@ def test_render_multiline(font): assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) -def test_render_multiline_text(font): +def test_render_multiline_text(font) -> None: # Test that text() correctly connects to multiline_text() # and that align defaults to left im = Image.new(mode="RGB", size=(300, 100)) @@ -243,7 +243,7 @@ def test_render_multiline_text(font): @pytest.mark.parametrize( "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) ) -def test_render_multiline_text_align(font, align, ext): +def test_render_multiline_text_align(font, align, ext) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) @@ -251,7 +251,7 @@ def test_render_multiline_text_align(font, align, ext): assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) -def test_unknown_align(font): +def test_unknown_align(font) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -260,14 +260,14 @@ def test_unknown_align(font): draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") -def test_draw_align(font): +def test_draw_align(font) -> None: im = Image.new("RGB", (300, 100), "white") draw = ImageDraw.Draw(im) line = "some text" draw.text((100, 40), line, (0, 0, 0), font=font, align="left") -def test_multiline_bbox(font): +def test_multiline_bbox(font) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -285,7 +285,7 @@ def test_multiline_bbox(font): draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) -def test_multiline_width(font): +def test_multiline_width(font) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -295,7 +295,7 @@ def test_multiline_width(font): ) -def test_multiline_spacing(font): +def test_multiline_spacing(font) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) @@ -306,7 +306,7 @@ def test_multiline_spacing(font): @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font(font, orientation): +def test_rotated_transposed_font(font, orientation) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -347,7 +347,7 @@ def test_rotated_transposed_font(font, orientation): Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font(font, orientation): +def test_unrotated_transposed_font(font, orientation) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -382,7 +382,7 @@ def test_unrotated_transposed_font(font, orientation): @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font_get_mask(font, orientation): +def test_rotated_transposed_font_get_mask(font, orientation) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -403,7 +403,7 @@ def test_rotated_transposed_font_get_mask(font, orientation): Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font_get_mask(font, orientation): +def test_unrotated_transposed_font_get_mask(font, orientation) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -415,11 +415,11 @@ def test_unrotated_transposed_font_get_mask(font, orientation): assert mask.size == (108, 13) -def test_free_type_font_get_name(font): +def test_free_type_font_get_name(font) -> None: assert ("FreeMono", "Regular") == font.getname() -def test_free_type_font_get_metrics(font): +def test_free_type_font_get_metrics(font) -> None: ascent, descent = font.getmetrics() assert isinstance(ascent, int) @@ -427,7 +427,7 @@ def test_free_type_font_get_metrics(font): assert (ascent, descent) == (16, 4) -def test_free_type_font_get_mask(font): +def test_free_type_font_get_mask(font) -> None: # Arrange text = "mask this" @@ -438,7 +438,7 @@ def test_free_type_font_get_mask(font): assert mask.size == (108, 13) -def test_load_path_not_found(): +def test_load_path_not_found() -> None: # Arrange filename = "somefilenamethatdoesntexist.ttf" @@ -449,13 +449,13 @@ def test_load_path_not_found(): ImageFont.truetype(filename) -def test_load_non_font_bytes(): +def test_load_non_font_bytes() -> None: with open("Tests/images/hopper.jpg", "rb") as f: with pytest.raises(OSError): ImageFont.truetype(f) -def test_default_font(): +def test_default_font() -> None: # Arrange txt = "This is a default font using FreeType support." im = Image.new(mode="RGB", size=(300, 100)) @@ -473,16 +473,16 @@ def test_default_font(): @pytest.mark.parametrize("mode", (None, "1", "RGBA")) -def test_getbbox(font, mode): +def test_getbbox(font, mode) -> None: assert (0, 4, 12, 16) == font.getbbox("A", mode) -def test_getbbox_empty(font): +def test_getbbox_empty(font) -> None: # issue #2614, should not crash. assert (0, 0, 0, 0) == font.getbbox("") -def test_render_empty(font): +def test_render_empty(font) -> None: # issue 2666 im = Image.new(mode="RGB", size=(300, 100)) target = im.copy() @@ -492,7 +492,7 @@ def test_render_empty(font): assert_image_equal(im, target) -def test_unicode_extended(layout_engine): +def test_unicode_extended(layout_engine) -> None: # issue #3777 text = "A\u278A\U0001F12B" target = "Tests/images/unicode_extended.png" @@ -515,8 +515,8 @@ def test_unicode_extended(layout_engine): (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), ) @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") -def test_find_font(monkeypatch, platform, font_directory): - def _test_fake_loading_font(path_to_fake, fontname): +def test_find_font(monkeypatch, platform, font_directory) -> None: + def _test_fake_loading_font(path_to_fake, fontname) -> None: # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) with monkeypatch.context() as m: @@ -567,7 +567,7 @@ def fake_walker(path): _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") -def test_imagefont_getters(font): +def test_imagefont_getters(font) -> None: assert font.getmetrics() == (16, 4) assert font.font.ascent == 16 assert font.font.descent == 4 @@ -588,7 +588,7 @@ def test_imagefont_getters(font): @pytest.mark.parametrize("stroke_width", (0, 2)) -def test_getsize_stroke(font, stroke_width): +def test_getsize_stroke(font, stroke_width) -> None: assert font.getbbox("A", stroke_width=stroke_width) == ( 0 - stroke_width, 4 - stroke_width, @@ -597,7 +597,7 @@ def test_getsize_stroke(font, stroke_width): ) -def test_complex_font_settings(): +def test_complex_font_settings() -> None: t = ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.BASIC) with pytest.raises(KeyError): t.getmask("абвг", direction="rtl") @@ -607,7 +607,7 @@ def test_complex_font_settings(): t.getmask("абвг", language="sr") -def test_variation_get(font): +def test_variation_get(font) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -677,7 +677,7 @@ def _check_text(font, path, epsilon): raise -def test_variation_set_by_name(font): +def test_variation_set_by_name(font) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -702,7 +702,7 @@ def test_variation_set_by_name(font): _check_text(font, "Tests/images/variation_tiny_name.png", 40) -def test_variation_set_by_axes(font): +def test_variation_set_by_axes(font) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -737,7 +737,7 @@ def test_variation_set_by_axes(font): ), ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), ) -def test_anchor(layout_engine, anchor, left, top): +def test_anchor(layout_engine, anchor, left, top) -> None: name, text = "quick", "Quick" path = f"Tests/images/test_anchor_{name}_{anchor}.png" @@ -782,7 +782,7 @@ def test_anchor(layout_engine, anchor, left, top): ("md", "center"), ), ) -def test_anchor_multiline(layout_engine, anchor, align): +def test_anchor_multiline(layout_engine, anchor, align) -> None: target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" text = "a\nlong\ntext sample" @@ -800,7 +800,7 @@ def test_anchor_multiline(layout_engine, anchor, align): assert_image_similar_tofile(im, target, 4) -def test_anchor_invalid(font): +def test_anchor_invalid(font) -> None: im = Image.new("RGB", (100, 100), "white") d = ImageDraw.Draw(im) d.font = font @@ -826,7 +826,7 @@ def test_anchor_invalid(font): @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) -def test_bitmap_font(layout_engine, bpp): +def test_bitmap_font(layout_engine, bpp) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" @@ -843,7 +843,7 @@ def test_bitmap_font(layout_engine, bpp): assert_image_equal_tofile(im, target) -def test_bitmap_font_stroke(layout_engine): +def test_bitmap_font_stroke(layout_engine) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" @@ -861,7 +861,7 @@ def test_bitmap_font_stroke(layout_engine): @pytest.mark.parametrize("embedded_color", (False, True)) -def test_bitmap_blend(layout_engine, embedded_color): +def test_bitmap_blend(layout_engine, embedded_color) -> None: font = ImageFont.truetype( "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine ) @@ -873,7 +873,7 @@ def test_bitmap_blend(layout_engine, embedded_color): assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png") -def test_standard_embedded_color(layout_engine): +def test_standard_embedded_color(layout_engine) -> None: txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) ttf.getbbox(txt) @@ -908,7 +908,7 @@ def test_float_coord(layout_engine, fontmode): raise -def test_cbdt(layout_engine): +def test_cbdt(layout_engine) -> None: try: font = ImageFont.truetype( "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine @@ -925,7 +925,7 @@ def test_cbdt(layout_engine): pytest.skip("freetype compiled without libpng or CBDT support") -def test_cbdt_mask(layout_engine): +def test_cbdt_mask(layout_engine) -> None: try: font = ImageFont.truetype( "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine @@ -942,7 +942,7 @@ def test_cbdt_mask(layout_engine): pytest.skip("freetype compiled without libpng or CBDT support") -def test_sbix(layout_engine): +def test_sbix(layout_engine) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -959,7 +959,7 @@ def test_sbix(layout_engine): pytest.skip("freetype compiled without libpng or SBIX support") -def test_sbix_mask(layout_engine): +def test_sbix_mask(layout_engine) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -977,7 +977,7 @@ def test_sbix_mask(layout_engine): @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr(layout_engine): +def test_colr(layout_engine) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -993,7 +993,7 @@ def test_colr(layout_engine): @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr_mask(layout_engine): +def test_colr_mask(layout_engine) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -1008,7 +1008,7 @@ def test_colr_mask(layout_engine): assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) -def test_woff2(layout_engine): +def test_woff2(layout_engine) -> None: try: font = ImageFont.truetype( "Tests/fonts/OpenSans.woff2", @@ -1027,7 +1027,7 @@ def test_woff2(layout_engine): assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5) -def test_render_mono_size(): +def test_render_mono_size() -> None: # issue 4177 im = Image.new("P", (100, 30), "white") @@ -1042,7 +1042,7 @@ def test_render_mono_size(): assert_image_equal_tofile(im, "Tests/images/text_mono.gif") -def test_too_many_characters(font): +def test_too_many_characters(font) -> None: with pytest.raises(ValueError): font.getlength("A" * 1_000_001) with pytest.raises(ValueError): @@ -1070,14 +1070,14 @@ def test_too_many_characters(font): "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", ], ) -def test_oom(test_file): +def test_oom(test_file) -> None: with open(test_file, "rb") as f: font = ImageFont.truetype(BytesIO(f.read())) with pytest.raises(Image.DecompressionBombError): font.getmask("Test Text") -def test_raqm_missing_warning(monkeypatch): +def test_raqm_missing_warning(monkeypatch) -> None: monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) with pytest.warns(UserWarning) as record: font = ImageFont.truetype( @@ -1091,6 +1091,6 @@ def test_raqm_missing_warning(monkeypatch): @pytest.mark.parametrize("size", [-1, 0]) -def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size): +def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size) -> None: with pytest.raises(ValueError): ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 09e68ea488a..325e7ef216b 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -12,7 +12,7 @@ pytestmark = skip_unless_feature("raqm") -def test_english(): +def test_english() -> None: # smoke test, this should not fail ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -20,7 +20,7 @@ def test_english(): draw.text((0, 0), "TEST", font=ttf, fill=500, direction="ltr") -def test_complex_text(): +def test_complex_text() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -31,7 +31,7 @@ def test_complex_text(): assert_image_similar_tofile(im, target, 0.5) -def test_y_offset(): +def test_y_offset() -> None: ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -42,7 +42,7 @@ def test_y_offset(): assert_image_similar_tofile(im, target, 1.7) -def test_complex_unicode_text(): +def test_complex_unicode_text() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -62,7 +62,7 @@ def test_complex_unicode_text(): assert_image_similar_tofile(im, target, 2.33) -def test_text_direction_rtl(): +def test_text_direction_rtl() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -73,7 +73,7 @@ def test_text_direction_rtl(): assert_image_similar_tofile(im, target, 0.5) -def test_text_direction_ltr(): +def test_text_direction_ltr() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -84,7 +84,7 @@ def test_text_direction_ltr(): assert_image_similar_tofile(im, target, 0.5) -def test_text_direction_rtl2(): +def test_text_direction_rtl2() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -95,7 +95,7 @@ def test_text_direction_rtl2(): assert_image_similar_tofile(im, target, 0.5) -def test_text_direction_ttb(): +def test_text_direction_ttb() -> None: ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", FONT_SIZE) im = Image.new(mode="RGB", size=(100, 300)) @@ -110,7 +110,7 @@ def test_text_direction_ttb(): assert_image_similar_tofile(im, target, 2.8) -def test_text_direction_ttb_stroke(): +def test_text_direction_ttb_stroke() -> None: ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50) im = Image.new(mode="RGB", size=(100, 300)) @@ -133,7 +133,7 @@ def test_text_direction_ttb_stroke(): assert_image_similar_tofile(im, target, 19.4) -def test_ligature_features(): +def test_ligature_features() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -146,7 +146,7 @@ def test_ligature_features(): assert liga_bbox == (0, 4, 13, 19) -def test_kerning_features(): +def test_kerning_features() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -157,7 +157,7 @@ def test_kerning_features(): assert_image_similar_tofile(im, target, 0.5) -def test_arabictext_features(): +def test_arabictext_features() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -174,7 +174,7 @@ def test_arabictext_features(): assert_image_similar_tofile(im, target, 0.5) -def test_x_max_and_y_offset(): +def test_x_max_and_y_offset() -> None: ttf = ImageFont.truetype("Tests/fonts/ArefRuqaa-Regular.ttf", 40) im = Image.new(mode="RGB", size=(50, 100)) @@ -185,7 +185,7 @@ def test_x_max_and_y_offset(): assert_image_similar_tofile(im, target, 0.5) -def test_language(): +def test_language() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -208,7 +208,7 @@ def test_language(): ), ids=("None", "ltr", "rtl2", "rtl", "ttb"), ) -def test_getlength(mode, text, direction, expected): +def test_getlength(mode, text, direction, expected) -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode, (1, 1), 0) d = ImageDraw.Draw(im) @@ -230,7 +230,7 @@ def test_getlength(mode, text, direction, expected): ("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"), ids=("caron-above", "caron-below", "double-breve", "overline"), ) -def test_getlength_combine(mode, direction, text): +def test_getlength_combine(mode, direction, text) -> None: if text == "i\u0305i" and direction == "ttb": pytest.skip("fails with this font") @@ -250,7 +250,7 @@ def test_getlength_combine(mode, direction, text): @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) -def test_anchor_ttb(anchor): +def test_anchor_ttb(anchor) -> None: text = "f" path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120) @@ -306,7 +306,7 @@ def test_anchor_ttb(anchor): @pytest.mark.parametrize( "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] ) -def test_combine(name, text, dir, anchor, epsilon): +def test_combine(name, text, dir, anchor, epsilon) -> None: path = f"Tests/images/test_combine_{name}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) @@ -337,7 +337,7 @@ def test_combine(name, text, dir, anchor, epsilon): ("rm", "right"), # pass with getsize ), ) -def test_combine_multiline(anchor, align): +def test_combine_multiline(anchor, align) -> None: # test that multiline text uses getlength, not getsize or getbbox path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" @@ -355,7 +355,7 @@ def test_combine_multiline(anchor, align): assert_image_similar_tofile(im, path, 0.015) -def test_anchor_invalid_ttb(): +def test_anchor_invalid_ttb() -> None: font = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new("RGB", (100, 100), "white") d = ImageDraw.Draw(im) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index be4be1c546e..3b1c14b4e18 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -12,16 +12,16 @@ original_core = ImageFont.core -def setup_module(): +def setup_module() -> None: if features.check_module("freetype2"): ImageFont.core = _util.DeferredError(ImportError) -def teardown_module(): +def teardown_module() -> None: ImageFont.core = original_core -def test_default_font(): +def test_default_font() -> None: # Arrange txt = 'This is a "better than nothing" default font.' im = Image.new(mode="RGB", size=(300, 100)) @@ -35,12 +35,12 @@ def test_default_font(): assert_image_equal_tofile(im, "Tests/images/default_font.png") -def test_size_without_freetype(): +def test_size_without_freetype() -> None: with pytest.raises(ImportError): ImageFont.load_default(size=14) -def test_unicode(): +def test_unicode() -> None: # should not segfault, should return UnicodeDecodeError # issue #2826 font = ImageFont.load_default() @@ -48,7 +48,7 @@ def test_unicode(): font.getbbox("’") -def test_textbbox(): +def test_textbbox() -> None: im = Image.new("RGB", (200, 200)) d = ImageDraw.Draw(im) default_font = ImageFont.load_default() @@ -56,7 +56,7 @@ def test_textbbox(): assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11) -def test_decompression_bomb(): +def test_decompression_bomb() -> None: glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 256, 256, 0, 0, 256, 256) fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) @@ -67,7 +67,7 @@ def test_decompression_bomb(): @pytest.mark.timeout(4) -def test_oom(): +def test_oom() -> None: glyph = struct.pack( ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 ) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 9d3d40398f6..40c1d323e77 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -20,7 +20,7 @@ class TestImageGrab: @pytest.mark.skipif( sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS" ) - def test_grab(self): + def test_grab(self) -> None: ImageGrab.grab() ImageGrab.grab(include_layered_windows=True) ImageGrab.grab(all_screens=True) @@ -29,7 +29,7 @@ def test_grab(self): assert im.size == (40, 60) @skip_unless_feature("xcb") - def test_grab_x11(self): + def test_grab_x11(self) -> None: try: if sys.platform not in ("win32", "darwin"): ImageGrab.grab() @@ -39,7 +39,7 @@ def test_grab_x11(self): pytest.skip(str(e)) @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") - def test_grab_no_xcb(self): + def test_grab_no_xcb(self) -> None: if sys.platform not in ("win32", "darwin") and not shutil.which( "gnome-screenshot" ): @@ -52,12 +52,12 @@ def test_grab_no_xcb(self): assert str(e.value).startswith("Pillow was built without XCB support") @skip_unless_feature("xcb") - def test_grab_invalid_xdisplay(self): + def test_grab_invalid_xdisplay(self) -> None: with pytest.raises(OSError) as e: ImageGrab.grab(xdisplay="error.test:0.0") assert str(e.value).startswith("X connection failed") - def test_grabclipboard(self): + def test_grabclipboard(self) -> None: if sys.platform == "darwin": subprocess.call(["screencapture", "-cx"]) elif sys.platform == "win32": @@ -82,7 +82,7 @@ def test_grabclipboard(self): ImageGrab.grabclipboard() @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") - def test_grabclipboard_file(self): + def test_grabclipboard_file(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') p.communicate() @@ -92,7 +92,7 @@ def test_grabclipboard_file(self): assert os.path.samefile(im[0], "Tests/images/hopper.gif") @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") - def test_grabclipboard_png(self): + def test_grabclipboard_png(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) p.stdin.write( rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") @@ -113,7 +113,7 @@ def test_grabclipboard_png(self): reason="Linux with wl-clipboard only", ) @pytest.mark.parametrize("ext", ("gif", "png", "ico")) - def test_grabclipboard_wl_clipboard(self, ext): + def test_grabclipboard_wl_clipboard(self, ext) -> None: image_path = "Tests/images/hopper." + ext with open(image_path, "rb") as fp: subprocess.call(["wl-copy"], stdin=fp) diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 622ad27eacb..ea6e80f1ee7 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -24,7 +24,7 @@ def pixel(im): images = {"A": A, "B": B, "F": F, "I": I} -def test_sanity(): +def test_sanity() -> None: assert ImageMath.eval("1") == 1 assert ImageMath.eval("1+A", A=2) == 3 assert pixel(ImageMath.eval("A+B", A=A, B=B)) == "I 3" @@ -33,7 +33,7 @@ def test_sanity(): assert pixel(ImageMath.eval("int(float(A)+B)", images)) == "I 3" -def test_ops(): +def test_ops() -> None: assert pixel(ImageMath.eval("-A", images)) == "I -1" assert pixel(ImageMath.eval("+B", images)) == "L 2" @@ -60,51 +60,51 @@ def test_ops(): "(lambda: (lambda: exec('pass'))())()", ), ) -def test_prevent_exec(expression): +def test_prevent_exec(expression) -> None: with pytest.raises(ValueError): ImageMath.eval(expression) -def test_prevent_double_underscores(): +def test_prevent_double_underscores() -> None: with pytest.raises(ValueError): ImageMath.eval("1", {"__": None}) -def test_prevent_builtins(): +def test_prevent_builtins() -> None: with pytest.raises(ValueError): ImageMath.eval("(lambda: exec('exit()'))()", {"exec": None}) -def test_logical(): +def test_logical() -> None: assert pixel(ImageMath.eval("not A", images)) == 0 assert pixel(ImageMath.eval("A and B", images)) == "L 2" assert pixel(ImageMath.eval("A or B", images)) == "L 1" -def test_convert(): +def test_convert() -> None: assert pixel(ImageMath.eval("convert(A+B, 'L')", images)) == "L 3" assert pixel(ImageMath.eval("convert(A+B, '1')", images)) == "1 0" assert pixel(ImageMath.eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)" -def test_compare(): +def test_compare() -> None: assert pixel(ImageMath.eval("min(A, B)", images)) == "I 1" assert pixel(ImageMath.eval("max(A, B)", images)) == "I 2" assert pixel(ImageMath.eval("A == 1", images)) == "I 1" assert pixel(ImageMath.eval("A == 2", images)) == "I 0" -def test_one_image_larger(): +def test_one_image_larger() -> None: assert pixel(ImageMath.eval("A+B", A=A2, B=B)) == "I 3" assert pixel(ImageMath.eval("A+B", A=A, B=B2)) == "I 3" -def test_abs(): +def test_abs() -> None: assert pixel(ImageMath.eval("abs(A)", A=A)) == "I 1" assert pixel(ImageMath.eval("abs(B)", B=B)) == "I 2" -def test_binary_mod(): +def test_binary_mod() -> None: assert pixel(ImageMath.eval("A%A", A=A)) == "I 0" assert pixel(ImageMath.eval("B%B", B=B)) == "I 0" assert pixel(ImageMath.eval("A%B", A=A, B=B)) == "I 1" @@ -113,90 +113,90 @@ def test_binary_mod(): assert pixel(ImageMath.eval("Z%B", B=B, Z=Z)) == "I 0" -def test_bitwise_invert(): +def test_bitwise_invert() -> None: assert pixel(ImageMath.eval("~Z", Z=Z)) == "I -1" assert pixel(ImageMath.eval("~A", A=A)) == "I -2" assert pixel(ImageMath.eval("~B", B=B)) == "I -3" -def test_bitwise_and(): +def test_bitwise_and() -> None: assert pixel(ImageMath.eval("Z&Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z&A", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("A&Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("A&A", A=A, Z=Z)) == "I 1" -def test_bitwise_or(): +def test_bitwise_or() -> None: assert pixel(ImageMath.eval("Z|Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z|A", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A|Z", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A|A", A=A, Z=Z)) == "I 1" -def test_bitwise_xor(): +def test_bitwise_xor() -> None: assert pixel(ImageMath.eval("Z^Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z^A", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A^Z", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A^A", A=A, Z=Z)) == "I 0" -def test_bitwise_leftshift(): +def test_bitwise_leftshift() -> None: assert pixel(ImageMath.eval("Z<<0", Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z<<1", Z=Z)) == "I 0" assert pixel(ImageMath.eval("A<<0", A=A)) == "I 1" assert pixel(ImageMath.eval("A<<1", A=A)) == "I 2" -def test_bitwise_rightshift(): +def test_bitwise_rightshift() -> None: assert pixel(ImageMath.eval("Z>>0", Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z>>1", Z=Z)) == "I 0" assert pixel(ImageMath.eval("A>>0", A=A)) == "I 1" assert pixel(ImageMath.eval("A>>1", A=A)) == "I 0" -def test_logical_eq(): +def test_logical_eq() -> None: assert pixel(ImageMath.eval("A==A", A=A)) == "I 1" assert pixel(ImageMath.eval("B==B", B=B)) == "I 1" assert pixel(ImageMath.eval("A==B", A=A, B=B)) == "I 0" assert pixel(ImageMath.eval("B==A", A=A, B=B)) == "I 0" -def test_logical_ne(): +def test_logical_ne() -> None: assert pixel(ImageMath.eval("A!=A", A=A)) == "I 0" assert pixel(ImageMath.eval("B!=B", B=B)) == "I 0" assert pixel(ImageMath.eval("A!=B", A=A, B=B)) == "I 1" assert pixel(ImageMath.eval("B!=A", A=A, B=B)) == "I 1" -def test_logical_lt(): +def test_logical_lt() -> None: assert pixel(ImageMath.eval("A None: assert pixel(ImageMath.eval("A<=A", A=A)) == "I 1" assert pixel(ImageMath.eval("B<=B", B=B)) == "I 1" assert pixel(ImageMath.eval("A<=B", A=A, B=B)) == "I 1" assert pixel(ImageMath.eval("B<=A", A=A, B=B)) == "I 0" -def test_logical_gt(): +def test_logical_gt() -> None: assert pixel(ImageMath.eval("A>A", A=A)) == "I 0" assert pixel(ImageMath.eval("B>B", B=B)) == "I 0" assert pixel(ImageMath.eval("A>B", A=A, B=B)) == "I 0" assert pixel(ImageMath.eval("B>A", A=A, B=B)) == "I 1" -def test_logical_ge(): +def test_logical_ge() -> None: assert pixel(ImageMath.eval("A>=A", A=A)) == "I 1" assert pixel(ImageMath.eval("B>=B", B=B)) == "I 1" assert pixel(ImageMath.eval("A>=B", A=A, B=B)) == "I 0" assert pixel(ImageMath.eval("B>=A", A=A, B=B)) == "I 1" -def test_logical_equal(): +def test_logical_equal() -> None: assert pixel(ImageMath.eval("equal(A, A)", A=A)) == "I 1" assert pixel(ImageMath.eval("equal(B, B)", B=B)) == "I 1" assert pixel(ImageMath.eval("equal(Z, Z)", Z=Z)) == "I 1" @@ -205,7 +205,7 @@ def test_logical_equal(): assert pixel(ImageMath.eval("equal(A, Z)", A=A, Z=Z)) == "I 0" -def test_logical_not_equal(): +def test_logical_not_equal() -> None: assert pixel(ImageMath.eval("notequal(A, A)", A=A)) == "I 0" assert pixel(ImageMath.eval("notequal(B, B)", B=B)) == "I 0" assert pixel(ImageMath.eval("notequal(Z, Z)", Z=Z)) == "I 0" diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 0708ee63905..0b0c6d2d31a 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,6 +1,8 @@ # Test the ImageMorphology functionality from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImageMorph, _imagingmorph @@ -50,18 +52,18 @@ def img_string_normalize(im): return img_to_string(string_to_img(im)) -def assert_img_equal_img_string(a, b_string): +def assert_img_equal_img_string(a, b_string) -> None: assert img_to_string(a) == img_string_normalize(b_string) -def test_str_to_img(): +def test_str_to_img() -> None: assert_image_equal_tofile(A, "Tests/images/morph_a.png") @pytest.mark.parametrize( "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge") ) -def test_lut(op): +def test_lut(op) -> None: lb = ImageMorph.LutBuilder(op_name=op) assert lb.get_lut() is None @@ -70,7 +72,7 @@ def test_lut(op): assert lut == bytearray(f.read()) -def test_no_operator_loaded(): +def test_no_operator_loaded() -> None: mop = ImageMorph.MorphOp() with pytest.raises(Exception) as e: mop.apply(None) @@ -84,7 +86,7 @@ def test_no_operator_loaded(): # Test the named patterns -def test_erosion8(): +def test_erosion8() -> None: # erosion8 mop = ImageMorph.MorphOp(op_name="erosion8") count, Aout = mop.apply(A) @@ -103,7 +105,7 @@ def test_erosion8(): ) -def test_dialation8(): +def test_dialation8() -> None: # dialation8 mop = ImageMorph.MorphOp(op_name="dilation8") count, Aout = mop.apply(A) @@ -122,7 +124,7 @@ def test_dialation8(): ) -def test_erosion4(): +def test_erosion4() -> None: # erosion4 mop = ImageMorph.MorphOp(op_name="dilation4") count, Aout = mop.apply(A) @@ -141,7 +143,7 @@ def test_erosion4(): ) -def test_edge(): +def test_edge() -> None: # edge mop = ImageMorph.MorphOp(op_name="edge") count, Aout = mop.apply(A) @@ -160,7 +162,7 @@ def test_edge(): ) -def test_corner(): +def test_corner() -> None: # Create a corner detector pattern mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) count, Aout = mop.apply(A) @@ -188,7 +190,7 @@ def test_corner(): assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) -def test_mirroring(): +def test_mirroring() -> None: # Test 'M' for mirroring mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "M:(00. 01. ...)->1"]) count, Aout = mop.apply(A) @@ -207,7 +209,7 @@ def test_mirroring(): ) -def test_negate(): +def test_negate() -> None: # Test 'N' for negate mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "N:(00. 01. ...)->1"]) count, Aout = mop.apply(A) @@ -226,7 +228,7 @@ def test_negate(): ) -def test_incorrect_mode(): +def test_incorrect_mode() -> None: im = hopper("RGB") mop = ImageMorph.MorphOp(op_name="erosion8") @@ -241,7 +243,7 @@ def test_incorrect_mode(): assert str(e.value) == "Image mode must be L" -def test_add_patterns(): +def test_add_patterns() -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") assert lb.patterns == ["1:(... ... ...)->0", "4:(00. 01. ...)->1"] @@ -259,12 +261,12 @@ def test_add_patterns(): ] -def test_unknown_pattern(): +def test_unknown_pattern() -> None: with pytest.raises(Exception): ImageMorph.LutBuilder(op_name="unknown") -def test_pattern_syntax_error(): +def test_pattern_syntax_error() -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") new_patterns = ["a pattern with a syntax error"] @@ -276,7 +278,7 @@ def test_pattern_syntax_error(): assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' -def test_load_invalid_mrl(): +def test_load_invalid_mrl() -> None: # Arrange invalid_mrl = "Tests/images/hopper.png" mop = ImageMorph.MorphOp() @@ -287,7 +289,7 @@ def test_load_invalid_mrl(): assert str(e.value) == "Wrong size operator file!" -def test_roundtrip_mrl(tmp_path): +def test_roundtrip_mrl(tmp_path: Path) -> None: # Arrange tempfile = str(tmp_path / "temp.mrl") mop = ImageMorph.MorphOp(op_name="corner") @@ -301,7 +303,7 @@ def test_roundtrip_mrl(tmp_path): assert mop.lut == initial_lut -def test_set_lut(): +def test_set_lut() -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") lut = lb.build_lut() @@ -314,7 +316,7 @@ def test_set_lut(): assert mop.lut == lut -def test_wrong_mode(): +def test_wrong_mode() -> None: lut = ImageMorph.LutBuilder(op_name="corner").build_lut() imrgb = Image.new("RGB", (10, 10)) iml = Image.new("L", (10, 10)) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 636b99dbe8f..50bf404aebe 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -22,7 +22,7 @@ def getmesh(self, im): deformer = Deformer() -def test_sanity(): +def test_sanity() -> None: ImageOps.autocontrast(hopper("L")) ImageOps.autocontrast(hopper("RGB")) @@ -84,7 +84,7 @@ def test_sanity(): ImageOps.exif_transpose(hopper("RGB")) -def test_1pxfit(): +def test_1pxfit() -> None: # Division by zero in equalize if image is 1 pixel high newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35)) assert newimg.size == (35, 35) @@ -96,7 +96,7 @@ def test_1pxfit(): assert newimg.size == (35, 35) -def test_fit_same_ratio(): +def test_fit_same_ratio() -> None: # The ratio for this image is 1000.0 / 755 = 1.3245033112582782 # If the ratios are not acknowledged to be the same, # and Pillow attempts to adjust the width to @@ -108,13 +108,13 @@ def test_fit_same_ratio(): @pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512))) -def test_contain(new_size): +def test_contain(new_size) -> None: im = hopper() new_im = ImageOps.contain(im, new_size) assert new_im.size == (256, 256) -def test_contain_round(): +def test_contain_round() -> None: im = Image.new("1", (43, 63), 1) new_im = ImageOps.contain(im, (5, 7)) assert new_im.width == 5 @@ -132,13 +132,13 @@ def test_contain_round(): ("hopper.png", (256, 256)), # square ), ) -def test_cover(image_name, expected_size): +def test_cover(image_name, expected_size) -> None: with Image.open("Tests/images/" + image_name) as im: new_im = ImageOps.cover(im, (256, 256)) assert new_im.size == expected_size -def test_pad(): +def test_pad() -> None: # Same ratio im = hopper() new_size = (im.width * 2, im.height * 2) @@ -158,7 +158,7 @@ def test_pad(): ) -def test_pad_round(): +def test_pad_round() -> None: im = Image.new("1", (1, 1), 1) new_im = ImageOps.pad(im, (4, 1)) assert new_im.load()[2, 0] == 1 @@ -168,7 +168,7 @@ def test_pad_round(): @pytest.mark.parametrize("mode", ("P", "PA")) -def test_palette(mode): +def test_palette(mode) -> None: im = hopper(mode) # Expand @@ -182,7 +182,7 @@ def test_palette(mode): ) -def test_pil163(): +def test_pil163() -> None: # Division by zero in equalize if < 255 pixels in image (@PIL163) i = hopper("RGB").resize((15, 16)) @@ -192,7 +192,7 @@ def test_pil163(): ImageOps.equalize(i.convert("RGB")) -def test_scale(): +def test_scale() -> None: # Test the scaling function i = hopper("L").resize((50, 50)) @@ -210,7 +210,7 @@ def test_scale(): @pytest.mark.parametrize("border", (10, (1, 2, 3, 4))) -def test_expand_palette(border): +def test_expand_palette(border) -> None: with Image.open("Tests/images/p_16.tga") as im: im_expanded = ImageOps.expand(im, border, (255, 0, 0)) @@ -236,7 +236,7 @@ def test_expand_palette(border): assert_image_equal(im_cropped, im) -def test_colorize_2color(): +def test_colorize_2color() -> None: # Test the colorizing function with 2-color functionality # Open test image (256px by 10px, black to white) @@ -270,7 +270,7 @@ def test_colorize_2color(): ) -def test_colorize_2color_offset(): +def test_colorize_2color_offset() -> None: # Test the colorizing function with 2-color functionality and offset # Open test image (256px by 10px, black to white) @@ -306,7 +306,7 @@ def test_colorize_2color_offset(): ) -def test_colorize_3color_offset(): +def test_colorize_3color_offset() -> None: # Test the colorizing function with 3-color functionality and offset # Open test image (256px by 10px, black to white) @@ -359,14 +359,14 @@ def test_colorize_3color_offset(): ) -def test_exif_transpose(): +def test_exif_transpose() -> None: exts = [".jpg"] if features.check("webp") and features.check("webp_anim"): exts.append(".webp") for ext in exts: with Image.open("Tests/images/hopper" + ext) as base_im: - def check(orientation_im): + def check(orientation_im) -> None: for im in [ orientation_im, orientation_im.copy(), @@ -423,7 +423,7 @@ def check(orientation_im): assert 0x0112 not in transposed_im.getexif() -def test_exif_transpose_in_place(): +def test_exif_transpose_in_place() -> None: with Image.open("Tests/images/orientation_rectangle.jpg") as im: assert im.size == (2, 1) assert im.getexif()[0x0112] == 8 @@ -435,13 +435,13 @@ def test_exif_transpose_in_place(): assert_image_equal(im, expected) -def test_autocontrast_unsupported_mode(): +def test_autocontrast_unsupported_mode() -> None: im = Image.new("RGBA", (1, 1)) with pytest.raises(OSError): ImageOps.autocontrast(im) -def test_autocontrast_cutoff(): +def test_autocontrast_cutoff() -> None: # Test the cutoff argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: @@ -452,7 +452,7 @@ def autocontrast(cutoff): assert autocontrast(10) != autocontrast((1, 10)) -def test_autocontrast_mask_toy_input(): +def test_autocontrast_mask_toy_input() -> None: # Test the mask argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: rect_mask = Image.new("L", img.size, 0) @@ -471,7 +471,7 @@ def test_autocontrast_mask_toy_input(): assert ImageStat.Stat(result_nomask).median == [128] -def test_autocontrast_mask_real_input(): +def test_autocontrast_mask_real_input() -> None: # Test the autocontrast with a rectangular mask with Image.open("Tests/images/iptc.jpg") as img: rect_mask = Image.new("L", img.size, 0) @@ -498,7 +498,7 @@ def test_autocontrast_mask_real_input(): ) -def test_autocontrast_preserve_tone(): +def test_autocontrast_preserve_tone() -> None: def autocontrast(mode, preserve_tone): im = hopper(mode) return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() @@ -507,7 +507,7 @@ def autocontrast(mode, preserve_tone): assert autocontrast("L", True) == autocontrast("L", False) -def test_autocontrast_preserve_gradient(): +def test_autocontrast_preserve_gradient() -> None: gradient = Image.linear_gradient("L") # test with a grayscale gradient that extends to 0,255. @@ -533,7 +533,7 @@ def test_autocontrast_preserve_gradient(): @pytest.mark.parametrize( "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) ) -def test_autocontrast_preserve_one_color(color): +def test_autocontrast_preserve_one_color(color) -> None: img = Image.new("RGB", (10, 10), color) # single color images shouldn't change diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 8ffb9bff7a2..03302e20f2a 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -18,7 +18,7 @@ def test_images(): im.close() -def test_filter_api(test_images): +def test_filter_api(test_images) -> None: im = test_images["im"] test_filter = ImageFilter.GaussianBlur(2.0) @@ -32,7 +32,7 @@ def test_filter_api(test_images): assert i.size == (128, 128) -def test_usm_formats(test_images): +def test_usm_formats(test_images) -> None: im = test_images["im"] usm = ImageFilter.UnsharpMask @@ -50,7 +50,7 @@ def test_usm_formats(test_images): im.convert("YCbCr").filter(usm) -def test_blur_formats(test_images): +def test_blur_formats(test_images) -> None: im = test_images["im"] blur = ImageFilter.GaussianBlur @@ -68,7 +68,7 @@ def test_blur_formats(test_images): im.convert("YCbCr").filter(blur) -def test_usm_accuracy(test_images): +def test_usm_accuracy(test_images) -> None: snakes = test_images["snakes"] src = snakes.convert("RGB") @@ -77,7 +77,7 @@ def test_usm_accuracy(test_images): assert i.tobytes() == src.tobytes() -def test_blur_accuracy(test_images): +def test_blur_accuracy(test_images) -> None: snakes = test_images["snakes"] i = snakes.filter(ImageFilter.GaussianBlur(0.4)) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index be21464b4ae..545229500a6 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImagePalette @@ -7,19 +9,19 @@ from .helper import assert_image_equal, assert_image_equal_tofile -def test_sanity(): +def test_sanity() -> None: palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) assert len(palette.colors) == 256 -def test_reload(): +def test_reload() -> None: with Image.open("Tests/images/hopper.gif") as im: original = im.copy() im.palette.dirty = 1 assert_image_equal(im.convert("RGB"), original.convert("RGB")) -def test_getcolor(): +def test_getcolor() -> None: palette = ImagePalette.ImagePalette() assert len(palette.palette) == 0 assert len(palette.colors) == 0 @@ -46,7 +48,7 @@ def test_getcolor(): palette.getcolor("unknown") -def test_getcolor_rgba_color_rgb_palette(): +def test_getcolor_rgba_color_rgb_palette() -> None: palette = ImagePalette.ImagePalette("RGB") # Opaque RGBA colors are converted @@ -65,7 +67,7 @@ def test_getcolor_rgba_color_rgb_palette(): (255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)), ], ) -def test_getcolor_not_special(index, palette): +def test_getcolor_not_special(index, palette) -> None: im = Image.new("P", (1, 1)) # Do not use transparency index as a new color @@ -79,7 +81,7 @@ def test_getcolor_not_special(index, palette): assert index2 not in (index, index1) -def test_file(tmp_path): +def test_file(tmp_path: Path) -> None: palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) f = str(tmp_path / "temp.lut") @@ -97,7 +99,7 @@ def test_file(tmp_path): assert p.palette == palette.tobytes() -def test_make_linear_lut(): +def test_make_linear_lut() -> None: # Arrange black = 0 white = 255 @@ -113,7 +115,7 @@ def test_make_linear_lut(): assert lut[i] == i -def test_make_linear_lut_not_yet_implemented(): +def test_make_linear_lut_not_yet_implemented() -> None: # Update after FIXME # Arrange black = 1 @@ -124,7 +126,7 @@ def test_make_linear_lut_not_yet_implemented(): ImagePalette.make_linear_lut(black, white) -def test_make_gamma_lut(): +def test_make_gamma_lut() -> None: # Arrange exp = 5 @@ -142,7 +144,7 @@ def test_make_gamma_lut(): assert lut[255] == 255 -def test_rawmode_valueerrors(tmp_path): +def test_rawmode_valueerrors(tmp_path: Path) -> None: # Arrange palette = ImagePalette.raw("RGB", list(range(256)) * 3) @@ -156,7 +158,7 @@ def test_rawmode_valueerrors(tmp_path): palette.save(f) -def test_getdata(): +def test_getdata() -> None: # Arrange data_in = list(range(256)) * 3 palette = ImagePalette.ImagePalette("RGB", data_in) @@ -168,7 +170,7 @@ def test_getdata(): assert mode == "RGB" -def test_rawmode_getdata(): +def test_rawmode_getdata() -> None: # Arrange data_in = list(range(256)) * 3 palette = ImagePalette.raw("RGB", data_in) @@ -181,7 +183,7 @@ def test_rawmode_getdata(): assert data_in == data_out -def test_2bit_palette(tmp_path): +def test_2bit_palette(tmp_path: Path) -> None: # issue #2258, 2 bit palettes are corrupted. outfile = str(tmp_path / "temp.png") @@ -193,6 +195,6 @@ def test_2bit_palette(tmp_path): assert_image_equal_tofile(img, outfile) -def test_invalid_palette(): +def test_invalid_palette() -> None: with pytest.raises(OSError): ImagePalette.load("Tests/images/hopper.jpg") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 5c6393e237f..8ba745f215d 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -9,7 +9,7 @@ from PIL import Image, ImagePath -def test_path(): +def test_path() -> None: p = ImagePath.Path(list(range(10))) # sequence interface @@ -57,7 +57,7 @@ def test_path(): ImagePath.Path((0, 1)), ), ) -def test_path_constructors(coords): +def test_path_constructors(coords) -> None: # Arrange / Act p = ImagePath.Path(coords) @@ -75,7 +75,7 @@ def test_path_constructors(coords): [[0.0, 1.0]], ), ) -def test_invalid_path_constructors(coords): +def test_invalid_path_constructors(coords) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -93,7 +93,7 @@ def test_invalid_path_constructors(coords): [0, 1, 2], ), ) -def test_path_odd_number_of_coordinates(coords): +def test_path_odd_number_of_coordinates(coords) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -111,7 +111,7 @@ def test_path_odd_number_of_coordinates(coords): (1, (0.0, 0.0, 0.0, 0.0)), ], ) -def test_getbbox(coords, expected): +def test_getbbox(coords, expected) -> None: # Arrange p = ImagePath.Path(coords) @@ -119,7 +119,7 @@ def test_getbbox(coords, expected): assert p.getbbox() == expected -def test_getbbox_no_args(): +def test_getbbox_no_args() -> None: # Arrange p = ImagePath.Path([0, 1, 2, 3]) @@ -135,7 +135,7 @@ def test_getbbox_no_args(): (list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]), ], ) -def test_map(coords, expected): +def test_map(coords, expected) -> None: # Arrange p = ImagePath.Path(coords) @@ -147,7 +147,7 @@ def test_map(coords, expected): assert list(p) == expected -def test_transform(): +def test_transform() -> None: # Arrange p = ImagePath.Path([0, 1, 2, 3]) theta = math.pi / 15 @@ -165,7 +165,7 @@ def test_transform(): ] -def test_transform_with_wrap(): +def test_transform_with_wrap() -> None: # Arrange p = ImagePath.Path([0, 1, 2, 3]) theta = math.pi / 15 @@ -184,7 +184,7 @@ def test_transform_with_wrap(): ] -def test_overflow_segfault(): +def test_overflow_segfault() -> None: # Some Pythons fail getting the argument as an integer, and it falls # through to the sequence. Seeing this on 32-bit Windows. with pytest.raises((TypeError, MemoryError)): @@ -198,12 +198,12 @@ def test_overflow_segfault(): class Evil: - def __init__(self): + def __init__(self) -> None: self.corrupt = Image.core.path(0x4000000000000000) def __getitem__(self, i): x = self.corrupt[i] return struct.pack("dd", x[0], x[1]) - def __setitem__(self, i, x): + def __setitem__(self, i, x) -> None: self.corrupt[i] = struct.unpack("dd", x) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index d55d980d9be..909f9716700 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -16,7 +16,7 @@ from PIL.ImageQt import qRgba -def test_rgb(): +def test_rgb() -> None: # from https://doc.qt.io/archives/qt-4.8/qcolor.html # typedef QRgb # An ARGB quadruplet on the format #AARRGGBB, @@ -28,7 +28,7 @@ def test_rgb(): assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) - def checkrgb(r, g, b): + def checkrgb(r, g, b) -> None: val = ImageQt.rgb(r, g, b) val = val % 2**24 # drop the alpha assert val >> 16 == r @@ -41,7 +41,7 @@ def checkrgb(r, g, b): checkrgb(0, 0, 255) -def test_image(): +def test_image() -> None: modes = ["1", "RGB", "RGBA", "L", "P"] qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ @@ -55,6 +55,6 @@ def test_image(): assert_image_similar(roundtripped_im, im, 1) -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): ImageQt.ImageQt("Tests/images/hopper.gif") diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 66d553bcbc7..7280dded01f 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImageSequence, TiffImagePlugin @@ -7,7 +9,7 @@ from .helper import assert_image_equal, hopper, skip_unless_feature -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.im") im = hopper("RGB") @@ -27,7 +29,7 @@ def test_sanity(tmp_path): ImageSequence.Iterator(0) -def test_iterator(): +def test_iterator() -> None: with Image.open("Tests/images/multipage.tiff") as im: i = ImageSequence.Iterator(im) for index in range(0, im.n_frames): @@ -38,14 +40,14 @@ def test_iterator(): next(i) -def test_iterator_min_frame(): +def test_iterator_min_frame() -> None: with Image.open("Tests/images/hopper.psd") as im: i = ImageSequence.Iterator(im) for index in range(1, im.n_frames): assert i[index] == next(i) -def _test_multipage_tiff(): +def _test_multipage_tiff() -> None: with Image.open("Tests/images/multipage.tiff") as im: for index, frame in enumerate(ImageSequence.Iterator(im)): frame.load() @@ -53,18 +55,18 @@ def _test_multipage_tiff(): frame.convert("RGB") -def test_tiff(): +def test_tiff() -> None: _test_multipage_tiff() @skip_unless_feature("libtiff") -def test_libtiff(): +def test_libtiff() -> None: TiffImagePlugin.READ_LIBTIFF = True _test_multipage_tiff() TiffImagePlugin.READ_LIBTIFF = False -def test_consecutive(): +def test_consecutive() -> None: with Image.open("Tests/images/multipage.tiff") as im: first_frame = None for frame in ImageSequence.Iterator(im): @@ -75,7 +77,7 @@ def test_consecutive(): break -def test_palette_mmap(): +def test_palette_mmap() -> None: # Using mmap in ImageFile can require to reload the palette. with Image.open("Tests/images/multipage-mmap.tiff") as im: color1 = im.getpalette()[:3] @@ -84,7 +86,7 @@ def test_palette_mmap(): assert color1 == color2 -def test_all_frames(): +def test_all_frames() -> None: # Test a single image with Image.open("Tests/images/iss634.gif") as im: ims = ImageSequence.all_frames(im) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 0996ad41d4d..f7269d45b56 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -7,12 +7,12 @@ from .helper import hopper, is_win32, on_ci -def test_sanity(): +def test_sanity() -> None: dir(Image) dir(ImageShow) -def test_register(): +def test_register() -> None: # Test registering a viewer that is not a class ImageShow.register("not a class") @@ -24,9 +24,9 @@ def test_register(): "order", [-1, 0], ) -def test_viewer_show(order): +def test_viewer_show(order) -> None: class TestViewer(ImageShow.Viewer): - def show_image(self, image, **options): + def show_image(self, image, **options) -> bool: self.methodCalled = True return True @@ -48,12 +48,12 @@ def show_image(self, image, **options): reason="Only run on CIs; hangs on Windows CIs", ) @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) -def test_show(mode): +def test_show(mode) -> None: im = hopper(mode) assert ImageShow.show(im) -def test_show_without_viewers(): +def test_show_without_viewers() -> None: viewers = ImageShow._viewers ImageShow._viewers = [] @@ -63,7 +63,7 @@ def test_show_without_viewers(): ImageShow._viewers = viewers -def test_viewer(): +def test_viewer() -> None: viewer = ImageShow.Viewer() assert viewer.get_format(None) is None @@ -73,14 +73,14 @@ def test_viewer(): @pytest.mark.parametrize("viewer", ImageShow._viewers) -def test_viewers(viewer): +def test_viewers(viewer) -> None: try: viewer.get_command("test.jpg") except NotImplementedError: pass -def test_ipythonviewer(): +def test_ipythonviewer() -> None: pytest.importorskip("IPython", reason="IPython not installed") for viewer in ImageShow._viewers: if isinstance(viewer, ImageShow.IPythonViewer): diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 01687db353e..b1c1306c1ec 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -7,7 +7,7 @@ from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() st = ImageStat.Stat(im) @@ -31,7 +31,7 @@ def test_sanity(): ImageStat.Stat(1) -def test_hopper(): +def test_hopper() -> None: im = hopper() st = ImageStat.Stat(im) @@ -44,7 +44,7 @@ def test_hopper(): assert st.sum[2] == 1563008 -def test_constant(): +def test_constant() -> None: im = Image.new("L", (128, 128), 128) st = ImageStat.Stat(im) diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index c06fc58235b..a216bd21de2 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -23,7 +23,7 @@ pytestmark = pytest.mark.skipif(not HAS_TK, reason="Tk not installed") -def setup_module(): +def setup_module() -> None: try: # setup tk tk.Frame() @@ -34,7 +34,7 @@ def setup_module(): pytest.skip(f"TCL Error: {v}") -def test_kw(): +def test_kw() -> None: TEST_JPG = "Tests/images/hopper.jpg" TEST_PNG = "Tests/images/hopper.png" with Image.open(TEST_JPG) as im1: @@ -57,7 +57,7 @@ def test_kw(): @pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage(mode): +def test_photoimage(mode) -> None: # test as image: im = hopper(mode) @@ -71,7 +71,7 @@ def test_photoimage(mode): assert_image_equal(reloaded, im.convert("RGBA")) -def test_photoimage_apply_transparency(): +def test_photoimage_apply_transparency() -> None: with Image.open("Tests/images/pil123p.png") as im: im_tk = ImageTk.PhotoImage(im) reloaded = ImageTk.getimage(im_tk) @@ -79,7 +79,7 @@ def test_photoimage_apply_transparency(): @pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage_blank(mode): +def test_photoimage_blank(mode) -> None: # test a image using mode/size: im_tk = ImageTk.PhotoImage(mode, (100, 100)) @@ -91,7 +91,7 @@ def test_photoimage_blank(mode): assert_image_equal(reloaded.convert(mode), im) -def test_bitmapimage(): +def test_bitmapimage() -> None: im = hopper("1") # this should not crash diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index f93eabcb4ff..b43c31b521f 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -8,10 +8,10 @@ class TestImageWin: - def test_sanity(self): + def test_sanity(self) -> None: dir(ImageWin) - def test_hdc(self): + def test_hdc(self) -> None: # Arrange dc = 50 @@ -22,7 +22,7 @@ def test_hdc(self): # Assert assert dc2 == 50 - def test_hwnd(self): + def test_hwnd(self) -> None: # Arrange wnd = 50 @@ -36,7 +36,7 @@ def test_hwnd(self): @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestImageWinDib: - def test_dib_image(self): + def test_dib_image(self) -> None: # Arrange im = hopper() @@ -46,7 +46,7 @@ def test_dib_image(self): # Assert assert dib.size == im.size - def test_dib_mode_string(self): + def test_dib_mode_string(self) -> None: # Arrange mode = "RGBA" size = (128, 128) @@ -57,7 +57,7 @@ def test_dib_mode_string(self): # Assert assert dib.size == (128, 128) - def test_dib_paste(self): + def test_dib_paste(self) -> None: # Arrange im = hopper() @@ -71,7 +71,7 @@ def test_dib_paste(self): # Assert assert dib.size == (128, 128) - def test_dib_paste_bbox(self): + def test_dib_paste_bbox(self) -> None: # Arrange im = hopper() bbox = (0, 0, 10, 10) @@ -86,7 +86,7 @@ def test_dib_paste_bbox(self): # Assert assert dib.size == (128, 128) - def test_dib_frombytes_tobytes_roundtrip(self): + def test_dib_frombytes_tobytes_roundtrip(self) -> None: # Arrange # Make two different DIB images im = hopper() diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index 63d6b903ca6..c7f633e6290 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from pathlib import Path from PIL import Image, ImageWin @@ -83,7 +84,7 @@ def serialize_dib(bi, pixels): memcpy(bp + bf.bfOffBits, pixels, bi.biSizeImage) return bytearray(buf) - def test_pointer(tmp_path): + def test_pointer(tmp_path: Path) -> None: im = hopper() (width, height) = im.size opath = str(tmp_path / "temp.png") diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index e2024abbf2e..c8d6d33d223 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -10,7 +10,7 @@ class TestLibPack: - def assert_pack(self, mode, rawmode, data, *pixels): + def assert_pack(self, mode, rawmode, data, *pixels) -> None: """ data - either raw bytes with data or just number of bytes in rawmode. """ @@ -24,7 +24,7 @@ def assert_pack(self, mode, rawmode, data, *pixels): assert data == im.tobytes("raw", rawmode) - def test_1(self): + def test_1(self) -> None: self.assert_pack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) self.assert_pack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) self.assert_pack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) @@ -37,29 +37,29 @@ def test_1(self): self.assert_pack("1", "L", b"\xff\x00\x00\xff\x00\x00", X, 0, 0, X, 0, 0) - def test_L(self): + def test_L(self) -> None: self.assert_pack("L", "L", 1, 1, 2, 3, 4) self.assert_pack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) self.assert_pack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) - def test_LA(self): + def test_LA(self) -> None: self.assert_pack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) self.assert_pack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_La(self): + def test_La(self) -> None: self.assert_pack("La", "La", 2, (1, 2), (3, 4), (5, 6)) - def test_P(self): + def test_P(self) -> None: self.assert_pack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 255, 0, 0) self.assert_pack("P", "P;2", b"\xe4", 3, 2, 1, 0) self.assert_pack("P", "P;4", b"\x02\xef", 0, 2, 14, 15) self.assert_pack("P", "P", 1, 1, 2, 3, 4) - def test_PA(self): + def test_PA(self) -> None: self.assert_pack("PA", "PA", 2, (1, 2), (3, 4), (5, 6)) self.assert_pack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_RGB(self): + def test_RGB(self) -> None: self.assert_pack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_pack( "RGB", "RGBX", b"\x01\x02\x03\xff\x05\x06\x07\xff", (1, 2, 3), (5, 6, 7) @@ -79,7 +79,7 @@ def test_RGB(self): self.assert_pack("RGB", "G", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("RGB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - def test_RGBA(self): + def test_RGBA(self) -> None: self.assert_pack("RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) @@ -101,12 +101,12 @@ def test_RGBA(self): self.assert_pack("RGBA", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) self.assert_pack("RGBA", "A", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - def test_RGBa(self): + def test_RGBa(self) -> None: self.assert_pack("RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack("RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) self.assert_pack("RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) - def test_RGBX(self): + def test_RGBX(self) -> None: self.assert_pack("RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) @@ -134,7 +134,7 @@ def test_RGBX(self): self.assert_pack("RGBX", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) self.assert_pack("RGBX", "X", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - def test_CMYK(self): + def test_CMYK(self) -> None: self.assert_pack("CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( "CMYK", @@ -149,7 +149,7 @@ def test_CMYK(self): ) self.assert_pack("CMYK", "K", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - def test_YCbCr(self): + def test_YCbCr(self) -> None: self.assert_pack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_pack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_pack( @@ -172,19 +172,19 @@ def test_YCbCr(self): self.assert_pack("YCbCr", "Cb", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) self.assert_pack("YCbCr", "Cr", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) - def test_LAB(self): + def test_LAB(self) -> None: self.assert_pack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) self.assert_pack("LAB", "L", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) self.assert_pack("LAB", "A", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("LAB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - def test_HSV(self): + def test_HSV(self) -> None: self.assert_pack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_pack("HSV", "H", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) self.assert_pack("HSV", "S", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("HSV", "V", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - def test_I(self): + def test_I(self) -> None: self.assert_pack("I", "I;16B", 2, 0x0102, 0x0304) self.assert_pack( "I", "I;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 0x01000083, -2097151999 @@ -209,10 +209,10 @@ def test_I(self): 0x01000083, ) - def test_I16(self): + def test_I16(self) -> None: self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) - def test_F_float(self): + def test_F_float(self) -> None: self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) if sys.byteorder == "little": @@ -228,7 +228,7 @@ def test_F_float(self): class TestLibUnpack: - def assert_unpack(self, mode, rawmode, data, *pixels): + def assert_unpack(self, mode, rawmode, data, *pixels) -> None: """ data - either raw bytes with data or just number of bytes in rawmode. """ @@ -241,7 +241,7 @@ def assert_unpack(self, mode, rawmode, data, *pixels): for x, pixel in enumerate(pixels): assert pixel == im.getpixel((x, 0)) - def test_1(self): + def test_1(self) -> None: self.assert_unpack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) self.assert_unpack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) self.assert_unpack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) @@ -254,7 +254,7 @@ def test_1(self): self.assert_unpack("1", "1;8", b"\x00\x01\x02\xff", 0, X, X, X) - def test_L(self): + def test_L(self) -> None: self.assert_unpack("L", "L;2", b"\xe4", 255, 170, 85, 0) self.assert_unpack("L", "L;2I", b"\xe4", 0, 85, 170, 255) self.assert_unpack("L", "L;2R", b"\xe4", 0, 170, 85, 255) @@ -273,14 +273,14 @@ def test_L(self): self.assert_unpack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) self.assert_unpack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) - def test_LA(self): + def test_LA(self) -> None: self.assert_unpack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) self.assert_unpack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_La(self): + def test_La(self) -> None: self.assert_unpack("La", "La", 2, (1, 2), (3, 4), (5, 6)) - def test_P(self): + def test_P(self) -> None: self.assert_unpack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 1, 0, 0) self.assert_unpack("P", "P;2", b"\xe4", 3, 2, 1, 0) # erroneous? @@ -291,11 +291,11 @@ def test_P(self): self.assert_unpack("P", "P", 1, 1, 2, 3, 4) self.assert_unpack("P", "P;R", 1, 128, 64, 192, 32) - def test_PA(self): + def test_PA(self) -> None: self.assert_unpack("PA", "PA", 2, (1, 2), (3, 4), (5, 6)) self.assert_unpack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_RGB(self): + def test_RGB(self) -> None: self.assert_unpack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_unpack("RGB", "RGB;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_unpack("RGB", "RGB;R", 3, (128, 64, 192), (32, 160, 96)) @@ -346,14 +346,14 @@ def test_RGB(self): "RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233) ) - def test_BGR(self): + def test_BGR(self) -> None: self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8)) self.assert_unpack( "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) ) self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - def test_RGBA(self): + def test_RGBA(self) -> None: self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) self.assert_unpack( "RGBA", "LA;16B", 4, (1, 1, 1, 3), (5, 5, 5, 7), (9, 9, 9, 11) @@ -522,7 +522,7 @@ def test_RGBA(self): "RGBA", "A;16N", 2, (0, 0, 0, 1), (0, 0, 0, 3), (0, 0, 0, 5) ) - def test_RGBa(self): + def test_RGBa(self) -> None: self.assert_unpack( "RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) ) @@ -536,7 +536,7 @@ def test_RGBa(self): "RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9) ) - def test_RGBX(self): + def test_RGBX(self) -> None: self.assert_unpack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) self.assert_unpack("RGBX", "RGB;L", 3, (1, 4, 7, X), (2, 5, 8, X), (3, 6, 9, X)) self.assert_unpack("RGBX", "RGB;16B", 6, (1, 3, 5, X), (7, 9, 11, X)) @@ -581,7 +581,7 @@ def test_RGBX(self): self.assert_unpack("RGBX", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) self.assert_unpack("RGBX", "X", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) - def test_CMYK(self): + def test_CMYK(self) -> None: self.assert_unpack( "CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) ) @@ -619,25 +619,25 @@ def test_CMYK(self): "CMYK", "K;I", 1, (0, 0, 0, 254), (0, 0, 0, 253), (0, 0, 0, 252) ) - def test_YCbCr(self): + def test_YCbCr(self) -> None: self.assert_unpack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_unpack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_unpack("YCbCr", "YCbCrK", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) self.assert_unpack("YCbCr", "YCbCrX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) - def test_LAB(self): + def test_LAB(self) -> None: self.assert_unpack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) self.assert_unpack("LAB", "L", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) self.assert_unpack("LAB", "A", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) self.assert_unpack("LAB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) - def test_HSV(self): + def test_HSV(self) -> None: self.assert_unpack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_unpack("HSV", "H", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) self.assert_unpack("HSV", "S", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) self.assert_unpack("HSV", "V", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) - def test_I(self): + def test_I(self) -> None: self.assert_unpack("I", "I;8", 1, 0x01, 0x02, 0x03, 0x04) self.assert_unpack("I", "I;8S", b"\x01\x83", 1, -125) self.assert_unpack("I", "I;16", 2, 0x0201, 0x0403) @@ -678,7 +678,7 @@ def test_I(self): 0x01000083, ) - def test_F_int(self): + def test_F_int(self) -> None: self.assert_unpack("F", "F;8", 1, 0x01, 0x02, 0x03, 0x04) self.assert_unpack("F", "F;8S", b"\x01\x83", 1, -125) self.assert_unpack("F", "F;16", 2, 0x0201, 0x0403) @@ -717,7 +717,7 @@ def test_F_int(self): 16777348, ) - def test_F_float(self): + def test_F_float(self) -> None: self.assert_unpack( "F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34 ) @@ -768,7 +768,7 @@ def test_F_float(self): -1234.5, ) - def test_I16(self): + def test_I16(self) -> None: self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506) @@ -785,7 +785,7 @@ def test_I16(self): self.assert_unpack("I;16L", "I;16N", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) - def test_CMYK16(self): + def test_CMYK16(self) -> None: self.assert_unpack("CMYK", "CMYK;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) self.assert_unpack("CMYK", "CMYK;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) if sys.byteorder == "little": @@ -793,7 +793,7 @@ def test_CMYK16(self): else: self.assert_unpack("CMYK", "CMYK;16N", 8, (1, 3, 5, 7), (9, 11, 13, 15)) - def test_value_error(self): + def test_value_error(self) -> None: with pytest.raises(ValueError): self.assert_unpack("L", "L", 0, 0) with pytest.raises(ValueError): diff --git a/Tests/test_map.py b/Tests/test_map.py index 9c79fe35906..93140f6e5a5 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -7,7 +7,7 @@ from PIL import Image -def test_overflow(): +def test_overflow() -> None: # There is the potential to overflow comparisons in map.c # if there are > SIZE_MAX bytes in the image or if # the file encodes an offset that makes @@ -25,7 +25,7 @@ def test_overflow(): Image.MAX_IMAGE_PIXELS = max_pixels -def test_tobytes(): +def test_tobytes() -> None: # Note that this image triggers the decompression bomb warning: max_pixels = Image.MAX_IMAGE_PIXELS Image.MAX_IMAGE_PIXELS = None @@ -39,7 +39,7 @@ def test_tobytes(): @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") -def test_ysize(): +def test_ysize() -> None: numpy = pytest.importorskip("numpy", reason="NumPy not installed") # Should not raise 'Integer overflow in ysize' diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index d3ee511b7c2..f2540bb465e 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -9,7 +11,7 @@ original = hopper().resize((32, 32)).convert("I") -def verify(im1): +def verify(im1) -> None: im2 = original.copy() assert im1.size == im2.size pix1 = im1.load() @@ -25,7 +27,7 @@ def verify(im1): @pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I")) -def test_basic(tmp_path, mode): +def test_basic(tmp_path: Path, mode) -> None: # PIL 1.1 has limited support for 16-bit image data. Check that # create/copy/transform and save works as expected. @@ -75,7 +77,7 @@ def test_basic(tmp_path, mode): assert im_in.getpixel((0, 0)) == min(512, maximum) -def test_tobytes(): +def test_tobytes() -> None: def tobytes(mode): return Image.new(mode, (1, 1), 1).tobytes() @@ -87,7 +89,7 @@ def tobytes(mode): assert tobytes("I") == b"\x01\x00\x00\x00"[::order] -def test_convert(): +def test_convert() -> None: im = original.copy() for mode in ("I;16", "I;16B", "I;16N"): diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 24dff36a610..6ba95c2d700 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -13,8 +13,8 @@ TEST_IMAGE_SIZE = (10, 10) -def test_numpy_to_image(): - def to_image(dtype, bands=1, boolean=0): +def test_numpy_to_image() -> None: + def to_image(dtype, bands: int = 1, boolean: int = 0): if bands == 1: if boolean: data = [0, 255] * 50 @@ -82,7 +82,7 @@ def to_image(dtype, bands=1, boolean=0): # Based on an erring example at # https://stackoverflow.com/questions/10854903/what-is-causing-dimension-dependent-attributeerror-in-pil-fromarray-function -def test_3d_array(): +def test_3d_array() -> None: size = (5, TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1]) a = numpy.ones(size, dtype=numpy.uint8) assert_image(Image.fromarray(a[1, :, :]), "L", TEST_IMAGE_SIZE) @@ -94,12 +94,12 @@ def test_3d_array(): assert_image(Image.fromarray(a[:, :, 1]), "L", TEST_IMAGE_SIZE) -def test_1d_array(): +def test_1d_array() -> None: a = numpy.ones(5, dtype=numpy.uint8) assert_image(Image.fromarray(a), "L", (1, 5)) -def _test_img_equals_nparray(img, np): +def _test_img_equals_nparray(img, np) -> None: assert len(np.shape) >= 2 np_size = np.shape[1], np.shape[0] assert img.size == np_size @@ -109,14 +109,14 @@ def _test_img_equals_nparray(img, np): assert_deep_equal(px[x, y], np[y, x]) -def test_16bit(): +def test_16bit() -> None: with Image.open("Tests/images/16bit.cropped.tif") as img: np_img = numpy.array(img) _test_img_equals_nparray(img, np_img) assert np_img.dtype == numpy.dtype(" None: # Test that 1-bit arrays convert to numpy and back # See: https://github.com/python-pillow/Pillow/issues/350 arr = numpy.array([[1, 0, 0, 1, 0], [0, 1, 0, 0, 0]], "u1") @@ -126,7 +126,7 @@ def test_1bit(): numpy.testing.assert_array_equal(arr, arr_back) -def test_save_tiff_uint16(): +def test_save_tiff_uint16() -> None: # Tests that we're getting the pixel value in the right byte order. pixel_value = 0x1234 a = numpy.array( @@ -157,7 +157,7 @@ def test_save_tiff_uint16(): ("HSV", numpy.uint8), ), ) -def test_to_array(mode, dtype): +def test_to_array(mode, dtype) -> None: img = hopper(mode) # Resize to non-square @@ -169,7 +169,7 @@ def test_to_array(mode, dtype): assert np_img.dtype == dtype -def test_point_lut(): +def test_point_lut() -> None: # See https://github.com/python-pillow/Pillow/issues/439 data = list(range(256)) * 3 @@ -180,7 +180,7 @@ def test_point_lut(): im.point(lut) -def test_putdata(): +def test_putdata() -> None: # Shouldn't segfault # See https://github.com/python-pillow/Pillow/issues/1008 @@ -207,12 +207,12 @@ def test_putdata(): numpy.float64, ), ) -def test_roundtrip_eye(dtype): +def test_roundtrip_eye(dtype) -> None: arr = numpy.eye(10, dtype=dtype) numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr))) -def test_zero_size(): +def test_zero_size() -> None: # Shouldn't cause floating point exception # See https://github.com/python-pillow/Pillow/issues/2259 @@ -222,13 +222,13 @@ def test_zero_size(): @skip_unless_feature("libtiff") -def test_load_first(): +def test_load_first() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: a = numpy.array(im) assert a.shape == (88, 590) -def test_bool(): +def test_bool() -> None: # https://github.com/python-pillow/Pillow/issues/2044 a = numpy.zeros((10, 2), dtype=bool) a[0][0] = True @@ -237,7 +237,7 @@ def test_bool(): assert im2.getdata()[0] == 255 -def test_no_resource_warning_for_numpy_array(): +def test_no_resource_warning_for_numpy_array() -> None: # https://github.com/python-pillow/Pillow/issues/835 # Arrange from numpy import array diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index a89d75b59d3..f6b12cb2047 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -19,14 +19,14 @@ ) -def test_text_encode_decode(): +def test_text_encode_decode() -> None: assert encode_text("abc") == b"\xFE\xFF\x00a\x00b\x00c" assert decode_text(b"\xFE\xFF\x00a\x00b\x00c") == "abc" assert decode_text(b"abc") == "abc" assert decode_text(b"\x1B a \x1C") == "\u02D9 a \u02DD" -def test_indirect_refs(): +def test_indirect_refs() -> None: assert IndirectReference(1, 2) == IndirectReference(1, 2) assert IndirectReference(1, 2) != IndirectReference(1, 3) assert IndirectReference(1, 2) != IndirectObjectDef(1, 2) @@ -37,7 +37,7 @@ def test_indirect_refs(): assert IndirectObjectDef(1, 2) != (1, 2) -def test_parsing(): +def test_parsing() -> None: assert PdfParser.interpret_name(b"Name#23Hash") == b"Name#Hash" assert PdfParser.interpret_name(b"Name#23Hash", as_text=True) == "Name#Hash" assert PdfParser.get_value(b"1 2 R ", 0) == (IndirectReference(1, 2), 5) @@ -95,7 +95,7 @@ def test_parsing(): assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value -def test_pdf_repr(): +def test_pdf_repr() -> None: assert bytes(IndirectReference(1, 2)) == b"1 2 R" assert bytes(IndirectObjectDef(*IndirectReference(1, 2))) == b"1 2 obj" assert bytes(PdfName(b"Name#Hash")) == b"/Name#23Hash" @@ -121,7 +121,7 @@ def test_pdf_repr(): assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" -def test_duplicate_xref_entry(): +def test_duplicate_xref_entry() -> None: pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf") assert pdf.xref_table.existing_entries[6][0] == 1197 pdf.close() diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index c445e349447..560cdbd35f7 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -1,6 +1,7 @@ from __future__ import annotations import pickle +from pathlib import Path import pytest @@ -12,7 +13,7 @@ FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" -def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode): +def helper_pickle_file(tmp_path: Path, pickle, protocol, test_file, mode) -> None: # Arrange with Image.open(test_file) as im: filename = str(tmp_path / "temp.pkl") @@ -29,7 +30,7 @@ def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode): assert im == loaded_im -def helper_pickle_string(pickle, protocol, test_file, mode): +def helper_pickle_string(pickle, protocol, test_file, mode) -> None: with Image.open(test_file) as im: if mode: im = im.convert(mode) @@ -63,13 +64,13 @@ def helper_pickle_string(pickle, protocol, test_file, mode): ], ) @pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1)) -def test_pickle_image(tmp_path, test_file, test_mode, protocol): +def test_pickle_image(tmp_path: Path, test_file, test_mode, protocol) -> None: # Act / Assert helper_pickle_string(pickle, protocol, test_file, test_mode) helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode) -def test_pickle_la_mode_with_palette(tmp_path): +def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: # Arrange filename = str(tmp_path / "temp.pkl") with Image.open("Tests/images/hopper.jpg") as im: @@ -88,7 +89,7 @@ def test_pickle_la_mode_with_palette(tmp_path): @skip_unless_feature("webp") -def test_pickle_tell(): +def test_pickle_tell() -> None: # Arrange with Image.open("Tests/images/hopper.webp") as image: # Act: roundtrip @@ -98,7 +99,7 @@ def test_pickle_tell(): assert unpickled_image.tell() == 0 -def helper_assert_pickled_font_images(font1, font2): +def helper_assert_pickled_font_images(font1, font2) -> None: # Arrange im1 = Image.new(mode="RGBA", size=(300, 100)) im2 = Image.new(mode="RGBA", size=(300, 100)) @@ -116,7 +117,7 @@ def helper_assert_pickled_font_images(font1, font2): @skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) -def test_pickle_font_string(protocol): +def test_pickle_font_string(protocol) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -130,7 +131,7 @@ def test_pickle_font_string(protocol): @skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) -def test_pickle_font_file(tmp_path, protocol): +def test_pickle_font_file(tmp_path: Path, protocol) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) filename = str(tmp_path / "temp.pkl") diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 7f618d0f53b..797539f35ef 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -3,13 +3,14 @@ import os import sys from io import BytesIO +from pathlib import Path import pytest from PIL import Image, PSDraw -def _create_document(ps): +def _create_document(ps) -> None: title = "hopper" box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points @@ -31,7 +32,7 @@ def _create_document(ps): ps.end_document() -def test_draw_postscript(tmp_path): +def test_draw_postscript(tmp_path: Path) -> None: # Based on Pillow tutorial, but there is no textsize: # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript @@ -49,7 +50,7 @@ def test_draw_postscript(tmp_path): @pytest.mark.parametrize("buffer", (True, False)) -def test_stdout(buffer): +def test_stdout(buffer) -> None: # Temporarily redirect stdout old_stdout = sys.stdout diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index ad2b5ad9bf5..7d6c0a8cb7d 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import ImageQt @@ -19,7 +21,7 @@ from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget class Example(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() img = hopper().resize((1000, 1000)) @@ -35,14 +37,14 @@ def __init__(self): lbl.setPixmap(pixmap1.copy()) -def roundtrip(expected): +def roundtrip(expected) -> None: result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb assert_image_similar(result, expected.convert("RGB"), 1) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: # Segfault test app = QApplication([]) ex = Example() diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index b26787ce668..a222a7d71b9 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import ImageQt @@ -15,7 +17,7 @@ @pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) -def test_sanity(mode, tmp_path): +def test_sanity(mode, tmp_path: Path) -> None: src = hopper(mode) data = ImageQt.toqimage(src) diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index dee6258ecb5..9442801d05c 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -21,7 +21,7 @@ "Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi", ], ) -def test_crashes(test_file): +def test_crashes(test_file) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 9f3e86a32a8..3db0660eab7 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,6 +1,7 @@ from __future__ import annotations import shutil +from pathlib import Path import pytest @@ -16,7 +17,7 @@ @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") class TestShellInjection: - def assert_save_filename_check(self, tmp_path, src_img, save_func): + def assert_save_filename_check(self, tmp_path: Path, src_img, save_func) -> None: for filename in test_filenames: dest_file = str(tmp_path / filename) save_func(src_img, 0, dest_file) @@ -25,7 +26,7 @@ def assert_save_filename_check(self, tmp_path, src_img, save_func): im.load() @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") - def test_load_djpeg_filename(self, tmp_path): + def test_load_djpeg_filename(self, tmp_path: Path) -> None: for filename in test_filenames: src_file = str(tmp_path / filename) shutil.copy(TEST_JPG, src_file) @@ -34,18 +35,18 @@ def test_load_djpeg_filename(self, tmp_path): im.load_djpeg() @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") - def test_save_cjpeg_filename(self, tmp_path): + def test_save_cjpeg_filename(self, tmp_path: Path) -> None: with Image.open(TEST_JPG) as im: self.assert_save_filename_check(tmp_path, im, JpegImagePlugin._save_cjpeg) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") - def test_save_netpbm_filename_bmp_mode(self, tmp_path): + def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: im = im.convert("RGB") self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") - def test_save_netpbm_filename_l_mode(self, tmp_path): + def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: im = im.convert("L") self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index c07e7f7d395..5368545232b 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,6 +1,7 @@ from __future__ import annotations from fractions import Fraction +from pathlib import Path from PIL import Image, TiffImagePlugin, features from PIL.TiffImagePlugin import IFDRational @@ -8,14 +9,14 @@ from .helper import hopper -def _test_equal(num, denom, target): +def _test_equal(num, denom, target) -> None: t = IFDRational(num, denom) assert target == t assert t == target -def test_sanity(): +def test_sanity() -> None: _test_equal(1, 1, 1) _test_equal(1, 1, Fraction(1, 1)) @@ -31,13 +32,13 @@ def test_sanity(): _test_equal(7, 5, 1.4) -def test_ranges(): +def test_ranges() -> None: for num in range(1, 10): for denom in range(1, 10): assert IFDRational(num, denom) == IFDRational(num, denom) -def test_nonetype(): +def test_nonetype() -> None: # Fails if the _delegate function doesn't return a valid function xres = IFDRational(72) @@ -51,7 +52,7 @@ def test_nonetype(): assert xres and yres -def test_ifd_rational_save(tmp_path): +def test_ifd_rational_save(tmp_path: Path) -> None: methods = (True, False) if not features.check("libtiff"): methods = (False,) From bb1fece57a2c894773597c9a6fb10bd81e36123d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 31 Jan 2024 21:55:32 +1100 Subject: [PATCH 0149/2374] Added type hints --- Tests/test_bmp_reference.py | 6 +- Tests/test_box_blur.py | 12 +- Tests/test_file_apng.py | 6 +- Tests/test_file_container.py | 10 +- Tests/test_file_gif.py | 30 ++-- Tests/test_file_mpo.py | 19 +-- Tests/test_file_ppm.py | 22 +-- Tests/test_file_sgi.py | 2 +- Tests/test_image_frombytes.py | 2 +- Tests/test_image_load.py | 2 +- Tests/test_imagedraw.py | 291 ++++++++++++++++++++++++++++------ Tests/test_imagetk.py | 4 +- Tests/test_lib_pack.py | 16 +- Tests/test_mode_i16.py | 6 +- 14 files changed, 327 insertions(+), 101 deletions(-) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 22ac9443e86..0ad49613553 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -10,7 +10,7 @@ base = os.path.join("Tests", "images", "bmp") -def get_files(d, ext: str = ".bmp"): +def get_files(d: str, ext: str = ".bmp") -> list[str]: return [ os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f ] @@ -29,7 +29,7 @@ def test_bad() -> None: pass -def test_questionable(): +def test_questionable() -> None: """These shouldn't crash/dos, but it's not well defined that these are in spec""" supported = [ @@ -80,7 +80,7 @@ def test_good() -> None: "rgb32bf.bmp": "rgb24.png", } - def get_compare(f): + def get_compare(f: str) -> str: name = os.path.split(f)[1] if name in file_map: return os.path.join(base, "html", file_map[name]) diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index dfedb48d911..1f6ed61277a 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -23,11 +23,11 @@ def test_imageops_box_blur() -> None: assert isinstance(i, Image.Image) -def box_blur(image, radius: int = 1, n: int = 1): +def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image: return image._new(image.im.box_blur((radius, radius), n)) -def assert_image(im, data, delta: int = 0) -> None: +def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None: it = iter(im.getdata()) for data_row in data: im_row = [next(it) for _ in range(im.size[0])] @@ -37,7 +37,13 @@ def assert_image(im, data, delta: int = 0) -> None: next(it) -def assert_blur(im, radius, data, passes: int = 1, delta: int = 0) -> None: +def assert_blur( + im: Image.Image, + radius: float, + data: list[list[int]], + passes: int = 1, + delta: int = 0, +) -> None: # check grayscale image assert_image(box_blur(im, radius, passes), data, delta) rgba = Image.merge("RGBA", (im, im, im, im)) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index f9edf6e9877..395165b3657 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -47,7 +47,7 @@ def test_apng_basic() -> None: "filename", ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"), ) -def test_apng_fdat(filename) -> None: +def test_apng_fdat(filename: str) -> None: with Image.open(filename) as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -338,7 +338,7 @@ def test_apng_syntax_errors() -> None: "sequence_fdat_fctl.png", ), ) -def test_apng_sequence_errors(test_file) -> None: +def test_apng_sequence_errors(test_file: str) -> None: with pytest.raises(SyntaxError): with Image.open(f"Tests/images/apng/{test_file}") as im: im.seek(im.n_frames - 1) @@ -681,7 +681,7 @@ def test_seek_after_close() -> None: @pytest.mark.parametrize("default_image", (True, False)) @pytest.mark.parametrize("duplicate", (True, False)) def test_different_modes_in_later_frames( - mode, default_image, duplicate, tmp_path: Path + mode: str, default_image: bool, duplicate: bool, tmp_path: Path ) -> None: test_file = str(tmp_path / "temp.png") diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 4dba4be5d5c..813b444dbcf 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -64,7 +64,7 @@ def test_seek_mode_2() -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n0(bytesmode) -> None: +def test_read_n0(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -80,7 +80,7 @@ def test_read_n0(bytesmode) -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n(bytesmode) -> None: +def test_read_n(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -96,7 +96,7 @@ def test_read_n(bytesmode) -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_eof(bytesmode) -> None: +def test_read_eof(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -112,7 +112,7 @@ def test_read_eof(bytesmode) -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_readline(bytesmode) -> None: +def test_readline(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 0, 120) @@ -127,7 +127,7 @@ def test_readline(bytesmode) -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_readlines(bytesmode) -> None: +def test_readlines(bytesmode: bool) -> None: # Arrange expected = [ "This is line 1\n", diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 3f550fd1109..db9d3586c4d 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -3,6 +3,7 @@ import warnings from io import BytesIO from pathlib import Path +from typing import Generator import pytest @@ -144,13 +145,13 @@ def test_strategy() -> None: def test_optimize() -> None: - def test_grayscale(optimize): + def test_grayscale(optimize: int) -> int: im = Image.new("L", (1, 1), 0) filename = BytesIO() im.save(filename, "GIF", optimize=optimize) return len(filename.getvalue()) - def test_bilevel(optimize): + def test_bilevel(optimize: int) -> int: im = Image.new("1", (1, 1), 0) test_file = BytesIO() im.save(test_file, "GIF", optimize=optimize) @@ -178,7 +179,9 @@ def test_bilevel(optimize): (4, 513, 256), ), ) -def test_optimize_correctness(colors, size, expected_palette_length) -> None: +def test_optimize_correctness( + colors: int, size: int, expected_palette_length: int +) -> None: # 256 color Palette image, posterize to > 128 and < 128 levels. # Size bigger and smaller than 512x512. # Check the palette for number of colors allocated. @@ -297,7 +300,7 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None: ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"), ), ) -def test_loading_multiple_palettes(path, mode) -> None: +def test_loading_multiple_palettes(path: str, mode: str) -> None: with Image.open(path) as im: assert im.mode == "P" first_frame_colors = im.palette.colors.keys() @@ -347,9 +350,9 @@ def test_palette_handling(tmp_path: Path) -> None: def test_palette_434(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/434 - def roundtrip(im, *args, **kwargs): + def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: out = str(tmp_path / "temp.gif") - im.copy().save(out, *args, **kwargs) + im.copy().save(out, **kwargs) reloaded = Image.open(out) return reloaded @@ -429,7 +432,7 @@ def test_seek_rewind() -> None: ("Tests/images/iss634.gif", 42), ), ) -def test_n_frames(path, n_frames) -> None: +def test_n_frames(path: str, n_frames: int) -> None: # Test is_animated before n_frames with Image.open(path) as im: assert im.is_animated == (n_frames != 1) @@ -541,7 +544,10 @@ def test_dispose_background_transparency() -> None: ), ), ) -def test_transparent_dispose(loading_strategy, expected_colors) -> None: +def test_transparent_dispose( + loading_strategy: GifImagePlugin.LoadingStrategy, + expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]], +) -> None: GifImagePlugin.LOADING_STRATEGY = loading_strategy try: with Image.open("Tests/images/transparent_dispose.gif") as img: @@ -889,7 +895,9 @@ def test_identical_frames(tmp_path: Path) -> None: 1500, ), ) -def test_identical_frames_to_single_frame(duration, tmp_path: Path) -> None: +def test_identical_frames_to_single_frame( + duration: int | list[int], tmp_path: Path +) -> None: out = str(tmp_path / "temp.gif") im_list = [ Image.new("L", (100, 100), "#000"), @@ -1049,7 +1057,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: def test_version(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") - def assert_version_after_save(im, version) -> None: + def assert_version_after_save(im: Image.Image, version: bytes) -> None: im.save(out) with Image.open(out) as reread: assert reread.info["version"] == version @@ -1088,7 +1096,7 @@ def test_append_images(tmp_path: Path) -> None: assert reread.n_frames == 3 # Tests appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims im.save(out, save_all=True, append_images=im_generator(ims)) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 55b04a1e076..4fb00d6994a 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -2,6 +2,7 @@ import warnings from io import BytesIO +from typing import Any import pytest @@ -19,7 +20,7 @@ pytestmark = skip_unless_feature("jpg") -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "MPO", **options) test_bytes = out.tell() @@ -30,7 +31,7 @@ def roundtrip(im, **options): @pytest.mark.parametrize("test_file", test_files) -def test_sanity(test_file) -> None: +def test_sanity(test_file: str) -> None: with Image.open(test_file) as im: im.load() assert im.mode == "RGB" @@ -70,7 +71,7 @@ def test_context_manager() -> None: @pytest.mark.parametrize("test_file", test_files) -def test_app(test_file) -> None: +def test_app(test_file: str) -> None: # Test APP/COM reader (@PIL135) with Image.open(test_file) as im: assert im.applist[0][0] == "APP1" @@ -82,7 +83,7 @@ def test_app(test_file) -> None: @pytest.mark.parametrize("test_file", test_files) -def test_exif(test_file) -> None: +def test_exif(test_file: str) -> None: with Image.open(test_file) as im_original: im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) @@ -143,7 +144,7 @@ def test_reload_exif_after_seek() -> None: @pytest.mark.parametrize("test_file", test_files) -def test_mp(test_file) -> None: +def test_mp(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() assert mpinfo[45056] == b"0100" @@ -168,7 +169,7 @@ def test_mp_no_data() -> None: @pytest.mark.parametrize("test_file", test_files) -def test_mp_attribute(test_file) -> None: +def test_mp_attribute(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() for frame_number, mpentry in enumerate(mpinfo[0xB002]): @@ -185,7 +186,7 @@ def test_mp_attribute(test_file) -> None: @pytest.mark.parametrize("test_file", test_files) -def test_seek(test_file) -> None: +def test_seek(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 # prior to first image raises an error, both blatant and borderline @@ -229,7 +230,7 @@ def test_eoferror() -> None: @pytest.mark.parametrize("test_file", test_files) -def test_image_grab(test_file) -> None: +def test_image_grab(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 im0 = im.tobytes() @@ -244,7 +245,7 @@ def test_image_grab(test_file) -> None: @pytest.mark.parametrize("test_file", test_files) -def test_save(test_file) -> None: +def test_save(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 jpg0 = roundtrip(im) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 94f66ee7d28..6e0fa32e49f 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -70,7 +70,9 @@ def test_sanity() -> None: ), ), ) -def test_arbitrary_maxval(data, mode, pixels) -> None: +def test_arbitrary_maxval( + data: bytes, mode: str, pixels: tuple[int | tuple[int, int, int], ...] +) -> None: fp = BytesIO(data) with Image.open(fp) as im: assert im.size == (3, 1) @@ -139,7 +141,7 @@ def test_pfm_big_endian(tmp_path: Path) -> None: b"Pf 1 1 -0.0 \0\0\0\0", ], ) -def test_pfm_invalid(data) -> None: +def test_pfm_invalid(data: bytes) -> None: with pytest.raises(ValueError): with Image.open(BytesIO(data)): pass @@ -162,7 +164,7 @@ def test_pfm_invalid(data) -> None: ), ), ) -def test_plain(plain_path, raw_path) -> None: +def test_plain(plain_path: str, raw_path: str) -> None: with Image.open(plain_path) as im: assert_image_equal_tofile(im, raw_path) @@ -186,7 +188,9 @@ def test_16bit_plain_pgm() -> None: (b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6), ), ) -def test_plain_data_with_comment(tmp_path: Path, header, data, comment_count) -> None: +def test_plain_data_with_comment( + tmp_path: Path, header: bytes, data: bytes, comment_count: int +) -> None: path1 = str(tmp_path / "temp1.ppm") path2 = str(tmp_path / "temp2.ppm") comment = b"# comment" * comment_count @@ -199,7 +203,7 @@ def test_plain_data_with_comment(tmp_path: Path, header, data, comment_count) -> @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) -def test_plain_truncated_data(tmp_path: Path, data) -> None: +def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -210,7 +214,7 @@ def test_plain_truncated_data(tmp_path: Path, data) -> None: @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) -def test_plain_invalid_data(tmp_path: Path, data) -> None: +def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -227,7 +231,7 @@ def test_plain_invalid_data(tmp_path: Path, data) -> None: b"P3\n128 128\n255\n012345678910 0", # token too long ), ) -def test_plain_ppm_token_too_long(tmp_path: Path, data) -> None: +def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -313,7 +317,7 @@ def test_not_enough_image_data(tmp_path: Path) -> None: @pytest.mark.parametrize("maxval", (b"0", b"65536")) -def test_invalid_maxval(maxval, tmp_path: Path) -> None: +def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n3 1 " + maxval) @@ -351,7 +355,7 @@ def test_mimetypes(tmp_path: Path) -> None: @pytest.mark.parametrize("buffer", (True, False)) -def test_save_stdout(buffer) -> None: +def test_save_stdout(buffer: bool) -> None: old_stdout = sys.stdout if buffer: diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 92aea07350e..e13a8019ed4 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -72,7 +72,7 @@ def test_invalid_file() -> None: def test_write(tmp_path: Path) -> None: - def roundtrip(img) -> None: + def roundtrip(img: Image.Image) -> None: out = str(tmp_path / "temp.sgi") img.save(out, format="sgi") assert_image_equal_tofile(img, out) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 6474daba108..98c0ea0b43c 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -8,7 +8,7 @@ @pytest.mark.parametrize("data_type", ("bytes", "memoryview")) -def test_sanity(data_type) -> None: +def test_sanity(data_type: str) -> None: im1 = hopper() data = im1.tobytes() diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 5b1a9ee2dda..0605821e0ad 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -26,7 +26,7 @@ def test_close() -> None: im.getpixel((0, 0)) -def test_close_after_load(caplog) -> None: +def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None: im = Image.open("Tests/images/hopper.gif") im.load() with caplog.at_level(logging.DEBUG): diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 86d25b1ebc7..c02ac49ddc3 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -74,7 +74,14 @@ def test_mode_mismatch() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) -def test_arc(bbox, start, end) -> None: +def test_arc( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int], + start: float, + end: float, +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -87,7 +94,12 @@ def test_arc(bbox, start, end) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_end_le_start(bbox) -> None: +def test_arc_end_le_start( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -102,7 +114,12 @@ def test_arc_end_le_start(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_no_loops(bbox) -> None: +def test_arc_no_loops( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -118,7 +135,12 @@ def test_arc_no_loops(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width(bbox) -> None: +def test_arc_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -131,7 +153,12 @@ def test_arc_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_pieslice_large(bbox) -> None: +def test_arc_width_pieslice_large( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) @@ -145,7 +172,12 @@ def test_arc_width_pieslice_large(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_fill(bbox) -> None: +def test_arc_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -158,7 +190,12 @@ def test_arc_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_non_whole_angle(bbox) -> None: +def test_arc_width_non_whole_angle( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -200,7 +237,13 @@ def test_bitmap() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_chord(mode, bbox) -> None: +def test_chord( + mode: str, + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int], +) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -214,7 +257,12 @@ def test_chord(mode, bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width(bbox) -> None: +def test_chord_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -227,7 +275,12 @@ def test_chord_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width_fill(bbox) -> None: +def test_chord_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -240,7 +293,12 @@ def test_chord_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_zero_width(bbox) -> None: +def test_chord_zero_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -266,7 +324,13 @@ def test_chord_too_fat() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(mode, bbox) -> None: +def test_ellipse( + mode: str, + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int], +) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -280,7 +344,12 @@ def test_ellipse(mode, bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_translucent(bbox) -> None: +def test_ellipse_translucent( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -317,7 +386,12 @@ def test_ellipse_symmetric() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width(bbox) -> None: +def test_ellipse_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -342,7 +416,12 @@ def test_ellipse_width_large() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width_fill(bbox) -> None: +def test_ellipse_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -355,7 +434,12 @@ def test_ellipse_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_zero_width(bbox) -> None: +def test_ellipse_zero_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -367,7 +451,7 @@ def test_ellipse_zero_width(bbox) -> None: assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png") -def ellipse_various_sizes_helper(filled): +def ellipse_various_sizes_helper(filled: bool) -> Image.Image: ellipse_sizes = range(32) image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1 im = Image.new("RGB", (image_size, image_size)) @@ -409,7 +493,12 @@ def test_ellipse_various_sizes_filled() -> None: @pytest.mark.parametrize("points", POINTS) -def test_line(points) -> None: +def test_line( + points: tuple[tuple[int, int], ...] + | list[tuple[int, int]] + | tuple[int, ...] + | list[int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -482,7 +571,14 @@ def test_transform() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) -def test_pieslice(bbox, start, end) -> None: +def test_pieslice( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int], + start: float, + end: float, +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -495,7 +591,12 @@ def test_pieslice(bbox, start, end) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width(bbox) -> None: +def test_pieslice_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -508,7 +609,12 @@ def test_pieslice_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width_fill(bbox) -> None: +def test_pieslice_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -522,7 +628,12 @@ def test_pieslice_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_zero_width(bbox) -> None: +def test_pieslice_zero_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -577,7 +688,12 @@ def test_pieslice_no_spikes() -> None: @pytest.mark.parametrize("points", POINTS) -def test_point(points) -> None: +def test_point( + points: tuple[tuple[int, int], ...] + | list[tuple[int, int]] + | tuple[int, ...] + | list[int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -602,7 +718,12 @@ def test_point_I16() -> None: @pytest.mark.parametrize("points", POINTS) -def test_polygon(points) -> None: +def test_polygon( + points: tuple[tuple[int, int], ...] + | list[tuple[int, int]] + | tuple[int, ...] + | list[int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -616,7 +737,9 @@ def test_polygon(points) -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("kite_points", KITE_POINTS) -def test_polygon_kite(mode, kite_points) -> None: +def test_polygon_kite( + mode: str, kite_points: tuple[tuple[int, int], ...] | list[tuple[int, int]] +) -> None: # Test drawing lines of different gradients (dx>dy, dy>dx) and # vertical (dx==0) and horizontal (dy==0) lines # Arrange @@ -673,7 +796,12 @@ def test_polygon_translucent() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox) -> None: +def test_rectangle( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -700,7 +828,12 @@ def test_big_rectangle() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width(bbox) -> None: +def test_rectangle_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -714,7 +847,12 @@ def test_rectangle_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width_fill(bbox) -> None: +def test_rectangle_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -728,7 +866,12 @@ def test_rectangle_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_zero_width(bbox) -> None: +def test_rectangle_zero_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -741,7 +884,12 @@ def test_rectangle_zero_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16(bbox) -> None: +def test_rectangle_I16( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) @@ -754,7 +902,12 @@ def test_rectangle_I16(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_translucent_outline(bbox) -> None: +def test_rectangle_translucent_outline( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -772,7 +925,11 @@ def test_rectangle_translucent_outline(bbox) -> None: "xy", [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], ) -def test_rounded_rectangle(xy) -> None: +def test_rounded_rectangle( + xy: tuple[int, int, int, int] + | tuple[list[int]] + | tuple[tuple[int, int], tuple[int, int]] +) -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -789,7 +946,7 @@ def test_rounded_rectangle(xy) -> None: @pytest.mark.parametrize("bottom_right", (True, False)) @pytest.mark.parametrize("bottom_left", (True, False)) def test_rounded_rectangle_corners( - top_left, top_right, bottom_right, bottom_left + top_left: bool, top_right: bool, bottom_right: bool, bottom_left: bool ) -> None: corners = (top_left, top_right, bottom_right, bottom_left) @@ -824,7 +981,9 @@ def test_rounded_rectangle_corners( ((10, 20, 190, 181), 85, "height"), ], ) -def test_rounded_rectangle_non_integer_radius(xy, radius, type) -> None: +def test_rounded_rectangle_non_integer_radius( + xy: tuple[int, int, int, int], radius: float, type: str +) -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -840,7 +999,12 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rounded_rectangle_zero_radius(bbox) -> None: +def test_rounded_rectangle_zero_radius( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -862,7 +1026,9 @@ def test_rounded_rectangle_zero_radius(bbox) -> None: ((20, 20, 80, 80), "both"), ], ) -def test_rounded_rectangle_translucent(xy, suffix) -> None: +def test_rounded_rectangle_translucent( + xy: tuple[int, int, int, int], suffix: str +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -879,7 +1045,12 @@ def test_rounded_rectangle_translucent(xy, suffix) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill(bbox) -> None: +def test_floodfill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: @@ -912,7 +1083,12 @@ def test_floodfill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_border(bbox) -> None: +def test_floodfill_border( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # floodfill() is experimental # Arrange @@ -934,7 +1110,12 @@ def test_floodfill_border(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_thresh(bbox) -> None: +def test_floodfill_thresh( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # floodfill() is experimental # Arrange @@ -968,8 +1149,11 @@ def test_floodfill_not_negative() -> None: def create_base_image_draw( - size, mode=DEFAULT_MODE, background1=WHITE, background2=GRAY -): + size: tuple[int, int], + mode: str = DEFAULT_MODE, + background1: tuple[int, int, int] = WHITE, + background2: tuple[int, int, int] = GRAY, +) -> tuple[Image.Image, ImageDraw.ImageDraw]: img = Image.new(mode, size, background1) for x in range(0, size[0]): for y in range(0, size[1]): @@ -1003,7 +1187,7 @@ def test_triangle_right() -> None: "fill, suffix", ((BLACK, "width"), (None, "width_no_fill")), ) -def test_triangle_right_width(fill, suffix) -> None: +def test_triangle_right_width(fill: tuple[int, int, int] | None, suffix: str) -> None: img, draw = create_base_image_draw((100, 100)) draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5) assert_image_equal_tofile( @@ -1235,7 +1419,7 @@ def test_wide_line_larger_than_int() -> None: ], ], ) -def test_line_joint(xy) -> None: +def test_line_joint(xy: list[tuple[int, int]] | tuple[int, ...] | list[int]) -> None: im = Image.new("RGB", (500, 325)) draw = ImageDraw.Draw(im) @@ -1388,7 +1572,12 @@ def test_default_font_size() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_same_color_outline(bbox) -> None: +def test_same_color_outline( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 @@ -1402,7 +1591,8 @@ def test_same_color_outline(bbox) -> None: # Begin for mode in ["RGB", "L"]: - for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: + fill = "red" + for outline in [None, "red", "#f00"]: for operation, args in { "chord": [bbox, 0, 180], "ellipse": [bbox], @@ -1417,6 +1607,7 @@ def test_same_color_outline(bbox) -> None: # Act draw_method = getattr(draw, operation) + assert isinstance(args, list) args += [fill, outline] draw_method(*args) @@ -1434,7 +1625,9 @@ def test_same_color_outline(bbox) -> None: (3, "triangle_width", {"width": 5, "outline": "yellow"}), ], ) -def test_draw_regular_polygon(n_sides, polygon_name, args) -> None: +def test_draw_regular_polygon( + n_sides: int, polygon_name: str, args: dict[str, int | str] +) -> None: im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) filename = f"Tests/images/imagedraw_{polygon_name}.png" draw = ImageDraw.Draw(im) @@ -1471,7 +1664,9 @@ def test_draw_regular_polygon(n_sides, polygon_name, args) -> None: ), ], ) -def test_compute_regular_polygon_vertices(n_sides, expected_vertices) -> None: +def test_compute_regular_polygon_vertices( + n_sides: int, expected_vertices: list[tuple[float, float]] +) -> None: bounding_circle = (W // 2, H // 2, 25) vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0) assert vertices == expected_vertices @@ -1569,7 +1764,7 @@ def test_polygon2() -> None: @pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) -def test_incorrectly_ordered_coordinates(xy) -> None: +def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) with pytest.raises(ValueError): diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index a216bd21de2..b607b8c43aa 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -57,7 +57,7 @@ def test_kw() -> None: @pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage(mode) -> None: +def test_photoimage(mode: str) -> None: # test as image: im = hopper(mode) @@ -79,7 +79,7 @@ def test_photoimage_apply_transparency() -> None: @pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage_blank(mode) -> None: +def test_photoimage_blank(mode: str) -> None: # test a image using mode/size: im_tk = ImageTk.PhotoImage(mode, (100, 100)) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index c8d6d33d223..629a6dc7a87 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -10,7 +10,13 @@ class TestLibPack: - def assert_pack(self, mode, rawmode, data, *pixels) -> None: + def assert_pack( + self, + mode: str, + rawmode: str, + data: int | bytes, + *pixels: int | float | tuple[int, ...], + ) -> None: """ data - either raw bytes with data or just number of bytes in rawmode. """ @@ -228,7 +234,13 @@ def test_F_float(self) -> None: class TestLibUnpack: - def assert_unpack(self, mode, rawmode, data, *pixels) -> None: + def assert_unpack( + self, + mode: str, + rawmode: str, + data: int | bytes, + *pixels: int | float | tuple[int, ...], + ) -> None: """ data - either raw bytes with data or just number of bytes in rawmode. """ diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index f2540bb465e..903f7e0c60e 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -11,7 +11,7 @@ original = hopper().resize((32, 32)).convert("I") -def verify(im1) -> None: +def verify(im1: Image.Image) -> None: im2 = original.copy() assert im1.size == im2.size pix1 = im1.load() @@ -27,7 +27,7 @@ def verify(im1) -> None: @pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I")) -def test_basic(tmp_path: Path, mode) -> None: +def test_basic(tmp_path: Path, mode: str) -> None: # PIL 1.1 has limited support for 16-bit image data. Check that # create/copy/transform and save works as expected. @@ -78,7 +78,7 @@ def test_basic(tmp_path: Path, mode) -> None: def test_tobytes() -> None: - def tobytes(mode): + def tobytes(mode: str) -> Image.Image: return Image.new(mode, (1, 1), 1).tobytes() order = 1 if Image._ENDIAN == "<" else -1 From b8769d1cf5782f3db934b861ad764dc9b1466fb4 Mon Sep 17 00:00:00 2001 From: FangFuxin <38530078+lajiyuan@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:02:50 +0800 Subject: [PATCH 0150/2374] Update Tests/test_file_png.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_png.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0884ddcc35d..ec8794b3008 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -778,7 +778,7 @@ class MyStdOut: with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) - def test_truncated_end_chunk(self): + def test_truncated_end_chunk(self) -> None: with Image.open("Tests/images/truncated_end_chunk.png") as im: with pytest.raises(OSError): im.load() From f2228e0a7c19d74c83b99f92edc113ba0cac7625 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:37:53 -0700 Subject: [PATCH 0151/2374] Replace bytes | str | Path with StrOrBytesPath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/PIL/_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_util.py b/src/PIL/_util.py index b649500abb5..f7a69fae15d 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -11,7 +11,7 @@ def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: return isinstance(f, (bytes, str, os.PathLike)) -def is_directory(f: Any) -> TypeGuard[bytes | str | Path]: +def is_directory(f: Any) -> TypeGuard[StrOrBytesPath]: """Checks if an object is a string, and that it points to a directory.""" return is_path(f) and os.path.isdir(f) From 256f3f1966d6b56f178d0a9bb2bc4b0b334c77b1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 19:38:49 +0000 Subject: [PATCH 0152/2374] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/_util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/_util.py b/src/PIL/_util.py index f7a69fae15d..6bc76281616 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -from pathlib import Path from typing import Any, NoReturn from ._typing import StrOrBytesPath, TypeGuard From 6dba9c988765084c104fd93c9fcc9ba3d18f6873 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:14:01 +0000 Subject: [PATCH 0153/2374] Update github-actions to v4 --- .github/workflows/test-cygwin.yml | 4 ++-- .github/workflows/test-docker.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index b5c8c39aaef..7bbe5a37f50 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -81,7 +81,7 @@ jobs: zlib-devel - name: Add Lapack to PATH - uses: egor-tensin/cleanup-path@v3 + uses: egor-tensin/cleanup-path@v4 with: dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' @@ -142,7 +142,7 @@ jobs: bash.exe .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: GHA_Cygwin diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 3bb6856f6e8..75aab9bd489 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -101,7 +101,7 @@ jobs: MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: flags: GHA_Docker name: ${{ matrix.docker }} diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index cdd51e2bb3f..acea78c37d9 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -82,7 +82,7 @@ jobs: python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 8cad7a8b281..b737615ca4b 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -202,7 +202,7 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ae84a4d8fd5..038bcfbc35a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -149,7 +149,7 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} From 2515938cdd321a5940a070f808c01ed48ad4e10e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Feb 2024 19:04:22 +1100 Subject: [PATCH 0154/2374] Simplified type hints --- Tests/test_imagedraw.py | 222 ++++++---------------------------------- 1 file changed, 32 insertions(+), 190 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index c02ac49ddc3..6e7dce420d2 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -2,6 +2,7 @@ import contextlib import os.path +from typing import Sequence import pytest @@ -74,14 +75,7 @@ def test_mode_mismatch() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) -def test_arc( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int], - start: float, - end: float, -) -> None: +def test_arc(bbox: Sequence[int | Sequence[int]], start: float, end: float) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -94,12 +88,7 @@ def test_arc( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_end_le_start( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_end_le_start(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -114,12 +103,7 @@ def test_arc_end_le_start( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_no_loops( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_no_loops(bbox: Sequence[int | Sequence[int]]) -> None: # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -135,12 +119,7 @@ def test_arc_no_loops( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -153,12 +132,7 @@ def test_arc_width( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_pieslice_large( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_width_pieslice_large(bbox: Sequence[int | Sequence[int]]) -> None: # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) @@ -172,12 +146,7 @@ def test_arc_width_pieslice_large( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -190,12 +159,7 @@ def test_arc_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_non_whole_angle( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_width_non_whole_angle(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -237,13 +201,7 @@ def test_bitmap() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_chord( - mode: str, - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int], -) -> None: +def test_chord(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -257,12 +215,7 @@ def test_chord( @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_chord_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -275,12 +228,7 @@ def test_chord_width( @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_chord_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -293,12 +241,7 @@ def test_chord_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_chord_zero_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_chord_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -324,13 +267,7 @@ def test_chord_too_fat() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse( - mode: str, - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int], -) -> None: +def test_ellipse(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -344,12 +281,7 @@ def test_ellipse( @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_translucent( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_ellipse_translucent(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -386,12 +318,7 @@ def test_ellipse_symmetric() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_ellipse_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -416,12 +343,7 @@ def test_ellipse_width_large() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_ellipse_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -434,12 +356,7 @@ def test_ellipse_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_zero_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_ellipse_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -572,12 +489,7 @@ def test_transform() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) def test_pieslice( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int], - start: float, - end: float, + bbox: Sequence[int | Sequence[int]], start: float, end: float ) -> None: # Arrange im = Image.new("RGB", (W, H)) @@ -591,12 +503,7 @@ def test_pieslice( @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_pieslice_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -609,12 +516,7 @@ def test_pieslice_width( @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_pieslice_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -628,12 +530,7 @@ def test_pieslice_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_zero_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_pieslice_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -796,12 +693,7 @@ def test_polygon_translucent() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -828,12 +720,7 @@ def test_big_rectangle() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -847,12 +734,7 @@ def test_rectangle_width( @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -866,12 +748,7 @@ def test_rectangle_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_zero_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -884,12 +761,7 @@ def test_rectangle_zero_width( @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_I16(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) @@ -902,12 +774,7 @@ def test_rectangle_I16( @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_translucent_outline( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_translucent_outline(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -999,12 +866,7 @@ def test_rounded_rectangle_non_integer_radius( @pytest.mark.parametrize("bbox", BBOX) -def test_rounded_rectangle_zero_radius( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rounded_rectangle_zero_radius(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -1045,12 +907,7 @@ def test_rounded_rectangle_translucent( @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_floodfill(bbox: Sequence[int | Sequence[int]]) -> None: red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: @@ -1083,12 +940,7 @@ def test_floodfill( @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_border( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_floodfill_border(bbox: Sequence[int | Sequence[int]]) -> None: # floodfill() is experimental # Arrange @@ -1110,12 +962,7 @@ def test_floodfill_border( @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_thresh( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_floodfill_thresh(bbox: Sequence[int | Sequence[int]]) -> None: # floodfill() is experimental # Arrange @@ -1572,12 +1419,7 @@ def test_default_font_size() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_same_color_outline( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_same_color_outline(bbox: Sequence[int | Sequence[int]]) -> None: # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 From 8d96e3bc590ec9c003efc47ad35295d7de4ed95c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Feb 2024 23:54:31 +1100 Subject: [PATCH 0155/2374] Changed name of first _Tile parameter --- src/PIL/ImageFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 5ba5a6f8277..487f53efe1f 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -91,7 +91,7 @@ def _tilesort(t): class _Tile(NamedTuple): - encoder_name: str + codec_name: str extents: tuple[int, int, int, int] offset: int args: tuple[Any, ...] | str | None From 6207ad419640475440de2f57c710e1a6235dfe90 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Feb 2024 02:24:17 +0000 Subject: [PATCH 0156/2374] Update release-drafter/release-drafter action to v6 --- .github/workflows/release-drafter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 8fc7bd3799d..a8ddef22c86 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -23,6 +23,6 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next release notes as pull requests are merged into "main" - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From dba7dea3263dfa3252f7381307323477531646c8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 3 Feb 2024 14:43:04 +0200 Subject: [PATCH 0157/2374] Pin codecov/codecov-action to v3.1.5 --- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-docker.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7bbe5a37f50..a6b2935a921 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -142,7 +142,7 @@ jobs: bash.exe .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3.1.5 with: file: ./coverage.xml flags: GHA_Cygwin diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 75aab9bd489..f40286fe434 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -101,7 +101,7 @@ jobs: MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3.1.5 with: flags: GHA_Docker name: ${{ matrix.docker }} diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index acea78c37d9..1c6d15b77d7 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -82,7 +82,7 @@ jobs: python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3.1.5 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index b737615ca4b..75fccf7959a 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -202,7 +202,7 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3.1.5 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 038bcfbc35a..19f4a6daedc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -149,7 +149,7 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3.1.5 with: flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} From 435c884ebbee326daf55599c6248028684206cc4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Feb 2024 23:44:33 +1100 Subject: [PATCH 0158/2374] Removed platform argument from setup-cygwin action --- .github/workflows/test-cygwin.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7bbe5a37f50..4b958e889bb 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -49,7 +49,6 @@ jobs: - name: Install Cygwin uses: egor-tensin/setup-cygwin@v4 with: - platform: x86_64 packages: > gcc-g++ ghostscript From 1b6723967440cf8474a9bd1e1c394c90c5c2f986 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Feb 2024 11:56:55 +1100 Subject: [PATCH 0159/2374] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7d80eec0345..a8404260ff3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745 + [nik012003, radarhere] + +- Remove execute bit from ``setup.py`` #7760 + [hugovk] + - Do not support using test-image-results to upload images after test failures #7739 [radarhere] From dfb48ff297aa2b227a98f20ff0ae5a0009644ad3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Feb 2024 19:16:15 +1100 Subject: [PATCH 0160/2374] Match mask size to pasted image size --- Tests/test_file_gif.py | 15 +++++++++++++++ src/PIL/GifImagePlugin.py | 4 +--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 3f550fd1109..263c897ef51 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1105,6 +1105,21 @@ def im_generator(ims): assert reread.n_frames == 10 +def test_append_different_size_image(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im = Image.new("RGB", (100, 100)) + bigger_im = Image.new("RGB", (200, 200), "#f00") + + im.save(out, save_all=True, append_images=[bigger_im]) + + with Image.open(out) as reread: + assert reread.size == (100, 100) + + reread.seek(1) + assert reread.size == (100, 100) + + def test_transparent_optimize(tmp_path: Path) -> None: # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses # transparency. diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 57d87078bca..935b95ca8c6 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -649,9 +649,7 @@ def _write_multiple_frames(im, fp, palette): if "transparency" in encoderinfo: # When the delta is zero, fill the image with transparency diff_frame = im_frame.copy() - fill = Image.new( - "P", diff_frame.size, encoderinfo["transparency"] - ) + fill = Image.new("P", delta.size, encoderinfo["transparency"]) if delta.mode == "RGBA": r, g, b, a = delta.split() mask = ImageMath.eval( From 5a8e7dda79e5ee4d0f8436179f61881a5d8bd286 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Feb 2024 20:36:34 +1100 Subject: [PATCH 0161/2374] Added type hints --- Tests/test_imagedraw.py | 2 +- src/PIL/Image.py | 2 +- src/PIL/ImageDraw.py | 73 +++++++++++++++++++++-------------------- src/PIL/ImageFont.py | 2 +- 4 files changed, 40 insertions(+), 39 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 6e7dce420d2..4503a929280 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1519,7 +1519,7 @@ def test_compute_regular_polygon_vertices( [ (None, (50, 50, 25), 0, TypeError, "n_sides should be an int"), (1, (50, 50, 25), 0, ValueError, "n_sides should be an int > 2"), - (3, 50, 0, TypeError, "bounding_circle should be a tuple"), + (3, 50, 0, TypeError, "bounding_circle should be a sequence"), ( 3, (50, 50, 100, 100), diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 553f36703b3..111d060129e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -571,7 +571,7 @@ def close(self): # object is gone. self.im = DeferredError(ValueError("Operation on closed image")) - def _copy(self): + def _copy(self) -> None: self.load() self.im = self.im.copy() self.pyaccess = None diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 84665f54fff..650e3085763 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -48,7 +48,7 @@ class ImageDraw: font = None - def __init__(self, im, mode=None): + def __init__(self, im: Image.Image, mode: str | None = None) -> None: """ Create a drawing instance. @@ -115,7 +115,7 @@ def getfont(self): self.font = ImageFont.load_default() return self.font - def _getfont(self, font_size): + def _getfont(self, font_size: float | None): if font_size is not None: from . import ImageFont @@ -124,7 +124,7 @@ def _getfont(self, font_size): font = self.getfont() return font - def _getink(self, ink, fill=None): + def _getink(self, ink, fill=None) -> tuple[int | None, int | None]: if ink is None and fill is None: if self.fill: fill = self.ink @@ -145,13 +145,13 @@ def _getink(self, ink, fill=None): fill = self.draw.draw_ink(fill) return ink, fill - def arc(self, xy, start, end, fill=None, width=1): + def arc(self, xy, start, end, fill=None, width=1) -> None: """Draw an arc.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_arc(xy, start, end, ink, width) - def bitmap(self, xy, bitmap, fill=None): + def bitmap(self, xy, bitmap, fill=None) -> None: """Draw a bitmap.""" bitmap.load() ink, fill = self._getink(fill) @@ -160,7 +160,7 @@ def bitmap(self, xy, bitmap, fill=None): if ink is not None: self.draw.draw_bitmap(xy, bitmap.im, ink) - def chord(self, xy, start, end, fill=None, outline=None, width=1): + def chord(self, xy, start, end, fill=None, outline=None, width=1) -> None: """Draw a chord.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -168,7 +168,7 @@ def chord(self, xy, start, end, fill=None, outline=None, width=1): if ink is not None and ink != fill and width != 0: self.draw.draw_chord(xy, start, end, ink, 0, width) - def ellipse(self, xy, fill=None, outline=None, width=1): + def ellipse(self, xy, fill=None, outline=None, width=1) -> None: """Draw an ellipse.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -176,7 +176,7 @@ def ellipse(self, xy, fill=None, outline=None, width=1): if ink is not None and ink != fill and width != 0: self.draw.draw_ellipse(xy, ink, 0, width) - def line(self, xy, fill=None, width=0, joint=None): + def line(self, xy, fill=None, width=0, joint=None) -> None: """Draw a line, or a connected sequence of line segments.""" ink = self._getink(fill)[0] if ink is not None: @@ -236,7 +236,7 @@ def coord_at_angle(coord, angle): ] self.line(gap_coords, fill, width=3) - def shape(self, shape, fill=None, outline=None): + def shape(self, shape, fill=None, outline=None) -> None: """(Experimental) Draw a shape.""" shape.close() ink, fill = self._getink(outline, fill) @@ -245,7 +245,7 @@ def shape(self, shape, fill=None, outline=None): if ink is not None and ink != fill: self.draw.draw_outline(shape, ink, 0) - def pieslice(self, xy, start, end, fill=None, outline=None, width=1): + def pieslice(self, xy, start, end, fill=None, outline=None, width=1) -> None: """Draw a pieslice.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -253,13 +253,13 @@ def pieslice(self, xy, start, end, fill=None, outline=None, width=1): if ink is not None and ink != fill and width != 0: self.draw.draw_pieslice(xy, start, end, ink, 0, width) - def point(self, xy, fill=None): + def point(self, xy, fill=None) -> None: """Draw one or more individual pixels.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_points(xy, ink) - def polygon(self, xy, fill=None, outline=None, width=1): + def polygon(self, xy, fill=None, outline=None, width=1) -> None: """Draw a polygon.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -267,7 +267,7 @@ def polygon(self, xy, fill=None, outline=None, width=1): if ink is not None and ink != fill and width != 0: if width == 1: self.draw.draw_polygon(xy, ink, 0, width) - else: + elif self.im is not None: # To avoid expanding the polygon outwards, # use the fill as a mask mask = Image.new("1", self.im.size) @@ -291,12 +291,12 @@ def polygon(self, xy, fill=None, outline=None, width=1): def regular_polygon( self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 - ): + ) -> None: """Draw a regular polygon.""" xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) self.polygon(xy, fill, outline, width) - def rectangle(self, xy, fill=None, outline=None, width=1): + def rectangle(self, xy, fill=None, outline=None, width=1) -> None: """Draw a rectangle.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -306,7 +306,7 @@ def rectangle(self, xy, fill=None, outline=None, width=1): def rounded_rectangle( self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None - ): + ) -> None: """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): (x0, y0), (x1, y1) = xy @@ -346,7 +346,7 @@ def rounded_rectangle( r = d // 2 ink, fill = self._getink(outline, fill) - def draw_corners(pieslice): + def draw_corners(pieslice) -> None: if full_x: # Draw top and bottom halves parts = ( @@ -431,12 +431,12 @@ def draw_corners(pieslice): right[3] -= r + 1 self.draw.draw_rectangle(right, ink, 1) - def _multiline_check(self, text): + def _multiline_check(self, text) -> bool: split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text - def _multiline_split(self, text): + def _multiline_split(self, text) -> list[str | bytes]: split_character = "\n" if isinstance(text, str) else b"\n" return text.split(split_character) @@ -465,7 +465,7 @@ def text( embedded_color=False, *args, **kwargs, - ): + ) -> None: """Draw text.""" if embedded_color and self.mode not in ("RGB", "RGBA"): msg = "Embedded color supported only in RGB and RGBA modes" @@ -497,7 +497,7 @@ def getink(fill): return fill return ink - def draw_text(ink, stroke_width=0, stroke_offset=None): + def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: mode = self.fontmode if stroke_width == 0 and embedded_color: mode = "RGBA" @@ -547,7 +547,8 @@ def draw_text(ink, stroke_width=0, stroke_offset=None): ink_alpha = struct.pack("i", ink)[3] color.fillband(3, ink_alpha) x, y = coord - self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) + if self.im is not None: + self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) else: self.draw.draw_bitmap(coord, mask, ink) @@ -584,7 +585,7 @@ def multiline_text( embedded_color=False, *, font_size=None, - ): + ) -> None: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" raise ValueError(msg) @@ -693,7 +694,7 @@ def textbbox( embedded_color=False, *, font_size=None, - ): + ) -> tuple[int, int, int, int]: """Get the bounding box of a given string, in pixels.""" if embedded_color and self.mode not in ("RGB", "RGBA"): msg = "Embedded color supported only in RGB and RGBA modes" @@ -738,7 +739,7 @@ def multiline_textbbox( embedded_color=False, *, font_size=None, - ): + ) -> tuple[int, int, int, int]: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" raise ValueError(msg) @@ -777,7 +778,7 @@ def multiline_textbbox( elif anchor[1] == "d": top -= (len(lines) - 1) * line_spacing - bbox = None + bbox: tuple[int, int, int, int] | None = None for idx, line in enumerate(lines): left = xy[0] @@ -828,7 +829,7 @@ def multiline_textbbox( return bbox -def Draw(im, mode=None): +def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: """ A simple 2D drawing interface for PIL images. @@ -876,7 +877,7 @@ def getdraw(im=None, hints=None): return im, handler -def floodfill(image, xy, value, border=None, thresh=0): +def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None: """ (experimental) Fills a bounded region with a given color. @@ -932,7 +933,7 @@ def floodfill(image, xy, value, border=None, thresh=0): edge = new_edge -def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): +def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) -> list[tuple[float, float]]: """ Generate a list of vertices for a 2D regular polygon. @@ -982,7 +983,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): # 1.2 Check `bounding_circle` has an appropriate value if not isinstance(bounding_circle, (list, tuple)): - msg = "bounding_circle should be a tuple" + msg = "bounding_circle should be a sequence" raise TypeError(msg) if len(bounding_circle) == 3: @@ -1014,7 +1015,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): raise ValueError(msg) # 2. Define Helper Functions - def _apply_rotation(point, degrees, centroid): + def _apply_rotation(point: list[float], degrees: float) -> tuple[int, int]: return ( round( point[0] * math.cos(math.radians(360 - degrees)) @@ -1030,11 +1031,11 @@ def _apply_rotation(point, degrees, centroid): ), ) - def _compute_polygon_vertex(centroid, polygon_radius, angle): + def _compute_polygon_vertex(angle: float) -> tuple[int, int]: start_point = [polygon_radius, 0] - return _apply_rotation(start_point, angle, centroid) + return _apply_rotation(start_point, angle) - def _get_angles(n_sides, rotation): + def _get_angles(n_sides: int, rotation: float) -> list[float]: angles = [] degrees = 360 / n_sides # Start with the bottom left polygon vertex @@ -1051,11 +1052,11 @@ def _get_angles(n_sides, rotation): # 4. Compute Vertices return [ - _compute_polygon_vertex(centroid, polygon_radius, angle) for angle in angles + _compute_polygon_vertex(angle) for angle in angles ] -def _color_diff(color1, color2): +def _color_diff(color1, color2: float | tuple[int, ...]) -> float: """ Uses 1-norm distance to calculate difference between two values. """ diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a63b73b33f5..1ec8a9f4d1f 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -872,7 +872,7 @@ def load_path(filename): raise OSError(msg) -def load_default(size=None): +def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: """If FreeType support is available, load a version of Aileron Regular, https://dotcolon.net/font/aileron, with a more limited character set. From e0da2b71206c8c06ff4a9f67d6dade6973a7ae96 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 17:18:25 +0000 Subject: [PATCH 0162/2374] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.9 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.9...v0.2.0) - [github.com/psf/black-pre-commit-mirror: 23.12.1 → 24.1.1](https://github.com/psf/black-pre-commit-mirror/compare/23.12.1...24.1.1) - [github.com/PyCQA/bandit: 1.7.6 → 1.7.7](https://github.com/PyCQA/bandit/compare/1.7.6...1.7.7) - [github.com/tox-dev/pyproject-fmt: 1.5.3 → 1.7.0](https://github.com/tox-dev/pyproject-fmt/compare/1.5.3...1.7.0) - [github.com/abravalheri/validate-pyproject: v0.15 → v0.16](https://github.com/abravalheri/validate-pyproject/compare/v0.15...v0.16) --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ce0c9a1792..c52fdcb552d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,17 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.9 + rev: v0.2.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.1 + rev: 24.1.1 hooks: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.7.6 + rev: 1.7.7 hooks: - id: bandit args: [--severity-level=high] @@ -48,12 +48,12 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 1.5.3 + rev: 1.7.0 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.15 + rev: v0.16 hooks: - id: validate-pyproject From 27b0cf67e784c0c9e58e60afd8ffa1ba274681c6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 17:18:49 +0000 Subject: [PATCH 0163/2374] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .github/workflows/system-info.py | 1 + Tests/helper.py | 1 + Tests/test_file_dds.py | 1 + Tests/test_file_libtiff_small.py | 1 - Tests/test_image_access.py | 4 +--- Tests/test_image_resize.py | 1 + Tests/test_imagecms.py | 8 +++++--- docs/example/DdsImagePlugin.py | 1 + src/PIL/BlpImagePlugin.py | 1 + src/PIL/DdsImagePlugin.py | 1 + src/PIL/FontFile.py | 4 +--- src/PIL/FtexImagePlugin.py | 1 + src/PIL/GifImagePlugin.py | 6 +++--- src/PIL/ImageCms.py | 2 -- src/PIL/JpegPresets.py | 1 + src/PIL/PdfImagePlugin.py | 6 +++--- src/PIL/__init__.py | 1 + src/PIL/_tkinter_finder.py | 1 + 18 files changed, 24 insertions(+), 18 deletions(-) diff --git a/.github/workflows/system-info.py b/.github/workflows/system-info.py index 57f28c620b7..9e97b897104 100644 --- a/.github/workflows/system-info.py +++ b/.github/workflows/system-info.py @@ -6,6 +6,7 @@ Requested here: https://github.com/actions/virtual-environments/issues/79 """ + from __future__ import annotations import os diff --git a/Tests/helper.py b/Tests/helper.py index b2e7d43dd67..3e2a40e02da 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -1,6 +1,7 @@ """ Helper functions. """ + from __future__ import annotations import logging diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 09ee8986aca..b78a0dd8109 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -1,4 +1,5 @@ """Test DdsImagePlugin""" + from __future__ import annotations from io import BytesIO diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index ac5270eac30..617e1e89c72 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -9,7 +9,6 @@ class TestFileLibTiffSmall(LibTiffTestCase): - """The small lena image was failing on open in the libtiff decoder because the file pointer was set to the wrong place by a spurious seek. It wasn't failing with the byteio method. diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 00cd4e7a9a1..e4cb2dad102 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -230,9 +230,7 @@ def test_list(self) -> None: assert im.getpixel([0, 0]) == (20, 20, 70) @pytest.mark.parametrize("mode", ("I;16", "I;16B")) - @pytest.mark.parametrize( - "expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1) - ) + @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) def test_signedness(self, mode, expected_color) -> None: # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index bd45ee893ad..a64e4a84621 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -1,6 +1,7 @@ """ Tests for resize functionality. """ + from __future__ import annotations from itertools import permutations diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 7f652715566..83fc38ed3fd 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -342,9 +342,11 @@ def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None: def truncate_tuple(tuple_or_float): return tuple( - truncate_tuple(val) - if isinstance(val, tuple) - else int(val * power) / power + ( + truncate_tuple(val) + if isinstance(val, tuple) + else int(val * power) / power + ) for val in tuple_or_float ) diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index e98bb86806f..2a2a0ba2961 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -9,6 +9,7 @@ Full text of the CC0 license: https://creativecommons.org/publicdomain/zero/1.0/ """ + from __future__ import annotations import struct diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index b8f38b78a2e..f0fbc8cc28a 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -28,6 +28,7 @@ - DXT3 compression is used if alpha_encoding == 1. - DXT5 compression is used if alpha_encoding == 7. """ + from __future__ import annotations import os diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index eb4c8f557af..3785174ef65 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -9,6 +9,7 @@ Full text of the CC0 license: https://creativecommons.org/publicdomain/zero/1.0/ """ + from __future__ import annotations import io diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 3ec1ae819fc..1e0c1c166b5 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -50,9 +50,7 @@ def __init__(self) -> None: | None ] = [None] * 256 - def __getitem__( - self, ix: int - ) -> ( + def __getitem__(self, ix: int) -> ( tuple[ tuple[int, int], tuple[int, int, int, int], diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index d5513a56a11..b4488e6ee9c 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -50,6 +50,7 @@ Note: All data is stored in little-Endian (Intel) byte order. """ + from __future__ import annotations import struct diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 57d87078bca..dc842d7a30b 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -641,9 +641,9 @@ def _write_multiple_frames(im, fp, palette): if encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: try: - encoderinfo[ - "transparency" - ] = im_frame.palette._new_color_index(im_frame) + encoderinfo["transparency"] = ( + im_frame.palette._new_color_index(im_frame) + ) except ValueError: pass if "transparency" in encoderinfo: diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 3e40105e46a..2b0ed6c9d2f 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -281,7 +281,6 @@ def tobytes(self): class ImageCmsTransform(Image.ImagePointHandler): - """ Transform. This can be used with the procedural API, or with the standard :py:func:`~PIL.Image.Image.point` method. @@ -369,7 +368,6 @@ def get_display_profile(handle=None): class PyCMSError(Exception): - """(pyCMS) Exception class. This is used for all errors in the pyCMS API.""" diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py index 9ecfdb2599a..d0e64a35ee1 100644 --- a/src/PIL/JpegPresets.py +++ b/src/PIL/JpegPresets.py @@ -62,6 +62,7 @@ https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html """ + from __future__ import annotations # fmt: off diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 3506aadce83..1777f1f20db 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -188,9 +188,9 @@ def _save(im, fp, filename, save_all=False): x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) info = { - "title": None - if is_appending - else os.path.splitext(os.path.basename(filename))[0], + "title": ( + None if is_appending else os.path.splitext(os.path.basename(filename))[0] + ), "author": None, "subject": None, "keywords": None, diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 3fcac8643cb..63a45769ba6 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -12,6 +12,7 @@ ;-) """ + from __future__ import annotations from . import _version diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 03a6eba44ff..71c0ad4651b 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -1,5 +1,6 @@ """ Find compiled module linking to Tcl / Tk libraries """ + from __future__ import annotations import sys From 1acaf20f7215c567f0ea04bf20c396a237a2a542 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:37:45 +0200 Subject: [PATCH 0164/2374] Enable LOG rules for Ruff linter --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b1ce9cf1d07..48257b750a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,12 +104,12 @@ select = [ "F", # pyflakes errors "I", # isort "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging "PGH", # pygrep-hooks "RUF100", # unused noqa (yesqa) "UP", # pyupgrade "W", # pycodestyle warnings "YTT", # flake8-2020 - # "LOG", # TODO: enable flake8-logging when it's not in preview anymore ] extend-ignore = [ "E203", # Whitespace before ':' From 3bcc7072d68d3d534f06938879cd6b2fa31e61b9 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:39:08 +0200 Subject: [PATCH 0165/2374] Move linter config from deprecated top-level to own section --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 48257b750a4..d7b60ef17d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,7 @@ config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" -[tool.ruff] +[tool.ruff.lint] select = [ "C4", # flake8-comprehensions "E", # pycodestyle errors @@ -118,11 +118,11 @@ extend-ignore = [ "E241", # Multiple spaces after ',' ] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "Tests/oss-fuzz/fuzz_font.py" = ["I002"] "Tests/oss-fuzz/fuzz_pillow.py" = ["I002"] -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["PIL"] required-imports = ["from __future__ import annotations"] From 65cb0b0487c29c91f0145226e1cd173511bc3586 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Feb 2024 07:49:43 +1100 Subject: [PATCH 0166/2374] Added _typing.Coords --- Tests/test_imagedraw.py | 87 +++++++++++++++++------------------------ src/PIL/ImageDraw.py | 87 ++++++++++++++++++++++++----------------- src/PIL/_typing.py | 4 ++ 3 files changed, 91 insertions(+), 87 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 4503a929280..4e6cedcd15f 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -2,11 +2,11 @@ import contextlib import os.path -from typing import Sequence import pytest from PIL import Image, ImageColor, ImageDraw, ImageFont, features +from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -75,7 +75,7 @@ def test_mode_mismatch() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) -def test_arc(bbox: Sequence[int | Sequence[int]], start: float, end: float) -> None: +def test_arc(bbox: Coords, start: float, end: float) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -88,7 +88,7 @@ def test_arc(bbox: Sequence[int | Sequence[int]], start: float, end: float) -> N @pytest.mark.parametrize("bbox", BBOX) -def test_arc_end_le_start(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_end_le_start(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -103,7 +103,7 @@ def test_arc_end_le_start(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_no_loops(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_no_loops(bbox: Coords) -> None: # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -119,7 +119,7 @@ def test_arc_no_loops(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -132,7 +132,7 @@ def test_arc_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_pieslice_large(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_width_pieslice_large(bbox: Coords) -> None: # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) @@ -146,7 +146,7 @@ def test_arc_width_pieslice_large(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -159,7 +159,7 @@ def test_arc_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_non_whole_angle(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_width_non_whole_angle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -201,7 +201,7 @@ def test_bitmap() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_chord(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: +def test_chord(mode: str, bbox: Coords) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -215,7 +215,7 @@ def test_chord(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_chord_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -228,7 +228,7 @@ def test_chord_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_chord_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -241,7 +241,7 @@ def test_chord_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_chord_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -267,7 +267,7 @@ def test_chord_too_fat() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse(mode: str, bbox: Coords) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -281,7 +281,7 @@ def test_ellipse(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_translucent(bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse_translucent(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -318,7 +318,7 @@ def test_ellipse_symmetric() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -343,7 +343,7 @@ def test_ellipse_width_large() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -356,7 +356,7 @@ def test_ellipse_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -410,12 +410,7 @@ def test_ellipse_various_sizes_filled() -> None: @pytest.mark.parametrize("points", POINTS) -def test_line( - points: tuple[tuple[int, int], ...] - | list[tuple[int, int]] - | tuple[int, ...] - | list[int] -) -> None: +def test_line(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -488,9 +483,7 @@ def test_transform() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) -def test_pieslice( - bbox: Sequence[int | Sequence[int]], start: float, end: float -) -> None: +def test_pieslice(bbox: Coords, start: float, end: float) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -503,7 +496,7 @@ def test_pieslice( @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_pieslice_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -516,7 +509,7 @@ def test_pieslice_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_pieslice_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -530,7 +523,7 @@ def test_pieslice_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_pieslice_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -585,12 +578,7 @@ def test_pieslice_no_spikes() -> None: @pytest.mark.parametrize("points", POINTS) -def test_point( - points: tuple[tuple[int, int], ...] - | list[tuple[int, int]] - | tuple[int, ...] - | list[int] -) -> None: +def test_point(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -615,12 +603,7 @@ def test_point_I16() -> None: @pytest.mark.parametrize("points", POINTS) -def test_polygon( - points: tuple[tuple[int, int], ...] - | list[tuple[int, int]] - | tuple[int, ...] - | list[int] -) -> None: +def test_polygon(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -693,7 +676,7 @@ def test_polygon_translucent() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -720,7 +703,7 @@ def test_big_rectangle() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -734,7 +717,7 @@ def test_rectangle_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -748,7 +731,7 @@ def test_rectangle_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -761,7 +744,7 @@ def test_rectangle_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_I16(bbox: Coords) -> None: # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) @@ -774,7 +757,7 @@ def test_rectangle_I16(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_translucent_outline(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_translucent_outline(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -866,7 +849,7 @@ def test_rounded_rectangle_non_integer_radius( @pytest.mark.parametrize("bbox", BBOX) -def test_rounded_rectangle_zero_radius(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rounded_rectangle_zero_radius(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -907,7 +890,7 @@ def test_rounded_rectangle_translucent( @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_floodfill(bbox: Coords) -> None: red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: @@ -940,7 +923,7 @@ def test_floodfill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_border(bbox: Sequence[int | Sequence[int]]) -> None: +def test_floodfill_border(bbox: Coords) -> None: # floodfill() is experimental # Arrange @@ -962,7 +945,7 @@ def test_floodfill_border(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_thresh(bbox: Sequence[int | Sequence[int]]) -> None: +def test_floodfill_thresh(bbox: Coords) -> None: # floodfill() is experimental # Arrange @@ -1419,7 +1402,7 @@ def test_default_font_size() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_same_color_outline(bbox: Sequence[int | Sequence[int]]) -> None: +def test_same_color_outline(bbox: Coords) -> None: # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 650e3085763..d4e000087c4 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -34,8 +34,10 @@ import math import numbers import struct +from typing import Sequence, cast from . import Image, ImageColor +from ._typing import Coords """ A simple 2D drawing interface for PIL images. @@ -145,13 +147,13 @@ def _getink(self, ink, fill=None) -> tuple[int | None, int | None]: fill = self.draw.draw_ink(fill) return ink, fill - def arc(self, xy, start, end, fill=None, width=1) -> None: + def arc(self, xy: Coords, start, end, fill=None, width=1) -> None: """Draw an arc.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_arc(xy, start, end, ink, width) - def bitmap(self, xy, bitmap, fill=None) -> None: + def bitmap(self, xy: Sequence[int], bitmap, fill=None) -> None: """Draw a bitmap.""" bitmap.load() ink, fill = self._getink(fill) @@ -160,7 +162,7 @@ def bitmap(self, xy, bitmap, fill=None) -> None: if ink is not None: self.draw.draw_bitmap(xy, bitmap.im, ink) - def chord(self, xy, start, end, fill=None, outline=None, width=1) -> None: + def chord(self, xy: Coords, start, end, fill=None, outline=None, width=1) -> None: """Draw a chord.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -168,7 +170,7 @@ def chord(self, xy, start, end, fill=None, outline=None, width=1) -> None: if ink is not None and ink != fill and width != 0: self.draw.draw_chord(xy, start, end, ink, 0, width) - def ellipse(self, xy, fill=None, outline=None, width=1) -> None: + def ellipse(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw an ellipse.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -176,20 +178,29 @@ def ellipse(self, xy, fill=None, outline=None, width=1) -> None: if ink is not None and ink != fill and width != 0: self.draw.draw_ellipse(xy, ink, 0, width) - def line(self, xy, fill=None, width=0, joint=None) -> None: + def line(self, xy: Coords, fill=None, width=0, joint=None) -> None: """Draw a line, or a connected sequence of line segments.""" ink = self._getink(fill)[0] if ink is not None: self.draw.draw_lines(xy, ink, width) if joint == "curve" and width > 4: - if not isinstance(xy[0], (list, tuple)): - xy = [tuple(xy[i : i + 2]) for i in range(0, len(xy), 2)] - for i in range(1, len(xy) - 1): - point = xy[i] + points: Sequence[Sequence[float]] + if isinstance(xy[0], (list, tuple)): + points = cast(Sequence[Sequence[float]], xy) + else: + points = [ + cast(Sequence[float], tuple(xy[i : i + 2])) + for i in range(0, len(xy), 2) + ] + for i in range(1, len(points) - 1): + point = points[i] angles = [ math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) % 360 - for start, end in ((xy[i - 1], point), (point, xy[i + 1])) + for start, end in ( + (points[i - 1], point), + (point, points[i + 1]), + ) ] if angles[0] == angles[1]: # This is a straight line, so no joint is required @@ -245,7 +256,9 @@ def shape(self, shape, fill=None, outline=None) -> None: if ink is not None and ink != fill: self.draw.draw_outline(shape, ink, 0) - def pieslice(self, xy, start, end, fill=None, outline=None, width=1) -> None: + def pieslice( + self, xy: Coords, start, end, fill=None, outline=None, width=1 + ) -> None: """Draw a pieslice.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -253,13 +266,13 @@ def pieslice(self, xy, start, end, fill=None, outline=None, width=1) -> None: if ink is not None and ink != fill and width != 0: self.draw.draw_pieslice(xy, start, end, ink, 0, width) - def point(self, xy, fill=None) -> None: + def point(self, xy: Coords, fill=None) -> None: """Draw one or more individual pixels.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_points(xy, ink) - def polygon(self, xy, fill=None, outline=None, width=1) -> None: + def polygon(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw a polygon.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -296,7 +309,7 @@ def regular_polygon( xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) self.polygon(xy, fill, outline, width) - def rectangle(self, xy, fill=None, outline=None, width=1) -> None: + def rectangle(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw a rectangle.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -305,13 +318,13 @@ def rectangle(self, xy, fill=None, outline=None, width=1) -> None: self.draw.draw_rectangle(xy, ink, 0, width) def rounded_rectangle( - self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None + self, xy: Coords, radius=0, fill=None, outline=None, width=1, *, corners=None ) -> None: """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): - (x0, y0), (x1, y1) = xy + (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy) else: - x0, y0, x1, y1 = xy + x0, y0, x1, y1 = cast(Sequence[float], xy) if x1 < x0: msg = "x1 must be greater than or equal to x0" raise ValueError(msg) @@ -347,6 +360,7 @@ def rounded_rectangle( ink, fill = self._getink(outline, fill) def draw_corners(pieslice) -> None: + parts: tuple[tuple[tuple[float, float, float, float], int, int], ...] if full_x: # Draw top and bottom halves parts = ( @@ -361,17 +375,18 @@ def draw_corners(pieslice) -> None: ) else: # Draw four separate corners - parts = [] - for i, part in enumerate( - ( - ((x0, y0, x0 + d, y0 + d), 180, 270), - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), + parts = tuple( + part + for i, part in enumerate( + ( + ((x0, y0, x0 + d, y0 + d), 180, 270), + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ) ) - ): - if corners[i]: - parts.append(part) + if corners[i] + ) for part in parts: if pieslice: self.draw.draw_pieslice(*(part + (fill, 1))) @@ -520,7 +535,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: *args, **kwargs, ) - coord = coord[0] + offset[0], coord[1] + offset[1] + coord = [coord[0] + offset[0], coord[1] + offset[1]] except AttributeError: try: mask = font.getmask( @@ -539,7 +554,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: except TypeError: mask = font.getmask(text) if stroke_offset: - coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] + coord = [coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]] if mode == "RGBA": # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A # extract mask and set text alpha @@ -548,7 +563,9 @@ def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: color.fillband(3, ink_alpha) x, y = coord if self.im is not None: - self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) + self.im.paste( + color, (x, y, x + mask.size[0], y + mask.size[1]), mask + ) else: self.draw.draw_bitmap(coord, mask, ink) @@ -829,7 +846,7 @@ def multiline_textbbox( return bbox -def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: +def Draw(im, mode: str | None = None) -> ImageDraw: """ A simple 2D drawing interface for PIL images. @@ -933,7 +950,9 @@ def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None: edge = new_edge -def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) -> list[tuple[float, float]]: +def _compute_regular_polygon_vertices( + bounding_circle, n_sides, rotation +) -> list[tuple[float, float]]: """ Generate a list of vertices for a 2D regular polygon. @@ -1051,9 +1070,7 @@ def _get_angles(n_sides: int, rotation: float) -> list[float]: angles = _get_angles(n_sides, rotation) # 4. Compute Vertices - return [ - _compute_polygon_vertex(angle) for angle in angles - ] + return [_compute_polygon_vertex(angle) for angle in angles] def _color_diff(color1, color2: float | tuple[int, ...]) -> float: diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 608b2b41fa8..ddea0b41467 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -1,6 +1,7 @@ from __future__ import annotations import sys +from typing import Sequence, Union if sys.version_info >= (3, 10): from typing import TypeGuard @@ -15,4 +16,7 @@ def __class_getitem__(cls, item: Any) -> type[bool]: return bool +Coords = Union[Sequence[float], Sequence[Sequence[float]]] + + __all__ = ["TypeGuard"] From 5f115df74f7aa26ec94b79ecad720a707be029e8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Feb 2024 08:05:30 +1100 Subject: [PATCH 0167/2374] Replace deprecated "extend-ignore" with "ignore" --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d7b60ef17d2..652ae36333c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,7 +111,7 @@ select = [ "W", # pycodestyle warnings "YTT", # flake8-2020 ] -extend-ignore = [ +ignore = [ "E203", # Whitespace before ':' "E221", # Multiple spaces before operator "E226", # Missing whitespace around arithmetic operator From 469db5114cf317ea128bd8c4b508eed537b7ce9e Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Tue, 6 Feb 2024 15:41:08 -0500 Subject: [PATCH 0168/2374] Release GIL while calling WebPAnimDecoderGetNext --- src/_webp.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_webp.c b/src/_webp.c index a1b4dbc1a25..4e7d41f1121 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -450,12 +450,16 @@ _anim_decoder_get_next(PyObject *self) { int timestamp; PyObject *bytes; PyObject *ret; + ImagingSectionCookie cookie; WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; + ImagingSectionEnter(&cookie); if (!WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp)) { + ImagingSectionLeave(&cookie); PyErr_SetString(PyExc_OSError, "failed to read next frame"); return NULL; } + ImagingSectionLeave(&cookie); bytes = PyBytes_FromStringAndSize( (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height); From 91645f9efffc623cb83221a8d9c9a0b98d3ce548 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Feb 2024 10:19:00 +1100 Subject: [PATCH 0169/2374] Lint fix --- Tests/test_imagedraw.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 4e6cedcd15f..f7aea30348d 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -776,9 +776,11 @@ def test_rectangle_translucent_outline(bbox: Coords) -> None: [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], ) def test_rounded_rectangle( - xy: tuple[int, int, int, int] - | tuple[list[int]] - | tuple[tuple[int, int], tuple[int, int]] + xy: ( + tuple[int, int, int, int] + | tuple[list[int]] + | tuple[tuple[int, int], tuple[int, int]] + ) ) -> None: # Arrange im = Image.new("RGB", (200, 200)) From cdc498e6f3b2060906ca14fe9b9187e0a93a1613 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Feb 2024 19:16:28 +1100 Subject: [PATCH 0170/2374] Added type hints --- Tests/test_color_lut.py | 4 ++- Tests/test_file_eps.py | 32 ++++++++++---------- Tests/test_file_jpeg.py | 35 +++++++++++----------- Tests/test_file_jpeg2k.py | 21 ++++++------- Tests/test_file_libtiff.py | 38 +++++++++++++----------- Tests/test_file_png.py | 15 +++++----- Tests/test_image_paste.py | 41 +++++++++++++++----------- Tests/test_image_reduce.py | 43 +++++++++++++++------------ Tests/test_image_resample.py | 57 ++++++++++++++++++++---------------- Tests/test_imagemath.py | 8 ++--- 10 files changed, 160 insertions(+), 134 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index e6c8d7819a6..2bb1b57d482 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -15,7 +15,9 @@ class TestColorLut3DCoreAPI: - def generate_identity_table(self, channels, size): + def generate_identity_table( + self, channels: int, size: int | tuple[int, int, int] + ) -> tuple[int, int, int, int, list[float]]: if isinstance(size, tuple): size_1d, size_2d, size_3d = size else: diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 06f927c7bb2..00f5f39e8c8 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -84,7 +84,7 @@ ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) ) @pytest.mark.parametrize("scale", (1, 2)) -def test_sanity(filename, size, scale) -> None: +def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: expected_size = tuple(s * scale for s in size) with Image.open(filename) as image: image.load(scale=scale) @@ -129,28 +129,28 @@ def test_binary_header_only() -> None: @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_version_comment(prefix) -> None: +def test_missing_version_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_boundingbox_comment(prefix) -> None: +def test_missing_boundingbox_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox)) with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment(prefix) -> None: +def test_invalid_boundingbox_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) with pytest.raises(OSError, match="cannot determine EPS bounding box"): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix) -> None: +def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None: data = io.BytesIO( prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) ) @@ -161,21 +161,21 @@ def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix) -> None: @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_ascii_comment_too_long(prefix) -> None: +def test_ascii_comment_too_long(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) with pytest.raises(SyntaxError, match="not an EPS file"): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_long_binary_data(prefix) -> None: +def test_long_binary_data(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) EpsImagePlugin.EpsImageFile(data) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_load_long_binary_data(prefix) -> None: +def test_load_long_binary_data(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) with Image.open(data) as img: img.load() @@ -305,7 +305,7 @@ def test_render_scale2() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) -def test_resize(filename) -> None: +def test_resize(filename: str) -> None: with Image.open(filename) as im: new_size = (100, 100) im = im.resize(new_size) @@ -314,7 +314,7 @@ def test_resize(filename) -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("filename", (FILE1, FILE2)) -def test_thumbnail(filename) -> None: +def test_thumbnail(filename: str) -> None: # Issue #619 with Image.open(filename) as im: new_size = (100, 100) @@ -335,7 +335,7 @@ def test_readline_psfile(tmp_path: Path) -> None: line_endings = ["\r\n", "\n", "\n\r", "\r"] strings = ["something", "else", "baz", "bif"] - def _test_readline(t, ending) -> None: + def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None: ending = "Failure with line ending: %s" % ( "".join("%s" % ord(s) for s in ending) ) @@ -344,13 +344,13 @@ def _test_readline(t, ending) -> None: assert t.readline().strip("\r\n") == "baz", ending assert t.readline().strip("\r\n") == "bif", ending - def _test_readline_io_psfile(test_string, ending) -> None: + def _test_readline_io_psfile(test_string: str, ending: str) -> None: f = io.BytesIO(test_string.encode("latin-1")) with pytest.warns(DeprecationWarning): t = EpsImagePlugin.PSFile(f) _test_readline(t, ending) - def _test_readline_file_psfile(test_string, ending) -> None: + def _test_readline_file_psfile(test_string: str, ending: str) -> None: f = str(tmp_path / "temp.txt") with open(f, "wb") as w: w.write(test_string.encode("latin-1")) @@ -376,7 +376,7 @@ def test_psfile_deprecation() -> None: "line_ending", (b"\r\n", b"\n", b"\n\r", b"\r"), ) -def test_readline(prefix, line_ending) -> None: +def test_readline(prefix: bytes, line_ending: bytes) -> None: simple_file = prefix + line_ending.join(simple_eps_file_with_comments) data = io.BytesIO(simple_file) test_file = EpsImagePlugin.EpsImageFile(data) @@ -394,7 +394,7 @@ def test_readline(prefix, line_ending) -> None: "Tests/images/illuCS6_preview.eps", ), ) -def test_open_eps(filename) -> None: +def test_open_eps(filename: str) -> None: # https://github.com/python-pillow/Pillow/issues/1104 with Image.open(filename) as img: assert img.mode == "RGB" @@ -417,7 +417,7 @@ def test_emptyline() -> None: "test_file", ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], ) -def test_timeout(test_file) -> None: +def test_timeout(test_file: str) -> None: with open(test_file, "rb") as f: with pytest.raises(Image.UnidentifiedImageError): with Image.open(f): diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index ff278d4c1bf..6b0662e0be3 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -5,6 +5,7 @@ import warnings from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -42,7 +43,7 @@ @skip_unless_feature("jpg") class TestFileJpeg: - def roundtrip(self, im, **options): + def roundtrip(self, im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "JPEG", **options) test_bytes = out.tell() @@ -51,7 +52,7 @@ def roundtrip(self, im, **options): im.bytes = test_bytes # for testing only return im - def gen_random_image(self, size, mode: str = "RGB"): + def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image: """Generates a very hard to compress file :param size: tuple :param mode: optional image mode @@ -71,7 +72,7 @@ def test_sanity(self) -> None: assert im.get_format_mimetype() == "image/jpeg" @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero(self, size, tmp_path: Path) -> None: + def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") im = Image.new("RGB", size) with pytest.raises(ValueError): @@ -108,13 +109,11 @@ def test_comment_write(self) -> None: assert "comment" not in reloaded.info # Test that a comment argument overrides the default comment - for comment in ("Test comment text", b"Text comment text"): + for comment in ("Test comment text", b"Test comment text"): out = BytesIO() im.save(out, format="JPEG", comment=comment) with Image.open(out) as reloaded: - if not isinstance(comment, bytes): - comment = comment.encode() - assert reloaded.info["comment"] == comment + assert reloaded.info["comment"] == b"Test comment text" def test_cmyk(self) -> None: # Test CMYK handling. Thanks to Tim and Charlie for test data, @@ -145,7 +144,7 @@ def test_cmyk(self) -> None: assert k > 0.9 def test_rgb(self) -> None: - def getchannels(im): + def getchannels(im: Image.Image) -> tuple[int, int, int]: return tuple(v[0] for v in im.layer) im = hopper() @@ -161,8 +160,8 @@ def getchannels(im): "test_image_path", [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], ) - def test_dpi(self, test_image_path) -> None: - def test(xdpi, ydpi=None): + def test_dpi(self, test_image_path: str) -> None: + def test(xdpi: int, ydpi: int | None = None): with Image.open(test_image_path) as im: im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) return im.info.get("dpi") @@ -207,7 +206,7 @@ def test_icc(self, tmp_path: Path) -> None: ImageFile.MAXBLOCK * 4 + 3, # large block ), ) - def test_icc_big(self, n) -> None: + def test_icc_big(self, n: int) -> None: # Make sure that the "extra" support handles large blocks # The ICC APP marker can store 65519 bytes per marker, so # using a 4-byte test code should allow us to detect out of @@ -433,7 +432,7 @@ def test_smooth(self) -> None: assert_image(im1, im2.mode, im2.size) def test_subsampling(self) -> None: - def getsampling(im): + def getsampling(im: Image.Image): layer = im.layer return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] @@ -530,7 +529,7 @@ def test_truncated_jpeg_throws_oserror(self) -> None: pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) def test_qtables(self, tmp_path: Path) -> None: - def _n_qtables_helper(n, test_file) -> None: + def _n_qtables_helper(n: int, test_file: str) -> None: with Image.open(test_file) as im: f = str(tmp_path / "temp.jpg") im.save(f, qtables=[[n] * 64] * n) @@ -666,7 +665,7 @@ def test_save_low_quality_baseline_qtables(self) -> None: "blocks, rows, markers", ((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)), ) - def test_restart_markers(self, blocks, rows, markers) -> None: + def test_restart_markers(self, blocks: int, rows: int, markers: int) -> None: im = Image.new("RGB", (32, 32)) # 16 MCUs out = BytesIO() im.save( @@ -724,13 +723,13 @@ def test_bad_mpo_header(self) -> None: assert im.format == "JPEG" @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) - def test_save_correct_modes(self, mode) -> None: + def test_save_correct_modes(self, mode: str) -> None: out = BytesIO() img = Image.new(mode, (20, 20)) img.save(out, "JPEG") @pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P")) - def test_save_wrong_modes(self, mode) -> None: + def test_save_wrong_modes(self, mode: str) -> None: # ref https://github.com/python-pillow/Pillow/issues/2005 out = BytesIO() img = Image.new(mode, (20, 20)) @@ -982,12 +981,12 @@ def test_eof(self) -> None: # Even though this decoder never says that it is finished # the image should still end when there is no new data class InfiniteMockPyDecoder(ImageFile.PyDecoder): - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: return 0, 0 decoder = InfiniteMockPyDecoder(None) - def closure(mode, *args): + def closure(mode: str, *args) -> InfiniteMockPyDecoder: decoder.__init__(mode, *args) return decoder diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index e3f1fa8fde1..fab19e2eab4 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -4,6 +4,7 @@ import re from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -36,7 +37,7 @@ # 'Not enough memory to handle tile data' -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "JPEG2000", **options) test_bytes = out.tell() @@ -138,7 +139,7 @@ def test_prog_res_rt() -> None: @pytest.mark.parametrize("num_resolutions", range(2, 6)) -def test_default_num_resolutions(num_resolutions) -> None: +def test_default_num_resolutions(num_resolutions: int) -> None: d = 1 << (num_resolutions - 1) im = test_card.resize((d - 1, d - 1)) with pytest.raises(OSError): @@ -198,9 +199,9 @@ def test_layers_type(tmp_path: Path) -> None: for quality_layers in [[100, 50, 10], (100, 50, 10), None]: test_card.save(outfile, quality_layers=quality_layers) - for quality_layers in ["quality_layers", ("100", "50", "10")]: + for quality_layers_str in ["quality_layers", ("100", "50", "10")]: with pytest.raises(ValueError): - test_card.save(outfile, quality_layers=quality_layers) + test_card.save(outfile, quality_layers=quality_layers_str) def test_layers() -> None: @@ -233,7 +234,7 @@ def test_layers() -> None: ("foo.jp2", {"no_jp2": False}, 4, b"jP"), ), ) -def test_no_jp2(name, args, offset, data) -> None: +def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: out = BytesIO() if name: out.name = name @@ -278,7 +279,7 @@ def test_sgnd(tmp_path: Path) -> None: @pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_rgba(ext) -> None: +def test_rgba(ext: str) -> None: # Arrange with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im: # Act @@ -289,7 +290,7 @@ def test_rgba(ext) -> None: @pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_16bit_monochrome_has_correct_mode(ext) -> None: +def test_16bit_monochrome_has_correct_mode(ext: str) -> None: with Image.open("Tests/images/16bit.cropped" + ext) as im: im.load() assert im.mode == "I;16" @@ -346,12 +347,12 @@ def test_parser_feed() -> None: not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) @pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2")) -def test_subsampling_decode(name) -> None: +def test_subsampling_decode(name: str) -> None: test = f"{EXTRA_DIR}/{name}.jp2" reference = f"{EXTRA_DIR}/{name}.ppm" with Image.open(test) as im: - epsilon = 3 # for YCbCr images + epsilon = 3.0 # for YCbCr images with Image.open(reference) as im2: width, height = im2.size if name[-1] == "2": @@ -400,7 +401,7 @@ def test_save_comment() -> None: "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", ], ) -def test_crashes(test_file) -> None: +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: # Valgrind should not complain here diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 1386034e5a3..0994d99040e 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -27,7 +27,7 @@ @skip_unless_feature("libtiff") class LibTiffTestCase: - def _assert_noerr(self, tmp_path: Path, im) -> None: + def _assert_noerr(self, tmp_path: Path, im: Image.Image) -> None: """Helper tests that assert basic sanity about the g4 tiff reading""" # 1 bit assert im.mode == "1" @@ -140,7 +140,7 @@ def test_adobe_deflate_tiff(self) -> None: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") @pytest.mark.parametrize("legacy_api", (False, True)) - def test_write_metadata(self, legacy_api, tmp_path: Path) -> None: + def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None: """Test metadata writing through libtiff""" f = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper_g4.tif") as img: @@ -243,7 +243,7 @@ def test_additional_metadata(self, tmp_path: Path) -> None: TiffImagePlugin.WRITE_LIBTIFF = False def test_custom_metadata(self, tmp_path: Path) -> None: - tc = namedtuple("test_case", "value,type,supported_by_default") + tc = namedtuple("tc", "value,type,supported_by_default") custom = { 37000 + k: v for k, v in enumerate( @@ -284,7 +284,9 @@ def test_custom_metadata(self, tmp_path: Path) -> None: for libtiff in libtiffs: TiffImagePlugin.WRITE_LIBTIFF = libtiff - def check_tags(tiffinfo) -> None: + def check_tags( + tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] + ) -> None: im = hopper() out = str(tmp_path / "temp.tif") @@ -502,7 +504,7 @@ def test_cmyk_save(self, tmp_path: Path) -> None: assert_image_equal_tofile(im, out) @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) - def test_palette_save(self, im, tmp_path: Path) -> None: + def test_palette_save(self, im: Image.Image, tmp_path: Path) -> None: out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True @@ -514,7 +516,7 @@ def test_palette_save(self, im, tmp_path: Path) -> None: assert len(reloaded.tag_v2[320]) == 768 @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) - def test_bw_compression_w_rgb(self, compression, tmp_path: Path) -> None: + def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") @@ -647,7 +649,7 @@ def test_save_bytesio(self) -> None: # Generate test image pilim = hopper() - def save_bytesio(compression=None) -> None: + def save_bytesio(compression: str | None = None) -> None: buffer_io = io.BytesIO() pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) @@ -731,7 +733,7 @@ def test_read_icc(self) -> None: assert icc == icc_libtiff def test_write_icc(self, tmp_path: Path) -> None: - def check_write(libtiff) -> None: + def check_write(libtiff: bool) -> None: TiffImagePlugin.WRITE_LIBTIFF = libtiff with Image.open("Tests/images/hopper.iccprofile.tif") as img: @@ -837,7 +839,7 @@ def test_sampleformat_write(self, tmp_path: Path) -> None: assert reloaded.mode == "F" assert reloaded.getexif()[SAMPLEFORMAT] == 3 - def test_lzma(self, capfd): + def test_lzma(self, capfd: pytest.CaptureFixture[str]) -> None: try: with Image.open("Tests/images/hopper_lzma.tif") as im: assert im.mode == "RGB" @@ -853,7 +855,7 @@ def test_lzma(self, capfd): sys.stderr.write(captured.err) raise - def test_webp(self, capfd): + def test_webp(self, capfd: pytest.CaptureFixture[str]) -> None: try: with Image.open("Tests/images/hopper_webp.tif") as im: assert im.mode == "RGB" @@ -971,7 +973,7 @@ def test_strip_planar_16bit_RGBa(self) -> None: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") @pytest.mark.parametrize("compression", (None, "jpeg")) - def test_block_tile_tags(self, compression, tmp_path: Path) -> None: + def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.tif") @@ -1020,7 +1022,9 @@ def test_open_missing_samplesperpixel(self) -> None: ), ], ) - def test_wrong_bits_per_sample(self, file_name, mode, size, tile) -> None: + def test_wrong_bits_per_sample( + self, file_name: str, mode: str, size: tuple[int, int], tile + ) -> None: with Image.open("Tests/images/" + file_name) as im: assert im.mode == mode assert im.size == size @@ -1086,7 +1090,7 @@ def test_realloc_overflow(self) -> None: TiffImagePlugin.READ_LIBTIFF = False @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) - def test_save_multistrip(self, compression, tmp_path: Path) -> None: + def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") im.save(out, compression=compression) @@ -1096,14 +1100,14 @@ def test_save_multistrip(self, compression, tmp_path: Path) -> None: assert len(im.tag_v2[STRIPOFFSETS]) > 1 @pytest.mark.parametrize("argument", (True, False)) - def test_save_single_strip(self, argument, tmp_path: Path) -> None: + def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") if not argument: TiffImagePlugin.STRIP_SIZE = 2**18 try: - arguments = {"compression": "tiff_adobe_deflate"} + arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} if argument: arguments["strip_size"] = 2**18 im.save(out, **arguments) @@ -1114,7 +1118,7 @@ def test_save_single_strip(self, argument, tmp_path: Path) -> None: TiffImagePlugin.STRIP_SIZE = 65536 @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) - def test_save_zero(self, compression, tmp_path: Path) -> None: + def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: im = Image.new("RGB", (0, 0)) out = str(tmp_path / "temp.tif") with pytest.raises(SystemError): @@ -1134,7 +1138,7 @@ def test_save_many_compressed(self, tmp_path: Path) -> None: ("Tests/images/child_ifd_jpeg.tiff", (20,)), ), ) - def test_get_child_images(self, path, sizes) -> None: + def test_get_child_images(self, path: str, sizes: tuple[int, ...]) -> None: with Image.open(path) as im: ims = im.get_child_images() diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0f1d96365ea..d4a63431647 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -6,6 +6,7 @@ import zlib from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -36,7 +37,7 @@ MAGIC = PngImagePlugin._MAGIC -def chunk(cid, *data): +def chunk(cid: bytes, *data: bytes) -> bytes: test_file = BytesIO() PngImagePlugin.putchunk(*(test_file, cid) + data) return test_file.getvalue() @@ -52,11 +53,11 @@ def chunk(cid, *data): TAIL = IDAT + IEND -def load(data): +def load(data: bytes) -> Image.Image: return Image.open(BytesIO(data)) -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "PNG", **options) out.seek(0) @@ -65,7 +66,7 @@ def roundtrip(im, **options): @skip_unless_feature("zlib") class TestFilePng: - def get_chunks(self, filename): + def get_chunks(self, filename: str) -> list[bytes]: chunks = [] with open(filename, "rb") as fp: fp.read(8) @@ -436,7 +437,7 @@ def test_nonunicode_text(self) -> None: def test_unicode_text(self) -> None: # Check preservation of non-ASCII characters - def rt_text(value) -> None: + def rt_text(value: str) -> None: im = Image.new("RGB", (32, 32)) info = PngImagePlugin.PngInfo() info.add_text("Text", value) @@ -636,7 +637,7 @@ def test_padded_idat(self) -> None: @pytest.mark.parametrize( "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") ) - def test_truncated_chunks(self, cid) -> None: + def test_truncated_chunks(self, cid: bytes) -> None: fp = BytesIO() with PngImagePlugin.PngStream(fp) as png: with pytest.raises(ValueError): @@ -755,7 +756,7 @@ def test_seek(self) -> None: im.seek(1) @pytest.mark.parametrize("buffer", (True, False)) - def test_save_stdout(self, buffer) -> None: + def test_save_stdout(self, buffer: bool) -> None: old_stdout = sys.stdout if buffer: diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 34a2f8f3db5..c4d7a5dd254 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -11,10 +11,9 @@ class TestImagingPaste: masks = {} size = 128 - def assert_9points_image(self, im, expected) -> None: - expected = [ - point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected - ] + def assert_9points_image( + self, im: Image.Image, expected: list[tuple[int, int, int, int]] + ) -> None: px = im.load() actual = [ px[0, 0], @@ -27,9 +26,17 @@ def assert_9points_image(self, im, expected) -> None: px[self.size // 2, self.size - 1], px[self.size - 1, self.size - 1], ] - assert actual == expected + assert actual == [ + point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected + ] - def assert_9points_paste(self, im, im2, mask, expected) -> None: + def assert_9points_paste( + self, + im: Image.Image, + im2: Image.Image, + mask: Image.Image, + expected: list[tuple[int, int, int, int]], + ) -> None: im3 = im.copy() im3.paste(im2, (0, 0), mask) self.assert_9points_image(im3, expected) @@ -106,7 +113,7 @@ def gradient_RGBa(self): ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_solid(self, mode) -> None: + def test_image_solid(self, mode: str) -> None: im = Image.new(mode, (200, 200), "red") im2 = getattr(self, "gradient_" + mode) @@ -116,7 +123,7 @@ def test_image_solid(self, mode) -> None: assert_image_equal(im, im2) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_1(self, mode) -> None: + def test_image_mask_1(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -138,7 +145,7 @@ def test_image_mask_1(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_L(self, mode) -> None: + def test_image_mask_L(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -160,7 +167,7 @@ def test_image_mask_L(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_LA(self, mode) -> None: + def test_image_mask_LA(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -182,7 +189,7 @@ def test_image_mask_LA(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBA(self, mode) -> None: + def test_image_mask_RGBA(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -204,7 +211,7 @@ def test_image_mask_RGBA(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBa(self, mode) -> None: + def test_image_mask_RGBa(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -226,7 +233,7 @@ def test_image_mask_RGBa(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_solid(self, mode) -> None: + def test_color_solid(self, mode: str) -> None: im = Image.new(mode, (200, 200), "black") rect = (12, 23, 128 + 12, 128 + 23) @@ -239,7 +246,7 @@ def test_color_solid(self, mode) -> None: assert sum(head[:255]) == 0 @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_1(self, mode) -> None: + def test_color_mask_1(self, mode: str) -> None: im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) color = (10, 20, 30, 40)[: len(mode)] @@ -261,7 +268,7 @@ def test_color_mask_1(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_L(self, mode) -> None: + def test_color_mask_L(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -283,7 +290,7 @@ def test_color_mask_L(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBA(self, mode) -> None: + def test_color_mask_RGBA(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -305,7 +312,7 @@ def test_color_mask_RGBA(self, mode) -> None: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBa(self, mode) -> None: + def test_color_mask_RGBa(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index c29830a7e5f..33b33d6b7fc 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -48,7 +48,7 @@ ((1, 3), (10, 4)), ), ) -def test_args_factor(size, expected) -> None: +def test_args_factor(size: int | tuple[int, int], expected: tuple[int, int]) -> None: im = Image.new("L", (10, 10)) assert expected == im.reduce(size).size @@ -56,7 +56,7 @@ def test_args_factor(size, expected) -> None: @pytest.mark.parametrize( "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) ) -def test_args_factor_error(size, expected_error) -> None: +def test_args_factor_error(size: float | tuple[int, int], expected_error) -> None: im = Image.new("L", (10, 10)) with pytest.raises(expected_error): im.reduce(size) @@ -69,7 +69,7 @@ def test_args_factor_error(size, expected_error) -> None: ((5, 5, 6, 6), (1, 1)), ), ) -def test_args_box(size, expected) -> None: +def test_args_box(size: tuple[int, int, int, int], expected: tuple[int, int]) -> None: im = Image.new("L", (10, 10)) assert expected == im.reduce(2, size).size @@ -86,20 +86,20 @@ def test_args_box(size, expected) -> None: ((5, 0, 5, 10), ValueError), ), ) -def test_args_box_error(size, expected_error) -> None: +def test_args_box_error(size: str | tuple[int, int, int, int], expected_error) -> None: im = Image.new("L", (10, 10)) with pytest.raises(expected_error): im.reduce(2, size).size @pytest.mark.parametrize("mode", ("P", "1", "I;16")) -def test_unsupported_modes(mode) -> None: +def test_unsupported_modes(mode: str) -> None: im = Image.new("P", (10, 10)) with pytest.raises(ValueError): im.reduce(3) -def get_image(mode): +def get_image(mode: str) -> Image.Image: mode_info = ImageMode.getmode(mode) if mode_info.basetype == "L": bands = [gradients_image] @@ -119,7 +119,7 @@ def get_image(mode): return im.crop((0, 0, im.width, im.height - 5)) -def compare_reduce_with_box(im, factor) -> None: +def compare_reduce_with_box(im: Image.Image, factor: int | tuple[int, int]) -> None: box = (11, 13, 146, 164) reduced = im.reduce(factor, box=box) reference = im.crop(box).reduce(factor) @@ -127,7 +127,10 @@ def compare_reduce_with_box(im, factor) -> None: def compare_reduce_with_reference( - im, factor, average_diff: float = 0.4, max_diff: int = 1 + im: Image.Image, + factor: int | tuple[int, int], + average_diff: float = 0.4, + max_diff: int = 1, ) -> None: """Image.reduce() should look very similar to Image.resize(BOX). @@ -173,7 +176,9 @@ def compare_reduce_with_reference( assert_compare_images(reduced, reference, average_diff, max_diff) -def assert_compare_images(a, b, max_average_diff, max_diff: int = 255) -> None: +def assert_compare_images( + a: Image.Image, b: Image.Image, max_average_diff: float, max_diff: int = 255 +) -> None: assert a.mode == b.mode, f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, f"got size {repr(a.size)}, expected {repr(b.size)}" @@ -201,20 +206,20 @@ def assert_compare_images(a, b, max_average_diff, max_diff: int = 255) -> None: @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_L(factor) -> None: +def test_mode_L(factor: int | tuple[int, int]) -> None: im = get_image("L") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA(factor) -> None: +def test_mode_LA(factor: int | tuple[int, int]) -> None: im = get_image("LA") compare_reduce_with_reference(im, factor, 0.8, 5) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA_opaque(factor) -> None: +def test_mode_LA_opaque(factor: int | tuple[int, int]) -> None: im = get_image("LA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) @@ -223,27 +228,27 @@ def test_mode_LA_opaque(factor) -> None: @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_La(factor) -> None: +def test_mode_La(factor: int | tuple[int, int]) -> None: im = get_image("La") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGB(factor) -> None: +def test_mode_RGB(factor: int | tuple[int, int]) -> None: im = get_image("RGB") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA(factor) -> None: +def test_mode_RGBA(factor: int | tuple[int, int]) -> None: im = get_image("RGBA") compare_reduce_with_reference(im, factor, 0.8, 5) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA_opaque(factor) -> None: +def test_mode_RGBA_opaque(factor: int | tuple[int, int]) -> None: im = get_image("RGBA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) @@ -252,21 +257,21 @@ def test_mode_RGBA_opaque(factor) -> None: @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBa(factor) -> None: +def test_mode_RGBa(factor: int | tuple[int, int]) -> None: im = get_image("RGBa") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_I(factor) -> None: +def test_mode_I(factor: int | tuple[int, int]) -> None: im = get_image("I") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_F(factor) -> None: +def test_mode_F(factor: int | tuple[int, int]) -> None: im = get_image("F") compare_reduce_with_reference(im, factor, 0, 0) compare_reduce_with_box(im, factor) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index f4c9eb0e6fa..f3ec12c053b 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,6 +1,7 @@ from __future__ import annotations from contextlib import contextmanager +from typing import Generator import pytest @@ -51,7 +52,7 @@ def test_modify_after_resizing(self) -> None: class TestImagingCoreResampleAccuracy: - def make_case(self, mode, size, color): + def make_case(self, mode: str, size: tuple[int, int], color: int) -> Image.Image: """Makes a sample image with two dark and two bright squares. For example: e0 e0 1f 1f @@ -66,7 +67,7 @@ def make_case(self, mode, size, color): return Image.merge(mode, [case] * len(mode)) - def make_sample(self, data, size): + def make_sample(self, data: str, size: tuple[int, int]) -> Image.Image: """Restores a sample image from given data string which contains hex-encoded pixels from the top left fourth of a sample. """ @@ -83,7 +84,7 @@ def make_sample(self, data, size): s_px[size[0] - x - 1, y] = 255 - val return sample - def check_case(self, case, sample) -> None: + def check_case(self, case: Image.Image, sample: Image.Image) -> None: s_px = sample.load() c_px = case.load() for y in range(case.size[1]): @@ -95,7 +96,7 @@ def check_case(self, case, sample) -> None: ) assert s_px[x, y] == c_px[x, y], message - def serialize_image(self, image): + def serialize_image(self, image: Image.Image) -> str: s_px = image.load() return "\n".join( " ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0])) @@ -103,7 +104,7 @@ def serialize_image(self, image): ) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_box(self, mode) -> None: + def test_reduce_box(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off @@ -114,7 +115,7 @@ def test_reduce_box(self, mode) -> None: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bilinear(self, mode) -> None: + def test_reduce_bilinear(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off @@ -125,7 +126,7 @@ def test_reduce_bilinear(self, mode) -> None: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_hamming(self, mode) -> None: + def test_reduce_hamming(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off @@ -136,7 +137,7 @@ def test_reduce_hamming(self, mode) -> None: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bicubic(self, mode) -> None: + def test_reduce_bicubic(self, mode: str) -> None: case = self.make_case(mode, (12, 12), 0xE1) case = case.resize((6, 6), Image.Resampling.BICUBIC) # fmt: off @@ -148,7 +149,7 @@ def test_reduce_bicubic(self, mode) -> None: self.check_case(channel, self.make_sample(data, (6, 6))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_lanczos(self, mode) -> None: + def test_reduce_lanczos(self, mode: str) -> None: case = self.make_case(mode, (16, 16), 0xE1) case = case.resize((8, 8), Image.Resampling.LANCZOS) # fmt: off @@ -161,7 +162,7 @@ def test_reduce_lanczos(self, mode) -> None: self.check_case(channel, self.make_sample(data, (8, 8))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_box(self, mode) -> None: + def test_enlarge_box(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off @@ -172,7 +173,7 @@ def test_enlarge_box(self, mode) -> None: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bilinear(self, mode) -> None: + def test_enlarge_bilinear(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off @@ -183,7 +184,7 @@ def test_enlarge_bilinear(self, mode) -> None: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_hamming(self, mode) -> None: + def test_enlarge_hamming(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off @@ -194,7 +195,7 @@ def test_enlarge_hamming(self, mode) -> None: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bicubic(self, mode) -> None: + def test_enlarge_bicubic(self, mode: str) -> None: case = self.make_case(mode, (4, 4), 0xE1) case = case.resize((8, 8), Image.Resampling.BICUBIC) # fmt: off @@ -207,7 +208,7 @@ def test_enlarge_bicubic(self, mode) -> None: self.check_case(channel, self.make_sample(data, (8, 8))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_lanczos(self, mode) -> None: + def test_enlarge_lanczos(self, mode: str) -> None: case = self.make_case(mode, (6, 6), 0xE1) case = case.resize((12, 12), Image.Resampling.LANCZOS) data = ( @@ -230,7 +231,7 @@ def test_box_filter_correct_range(self) -> None: class TestCoreResampleConsistency: - def make_case(self, mode, fill): + def make_case(self, mode: str, fill: tuple[int, int, int] | float): im = Image.new(mode, (512, 9), fill) return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] @@ -265,7 +266,7 @@ def test_32f(self) -> None: class TestCoreResampleAlphaCorrect: - def make_levels_case(self, mode): + def make_levels_case(self, mode: str) -> Image.Image: i = Image.new(mode, (256, 16)) px = i.load() for y in range(i.size[1]): @@ -275,7 +276,7 @@ def make_levels_case(self, mode): px[x, y] = tuple(pix) return i - def run_levels_case(self, i) -> None: + def run_levels_case(self, i: Image.Image) -> None: px = i.load() for y in range(i.size[1]): used_colors = {px[x, y][0] for x in range(i.size[0])} @@ -302,7 +303,9 @@ def test_levels_la(self) -> None: self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) - def make_dirty_case(self, mode, clean_pixel, dirty_pixel): + def make_dirty_case( + self, mode: str, clean_pixel: tuple[int, ...], dirty_pixel: tuple[int, ...] + ) -> Image.Image: i = Image.new(mode, (64, 64), dirty_pixel) px = i.load() xdiv4 = i.size[0] // 4 @@ -312,7 +315,7 @@ def make_dirty_case(self, mode, clean_pixel, dirty_pixel): px[x + xdiv4, y + ydiv4] = clean_pixel return i - def run_dirty_case(self, i, clean_pixel) -> None: + def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None: px = i.load() for y in range(i.size[1]): for x in range(i.size[0]): @@ -432,7 +435,7 @@ class TestCoreResampleBox: Image.Resampling.LANCZOS, ), ) - def test_wrong_arguments(self, resample) -> None: + def test_wrong_arguments(self, resample: Image.Resampling) -> None: im = hopper() im.resize((32, 32), resample, (0, 0, im.width, im.height)) im.resize((32, 32), resample, (20, 20, im.width, im.height)) @@ -459,8 +462,12 @@ def test_wrong_arguments(self, resample) -> None: with pytest.raises(ValueError, match="can't exceed"): im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) - def resize_tiled(self, im, dst_size, xtiles, ytiles): - def split_range(size, tiles): + def resize_tiled( + self, im: Image.Image, dst_size: tuple[int, int], xtiles: int, ytiles: int + ) -> Image.Image: + def split_range( + size: int, tiles: int + ) -> Generator[tuple[int, int], None, None]: scale = size / tiles for i in range(tiles): yield int(round(scale * i)), int(round(scale * (i + 1))) @@ -518,7 +525,7 @@ def test_subsample(self) -> None: @pytest.mark.parametrize( "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR) ) - def test_formats(self, mode, resample) -> None: + def test_formats(self, mode: str, resample: Image.Resampling) -> None: im = hopper(mode) box = (20, 20, im.size[0] - 20, im.size[1] - 20) with_box = im.resize((32, 32), resample, box) @@ -558,7 +565,7 @@ def test_no_passthrough(self) -> None: @pytest.mark.parametrize( "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) ) - def test_skip_horizontal(self, flt) -> None: + def test_skip_horizontal(self, flt: Image.Resampling) -> None: # Can skip resize for one dimension im = hopper() @@ -581,7 +588,7 @@ def test_skip_horizontal(self, flt) -> None: @pytest.mark.parametrize( "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) ) - def test_skip_vertical(self, flt) -> None: + def test_skip_vertical(self, flt: Image.Resampling) -> None: # Can skip resize for one dimension im = hopper() diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index ea6e80f1ee7..b65ea874063 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -5,11 +5,11 @@ from PIL import Image, ImageMath -def pixel(im): - if hasattr(im, "im"): - return f"{im.mode} {repr(im.getpixel((0, 0)))}" +def pixel(im: Image.Image | int) -> str | int: if isinstance(im, int): return int(im) # hack to deal with booleans + else: + return f"{im.mode} {repr(im.getpixel((0, 0)))}" A = Image.new("L", (1, 1), 1) @@ -60,7 +60,7 @@ def test_ops() -> None: "(lambda: (lambda: exec('pass'))())()", ), ) -def test_prevent_exec(expression) -> None: +def test_prevent_exec(expression: str) -> None: with pytest.raises(ValueError): ImageMath.eval(expression) From 463c36821136652a05517a4db810a265d25c9b0c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 7 Feb 2024 21:02:34 +1100 Subject: [PATCH 0171/2374] Simplified code Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_imagemath.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index b65ea874063..a21e2307d5f 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -8,8 +8,8 @@ def pixel(im: Image.Image | int) -> str | int: if isinstance(im, int): return int(im) # hack to deal with booleans - else: - return f"{im.mode} {repr(im.getpixel((0, 0)))}" + + return f"{im.mode} {repr(im.getpixel((0, 0)))}" A = Image.new("L", (1, 1), 1) From c93b23239d4cbe8b8c6d4d6c04db35763a25db62 Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Wed, 7 Feb 2024 20:20:27 -0500 Subject: [PATCH 0172/2374] Update src/_webp.c Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/_webp.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index 4e7d41f1121..927d8dc3f4c 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -448,18 +448,19 @@ PyObject * _anim_decoder_get_next(PyObject *self) { uint8_t *buf; int timestamp; + int ok; PyObject *bytes; PyObject *ret; ImagingSectionCookie cookie; WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; ImagingSectionEnter(&cookie); - if (!WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp)) { - ImagingSectionLeave(&cookie); + ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp) + ImagingSectionLeave(&cookie); + if (!ok) { PyErr_SetString(PyExc_OSError, "failed to read next frame"); return NULL; } - ImagingSectionLeave(&cookie); bytes = PyBytes_FromStringAndSize( (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height); From cb39b1c89e71f67ce4dacd41cebf723ff86306dd Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 8 Feb 2024 12:29:06 +1100 Subject: [PATCH 0173/2374] Corrected syntax --- src/_webp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_webp.c b/src/_webp.c index 927d8dc3f4c..47592547cc2 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -455,7 +455,7 @@ _anim_decoder_get_next(PyObject *self) { WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; ImagingSectionEnter(&cookie); - ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp) + ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp); ImagingSectionLeave(&cookie); if (!ok) { PyErr_SetString(PyExc_OSError, "failed to read next frame"); From a276cf2c9fadf39cc5e663e44bc160c566d7c050 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 Feb 2024 18:48:38 +1100 Subject: [PATCH 0174/2374] Use _typing alias --- src/PIL/ImageFont.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 9f8394d63b4..7be2fdf0445 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -33,10 +33,10 @@ import warnings from enum import IntEnum from io import BytesIO -from pathlib import Path from typing import BinaryIO from . import Image +from ._typing import StrOrBytesPath from ._util import is_directory, is_path @@ -193,7 +193,7 @@ class FreeTypeFont: def __init__( self, - font: bytes | str | Path | BinaryIO | None = None, + font: StrOrBytesPath | BinaryIO | None = None, size: float = 10, index: int = 0, encoding: str = "", From a118a82c30acf6427653b129fab263fde3bdbbac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 Feb 2024 18:35:37 +1100 Subject: [PATCH 0175/2374] Use os.path.realpath consistently when os.fspath is used --- src/PIL/Image.py | 2 +- src/PIL/ImageFont.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d7d0a1ae7f0..adb63b07f79 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2385,7 +2385,7 @@ def save(self, fp, format=None, **params) -> None: filename = "" open_fp = False if is_path(fp): - filename = os.fspath(fp) + filename = os.path.realpath(os.fspath(fp)) open_fp = True elif fp == sys.stdout: try: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 7be2fdf0445..256c581df0c 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -230,7 +230,7 @@ def load_from_bytes(f): ) if is_path(font): - font = os.fspath(font) + font = os.path.realpath(os.fspath(font)) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() try: From e6a521130e975f89529442597735a6a48cc3685f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Feb 2024 19:47:09 +1100 Subject: [PATCH 0176/2374] If previous disposal was 2, do not fill identical pixels --- Tests/test_file_gif.py | 3 +++ src/PIL/GifImagePlugin.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index db9d3586c4d..0399c6b6798 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -647,6 +647,9 @@ def test_dispose2_palette(tmp_path: Path) -> None: # Center remains red every frame assert rgb_img.getpixel((50, 50)) == circle + # Check that frame transparency wasn't added unnecessarily + assert img._frame_transparency is None + def test_dispose2_diff(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index dc842d7a30b..73a5487d9a7 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -638,7 +638,11 @@ def _write_multiple_frames(im, fp, palette): background_im = Image.new("P", im_frame.size, background) background_im.putpalette(im_frames[0]["im"].palette) delta, bbox = _getbbox(background_im, im_frame) - if encoderinfo.get("optimize") and im_frame.mode != "1": + if ( + encoderinfo.get("optimize") + and im_frames[-1]["encoderinfo"].get("disposal") != 2 + and im_frame.mode != "1" + ): if "transparency" not in encoderinfo: try: encoderinfo["transparency"] = ( From 152a24e13abfe099d4cf75dc7982290feb200ad2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 16:48:02 +1100 Subject: [PATCH 0177/2374] Simplified code --- src/PIL/Image.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index adb63b07f79..231674f5448 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3240,10 +3240,8 @@ def open(fp, mode="r", formats=None) -> Image: exclusive_fp = False filename = "" - if isinstance(fp, os.PathLike): + if is_path(fp): filename = os.path.realpath(os.fspath(fp)) - elif is_path(fp): - filename = fp if filename: fp = builtins.open(filename, "rb") From 373c62e5cbcd27a4e497e61b44cda89911f38807 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 19:47:50 +1100 Subject: [PATCH 0178/2374] Use subprocess with CREATE_NO_WINDOW flag in WindowsViewer --- src/PIL/ImageShow.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index c03122c11aa..4a801e5b0fb 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -138,6 +138,17 @@ def get_command(self, file: str, **options: Any) -> str: f'&& del /f "{file}"' ) + def show_file(self, path: str, **options: Any) -> int: + """ + Display given file. + """ + subprocess.Popen( + self.get_command(path, **options), + shell=True, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW"), + ) # nosec + return 1 + if sys.platform == "win32": register(WindowsViewer) From 19a6edeecce2f3605fcdb074c00ac0152c6bdf05 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 19:50:45 +1100 Subject: [PATCH 0179/2374] Added type hints --- pyproject.toml | 10 ---- src/PIL/DdsImagePlugin.py | 16 ++++-- src/PIL/ImImagePlugin.py | 4 +- src/PIL/Image.py | 86 +++++++++++++++++++---------- src/PIL/ImageQt.py | 12 +++- src/PIL/PdfParser.py | 11 +++- src/PIL/PyAccess.py | 1 + src/PIL/TiffImagePlugin.py | 109 +++++++++++++++++++++---------------- src/PIL/TiffTags.py | 4 +- src/PIL/_imaging.pyi | 5 ++ src/PIL/_tkinter_finder.py | 3 +- src/PIL/_webp.pyi | 5 ++ tox.ini | 5 ++ 13 files changed, 171 insertions(+), 100 deletions(-) create mode 100644 src/PIL/_imaging.pyi create mode 100644 src/PIL/_webp.pyi diff --git a/pyproject.toml b/pyproject.toml index 652ae36333c..48c59f2a1b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,16 +141,6 @@ warn_redundant_casts = true warn_unreachable = true warn_unused_ignores = true exclude = [ - '^src/PIL/_tkinter_finder.py$', - '^src/PIL/DdsImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$', - '^src/PIL/Image.py$', - '^src/PIL/ImageQt.py$', - '^src/PIL/ImImagePlugin.py$', '^src/PIL/MicImagePlugin.py$', - '^src/PIL/PdfParser.py$', - '^src/PIL/PyAccess.py$', - '^src/PIL/TiffImagePlugin.py$', - '^src/PIL/TiffTags.py$', - '^src/PIL/WebPImagePlugin.py$', ] diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 3785174ef65..be17f4223aa 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -270,13 +270,17 @@ class D3DFMT(IntEnum): # Backward compatibility layer module = sys.modules[__name__] for item in DDSD: + assert item.name is not None setattr(module, "DDSD_" + item.name, item.value) -for item in DDSCAPS: - setattr(module, "DDSCAPS_" + item.name, item.value) -for item in DDSCAPS2: - setattr(module, "DDSCAPS2_" + item.name, item.value) -for item in DDPF: - setattr(module, "DDPF_" + item.name, item.value) +for item1 in DDSCAPS: + assert item1.name is not None + setattr(module, "DDSCAPS_" + item1.name, item1.value) +for item2 in DDSCAPS2: + assert item2.name is not None + setattr(module, "DDSCAPS2_" + item2.name, item2.value) +for item3 in DDPF: + assert item3.name is not None + setattr(module, "DDPF_" + item3.name, item3.value) DDS_FOURCC = DDPF.FOURCC DDS_RGB = DDPF.RGB diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 97d726a8a65..4613e40b60f 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -93,8 +93,8 @@ for i in ["32S"]: OPEN[f"L {i} image"] = ("I", f"I;{i}") OPEN[f"L*{i} image"] = ("I", f"I;{i}") -for i in range(2, 33): - OPEN[f"L*{i} image"] = ("F", f"F;{i}") +for j in range(2, 33): + OPEN[f"L*{j} image"] = ("F", f"F;{j}") # -------------------------------------------------------------------- diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 111d060129e..d32a0fc199e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -26,6 +26,7 @@ from __future__ import annotations +import abc import atexit import builtins import io @@ -40,11 +41,8 @@ from collections.abc import Callable, MutableMapping from enum import IntEnum from pathlib import Path - -try: - from defusedxml import ElementTree -except ImportError: - ElementTree = None +from types import ModuleType +from typing import IO, TYPE_CHECKING, Any # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -60,6 +58,12 @@ from ._binary import i32le, o32be, o32le from ._util import DeferredError, is_path +ElementTree: ModuleType | None +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None + logger = logging.getLogger(__name__) @@ -110,6 +114,7 @@ class DecompressionBombError(Exception): USE_CFFI_ACCESS = False +cffi: ModuleType | None try: import cffi except ImportError: @@ -211,14 +216,22 @@ class Quantize(IntEnum): # -------------------------------------------------------------------- # Registries -ID = [] -OPEN = {} -MIME = {} -SAVE = {} -SAVE_ALL = {} -EXTENSION = {} -DECODERS = {} -ENCODERS = {} +if TYPE_CHECKING: + from . import ImageFile # pragma: no cover +ID: list[str] = [] +OPEN: dict[ + str, + tuple[ + Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + Callable[[bytes], bool] | None, + ], +] = {} +MIME: dict[str, str] = {} +SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} +SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} +EXTENSION: dict[str, str] = {} +DECODERS: dict[str, object] = {} +ENCODERS: dict[str, object] = {} # -------------------------------------------------------------------- # Modes @@ -2383,12 +2396,12 @@ def save(self, fp, format=None, **params) -> None: may have been created, and may contain partial data. """ - filename = "" + filename: str | bytes = "" open_fp = False if isinstance(fp, Path): filename = str(fp) open_fp = True - elif is_path(fp): + elif isinstance(fp, (str, bytes)): filename = fp open_fp = True elif fp == sys.stdout: @@ -2398,7 +2411,7 @@ def save(self, fp, format=None, **params) -> None: pass if not filename and hasattr(fp, "name") and is_path(fp.name): # only set the name for metadata purposes - filename = fp.name + filename = os.path.realpath(os.fspath(fp.name)) # may mutate self! self._ensure_mutable() @@ -2409,7 +2422,8 @@ def save(self, fp, format=None, **params) -> None: preinit() - ext = os.path.splitext(filename)[1].lower() + filename_ext = os.path.splitext(filename)[1].lower() + ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext if not format: if ext not in EXTENSION: @@ -2451,7 +2465,7 @@ def save(self, fp, format=None, **params) -> None: if open_fp: fp.close() - def seek(self, frame) -> Image: + def seek(self, frame) -> None: """ Seeks to the given frame in this sequence file. If you seek beyond the end of the sequence, the method raises an @@ -2511,10 +2525,9 @@ def split(self) -> tuple[Image, ...]: self.load() if self.im.bands == 1: - ims = [self.copy()] + return (self.copy(),) else: - ims = map(self._new, self.im.split()) - return tuple(ims) + return tuple(map(self._new, self.im.split())) def getchannel(self, channel): """ @@ -2871,7 +2884,14 @@ class ImageTransformHandler: (for use with :py:meth:`~PIL.Image.Image.transform`) """ - pass + @abc.abstractmethod + def transform( + self, + size: tuple[int, int], + image: Image, + **options: dict[str, str | int | tuple[int, ...] | list[int]], + ) -> Image: + pass # pragma: no cover # -------------------------------------------------------------------- @@ -3243,11 +3263,9 @@ def open(fp, mode="r", formats=None) -> Image: raise TypeError(msg) exclusive_fp = False - filename = "" - if isinstance(fp, Path): - filename = str(fp.resolve()) - elif is_path(fp): - filename = fp + filename: str | bytes = "" + if is_path(fp): + filename = os.path.realpath(os.fspath(fp)) if filename: fp = builtins.open(filename, "rb") @@ -3421,7 +3439,11 @@ def merge(mode, bands): # Plugin registry -def register_open(id, factory, accept=None) -> None: +def register_open( + id, + factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + accept: Callable[[bytes], bool] | None = None, +) -> None: """ Register an image file plugin. This function should not be used in application code. @@ -3631,7 +3653,13 @@ def _apply_env_variables(env=None): atexit.register(core.clear_cache) -class Exif(MutableMapping): +if TYPE_CHECKING: + _ExifBase = MutableMapping[int, Any] # pragma: no cover +else: + _ExifBase = MutableMapping + + +class Exif(_ExifBase): """ This class provides read and write access to EXIF image data:: diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 6377c750105..293ba4941e3 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,19 +19,26 @@ import sys from io import BytesIO +from typing import Callable from . import Image from ._util import is_path +qt_version: str | None qt_versions = [ ["6", "PyQt6"], ["side6", "PySide6"], ] # If a version has already been imported, attempt it first -qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True) -for qt_version, qt_module in qt_versions: +qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True) +for version, qt_module in qt_versions: try: + QBuffer: type + QIODevice: type + QImage: type + QPixmap: type + qRgba: Callable[[int, int, int, int], int] if qt_module == "PyQt6": from PyQt6.QtCore import QBuffer, QIODevice from PyQt6.QtGui import QImage, QPixmap, qRgba @@ -41,6 +48,7 @@ except (ImportError, RuntimeError): continue qt_is_installed = True + qt_version = version break else: qt_is_installed = False diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 0144600066f..9aa8dde8327 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -8,6 +8,7 @@ import re import time import zlib +from typing import TYPE_CHECKING, Any, List, Union # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set @@ -239,12 +240,18 @@ def __bytes__(self): return bytes(result) -class PdfArray(list): +class PdfArray(List[Any]): def __bytes__(self): return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" -class PdfDict(collections.UserDict): +if TYPE_CHECKING: + _DictBase = collections.UserDict[Union[str, bytes], Any] # pragma: no cover +else: + _DictBase = collections.UserDict + + +class PdfDict(_DictBase): def __setattr__(self, key, value): if key == "data": collections.UserDict.__setattr__(self, key, value) diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 07bb712d83e..2c831913d69 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -25,6 +25,7 @@ from ._deprecate import deprecate +FFI: type try: from cffi import FFI diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index e20d4d5ea81..af22d76cbf5 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -50,6 +50,7 @@ from collections.abc import MutableMapping from fractions import Fraction from numbers import Number, Rational +from typing import TYPE_CHECKING, Any, Callable from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 @@ -306,6 +307,13 @@ def _limit_signed_rational(val, max_val, min_val): _write_dispatch = {} +def _delegate(op): + def delegate(self, *args): + return getattr(self._val, op)(*args) + + return delegate + + class IFDRational(Rational): """Implements a rational class where 0/0 is a legal value to match the in the wild use of exif rationals. @@ -391,12 +399,6 @@ def __setstate__(self, state): self._numerator = _numerator self._denominator = _denominator - def _delegate(op): - def delegate(self, *args): - return getattr(self._val, op)(*args) - - return delegate - """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', 'mod','rmod', 'pow','rpow', 'pos', 'neg', @@ -436,7 +438,50 @@ def delegate(self, *args): __int__ = _delegate("__int__") -class ImageFileDirectory_v2(MutableMapping): +def _register_loader(idx, size): + def decorator(func): + from .TiffTags import TYPES + + if func.__name__.startswith("load_"): + TYPES[idx] = func.__name__[5:].replace("_", " ") + _load_dispatch[idx] = size, func # noqa: F821 + return func + + return decorator + + +def _register_writer(idx): + def decorator(func): + _write_dispatch[idx] = func # noqa: F821 + return func + + return decorator + + +def _register_basic(idx_fmt_name): + from .TiffTags import TYPES + + idx, fmt, name = idx_fmt_name + TYPES[idx] = name + size = struct.calcsize("=" + fmt) + _load_dispatch[idx] = ( # noqa: F821 + size, + lambda self, data, legacy_api=True: ( + self._unpack(f"{len(data) // size}{fmt}", data) + ), + ) + _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 + b"".join(self._pack(fmt, value) for value in values) + ) + + +if TYPE_CHECKING: + _IFDv2Base = MutableMapping[int, Any] # pragma: no cover +else: + _IFDv2Base = MutableMapping + + +class ImageFileDirectory_v2(_IFDv2Base): """This class represents a TIFF tag directory. To speed things up, we don't decode tags unless they're asked for. @@ -497,6 +542,9 @@ class ImageFileDirectory_v2(MutableMapping): """ + _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} + _write_dispatch: dict[int, Callable[..., Any]] = {} + def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): """Initialize an ImageFileDirectory. @@ -531,7 +579,10 @@ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): prefix = property(lambda self: self._prefix) offset = property(lambda self: self._offset) - legacy_api = property(lambda self: self._legacy_api) + + @property + def legacy_api(self): + return self._legacy_api @legacy_api.setter def legacy_api(self, value): @@ -674,40 +725,6 @@ def _unpack(self, fmt, data): def _pack(self, fmt, *values): return struct.pack(self._endian + fmt, *values) - def _register_loader(idx, size): - def decorator(func): - from .TiffTags import TYPES - - if func.__name__.startswith("load_"): - TYPES[idx] = func.__name__[5:].replace("_", " ") - _load_dispatch[idx] = size, func # noqa: F821 - return func - - return decorator - - def _register_writer(idx): - def decorator(func): - _write_dispatch[idx] = func # noqa: F821 - return func - - return decorator - - def _register_basic(idx_fmt_name): - from .TiffTags import TYPES - - idx, fmt, name = idx_fmt_name - TYPES[idx] = name - size = struct.calcsize("=" + fmt) - _load_dispatch[idx] = ( # noqa: F821 - size, - lambda self, data, legacy_api=True: ( - self._unpack(f"{len(data) // size}{fmt}", data) - ), - ) - _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 - b"".join(self._pack(fmt, value) for value in values) - ) - list( map( _register_basic, @@ -995,7 +1012,7 @@ def __init__(self, *args, **kwargs): tagdata = property(lambda self: self._tagdata) # defined in ImageFileDirectory_v2 - tagtype: dict + tagtype: dict[int, int] """Dictionary of tag types""" @classmethod @@ -1835,11 +1852,11 @@ def _save(im, fp, filename): tags = list(atts.items()) tags.sort() a = (rawmode, compression, _fp, filename, tags, types) - e = Image._getencoder(im.mode, "libtiff", a, encoderconfig) - e.setimage(im.im, (0, 0) + im.size) + encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) + encoder.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - errcode, data = e.encode(16 * 1024)[1:] + errcode, data = encoder.encode(16 * 1024)[1:] if not _fp: fp.write(data) if errcode: diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 88ff2f4fcd5..b9419393119 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -22,7 +22,7 @@ class TagInfo(namedtuple("_TagInfo", "value name type length enum")): - __slots__ = [] + __slots__: list[str] = [] def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): return super().__new__(cls, value, name, type, length, enum or {}) @@ -437,7 +437,7 @@ def _populate(): ## # Map type numbers to type names -- defined in ImageFileDirectory. -TYPES = {} +TYPES: dict[int, str] = {} # # These tags are handled by default in libtiff, without diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi new file mode 100644 index 00000000000..b0235555dc5 --- /dev/null +++ b/src/PIL/_imaging.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 71c0ad4651b..beddfb0628a 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -5,7 +5,8 @@ import sys import tkinter -from tkinter import _tkinter as tk + +tk = getattr(tkinter, "_tkinter") try: if hasattr(sys, "pypy_find_executable"): diff --git a/src/PIL/_webp.pyi b/src/PIL/_webp.pyi new file mode 100644 index 00000000000..b0235555dc5 --- /dev/null +++ b/src/PIL/_webp.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/tox.ini b/tox.ini index fb6746ce7bf..8c818df7a6a 100644 --- a/tox.ini +++ b/tox.ini @@ -33,9 +33,14 @@ commands = [testenv:mypy] skip_install = true deps = + IceSpringPySideStubs-PyQt6 + IceSpringPySideStubs-PySide6 ipython mypy==1.7.1 numpy + packaging + types-cffi + types-defusedxml extras = typing commands = From 517b797132a65a8a873d0c22008d760d2a706ff6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 20:47:32 +1100 Subject: [PATCH 0180/2374] Removed FileDescriptor --- src/PIL/GdImageFile.py | 6 ++---- src/PIL/_typing.py | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 315ac6d6c1a..88b87a22cd6 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -32,7 +32,7 @@ class is not registered for use with :py:func:`PIL.Image.open()`. To open a from . import ImageFile, ImagePalette, UnidentifiedImageError from ._binary import i16be as i16 from ._binary import i32be as i32 -from ._typing import FileDescriptor, StrOrBytesPath +from ._typing import StrOrBytesPath class GdImageFile(ImageFile.ImageFile): @@ -81,9 +81,7 @@ def _open(self) -> None: ] -def open( - fp: StrOrBytesPath | FileDescriptor | IO[bytes], mode: str = "r" -) -> GdImageFile: +def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile: """ Load texture from a GD image file. diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 346702037d2..7075e86726a 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -27,8 +27,7 @@ class SupportsRead(Protocol[_T_co]): def read(self, __length: int = ...) -> _T_co: ... -FileDescriptor = int StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] -__all__ = ["FileDescriptor", "TypeGuard", "StrOrBytesPath", "SupportsRead"] +__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"] From 430f50606e2fc5620c6605cd43eb360be6bbd655 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 21:33:32 +1100 Subject: [PATCH 0181/2374] Current delta is determined by previous disposal --- Tests/test_file_gif.py | 19 +++++++++++++++++++ src/PIL/GifImagePlugin.py | 10 +++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 0399c6b6798..6527d90de96 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -737,6 +737,25 @@ def test_dispose2_background_frame(tmp_path: Path) -> None: assert im.n_frames == 3 +def test_dispose2_previous_frame(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im = Image.new("P", (100, 100)) + im.info["transparency"] = 0 + d = ImageDraw.Draw(im) + d.rectangle([(0, 0), (100, 50)], 1) + im.putpalette((0, 0, 0, 255, 0, 0)) + + im2 = Image.new("P", (100, 100)) + im2.putpalette((0, 0, 0)) + + im.save(out, save_all=True, append_images=[im2], disposal=[0, 2]) + + with Image.open(out) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (0, 0, 0, 255) + + def test_transparency_in_second_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/different_transparency.gif") as im: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 73a5487d9a7..9368dd7e7c4 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -629,7 +629,7 @@ def _write_multiple_frames(im, fp, palette): "duration" ] continue - if encoderinfo.get("disposal") == 2: + if im_frames[-1]["encoderinfo"].get("disposal") == 2: if background_im is None: color = im.encoderinfo.get( "transparency", im.info.get("transparency", (0, 0, 0)) @@ -637,12 +637,8 @@ def _write_multiple_frames(im, fp, palette): background = _get_background(im_frame, color) background_im = Image.new("P", im_frame.size, background) background_im.putpalette(im_frames[0]["im"].palette) - delta, bbox = _getbbox(background_im, im_frame) - if ( - encoderinfo.get("optimize") - and im_frames[-1]["encoderinfo"].get("disposal") != 2 - and im_frame.mode != "1" - ): + bbox = _getbbox(background_im, im_frame)[1] + elif encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: try: encoderinfo["transparency"] = ( From 68db96981c0819efc51bea995915eaa389a292e7 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 10 Feb 2024 21:50:48 +1100 Subject: [PATCH 0182/2374] Removed else Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/Image.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d32a0fc199e..c3ab6217452 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2526,8 +2526,7 @@ def split(self) -> tuple[Image, ...]: self.load() if self.im.bands == 1: return (self.copy(),) - else: - return tuple(map(self._new, self.im.split())) + return tuple(map(self._new, self.im.split())) def getchannel(self, channel): """ From d02a778efd443db9f69233763f187e14eebde6db Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 10 Feb 2024 21:57:59 +1100 Subject: [PATCH 0183/2374] Removed no cover pragmas Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/Image.py | 6 +++--- src/PIL/PdfParser.py | 2 +- src/PIL/TiffImagePlugin.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c3ab6217452..d9d708d5da4 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -217,7 +217,7 @@ class Quantize(IntEnum): # Registries if TYPE_CHECKING: - from . import ImageFile # pragma: no cover + from . import ImageFile ID: list[str] = [] OPEN: dict[ str, @@ -2890,7 +2890,7 @@ def transform( image: Image, **options: dict[str, str | int | tuple[int, ...] | list[int]], ) -> Image: - pass # pragma: no cover + pass # -------------------------------------------------------------------- @@ -3653,7 +3653,7 @@ def _apply_env_variables(env=None): if TYPE_CHECKING: - _ExifBase = MutableMapping[int, Any] # pragma: no cover + _ExifBase = MutableMapping[int, Any] else: _ExifBase = MutableMapping diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 9aa8dde8327..4c510173814 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -246,7 +246,7 @@ def __bytes__(self): if TYPE_CHECKING: - _DictBase = collections.UserDict[Union[str, bytes], Any] # pragma: no cover + _DictBase = collections.UserDict[Union[str, bytes], Any] else: _DictBase = collections.UserDict diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index af22d76cbf5..3ba4de9d12e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -476,7 +476,7 @@ def _register_basic(idx_fmt_name): if TYPE_CHECKING: - _IFDv2Base = MutableMapping[int, Any] # pragma: no cover + _IFDv2Base = MutableMapping[int, Any] else: _IFDv2Base = MutableMapping From 8ef0ffc2b849245bde6f96b58b4af48bf498bda7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 22:37:42 +1100 Subject: [PATCH 0184/2374] Removed no cover pragma --- src/PIL/ImageShow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index c03122c11aa..d90545e92ec 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -184,7 +184,7 @@ class UnixViewer(Viewer): @abc.abstractmethod def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - pass # pragma: no cover + pass def get_command(self, file: str, **options: Any) -> str: command = self.get_command_ex(file, **options)[0] From e614bbfe501811bcb4a080ba9f07d745ba3ad231 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 22:39:18 +1100 Subject: [PATCH 0185/2374] Exclude code only for type checking --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 46df3f90d27..115286b749c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,7 @@ exclude_also = if DEBUG: # Don't complain about compatibility code for missing optional dependencies except ImportError + if TYPE_CHECKING: [run] omit = From 112a5a4813f34235530c2ac382108a5cf722789a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 22:40:24 +1100 Subject: [PATCH 0186/2374] Exclude abstract methods --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 115286b749c..ca5f114c645 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,6 +11,7 @@ exclude_also = # Don't complain about compatibility code for missing optional dependencies except ImportError if TYPE_CHECKING: + @abc.abstractmethod [run] omit = From 3977124908b934a9b037d1e8ba5549393bed9dda Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 10 Feb 2024 14:54:20 +0200 Subject: [PATCH 0187/2374] Update docs/reference/internal_modules.rst Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/reference/internal_modules.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index c3cc700607f..899e4966ff2 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -33,10 +33,6 @@ Internal Modules Provides a convenient way to import type hints that are not available on some Python versions. -.. py:class:: FileDescriptor - - Typing alias. - .. py:class:: StrOrBytesPath Typing alias. From 3f6422b512ff39cffaa5a37915c970d7683b6d62 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Feb 2024 09:28:53 +1100 Subject: [PATCH 0188/2374] Added type hints --- Tests/helper.py | 2 +- Tests/test_image_access.py | 47 ++++++++++++++++++-------------- Tests/test_image_array.py | 10 ++++--- Tests/test_image_draft.py | 7 ++++- Tests/test_image_entropy.py | 2 +- Tests/test_image_filter.py | 27 ++++++++++++------ Tests/test_image_getextrema.py | 2 +- Tests/test_image_getpalette.py | 2 +- Tests/test_image_paste.py | 14 +++++----- Tests/test_image_putdata.py | 6 ++-- Tests/test_image_putpalette.py | 4 +-- Tests/test_image_resample.py | 8 ++++-- Tests/test_image_rotate.py | 14 +++++++--- Tests/test_image_thumbnail.py | 2 +- Tests/test_image_transform.py | 50 ++++++++++++++++++++++++---------- 15 files changed, 124 insertions(+), 73 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 3e2a40e02da..b9888394659 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -244,7 +244,7 @@ def fromstring(data: bytes) -> Image.Image: return Image.open(BytesIO(data)) -def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes: +def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes: out = BytesIO() im.save(out, string_format, **options) return out.getvalue() diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index e4cb2dad102..3bdaea75026 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -4,6 +4,7 @@ import subprocess import sys import sysconfig +from types import ModuleType import pytest @@ -23,6 +24,7 @@ except ImportError: cffi = None +numpy: ModuleType | None try: import numpy except ImportError: @@ -71,9 +73,10 @@ def test_sanity(self) -> None: pix1 = im1.load() pix2 = im2.load() - for x, y in ((0, "0"), ("0", 0)): - with pytest.raises(TypeError): - pix1[x, y] + with pytest.raises(TypeError): + pix1[0, "0"] + with pytest.raises(TypeError): + pix1["0", 0] for y in range(im1.size[1]): for x in range(im1.size[0]): @@ -123,12 +126,13 @@ def test_numpy(self) -> None: im = hopper() pix = im.load() + assert numpy is not None assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) class TestImageGetPixel(AccessTest): @staticmethod - def color(mode): + def color(mode: str) -> int | tuple[int, ...]: bands = Image.getmodebands(mode) if bands == 1: return 1 @@ -138,12 +142,13 @@ def color(mode): return (16, 32, 49) return tuple(range(1, bands + 1)) - def check(self, mode, expected_color=None) -> None: + def check(self, mode: str, expected_color_int: int | None = None) -> None: if self._need_cffi_access and mode.startswith("BGR;"): pytest.skip("Support not added to deprecated module for BGR;* modes") - if not expected_color: - expected_color = self.color(mode) + expected_color = ( + expected_color_int if expected_color_int is not None else self.color(mode) + ) # check putpixel im = Image.new(mode, (1, 1), None) @@ -222,7 +227,7 @@ def check(self, mode, expected_color=None) -> None: "YCbCr", ), ) - def test_basic(self, mode) -> None: + def test_basic(self, mode: str) -> None: self.check(mode) def test_list(self) -> None: @@ -231,14 +236,14 @@ def test_list(self) -> None: @pytest.mark.parametrize("mode", ("I;16", "I;16B")) @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) - def test_signedness(self, mode, expected_color) -> None: + def test_signedness(self, mode: str, expected_color: int) -> None: # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* self.check(mode, expected_color) @pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) - def test_p_putpixel_rgb_rgba(self, mode, color) -> None: + def test_p_putpixel_rgb_rgba(self, mode: str, color: tuple[int, ...]) -> None: im = Image.new(mode, (1, 1)) im.putpixel((0, 0), color) @@ -262,7 +267,7 @@ class TestCffiGetPixel(TestImageGetPixel): class TestCffi(AccessTest): _need_cffi_access = True - def _test_get_access(self, im) -> None: + def _test_get_access(self, im: Image.Image) -> None: """Do we get the same thing as the old pixel access Using private interfaces, forcing a capi access and @@ -299,7 +304,7 @@ def test_get_vs_c(self) -> None: # im = Image.new('I;32B', (10, 10), 2**10) # self._test_get_access(im) - def _test_set_access(self, im, color) -> None: + def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None: """Are we writing the correct bits into the image? Using private interfaces, forcing a capi access and @@ -359,7 +364,7 @@ def test_reference_counting(self) -> None: assert px[i, 0] == 0 @pytest.mark.parametrize("mode", ("P", "PA")) - def test_p_putpixel_rgb_rgba(self, mode) -> None: + def test_p_putpixel_rgb_rgba(self, mode: str) -> None: for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): im = Image.new(mode, (1, 1)) with pytest.warns(DeprecationWarning): @@ -377,7 +382,7 @@ class TestImagePutPixelError(AccessTest): INVALID_TYPES = ["foo", 1.0, None] @pytest.mark.parametrize("mode", IMAGE_MODES1) - def test_putpixel_type_error1(self, mode) -> None: + def test_putpixel_type_error1(self, mode: str) -> None: im = hopper(mode) for v in self.INVALID_TYPES: with pytest.raises(TypeError, match="color must be int or tuple"): @@ -400,14 +405,16 @@ def test_putpixel_type_error1(self, mode) -> None: ), ), ) - def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match) -> None: + def test_putpixel_invalid_number_of_bands( + self, mode: str, band_numbers: tuple[int, ...], match: str + ) -> None: im = hopper(mode) for band_number in band_numbers: with pytest.raises(TypeError, match=match): im.putpixel((0, 0), (0,) * band_number) @pytest.mark.parametrize("mode", IMAGE_MODES2) - def test_putpixel_type_error2(self, mode) -> None: + def test_putpixel_type_error2(self, mode: str) -> None: im = hopper(mode) for v in self.INVALID_TYPES: with pytest.raises( @@ -416,7 +423,7 @@ def test_putpixel_type_error2(self, mode) -> None: im.putpixel((0, 0), v) @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) - def test_putpixel_overflow_error(self, mode) -> None: + def test_putpixel_overflow_error(self, mode: str) -> None: im = hopper(mode) with pytest.raises(OverflowError): im.putpixel((0, 0), 2**80) @@ -428,7 +435,7 @@ class TestEmbeddable: def test_embeddable(self) -> None: import ctypes - from setuptools.command.build_ext import new_compiler + from setuptools.command import build_ext with open("embed_pil.c", "w", encoding="utf-8") as fh: fh.write( @@ -457,7 +464,7 @@ def test_embeddable(self) -> None: % sys.prefix.replace("\\", "\\\\") ) - compiler = new_compiler() + compiler = getattr(build_ext, "new_compiler")() compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( @@ -471,7 +478,7 @@ def test_embeddable(self) -> None: env["PATH"] = sys.prefix + ";" + env["PATH"] # do not display the Windows Error Reporting dialog - ctypes.windll.kernel32.SetErrorMode(0x0002) + getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) process = subprocess.Popen(["embed_pil.exe"], env=env) process.communicate() diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 0125ab56af9..cf85ee4fa1c 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + import pytest from packaging.version import parse as parse_version @@ -13,7 +15,7 @@ def test_toarray() -> None: - def test(mode): + def test(mode: str) -> tuple[tuple[int, ...], str, int]: ai = numpy.array(im.convert(mode)) return ai.shape, ai.dtype.str, ai.nbytes @@ -50,14 +52,14 @@ def test_fromarray() -> None: class Wrapper: """Class with API matching Image.fromarray""" - def __init__(self, img, arr_params) -> None: + def __init__(self, img: Image.Image, arr_params: dict[str, Any]) -> None: self.img = img self.__array_interface__ = arr_params - def tobytes(self): + def tobytes(self) -> bytes: return self.img.tobytes() - def test(mode): + def test(mode: str) -> tuple[str, tuple[int, int], bool]: i = im.convert(mode) a = numpy.array(i) # Make wrapper instance for image, new array interface diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 54474311a09..1ce1a7cd8f6 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -7,7 +7,12 @@ pytestmark = skip_unless_feature("jpg") -def draft_roundtrip(in_mode, in_size, req_mode, req_size): +def draft_roundtrip( + in_mode: str, + in_size: tuple[int, int], + req_mode: str | None, + req_size: tuple[int, int] | None, +) -> Image.Image: im = Image.new(in_mode, in_size) data = tostring(im, "JPEG") im = fromstring(data) diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py index 01107ae6b88..c1dbb879b0b 100644 --- a/Tests/test_image_entropy.py +++ b/Tests/test_image_entropy.py @@ -4,7 +4,7 @@ def test_entropy() -> None: - def entropy(mode): + def entropy(mode: str) -> float: return hopper(mode).entropy() assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0 diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 2b6787933cd..6a10ae4532b 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -36,7 +36,7 @@ ), ) @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity(filter_to_apply, mode) -> None: +def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None: im = hopper(mode) if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): out = im.filter(filter_to_apply) @@ -45,7 +45,7 @@ def test_sanity(filter_to_apply, mode) -> None: @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity_error(mode) -> None: +def test_sanity_error(mode: str) -> None: with pytest.raises(TypeError): im = hopper(mode) im.filter("hello") @@ -53,7 +53,7 @@ def test_sanity_error(mode) -> None: # crashes on small images @pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3))) -def test_crash(size) -> None: +def test_crash(size: tuple[int, int]) -> None: im = Image.new("RGB", size) im.filter(ImageFilter.SMOOTH) @@ -67,7 +67,10 @@ def test_crash(size) -> None: ("RGB", ((4, 0, 0), (0, 0, 0))), ), ) -def test_modefilter(mode, expected) -> None: +def test_modefilter( + mode: str, + expected: tuple[int, int] | tuple[tuple[int, int, int], tuple[int, int, int]], +) -> None: im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) # image is: @@ -90,7 +93,13 @@ def test_modefilter(mode, expected) -> None: ("F", (0.0, 4.0, 8.0)), ), ) -def test_rankfilter(mode, expected) -> None: +def test_rankfilter( + mode: str, + expected: ( + tuple[float, float, float] + | tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]] + ), +) -> None: im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) # image is: @@ -106,7 +115,7 @@ def test_rankfilter(mode, expected) -> None: @pytest.mark.parametrize( "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter) ) -def test_rankfilter_error(filter) -> None: +def test_rankfilter_error(filter: ImageFilter.RankFilter) -> None: with pytest.raises(ValueError): im = Image.new("P", (3, 3), None) im.putdata(list(range(9))) @@ -137,7 +146,7 @@ def test_kernel_not_enough_coefficients() -> None: @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_3x3(mode) -> None: +def test_consistency_3x3(mode: str) -> None: with Image.open("Tests/images/hopper.bmp") as source: reference_name = "hopper_emboss" reference_name += "_I.png" if mode == "I" else ".bmp" @@ -163,7 +172,7 @@ def test_consistency_3x3(mode) -> None: @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_5x5(mode) -> None: +def test_consistency_5x5(mode: str) -> None: with Image.open("Tests/images/hopper.bmp") as source: reference_name = "hopper_emboss_more" reference_name += "_I.png" if mode == "I" else ".bmp" @@ -199,7 +208,7 @@ def test_consistency_5x5(mode) -> None: (2, -2), ), ) -def test_invalid_box_blur_filter(radius) -> None: +def test_invalid_box_blur_filter(radius: int | tuple[int, int]) -> None: with pytest.raises(ValueError): ImageFilter.BoxBlur(radius) diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index 0107fdcc426..a5b974459a6 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -6,7 +6,7 @@ def test_extrema() -> None: - def extrema(mode): + def extrema(mode: str) -> tuple[int, int] | tuple[tuple[int, int], ...]: return hopper(mode).getextrema() assert extrema("1") == (0, 255) diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index e7304c98f30..6a8f157fc3e 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -6,7 +6,7 @@ def test_palette() -> None: - def palette(mode): + def palette(mode: str) -> list[int] | None: p = hopper(mode).getpalette() if p: return p[:10] diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index c4d7a5dd254..ce73455729a 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -46,7 +46,7 @@ def assert_9points_paste( self.assert_9points_image(im, expected) @CachedProperty - def mask_1(self): + def mask_1(self) -> Image.Image: mask = Image.new("1", (self.size, self.size)) px = mask.load() for y in range(mask.height): @@ -55,11 +55,11 @@ def mask_1(self): return mask @CachedProperty - def mask_L(self): + def mask_L(self) -> Image.Image: return self.gradient_L.transpose(Image.Transpose.ROTATE_270) @CachedProperty - def gradient_L(self): + def gradient_L(self) -> Image.Image: gradient = Image.new("L", (self.size, self.size)) px = gradient.load() for y in range(gradient.height): @@ -68,7 +68,7 @@ def gradient_L(self): return gradient @CachedProperty - def gradient_RGB(self): + def gradient_RGB(self) -> Image.Image: return Image.merge( "RGB", [ @@ -79,7 +79,7 @@ def gradient_RGB(self): ) @CachedProperty - def gradient_LA(self): + def gradient_LA(self) -> Image.Image: return Image.merge( "LA", [ @@ -89,7 +89,7 @@ def gradient_LA(self): ) @CachedProperty - def gradient_RGBA(self): + def gradient_RGBA(self) -> Image.Image: return Image.merge( "RGBA", [ @@ -101,7 +101,7 @@ def gradient_RGBA(self): ) @CachedProperty - def gradient_RGBa(self): + def gradient_RGBa(self) -> Image.Image: return Image.merge( "RGBa", [ diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 10301991624..73145faac15 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -31,7 +31,7 @@ def test_sanity() -> None: def test_long_integers() -> None: # see bug-200802-systemerror - def put(value): + def put(value: int) -> tuple[int, int, int, int]: im = Image.new("RGBA", (1, 1)) im.putdata([value]) return im.getpixel((0, 0)) @@ -58,7 +58,7 @@ def test_mode_with_L_with_float() -> None: @pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) -def test_mode_i(mode) -> None: +def test_mode_i(mode: str) -> None: src = hopper("L") data = list(src.getdata()) im = Image.new(mode, src.size, 0) @@ -79,7 +79,7 @@ def test_mode_F() -> None: @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) -def test_mode_BGR(mode) -> None: +def test_mode_BGR(mode: str) -> None: data = [(16, 32, 49), (32, 32, 98)] im = Image.new(mode, (1, 2)) im.putdata(data) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index ffe2551d239..cc7cf58f0fc 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -8,7 +8,7 @@ def test_putpalette() -> None: - def palette(mode): + def palette(mode: str) -> str | tuple[str, list[int]]: im = hopper(mode).copy() im.putpalette(list(range(256)) * 3) p = im.getpalette() @@ -81,7 +81,7 @@ def test_putpalette_with_alpha_values() -> None: ("RGBAX", (1, 2, 3, 4, 0)), ), ) -def test_rgba_palette(mode, palette) -> None: +def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None: im = Image.new("P", (1, 1)) im.putpalette(palette, mode) assert im.getpalette() == [1, 2, 3] diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index f3ec12c053b..7090ff9cdb8 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -231,11 +231,13 @@ def test_box_filter_correct_range(self) -> None: class TestCoreResampleConsistency: - def make_case(self, mode: str, fill: tuple[int, int, int] | float): + def make_case( + self, mode: str, fill: tuple[int, int, int] | float + ) -> tuple[Image.Image, tuple[int, ...]]: im = Image.new(mode, (512, 9), fill) return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] - def run_case(self, case) -> None: + def run_case(self, case: tuple[Image.Image, Image.Image]) -> None: channel, color = case px = channel.load() for x in range(channel.size[0]): @@ -353,7 +355,7 @@ def test_dirty_pixels_la(self) -> None: class TestCoreResamplePasses: @contextmanager - def count(self, diff): + def count(self, diff: int) -> Generator[None, None, None]: count = Image.core.get_stats()["new_count"] yield assert Image.core.get_stats()["new_count"] - count == diff diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 51e0f585421..c10c96da6f9 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -12,7 +12,13 @@ ) -def rotate(im, mode, angle, center=None, translate=None) -> None: +def rotate( + im: Image.Image, + mode: str, + angle: int, + center: tuple[int, int] | None = None, + translate: tuple[int, int] | None = None, +) -> None: out = im.rotate(angle, center=center, translate=translate) assert out.mode == mode assert out.size == im.size # default rotate clips output @@ -27,13 +33,13 @@ def rotate(im, mode, angle, center=None, translate=None) -> None: @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_mode(mode) -> None: +def test_mode(mode: str) -> None: im = hopper(mode) rotate(im, mode, 45) @pytest.mark.parametrize("angle", (0, 90, 180, 270)) -def test_angle(angle) -> None: +def test_angle(angle: int) -> None: with Image.open("Tests/images/test-card.png") as im: rotate(im, im.mode, angle) @@ -42,7 +48,7 @@ def test_angle(angle) -> None: @pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) -def test_zero(angle) -> None: +def test_zero(angle: int) -> None: im = Image.new("RGB", (0, 0)) rotate(im, im.mode, angle) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 6aeeea2ed0d..2ca1d2cfc03 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -111,7 +111,7 @@ def test_load_first_unless_jpeg() -> None: with Image.open("Tests/images/hopper.jpg") as im: draft = im.draft - def im_draft(mode, size): + def im_draft(mode: str, size: tuple[int, int]): result = draft(mode, size) assert result is not None diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 1067dd563c1..638d1224710 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,6 +1,7 @@ from __future__ import annotations import math +from typing import Callable import pytest @@ -91,7 +92,7 @@ def test_quad(self) -> None: ("LA", (76, 0)), ), ) - def test_fill(self, mode, expected_pixel) -> None: + def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None: im = hopper(mode) (w, h) = im.size transformed = im.transform( @@ -142,7 +143,9 @@ def test_mesh(self) -> None: assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2))) assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h))) - def _test_alpha_premult(self, op) -> None: + def _test_alpha_premult( + self, op: Callable[[Image.Image, tuple[int, int]], Image.Image] + ) -> None: # create image with half white, half black, # with the black half transparent. # do op, @@ -159,13 +162,13 @@ def _test_alpha_premult(self, op) -> None: assert 40 * 10 == hist[-1] def test_alpha_premult_resize(self) -> None: - def op(im, sz): + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: return im.resize(sz, Image.Resampling.BILINEAR) self._test_alpha_premult(op) def test_alpha_premult_transform(self) -> None: - def op(im, sz): + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: (w, h) = im.size return im.transform( sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR @@ -173,7 +176,9 @@ def op(im, sz): self._test_alpha_premult(op) - def _test_nearest(self, op, mode) -> None: + def _test_nearest( + self, op: Callable[[Image.Image, tuple[int, int]], Image.Image], mode: str + ) -> None: # create white image with half transparent, # do op, # the image should remain white with half transparent @@ -196,15 +201,15 @@ def _test_nearest(self, op, mode) -> None: ) @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_resize(self, mode) -> None: - def op(im, sz): + def test_nearest_resize(self, mode: str) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: return im.resize(sz, Image.Resampling.NEAREST) self._test_nearest(op, mode) @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_transform(self, mode) -> None: - def op(im, sz): + def test_nearest_transform(self, mode: str) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: (w, h) = im.size return im.transform( sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST @@ -227,7 +232,9 @@ def test_blank_fill(self) -> None: # Running by default, but I'd totally understand not doing it in # the future - pattern = [Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)] + pattern: list[Image.Image] | None = [ + Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65) + ] # Yeah. Watch some JIT optimize this out. pattern = None # noqa: F841 @@ -240,7 +247,7 @@ def test_missing_method_data(self) -> None: im.transform((100, 100), None) @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) - def test_unknown_resampling_filter(self, resample) -> None: + def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None: with hopper() as im: (w, h) = im.size with pytest.raises(ValueError): @@ -250,7 +257,7 @@ def test_unknown_resampling_filter(self, resample) -> None: class TestImageTransformAffine: transform = Image.Transform.AFFINE - def _test_image(self): + def _test_image(self) -> Image.Image: im = hopper("RGB") return im.crop((10, 20, im.width - 10, im.height - 20)) @@ -263,7 +270,7 @@ def _test_image(self): (270, Image.Transpose.ROTATE_270), ), ) - def test_rotate(self, deg, transpose) -> None: + def test_rotate(self, deg: int, transpose: Image.Transpose | None) -> None: im = self._test_image() angle = -math.radians(deg) @@ -313,7 +320,13 @@ def test_rotate(self, deg, transpose) -> None: (Image.Resampling.BICUBIC, 1), ), ) - def test_resize(self, scale, epsilon_scale, resample, epsilon) -> None: + def test_resize( + self, + scale: float, + epsilon_scale: float, + resample: Image.Resampling, + epsilon: int, + ) -> None: im = self._test_image() size_up = int(round(im.width * scale)), int(round(im.height * scale)) @@ -342,7 +355,14 @@ def test_resize(self, scale, epsilon_scale, resample, epsilon) -> None: (Image.Resampling.BICUBIC, 1), ), ) - def test_translate(self, x, y, epsilon_scale, resample, epsilon) -> None: + def test_translate( + self, + x: float, + y: float, + epsilon_scale: float, + resample: Image.Resampling, + epsilon: float, + ) -> None: im = self._test_image() size_up = int(round(im.width + x)), int(round(im.height + y)) From ea0240bf2d414ff297c7959f904c252bae696ff3 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 12 Feb 2024 19:12:08 +1100 Subject: [PATCH 0189/2374] Use is None Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_image_access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 3bdaea75026..380b89de86f 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -147,7 +147,7 @@ def check(self, mode: str, expected_color_int: int | None = None) -> None: pytest.skip("Support not added to deprecated module for BGR;* modes") expected_color = ( - expected_color_int if expected_color_int is not None else self.color(mode) + self.color(mode) if expected_color_int is None else expected_color_int ) # check putpixel From 4ce06aac3bc6aeb0425f78fc691210a839d6d875 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Feb 2024 21:06:17 +1100 Subject: [PATCH 0190/2374] Added type hints --- Tests/test_color_lut.py | 4 ++++ Tests/test_core_resources.py | 2 +- Tests/test_file_dds.py | 18 ++++++++-------- Tests/test_file_fli.py | 4 ++-- Tests/test_file_gribstub.py | 7 +++--- Tests/test_file_hdf5stub.py | 7 +++--- Tests/test_file_jpeg.py | 22 ++++++++++--------- Tests/test_file_palm.py | 8 +++---- Tests/test_file_tar.py | 2 +- Tests/test_file_webp_metadata.py | 2 ++ Tests/test_image.py | 37 +++++++++++++++++++------------- Tests/test_imagefontctl.py | 14 +++++++----- Tests/test_imagemorph.py | 10 ++++----- Tests/test_imagepalette.py | 2 +- Tests/test_pickle.py | 22 ++++++++++++------- Tests/test_psdraw.py | 4 ++-- Tests/test_sgi_crash.py | 2 +- Tests/test_shell_injection.py | 8 ++++++- Tests/test_tiff_crashes.py | 2 +- Tests/test_tiff_ifdrational.py | 6 +++--- 20 files changed, 108 insertions(+), 75 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 2bb1b57d482..c8886a7796d 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,6 +1,7 @@ from __future__ import annotations from array import array +from types import ModuleType import pytest @@ -8,6 +9,7 @@ from .helper import assert_image_equal +numpy: ModuleType | None try: import numpy except ImportError: @@ -397,6 +399,7 @@ def test_convert_table(self) -> None: @pytest.mark.skipif(numpy is None, reason="NumPy not installed") def test_numpy_sources(self) -> None: + assert numpy is not None table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16) with pytest.raises(ValueError, match="should have either channels"): lut = ImageFilter.Color3DLUT((5, 6, 7), table) @@ -430,6 +433,7 @@ def test_numpy_sources(self) -> None: @pytest.mark.skipif(numpy is None, reason="NumPy not installed") def test_numpy_formats(self) -> None: + assert numpy is not None g = Image.linear_gradient("L") im = Image.merge( "RGB", diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 5eabe8f11ba..2c1de8bc3d2 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -187,6 +187,6 @@ def test_units(self) -> None: {"PILLOW_BLOCKS_MAX": "wat"}, ), ) - def test_warnings(self, var) -> None: + def test_warnings(self, var: dict[str, str]) -> None: with pytest.warns(UserWarning): Image._apply_env_variables(var) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index b78a0dd8109..ebc0e89a1c5 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -48,7 +48,7 @@ TEST_FILE_DX10_BC1_TYPELESS, ), ) -def test_sanity_dxt1_bc1(image_path) -> None: +def test_sanity_dxt1_bc1(image_path: str) -> None: """Check DXT1 and BC1 images can be opened""" with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: target = target.convert("RGBA") @@ -96,7 +96,7 @@ def test_sanity_dxt5() -> None: TEST_FILE_BC4U, ), ) -def test_sanity_ati1_bc4u(image_path) -> None: +def test_sanity_ati1_bc4u(image_path: str) -> None: """Check ATI1 and BC4U images can be opened""" with Image.open(image_path) as im: @@ -117,7 +117,7 @@ def test_sanity_ati1_bc4u(image_path) -> None: TEST_FILE_DX10_BC4_TYPELESS, ), ) -def test_dx10_bc4(image_path) -> None: +def test_dx10_bc4(image_path: str) -> None: """Check DX10 BC4 images can be opened""" with Image.open(image_path) as im: @@ -138,7 +138,7 @@ def test_dx10_bc4(image_path) -> None: TEST_FILE_BC5U, ), ) -def test_sanity_ati2_bc5u(image_path) -> None: +def test_sanity_ati2_bc5u(image_path: str) -> None: """Check ATI2 and BC5U images can be opened""" with Image.open(image_path) as im: @@ -162,7 +162,7 @@ def test_sanity_ati2_bc5u(image_path) -> None: (TEST_FILE_BC5S, TEST_FILE_BC5S), ), ) -def test_dx10_bc5(image_path, expected_path) -> None: +def test_dx10_bc5(image_path: str, expected_path: str) -> None: """Check DX10 BC5 images can be opened""" with Image.open(image_path) as im: @@ -176,7 +176,7 @@ def test_dx10_bc5(image_path, expected_path) -> None: @pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS)) -def test_dx10_bc6h(image_path) -> None: +def test_dx10_bc6h(image_path: str) -> None: """Check DX10 BC6H/BC6HS images can be opened""" with Image.open(image_path) as im: @@ -257,7 +257,7 @@ def test_dx10_r8g8b8a8_unorm_srgb() -> None: ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA), ], ) -def test_uncompressed(mode, size, test_file) -> None: +def test_uncompressed(mode: str, size: tuple[int, int], test_file: str) -> None: """Check uncompressed images can be opened""" with Image.open(test_file) as im: @@ -359,7 +359,7 @@ def test_unsupported_bitcount() -> None: "Tests/images/unimplemented_pfflags.dds", ), ) -def test_not_implemented(test_file) -> None: +def test_not_implemented(test_file: str) -> None: with pytest.raises(NotImplementedError): with Image.open(test_file): pass @@ -381,7 +381,7 @@ def test_save_unsupported_mode(tmp_path: Path) -> None: ("RGBA", "Tests/images/pil123rgba.png"), ], ) -def test_save(mode, test_file, tmp_path: Path) -> None: +def test_save(mode: str, test_file: str, tmp_path: Path) -> None: out = str(tmp_path / "temp.dds") with Image.open(test_file) as im: assert im.mode == mode diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index a673d4af851..fc524721c2b 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -147,7 +147,7 @@ def test_seek() -> None: ], ) @pytest.mark.timeout(timeout=3) -def test_timeouts(test_file) -> None: +def test_timeouts(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): @@ -160,7 +160,7 @@ def test_timeouts(test_file) -> None: "Tests/images/crash-5762152299364352.fli", ], ) -def test_crash(test_file) -> None: +def test_crash(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index 4945468be40..096a5b88b21 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,6 +1,7 @@ from __future__ import annotations from pathlib import Path +from typing import IO import pytest @@ -55,15 +56,15 @@ class TestHandler: loaded = False saved = False - def open(self, im) -> None: + def open(self, im: Image.Image) -> None: self.opened = True - def load(self, im): + def load(self, im: Image.Image) -> Image.Image: self.loaded = True im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename) -> None: + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index ac3d40bf287..f871e2eff16 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,6 +1,7 @@ from __future__ import annotations from pathlib import Path +from typing import IO import pytest @@ -56,15 +57,15 @@ class TestHandler: loaded = False saved = False - def open(self, im) -> None: + def open(self, im: Image.Image) -> None: self.opened = True - def load(self, im): + def load(self, im: Image.Image) -> Image.Image: self.loaded = True im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename) -> None: + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 6b0662e0be3..65424214838 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -5,6 +5,7 @@ import warnings from io import BytesIO from pathlib import Path +from types import ModuleType from typing import Any import pytest @@ -33,6 +34,7 @@ skip_unless_feature, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: @@ -440,25 +442,25 @@ def getsampling(im: Image.Image): for subsampling in (-1, 3): # (default, invalid) im = self.roundtrip(hopper(), subsampling=subsampling) assert getsampling(im) == (2, 2, 1, 1, 1, 1) - for subsampling in (0, "4:4:4"): - im = self.roundtrip(hopper(), subsampling=subsampling) + for subsampling1 in (0, "4:4:4"): + im = self.roundtrip(hopper(), subsampling=subsampling1) assert getsampling(im) == (1, 1, 1, 1, 1, 1) - for subsampling in (1, "4:2:2"): - im = self.roundtrip(hopper(), subsampling=subsampling) + for subsampling1 in (1, "4:2:2"): + im = self.roundtrip(hopper(), subsampling=subsampling1) assert getsampling(im) == (2, 1, 1, 1, 1, 1) - for subsampling in (2, "4:2:0", "4:1:1"): - im = self.roundtrip(hopper(), subsampling=subsampling) + for subsampling1 in (2, "4:2:0", "4:1:1"): + im = self.roundtrip(hopper(), subsampling=subsampling1) assert getsampling(im) == (2, 2, 1, 1, 1, 1) # RGB colorspace - for subsampling in (-1, 0, "4:4:4"): + for subsampling1 in (-1, 0, "4:4:4"): # "4:4:4" doesn't really make sense for RGB, but the conversion # to an integer happens at a higher level - im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling) + im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1) assert getsampling(im) == (1, 1, 1, 1, 1, 1) - for subsampling in (1, "4:2:2", 2, "4:2:0", 3): + for subsampling1 in (1, "4:2:2", 2, "4:2:0", 3): with pytest.raises(OSError): - self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling) + self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1) with pytest.raises(TypeError): self.roundtrip(hopper(), subsampling="1:1:1") diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 55041a4b202..194f39b30b8 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -11,7 +11,7 @@ from .helper import assert_image_equal, hopper, magick_command -def helper_save_as_palm(tmp_path: Path, mode) -> None: +def helper_save_as_palm(tmp_path: Path, mode: str) -> None: # Arrange im = hopper(mode) outfile = str(tmp_path / ("temp_" + mode + ".palm")) @@ -24,7 +24,7 @@ def helper_save_as_palm(tmp_path: Path, mode) -> None: assert os.path.getsize(outfile) > 0 -def open_with_magick(magick, tmp_path: Path, f): +def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image: outfile = str(tmp_path / "temp.png") rc = subprocess.call( magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT @@ -33,7 +33,7 @@ def open_with_magick(magick, tmp_path: Path, f): return Image.open(outfile) -def roundtrip(tmp_path: Path, mode) -> None: +def roundtrip(tmp_path: Path, mode: str) -> None: magick = magick_command() if not magick: return @@ -66,6 +66,6 @@ def test_p_mode(tmp_path: Path) -> None: @pytest.mark.parametrize("mode", ("L", "RGB")) -def test_oserror(tmp_path: Path, mode) -> None: +def test_oserror(tmp_path: Path, mode: str) -> None: with pytest.raises(OSError): helper_save_as_palm(tmp_path, mode) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 44e78e972dc..6217ebedd8a 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -19,7 +19,7 @@ ("jpg", "hopper.jpg", "JPEG"), ), ) -def test_sanity(codec, test_path, format) -> None: +def test_sanity(codec: str, test_path: str, format: str) -> None: if features.check(codec): with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: with Image.open(tar) as im: diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index fea19694109..8759412408c 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -2,6 +2,7 @@ from io import BytesIO from pathlib import Path +from types import ModuleType import pytest @@ -14,6 +15,7 @@ skip_unless_feature("webp_mux"), ] +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: diff --git a/Tests/test_image.py b/Tests/test_image.py index 67a7d7eca54..75b28c2dc07 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -8,6 +8,7 @@ import tempfile import warnings from pathlib import Path +from typing import IO import pytest @@ -61,11 +62,11 @@ class TestImage: "HSV", ), ) - def test_image_modes_success(self, mode) -> None: + def test_image_modes_success(self, mode: str) -> None: Image.new(mode, (1, 1)) @pytest.mark.parametrize("mode", ("", "bad", "very very long")) - def test_image_modes_fail(self, mode) -> None: + def test_image_modes_fail(self, mode: str) -> None: with pytest.raises(ValueError) as e: Image.new(mode, (1, 1)) assert str(e.value) == "unrecognized image mode" @@ -100,7 +101,7 @@ def test_sanity(self) -> None: def test_repr_pretty(self) -> None: class Pretty: - def text(self, text) -> None: + def text(self, text: str) -> None: self.pretty_output = text im = Image.new("L", (100, 100)) @@ -184,7 +185,9 @@ def test_fp_name(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.jpg") class FP: - def write(self, b) -> None: + name: str + + def write(self, b: bytes) -> None: pass fp = FP() @@ -538,7 +541,7 @@ def test_check_size(self) -> None: "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) - def test_empty_image(self, size) -> None: + def test_empty_image(self, size: tuple[int, int]) -> None: Image.new("RGB", size) def test_storage_neg(self) -> None: @@ -565,7 +568,7 @@ def test_linear_gradient_wrong_mode(self) -> None: Image.linear_gradient(wrong_mode) @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) - def test_linear_gradient(self, mode) -> None: + def test_linear_gradient(self, mode: str) -> None: # Arrange target_file = "Tests/images/linear_gradient.png" @@ -590,7 +593,7 @@ def test_radial_gradient_wrong_mode(self) -> None: Image.radial_gradient(wrong_mode) @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) - def test_radial_gradient(self, mode) -> None: + def test_radial_gradient(self, mode: str) -> None: # Arrange target_file = "Tests/images/radial_gradient.png" @@ -665,7 +668,11 @@ def test__new(self) -> None: blank_p.palette = None blank_pa.palette = None - def _make_new(base_image, image, palette_result=None) -> None: + def _make_new( + base_image: Image.Image, + image: Image.Image, + palette_result: ImagePalette.ImagePalette | None = None, + ) -> None: new_image = base_image._new(image.im) assert new_image.mode == image.mode assert new_image.size == image.size @@ -713,7 +720,7 @@ def test_no_new_file_on_error(self, tmp_path: Path) -> None: def test_load_on_nonexclusive_multiframe(self) -> None: with open("Tests/images/frozenpond.mpo", "rb") as fp: - def act(fp) -> None: + def act(fp: IO[bytes]) -> None: im = Image.open(fp) im.load() @@ -906,12 +913,12 @@ def test_exif_hide_offsets(self) -> None: assert exif.get_ifd(0xA005) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero_tobytes(self, size) -> None: + def test_zero_tobytes(self, size: tuple[int, int]) -> None: im = Image.new("RGB", size) assert im.tobytes() == b"" @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero_frombytes(self, size) -> None: + def test_zero_frombytes(self, size: tuple[int, int]) -> None: Image.frombytes("RGB", size, b"") im = Image.new("RGB", size) @@ -996,7 +1003,7 @@ def test_constants(self) -> None: "01r_00.pcx", ], ) - def test_overrun(self, path) -> None: + def test_overrun(self, path: str) -> None: """For overrun completeness, test as: valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c """ @@ -1023,7 +1030,7 @@ def test_exit_fp(self) -> None: pass assert not hasattr(im, "fp") - def test_close_graceful(self, caplog) -> None: + def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None: with Image.open("Tests/images/hopper.jpg") as im: copy = im.copy() with caplog.at_level(logging.DEBUG): @@ -1034,10 +1041,10 @@ def test_close_graceful(self, caplog) -> None: class MockEncoder: - pass + args: tuple[str, ...] -def mock_encode(*args): +def mock_encode(*args: str) -> MockEncoder: encoder = MockEncoder() encoder.args = args return encoder diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 325e7ef216b..24c7b871a68 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -208,7 +208,9 @@ def test_language() -> None: ), ids=("None", "ltr", "rtl2", "rtl", "ttb"), ) -def test_getlength(mode, text, direction, expected) -> None: +def test_getlength( + mode: str, text: str, direction: str | None, expected: float +) -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode, (1, 1), 0) d = ImageDraw.Draw(im) @@ -230,7 +232,7 @@ def test_getlength(mode, text, direction, expected) -> None: ("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"), ids=("caron-above", "caron-below", "double-breve", "overline"), ) -def test_getlength_combine(mode, direction, text) -> None: +def test_getlength_combine(mode: str, direction: str, text: str) -> None: if text == "i\u0305i" and direction == "ttb": pytest.skip("fails with this font") @@ -250,7 +252,7 @@ def test_getlength_combine(mode, direction, text) -> None: @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) -def test_anchor_ttb(anchor) -> None: +def test_anchor_ttb(anchor: str) -> None: text = "f" path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120) @@ -306,7 +308,9 @@ def test_anchor_ttb(anchor) -> None: @pytest.mark.parametrize( "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] ) -def test_combine(name, text, dir, anchor, epsilon) -> None: +def test_combine( + name: str, text: str, dir: str | None, anchor: str | None, epsilon: float +) -> None: path = f"Tests/images/test_combine_{name}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) @@ -337,7 +341,7 @@ def test_combine(name, text, dir, anchor, epsilon) -> None: ("rm", "right"), # pass with getsize ), ) -def test_combine_multiline(anchor, align) -> None: +def test_combine_multiline(anchor: str, align: str) -> None: # test that multiline text uses getlength, not getsize or getbbox path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 0b0c6d2d31a..46b473d7ac8 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -10,7 +10,7 @@ from .helper import assert_image_equal_tofile, hopper -def string_to_img(image_string): +def string_to_img(image_string: str) -> Image.Image: """Turn a string image representation into a binary image""" rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)] height = len(rows) @@ -38,7 +38,7 @@ def string_to_img(image_string): ) -def img_to_string(im): +def img_to_string(im: Image.Image) -> str: """Turn a (small) binary image into a string representation""" chars = ".1" width, height = im.size @@ -48,11 +48,11 @@ def img_to_string(im): ) -def img_string_normalize(im): +def img_string_normalize(im: str) -> str: return img_to_string(string_to_img(im)) -def assert_img_equal_img_string(a, b_string) -> None: +def assert_img_equal_img_string(a: Image.Image, b_string: str) -> None: assert img_to_string(a) == img_string_normalize(b_string) @@ -63,7 +63,7 @@ def test_str_to_img() -> None: @pytest.mark.parametrize( "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge") ) -def test_lut(op) -> None: +def test_lut(op: str) -> None: lb = ImageMorph.LutBuilder(op_name=op) assert lb.get_lut() is None diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 545229500a6..8e2db15aab7 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -67,7 +67,7 @@ def test_getcolor_rgba_color_rgb_palette() -> None: (255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)), ], ) -def test_getcolor_not_special(index, palette) -> None: +def test_getcolor_not_special(index: int, palette: ImagePalette.ImagePalette) -> None: im = Image.new("P", (1, 1)) # Do not use transparency index as a new color diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 560cdbd35f7..ed415953f06 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -13,7 +13,9 @@ FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" -def helper_pickle_file(tmp_path: Path, pickle, protocol, test_file, mode) -> None: +def helper_pickle_file( + tmp_path: Path, protocol: int, test_file: str, mode: str | None +) -> None: # Arrange with Image.open(test_file) as im: filename = str(tmp_path / "temp.pkl") @@ -30,7 +32,7 @@ def helper_pickle_file(tmp_path: Path, pickle, protocol, test_file, mode) -> Non assert im == loaded_im -def helper_pickle_string(pickle, protocol, test_file, mode) -> None: +def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> None: with Image.open(test_file) as im: if mode: im = im.convert(mode) @@ -64,10 +66,12 @@ def helper_pickle_string(pickle, protocol, test_file, mode) -> None: ], ) @pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1)) -def test_pickle_image(tmp_path: Path, test_file, test_mode, protocol) -> None: +def test_pickle_image( + tmp_path: Path, test_file: str, test_mode: str | None, protocol: int +) -> None: # Act / Assert - helper_pickle_string(pickle, protocol, test_file, test_mode) - helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode) + helper_pickle_string(protocol, test_file, test_mode) + helper_pickle_file(tmp_path, protocol, test_file, test_mode) def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: @@ -99,7 +103,9 @@ def test_pickle_tell() -> None: assert unpickled_image.tell() == 0 -def helper_assert_pickled_font_images(font1, font2) -> None: +def helper_assert_pickled_font_images( + font1: ImageFont.FreeTypeFont, font2: ImageFont.FreeTypeFont +) -> None: # Arrange im1 = Image.new(mode="RGBA", size=(300, 100)) im2 = Image.new(mode="RGBA", size=(300, 100)) @@ -117,7 +123,7 @@ def helper_assert_pickled_font_images(font1, font2) -> None: @skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) -def test_pickle_font_string(protocol) -> None: +def test_pickle_font_string(protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -131,7 +137,7 @@ def test_pickle_font_string(protocol) -> None: @skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) -def test_pickle_font_file(tmp_path: Path, protocol) -> None: +def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) filename = str(tmp_path / "temp.pkl") diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 797539f35ef..64dfb2c95fb 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -10,7 +10,7 @@ from PIL import Image, PSDraw -def _create_document(ps) -> None: +def _create_document(ps: PSDraw.PSDraw) -> None: title = "hopper" box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points @@ -50,7 +50,7 @@ def test_draw_postscript(tmp_path: Path) -> None: @pytest.mark.parametrize("buffer", (True, False)) -def test_stdout(buffer) -> None: +def test_stdout(buffer: bool) -> None: # Temporarily redirect stdout old_stdout = sys.stdout diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index 9442801d05c..3ce31cd2d1e 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -21,7 +21,7 @@ "Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi", ], ) -def test_crashes(test_file) -> None: +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 3db0660eab7..2a072fd44c5 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -2,6 +2,7 @@ import shutil from pathlib import Path +from typing import Callable import pytest @@ -17,7 +18,12 @@ @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") class TestShellInjection: - def assert_save_filename_check(self, tmp_path: Path, src_img, save_func) -> None: + def assert_save_filename_check( + self, + tmp_path: Path, + src_img: Image.Image, + save_func: Callable[[Image.Image, int, str], None], + ) -> None: for filename in test_filenames: dest_file = str(tmp_path / filename) save_func(src_img, 0, dest_file) diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index 64e781cbada..f51e8b3a8bd 100644 --- a/Tests/test_tiff_crashes.py +++ b/Tests/test_tiff_crashes.py @@ -42,7 +42,7 @@ @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") @pytest.mark.filterwarnings("ignore:Metadata warning") @pytest.mark.filterwarnings("ignore:Truncated File Read") -def test_tiff_crashes(test_file): +def test_tiff_crashes(test_file: str) -> None: try: with Image.open(test_file) as im: im.load() diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 5368545232b..f6adae3e6e7 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -53,9 +53,9 @@ def test_nonetype() -> None: def test_ifd_rational_save(tmp_path: Path) -> None: - methods = (True, False) - if not features.check("libtiff"): - methods = (False,) + methods = [True] + if features.check("libtiff"): + methods.append(False) for libtiff in methods: TiffImagePlugin.WRITE_LIBTIFF = libtiff From 47eaf0937f8e4f10fce8473007aba449c0c280f7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Feb 2024 22:26:23 +1100 Subject: [PATCH 0191/2374] Use IO[bytes] in type hints --- src/PIL/ImageFile.py | 4 ++-- src/PIL/MspImagePlugin.py | 3 ++- src/PIL/PcxImagePlugin.py | 3 ++- src/PIL/PpmImagePlugin.py | 4 ++-- src/PIL/SgiImagePlugin.py | 4 ++-- src/PIL/TgaImagePlugin.py | 4 ++-- src/PIL/XbmImagePlugin.py | 4 ++-- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 487f53efe1f..e929b665e06 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -32,7 +32,7 @@ import itertools import struct import sys -from typing import Any, NamedTuple +from typing import IO, Any, NamedTuple from . import Image from ._deprecate import deprecate @@ -616,7 +616,7 @@ def extents(self): class PyCodec: - fd: io.BytesIO | None + fd: IO[bytes] | None def __init__(self, mode, *args): self.im = None diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index bb7e466a790..65cc70624b7 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -26,6 +26,7 @@ import io import struct +from typing import IO from . import Image, ImageFile from ._binary import i16le as i16 @@ -163,7 +164,7 @@ def decode(self, buffer: bytes) -> tuple[int, int]: # write MSP files (uncompressed only) -def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as MSP" raise OSError(msg) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 3e0968a8386..026bfd9a01b 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -28,6 +28,7 @@ import io import logging +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -143,7 +144,7 @@ def _open(self) -> None: } -def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 3e45ba95c84..6ac7a9bbc79 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -16,7 +16,7 @@ from __future__ import annotations import math -from io import BytesIO +from typing import IO from . import Image, ImageFile from ._binary import i16be as i16 @@ -324,7 +324,7 @@ def decode(self, buffer: bytes) -> tuple[int, int]: # -------------------------------------------------------------------- -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index ccf661ff1f3..7bd84ebd491 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -24,7 +24,7 @@ import os import struct -from io import BytesIO +from typing import IO from . import Image, ImageFile from ._binary import i16be as i16 @@ -125,7 +125,7 @@ def _open(self) -> None: ] -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode not in {"RGB", "RGBA", "L"}: msg = "Unsupported SGI image mode" raise ValueError(msg) diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 584932d2c7d..828701342b7 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -18,7 +18,7 @@ from __future__ import annotations import warnings -from io import BytesIO +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -175,7 +175,7 @@ def load_end(self) -> None: } -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 0291e2858ac..eee7274361c 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -21,7 +21,7 @@ from __future__ import annotations import re -from io import BytesIO +from typing import IO from . import Image, ImageFile @@ -70,7 +70,7 @@ def _open(self) -> None: self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as XBM" raise OSError(msg) From 63987b7abaf6907a8985b867be154debfef0ec1b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Feb 2024 06:55:13 +1100 Subject: [PATCH 0192/2374] Set mode to L if palette is missing --- Tests/test_file_tga.py | 5 +++++ src/PIL/TgaImagePlugin.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bd8e522c7b5..87a59ff3d7a 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -133,6 +133,11 @@ def test_small_palette(tmp_path: Path) -> None: assert reloaded.getpalette() == colors +def test_missing_palette() -> None: + with Image.open("Tests/images/dilation4.lut") as im: + assert im.mode == "L" + + def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper("PA") out = str(tmp_path / "temp.tga") diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 584932d2c7d..5d48275251e 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -85,7 +85,7 @@ def _open(self) -> None: elif depth == 16: self._mode = "LA" elif imagetype in (1, 9): - self._mode = "P" + self._mode = "P" if colormaptype else "L" elif imagetype in (2, 10): self._mode = "RGB" if depth == 32: From 818500b329555969cdb852c81d667cc70faaed94 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Feb 2024 07:10:44 +1100 Subject: [PATCH 0193/2374] Raise an error if map depth is unknown --- Tests/images/p_8.tga | Bin 0 -> 18 bytes Tests/test_file_tga.py | 7 ++++++- src/PIL/TgaImagePlugin.py | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 Tests/images/p_8.tga diff --git a/Tests/images/p_8.tga b/Tests/images/p_8.tga new file mode 100644 index 0000000000000000000000000000000000000000..73759a2822419fd38134be80acf7918c0a437e43 GIT binary patch literal 18 Wcmb1RWMp7qVB`Q23=KfY!2kdU!vQh? literal 0 HcmV?d00001 diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 87a59ff3d7a..75c592da26b 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -7,7 +7,7 @@ import pytest -from PIL import Image +from PIL import Image, UnidentifiedImageError from .helper import assert_image_equal, assert_image_equal_tofile, hopper @@ -65,6 +65,11 @@ def roundtrip(original_im) -> None: roundtrip(original_im) +def test_palette_depth_8(tmp_path: Path) -> None: + with pytest.raises(UnidentifiedImageError): + Image.open("Tests/images/p_8.tga") + + def test_palette_depth_16(tmp_path: Path) -> None: with Image.open("Tests/images/p_16.tga") as im: assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png") diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 5d48275251e..b6748c25ed2 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -128,6 +128,9 @@ def _open(self) -> None: self.palette = ImagePalette.raw( "BGRA", b"\0" * 4 * start + self.fp.read(4 * size) ) + else: + msg = "unknown TGA map depth" + raise SyntaxError(msg) # setup tile descriptor try: From 21e5d5d082dfe47fda2779e10d446e6593c04dd8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Feb 2024 09:17:22 +1100 Subject: [PATCH 0194/2374] Use palette when loading --- Tests/test_file_ico.py | 11 +++++++++++ src/PIL/IcoImagePlugin.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 65f090931b4..e75561f6978 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -38,6 +38,17 @@ def test_black_and_white() -> None: assert im.size == (16, 16) +def test_palette(tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.ico") + + im = Image.new("P", (16, 16)) + im.save(temp_file) + + with Image.open(temp_file) as reloaded: + assert reloaded.mode == "P" + assert reloaded.palette is not None + + def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 1b22f8645dc..b558fdf3476 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -329,6 +329,8 @@ def load(self): self.im = im.im self.pyaccess = None self._mode = im.mode + if im.palette: + self.palette = im.palette if im.size != self.size: warnings.warn("Image was not the expected size") From 3199c0ea40c041d41fe2499c86893a7e795f0929 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Feb 2024 20:20:42 +1100 Subject: [PATCH 0195/2374] Decoder and encoders subclass PyDecoder and PyEncoder --- Tests/test_file_jpeg.py | 8 +----- Tests/test_image.py | 16 ++++------- Tests/test_imagefile.py | 64 ++++++++++++++++++++--------------------- src/PIL/Image.py | 14 ++++----- 4 files changed, 45 insertions(+), 57 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 65424214838..4858d92e6ea 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -986,13 +986,7 @@ class InfiniteMockPyDecoder(ImageFile.PyDecoder): def decode(self, buffer: bytes) -> tuple[int, int]: return 0, 0 - decoder = InfiniteMockPyDecoder(None) - - def closure(mode: str, *args) -> InfiniteMockPyDecoder: - decoder.__init__(mode, *args) - return decoder - - Image.register_decoder("INFINITE", closure) + Image.register_decoder("INFINITE", InfiniteMockPyDecoder) with Image.open(TEST_FILE) as im: im.tile = [ diff --git a/Tests/test_image.py b/Tests/test_image.py index 4c04e0da48e..aae51eaa42a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -16,6 +16,7 @@ ExifTags, Image, ImageDraw, + ImageFile, ImagePalette, UnidentifiedImageError, features, @@ -1038,25 +1039,20 @@ def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None: assert im.fp is None -class MockEncoder: - args: tuple[str, ...] - - -def mock_encode(*args: str) -> MockEncoder: - encoder = MockEncoder() - encoder.args = args - return encoder +class MockEncoder(ImageFile.PyEncoder): + pass class TestRegistry: def test_encode_registry(self) -> None: - Image.register_encoder("MOCK", mock_encode) + Image.register_encoder("MOCK", MockEncoder) assert "MOCK" in Image.ENCODERS enc = Image._getencoder("RGB", "MOCK", ("args",), extra=("extra",)) assert isinstance(enc, MockEncoder) - assert enc.args == ("RGB", "args", "extra") + assert enc.mode == "RGB" + assert enc.args == ("args", "extra") def test_encode_registry_fail(self) -> None: with pytest.raises(OSError): diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 49140978168..cf251c9cec6 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from typing import Any import pytest @@ -201,12 +202,22 @@ def test_broken_datastream_without_errors(self) -> None: class MockPyDecoder(ImageFile.PyDecoder): + def __init__(self, mode: str, *args: Any) -> None: + MockPyDecoder.last = self + + super().__init__(mode, *args) + def decode(self, buffer): # eof return -1, 0 class MockPyEncoder(ImageFile.PyEncoder): + def __init__(self, mode: str, *args: Any) -> None: + MockPyEncoder.last = self + + super().__init__(mode, *args) + def encode(self, buffer): return 1, 1, b"" @@ -228,19 +239,8 @@ def _open(self) -> None: class CodecsTest: @classmethod def setup_class(cls) -> None: - cls.decoder = MockPyDecoder(None) - cls.encoder = MockPyEncoder(None) - - def decoder_closure(mode, *args): - cls.decoder.__init__(mode, *args) - return cls.decoder - - def encoder_closure(mode, *args): - cls.encoder.__init__(mode, *args) - return cls.encoder - - Image.register_decoder("MOCK", decoder_closure) - Image.register_encoder("MOCK", encoder_closure) + Image.register_decoder("MOCK", MockPyDecoder) + Image.register_encoder("MOCK", MockPyEncoder) class TestPyDecoder(CodecsTest): @@ -251,13 +251,13 @@ def test_setimage(self) -> None: im.load() - assert self.decoder.state.xoff == xoff - assert self.decoder.state.yoff == yoff - assert self.decoder.state.xsize == xsize - assert self.decoder.state.ysize == ysize + assert MockPyDecoder.last.state.xoff == xoff + assert MockPyDecoder.last.state.yoff == yoff + assert MockPyDecoder.last.state.xsize == xsize + assert MockPyDecoder.last.state.ysize == ysize with pytest.raises(ValueError): - self.decoder.set_as_raw(b"\x00") + MockPyDecoder.last.set_as_raw(b"\x00") def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -267,10 +267,10 @@ def test_extents_none(self) -> None: im.load() - assert self.decoder.state.xoff == 0 - assert self.decoder.state.yoff == 0 - assert self.decoder.state.xsize == 200 - assert self.decoder.state.ysize == 200 + assert MockPyDecoder.last.state.xoff == 0 + assert MockPyDecoder.last.state.yoff == 0 + assert MockPyDecoder.last.state.xsize == 200 + assert MockPyDecoder.last.state.ysize == 200 def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -315,10 +315,10 @@ def test_setimage(self) -> None: im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] ) - assert self.encoder.state.xoff == xoff - assert self.encoder.state.yoff == yoff - assert self.encoder.state.xsize == xsize - assert self.encoder.state.ysize == ysize + assert MockPyEncoder.last.state.xoff == xoff + assert MockPyEncoder.last.state.yoff == yoff + assert MockPyEncoder.last.state.xsize == xsize + assert MockPyEncoder.last.state.ysize == ysize def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -329,10 +329,10 @@ def test_extents_none(self) -> None: fp = BytesIO() ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) - assert self.encoder.state.xoff == 0 - assert self.encoder.state.yoff == 0 - assert self.encoder.state.xsize == 200 - assert self.encoder.state.ysize == 200 + assert MockPyEncoder.last.state.xoff == 0 + assert MockPyEncoder.last.state.yoff == 0 + assert MockPyEncoder.last.state.xsize == 200 + assert MockPyEncoder.last.state.ysize == 200 def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -340,12 +340,12 @@ def test_negsize(self) -> None: im = MockImageFile(buf) fp = BytesIO() - self.encoder.cleanup_called = False + MockPyEncoder.last = None with pytest.raises(ValueError): ImageFile._save( im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] ) - assert self.encoder.cleanup_called + assert MockPyEncoder.last.cleanup_called with pytest.raises(ValueError): ImageFile._save( diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a770488b7ec..eba30537f13 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -229,8 +229,8 @@ class Quantize(IntEnum): SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} EXTENSION: dict[str, str] = {} -DECODERS: dict[str, object] = {} -ENCODERS: dict[str, object] = {} +DECODERS: dict[str, type[ImageFile.PyDecoder]] = {} +ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {} # -------------------------------------------------------------------- # Modes @@ -3524,28 +3524,26 @@ def registered_extensions(): return EXTENSION -def register_decoder(name: str, decoder) -> None: +def register_decoder(name: str, decoder: type[ImageFile.PyDecoder]) -> None: """ Registers an image decoder. This function should not be used in application code. :param name: The name of the decoder - :param decoder: A callable(mode, args) that returns an - ImageFile.PyDecoder object + :param decoder: An ImageFile.PyDecoder object .. versionadded:: 4.1.0 """ DECODERS[name] = decoder -def register_encoder(name, encoder): +def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: """ Registers an image encoder. This function should not be used in application code. :param name: The name of the encoder - :param encoder: A callable(mode, args) that returns an - ImageFile.PyEncoder object + :param encoder: An ImageFile.PyEncoder object .. versionadded:: 4.1.0 """ From 26e0f6df56c1289d52b156642c6ee1197d2bf69b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 10:18:54 +1100 Subject: [PATCH 0196/2374] Pin Python 3.13 on Windows to a3 --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 75fccf7959a..79a2e60b251 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-alpha.3"] timeout-minutes: 30 From 5c858d75e4a58e895bed56c2ff6c0bae245c88cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 10:45:52 +1100 Subject: [PATCH 0197/2374] Added type hints --- src/PIL/Image.py | 33 ++++---- src/PIL/ImageColor.py | 2 +- src/PIL/ImageOps.py | 171 +++++++++++++++++++++++++++------------- src/PIL/ImagePalette.py | 7 +- 4 files changed, 139 insertions(+), 74 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a770488b7ec..ba81a22c7f0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1430,7 +1430,7 @@ def get_value(element): root = ElementTree.fromstring(xmp_tags) return {get_name(root.tag): get_value(root)} - def getexif(self): + def getexif(self) -> Exif: """ Gets EXIF data from the image. @@ -1438,7 +1438,6 @@ def getexif(self): """ if self._exif is None: self._exif = Exif() - self._exif._loaded = False elif self._exif._loaded: return self._exif self._exif._loaded = True @@ -1525,7 +1524,7 @@ def getim(self): self.load() return self.im.ptr - def getpalette(self, rawmode="RGB"): + def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: """ Returns the image palette as a list. @@ -1615,7 +1614,7 @@ def getprojection(self): x, y = self.im.getprojection() return list(x), list(y) - def histogram(self, mask=None, extrema=None): + def histogram(self, mask=None, extrema=None) -> list[int]: """ Returns a histogram for the image. The histogram is returned as a list of pixel counts, one for each pixel value in the source @@ -1804,7 +1803,7 @@ def alpha_composite(self, im, dest=(0, 0), source=(0, 0)): result = alpha_composite(background, overlay) self.paste(result, box) - def point(self, lut, mode=None): + def point(self, lut, mode: str | None = None) -> Image: """ Maps this image through a lookup table or function. @@ -1928,7 +1927,7 @@ def putdata(self, data, scale=1.0, offset=0.0): self.im.putdata(data, scale, offset) - def putpalette(self, data, rawmode="RGB"): + def putpalette(self, data, rawmode="RGB") -> None: """ Attaches a palette to this image. The image must be a "P", "PA", "L" or "LA" image. @@ -2108,7 +2107,7 @@ def _get_safe_box(self, size, resample, box): min(self.size[1], math.ceil(box[3] + support_y)), ) - def resize(self, size, resample=None, box=None, reducing_gap=None): + def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image: """ Returns a resized copy of this image. @@ -2200,10 +2199,11 @@ def resize(self, size, resample=None, box=None, reducing_gap=None): if factor_x > 1 or factor_y > 1: reduce_box = self._get_safe_box(size, resample, box) factor = (factor_x, factor_y) - if callable(self.reduce): - self = self.reduce(factor, box=reduce_box) - else: - self = Image.reduce(self, factor, box=reduce_box) + self = ( + self.reduce(factor, box=reduce_box) + if callable(self.reduce) + else Image.reduce(self, factor, box=reduce_box) + ) box = ( (box[0] - reduce_box[0]) / factor_x, (box[1] - reduce_box[1]) / factor_y, @@ -2818,7 +2818,7 @@ def __transformer( self.im.transform2(box, image.im, method, data, resample, fill) - def transpose(self, method): + def transpose(self, method: Transpose) -> Image: """ Transpose image (flip or rotate in 90 degree steps) @@ -2870,7 +2870,9 @@ class ImagePointHandler: (for use with :py:meth:`~PIL.Image.Image.point`) """ - pass + @abc.abstractmethod + def point(self, im: Image) -> Image: + pass class ImageTransformHandler: @@ -3690,6 +3692,7 @@ class Exif(_ExifBase): endian = None bigtiff = False + _loaded = False def __init__(self): self._data = {} @@ -3805,7 +3808,7 @@ def _get_merged_dict(self): return merged_dict - def tobytes(self, offset=8): + def tobytes(self, offset: int = 8) -> bytes: from . import TiffImagePlugin head = self._get_head() @@ -3960,7 +3963,7 @@ def __setitem__(self, tag, value): del self._info[tag] self._data[tag] = value - def __delitem__(self, tag): + def __delitem__(self, tag: int) -> None: if self._info is not None and tag in self._info: del self._info[tag] else: diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index ad59b066779..5fb80b75310 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -124,7 +124,7 @@ def getrgb(color): @lru_cache -def getcolor(color, mode): +def getcolor(color, mode: str) -> tuple[int, ...]: """ Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index a9e626b2b2a..6218c723f85 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -21,6 +21,7 @@ import functools import operator import re +from typing import Protocol, Sequence, cast from . import ExifTags, Image, ImagePalette @@ -28,7 +29,7 @@ # helpers -def _border(border): +def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: if isinstance(border, tuple): if len(border) == 2: left, top = right, bottom = border @@ -39,7 +40,7 @@ def _border(border): return left, top, right, bottom -def _color(color, mode): +def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]: if isinstance(color, str): from . import ImageColor @@ -47,7 +48,7 @@ def _color(color, mode): return color -def _lut(image, lut): +def _lut(image: Image.Image, lut: list[int]) -> Image.Image: if image.mode == "P": # FIXME: apply to lookup table, not image data msg = "mode P support coming soon" @@ -65,7 +66,13 @@ def _lut(image, lut): # actions -def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): +def autocontrast( + image: Image.Image, + cutoff: float | tuple[float, float] = 0, + ignore: int | Sequence[int] | None = None, + mask: Image.Image | None = None, + preserve_tone: bool = False, +) -> Image.Image: """ Maximize (normalize) image contrast. This function calculates a histogram of the input image (or mask region), removes ``cutoff`` percent of the @@ -97,10 +104,9 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): h = histogram[layer : layer + 256] if ignore is not None: # get rid of outliers - try: + if isinstance(ignore, int): h[ignore] = 0 - except TypeError: - # assume sequence + else: for ix in ignore: h[ix] = 0 if cutoff: @@ -112,7 +118,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): for ix in range(256): n = n + h[ix] # remove cutoff% pixels from the low end - cut = n * cutoff[0] // 100 + cut = int(n * cutoff[0] // 100) for lo in range(256): if cut > h[lo]: cut = cut - h[lo] @@ -123,7 +129,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): if cut <= 0: break # remove cutoff% samples from the high end - cut = n * cutoff[1] // 100 + cut = int(n * cutoff[1] // 100) for hi in range(255, -1, -1): if cut > h[hi]: cut = cut - h[hi] @@ -156,7 +162,15 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): return _lut(image, lut) -def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127): +def colorize( + image: Image.Image, + black: str | tuple[int, ...], + white: str | tuple[int, ...], + mid: str | int | tuple[int, ...] | None = None, + blackpoint: int = 0, + whitepoint: int = 255, + midpoint: int = 127, +) -> Image.Image: """ Colorize grayscale image. This function calculates a color wedge which maps all black pixels in @@ -188,10 +202,9 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 # Define colors from arguments - black = _color(black, "RGB") - white = _color(white, "RGB") - if mid is not None: - mid = _color(mid, "RGB") + rgb_black = cast(Sequence[int], _color(black, "RGB")) + rgb_white = cast(Sequence[int], _color(white, "RGB")) + rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None # Empty lists for the mapping red = [] @@ -200,18 +213,24 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the low-end values for i in range(0, blackpoint): - red.append(black[0]) - green.append(black[1]) - blue.append(black[2]) + red.append(rgb_black[0]) + green.append(rgb_black[1]) + blue.append(rgb_black[2]) # Create the mapping (2-color) - if mid is None: + if rgb_mid is None: range_map = range(0, whitepoint - blackpoint) for i in range_map: - red.append(black[0] + i * (white[0] - black[0]) // len(range_map)) - green.append(black[1] + i * (white[1] - black[1]) // len(range_map)) - blue.append(black[2] + i * (white[2] - black[2]) // len(range_map)) + red.append( + rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map) + ) + green.append( + rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map) + ) + blue.append( + rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map) + ) # Create the mapping (3-color) else: @@ -219,26 +238,36 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi range_map2 = range(0, whitepoint - midpoint) for i in range_map1: - red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1)) - green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1)) - blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1)) + red.append( + rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1) + ) + green.append( + rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1) + ) + blue.append( + rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1) + ) for i in range_map2: - red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2)) - green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2)) - blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2)) + red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2)) + green.append( + rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2) + ) + blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2)) # Create the high-end values for i in range(0, 256 - whitepoint): - red.append(white[0]) - green.append(white[1]) - blue.append(white[2]) + red.append(rgb_white[0]) + green.append(rgb_white[1]) + blue.append(rgb_white[2]) # Return converted image image = image.convert("RGB") return _lut(image, red + green + blue) -def contain(image, size, method=Image.Resampling.BICUBIC): +def contain( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a resized version of the image, set to the maximum width and height within the requested size, while maintaining the original aspect ratio. @@ -267,7 +296,9 @@ def contain(image, size, method=Image.Resampling.BICUBIC): return image.resize(size, resample=method) -def cover(image, size, method=Image.Resampling.BICUBIC): +def cover( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a resized version of the image, so that the requested size is covered, while maintaining the original aspect ratio. @@ -296,7 +327,13 @@ def cover(image, size, method=Image.Resampling.BICUBIC): return image.resize(size, resample=method) -def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)): +def pad( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + color: str | int | tuple[int, ...] | None = None, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: """ Returns a resized and padded version of the image, expanded to fill the requested aspect ratio and size. @@ -334,7 +371,7 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 return out -def crop(image, border=0): +def crop(image: Image.Image, border: int = 0) -> Image.Image: """ Remove border from image. The same amount of pixels are removed from all four sides. This function works on all image modes. @@ -349,7 +386,9 @@ def crop(image, border=0): return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) -def scale(image, factor, resample=Image.Resampling.BICUBIC): +def scale( + image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a rescaled image by a specific factor given in parameter. A factor greater than 1 expands the image, between 0 and 1 contracts the @@ -372,7 +411,19 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC): return image.resize(size, resample) -def deform(image, deformer, resample=Image.Resampling.BILINEAR): +class _SupportsGetMesh(Protocol): + def getmesh( + self, image: Image.Image + ) -> list[ + tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] + ]: ... + + +def deform( + image: Image.Image, + deformer: _SupportsGetMesh, + resample: int = Image.Resampling.BILINEAR, +) -> Image.Image: """ Deform the image. @@ -388,7 +439,7 @@ def deform(image, deformer, resample=Image.Resampling.BILINEAR): ) -def equalize(image, mask=None): +def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image: """ Equalize the image histogram. This function applies a non-linear mapping to the input image, in order to create a uniform @@ -419,7 +470,11 @@ def equalize(image, mask=None): return _lut(image, lut) -def expand(image, border=0, fill=0): +def expand( + image: Image.Image, + border: int | tuple[int, ...] = 0, + fill: str | int | tuple[int, ...] = 0, +) -> Image.Image: """ Add border to the image @@ -445,7 +500,13 @@ def expand(image, border=0, fill=0): return out -def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, 0.5)): +def fit( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + bleed: float = 0.0, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: """ Returns a resized and cropped version of the image, cropped to the requested aspect ratio and size. @@ -479,13 +540,12 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, # kevin@cazabon.com # https://www.cazabon.com - # ensure centering is mutable - centering = list(centering) + centering_x, centering_y = centering - if not 0.0 <= centering[0] <= 1.0: - centering[0] = 0.5 - if not 0.0 <= centering[1] <= 1.0: - centering[1] = 0.5 + if not 0.0 <= centering_x <= 1.0: + centering_x = 0.5 + if not 0.0 <= centering_y <= 1.0: + centering_y = 0.5 if not 0.0 <= bleed < 0.5: bleed = 0.0 @@ -522,8 +582,8 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, crop_height = live_size[0] / output_ratio # make the crop - crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering[0] - crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1] + crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x + crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) @@ -531,7 +591,7 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, return image.resize(size, method, box=crop) -def flip(image): +def flip(image: Image.Image) -> Image.Image: """ Flip the image vertically (top to bottom). @@ -541,7 +601,7 @@ def flip(image): return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) -def grayscale(image): +def grayscale(image: Image.Image) -> Image.Image: """ Convert the image to grayscale. @@ -551,7 +611,7 @@ def grayscale(image): return image.convert("L") -def invert(image): +def invert(image: Image.Image) -> Image.Image: """ Invert (negate) the image. @@ -562,7 +622,7 @@ def invert(image): return image.point(lut) if image.mode == "1" else _lut(image, lut) -def mirror(image): +def mirror(image: Image.Image) -> Image.Image: """ Flip image horizontally (left to right). @@ -572,7 +632,7 @@ def mirror(image): return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) -def posterize(image, bits): +def posterize(image: Image.Image, bits: int) -> Image.Image: """ Reduce the number of bits for each color channel. @@ -585,7 +645,7 @@ def posterize(image, bits): return _lut(image, lut) -def solarize(image, threshold=128): +def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: """ Invert all pixel values above a threshold. @@ -602,7 +662,7 @@ def solarize(image, threshold=128): return _lut(image, lut) -def exif_transpose(image, *, in_place=False): +def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None: """ If an image has an EXIF Orientation tag, other than 1, transpose the image accordingly, and remove the orientation data. @@ -616,7 +676,7 @@ def exif_transpose(image, *, in_place=False): """ image.load() image_exif = image.getexif() - orientation = image_exif.get(ExifTags.Base.Orientation) + orientation = image_exif.get(ExifTags.Base.Orientation, 1) method = { 2: Image.Transpose.FLIP_LEFT_RIGHT, 3: Image.Transpose.ROTATE_180, @@ -653,3 +713,4 @@ def exif_transpose(image, *, in_place=False): return transposed_image elif not in_place: return image.copy() + return None diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 2b6cecc6105..770d10025c8 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -18,6 +18,7 @@ from __future__ import annotations import array +from typing import Sequence from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile @@ -34,11 +35,11 @@ class ImagePalette: Defaults to an empty palette. """ - def __init__(self, mode="RGB", palette=None): + def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None: self.mode = mode self.rawmode = None # if set, palette contains raw data self.palette = palette or bytearray() - self.dirty = None + self.dirty: int | None = None @property def palette(self): @@ -127,7 +128,7 @@ def _new_color_index(self, image=None, e=None): raise ValueError(msg) from e return index - def getcolor(self, color, image=None): + def getcolor(self, color, image=None) -> int: """Given an rgb tuple, allocate palette entry. .. warning:: This method is experimental. From d3b974b78607d36fbfa392b80c81adbd08d277ac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 11:19:32 +1100 Subject: [PATCH 0198/2374] Use font in ImageDraw examples --- docs/deprecations.rst | 8 ++++---- docs/releasenotes/9.2.0.rst | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 205fcb9abcd..a58ce9bcba6 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -232,10 +232,10 @@ Previous code:: im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) - width, height = draw.textsize("Hello world") + width, height = draw.textsize("Hello world", font) width, height = font.getsize_multiline("Hello\nworld") - width, height = draw.multiline_textsize("Hello\nworld") + width, height = draw.multiline_textsize("Hello\nworld", font) Use instead:: @@ -247,9 +247,9 @@ Use instead:: im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) - width = draw.textlength("Hello world") + width = draw.textlength("Hello world", font) - left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld") + left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) width, height = right - left, bottom - top FreeTypeFont.getmask2 fill parameter diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index b875edf8e5c..3b8d2535f74 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -69,10 +69,10 @@ Previous code:: im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) - width, height = draw.textsize("Hello world") + width, height = draw.textsize("Hello world", font) width, height = font.getsize_multiline("Hello\nworld") - width, height = draw.multiline_textsize("Hello\nworld") + width, height = draw.multiline_textsize("Hello\nworld", font) Use instead:: @@ -84,9 +84,9 @@ Use instead:: im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) - width = draw.textlength("Hello world") + width = draw.textlength("Hello world", font) - left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld") + left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) width, height = right - left, bottom - top API Additions From 617b9cbc00759f042d2c38ea63a14adca2bc902b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 11:50:48 +1100 Subject: [PATCH 0199/2374] Describe difference between size and bbox --- docs/deprecations.rst | 4 ++++ docs/releasenotes/9.2.0.rst | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a58ce9bcba6..c90ad481adb 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -252,6 +252,10 @@ Use instead:: left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) width, height = right - left, bottom - top +Previously, the ``size`` methods returned a ``height`` that included the vertical +offset of the text, while the new ``bbox`` methods explicitly distinguish this as a +``top`` offset. + FreeTypeFont.getmask2 fill parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 3b8d2535f74..495926ca749 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -89,6 +89,10 @@ Use instead:: left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld", font) width, height = right - left, bottom - top +Previously, the ``size`` methods returned a ``height`` that included the vertical +offset of the text, while the new ``bbox`` methods explicitly distinguish this as a +``top`` offset. + API Additions ============= From 1a108281b9b6d894574ec63534a043385549b3be Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 14:03:56 +1100 Subject: [PATCH 0200/2374] Removed unused code --- Tests/test_format_hsv.py | 4 ---- Tests/test_image_paste.py | 1 - 2 files changed, 5 deletions(-) diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 73aaae6e757..da909c06c80 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -12,10 +12,6 @@ def int_to_float(i): return i / 255 -def str_to_float(i): - return ord(i) / 255 - - def tuple_to_ints(tp): x, y, z = tp return int(x * 255.0), int(y * 255.0), int(z * 255.0) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index ce73455729a..2966f724f4a 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -8,7 +8,6 @@ class TestImagingPaste: - masks = {} size = 128 def assert_9points_image( From 5ff7d926fd24acc2d6d575959635d59123b308a6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 15:00:38 +1100 Subject: [PATCH 0201/2374] Added type hints --- Tests/test_features.py | 7 +- Tests/test_file_blp.py | 2 +- Tests/test_file_bmp.py | 4 +- Tests/test_file_im.py | 2 +- Tests/test_file_pcx.py | 6 +- Tests/test_file_pdf.py | 15 ++-- Tests/test_file_tiff.py | 19 ++--- Tests/test_format_hsv.py | 19 +++-- Tests/test_imagechops.py | 6 +- Tests/test_imagedraw2.py | 11 +-- Tests/test_imageenhance.py | 8 ++- Tests/test_imagefile.py | 2 +- Tests/test_imagefont.py | 140 ++++++++++++++++++++++--------------- Tests/test_imageops.py | 22 +++--- Tests/test_imageops_usm.py | 14 ++-- Tests/test_imagepath.py | 13 ++-- 16 files changed, 170 insertions(+), 120 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index de74e9c1829..8d2d198ffcd 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -2,6 +2,7 @@ import io import re +from typing import Callable import pytest @@ -29,7 +30,7 @@ def test_version() -> None: # Check the correctness of the convenience function # and the format of version numbers - def test(name, function) -> None: + def test(name: str, function: Callable[[str], bool]) -> None: version = features.version(name) if not features.check(name): assert version is None @@ -73,12 +74,12 @@ def test_libimagequant_version() -> None: @pytest.mark.parametrize("feature", features.modules) -def test_check_modules(feature) -> None: +def test_check_modules(feature: str) -> None: assert features.check_module(feature) in [True, False] @pytest.mark.parametrize("feature", features.codecs) -def test_check_codecs(feature) -> None: +def test_check_codecs(feature: str) -> None: assert features.check_codec(feature) in [True, False] diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 3904d3bc5b5..1e2f20c407b 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -71,7 +71,7 @@ def test_save(tmp_path: Path) -> None: "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", ], ) -def test_crashes(test_file) -> None: +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index c36466e0269..1eaff0c7df0 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -16,7 +16,7 @@ def test_sanity(tmp_path: Path) -> None: - def roundtrip(im) -> None: + def roundtrip(im: Image.Image) -> None: outfile = str(tmp_path / "temp.bmp") im.save(outfile, "BMP") @@ -194,7 +194,7 @@ def test_rle4() -> None: ("Tests/images/bmp/g/pal8rle.bmp", 1064), ), ) -def test_rle8_eof(file_name, length) -> None: +def test_rle8_eof(file_name: str, length: int) -> None: with open(file_name, "rb") as fp: data = fp.read(length) with Image.open(io.BytesIO(data)) as im: diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index f932069b9c3..036965bf5d8 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -82,7 +82,7 @@ def test_eoferror() -> None: @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) -def test_roundtrip(mode, tmp_path: Path) -> None: +def test_roundtrip(mode: str, tmp_path: Path) -> None: out = str(tmp_path / "temp.im") im = hopper(mode) im.save(out) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index a2486be40c5..ab9f9663e1b 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -9,7 +9,7 @@ from .helper import assert_image_equal, hopper -def _roundtrip(tmp_path: Path, im) -> None: +def _roundtrip(tmp_path: Path, im: Image.Image) -> None: f = str(tmp_path / "temp.pcx") im.save(f) with Image.open(f) as im2: @@ -44,7 +44,7 @@ def test_invalid_file() -> None: @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) -def test_odd(tmp_path: Path, mode) -> None: +def test_odd(tmp_path: Path, mode: str) -> None: # See issue #523, odd sized images should have a stride that's even. # Not that ImageMagick or GIMP write PCX that way. # We were not handling properly. @@ -89,7 +89,7 @@ def test_large_count(tmp_path: Path) -> None: _roundtrip(tmp_path, im) -def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None: +def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None: _last = ImageFile.MAXBLOCK ImageFile.MAXBLOCK = size try: diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 65a93c138e9..d39a86565ce 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -6,6 +6,7 @@ import tempfile import time from pathlib import Path +from typing import Any, Generator import pytest @@ -14,7 +15,7 @@ from .helper import hopper, mark_if_feature_version, skip_unless_feature -def helper_save_as_pdf(tmp_path: Path, mode, **kwargs): +def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str: # Arrange im = hopper(mode) outfile = str(tmp_path / ("temp_" + mode + ".pdf")) @@ -41,13 +42,13 @@ def helper_save_as_pdf(tmp_path: Path, mode, **kwargs): @pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) -def test_save(tmp_path: Path, mode) -> None: +def test_save(tmp_path: Path, mode: str) -> None: helper_save_as_pdf(tmp_path, mode) @skip_unless_feature("jpg_2000") @pytest.mark.parametrize("mode", ("LA", "RGBA")) -def test_save_alpha(tmp_path: Path, mode) -> None: +def test_save_alpha(tmp_path: Path, mode: str) -> None: helper_save_as_pdf(tmp_path, mode) @@ -112,7 +113,7 @@ def test_resolution(tmp_path: Path) -> None: {"dpi": (75, 150), "resolution": 200}, ), ) -def test_dpi(params, tmp_path: Path) -> None: +def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: im = hopper() outfile = str(tmp_path / "temp.pdf") @@ -156,7 +157,7 @@ def test_save_all(tmp_path: Path) -> None: assert os.path.getsize(outfile) > 0 # Test appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims im.save(outfile, save_all=True, append_images=im_generator(ims)) @@ -226,7 +227,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None: im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) -def check_pdf_pages_consistency(pdf) -> None: +def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None: pages_info = pdf.read_indirect(pdf.pages_ref) assert b"Parent" not in pages_info assert b"Kids" in pages_info @@ -339,7 +340,7 @@ def test_pdf_append_to_bytesio() -> None: @pytest.mark.timeout(1) @pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") @pytest.mark.parametrize("newline", (b"\r", b"\n")) -def test_redos(newline) -> None: +def test_redos(newline: bytes) -> None: malicious = b" trailer<<>>" + newline * 3456 # This particular exception isn't relevant here. diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a16b76e19f7..0110948aeac 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -4,6 +4,8 @@ import warnings from io import BytesIO from pathlib import Path +from types import ModuleType +from typing import Generator import pytest @@ -20,6 +22,7 @@ is_win32, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: @@ -156,7 +159,7 @@ def test_int_resolution(self) -> None: "resolution_unit, dpi", [(None, 72.8), (2, 72.8), (3, 184.912)], ) - def test_load_float_dpi(self, resolution_unit, dpi) -> None: + def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None: with Image.open( "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" ) as im: @@ -284,7 +287,7 @@ def test_unknown_pixel_mode(self) -> None: ("Tests/images/multipage.tiff", 3), ), ) - def test_n_frames(self, path, n_frames) -> None: + def test_n_frames(self, path: str, n_frames: int) -> None: with Image.open(path) as im: assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) @@ -402,7 +405,7 @@ def test__delitem__(self) -> None: assert len_before == len_after + 1 @pytest.mark.parametrize("legacy_api", (False, True)) - def test_load_byte(self, legacy_api) -> None: + def test_load_byte(self, legacy_api: bool) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc" ret = ifd.load_byte(data, legacy_api) @@ -431,7 +434,7 @@ def test_ifd_tag_type(self) -> None: assert 0x8825 in im.tag_v2 def test_exif(self, tmp_path: Path) -> None: - def check_exif(exif) -> None: + def check_exif(exif: Image.Exif) -> None: assert sorted(exif.keys()) == [ 256, 257, @@ -511,7 +514,7 @@ def test_exif_frames(self) -> None: assert im.getexif()[273] == (1408, 1907) @pytest.mark.parametrize("mode", ("1", "L")) - def test_photometric(self, mode, tmp_path: Path) -> None: + def test_photometric(self, mode: str, tmp_path: Path) -> None: filename = str(tmp_path / "temp.tif") im = hopper(mode) im.save(filename, tiffinfo={262: 0}) @@ -660,7 +663,7 @@ def test_planar_configuration_save(self, tmp_path: Path) -> None: assert_image_equal_tofile(reloaded, infile) @pytest.mark.parametrize("mode", ("P", "PA")) - def test_palette(self, mode, tmp_path: Path) -> None: + def test_palette(self, mode: str, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") im = hopper(mode) @@ -689,7 +692,7 @@ def test_tiff_save_all(self) -> None: assert reread.n_frames == 3 # Test appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims mp = BytesIO() @@ -860,7 +863,7 @@ def test_timeout(self) -> None: ], ) @pytest.mark.timeout(2) - def test_oom(self, test_file) -> None: + def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): with Image.open(test_file): diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 73aaae6e757..fe055bf4b15 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -2,21 +2,22 @@ import colorsys import itertools +from typing import Callable from PIL import Image from .helper import assert_image_similar, hopper -def int_to_float(i): +def int_to_float(i: int) -> float: return i / 255 -def str_to_float(i): +def str_to_float(i: str) -> float: return ord(i) / 255 -def tuple_to_ints(tp): +def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]: x, y, z = tp return int(x * 255.0), int(y * 255.0), int(z * 255.0) @@ -25,7 +26,7 @@ def test_sanity() -> None: Image.new("HSV", (100, 100)) -def wedge(): +def wedge() -> Image.Image: w = Image._wedge() w90 = w.rotate(90) @@ -49,7 +50,11 @@ def wedge(): return img -def to_xxx_colorsys(im, func, mode): +def to_xxx_colorsys( + im: Image.Image, + func: Callable[[float, float, float], tuple[float, float, float]], + mode: str, +) -> Image.Image: # convert the hard way using the library colorsys routines. (r, g, b) = im.split() @@ -70,11 +75,11 @@ def to_xxx_colorsys(im, func, mode): return hsv -def to_hsv_colorsys(im): +def to_hsv_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") -def to_rgb_colorsys(im): +def to_rgb_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 94f57e06690..7e2290c156e 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Callable + from PIL import Image, ImageChops from .helper import assert_image_equal, hopper @@ -387,7 +389,9 @@ def test_overlay() -> None: def test_logical() -> None: - def table(op, a, b): + def table( + op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int + ) -> tuple[int, int, int, int]: out = [] for x in (a, b): imx = Image.new("1", (1, 1), x) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 07a25b84b4e..3171eb9aec1 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -5,6 +5,7 @@ import pytest from PIL import Image, ImageDraw, ImageDraw2, features +from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -56,7 +57,7 @@ def test_sanity() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(bbox) -> None: +def test_ellipse(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -84,7 +85,7 @@ def test_ellipse_edge() -> None: @pytest.mark.parametrize("points", POINTS) -def test_line(points) -> None: +def test_line(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -98,7 +99,7 @@ def test_line(points) -> None: @pytest.mark.parametrize("points", POINTS) -def test_line_pen_as_brush(points) -> None: +def test_line_pen_as_brush(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -114,7 +115,7 @@ def test_line_pen_as_brush(points) -> None: @pytest.mark.parametrize("points", POINTS) -def test_polygon(points) -> None: +def test_polygon(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -129,7 +130,7 @@ def test_polygon(points) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox) -> None: +def test_rectangle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index 9ce9cda8267..6ebc61e1bc4 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -22,7 +22,7 @@ def test_crash() -> None: ImageEnhance.Sharpness(im).enhance(0.5) -def _half_transparent_image(): +def _half_transparent_image() -> Image.Image: # returns an image, half transparent, half solid im = hopper("RGB") @@ -34,7 +34,9 @@ def _half_transparent_image(): return im -def _check_alpha(im, original, op, amount) -> None: +def _check_alpha( + im: Image.Image, original: Image.Image, op: str, amount: float +) -> None: assert im.getbands() == original.getbands() assert_image_equal( im.getchannel("A"), @@ -44,7 +46,7 @@ def _check_alpha(im, original, op, amount) -> None: @pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) -def test_alpha(op) -> None: +def test_alpha(op: str) -> None: # Issue https://github.com/python-pillow/Pillow/issues/899 # Is alpha preserved through image enhancement? diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 49140978168..44521a8b3dd 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -31,7 +31,7 @@ class TestImageFile: def test_parser(self) -> None: - def roundtrip(format): + def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 909026dc8c6..c79b36ca432 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -7,11 +7,13 @@ import sys from io import BytesIO from pathlib import Path +from typing import BinaryIO import pytest from packaging.version import parse as parse_version from PIL import Image, ImageDraw, ImageFont, features +from PIL._typing import StrOrBytesPath from .helper import ( assert_image_equal, @@ -47,11 +49,11 @@ def layout_engine(request): @pytest.fixture(scope="module") -def font(layout_engine): +def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont: return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine) -def test_font_properties(font) -> None: +def test_font_properties(font: ImageFont.FreeTypeFont) -> None: assert font.path == FONT_PATH assert font.size == FONT_SIZE @@ -67,7 +69,9 @@ def test_font_properties(font) -> None: assert font_copy.path == second_font_path -def _render(font, layout_engine): +def _render( + font: StrOrBytesPath | BinaryIO, layout_engine: ImageFont.Layout +) -> Image.Image: txt = "Hello World!" ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine) ttf.getbbox(txt) @@ -80,12 +84,12 @@ def _render(font, layout_engine): @pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH))) -def test_font_with_name(layout_engine, font) -> None: +def test_font_with_name(layout_engine: ImageFont.Layout, font: str | Path) -> None: _render(font, layout_engine) -def test_font_with_filelike(layout_engine) -> None: - def _font_as_bytes(): +def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None: + def _font_as_bytes() -> BytesIO: with open(FONT_PATH, "rb") as f: font_bytes = BytesIO(f.read()) return font_bytes @@ -102,12 +106,12 @@ def _font_as_bytes(): # _render(shared_bytes) -def test_font_with_open_file(layout_engine) -> None: +def test_font_with_open_file(layout_engine: ImageFont.Layout) -> None: with open(FONT_PATH, "rb") as f: _render(f, layout_engine) -def test_render_equal(layout_engine) -> None: +def test_render_equal(layout_engine: ImageFont.Layout) -> None: img_path = _render(FONT_PATH, layout_engine) with open(FONT_PATH, "rb") as f: font_filelike = BytesIO(f.read()) @@ -116,7 +120,7 @@ def test_render_equal(layout_engine) -> None: assert_image_equal(img_path, img_filelike) -def test_non_ascii_path(tmp_path: Path, layout_engine) -> None: +def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None: tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) try: shutil.copy(FONT_PATH, tempfile) @@ -126,7 +130,7 @@ def test_non_ascii_path(tmp_path: Path, layout_engine) -> None: ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine) -def test_transparent_background(font) -> None: +def test_transparent_background(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGBA", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -140,7 +144,7 @@ def test_transparent_background(font) -> None: assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_I16(font) -> None: +def test_I16(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="I;16", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -153,7 +157,7 @@ def test_I16(font) -> None: assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_textbbox_equal(font) -> None: +def test_textbbox_equal(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -181,7 +185,13 @@ def test_textbbox_equal(font) -> None: ), ) def test_getlength( - text, mode, fontname, size, layout_engine, length_basic, length_raqm + text: str, + mode: str, + fontname: str, + size: int, + layout_engine: ImageFont.Layout, + length_basic: int, + length_raqm: float, ) -> None: f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) @@ -207,7 +217,7 @@ def test_float_size() -> None: assert lengths[0] != lengths[1] != lengths[2] -def test_render_multiline(font) -> None: +def test_render_multiline(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) line_spacing = font.getbbox("A")[3] + 4 @@ -223,7 +233,7 @@ def test_render_multiline(font) -> None: assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) -def test_render_multiline_text(font) -> None: +def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None: # Test that text() correctly connects to multiline_text() # and that align defaults to left im = Image.new(mode="RGB", size=(300, 100)) @@ -243,7 +253,9 @@ def test_render_multiline_text(font) -> None: @pytest.mark.parametrize( "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) ) -def test_render_multiline_text_align(font, align, ext) -> None: +def test_render_multiline_text_align( + font: ImageFont.FreeTypeFont, align: str, ext: str +) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) @@ -251,7 +263,7 @@ def test_render_multiline_text_align(font, align, ext) -> None: assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) -def test_unknown_align(font) -> None: +def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -260,14 +272,14 @@ def test_unknown_align(font) -> None: draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") -def test_draw_align(font) -> None: +def test_draw_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (300, 100), "white") draw = ImageDraw.Draw(im) line = "some text" draw.text((100, 40), line, (0, 0, 0), font=font, align="left") -def test_multiline_bbox(font) -> None: +def test_multiline_bbox(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -285,7 +297,7 @@ def test_multiline_bbox(font) -> None: draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) -def test_multiline_width(font) -> None: +def test_multiline_width(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -295,7 +307,7 @@ def test_multiline_width(font) -> None: ) -def test_multiline_spacing(font) -> None: +def test_multiline_spacing(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) @@ -306,7 +318,9 @@ def test_multiline_spacing(font) -> None: @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font(font, orientation) -> None: +def test_rotated_transposed_font( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -347,7 +361,9 @@ def test_rotated_transposed_font(font, orientation) -> None: Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font(font, orientation) -> None: +def test_unrotated_transposed_font( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -382,7 +398,9 @@ def test_unrotated_transposed_font(font, orientation) -> None: @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font_get_mask(font, orientation) -> None: +def test_rotated_transposed_font_get_mask( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -403,7 +421,9 @@ def test_rotated_transposed_font_get_mask(font, orientation) -> None: Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font_get_mask(font, orientation) -> None: +def test_unrotated_transposed_font_get_mask( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -415,11 +435,11 @@ def test_unrotated_transposed_font_get_mask(font, orientation) -> None: assert mask.size == (108, 13) -def test_free_type_font_get_name(font) -> None: +def test_free_type_font_get_name(font: ImageFont.FreeTypeFont) -> None: assert ("FreeMono", "Regular") == font.getname() -def test_free_type_font_get_metrics(font) -> None: +def test_free_type_font_get_metrics(font: ImageFont.FreeTypeFont) -> None: ascent, descent = font.getmetrics() assert isinstance(ascent, int) @@ -427,7 +447,7 @@ def test_free_type_font_get_metrics(font) -> None: assert (ascent, descent) == (16, 4) -def test_free_type_font_get_mask(font) -> None: +def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: # Arrange text = "mask this" @@ -473,16 +493,16 @@ def test_default_font() -> None: @pytest.mark.parametrize("mode", (None, "1", "RGBA")) -def test_getbbox(font, mode) -> None: +def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None: assert (0, 4, 12, 16) == font.getbbox("A", mode) -def test_getbbox_empty(font) -> None: +def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None: # issue #2614, should not crash. assert (0, 0, 0, 0) == font.getbbox("") -def test_render_empty(font) -> None: +def test_render_empty(font: ImageFont.FreeTypeFont) -> None: # issue 2666 im = Image.new(mode="RGB", size=(300, 100)) target = im.copy() @@ -492,7 +512,7 @@ def test_render_empty(font) -> None: assert_image_equal(im, target) -def test_unicode_extended(layout_engine) -> None: +def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: # issue #3777 text = "A\u278A\U0001F12B" target = "Tests/images/unicode_extended.png" @@ -516,7 +536,7 @@ def test_unicode_extended(layout_engine) -> None: ) @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") def test_find_font(monkeypatch, platform, font_directory) -> None: - def _test_fake_loading_font(path_to_fake, fontname) -> None: + def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None: # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) with monkeypatch.context() as m: @@ -567,7 +587,7 @@ def fake_walker(path): _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") -def test_imagefont_getters(font) -> None: +def test_imagefont_getters(font: ImageFont.FreeTypeFont) -> None: assert font.getmetrics() == (16, 4) assert font.font.ascent == 16 assert font.font.descent == 4 @@ -588,7 +608,7 @@ def test_imagefont_getters(font) -> None: @pytest.mark.parametrize("stroke_width", (0, 2)) -def test_getsize_stroke(font, stroke_width) -> None: +def test_getsize_stroke(font: ImageFont.FreeTypeFont, stroke_width: int) -> None: assert font.getbbox("A", stroke_width=stroke_width) == ( 0 - stroke_width, 4 - stroke_width, @@ -607,7 +627,7 @@ def test_complex_font_settings() -> None: t.getmask("абвг", language="sr") -def test_variation_get(font) -> None: +def test_variation_get(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -662,7 +682,7 @@ def test_variation_get(font) -> None: ] -def _check_text(font, path, epsilon): +def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None: im = Image.new("RGB", (100, 75), "white") d = ImageDraw.Draw(im) d.text((10, 10), "Text", font=font, fill="black") @@ -677,7 +697,7 @@ def _check_text(font, path, epsilon): raise -def test_variation_set_by_name(font) -> None: +def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -702,7 +722,7 @@ def test_variation_set_by_name(font) -> None: _check_text(font, "Tests/images/variation_tiny_name.png", 40) -def test_variation_set_by_axes(font) -> None: +def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -737,7 +757,9 @@ def test_variation_set_by_axes(font) -> None: ), ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), ) -def test_anchor(layout_engine, anchor, left, top) -> None: +def test_anchor( + layout_engine: ImageFont.Layout, anchor: str, left: int, top: int +) -> None: name, text = "quick", "Quick" path = f"Tests/images/test_anchor_{name}_{anchor}.png" @@ -782,7 +804,9 @@ def test_anchor(layout_engine, anchor, left, top) -> None: ("md", "center"), ), ) -def test_anchor_multiline(layout_engine, anchor, align) -> None: +def test_anchor_multiline( + layout_engine: ImageFont.Layout, anchor: str, align: str +) -> None: target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" text = "a\nlong\ntext sample" @@ -800,7 +824,7 @@ def test_anchor_multiline(layout_engine, anchor, align) -> None: assert_image_similar_tofile(im, target, 4) -def test_anchor_invalid(font) -> None: +def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (100, 100), "white") d = ImageDraw.Draw(im) d.font = font @@ -826,7 +850,7 @@ def test_anchor_invalid(font) -> None: @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) -def test_bitmap_font(layout_engine, bpp) -> None: +def test_bitmap_font(layout_engine: ImageFont.Layout, bpp: int) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" @@ -843,7 +867,7 @@ def test_bitmap_font(layout_engine, bpp) -> None: assert_image_equal_tofile(im, target) -def test_bitmap_font_stroke(layout_engine) -> None: +def test_bitmap_font_stroke(layout_engine: ImageFont.Layout) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" @@ -861,7 +885,7 @@ def test_bitmap_font_stroke(layout_engine) -> None: @pytest.mark.parametrize("embedded_color", (False, True)) -def test_bitmap_blend(layout_engine, embedded_color) -> None: +def test_bitmap_blend(layout_engine: ImageFont.Layout, embedded_color: bool) -> None: font = ImageFont.truetype( "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine ) @@ -873,7 +897,7 @@ def test_bitmap_blend(layout_engine, embedded_color) -> None: assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png") -def test_standard_embedded_color(layout_engine) -> None: +def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) ttf.getbbox(txt) @@ -886,7 +910,7 @@ def test_standard_embedded_color(layout_engine) -> None: @pytest.mark.parametrize("fontmode", ("1", "L", "RGBA")) -def test_float_coord(layout_engine, fontmode): +def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None: txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) @@ -908,7 +932,7 @@ def test_float_coord(layout_engine, fontmode): raise -def test_cbdt(layout_engine) -> None: +def test_cbdt(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine @@ -925,7 +949,7 @@ def test_cbdt(layout_engine) -> None: pytest.skip("freetype compiled without libpng or CBDT support") -def test_cbdt_mask(layout_engine) -> None: +def test_cbdt_mask(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine @@ -942,7 +966,7 @@ def test_cbdt_mask(layout_engine) -> None: pytest.skip("freetype compiled without libpng or CBDT support") -def test_sbix(layout_engine) -> None: +def test_sbix(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -959,7 +983,7 @@ def test_sbix(layout_engine) -> None: pytest.skip("freetype compiled without libpng or SBIX support") -def test_sbix_mask(layout_engine) -> None: +def test_sbix_mask(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -977,7 +1001,7 @@ def test_sbix_mask(layout_engine) -> None: @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr(layout_engine) -> None: +def test_colr(layout_engine: ImageFont.Layout) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -993,7 +1017,7 @@ def test_colr(layout_engine) -> None: @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr_mask(layout_engine) -> None: +def test_colr_mask(layout_engine: ImageFont.Layout) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -1008,7 +1032,7 @@ def test_colr_mask(layout_engine) -> None: assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) -def test_woff2(layout_engine) -> None: +def test_woff2(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/OpenSans.woff2", @@ -1042,7 +1066,7 @@ def test_render_mono_size() -> None: assert_image_equal_tofile(im, "Tests/images/text_mono.gif") -def test_too_many_characters(font) -> None: +def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None: with pytest.raises(ValueError): font.getlength("A" * 1_000_001) with pytest.raises(ValueError): @@ -1070,7 +1094,7 @@ def test_too_many_characters(font) -> None: "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", ], ) -def test_oom(test_file) -> None: +def test_oom(test_file: str) -> None: with open(test_file, "rb") as f: font = ImageFont.truetype(BytesIO(f.read())) with pytest.raises(Image.DecompressionBombError): @@ -1091,6 +1115,8 @@ def test_raqm_missing_warning(monkeypatch) -> None: @pytest.mark.parametrize("size", [-1, 0]) -def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size) -> None: +def test_invalid_truetype_sizes_raise_valueerror( + layout_engine: ImageFont.Layout, size: int +) -> None: with pytest.raises(ValueError): ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 50bf404aebe..b320e79c10e 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -14,7 +14,7 @@ class Deformer: - def getmesh(self, im): + def getmesh(self, im: Image.Image) -> list[tuple[tuple[int, ...], tuple[int, ...]]]: x, y = im.size return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] @@ -108,7 +108,7 @@ def test_fit_same_ratio() -> None: @pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512))) -def test_contain(new_size) -> None: +def test_contain(new_size: tuple[int, int]) -> None: im = hopper() new_im = ImageOps.contain(im, new_size) assert new_im.size == (256, 256) @@ -132,7 +132,7 @@ def test_contain_round() -> None: ("hopper.png", (256, 256)), # square ), ) -def test_cover(image_name, expected_size) -> None: +def test_cover(image_name: str, expected_size: tuple[int, int]) -> None: with Image.open("Tests/images/" + image_name) as im: new_im = ImageOps.cover(im, (256, 256)) assert new_im.size == expected_size @@ -168,7 +168,7 @@ def test_pad_round() -> None: @pytest.mark.parametrize("mode", ("P", "PA")) -def test_palette(mode) -> None: +def test_palette(mode: str) -> None: im = hopper(mode) # Expand @@ -210,7 +210,7 @@ def test_scale() -> None: @pytest.mark.parametrize("border", (10, (1, 2, 3, 4))) -def test_expand_palette(border) -> None: +def test_expand_palette(border: int | tuple[int, int, int, int]) -> None: with Image.open("Tests/images/p_16.tga") as im: im_expanded = ImageOps.expand(im, border, (255, 0, 0)) @@ -366,7 +366,7 @@ def test_exif_transpose() -> None: for ext in exts: with Image.open("Tests/images/hopper" + ext) as base_im: - def check(orientation_im) -> None: + def check(orientation_im: Image.Image) -> None: for im in [ orientation_im, orientation_im.copy(), @@ -445,7 +445,7 @@ def test_autocontrast_cutoff() -> None: # Test the cutoff argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: - def autocontrast(cutoff): + def autocontrast(cutoff: int | tuple[int, int]): return ImageOps.autocontrast(img, cutoff).histogram() assert autocontrast(10) == autocontrast((10, 10)) @@ -486,20 +486,20 @@ def test_autocontrast_mask_real_input() -> None: assert result_nomask != result assert_tuple_approx_equal( ImageStat.Stat(result, mask=rect_mask).median, - [195, 202, 184], + (195, 202, 184), threshold=2, msg="autocontrast with mask pixel incorrect", ) assert_tuple_approx_equal( ImageStat.Stat(result_nomask).median, - [119, 106, 79], + (119, 106, 79), threshold=2, msg="autocontrast without mask pixel incorrect", ) def test_autocontrast_preserve_tone() -> None: - def autocontrast(mode, preserve_tone): + def autocontrast(mode: str, preserve_tone: bool) -> Image.Image: im = hopper(mode) return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() @@ -533,7 +533,7 @@ def test_autocontrast_preserve_gradient() -> None: @pytest.mark.parametrize( "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) ) -def test_autocontrast_preserve_one_color(color) -> None: +def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None: img = Image.new("RGB", (10, 10), color) # single color images shouldn't change diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 03302e20f2a..519d791050d 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,12 +1,14 @@ from __future__ import annotations +from typing import Generator + import pytest from PIL import Image, ImageFilter @pytest.fixture -def test_images(): +def test_images() -> Generator[dict[str, Image.Image], None, None]: ims = { "im": Image.open("Tests/images/hopper.ppm"), "snakes": Image.open("Tests/images/color_snakes.png"), @@ -18,7 +20,7 @@ def test_images(): im.close() -def test_filter_api(test_images) -> None: +def test_filter_api(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] test_filter = ImageFilter.GaussianBlur(2.0) @@ -32,7 +34,7 @@ def test_filter_api(test_images) -> None: assert i.size == (128, 128) -def test_usm_formats(test_images) -> None: +def test_usm_formats(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] usm = ImageFilter.UnsharpMask @@ -50,7 +52,7 @@ def test_usm_formats(test_images) -> None: im.convert("YCbCr").filter(usm) -def test_blur_formats(test_images) -> None: +def test_blur_formats(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] blur = ImageFilter.GaussianBlur @@ -68,7 +70,7 @@ def test_blur_formats(test_images) -> None: im.convert("YCbCr").filter(blur) -def test_usm_accuracy(test_images) -> None: +def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None: snakes = test_images["snakes"] src = snakes.convert("RGB") @@ -77,7 +79,7 @@ def test_usm_accuracy(test_images) -> None: assert i.tobytes() == src.tobytes() -def test_blur_accuracy(test_images) -> None: +def test_blur_accuracy(test_images: dict[str, Image.Image]) -> None: snakes = test_images["snakes"] i = snakes.filter(ImageFilter.GaussianBlur(0.4)) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 8ba745f215d..bd600b17744 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -3,6 +3,7 @@ import array import math import struct +from typing import Sequence import pytest @@ -75,7 +76,9 @@ def test_path_constructors(coords) -> None: [[0.0, 1.0]], ), ) -def test_invalid_path_constructors(coords) -> None: +def test_invalid_path_constructors( + coords: tuple[str, str] | Sequence[Sequence[int]] +) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -93,7 +96,7 @@ def test_invalid_path_constructors(coords) -> None: [0, 1, 2], ), ) -def test_path_odd_number_of_coordinates(coords) -> None: +def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -111,7 +114,9 @@ def test_path_odd_number_of_coordinates(coords) -> None: (1, (0.0, 0.0, 0.0, 0.0)), ], ) -def test_getbbox(coords, expected) -> None: +def test_getbbox( + coords: int | list[int], expected: tuple[float, float, float, float] +) -> None: # Arrange p = ImagePath.Path(coords) @@ -135,7 +140,7 @@ def test_getbbox_no_args() -> None: (list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]), ], ) -def test_map(coords, expected) -> None: +def test_map(coords: int | list[int], expected: list[tuple[float, float]]) -> None: # Arrange p = ImagePath.Path(coords) From 96fc60d5d2aa0ad13be0951efb1fe990a64f190a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Feb 2024 20:21:25 +1100 Subject: [PATCH 0202/2374] Removed mypy excludes --- pyproject.toml | 4 ---- tox.ini | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 48c59f2a1b4..e687f4bcf76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,7 +140,3 @@ follow_imports = "silent" warn_redundant_casts = true warn_unreachable = true warn_unused_ignores = true -exclude = [ - '^src/PIL/FpxImagePlugin.py$', - '^src/PIL/MicImagePlugin.py$', -] diff --git a/tox.ini b/tox.ini index 8c818df7a6a..3ef011c9e94 100644 --- a/tox.ini +++ b/tox.ini @@ -41,6 +41,7 @@ deps = packaging types-cffi types-defusedxml + types-olefile extras = typing commands = From b6fdf2e9e7a65bf23cac224b2ab96c6b1d2c8449 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Feb 2024 07:54:58 +1100 Subject: [PATCH 0203/2374] Updated package name for Tidelift --- .github/FUNDING.yml | 2 +- README.md | 2 +- docs/conf.py | 2 +- docs/index.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e0e6804bfe4..8fc6bd0ad54 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -tidelift: "pypi/Pillow" +tidelift: "pypi/pillow" diff --git a/README.md b/README.md index 6ca870166a1..9776c40e2f7 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ As of 2019, Pillow development is src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"> Tidelift + src="https://tidelift.com/badges/package/pypi/pillow?style=flat"> Newest PyPI version diff --git a/docs/conf.py b/docs/conf.py index 9ae7ae605fd..97289c91d0c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -326,7 +326,7 @@ r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", - r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", + r"https://tidelift.com/badges/package/pypi/pillow?.*": r"https://img.shields.io/badge/.*", r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", } diff --git a/docs/index.rst b/docs/index.rst index 5583699193c..bf2feea9a53 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -49,7 +49,7 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Mon, 19 Feb 2024 17:08:21 +1100 Subject: [PATCH 0204/2374] Added image to illustrate size vs bbox --- docs/deprecations.rst | 8 ++++++-- docs/example/size_vs_bbox.png | Bin 0 -> 12934 bytes docs/releasenotes/9.2.0.rst | 8 ++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 docs/example/size_vs_bbox.png diff --git a/docs/deprecations.rst b/docs/deprecations.rst index c90ad481adb..74021a218ef 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -253,8 +253,12 @@ Use instead:: width, height = right - left, bottom - top Previously, the ``size`` methods returned a ``height`` that included the vertical -offset of the text, while the new ``bbox`` methods explicitly distinguish this as a -``top`` offset. +offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` +offset. + +.. image:: ./example/size_vs_bbox.png + :alt: Demonstration of size height vs bbox top and bottom + :align: center FreeTypeFont.getmask2 fill parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/example/size_vs_bbox.png b/docs/example/size_vs_bbox.png new file mode 100644 index 0000000000000000000000000000000000000000..11a05d2a8146a612741c7e4ec4d3cf0ce05a35a0 GIT binary patch literal 12934 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 literal 0 HcmV?d00001 diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 495926ca749..db514068f77 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -90,8 +90,12 @@ Use instead:: width, height = right - left, bottom - top Previously, the ``size`` methods returned a ``height`` that included the vertical -offset of the text, while the new ``bbox`` methods explicitly distinguish this as a -``top`` offset. +offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` +offset. + +.. image:: ./example/size_vs_bbox.png + :alt: Demonstration of size height vs bbox top and bottom + :align: center API Additions ============= From 0692ad8cdd4fe979478d910b705dcc12f8e82f61 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Feb 2024 23:29:58 +1100 Subject: [PATCH 0205/2374] Updated giflib on macOS to 5.2.2 --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 26bf2f6d655..1ec2811f6d8 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -24,7 +24,7 @@ XZ_VERSION=5.4.5 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 if [[ -n "$IS_MACOS" ]]; then - GIFLIB_VERSION=5.1.4 + GIFLIB_VERSION=5.2.2 else GIFLIB_VERSION=5.2.1 fi From fe1edb1e0f8608e3924f67b53776f653c418ddfe Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 19 Feb 2024 16:47:07 +0200 Subject: [PATCH 0206/2374] Install mypy from requirements file So Renovate can update it on a schedule --- .ci/requirements-mypy.txt | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .ci/requirements-mypy.txt diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt new file mode 100644 index 00000000000..ed32694607d --- /dev/null +++ b/.ci/requirements-mypy.txt @@ -0,0 +1 @@ +mypy==1.7.1 diff --git a/tox.ini b/tox.ini index 8c818df7a6a..85800ff8d7a 100644 --- a/tox.ini +++ b/tox.ini @@ -33,10 +33,10 @@ commands = [testenv:mypy] skip_install = true deps = + -r .ci/requirements-mypy.txt IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython - mypy==1.7.1 numpy packaging types-cffi From 7490aee8362c589a475035bf86c05b29c894593c Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 19 Feb 2024 20:18:18 +0100 Subject: [PATCH 0207/2374] Show how to use anchors to align text in imagefont deprecations --- docs/deprecations.rst | 24 ++++++++++++++++++++++++ docs/releasenotes/9.2.0.rst | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 74021a218ef..9efb1316d5c 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -260,6 +260,30 @@ offset. :alt: Demonstration of size height vs bbox top and bottom :align: center +If you are using these methods for aligning text, consider using :ref:`text-anchors` instead +which avoid issues that can occur with non-English text or unusual fonts. +For example, instead of the following code:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width, height = draw.textsize("Hello world", font) + x, y = (100 - width) / 2, (100 - height) / 2 + draw.text((x, y), "Hello world", font=font) + +Use instead:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + draw.text((100 / 2, 100 / 2), "Hello world", font=font, anchor="mm") + FreeTypeFont.getmask2 fill parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index db514068f77..e8bf33b60f2 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -97,6 +97,30 @@ offset. :alt: Demonstration of size height vs bbox top and bottom :align: center +If you are using these methods for aligning text, consider using :ref:`text-anchors` instead +which avoid issues that can occur with non-English text or unusual fonts. +For example, instead of the following code:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width, height = draw.textsize("Hello world", font) + x, y = (100 - width) / 2, (100 - height) / 2 + draw.text((x, y), "Hello world", font=font) + +Use instead:: + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + draw.text((100 / 2, 100 / 2), "Hello world", font=font, anchor="mm") + API Additions ============= From 531b1e1b9a6b3f83519d9b6687523474f4a18d83 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 19 Feb 2024 22:50:06 +0200 Subject: [PATCH 0208/2374] Remove outdated installation warnings --- docs/installation.rst | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 116bdcf2fb1..980bbd99def 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -9,15 +9,6 @@ Installation }); -Warnings --------- - -.. warning:: Pillow and PIL cannot co-exist in the same environment. Before installing Pillow, please uninstall PIL. - -.. warning:: Pillow >= 1.0 no longer supports ``import Image``. Please use ``from PIL import Image`` instead. - -.. warning:: Pillow >= 2.1.0 no longer supports ``import _imaging``. Please use ``from PIL.Image import core as _imaging`` instead. - Python Support -------------- From e39765d755cc2d37e79d07f58ebc77a8e44812c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 20 Feb 2024 15:41:20 +1100 Subject: [PATCH 0209/2374] Added type hints --- Tests/test_deprecate.py | 6 +++--- Tests/test_file_ico.py | 2 +- Tests/test_file_iptc.py | 2 +- Tests/test_file_msp.py | 2 +- Tests/test_file_png.py | 2 ++ Tests/test_file_psd.py | 2 +- Tests/test_file_tga.py | 4 ++-- Tests/test_file_tiff_metadata.py | 6 ++++-- Tests/test_imagecms.py | 28 ++++++++++++++++++++++------ Tests/test_imagefont.py | 22 ++++++++++++---------- Tests/test_imagegrab.py | 6 ++++-- Tests/test_imagepath.py | 8 +++++--- Tests/test_imageqt.py | 2 +- Tests/test_imageshow.py | 10 ++++++---- Tests/test_imagewin_pointers.py | 2 +- Tests/test_numpy.py | 6 +++--- Tests/test_qt_image_qapplication.py | 4 ++-- Tests/test_qt_image_toqimage.py | 2 +- Tests/test_util.py | 2 +- 19 files changed, 73 insertions(+), 45 deletions(-) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 6ffc8f6f589..584d8f91d67 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -20,7 +20,7 @@ ), ], ) -def test_version(version, expected) -> None: +def test_version(version: int | None, expected: str) -> None: with pytest.warns(DeprecationWarning, match=expected): _deprecate.deprecate("Old thing", version, "new thing") @@ -46,7 +46,7 @@ def test_unknown_version() -> None: ), ], ) -def test_old_version(deprecated, plural, expected) -> None: +def test_old_version(deprecated: str, plural: bool, expected: str) -> None: expected = r"" with pytest.raises(RuntimeError, match=expected): _deprecate.deprecate(deprecated, 1, plural=plural) @@ -76,7 +76,7 @@ def test_replacement_and_action() -> None: "Upgrade to new thing.", ], ) -def test_action(action) -> None: +def test_action(action: str) -> None: expected = ( r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Upgrade to new thing\." diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 65f090931b4..f69a290fabf 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -135,7 +135,7 @@ def test_different_bit_depths(tmp_path: Path) -> None: @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) -def test_save_to_bytes_bmp(mode) -> None: +def test_save_to_bytes_bmp(mode: str) -> None: output = io.BytesIO() im = hopper(mode) im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 9c0969437ea..88c30d46895 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -98,7 +98,7 @@ def test_i() -> None: assert ret == 97 -def test_dump(monkeypatch) -> None: +def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: # Arrange c = b"abc" # Temporarily redirect stdout diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index f9f81d11413..b0964aabe12 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -52,7 +52,7 @@ def test_open_windows_v1() -> None: assert isinstance(im, MspImagePlugin.MspImageFile) -def _assert_file_image_equal(source_path, target_path) -> None: +def _assert_file_image_equal(source_path: str, target_path: str) -> None: with Image.open(source_path) as im: assert_image_equal_tofile(im, target_path) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index d4a63431647..c51f56ce7c0 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -6,6 +6,7 @@ import zlib from io import BytesIO from pathlib import Path +from types import ModuleType from typing import Any import pytest @@ -23,6 +24,7 @@ skip_unless_feature, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 7eca8d9b151..e60638b224d 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -157,7 +157,7 @@ def test_combined_larger_than_size() -> None: ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), ], ) -def test_crashes(test_file, raises) -> None: +def test_crashes(test_file: str, raises) -> None: with open(test_file, "rb") as f: with pytest.raises(raises): with Image.open(f): diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bd8e522c7b5..3c6da50c532 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -22,8 +22,8 @@ @pytest.mark.parametrize("mode", _MODES) -def test_sanity(mode, tmp_path: Path) -> None: - def roundtrip(original_im) -> None: +def test_sanity(mode: str, tmp_path: Path) -> None: + def roundtrip(original_im: Image.Image) -> None: out = str(tmp_path / "temp.tga") original_im.save(out, rle=rle) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index bb6225d075b..d7a18c72504 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -189,7 +189,9 @@ def test_iptc(tmp_path: Path) -> None: @pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) -def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None: +def test_writing_other_types_to_ascii( + value: bytes | int, expected: str, tmp_path: Path +) -> None: info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[271] @@ -206,7 +208,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None: @pytest.mark.parametrize("value", (1, IFDRational(1))) -def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None: +def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 83fc38ed3fd..21a0dd75b98 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -237,7 +237,7 @@ def test_invalid_color_temperature() -> None: @pytest.mark.parametrize("flag", ("my string", -1)) -def test_invalid_flag(flag) -> None: +def test_invalid_flag(flag: str | int) -> None: with hopper() as im: with pytest.raises( ImageCms.PyCMSError, match="flags must be an integer between 0 and " @@ -335,12 +335,26 @@ def test_extended_information() -> None: o = ImageCms.getOpenProfile(SRGB) p = o.profile - def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None: + def assert_truncated_tuple_equal( + tup1: tuple[tuple[float, float, float], ...] | tuple[float], + tup2: ( + tuple[tuple[tuple[float, float, float], ...], ...] + | tuple[tuple[float, float, float], ...] + | tuple[float] + ), + digits: int = 10, + ) -> None: # Helper function to reduce precision of tuples of floats # recursively and then check equality. power = 10**digits - def truncate_tuple(tuple_or_float): + def truncate_tuple( + tuple_or_float: ( + tuple[tuple[tuple[float, float, float], ...], ...] + | tuple[tuple[float, float, float], ...] + | tuple[float, ...] + ) + ) -> tuple[tuple[float, ...], ...]: return tuple( ( truncate_tuple(val) @@ -504,8 +518,10 @@ def test_profile_typesafety() -> None: ImageCms.ImageCmsProfile(1).tobytes() -def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None: - def create_test_image(): +def assert_aux_channel_preserved( + mode: str, transform_in_place: bool, preserved_channel: str +) -> None: + def create_test_image() -> Image.Image: # set up test image with something interesting in the tested aux channel. # fmt: off nine_grid_deltas = [ @@ -633,7 +649,7 @@ def test_auxiliary_channels_isolated() -> None: @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) -def test_rgb_lab(mode) -> None: +def test_rgb_lab(mode: str) -> None: im = Image.new(mode, (1, 1)) converted_im = im.convert("LAB") assert converted_im.getpixel((0, 0)) == (0, 128, 128) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index c79b36ca432..05b5d471691 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -7,7 +7,7 @@ import sys from io import BytesIO from pathlib import Path -from typing import BinaryIO +from typing import Any, BinaryIO import pytest from packaging.version import parse as parse_version @@ -44,7 +44,7 @@ def test_sanity() -> None: pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), ], ) -def layout_engine(request): +def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout: return request.param @@ -535,21 +535,23 @@ def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), ) @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") -def test_find_font(monkeypatch, platform, font_directory) -> None: +def test_find_font( + monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str +) -> None: def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None: # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) with monkeypatch.context() as m: m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False) - def loadable_font(filepath, size, index, encoding, *args, **kwargs): + def loadable_font( + filepath: str, size: int, index: int, encoding: str, *args: Any + ): if filepath == path_to_fake: return ImageFont._FreeTypeFont( - FONT_PATH, size, index, encoding, *args, **kwargs + FONT_PATH, size, index, encoding, *args ) - return ImageFont._FreeTypeFont( - filepath, size, index, encoding, *args, **kwargs - ) + return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args) m.setattr(ImageFont, "FreeTypeFont", loadable_font) font = ImageFont.truetype(fontname) @@ -563,7 +565,7 @@ def loadable_font(filepath, size, index, encoding, *args, **kwargs): if platform == "linux": monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") - def fake_walker(path): + def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]: if path == font_directory: return [ ( @@ -1101,7 +1103,7 @@ def test_oom(test_file: str) -> None: font.getmask("Test Text") -def test_raqm_missing_warning(monkeypatch) -> None: +def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) with pytest.warns(UserWarning) as record: font = ImageFont.truetype( diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 235a2f993bd..e23adeb7083 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -84,6 +84,7 @@ def test_grabclipboard(self) -> None: @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grabclipboard_file(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + assert p.stdin is not None p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') p.communicate() @@ -94,6 +95,7 @@ def test_grabclipboard_file(self) -> None: @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grabclipboard_png(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + assert p.stdin is not None p.stdin.write( rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") $ms = new-object System.IO.MemoryStream(, $bytes) @@ -113,7 +115,7 @@ def test_grabclipboard_png(self) -> None: reason="Linux with wl-clipboard only", ) @pytest.mark.parametrize("ext", ("gif", "png", "ico")) - def test_grabclipboard_wl_clipboard(self, ext) -> None: + def test_grabclipboard_wl_clipboard(self, ext: str) -> None: image_path = "Tests/images/hopper." + ext with open(image_path, "rb") as fp: subprocess.call(["wl-copy"], stdin=fp) @@ -128,6 +130,6 @@ def test_grabclipboard_wl_clipboard(self, ext) -> None: reason="Linux with wl-clipboard only", ) @pytest.mark.parametrize("arg", ("text", "--clear")) - def test_grabclipboard_wl_clipboard_errors(self, arg): + def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None: subprocess.call(["wl-copy", arg]) assert ImageGrab.grabclipboard() is None diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index bd600b17744..9487560af88 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -58,7 +58,9 @@ def test_path() -> None: ImagePath.Path((0, 1)), ), ) -def test_path_constructors(coords) -> None: +def test_path_constructors( + coords: Sequence[float] | array.array[float] | ImagePath.Path, +) -> None: # Arrange / Act p = ImagePath.Path(coords) @@ -206,9 +208,9 @@ class Evil: def __init__(self) -> None: self.corrupt = Image.core.path(0x4000000000000000) - def __getitem__(self, i): + def __getitem__(self, i: int) -> bytes: x = self.corrupt[i] return struct.pack("dd", x[0], x[1]) - def __setitem__(self, i, x) -> None: + def __setitem__(self, i: int, x: bytes) -> None: self.corrupt[i] = struct.unpack("dd", x) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 909f9716700..88ad1f9eee4 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -28,7 +28,7 @@ def test_rgb() -> None: assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) - def checkrgb(r, g, b) -> None: + def checkrgb(r: int, g: int, b: int) -> None: val = ImageQt.rgb(r, g, b) val = val % 2**24 # drop the alpha assert val >> 16 == r diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index f7269d45b56..8d741d94ada 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + import pytest from PIL import Image, ImageShow @@ -24,9 +26,9 @@ def test_register() -> None: "order", [-1, 0], ) -def test_viewer_show(order) -> None: +def test_viewer_show(order: int) -> None: class TestViewer(ImageShow.Viewer): - def show_image(self, image, **options) -> bool: + def show_image(self, image: Image.Image, **options: Any) -> bool: self.methodCalled = True return True @@ -48,7 +50,7 @@ def show_image(self, image, **options) -> bool: reason="Only run on CIs; hangs on Windows CIs", ) @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) -def test_show(mode) -> None: +def test_show(mode: str) -> None: im = hopper(mode) assert ImageShow.show(im) @@ -73,7 +75,7 @@ def test_viewer() -> None: @pytest.mark.parametrize("viewer", ImageShow._viewers) -def test_viewers(viewer) -> None: +def test_viewers(viewer: ImageShow.Viewer) -> None: try: viewer.get_command("test.jpg") except NotImplementedError: diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index c7f633e6290..f59ee7284b8 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -70,7 +70,7 @@ class BITMAPINFOHEADER(ctypes.Structure): ] CreateDIBSection.restype = ctypes.wintypes.HBITMAP - def serialize_dib(bi, pixels): + def serialize_dib(bi, pixels) -> bytearray: bf = BITMAPFILEHEADER() bf.bfType = 0x4D42 bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 6ba95c2d700..9f4e6534e8a 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -14,7 +14,7 @@ def test_numpy_to_image() -> None: - def to_image(dtype, bands: int = 1, boolean: int = 0): + def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image: if bands == 1: if boolean: data = [0, 255] * 50 @@ -99,7 +99,7 @@ def test_1d_array() -> None: assert_image(Image.fromarray(a), "L", (1, 5)) -def _test_img_equals_nparray(img, np) -> None: +def _test_img_equals_nparray(img: Image.Image, np) -> None: assert len(np.shape) >= 2 np_size = np.shape[1], np.shape[0] assert img.size == np_size @@ -157,7 +157,7 @@ def test_save_tiff_uint16() -> None: ("HSV", numpy.uint8), ), ) -def test_to_array(mode, dtype) -> None: +def test_to_array(mode: str, dtype) -> None: img = hopper(mode) # Resize to non-square diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 7d6c0a8cb7d..3cd323553fa 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -4,7 +4,7 @@ import pytest -from PIL import ImageQt +from PIL import Image, ImageQt from .helper import assert_image_equal_tofile, assert_image_similar, hopper @@ -37,7 +37,7 @@ def __init__(self) -> None: lbl.setPixmap(pixmap1.copy()) -def roundtrip(expected) -> None: +def roundtrip(expected: Image.Image) -> None: result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb assert_image_similar(result, expected.convert("RGB"), 1) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index a222a7d71b9..6110be707f5 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -17,7 +17,7 @@ @pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) -def test_sanity(mode, tmp_path: Path) -> None: +def test_sanity(mode: str, tmp_path: Path) -> None: src = hopper(mode) data = ImageQt.toqimage(src) diff --git a/Tests/test_util.py b/Tests/test_util.py index 73e4acd5555..197ef79eef5 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -10,7 +10,7 @@ @pytest.mark.parametrize( "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] ) -def test_is_path(test_path) -> None: +def test_is_path(test_path: str | Path | PurePath) -> None: # Act it_is = _util.is_path(test_path) From a655d7606e2f12f0e7700ef754ed92a6da45f658 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 20 Feb 2024 21:27:30 +1100 Subject: [PATCH 0210/2374] Simplified type hints --- Tests/test_imagecms.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 21a0dd75b98..a7bb31db512 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -6,6 +6,7 @@ import shutil from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -336,25 +337,13 @@ def test_extended_information() -> None: p = o.profile def assert_truncated_tuple_equal( - tup1: tuple[tuple[float, float, float], ...] | tuple[float], - tup2: ( - tuple[tuple[tuple[float, float, float], ...], ...] - | tuple[tuple[float, float, float], ...] - | tuple[float] - ), - digits: int = 10, + tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10 ) -> None: # Helper function to reduce precision of tuples of floats # recursively and then check equality. power = 10**digits - def truncate_tuple( - tuple_or_float: ( - tuple[tuple[tuple[float, float, float], ...], ...] - | tuple[tuple[float, float, float], ...] - | tuple[float, ...] - ) - ) -> tuple[tuple[float, ...], ...]: + def truncate_tuple(tuple_or_float: tuple[Any, ...]) -> tuple[Any, ...]: return tuple( ( truncate_tuple(val) From 64579510c018440edb1b0c9c67f36d478f969f38 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 20 Feb 2024 21:37:06 +1100 Subject: [PATCH 0211/2374] Updated alt text --- docs/deprecations.rst | 2 +- docs/releasenotes/9.2.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 74021a218ef..9fd64fdaa42 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -257,7 +257,7 @@ offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` offset. .. image:: ./example/size_vs_bbox.png - :alt: Demonstration of size height vs bbox top and bottom + :alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself. :align: center FreeTypeFont.getmask2 fill parameter diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index db514068f77..b596a6ab20f 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -94,7 +94,7 @@ offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` offset. .. image:: ./example/size_vs_bbox.png - :alt: Demonstration of size height vs bbox top and bottom + :alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself. :align: center API Additions From 56a02b76eb403356436e6480c5ada23930825e36 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 20 Feb 2024 21:37:40 +1100 Subject: [PATCH 0212/2374] Corrected image path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- docs/releasenotes/9.2.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index b596a6ab20f..677438bc63e 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -93,7 +93,7 @@ Previously, the ``size`` methods returned a ``height`` that included the vertica offset of the text, while the new ``bbox`` methods distinguish this as a ``top`` offset. -.. image:: ./example/size_vs_bbox.png +.. image:: ../example/size_vs_bbox.png :alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself. :align: center From 5b20811cabd4594fd4fafc596e3f94afe12bb361 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 20 Feb 2024 20:36:36 +0100 Subject: [PATCH 0213/2374] Add `--bugreport` argument to __main__.py to omit supported formats --- src/PIL/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/__main__.py b/src/PIL/__main__.py index 943789923dc..32de33624db 100644 --- a/src/PIL/__main__.py +++ b/src/PIL/__main__.py @@ -1,5 +1,7 @@ from __future__ import annotations +import sys + from .features import pilinfo -pilinfo() +pilinfo(supported_formats="--bugreport" not in sys.argv) From 10712be53d575a37d9dc2522c9f8f0f62871f3b6 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 20 Feb 2024 21:21:10 +0100 Subject: [PATCH 0214/2374] Build docs for Python changes --- .github/workflows/docs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4319cc8ff28..92e860cb547 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,10 +7,12 @@ on: paths: - ".github/workflows/docs.yml" - "docs/**" + - "src/PIL/**" pull_request: paths: - ".github/workflows/docs.yml" - "docs/**" + - "src/PIL/**" workflow_dispatch: permissions: From ab9dfd8181868922abee46dffb11ffcb9a772958 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 20 Feb 2024 20:37:33 +0100 Subject: [PATCH 0215/2374] Add sys.{executable,base_prefix,prefix} to features.pilinfo --- Tests/test_features.py | 12 +++++++++--- Tests/test_main.py | 12 +++++++++--- src/PIL/features.py | 9 +++++++-- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index 8d2d198ffcd..3fffa032f81 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -129,9 +129,15 @@ def test_pilinfo() -> None: while lines[0].startswith(" "): lines = lines[1:] assert lines[0] == "-" * 68 - assert lines[1].startswith("Python modules loaded from ") - assert lines[2].startswith("Binary modules loaded from ") - assert lines[3] == "-" * 68 + assert lines[1].startswith("Python executable is") + lines = lines[2:] + if lines[0].startswith("Environment Python files loaded from"): + lines = lines[1:] + assert lines[0].startswith("System Python files loaded from") + assert lines[1] == "-" * 68 + assert lines[2].startswith("Python Pillow modules loaded from ") + assert lines[3].startswith("Binary Pillow modules loaded from ") + assert lines[4] == "-" * 68 jpeg = ( "\n" + "-" * 68 diff --git a/Tests/test_main.py b/Tests/test_main.py index 46259f1dc52..e13e0c5e3ac 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -15,9 +15,15 @@ def test_main() -> None: while lines[0].startswith(" "): lines = lines[1:] assert lines[0] == "-" * 68 - assert lines[1].startswith("Python modules loaded from ") - assert lines[2].startswith("Binary modules loaded from ") - assert lines[3] == "-" * 68 + assert lines[1].startswith("Python executable is") + lines = lines[2:] + if lines[0].startswith("Environment Python files loaded from"): + lines = lines[1:] + assert lines[0].startswith("System Python files loaded from") + assert lines[1] == "-" * 68 + assert lines[2].startswith("Python Pillow modules loaded from ") + assert lines[3].startswith("Binary Pillow modules loaded from ") + assert lines[4] == "-" * 68 jpeg = ( os.linesep + "-" * 68 diff --git a/src/PIL/features.py b/src/PIL/features.py index b14d6df13f2..7c5112ef6e4 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -249,12 +249,17 @@ def pilinfo(out=None, supported_formats=True): for py_version in py_version[1:]: print(f" {py_version.strip()}", file=out) print("-" * 68, file=out) + print(f"Python executable is {sys.executable or 'unknown'}", file=out) + if sys.prefix != sys.base_prefix: + print(f"Environment Python files loaded from {sys.prefix}", file=out) + print(f"System Python files loaded from {sys.base_prefix}", file=out) + print("-" * 68, file=out) print( - f"Python modules loaded from {os.path.dirname(Image.__file__)}", + f"Python Pillow modules loaded from {os.path.dirname(Image.__file__)}", file=out, ) print( - f"Binary modules loaded from {os.path.dirname(Image.core.__file__)}", + f"Binary Pillow modules loaded from {os.path.dirname(Image.core.__file__)}", file=out, ) print("-" * 68, file=out) From 89c44be404081c77e68457c1c50a78e96a6d42b9 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 20 Feb 2024 20:39:40 +0100 Subject: [PATCH 0216/2374] Mention `python -m PIL --bugreport` in the issue template --- .github/ISSUE_TEMPLATE/ISSUE_REPORT.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md b/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md index 115f6135dfb..cfd576f3588 100644 --- a/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md +++ b/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md @@ -48,6 +48,10 @@ Thank you. * Python: * Pillow: +```text +please paste the output of running `python3 -m PIL --bugreport` here +``` + 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 RmVbqbo3jf9uwz-@Eq)%)BzFJ+ literal 0 HcmV?d00001 diff --git a/Tests/images/avif/rot1mir1.avif b/Tests/images/avif/rot1mir1.avif new file mode 100644 index 0000000000000000000000000000000000000000..0181b088cec5012953258419b42cbc90ea44b948 GIT binary patch literal 16588 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 literal 0 HcmV?d00001 diff --git a/Tests/images/avif/rot2mir0.avif b/Tests/images/avif/rot2mir0.avif new file mode 100644 index 0000000000000000000000000000000000000000..ddaa02f3f890f0cd65035a89294893956c2a30fd GIT binary patch literal 17001 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 literal 0 HcmV?d00001 diff --git a/Tests/images/avif/rot3mir1.avif b/Tests/images/avif/rot3mir1.avif new file mode 100644 index 0000000000000000000000000000000000000000..ccd8861d5ae9378bcb6613f6391f2c4270c5f706 GIT binary patch literal 17290 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 literal 0 HcmV?d00001 diff --git a/Tests/images/avif/star.avifs b/Tests/images/avif/star.avifs new file mode 100644 index 0000000000000000000000000000000000000000..f2753395f8c3c131267eb4d1846ca9a21a6a47a3 GIT binary patch literal 29724 zcmeFZWmH|w)+V}fcXyZI?oM!bcXxLS5Q00w-Q6966P)0X;O_3;J9*DJ-|5r0`}Q3@ zdW`$yElQr6HLF(Ds;6r0IasRz005J@tCypZyOj$7=+P{hj0-u$-= zs0jez0yAd6!S7nXdn#<}X!)mG|5XBP;dhCsgRR4#YvWouoBh=S0Dx%Z=4lJ83-aeW zzpNac9f55%SUH+F08?};7e}+-jRRfIKLWC&k;`u#V1NDuY$JDO(Lew`02US~0qeZE ze!u<*L|nj@MS&^6&DoyE&e6=`Pb9))VrAmUUe$22008Vy;Cg2Tijjc<*zTVd_*)BDp8yX?ov8l>b^Z(lsQZ6N z9RRxPUo!~*XJ7bpZBX?8O**muJDq^_f3LTHxqqV*@t?K+hg5CI$_zy0^MFJo@QvZbit6T&=Zvb#^zz_)xdces;0*3$K zqU1N=Hd<_FYUKLwJ3dP$3Npwiz)f4i?4F)Oi<2O5zf&V3KpF5E>hz7DCP10Sm+16n zZ39Yt57XAU1KywV?KuZvyqn5f-h$dRLk~lW!f7K~-cq$SGoy#07qdJKG$~vzk16jT zlkAf6-u+V#+wa&8=y8S6FNiiB;fY811|X5s*<)IYlNOLzS0mG8PDY9NQb~vMAMS&d zjmLEOaTsmC_3H%J{2Zh8xCOIw>kr7*Dr0p-O5O<@?QZJR5&qTKuRQ1>F}*g{O|lYd zB(6=wjlS1=mok;Cm%u7Msr5V>+KRDb_yBJnHo;kl4DZ8|b7~2`n*-;NBtVLKU5nbR;7EVf&>ov!A!xwb zHnd^+Ca?cBXZ##d6Zw9f41w1e1w2vPyv{$nLd_pdeMk46!+5}-M@|`~YXs9rfQn!v z2?3jpB08Bf>48lS8Ed)^S=l2vdw8GB!2Y`zis?tnJ}mY)?5uHG73iFE+wl#(afEGE zkjb?*!u2hFH@qGZoQRTk-s6LXyVH@b>WiC z!~@$UvY`{(xF6W|oP`}`~ShkP@SrL`r5LDa6CAQBL$ zCwxO#IYCj$;84_M#RT!4wjSfrobv;bY)7EOwOdq|hTA_6_7uIFm@c&Vw)O&ckj0^t zG%)oy3JMYSKx85HhGN;F71JK_;)Ibh3@)6h?N7{Fud+h8{7HvTAF)aKyc@mP)$%E3 z5%#}P|3s8hm^77al?sbj`brEPS4P#9GpX>^HW@sRk=RY=RacW{r&9ov6-l(tRsWTZ z&+J)D_do>B1dXX2nT~!fSF@tGl-2Xq?A2eg6#*;J1q#4os&L@~F{VqITXhR|+l%la zcYlWB!9WHbVLu5MWqE~ycTsOA%fmU@Es+!B+0{&`jaW7pa-DrA`rs{=QRpb!Hwv$6 z9WeNX5_}&OfjQ20<`|kq)$N+R>4Njb)US-O@XBOQqRnsl__)%^w6w?$ptt3t({*v| z>Nx2isp@Wb!5^w}8)yJdb9i8ZcBgv#g+0dFL8KK*KiE2Yx>|e=0^2w1e!kohkr=wV zk;YBwAeLIUas-blogi`1%n-%424ha&y5y~r@sYDlN<0tp=NDnt;V{?s86zBOl8pEl z(tf);E#t0Ny%%BE__5NWgVk49%GFfxKI@d^eV>hP!X`s09;4G~XiY*b>|GV72*R+2 znkSWq7>755IYYCBhd5vZ#Ar5teslxI#uCD!u zohWVThRQh$bB_6KHl-ho2_^1gES@j$f_lJYo?`--Vm%uV9+ZWV=-tymOSUeqnaDcD8QsLfQ+^M3s(4m0lv2UZS9&AeJ`C zV?W5@??*N9ez!*KDb1~D7pHaX%x$-?kb1ccbCv!?liI(B(oQ1as_|4jx8Jh?dsy_e z@StIQz3zxthf$QYndkK?vFC^Fy@-NoXp$CIi&8FYKd+FF%TF;t5~0Du5VPnk;UR!f zW}r6Xyo`4h?QizPib>D?VK$yua>wF_lP%@Rg=3l~n42^I+3sEt3IwWJ%^a`hkF$ZnSjk^PL`tPtzT0A*q{+H}>;JF4-Ktp03W4 zqfpsf(h&?Q+WRN($3aXOHTJZo61w-7A;yqR-;nIvr|sm=q?h3-x7%FdY`ir%19JUS2sOy} zeTXjDhjmb#!X_F_b3t*Cf=VV@^wt3n)SMz;(J`5POu+1&9oMhD^TAN)312U=G}6z|ImbB7v(xmb;q{hCc;l!sU!cb6{Rd!_(QHq zJ>(#7x3T7ZO6Ky-&BQ*V9DSk9n!BrA0IHqzC4WyPMlll7DF7`iTG^E9O6o-B@wkDV zc?2`PWJz5}55rq6uqnw3Re@v*dZp2@nnpY#*Olyg@psc7m~-K+C7pkCLWz~vU0<$c;u#W9d& zS0S0P?D`IlVx+DOS7NWCd~>KqULQz3f=i51&=P3YL@Yjh?C;VHr=OdmI>ty3sKmAX zRGjrh^kRyQ)8vHVfT3g~O`<2q>)x1oA>fy*a?=Mg*_My3mR!|4rUSA|>R}UicF?-= z^;&lYlYLl?uxKY)M1!j9eR(1^c<74f$N`bbnL^9nW%<*t@cFp_We*u_*|Z^2L&eB%!}a9bWn+@%q3BNEAM6E?9sfJ?k)<5>_iq)z zL!KRbB^L^fF}nL4&^~)qJjZhSm>@;|)yV_P$pc-AJV8nWC`mG#Pu+#iW)*7u@y$|s zv&rE7eV#aD<#;u`3=mS@SX8E6A+7xhrm?8Jaq5uMzJO&XVB7v&4)gQ`Mw)$Sg-XO< z2;Nd9Oen1^W1bk1>9!o2Yu~eD-EDbm6D#8|3BJ}SjvrhlsL@vYX^f?-VYEPC$D$-Y z$JG&%T+d|y<$lTKo6MzjF?%Z{_4xMXED1~ZM;2Eq7N%Ms#%hl5VzD^kTLJHdT2_X+ zEAua#qB7(ukd+Y#ljv+%05&WzhsOu1g3azD{b(s03X(=ylfAVf%LPcL8L>Tn3rdGW zhUCv54gy+3d*%T(5Nb*=+qF?WDl4Qujt`&l^}gv~!d8-nsy`q}IV#_h7_dkgusmqMJNFFM}Z5dzC%r5dkp%<1zEr5rmKNuc=ynITWY3FY=sZcOl0fjz8Myuv8lbptT(0 zx$g_5nOM^5TiI(%NmHu;+@-XuA--^o#crkz-r4JWoBB4MD0nVztA0NYwrJiZDY(z- z{{cY)wM4WO>5f0^0;LnJ^LDu=tbI- zu@pU2zdh6(VsO`hac11OPMlPrn*(Tcl7DBe)I$1F1_RvwvNm0^C)^c#^KlS)K55H_ zl8q;j<$wo5O!f6x@t^iVEIv@z*~UKUL8-MunHhM_=43^IKf6Z+%Z%5ove#5N;gJZU zI8rPcOH#WPAVQ_6GF7j4jx&~{r-rEfKCPq=@IDJQZ zk%w7e64LUgs<3z1CUa$G#xlB(R)>oW5#MAvX05s#i}7@^)B?zc9{Oyuo}0wmE2#P+ z1nr)E3rO7OGQz`|d?ijJK^g1Ve@lhdLdT(Bpq6;TBZq#Z+|9gHG;rSI@YAAW#ucZ` zF+hQPfNl&E?0Ufpmtu;y&!7KzBaNqOoIuP=+u$%6A6_C<;R(lmkCg6^H_AL%Y1r72 zE%&{eTWV5us7z(1NKnt@d3>yFjGUyn2;noG=jGuBg;mNDfChrL0Lhxca%G$-AGxE} zm;S+oRhDWUjr6NETW_MTW1@CuWdffw4)mfMCY3e!Ub4iz6-^u`MF<}EIBeVp-qgio znc;LuT1l4h{`msNy!*Ip>sIT{`zY76tst#$gER3z96BDoAR7GJ5^pwW@s5>J9@V=6 zU(u{q-CS@fmU)Z$LkGrGaQAdY*AkLbk}5z#QwKV=loO9Yj%U|{!Kq|E#+<3aM0m_A zj`jekoh2;>{aEH4h(x?>NXhx!)NznI3gR6cZMt7}L=I*iE%af34tI^rq8a?Q zD_W-KxnjOkcDk2BuO&^rx_@C!in$b5MXm3Jzji~UM#N6z@eFy8m@>$Tkzi*>GKrzs z1a_j({@^%OYmX2Nepvs>_5gZOl&Cd@@8j$8QTY2Z39pS6p=?7*lMgfQ*s~c;M5*BN zQ+&zD91XURVv!*f3Yt)e6Xta>s${zu9pB2eu!b#~esU862U5Cekd-aSkwyMg+iCEZ z;nd5*A2I`)Lk|SL3QZ3Iy(!V#VsbE=e5s;@Kj3_lQ&{^wMI{<_srKtmb9TQ0-}Qlkb^BUvtlat#9|vG0I*68DfUjM=>qL`Y}0-79osKYyc)a@UIAau3PKfXlfT!)GgRS zU9t=&qxRU2peGK8>ldXTxrD1Qo#^57KXZwG#+{-Eof@SU*+&Okk3utk#BH6hN1Rjl z)u3tEasYwd)`_-f@8yi20PvxRC zt34dj4n=lJ%9~?Dinv=8;p>1iC6!qJ894{HhJnff;(Zg1&y12wP_YIEhN_$da6!ue zFgL6lFd;fgV!+|>!LG#!sZa`!K63Z;)F`PFpTPGao!*Cb>|}XQbhKJ~RCs)iZo4lo zna6-v;E!qTG!*8qv|^*kPqq!KX`vbRJWS?scGj$!XYW{)UkvwJ?YDBnU>2;-wR)4I zqV!i-OhCl#N#UjO(3+!ZufAG>XiU(9;^uvpYza-@^1}2tbnC8>|Ex_W!jT=#^4^=7 zC65WK$yH+?)Ft16q@By+UIfjqeb%Z*$B!GKT1V3L$qiFycPS?WwfRJb+ba!ZV(2|V zzH$TGucPbdxJF!eVuS^A?1~Nw1*p$b*wTghKzmdh|B)i0&wiw@>q#!bmnORcs!HhL zXV@s3rT#eKDDI@}2A%uu6QQ-+6>7{7L}muRUqpZp;yP@Cn!58GG0*Tlj8hY`%jql_ z*9$di{kut>-?RF5k0n@soK!!HT9HijtcSEgk9B|#z;JJ^w-yVQc( z%3Z5QI_Sr_>X+)-wujcn`-B*(J8wu@998%u+fIw{dDe2Y2~FK9-d<$FmF9V}7ZrG$ zdxY^C7zb8r>7lZ8`1>PzO~Ni>ZSx|=Lm|;-{qPa?N-1eOd#nU)+lF!8;P#R9rU{<> zOHweRf4Ti{6nNroC8qI<|4GB5TiDBV`L7zgCD} zb4{*Ta1o@%v@K!qa(BlT?sifUhVIMrL4m66)rTM^ zifm8Gt|FY;S|;vUrEbr+ZQZ3U8Skc0GGh#RDe%yr19Sf%%7~msy(Kkmetl-ZoY7+C z^|^|2X1e<5vJMmo?C9~B8}N{6W@4%>%?krIJ3UE=Ag+R}#CrXQDpKO3b+?C7Sf%wo zl!@Xoewo3eHu+I_ zvmtBHCkDx*d#*gDz`oFI7_sYx5(f8`D(EgR?M$?R{a(y4$Z%1BvbMI|R^ zW_G4j@IHMyfwZa^rEdNOGkJdR@)T8FARb|Jl8ix^vT~cWiXO)-ha0j$T9>h)Adh4g zP{^=CFptT9K_9d!Z2|Kb9$Pja5E6{sK1|@b#Gnl)M)MTHG@N8FQIt0!rY`O%c(!Ag zw&@o0P*p zQQmqCI5USXqn$H*vi)2s9G{{xK{Zf<8B^_odPp3vb$}JG_)*D}iRzY_;TUt`Y z=y`YvwkQcVKglz`BI1k2g+by?djx!i3gUQFnMN>rWHM|m*m$>4L3zNU>KAD?Q()La zL*3(;I#e_Cwzm)ZF%3}_9a53AZxc3d8QXvUs#N>8<@C4Z^ta{ox8?M=<@C4Z^ta{o zx8?M=<@C4Z^ta{ox8?M=<@En%IkAjjGJ%2sN*E+Q0fYdL4lL<2Ox~p$?vNis?)ok% zQ#^fPVbwZo96X$mY^*l9a<{~atq9M)#Gf71f(JD@@@eHnd`8Ve#y`77#m+OxAhkIw zM5vqjVeWpFqpS4(X+HvD=YT>R&Kn1GGJ1H=4 zK6-T4Q?rCNG!MF7X!cN*>|h)`ncHtBw*p>LPngXs;F(c>Nu`P8p^nZkA^jOusu88Z zDSL{6#b$t>ZIX+LJF%duNQU=J$!y8KNCzco;!qv({9(?EW_e`j*63K%lMchkKq4k6 zKoOq=J8=Yby`aZwG!Gojrp|WJa^b|6W{zUNVlH~f8J8c$u$MR2)%0?Xl3M5?P*gm4 zc5iib`Kgykq{*I`*HFs5#F$;-NVd4heV1JnQ$-EMLdoFfh3zslG1iv8dg}7_SG}1O z%XHcX#~f~oIMN)j8FWc*SQ6}*{`;vL_%pEV?QPwvsL z^7zIuMG6w#HUaqwLW5@;G)G;VJAUfbUbXTTt-URXT`x%Z5`VYl(dETQaIf`r&z|vp zBA&fQ-G{TmM&zv9>kGELHH#QMWkGInhJY5Q>~{eE1}GJU{vNxMPu56}Bp z*Du}%#bK`Zv7Yymj$e9Rk9cxriXoF3;l~_H7RW~03csA}&4_(vOK*~-I9HPxQ49k? zZux2!Xi)(9Wwr_FOl=OI@*=neA@`pCmruEd15`8o_RI(>X_qI7L{Gub{1v5OqS=Nl zPA`nm+}a^?iDWr+T9LFO56n?f7(FogB_UTI$E00)w9HPWOQ(D;Tu@nAmMb>< zS5Lo+0dz8ByikHkdB6u`hYbi?E#NhEO~`DcLHwD}t#TRXTD0Ohm6+cX>p!-g+2X!9 z4D{P6gb?pNG|LP(y2i1t{wj+J#+>@d=qwQ6sGE&4NlJO3WJ}m=vyHqbPg^0u71&9- zP_(8!F57lde)5%Ws*|}C7g0- z2(oh6u1)BJk7JT`dN-{1Avj~<5M46~nuT>x2fi8!j=*E~^hT!n^i0Eu-4iD4k_vtn zVT^c9#`y$G$VSYoCl6Iw)a_w{gzy9ZP6KQSMrGX15l7U8nCGtHekVL6doq=7N)`Ln ztvv93O$zZ z3K~LMa8L@~k}0a2h-Z62U@en1hMx&ls!$z_Q8Tn%z#~mpLo74b-JhfnsonA>dWb$C z9yNPbe~~{c&ZnbMCh_C@+>C$@M_O?nCYCO0Zp09IIW0!mQbbq>%ePxaS|)bwM_GxY z@Sz+E!Xpf0?wtF6nN~UIs8^_8*%J4T-St)q%1Hu5oG5}Ztg9D>5BFk)uSwuiz<~Ys zhF`dA*7(duV?&c$;9bi`R_FQUv1`1}hGAq#vH%qL#im9iV)9kFxQ->$Mg`ugBF>1BjzC=eH}Txyju>j4D>mr z7bQxq%;#B=X=f8aXwp=ENE;O5H#{yGSdt&+TCQR#mFRmSFn2G=Rdc>!)9A~L(;mz1 zEwyws0xZ#)i{kLE4p7k&;_IC9;29(y)ExyMi2CL_b7VTb5cAXCA1vN9VZ_n4G(APC z!>McZN(<^txsAu5{l{(S!AU7plQk9YTUAQ|)!NQ`quxy%IkvlfSNcy=_B_VP<(YkU zCf%aQktgmqs<}N6!kso-1mTTT8=rNCdH~D6)<(FiE|%Bvyi8TM%nQdb9~*Kw{M8b; z<(GtDZ<`CUH$QZOPJ3^Z%Ko$|gj@c=EgD&BZth_<@A9h+wG#guG`x%15R*XR`>#Sq z)*q3hBi&|QDf>HoZ40@k3SIh?x*kjj;I0A2N+zBSx3&d#+w=xWUp2uhT;7Y;9{_3i z1|lnCu4aOr4#Jq{XDd`J7;B~vEfj7O^#f*VGF!9+I7v3=A&8)PA8RWRzI*E3+`%50 zaV80T)pcgsR;elvXZL-xcRuK7OXG)Q%X*_3d@-@tm}-ducR7@{7FX8X=P zSZiuwhQQj9=HZ{(Hbn6DtCQ#@xt=h2+dM>N`jN`v&@4LAv>4>8{p#KJam}FAO19~2 z_>(Y;M5vlPYww~cPa*6@xLCNLBGwXOC+WzUg^Nyl(Z+0VFF4dAo)q(W!O4a`Te|&2 z+|RXNFIP{_`1XFHppxWfhi4_*FagQYWiwvQcLHy&%x0leEV`Uhysjq4^EL?IdJ<;b zL|O&pe>LHEi_TBOp6wjKBBBrdl;NMe3iRP%hAlf4^ew?ixXP%v3xb3iD$}m)xX>;l z6H@g7gn#JtB3=U_Y(}lzBzTl_{W(ltX3;MjJhIR7o#`2JNzhROYH7TOI7O7cLtxGG zIJ07s9&^y07whnY-wYU%-=WXvX3k>(L;2S<(~zO=Nfk^8oX;OVhy!%&gPqWL73Jf* z`w^}Pxg99rb1lpYZ!i3;lny@zMdEs~62*|P$v-5(c(1AKa+@-DubDy5Mv!g~<7)^b zP@Suk=i}}0;i#uIeSr0wTIFoCTX|B#lR|QCSK)<|GI;Ej9lI-j2p@!i?mfX=DcG1%Y?a) z5%0#gUJ@hsealB2Lq+`gFk$Ecz{sI=WHC1ZL!*IR`%MDT9NMejDm)ho_N#-L!RPNZ zwu^@Gnh;L4E=8RIrSq zv8L9shRv7GZ^I?A6_UGgu_9J1h!^H`rV?sZ27bN)h$xB12l9~k;c4Go(Ht9hN?qL(ywm7-r{mfV@O+mFVuD4QJYY8YuIjhmc7C{x4#tPNxL=*F{Z zHipmKo@HOj1iD6{$%GV+Syjkks1e8YQq)I^7>q1>jN_WR$e2qs(kIr6?EEB1Vk-fy zEJj$}Q9T8AeUbdbUu}LDi4N7;1fdm%Z?to?rwxp#Wj2Ut=Kw8Nou2TBcAz$1jD#pP zJ?Q=l!PI??!S{5buMFFSZjmOCd8vsf`GfU2x&mD)2qqm!F^=pTv!I|!gaM^_@E0zc zQjNFR6ny<^?w0vnmIVwZa6lLqgFgWD6X1aYD=LrI`#f#2MYVq07&lj3e1HKa#MDZW z4CKoUbX>)4&G<7*=bO`-YI^HihKjIKeHp$ATrD)lb2V3HqxnZ(grQC5X+Vha8ojbw zAv*b?F~VR0D8YK(O!V%0q^EOIJRU6ft3`XS3>(Wp&=?-8snlxUN%=a&`S%-$%E<)b za@AD2uDYjHP69a2u;kplFV|Sj-cH76!fR(iQ|rGL%3Yz93{R>chxrF1vWjS*gAz5LD>Y{GP2WTiA^1`Z6q_Q3 z44SGu939m$F){bLLdGKP+T8oI;1yd8T~W~ToCC`BI1!HRL8L{~`#DbqhG!xS_fYT)8ycWS}L&iQk4jT%~zG35=9^vKqAw zdXBVP$4Yj;nNVZQ^seEiPh7dE=JoGU3alMtzu7J9o_RAIe1l=TzT#J=AFQ4Eq;|6E z(`cUr8lxEd6AN6Q&bXEkroX5wuxrdf~t}N-v$d+VYeKrJ5je*PwKo8hgs)M zBx%iEL@Wp~^cPLM5}m=$A>~%6&SjIy5+SE(h(nkU`M!0H$}~_vjBkk)7Q8TC^g0{v zka~Xxm9_-Q&VXAPCo$CL-03aV6IZJU%F~KmHuofYLt_ZYx{$_9|DrVfDWMqOOB8nF za*kGHERI806^np^jC3N>8;0-r>;MVwXD7H!UfpEx8NJN4f8HsTlQ`s7v*v4iKQAHG zU~gH|gBx$hq35X~bSZ4010-IGKsShRJgfJ3PWRkS;k-tGeYyIyMne3N79eM<*~PN> z2ZZ{}B)A5~3tl4RE!U>R;aTo|(!$Z%5JOynA}YE@H{wS)J#y;^kq<(9e&Qrl18o^O zNP5Pp3YwgEydm5-wWhE{khHbBT~C7zh`qHKQS%b1{z{JEe55wPXWi;~655r@;h%8L zVff)5zjIBK(FHg?5ab?m$yl8&!e3P|4zip(ueMwvTYrUMoAA=-K5+yesjI2Qk>W^W z)Lpys+CTCXyA09HV7mLZjjmM|wIv%33R`EY9fx(xn!*@s8`@uSMsHf>f&b$VFrcqAikHEQ+E`N}~V7KR2#LU6@YH1N;PNSM=$_u8XU{R^?WHQ|Sem6Nf zToF`B(^ppsJ6!RrmZq+FNc%FF`9<^0^IqeXJb7?&5t;*w z7JA}4O#g&xlu8k6MCQ$6d5^l))HC)q9(OuH)|op$4sP3-yG*QO8)3CpWA`uGbiCBRao7=XhPf1HlaKosHaoA z@ab#;8#{v_&+N6G7H{#bTO8^T$zC6oYLX!kc<8x9iq3}!UaG!Ot>~oFkd+cs1*S1r zSM74Y83me#9I!}{Sc015^R(0KWY7E-!(bq;+Lumm%hkzLC+Hf^-PmnfR5D==(hP z{f5o=Dl2t4X2^-zH9z;DO>{D`%jce5a6Ry@Nk+h z=4`rjp(7v-e*Jo^!5I=Erj^<^!44roVq-#RK`i+K!>Hz!eoc7WL=$tU0`DlHL!#e6 zU5UV_1&Kr*q_u=vl0OQj$h^LpT3Q^sAei|?rZwtXm>1g)5>qK2B9jxeJQIYr%ZwJ;RMinprrHlJ>C35lqowXWgv^0xTL};RT@`L$o4&V0$x>=Mzl)7=Y(yZnarAr^=ZlKD?Tlp`Il{je3sK* zoYeg`XtU}ur97hyt)ZWTkz&5?^nud23CkAS4Wh(O6;{lbo+BO4Od2Hya3QKbiGNje zgIH)~R5UQ8W?*_D-VV&%up8YWEMu0%`fRl9$gOiR#aO8~ayg82Uro-aRP6cehZ{6( zdr~PVyLs1dZPaI`ds~X{w66)FNo;P6WQ$ zNN&MQcfDuKg$VWqE0nnvOb~r+$JX|C*k`*K8tpk_kf?gDkrXN-ZaSA_VEvZg>1%E} zjI?i8iLzIf+YMU}f?|lu@Ww|PbIZGS1n8|KkAV7%)8HQi~QbU^_vHXCK(f$CVFEz88d~$?wS{ zoq#sWrqmzyiFfpt8ui&khoc#M`%vToQI`35okaizLHMvu7o`~CY{ z9e2}V)#%%F7IS2i>I*fyB+Q(Q&56;- zP0WmkL#t_9+mTobvrx_w#Y+Ymcu693ARbx@-5eoD!fQpjfnxRKOmSktwVy|oBosX` zI+wO)v$87AaRj~*;Za-@!iib}UVPf^h$hP!iV4=&w+{BNqv%XazH!wjXkDNV&G8!s zRPBr%wb1xrB_p44UQ}iDPVf5N)O&_Npn!4jV5u&A)3(W6KUjT6XQPrVYZhKCOmj1r z-4N;{?UW{c1>I$r4Jsuc*b``t+=QsN{4#4*6RW(gi8M2J9 z+`nv6`w&2u6FeD(D2zvB+?l8oxDqmqG1t7Xn6Xh|0!KV_UzH(kn$zUzb}(V)P_!<2 zU)wJDb8OkHpR#>8AJw-QG&_y8Uk>lnJ++%xKTg3F7{H}|$^g5vc0#cl_>3X=#=&{_ zS!FZ1S%hoEPBQ!l|GPXvGFD}LQOwGQJr-t%wa!i$W?bt#atYa)N}vY+&at{37ZnS( zbJDIBXO^6ET7LhUs~II4E9$lvqLX#OrZ-vj0^Xy%4n%RDhlFGYSr1DsG7}^~4C)_0 zY%!qGVDp!YU;tYCYeY0jNDy35*)Uw6lKO6{3DX5{; z+B-*cC|8aY&TR5Y8$k`s5cRAW#;{Ar7jDFO03o#upJWmXam;-qfBi)r!`d~X){&PuM6q2@4!B(IFd04Yk>^cGCFw^_bWFfiYspfRO^XPAQG z%n7gor73)zU7|(kL9Yi&eg`@;6>Qn+moaX7iSR43D+S`0oQ>QQtT~*xHiep1ebW{4 zzIfHRQ9e&Vyz&kB&v|g*XKUWY8m-SXvb9>#Vh_I2z1qE~Aqf@Yq3JGbp`bN1;Li-P zSBCGutS)1TdEeD1ZVy^Tq@iCMseN$BBj!90dsOd~>zTqrDLfDa2&H=1Hk#bSxWpx= z^!?veL{?E4_c4CleO#cpL0WJhO0LcR0=5C|N7=$26YDQPlq}pO{H_+eE&-E~9_QLu z&KLBv0TRW~x+=bpm--5(PM0toKNNdD@>64EP&uZyR3|)?iYgjseOsZHGQsRy=tQrz zujRh&Wf_&s+OiL8P=PWJf3R{{zmIBNO=VSo0*}m{F*FFO-eqR68sdRwYLs!h^#_vz z{x^8_+~KL+kJp&2JW~5}nFP`1pEy$buLHq^HpT}#=|9M=70=2lkja%*oY)QHcCl4d zJo@I>;Ij?t&Nv6Sk}@{J?`wU3)u3dER&F9%vX{##yi*$}D@H@psR-6*eiQ49Mj?QZ zGBZ>z3P8D4VTHqT=6f6x12L?k|4wtMX$iNw;1KktrhVWlO1;FmB5iw?c;h7CKxX|D zo}~yt?C27-RF~{PQy{j zRr?aqP3q+oTQ(AniYe`@&!P}(`?t}Yl4C8uda$DRddaJ9WDq3e2L>UKA@(D?MT5BL zSDOIIy15|WV$k9t_D_vFw|+y(6;okEQGqz*FK?(_`RbYvdGJ_y#tZK>jCYRQAM(R1 zi)k1|A*Me+uJ-5!oYS2MNUUC|1@6Y6B9 z!Rn8xdVyZeXXZru(rY9zJs445GBBVU4!?siQiWfP?BmgAbfc;zWoK4%A&_|AO{@O0 zPCoEH*5GcrEPi~DY;osaV6z)H==R~d=}2qeNeJX7+VlQggEhyzf=;{2N!sQxP@ezx z%v!@dxecWRGKQJ|8MQ64W-Wtf(=u|x%75wF)YkRtn*Q-?7!-fb2kYv@YWJ1|BM~Ha z%%Zv~gA$`U(Ak+IC+ei7jI` z*xM81!m6cVc#X3nAbuMv+=pK=iK)eSblcxD3o2vU-}!HqkTX5AhVyZlkvj)>VFX2g zp?b^P#O?`?;*(`qm|zBrSo?TR-RTeBu-H791jd1M24p0puKBlY4jdTC6@M?yxET;G zyk9Yd4Da3%*5bzcdM!#4d&Kq$+@oOTW6N+2>j+wYhG?A)M?Hv4hAl~HSG)1i%j`g= zcYA$iTxNXr9YW2_%rmG}9iVHwvqe}YW)rT?&T$cMb-p1L*_h+LplM?{g4H8n;iBb* zea528P4%KXtlHc-E<`8+MI3Q<6P2^|79)>HTD>=Ci&v8@m#7 zdN+})>_!>;6?%1!6w6gOt52y?#4CvwG?JPLviBww5wT_;00z!r8z0>De7T-0&VqER<8 z-->tUr9W+DWK=k$1{m-X2!t9;7t=D{f?zAiOG$$+NS3U{mGc4``hxJkHeNQ`cs;$c zgHwOClIl-ILP;y-CEy^?rFyulUl zwM%);zCgZf!yV2-a>I}^u~vq~znn|!8SP?-dVuCy-gr=f4}H3aFrh}C%$|Z=n7k}= zej|1=f5#|0R^*GV(vJZ%D5UO6TsAs3cRRvLI*33Fs;pT`YxD$%l$ZX{b5vghXacO{ z-gf~90Nb#yjZ&4-POJ;=Vx(T;G$LzukLuEj9!m|040_`r2n6oN{^Q^Q&T+- zyL5A~oNWeuC17?C$AZWN{jYy3FRdE?eGJ3j$1wbT48z~YF#LTC!{5g+{Cy0=-^Vcg z|M(aNmaBig5(hlh41&m!M{J~ODVpA&d_sI0}Ieeu#YvqCm0R%qrTXDHg#`@QowS1b$?# zHzP#~2{8~aMXD6@cYKo`_+|I*xmN0{RqYeK?cEjZO5j`M$5Dpu(c0y=p}6nx@A#-l zM|U+g2tBk2eHb;WS{@uhaNP_!JtXxIP<<1e9v&V(@5{t=vq13Mg*)kJ)I4I0-8(Tk zIYDt_$@^Bs-q$E6DsU`pe{}ua+mR`x`neCESn&E?2%gwaHFLko^*!#a*lYEB6(8IR z9F~VqxJl-K%rIN5jP@x1jn1$wEN{VhpUr)5lBX}ZXrPtG=g!7JK*%dDd-G>(r`o7( zS+;}ehEP1{sp#K+M$s>7;hk21?KE3EjkVw zG)p+WICs%c(u2318RQ~sWG>ws4ZlunXZHw7V~jF=``b5Gnq7;1ZFOHhmKWm@G6etN zN5Lj5N#NcR7U4tWu&JhrzCB5+nSSlV^WXb81~vA?vXI4Jktq0Z8`limvg@yL2u9ot zdqL%|vFjg43wmHjvpzk!y}XJ;W9<6vh;cM}<#pU=%9OW=JdH!u4c_U&IPRnE6tvKH zY8^{6GLPfSA$&Q&XZSbHS!Erape1^^*xKW0c9A9epsCi1h}KzEdL1;yYlTWpU84^V^>t$e{g9=G#0#q>MCxLYiGJpckM@Z+^Wi#1Usc$A@{qm^nE$ps9Y zcdXI2WTMd+}Zphe?3DTwG1+L zDM!Lk=Ifbq_)azER4|TB&5&8WC81y>W16mMWniS= zxda`KYRaN@#%Isx%#p66d;=|$^sDA%;s{ayFq2X+?aq2%mE>j_6e$j-#UGID!HedA z!F&s`XSuVb5^-GP&E`hG8cYN>K3V_Blsv%TIs;vG9Qpb}1g~qeg~4DS8fRD7*WD4` z+7*Yr9C@%7+1eF-2ol}emFSsclF!gLUYV%(^eG}>e@%)K5rc+QOO*)<7Wjeh%~!eY z1MyEbn|Z2b2}2V@7Ny7%21jO%bG3@L2&QXTyEgUm*=nht5s?(K$XmLBN^TGenpAxs z^|%G2KD{7@-7-teSjNP&Jjoq$79O)+h~2=euqaL#2ij6Y;8U??jePOQ_hr(KJpfyu zd0wAI`~-}D0|>@%uT1(P-m)ccsu^F8(&BLBas@^p;Vb9Qz?xwny6!^k`%OFRC37FQ zEn@1dTA^h{%Y&{bPzxo)K*v9S($iZ%!8s&dKN?h{5}k)olYJ|Rtm=mu&@MN6R8!O1 zAbHeuq0yV*^Zo9KfU$zEsN^DfhyfXpX!txF1Jj8F7wRpM)OqgKr7 zeQT1$r=)EtQA4_eFj=|g?vLgd3s<1P13EkXufRyI=#EL)t$$K=-uIre|1g8DF&E7WK=otGmcgtJk;Hu?^2R&g}!&Pp^R=Al!lKCPK&CLdR|g80KtNrA*8G?%+W{}lA#LxC?ThmnVjP8xazcy2$}ihn_+TAJm> zeMfZJ;>cs=ml(n6g-1!RA!&8rJS>&#Z$Z*NBFi_9WhsAt>wt%4Rs#`pbAkdPkAxnq zsGW}sG&gpq#8fi-?aluQOlBqnkRXQO*Uv+tUi0)Fe|$MV2l`NG`?m)N8cXeK8-^SZ z?E8S36X9`>IJ{3nx`U!ppIUrS+zX`?rLL#KC_^t9ke&v5Y&tt_c?P1Sd*Eu@jtBZG z$-lvktV@%QWXw9}7&sk#J0L#r2^R?6W(yHzC zazYlLSCSA4=O8`J+fp*}X({xHHs9d7hXw|^A&CRvU=UJ&MyLLqz~r%pCf)Iak*%AG zIqwo$d=mmb7#81n&bxT=Kq>Ca3kG=dQ%P%3!vJw|9_0WxIdOQMa|5Sg;~JUnR>qCjM7M$*oqTC1rqVK@vI9xoF6EUO zsj>s*Z{NmQ2lonFr*bbC{LJ6~gRlSi4;bdV9_QQiR(lvtb8ynfFYHXVQErmnsB4s+ zw^!YeD%Op+UUK`M9vznMRk{=o^yQed$aTYCNrz;eQ|R39>jN|@fxt)OhY!`Q|1+*X zF-ToFKg4*E8*bSe{{{9{dT_l}Y<6Grv-bCbuzZpE@5&G&Q;o2JHb^@IQ=!fd{5Dtb z|K?vA@46Z9m!)Txd!7&4C68mlf?M#QL4rGj>p+0u?gV#tC%6W;-~@LG5D38`*x>Fk zxCR?+cJh1g?QZR!`>8(P>Z)6H|F~86-0pL_hlAieX$48XX8SR<>`Sb>9Uq7ll6bv2 z=_SlGFm;p9RX#E*$r*OM43NX%IhIz%l~d)b35a$X22zuMX2GcEr^a{{JAXTZsAC=z zc%fLOQoe~tqA{wJzw@jysu;fTOh1RkZ9_8I>oS`4$et6E_avld#Vl6ysW3f4YSTXY z_GdGW>>m$mqDlt0+GJ5al_jAuS7#}?C06;0wOn&Gy#4Y3f3$`iELAJBis;1x@0J&j`>)Y{k{&tu4L6T*R=N$s(g_D@p1ZUzS-R}QZtPn^-lu!- zn_yAz63I=4kp;CA=7u~P(d*w-DpEX}r^EbItKrErTwBSGY0w-2^hG}bbcHK;=i~Ws z#^DlGL5cKl$aHtevIUj`*@vBDyzf$E!II5&b2)CdBU%ERJ&JnhWl-d7WYm{$iy&mw$_yVG|wM4eNRtgfzw)=KVqdg3J^e!fp?N0dU>!1YHu z?l15@;ofe^=7Px9Gu3y|T-e@MYk8M`!=iCw##gMH#~wYr>aPumdQ|H3g$ZAy{pqnM zd@V|A{Az3`&I4@h_|Dh?s137!MeDZ3?Ka{czc zOsmqPQG8D5q#2Kzoqwqhm*QQpR?2KU70yapU_2GxlcLk`5RfQJMrC|LTe1vV%>*eu zY-_7C3z5;xCNqo_et!(^63I8nCJXxh_;1?qRluTI$4vNRW?zH{nK1p?Iah+K)n%R0 zPDW+?3`CX|*5lNt-h=^6R0s;S;4vbL9SEECU1|ouldd{AjIG=cYn$A8dAV&K3s81= zGB|mbnR#f3c$KLX{A`ptgAyY3&Ws`(Z9_Fv_I-# z8sj<3k2WvxOXqgQt%7NXwO7aXma`%=Hg3DD^JuhWlJC}_c6EMzUSdTi8O#hObd)?@ zz*hK81c@DQqL4uTzgQE$B3Ltzr>zE@I*P2xI@jm8vATKoE6jGJ%J45WkA4WuZS&HX zN0i`8QWVgEezim_3f)Oy6TPt)?S%bq2yE{hh=v`He&p4MEkdDtJbLNltiltaDQLzAUCeUuShvkJEu@%7N(EMW+j3y}9`3fz^;jXUmGq z@_VvIC(g@-5F!W*dcFx0aM+-i^DVeYi3R2!hmfIsRR6dApKP9iFI+tw+`Ou7iuJ>h zyo7UP2BUGoLyDQBV{OA2fkyg=T4Tg;^QIz|cng!Jl{@>xZES=cuSpLcgu&ke<2vHX z+eXWO$NnRRHN4Q`io7@QKds9u%XHwm=lEMU7=}{UX0r+&7z31e@CvUdddFD?+OKEo zDHjd8hMDTj@7@VC3z}4dqIX?UO5Aa0We+LL{c*uZaVeIBvl+Ung0Gf(-94q-{C%}^ z4-`+zoqvUX%uk^G;l~|YBtk}XnA075RWv@IQ0e}C05zW`A%A0f$n|%~NcesL5P4Cn z@>_>D2Ak$S;plYB zZ0us9S7l}{`}|GsMmU==>R)%Fc{7QAfgP&yp@iL&a`-0EyCVKj^otMOyu9RCPEYou zxRG4TWbDJ6c3+hAwxEQzoe05cE<~xbvqM=83)$MUza!N`Cg1F@rxP(!P>YzEm>tv- z`i!%m2rF6Zy{hEBs>o6_77yXD-&jpRg5|jNbw>NmZNKGrXEF~0oDoTh5hfN9z663k zL{Gk2x*dluJo}v>%1)d-=i6^Q=Z+VOYZn8{k0XUy@#<}b}yaUrnjs%D> z5xdVj`wS9T%&*i+5?IWbjo&4(m<-&~Dy$3|E*ln#X~YYl`x5##>~;8)P0(EoUYZo| zN9dpaS@}e-Sh4mfqi6&;GyC3rARUcef{H_>ijr7#wBeLGF)4;OCB>2=vS5Hqk*Pcufg&SJ=p^yy9b_LU6@ zGV#7)(uwX_$6%*Z$%(Ch7^DBopHk6FoP=1tOKb;YcL%$l=B0DmzYP|5C>g<`DJMov zU`oB61X)kHZ+1VU7-Mj(|AMQV3$_-n&*I)Pg^bXFmqs90Ouqly>o7NK|TI1&?v@H92h9 z2{k$KHDN=*+qH(V`g!(wPKqnO3NQ8y>yGT37Td`*4TiLFH2Y+hxL{CN!Vf!1;U1X} znle<7J&MxA+2;zfe)o7=2;>RzJwqUW1XDAHte+vz^Zj_#rVW#-LjwQq9 zh-p@|X8Pt74uCBkuyJT|*5B!K=+%X$p9*={F)zi53 ztclfLJ+N``yF1uTUwt^!mtgoo`FmPKZZE#Xb_|yRQ}hGAdstk2uL}h#q^~MtracPN zcv#8x!5u=Pn9ZMLwSE5um4hLg+hFfJt$?`ptEM_4EsODjk6vo07#IZxse`a`n882TqWslb_ZHN4Q({`H|#`Acy- z)?!NAxtFi4PY}d`icI68vJLqcTA%8Ilk`Ex=C0NkEZ`CgT?sdWhM|h_OYID@1!BLY za+at$Bn484C4>>R5hvaqCxMy-vwzurecz8okL{bOA3%<_+es*Iq9ylJa#u2JcQwR40PcrRwjknY z2-?ULaYHHdOqFu9zJ9oEn!9Bf^W55&)H$y88za0%wZEtjvbLy6t zn=Ix%ig9S4lS7bQiaTkMOQ!;q+-#W!Q#*}Z>~*`=qQ|!om>h}v=CIOCE!iMjq{kKy zfOo~F2|Q2n*TzbF^&LBmKJ1N^?$(sRvw*sYG&P|W?@xmnYn_x^zcFJ_qY^1%9-4^+W<@+fBJ zwh_&YcT72y607H0Ci>TIzyDc^M+Lx258wPsrqgB)k0To(cNsWg?CE?zKrYbJN&G90 zvb$A+;nYxeP*`KQkx<2;pnE!QgEZvC6H5Wzyd_B1auw57>P6~B=j{o3UP~0e7~X+? z&VE{-HfC9yWT)D=WjaCHP$1com2N($wCoYEoQOaz7U2wv$Br;N~G z#@MK#8=eunQ4yvmhr7}wVZK$ue$J8$ARo!^7%6p+y!$(xUTi{W2=Cz2^ZnJX$}#Gb@g zT|%^EB_!?c!DUDxefTV%({`RsCcA-9Q#nF98^X>8?N9XVrKT#qaZ-9ah1FcFXyL#8RD4#GID<&ywW zsg-wQv7?^t0BvF31Ax6ly5=Rcn(i5kW7<;KDu}D@l^7YMrqK{Q_zxV%0qA&L?mcQtQvrh&%Axz!`wk$*oE zrNYf-ea$|%$5@DMoEZk?z8j^~6ZDqRA?#X|QohxrXdnr9tr6^Bj z^-YOq_KzB}zbh(LqxN`~gCZi=6@={yBEanfvoN@m&)HM>eTek%=?2I!ba|zS+mH38 zOmnGSGY2|MYdU^z$j$35d2otg&h|8=7O6$uK4GrOUZteu?h>8KsJbOz2$(4zq%BqCI6?OS z*5k|QGxbW|Sk-6;&6DE#&-a=G=<9Q+{N=69yFlqc;I3ns-$!Y8;4VxC(nmECE7HcA zhLkiWccRBtdx@7m=52+7+nWha{XzjzYqZ&ZxMU@+v7#<>Tde)sRTGe?i2P1O1)1vD z%ieoS{!f2m-AC3B$Ia7XHPT}J7yeop)-Ccnt!lGLuZOdfdZE7<-iXMX|fvL+i=C&Y)6@UAN3&3MGEL{Z;0=z09hk^j`lTT0> zAeJKc_70&Y>(4ntXk}=!5nB^}$c&E>q99tBBlYHPnStlv zSK{6^8*N5e!1u0NxB0T^Z?(1=v9=k}{g)V&W5|ucS+ZXd>DcG=*VOAGNRQ%}e9z55 zUF!p*Unrgv-yR-_F@I7iKf~F=!p9oqMUN{#BUELMZ$)Nuw$&lB5a91LZLX}6GQ*vc zB&W`th3G8AdoNQ%l6TawN1QXz{vR>~OQlAa$8fGv#X7KP7-d(DbNnDw}K_=hp)?$im5lE$+adKtg{U%`h98Gi92r7m}KO^d< zKETQ^!=wY6VcmO9f!SJ0L9A?CHkW=%mMrF$_?bmXETr9G6nSWZ`Kk zf8`-Yw0a**nD?+XA)^-*6!0u?z6|XiQF%sb^>a*eKuV-@1|o$O^VJjT++QRo1%ZyNL~>*zFO<#yQyUr z$K4)SOupP~F&24>R|^#$zNZ~a{5v^$d9-BqCG3Ph^Ad0rzNWK^|2x^DzPK=d3xuP- zUKMMx?dpx5ipFDd?(zCk;y?IN`i$d=i?Tev2rjCUt}JFyF{=lX{I03{G$5z^>g0k# zhx)14v6}CJpD8@BY}cE9^9wUB+w3Mnsoz~ne8F-y8WlRb$Egb?Fjtf7l6g{2VVMhXO27UnmpaS2Ap z)X$2X8Ot2)7N^j-qL~*fE(F2kH1Vsey43DR=qxM`-;z7@2pu0Eh2`XR#|>6nX(K(E zyf7}ajis!AY^~J-I(~d$@i*J&%cGJ>O8D?AX)M7RQ6?i@KDpXy<85#vnmFeMv+~(w zGp-h)a(_ll7Bth#UjVFO0#Xyl#GXBcKl9|m{Lj8vc(|pEO<3nLIZIay3wbvuB@0+X zL@HSudkZQRS0{6iPZs7>UN-L5RR8oiQe+YA$ma+4#| zU- zIilQd(&;GwOodAAUirY7NgEph%S!6CdFeC@iApNBm~8P8d+UQb0$(M z>OCLTGD?@ER-_x;or5N*^DY418lY1)F{`1O2GOJ$sq? zX;Fc9S$%=6vC7(zeazgxur=|w!tR$nsb4=1DEqbGV|0TA9z|(wel=Mh?i~rBfOWZh za+=s3RS+`(keqpVs!i3qh2{0KAPB51nj~6jZZGA$6azo*Vj^naS$2(OC(y{)yl?lJHHJ~vgP(tY(n;rj}@*e7!8TE`BrQ?qO| zC8aEXDF+v^yFF8UZ?3U>xg0Vq$=MxS5o;}^MAaEMa;KM3lPDf6`29|Q%O&guV$|iR@2J;zoEi?*_wQ9aRosy8 zX_$zuu)zWCty2XgN@VyXGe<1+LtaBPKgvc)obB&D0oLG>Y?M5ILycSx6+-@al>y!? zpO%x{9*})dz5k3MC$%s!pfHBw74#||ewgX4Cu#IvGJ7R6-S;9%DJ2X@n6e}% zsW~MIDTwj&T{^+`Je67eB$78%bhJFO@=typZx+^%TEi+0Z2rtD3!y8qaS>DgY<~y10R56+&@on`2 z)$F-~9TJI|eiFE|0yh1#pVl-#OdnbPTO)vyE4Pb29&XB3l-shujE{-52Tx+DPyhTWE1@2bj8uV3j&=$xfB{PBV!BtBb|?Sh z(H|(S+rpZ*VYfZ&ty|@EhzAmKN=BGWc)qF8eQ!*SDuax%WXUAu*sweZvR7|0fQ3%N z*ugYcw_>xB8wf)Lb`2N3ARcg-ce~6lhL0_5Km6ID6j}e?oM8G@AoWZFukuQHDmcLJ zsOZ1ITfY^z#px*zxuznS`;rpI2-GExS4Uj0b80wyAN*@VjN;apTq}@BW~pK^S79Fl zpdSx#$ARy9q|ZR67p?gcBP?T5xUJGt>{8q;vXg@I@{J#4+ZTA6U~|woy#9S+s#OA+ zqEqE{lUAr%R~`BI$puo~t)_Cd=EtpV^-4U?do26gmFTrM0gM+iozHn3Mh3^;JZ*gU zN8h{Yfls3+ne(}e^Q@&C!z3^*FhRA z#qah2sGm8!63+T}mk-~zfnAb-tENmr?$ep*WI?dDu*K2RW3H)6=qZbY6Hq^^bmhNF zL^I9>@_PxcD`}C^arl*TA7zrZW0pLXpPbb+oGFu~MPNE==+wWo%~c--%I3*yn966X zK~PJc*Gbk3r!9W+!wZ$CclnBJ54ixv=57q1OdMs`C-g>+e_w%+dUxiNK#!h5NcSRuw5fQ~dZ4Dzpl6Q~yc$ znhGWrMh?RoDqyXtJE!Un1?^~&&aXCsAJsK?5=@>MQwOXLu}UwgzGIohrng+fx_fG)l(j{WdfiL3U?>-j;c zM_q9%3(_;A8=K^bmF#csC->~)X7>S2%p=#1p&kYD91oMKR`V!N2Z?M=sT<~vi}>V{I6rmAV(Uaq8@Dta!6E0XXmbqVKZ`mNiQXkdS=!vsLzgswxf+iJvj+ zF0l>m$V-J~z@r+rR<$0oBi8Y|KB&T~CjT7Bc;MH?0Xva>=9L3&z*|>+J+_G5 literal 0 HcmV?d00001 diff --git a/Tests/images/avif/star.png b/Tests/images/avif/star.png new file mode 100644 index 0000000000000000000000000000000000000000..468dcde005da39fc6807cfda46036dc78a1efe1c GIT binary patch literal 3844 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 CD~X2y literal 0 HcmV?d00001 diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py new file mode 100644 index 00000000000..392a4bbd5b0 --- /dev/null +++ b/Tests/test_file_avif.py @@ -0,0 +1,778 @@ +from __future__ import annotations + +import gc +import os +import re +import warnings +from collections.abc import Generator, Sequence +from contextlib import contextmanager +from io import BytesIO +from pathlib import Path +from typing import Any + +import pytest + +from PIL import ( + AvifImagePlugin, + Image, + ImageDraw, + ImageFile, + UnidentifiedImageError, + features, +) + +from .helper import ( + PillowLeakTestCase, + assert_image, + assert_image_similar, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) + +try: + from PIL import _avif + + HAVE_AVIF = True +except ImportError: + HAVE_AVIF = False + + +TEST_AVIF_FILE = "Tests/images/avif/hopper.avif" + + +def assert_xmp_orientation(xmp: bytes, expected: int) -> None: + assert int(xmp.split(b'tiff:Orientation="')[1].split(b'"')[0]) == expected + + +def roundtrip(im: ImageFile.ImageFile, **options: Any) -> ImageFile.ImageFile: + out = BytesIO() + im.save(out, "AVIF", **options) + return Image.open(out) + + +def skip_unless_avif_decoder(codec_name: str) -> pytest.MarkDecorator: + reason = f"{codec_name} decode not available" + return pytest.mark.skipif( + not HAVE_AVIF or not _avif.decoder_codec_available(codec_name), reason=reason + ) + + +def skip_unless_avif_encoder(codec_name: str) -> pytest.MarkDecorator: + reason = f"{codec_name} encode not available" + return pytest.mark.skipif( + not HAVE_AVIF or not _avif.encoder_codec_available(codec_name), reason=reason + ) + + +def is_docker_qemu() -> bool: + try: + init_proc_exe = os.readlink("/proc/1/exe") + except (FileNotFoundError, PermissionError): + return False + return "qemu" in init_proc_exe + + +class TestUnsupportedAvif: + def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) + + with pytest.warns(UserWarning): + with pytest.raises(UnidentifiedImageError): + with Image.open(TEST_AVIF_FILE): + pass + + def test_unsupported_open(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) + + with pytest.raises(SyntaxError): + AvifImagePlugin.AvifImageFile(TEST_AVIF_FILE) + + +@skip_unless_feature("avif") +class TestFileAvif: + def test_version(self) -> None: + version = features.version_module("avif") + assert version is not None + assert re.search(r"^\d+\.\d+\.\d+$", version) + + def test_codec_version(self) -> None: + assert AvifImagePlugin.get_codec_version("unknown") is None + + for codec_name in ("aom", "dav1d", "rav1e", "svt"): + codec_version = AvifImagePlugin.get_codec_version(codec_name) + if _avif.decoder_codec_available( + codec_name + ) or _avif.encoder_codec_available(codec_name): + assert codec_version is not None + assert re.search(r"^v?\d+\.\d+\.\d+(-([a-z\d])+)*$", codec_version) + else: + assert codec_version is None + + def test_read(self) -> None: + """ + Can we read an AVIF file without error? + Does it have the bits we expect? + """ + + with Image.open(TEST_AVIF_FILE) as image: + assert image.mode == "RGB" + assert image.size == (128, 128) + assert image.format == "AVIF" + assert image.get_format_mimetype() == "image/avif" + image.getdata() + + # generated with: + # avifdec hopper.avif hopper_avif_write.png + assert_image_similar_tofile( + image, "Tests/images/avif/hopper_avif_write.png", 11.5 + ) + + def test_write_rgb(self, tmp_path: Path) -> None: + """ + Can we write a RGB mode file to avif without error? + Does it have the bits we expect? + """ + + temp_file = tmp_path / "temp.avif" + + im = hopper() + im.save(temp_file) + with Image.open(temp_file) as reloaded: + assert reloaded.mode == "RGB" + assert reloaded.size == (128, 128) + assert reloaded.format == "AVIF" + reloaded.getdata() + + # avifdec hopper.avif avif/hopper_avif_write.png + assert_image_similar_tofile( + reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02 + ) + + # This test asserts that the images are similar. If the average pixel + # difference between the two images is less than the epsilon value, + # then we're going to accept that it's a reasonable lossy version of + # the image. + assert_image_similar(reloaded, im, 8.62) + + def test_AvifEncoder_with_invalid_args(self) -> None: + """ + Calling encoder functions with no arguments should result in an error. + """ + with pytest.raises(TypeError): + _avif.AvifEncoder() + + def test_AvifDecoder_with_invalid_args(self) -> None: + """ + Calling decoder functions with no arguments should result in an error. + """ + with pytest.raises(TypeError): + _avif.AvifDecoder() + + def test_invalid_dimensions(self, tmp_path: Path) -> None: + test_file = tmp_path / "temp.avif" + im = Image.new("RGB", (0, 0)) + with pytest.raises(ValueError): + im.save(test_file) + + def test_encoder_finish_none_error( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """Save should raise an OSError if AvifEncoder.finish returns None""" + + class _mock_avif: + class AvifEncoder: + def __init__(self, *args: Any) -> None: + pass + + def add(self, *args: Any) -> None: + pass + + def finish(self) -> None: + return None + + monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif) + + im = Image.new("RGB", (150, 150)) + test_file = tmp_path / "temp.avif" + with pytest.raises(OSError): + im.save(test_file) + + def test_no_resource_warning(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + with warnings.catch_warnings(): + warnings.simplefilter("error") + + im.save(tmp_path / "temp.avif") + + @pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"]) + def test_accept_ftyp_brands(self, major_brand: bytes) -> None: + data = b"\x00\x00\x00\x1cftyp%s\x00\x00\x00\x00" % major_brand + assert AvifImagePlugin._accept(data) is True + + def test_file_pointer_could_be_reused(self) -> None: + with open(TEST_AVIF_FILE, "rb") as blob: + with Image.open(blob) as im: + im.load() + with Image.open(blob) as im: + im.load() + + def test_background_from_gif(self, tmp_path: Path) -> None: + with Image.open("Tests/images/chi.gif") as im: + original_value = im.convert("RGB").getpixel((1, 1)) + + # Save as AVIF + out_avif = tmp_path / "temp.avif" + im.save(out_avif, save_all=True) + + # Save as GIF + out_gif = tmp_path / "temp.gif" + with Image.open(out_avif) as im: + im.save(out_gif) + + with Image.open(out_gif) as reread: + reread_value = reread.convert("RGB").getpixel((1, 1)) + difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)]) + assert difference <= 3 + + def test_save_single_frame(self, tmp_path: Path) -> None: + temp_file = tmp_path / "temp.avif" + with Image.open("Tests/images/chi.gif") as im: + im.save(temp_file) + with Image.open(temp_file) as im: + assert im.n_frames == 1 + + def test_invalid_file(self) -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + AvifImagePlugin.AvifImageFile(invalid_file) + + def test_load_transparent_rgb(self) -> None: + test_file = "Tests/images/avif/transparency.avif" + with Image.open(test_file) as im: + assert_image(im, "RGBA", (64, 64)) + + # image has 876 transparent pixels + assert im.getchannel("A").getcolors()[0] == (876, 0) + + def test_save_transparent(self, tmp_path: Path) -> None: + im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) + assert im.getcolors() == [(100, (0, 0, 0, 0))] + + test_file = tmp_path / "temp.avif" + im.save(test_file) + + # check if saved image contains the same transparency + with Image.open(test_file) as im: + assert_image(im, "RGBA", (10, 10)) + assert im.getcolors() == [(100, (0, 0, 0, 0))] + + def test_save_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile_none.avif") as im: + assert "icc_profile" not in im.info + + with Image.open("Tests/images/avif/icc_profile.avif") as with_icc: + expected_icc = with_icc.info["icc_profile"] + assert expected_icc is not None + + im = roundtrip(im, icc_profile=expected_icc) + assert im.info["icc_profile"] == expected_icc + + def test_discard_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile.avif") as im: + im = roundtrip(im, icc_profile=None) + assert "icc_profile" not in im.info + + def test_roundtrip_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile.avif") as im: + expected_icc = im.info["icc_profile"] + + im = roundtrip(im) + assert im.info["icc_profile"] == expected_icc + + def test_roundtrip_no_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile_none.avif") as im: + assert "icc_profile" not in im.info + + im = roundtrip(im) + assert "icc_profile" not in im.info + + def test_exif(self) -> None: + # With an EXIF chunk + with Image.open("Tests/images/avif/exif.avif") as im: + exif = im.getexif() + assert exif[274] == 1 + + with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: + exif = im.getexif() + assert exif[274] == 3 + + @pytest.mark.parametrize("use_bytes", [True, False]) + @pytest.mark.parametrize("orientation", [1, 2]) + def test_exif_save( + self, + tmp_path: Path, + use_bytes: bool, + orientation: int, + ) -> None: + exif = Image.Exif() + exif[274] = orientation + exif_data = exif.tobytes() + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, exif=exif_data if use_bytes else exif) + + with Image.open(test_file) as reloaded: + if orientation == 1: + assert "exif" not in reloaded.info + else: + assert reloaded.info["exif"] == exif_data + + def test_exif_without_orientation(self, tmp_path: Path) -> None: + exif = Image.Exif() + exif[272] = b"test" + exif_data = exif.tobytes() + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, exif=exif) + + with Image.open(test_file) as reloaded: + assert reloaded.info["exif"] == exif_data + + def test_exif_invalid(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(SyntaxError): + im.save(test_file, exif=b"invalid") + + @pytest.mark.parametrize( + "rot, mir, exif_orientation", + [ + (0, 0, 4), + (0, 1, 2), + (1, 0, 5), + (1, 1, 7), + (2, 0, 2), + (2, 1, 4), + (3, 0, 7), + (3, 1, 5), + ], + ) + def test_rot_mir_exif( + self, rot: int, mir: int, exif_orientation: int, tmp_path: Path + ) -> None: + with Image.open(f"Tests/images/avif/rot{rot}mir{mir}.avif") as im: + exif = im.getexif() + assert exif[274] == exif_orientation + + test_file = tmp_path / "temp.avif" + im.save(test_file, exif=exif) + with Image.open(test_file) as reloaded: + assert reloaded.getexif()[274] == exif_orientation + + def test_xmp(self) -> None: + with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: + xmp = im.info["xmp"] + assert_xmp_orientation(xmp, 3) + + def test_xmp_save(self, tmp_path: Path) -> None: + xmp_arg = "\n".join( + [ + '', + '', + ' ', + ' ', + " ", + "", + '', + ] + ) + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, xmp=xmp_arg) + + with Image.open(test_file) as reloaded: + xmp = reloaded.info["xmp"] + assert_xmp_orientation(xmp, 1) + + def test_tell(self) -> None: + with Image.open(TEST_AVIF_FILE) as im: + assert im.tell() == 0 + + def test_seek(self) -> None: + with Image.open(TEST_AVIF_FILE) as im: + im.seek(0) + + with pytest.raises(EOFError): + im.seek(1) + + @pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:2:0", "4:0:0"]) + def test_encoder_subsampling(self, tmp_path: Path, subsampling: str) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, subsampling=subsampling) + + def test_encoder_subsampling_invalid(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, subsampling="foo") + + @pytest.mark.parametrize("value", ["full", "limited"]) + def test_encoder_range(self, tmp_path: Path, value: str) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, range=value) + + def test_encoder_range_invalid(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, range="foo") + + @skip_unless_avif_encoder("aom") + def test_encoder_codec_param(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, codec="aom") + + def test_encoder_codec_invalid(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, codec="foo") + + @skip_unless_avif_decoder("dav1d") + def test_decoder_codec_cannot_encode(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, codec="dav1d") + + @skip_unless_avif_encoder("aom") + @pytest.mark.parametrize( + "advanced", + [ + { + "aq-mode": "1", + "enable-chroma-deltaq": "1", + }, + (("aq-mode", "1"), ("enable-chroma-deltaq", "1")), + [("aq-mode", "1"), ("enable-chroma-deltaq", "1")], + ], + ) + def test_encoder_advanced_codec_options( + self, advanced: dict[str, str] | Sequence[tuple[str, str]] + ) -> None: + with Image.open(TEST_AVIF_FILE) as im: + ctrl_buf = BytesIO() + im.save(ctrl_buf, "AVIF", codec="aom") + test_buf = BytesIO() + im.save( + test_buf, + "AVIF", + codec="aom", + advanced=advanced, + ) + assert ctrl_buf.getvalue() != test_buf.getvalue() + + @skip_unless_avif_encoder("aom") + @pytest.mark.parametrize("advanced", [{"foo": "bar"}, {"foo": 1234}, 1234]) + def test_encoder_advanced_codec_options_invalid( + self, tmp_path: Path, advanced: dict[str, str] | int + ) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, codec="aom", advanced=advanced) + + @skip_unless_avif_decoder("aom") + def test_decoder_codec_param(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "aom") + + with Image.open(TEST_AVIF_FILE) as im: + assert im.size == (128, 128) + + @skip_unless_avif_encoder("rav1e") + def test_encoder_codec_cannot_decode( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "rav1e") + + with pytest.raises(ValueError): + with Image.open(TEST_AVIF_FILE): + pass + + def test_decoder_codec_invalid(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "foo") + + with pytest.raises(ValueError): + with Image.open(TEST_AVIF_FILE): + pass + + @skip_unless_avif_encoder("aom") + def test_encoder_codec_available(self) -> None: + assert _avif.encoder_codec_available("aom") is True + + def test_encoder_codec_available_bad_params(self) -> None: + with pytest.raises(TypeError): + _avif.encoder_codec_available() + + @skip_unless_avif_decoder("dav1d") + def test_encoder_codec_available_cannot_decode(self) -> None: + assert _avif.encoder_codec_available("dav1d") is False + + def test_encoder_codec_available_invalid(self) -> None: + assert _avif.encoder_codec_available("foo") is False + + def test_encoder_quality_valueerror(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, quality="invalid") + + @skip_unless_avif_decoder("aom") + def test_decoder_codec_available(self) -> None: + assert _avif.decoder_codec_available("aom") is True + + def test_decoder_codec_available_bad_params(self) -> None: + with pytest.raises(TypeError): + _avif.decoder_codec_available() + + @skip_unless_avif_encoder("rav1e") + def test_decoder_codec_available_cannot_decode(self) -> None: + assert _avif.decoder_codec_available("rav1e") is False + + def test_decoder_codec_available_invalid(self) -> None: + assert _avif.decoder_codec_available("foo") is False + + def test_p_mode_transparency(self, tmp_path: Path) -> None: + im = Image.new("P", size=(64, 64)) + draw = ImageDraw.Draw(im) + draw.rectangle(xy=[(0, 0), (32, 32)], fill=255) + draw.rectangle(xy=[(32, 32), (64, 64)], fill=255) + + out_png = tmp_path / "temp.png" + im.save(out_png, transparency=0) + with Image.open(out_png) as im_png: + out_avif = tmp_path / "temp.avif" + im_png.save(out_avif, quality=100) + + with Image.open(out_avif) as expected: + assert_image_similar(im_png.convert("RGBA"), expected, 0.17) + + def test_decoder_strict_flags(self) -> None: + # This would fail if full avif strictFlags were enabled + with Image.open("Tests/images/avif/hopper-missing-pixi.avif") as im: + assert im.size == (128, 128) + + @skip_unless_avif_encoder("aom") + @pytest.mark.parametrize("speed", [-1, 1, 11]) + def test_aom_optimizations(self, tmp_path: Path, speed: int) -> None: + test_file = tmp_path / "temp.avif" + hopper().save(test_file, codec="aom", speed=speed) + + @skip_unless_avif_encoder("svt") + def test_svt_optimizations(self, tmp_path: Path) -> None: + test_file = tmp_path / "temp.avif" + hopper().save(test_file, codec="svt", speed=1) + + +@skip_unless_feature("avif") +class TestAvifAnimation: + @contextmanager + def star_frames(self) -> Generator[list[Image.Image], None, None]: + with Image.open("Tests/images/avif/star.png") as f: + yield [f, f.rotate(90), f.rotate(180), f.rotate(270)] + + def test_n_frames(self) -> None: + """ + Ensure that AVIF format sets n_frames and is_animated attributes + correctly. + """ + + with Image.open(TEST_AVIF_FILE) as im: + assert im.n_frames == 1 + assert not im.is_animated + + with Image.open("Tests/images/avif/star.avifs") as im: + assert im.n_frames == 5 + assert im.is_animated + + def test_write_animation_P(self, tmp_path: Path) -> None: + """ + Convert an animated GIF to animated AVIF, then compare the frame + count, and ensure the frames are visually similar to the originals. + """ + + with Image.open("Tests/images/avif/star.gif") as original: + assert original.n_frames > 1 + + temp_file = tmp_path / "temp.avif" + original.save(temp_file, save_all=True) + with Image.open(temp_file) as im: + assert im.n_frames == original.n_frames + + # Compare first frame in P mode to frame from original GIF + assert_image_similar(im, original.convert("RGBA"), 2) + + # Compare later frames in RGBA mode to frames from original GIF + for frame in range(1, original.n_frames): + original.seek(frame) + im.seek(frame) + assert_image_similar(im, original, 2.54) + + def test_write_animation_RGBA(self, tmp_path: Path) -> None: + """ + Write an animated AVIF from RGBA frames, and ensure the frames + are visually similar to the originals. + """ + + def check(temp_file: Path) -> None: + with Image.open(temp_file) as im: + assert im.n_frames == 4 + + # Compare first frame to original + assert_image_similar(im, frame1, 2.7) + + # Compare second frame to original + im.seek(1) + assert_image_similar(im, frame2, 4.1) + + with self.star_frames() as frames: + frame1 = frames[0] + frame2 = frames[1] + temp_file1 = tmp_path / "temp.avif" + frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:]) + check(temp_file1) + + # Test appending using a generator + def imGenerator( + ims: list[Image.Image], + ) -> Generator[Image.Image, None, None]: + yield from ims + + temp_file2 = tmp_path / "temp_generator.avif" + frames[0].copy().save( + temp_file2, + save_all=True, + append_images=imGenerator(frames[1:]), + ) + check(temp_file2) + + def test_sequence_dimension_mismatch_check(self, tmp_path: Path) -> None: + temp_file = tmp_path / "temp.avif" + frame1 = Image.new("RGB", (100, 100)) + frame2 = Image.new("RGB", (150, 150)) + with pytest.raises(ValueError): + frame1.save(temp_file, save_all=True, append_images=[frame2]) + + def test_heif_raises_unidentified_image_error(self) -> None: + with pytest.raises(UnidentifiedImageError): + with Image.open("Tests/images/avif/hopper.heif"): + pass + + @pytest.mark.parametrize("alpha_premultiplied", [False, True]) + def test_alpha_premultiplied( + self, tmp_path: Path, alpha_premultiplied: bool + ) -> None: + temp_file = tmp_path / "temp.avif" + color = (200, 200, 200, 1) + im = Image.new("RGBA", (1, 1), color) + im.save(temp_file, alpha_premultiplied=alpha_premultiplied) + + expected = (255, 255, 255, 1) if alpha_premultiplied else color + with Image.open(temp_file) as reloaded: + assert reloaded.getpixel((0, 0)) == expected + + def test_timestamp_and_duration(self, tmp_path: Path) -> None: + """ + Try passing a list of durations, and make sure the encoded + timestamps and durations are correct. + """ + + durations = [1, 10, 20, 30, 40] + temp_file = tmp_path / "temp.avif" + with self.star_frames() as frames: + frames[0].save( + temp_file, + save_all=True, + append_images=(frames[1:] + [frames[0]]), + duration=durations, + ) + + with Image.open(temp_file) as im: + assert im.n_frames == 5 + assert im.is_animated + + # Check that timestamps and durations match original values specified + timestamp = 0 + for frame in range(im.n_frames): + im.seek(frame) + im.load() + assert im.info["duration"] == durations[frame] + assert im.info["timestamp"] == timestamp + timestamp += durations[frame] + + def test_seeking(self, tmp_path: Path) -> None: + """ + Create an animated AVIF file, and then try seeking through frames in + reverse-order, verifying the timestamps and durations are correct. + """ + + duration = 33 + temp_file = tmp_path / "temp.avif" + with self.star_frames() as frames: + frames[0].save( + temp_file, + save_all=True, + append_images=(frames[1:] + [frames[0]]), + duration=duration, + ) + + with Image.open(temp_file) as im: + assert im.n_frames == 5 + assert im.is_animated + + # Traverse frames in reverse, checking timestamps and durations + timestamp = duration * (im.n_frames - 1) + for frame in reversed(range(im.n_frames)): + im.seek(frame) + im.load() + assert im.info["duration"] == duration + assert im.info["timestamp"] == timestamp + timestamp -= duration + + def test_seek_errors(self) -> None: + with Image.open("Tests/images/avif/star.avifs") as im: + with pytest.raises(EOFError): + im.seek(-1) + + with pytest.raises(EOFError): + im.seek(42) + + +MAX_THREADS = os.cpu_count() or 1 + + +@skip_unless_feature("avif") +class TestAvifLeaks(PillowLeakTestCase): + mem_limit = MAX_THREADS * 3 * 1024 + iterations = 100 + + @pytest.mark.skipif( + is_docker_qemu(), reason="Skipping on cross-architecture containers" + ) + def test_leak_load(self) -> None: + with open(TEST_AVIF_FILE, "rb") as f: + im_data = f.read() + + def core() -> None: + with Image.open(BytesIO(im_data)) as im: + im.load() + gc.collect() + + self._test_leak(core) diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh new file mode 100755 index 00000000000..fc10d3e545c --- /dev/null +++ b/depends/install_libavif.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -eo pipefail + +version=1.2.1 + +./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz + +pushd libavif-$version + +if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then + PREFIX=$(brew --prefix) +else + PREFIX=/usr +fi + +PKGCONFIG=${PKGCONFIG:-pkg-config} + +LIBAVIF_CMAKE_FLAGS=() +HAS_DECODER=0 +HAS_ENCODER=0 + +if $PKGCONFIG --exists aom; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM) + HAS_ENCODER=1 + HAS_DECODER=1 +fi + +if $PKGCONFIG --exists dav1d; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM) + HAS_DECODER=1 +fi + +if $PKGCONFIG --exists libgav1; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM) + HAS_DECODER=1 +fi + +if $PKGCONFIG --exists rav1e; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM) + HAS_ENCODER=1 +fi + +if $PKGCONFIG --exists SvtAv1Enc; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM) + HAS_ENCODER=1 +fi + +if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL) +fi + +cmake \ + -DCMAKE_INSTALL_PREFIX=$PREFIX \ + -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_MACOSX_RPATH=OFF \ + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + "${LIBAVIF_CMAKE_FLAGS[@]}" \ + . + +sudo make install + +popd diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index c0b1a9d4e84..bfa462c04c9 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -24,6 +24,83 @@ present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``. Fully supported formats ----------------------- +AVIF +^^^^ + +Pillow reads and writes AVIF files, including AVIF sequence images. +It is only possible to save 8-bit AVIF images, and all AVIF images are decoded +as 8-bit RGB(A). + +The :py:meth:`~PIL.Image.Image.save` method supports the following options: + +**quality** + Integer, 0-100, defaults to 75. 0 gives the smallest size and poorest + quality, 100 the largest size and best quality. + +**subsampling** + If present, sets the subsampling for the encoder. Defaults to ``4:2:0``. + Options include: + + * ``4:0:0`` + * ``4:2:0`` + * ``4:2:2`` + * ``4:4:4`` + +**speed** + Quality/speed trade-off (0=slower/better, 10=fastest). Defaults to 6. + +**max_threads** + Limit the number of active threads used. By default, there is no limit. If the aom + codec is used, there is a maximum of 64. + +**range** + YUV range, either "full" or "limited". Defaults to "full". + +**codec** + AV1 codec to use for encoding. Specific values are "aom", "rav1e", and + "svt", presuming the chosen codec is available. Defaults to "auto", which + will choose the first available codec in the order of the preceding list. + +**tile_rows** / **tile_cols** + For tile encoding, the (log 2) number of tile rows and columns to use. + Valid values are 0-6, default 0. Ignored if "autotiling" is set to true. + +**autotiling** + Split the image up to allow parallelization. Enabled automatically if "tile_rows" + and "tile_cols" both have their default values of zero. + +**alpha_premultiplied** + Encode the image with premultiplied alpha. Defaults to ``False``. + +**advanced** + Codec specific options. + +**icc_profile** + The ICC Profile to include in the saved file. + +**exif** + The exif data to include in the saved file. + +**xmp** + The XMP data to include in the saved file. + +Saving sequences +~~~~~~~~~~~~~~~~ + +When calling :py:meth:`~PIL.Image.Image.save` to write an AVIF file, by default +only the first frame of a multiframe image will be saved. If the ``save_all`` +argument is present and true, then all frames will be saved, and the following +options will also be available. + +**append_images** + A list of images to append as additional frames. Each of the + images in the list can be single or multiframe images. + +**duration** + The display duration of each frame, in milliseconds. Pass a single + integer for a constant duration, or a list or tuple to set the + duration for each frame separately. + BLP ^^^ @@ -242,7 +319,7 @@ following options are available:: **append_images** A list of images to append as additional frames. Each of the images in the list can be single or multiframe images. - This is currently supported for GIF, PDF, PNG, TIFF, and WebP. + This is supported for AVIF, GIF, PDF, PNG, TIFF and WebP. It is also supported for ICO and ICNS. If images are passed in of relevant sizes, they will be used instead of scaling down the main image. diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 2790bc2e62c..9f953e718c3 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -89,6 +89,14 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. +* **libavif** provides support for the AVIF format. + + * Pillow requires libavif version **1.0.0** or greater. + * libavif is merely an API that wraps AVIF codecs. If you are compiling + libavif from source, you will also need to install both an AVIF encoder + and decoder, such as rav1e and dav1d, or libaom, which both encodes and + decodes AVIF images. + .. tab:: Linux If you didn't build Python from source, make sure you have Python's @@ -117,6 +125,12 @@ Many of Pillow's features require external libraries: To install libraqm, ``sudo apt-get install meson`` and then see ``depends/install_raqm.sh``. + Build prerequisites for libavif on Ubuntu are installed with:: + + sudo apt-get install cmake ninja-build nasm + + Then see ``depends/install_libavif.sh`` to build and install libavif. + Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ @@ -148,7 +162,15 @@ Many of Pillow's features require external libraries: The easiest way to install external libraries is via `Homebrew `_. After you install Homebrew, run:: - brew install libjpeg libraqm libtiff little-cms2 openjpeg webp + brew install libavif libjpeg libraqm libtiff little-cms2 openjpeg webp + + If you would like to use libavif with more codecs than just aom, then + instead of installing libavif through Homebrew directly, you can use + Homebrew to install libavif's build dependencies:: + + brew install aom dav1d rav1e svt-av1 + + Then see ``depends/install_libavif.sh`` to install libavif. .. tab:: Windows @@ -187,7 +209,8 @@ Many of Pillow's features require external libraries: mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-libimagequant \ - mingw-w64-x86_64-libraqm + mingw-w64-x86_64-libraqm \ + mingw-w64-x86_64-libavif .. tab:: FreeBSD @@ -199,7 +222,7 @@ Many of Pillow's features require external libraries: Prerequisites are installed on **FreeBSD 10 or 11** with:: - sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb libavif Then see ``depends/install_raqm_cmake.sh`` to install libraqm. diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 0e173fe8785..c5d89b838d9 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -21,6 +21,7 @@ Support for the following modules can be checked: * ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`. * ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`. * ``webp``: WebP image support. +* ``avif``: AVIF image support. .. autofunction:: PIL.features.check_module .. autofunction:: PIL.features.version_module diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 454b94d8ce7..c789f575700 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -1,6 +1,14 @@ Plugin reference ================ +:mod:`~PIL.AvifImagePlugin` Module +---------------------------------- + +.. automodule:: PIL.AvifImagePlugin + :members: + :undoc-members: + :show-inheritance: + :mod:`~PIL.BmpImagePlugin` Module --------------------------------- diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index d40d86f21a8..dbaa8a4a463 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -68,3 +68,12 @@ Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT5, BC2, BC3 and BC5 are supported:: im.save("out.dds", pixel_format="DXT1") + +Other Changes +============= + +Reading and writing AVIF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow can now read and write AVIF images. If you are building Pillow from source, this +will require libavif 1.0.0 or later. diff --git a/setup.py b/setup.py index 9fac993b10b..9d69b1d6ea6 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ def get_version() -> str: PILLOW_VERSION = get_version() +AVIF_ROOT = None FREETYPE_ROOT = None HARFBUZZ_ROOT = None FRIBIDI_ROOT = None @@ -306,6 +307,7 @@ class ext_feature: "jpeg2000", "imagequant", "xcb", + "avif", ] required = {"jpeg", "zlib"} @@ -481,6 +483,7 @@ def build_extensions(self) -> None: # # add configured kits for root_name, lib_name in { + "AVIF_ROOT": "avif", "JPEG_ROOT": "libjpeg", "JPEG2K_ROOT": "libopenjp2", "TIFF_ROOT": ("libtiff-5", "libtiff-4"), @@ -846,6 +849,12 @@ def build_extensions(self) -> None: if _find_library_file(self, "xcb"): feature.set("xcb", "xcb") + if feature.want("avif"): + _dbg("Looking for avif") + if _find_include_file(self, "avif/avif.h"): + if _find_library_file(self, "avif"): + feature.set("avif", "avif") + for f in feature: if not feature.get(f) and feature.require(f): if f in ("jpeg", "zlib"): @@ -934,6 +943,14 @@ def build_extensions(self) -> None: else: self._remove_extension("PIL._webp") + if feature.get("avif"): + libs = [feature.get("avif")] + if sys.platform == "win32": + libs.extend(["ntdll", "userenv", "ws2_32", "bcrypt"]) + self._update_extension("PIL._avif", libs) + else: + self._remove_extension("PIL._avif") + tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else [] self._update_extension("PIL._imagingtk", tk_libs) @@ -976,6 +993,7 @@ def summary_report(self, feature: ext_feature) -> None: (feature.get("lcms"), "LITTLECMS2"), (feature.get("webp"), "WEBP"), (feature.get("xcb"), "XCB (X protocol)"), + (feature.get("avif"), "LIBAVIF"), ] all = 1 @@ -1018,6 +1036,7 @@ def debug_build() -> bool: Extension("PIL._imagingft", ["src/_imagingft.c"]), Extension("PIL._imagingcms", ["src/_imagingcms.c"]), Extension("PIL._webp", ["src/_webp.c"]), + Extension("PIL._avif", ["src/_avif.c"]), Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), Extension("PIL._imagingmath", ["src/_imagingmath.c"]), Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py new file mode 100644 index 00000000000..b2c5ab15d7e --- /dev/null +++ b/src/PIL/AvifImagePlugin.py @@ -0,0 +1,292 @@ +from __future__ import annotations + +import os +from io import BytesIO +from typing import IO + +from . import ExifTags, Image, ImageFile + +try: + from . import _avif + + SUPPORTED = True +except ImportError: + SUPPORTED = False + +# Decoder options as module globals, until there is a way to pass parameters +# to Image.open (see https://github.com/python-pillow/Pillow/issues/569) +DECODE_CODEC_CHOICE = "auto" +# Decoding is only affected by this for libavif **0.8.4** or greater. +DEFAULT_MAX_THREADS = 0 + + +def get_codec_version(codec_name: str) -> str | None: + versions = _avif.codec_versions() + for version in versions.split(", "): + if version.split(" [")[0] == codec_name: + return version.split(":")[-1].split(" ")[0] + return None + + +def _accept(prefix: bytes) -> bool | str: + if prefix[4:8] != b"ftyp": + return False + major_brand = prefix[8:12] + if major_brand in ( + # coding brands + b"avif", + b"avis", + # We accept files with AVIF container brands; we can't yet know if + # the ftyp box has the correct compatible brands, but if it doesn't + # then the plugin will raise a SyntaxError which Pillow will catch + # before moving on to the next plugin that accepts the file. + # + # Also, because this file might not actually be an AVIF file, we + # don't raise an error if AVIF support isn't properly compiled. + b"mif1", + b"msf1", + ): + if not SUPPORTED: + return ( + "image file could not be identified because AVIF support not installed" + ) + return True + return False + + +def _get_default_max_threads() -> int: + if DEFAULT_MAX_THREADS: + return DEFAULT_MAX_THREADS + if hasattr(os, "sched_getaffinity"): + return len(os.sched_getaffinity(0)) + else: + return os.cpu_count() or 1 + + +class AvifImageFile(ImageFile.ImageFile): + format = "AVIF" + format_description = "AVIF image" + __frame = -1 + + def _open(self) -> None: + if not SUPPORTED: + msg = "image file could not be opened because AVIF support not installed" + raise SyntaxError(msg) + + if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available( + DECODE_CODEC_CHOICE + ): + msg = "Invalid opening codec" + raise ValueError(msg) + self._decoder = _avif.AvifDecoder( + self.fp.read(), + DECODE_CODEC_CHOICE, + _get_default_max_threads(), + ) + + # Get info from decoder + self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = ( + self._decoder.get_info() + ) + self.is_animated = self.n_frames > 1 + + if icc: + self.info["icc_profile"] = icc + if xmp: + self.info["xmp"] = xmp + + if exif_orientation != 1 or exif: + exif_data = Image.Exif() + if exif: + exif_data.load(exif) + original_orientation = exif_data.get(ExifTags.Base.Orientation, 1) + else: + original_orientation = 1 + if exif_orientation != original_orientation: + exif_data[ExifTags.Base.Orientation] = exif_orientation + exif = exif_data.tobytes() + if exif: + self.info["exif"] = exif + self.seek(0) + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + + # Set tile + self.__frame = frame + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)] + + def load(self) -> Image.core.PixelAccess | None: + if self.tile: + # We need to load the image data for this frame + data, timescale, pts_in_timescales, duration_in_timescales = ( + self._decoder.get_frame(self.__frame) + ) + self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale)) + self.info["duration"] = round(1000 * (duration_in_timescales / timescale)) + + if self.fp and self._exclusive_fp: + self.fp.close() + self.fp = BytesIO(data) + + return super().load() + + def load_seek(self, pos: int) -> None: + pass + + def tell(self) -> int: + return self.__frame + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + _save(im, fp, filename, save_all=True) + + +def _save( + im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False +) -> None: + info = im.encoderinfo.copy() + if save_all: + append_images = list(info.get("append_images", [])) + else: + append_images = [] + + total = 0 + for ims in [im] + append_images: + total += getattr(ims, "n_frames", 1) + + quality = info.get("quality", 75) + if not isinstance(quality, int) or quality < 0 or quality > 100: + msg = "Invalid quality setting" + raise ValueError(msg) + + duration = info.get("duration", 0) + subsampling = info.get("subsampling", "4:2:0") + speed = info.get("speed", 6) + max_threads = info.get("max_threads", _get_default_max_threads()) + codec = info.get("codec", "auto") + if codec != "auto" and not _avif.encoder_codec_available(codec): + msg = "Invalid saving codec" + raise ValueError(msg) + range_ = info.get("range", "full") + tile_rows_log2 = info.get("tile_rows", 0) + tile_cols_log2 = info.get("tile_cols", 0) + alpha_premultiplied = bool(info.get("alpha_premultiplied", False)) + autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0)) + + icc_profile = info.get("icc_profile", im.info.get("icc_profile")) + exif_orientation = 1 + if exif := info.get("exif"): + if isinstance(exif, Image.Exif): + exif_data = exif + else: + exif_data = Image.Exif() + exif_data.load(exif) + if ExifTags.Base.Orientation in exif_data: + exif_orientation = exif_data.pop(ExifTags.Base.Orientation) + exif = exif_data.tobytes() if exif_data else b"" + elif isinstance(exif, Image.Exif): + exif = exif_data.tobytes() + + xmp = info.get("xmp") + + if isinstance(xmp, str): + xmp = xmp.encode("utf-8") + + advanced = info.get("advanced") + if advanced is not None: + if isinstance(advanced, dict): + advanced = advanced.items() + try: + advanced = tuple(advanced) + except TypeError: + invalid = True + else: + invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced) + if invalid: + msg = ( + "advanced codec options must be a dict of key-value string " + "pairs or a series of key-value two-tuples" + ) + raise ValueError(msg) + + # Setup the AVIF encoder + enc = _avif.AvifEncoder( + im.size, + subsampling, + quality, + speed, + max_threads, + codec, + range_, + tile_rows_log2, + tile_cols_log2, + alpha_premultiplied, + autotiling, + icc_profile or b"", + exif or b"", + exif_orientation, + xmp or b"", + advanced, + ) + + # Add each frame + frame_idx = 0 + frame_duration = 0 + cur_idx = im.tell() + is_single_frame = total == 1 + try: + for ims in [im] + append_images: + # Get number of frames in this image + nfr = getattr(ims, "n_frames", 1) + + for idx in range(nfr): + ims.seek(idx) + + # Make sure image mode is supported + frame = ims + rawmode = ims.mode + if ims.mode not in {"RGB", "RGBA"}: + rawmode = "RGBA" if ims.has_transparency_data else "RGB" + frame = ims.convert(rawmode) + + # Update frame duration + if isinstance(duration, (list, tuple)): + frame_duration = duration[frame_idx] + else: + frame_duration = duration + + # Append the frame to the animation encoder + enc.add( + frame.tobytes("raw", rawmode), + frame_duration, + frame.size, + rawmode, + is_single_frame, + ) + + # Update frame index + frame_idx += 1 + + if not save_all: + break + + finally: + im.seek(cur_idx) + + # Get the final output from the encoder + data = enc.finish() + if data is None: + msg = "cannot write file as AVIF (encoder returned None)" + raise OSError(msg) + + fp.write(data) + + +Image.register_open(AvifImageFile.format, AvifImageFile, _accept) +if SUPPORTED: + Image.register_save(AvifImageFile.format, _save) + Image.register_save_all(AvifImageFile.format, _save_all) + Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"]) + Image.register_mime(AvifImageFile.format, "image/avif") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 19b22342acb..60850f4ffaf 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1520,6 +1520,8 @@ def getexif(self) -> Exif: # XMP tags if ExifTags.Base.Orientation not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") + if not xmp_tags and (xmp_tags := self.info.get("xmp")): + xmp_tags = xmp_tags.decode("utf-8") if xmp_tags: match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) if match: diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 09546fe6333..6e4c23f897f 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -25,6 +25,7 @@ _plugins = [ + "AvifImagePlugin", "BlpImagePlugin", "BmpImagePlugin", "BufrStubImagePlugin", diff --git a/src/PIL/_avif.pyi b/src/PIL/_avif.pyi new file mode 100644 index 00000000000..e27843e5338 --- /dev/null +++ b/src/PIL/_avif.pyi @@ -0,0 +1,3 @@ +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/features.py b/src/PIL/features.py index ae7ea4255ef..573f1d41256 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -17,6 +17,7 @@ "freetype2": ("PIL._imagingft", "freetype2_version"), "littlecms2": ("PIL._imagingcms", "littlecms_version"), "webp": ("PIL._webp", "webpdecoder_version"), + "avif": ("PIL._avif", "libavif_version"), } @@ -288,6 +289,7 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("freetype2", "FREETYPE2"), ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), + ("avif", "AVIF"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), ("zlib", "ZLIB (PNG/ZIP)"), diff --git a/src/_avif.c b/src/_avif.c new file mode 100644 index 00000000000..eabd9958e10 --- /dev/null +++ b/src/_avif.c @@ -0,0 +1,908 @@ +#define PY_SSIZE_T_CLEAN + +#include +#include "avif/avif.h" + +// Encoder type +typedef struct { + PyObject_HEAD avifEncoder *encoder; + avifImage *image; + int first_frame; +} AvifEncoderObject; + +static PyTypeObject AvifEncoder_Type; + +// Decoder type +typedef struct { + PyObject_HEAD avifDecoder *decoder; + Py_buffer buffer; +} AvifDecoderObject; + +static PyTypeObject AvifDecoder_Type; + +static int +normalize_tiles_log2(int value) { + if (value < 0) { + return 0; + } else if (value > 6) { + return 6; + } else { + return value; + } +} + +static PyObject * +exc_type_for_avif_result(avifResult result) { + switch (result) { + case AVIF_RESULT_INVALID_EXIF_PAYLOAD: + case AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION: + return PyExc_ValueError; + case AVIF_RESULT_INVALID_FTYP: + case AVIF_RESULT_BMFF_PARSE_FAILED: + case AVIF_RESULT_TRUNCATED_DATA: + case AVIF_RESULT_NO_CONTENT: + return PyExc_SyntaxError; + default: + return PyExc_RuntimeError; + } +} + +static uint8_t +irot_imir_to_exif_orientation(const avifImage *image) { + uint8_t axis = image->imir.axis; + int imir = image->transformFlags & AVIF_TRANSFORM_IMIR; + int irot = image->transformFlags & AVIF_TRANSFORM_IROT; + if (irot) { + uint8_t angle = image->irot.angle; + if (angle == 1) { + if (imir) { + return axis ? 7 // 90 degrees anti-clockwise then swap left and right. + : 5; // 90 degrees anti-clockwise then swap top and bottom. + } + return 6; // 90 degrees anti-clockwise. + } + if (angle == 2) { + if (imir) { + return axis + ? 4 // 180 degrees anti-clockwise then swap left and right. + : 2; // 180 degrees anti-clockwise then swap top and bottom. + } + return 3; // 180 degrees anti-clockwise. + } + if (angle == 3) { + if (imir) { + return axis + ? 5 // 270 degrees anti-clockwise then swap left and right. + : 7; // 270 degrees anti-clockwise then swap top and bottom. + } + return 8; // 270 degrees anti-clockwise. + } + } + if (imir) { + return axis ? 2 // Swap left and right. + : 4; // Swap top and bottom. + } + return 1; // Default orientation ("top-left", no-op). +} + +static void +exif_orientation_to_irot_imir(avifImage *image, int orientation) { + // Mapping from Exif orientation as defined in JEITA CP-3451C section 4.6.4.A + // Orientation to irot and imir boxes as defined in HEIF ISO/IEC 28002-12:2021 + // sections 6.5.10 and 6.5.12. + switch (orientation) { + case 2: // The 0th row is at the visual top of the image, and the 0th column is + // the visual right-hand side. + image->transformFlags |= AVIF_TRANSFORM_IMIR; + image->imir.axis = 1; + break; + case 3: // The 0th row is at the visual bottom of the image, and the 0th column + // is the visual right-hand side. + image->transformFlags |= AVIF_TRANSFORM_IROT; + image->irot.angle = 2; + break; + case 4: // The 0th row is at the visual bottom of the image, and the 0th column + // is the visual left-hand side. + image->transformFlags |= AVIF_TRANSFORM_IMIR; + break; + case 5: // The 0th row is the visual left-hand side of the image, and the 0th + // column is the visual top. + image->transformFlags |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; + image->irot.angle = 1; // applied before imir according to MIAF spec + // ISO/IEC 28002-12:2021 - section 7.3.6.7 + break; + case 6: // The 0th row is the visual right-hand side of the image, and the 0th + // column is the visual top. + image->transformFlags |= AVIF_TRANSFORM_IROT; + image->irot.angle = 3; + break; + case 7: // The 0th row is the visual right-hand side of the image, and the 0th + // column is the visual bottom. + image->transformFlags |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; + image->irot.angle = 3; // applied before imir according to MIAF spec + // ISO/IEC 28002-12:2021 - section 7.3.6.7 + break; + case 8: // The 0th row is the visual left-hand side of the image, and the 0th + // column is the visual bottom. + image->transformFlags |= AVIF_TRANSFORM_IROT; + image->irot.angle = 1; + break; + } +} + +static int +_codec_available(const char *name, avifCodecFlags flags) { + avifCodecChoice codec = avifCodecChoiceFromName(name); + if (codec == AVIF_CODEC_CHOICE_AUTO) { + return 0; + } + const char *codec_name = avifCodecName(codec, flags); + return (codec_name == NULL) ? 0 : 1; +} + +PyObject * +_decoder_codec_available(PyObject *self, PyObject *args) { + char *codec_name; + if (!PyArg_ParseTuple(args, "s", &codec_name)) { + return NULL; + } + int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_DECODE); + return PyBool_FromLong(is_available); +} + +PyObject * +_encoder_codec_available(PyObject *self, PyObject *args) { + char *codec_name; + if (!PyArg_ParseTuple(args, "s", &codec_name)) { + return NULL; + } + int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_ENCODE); + return PyBool_FromLong(is_available); +} + +PyObject * +_codec_versions(PyObject *self, PyObject *args) { + char buffer[256]; + avifCodecVersions(buffer); + return PyUnicode_FromString(buffer); +} + +static int +_add_codec_specific_options(avifEncoder *encoder, PyObject *opts) { + Py_ssize_t i, size; + PyObject *keyval, *py_key, *py_val; + if (!PyTuple_Check(opts)) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + size = PyTuple_GET_SIZE(opts); + + for (i = 0; i < size; i++) { + keyval = PyTuple_GetItem(opts, i); + if (!PyTuple_Check(keyval) || PyTuple_GET_SIZE(keyval) != 2) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + py_key = PyTuple_GetItem(keyval, 0); + py_val = PyTuple_GetItem(keyval, 1); + if (!PyUnicode_Check(py_key) || !PyUnicode_Check(py_val)) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + const char *key = PyUnicode_AsUTF8(py_key); + const char *val = PyUnicode_AsUTF8(py_val); + if (key == NULL || val == NULL) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + + avifResult result = avifEncoderSetCodecSpecificOption(encoder, key, val); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting advanced codec options failed: %s", + avifResultToString(result) + ); + return 1; + } + } + return 0; +} + +// Encoder functions +PyObject * +AvifEncoderNew(PyObject *self_, PyObject *args) { + unsigned int width, height; + AvifEncoderObject *self = NULL; + avifEncoder *encoder = NULL; + + char *subsampling; + int quality; + int speed; + int exif_orientation; + int max_threads; + Py_buffer icc_buffer; + Py_buffer exif_buffer; + Py_buffer xmp_buffer; + int alpha_premultiplied; + int autotiling; + int tile_rows_log2; + int tile_cols_log2; + + char *codec; + char *range; + + PyObject *advanced; + int error = 0; + + if (!PyArg_ParseTuple( + args, + "(II)siiissiippy*y*iy*O", + &width, + &height, + &subsampling, + &quality, + &speed, + &max_threads, + &codec, + &range, + &tile_rows_log2, + &tile_cols_log2, + &alpha_premultiplied, + &autotiling, + &icc_buffer, + &exif_buffer, + &exif_orientation, + &xmp_buffer, + &advanced + )) { + return NULL; + } + + // Create a new animation encoder and picture frame + avifImage *image = avifImageCreateEmpty(); + if (image == NULL) { + PyErr_SetString(PyExc_ValueError, "Image creation failed"); + error = 1; + goto end; + } + + // Set these in advance so any upcoming RGB -> YUV use the proper coefficients + if (strcmp(range, "full") == 0) { + image->yuvRange = AVIF_RANGE_FULL; + } else if (strcmp(range, "limited") == 0) { + image->yuvRange = AVIF_RANGE_LIMITED; + } else { + PyErr_SetString(PyExc_ValueError, "Invalid range"); + error = 1; + goto end; + } + if (strcmp(subsampling, "4:0:0") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV400; + } else if (strcmp(subsampling, "4:2:0") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV420; + } else if (strcmp(subsampling, "4:2:2") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV422; + } else if (strcmp(subsampling, "4:4:4") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV444; + } else { + PyErr_Format(PyExc_ValueError, "Invalid subsampling: %s", subsampling); + error = 1; + goto end; + } + + // Validate canvas dimensions + if (width == 0 || height == 0) { + PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions"); + error = 1; + goto end; + } + image->width = width; + image->height = height; + + image->depth = 8; + image->alphaPremultiplied = alpha_premultiplied ? AVIF_TRUE : AVIF_FALSE; + + encoder = avifEncoderCreate(); + if (!encoder) { + PyErr_SetString(PyExc_MemoryError, "Can't allocate encoder"); + error = 1; + goto end; + } + + int is_aom_encode = strcmp(codec, "aom") == 0 || + (strcmp(codec, "auto") == 0 && + _codec_available("aom", AVIF_CODEC_FLAG_CAN_ENCODE)); + encoder->maxThreads = is_aom_encode && max_threads > 64 ? 64 : max_threads; + + encoder->quality = quality; + + if (strcmp(codec, "auto") == 0) { + encoder->codecChoice = AVIF_CODEC_CHOICE_AUTO; + } else { + encoder->codecChoice = avifCodecChoiceFromName(codec); + } + if (speed < AVIF_SPEED_SLOWEST) { + speed = AVIF_SPEED_SLOWEST; + } else if (speed > AVIF_SPEED_FASTEST) { + speed = AVIF_SPEED_FASTEST; + } + encoder->speed = speed; + encoder->timescale = (uint64_t)1000; + + encoder->autoTiling = autotiling ? AVIF_TRUE : AVIF_FALSE; + if (!autotiling) { + encoder->tileRowsLog2 = normalize_tiles_log2(tile_rows_log2); + encoder->tileColsLog2 = normalize_tiles_log2(tile_cols_log2); + } + + if (advanced != Py_None && _add_codec_specific_options(encoder, advanced)) { + error = 1; + goto end; + } + + self = PyObject_New(AvifEncoderObject, &AvifEncoder_Type); + if (!self) { + PyErr_SetString(PyExc_RuntimeError, "could not create encoder object"); + error = 1; + goto end; + } + self->first_frame = 1; + + avifResult result; + if (icc_buffer.len) { + result = avifImageSetProfileICC(image, icc_buffer.buf, icc_buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting ICC profile failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + // colorPrimaries and transferCharacteristics are ignored when an ICC + // profile is present, so set them to UNSPECIFIED. + image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; + } else { + image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; + } + image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; + + if (exif_buffer.len) { + result = avifImageSetMetadataExif(image, exif_buffer.buf, exif_buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting EXIF data failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + } + + if (xmp_buffer.len) { + result = avifImageSetMetadataXMP(image, xmp_buffer.buf, xmp_buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting XMP data failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + } + + if (exif_orientation > 1) { + exif_orientation_to_irot_imir(image, exif_orientation); + } + + self->image = image; + self->encoder = encoder; + +end: + PyBuffer_Release(&icc_buffer); + PyBuffer_Release(&exif_buffer); + PyBuffer_Release(&xmp_buffer); + + if (error) { + if (image) { + avifImageDestroy(image); + } + if (encoder) { + avifEncoderDestroy(encoder); + } + if (self) { + PyObject_Del(self); + } + return NULL; + } + + return (PyObject *)self; +} + +PyObject * +_encoder_dealloc(AvifEncoderObject *self) { + if (self->encoder) { + avifEncoderDestroy(self->encoder); + } + if (self->image) { + avifImageDestroy(self->image); + } + Py_RETURN_NONE; +} + +PyObject * +_encoder_add(AvifEncoderObject *self, PyObject *args) { + uint8_t *rgb_bytes; + Py_ssize_t size; + unsigned int duration; + unsigned int width; + unsigned int height; + char *mode; + unsigned int is_single_frame; + int error = 0; + + avifRGBImage rgb; + avifResult result; + + avifEncoder *encoder = self->encoder; + avifImage *image = self->image; + avifImage *frame = NULL; + + if (!PyArg_ParseTuple( + args, + "y#I(II)sp", + (char **)&rgb_bytes, + &size, + &duration, + &width, + &height, + &mode, + &is_single_frame + )) { + return NULL; + } + + if (image->width != width || image->height != height) { + PyErr_Format( + PyExc_ValueError, + "Image sequence dimensions mismatch, %ux%u != %ux%u", + image->width, + image->height, + width, + height + ); + return NULL; + } + + if (self->first_frame) { + // If we don't have an image populated with yuv planes, this is the first frame + frame = image; + } else { + frame = avifImageCreateEmpty(); + if (image == NULL) { + PyErr_SetString(PyExc_ValueError, "Image creation failed"); + return NULL; + } + + frame->width = width; + frame->height = height; + frame->colorPrimaries = image->colorPrimaries; + frame->transferCharacteristics = image->transferCharacteristics; + frame->matrixCoefficients = image->matrixCoefficients; + frame->yuvRange = image->yuvRange; + frame->yuvFormat = image->yuvFormat; + frame->depth = image->depth; + frame->alphaPremultiplied = image->alphaPremultiplied; + } + + avifRGBImageSetDefaults(&rgb, frame); + + if (strcmp(mode, "RGBA") == 0) { + rgb.format = AVIF_RGB_FORMAT_RGBA; + } else { + rgb.format = AVIF_RGB_FORMAT_RGB; + } + + result = avifRGBImageAllocatePixels(&rgb); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Pixel allocation failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + + if (rgb.rowBytes * rgb.height != size) { + PyErr_Format( + PyExc_RuntimeError, + "rgb data has incorrect size: %u * %u (%u) != %u", + rgb.rowBytes, + rgb.height, + rgb.rowBytes * rgb.height, + size + ); + error = 1; + goto end; + } + + // rgb.pixels is safe for writes + memcpy(rgb.pixels, rgb_bytes, size); + + Py_BEGIN_ALLOW_THREADS; + result = avifImageRGBToYUV(frame, &rgb); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Conversion to YUV failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + + uint32_t addImageFlags = + is_single_frame ? AVIF_ADD_IMAGE_FLAG_SINGLE : AVIF_ADD_IMAGE_FLAG_NONE; + + Py_BEGIN_ALLOW_THREADS; + result = avifEncoderAddImage(encoder, frame, duration, addImageFlags); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to encode image: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + +end: + if (&rgb) { + avifRGBImageFreePixels(&rgb); + } + if (!self->first_frame) { + avifImageDestroy(frame); + } + + if (error) { + return NULL; + } + self->first_frame = 0; + Py_RETURN_NONE; +} + +PyObject * +_encoder_finish(AvifEncoderObject *self) { + avifEncoder *encoder = self->encoder; + + avifRWData raw = AVIF_DATA_EMPTY; + avifResult result; + PyObject *ret = NULL; + + Py_BEGIN_ALLOW_THREADS; + result = avifEncoderFinish(encoder, &raw); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to finish encoding: %s", + avifResultToString(result) + ); + avifRWDataFree(&raw); + return NULL; + } + + ret = PyBytes_FromStringAndSize((char *)raw.data, raw.size); + + avifRWDataFree(&raw); + + return ret; +} + +// Decoder functions +PyObject * +AvifDecoderNew(PyObject *self_, PyObject *args) { + Py_buffer buffer; + AvifDecoderObject *self = NULL; + avifDecoder *decoder; + + char *codec_str; + avifCodecChoice codec; + int max_threads; + + avifResult result; + + if (!PyArg_ParseTuple(args, "y*si", &buffer, &codec_str, &max_threads)) { + return NULL; + } + + if (strcmp(codec_str, "auto") == 0) { + codec = AVIF_CODEC_CHOICE_AUTO; + } else { + codec = avifCodecChoiceFromName(codec_str); + } + + self = PyObject_New(AvifDecoderObject, &AvifDecoder_Type); + if (!self) { + PyErr_SetString(PyExc_RuntimeError, "could not create decoder object"); + PyBuffer_Release(&buffer); + return NULL; + } + + decoder = avifDecoderCreate(); + if (!decoder) { + PyErr_SetString(PyExc_MemoryError, "Can't allocate decoder"); + PyBuffer_Release(&buffer); + PyObject_Del(self); + return NULL; + } + decoder->maxThreads = max_threads; + // Turn off libavif's 'clap' (clean aperture) property validation. + decoder->strictFlags &= ~AVIF_STRICT_CLAP_VALID; + // Allow the PixelInformationProperty ('pixi') to be missing in AV1 image + // items. libheif v1.11.0 and older does not add the 'pixi' item property to + // AV1 image items. + decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED; + decoder->codecChoice = codec; + + result = avifDecoderSetIOMemory(decoder, buffer.buf, buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting IO memory failed: %s", + avifResultToString(result) + ); + avifDecoderDestroy(decoder); + PyBuffer_Release(&buffer); + PyObject_Del(self); + return NULL; + } + + result = avifDecoderParse(decoder); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to decode image: %s", + avifResultToString(result) + ); + avifDecoderDestroy(decoder); + PyBuffer_Release(&buffer); + PyObject_Del(self); + return NULL; + } + + self->decoder = decoder; + self->buffer = buffer; + + return (PyObject *)self; +} + +PyObject * +_decoder_dealloc(AvifDecoderObject *self) { + if (self->decoder) { + avifDecoderDestroy(self->decoder); + } + PyBuffer_Release(&self->buffer); + Py_RETURN_NONE; +} + +PyObject * +_decoder_get_info(AvifDecoderObject *self) { + avifDecoder *decoder = self->decoder; + avifImage *image = decoder->image; + + PyObject *icc = NULL; + PyObject *exif = NULL; + PyObject *xmp = NULL; + PyObject *ret = NULL; + + if (image->xmp.size) { + xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size); + } + + if (image->exif.size) { + exif = + PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size); + } + + if (image->icc.size) { + icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size); + } + + ret = Py_BuildValue( + "(II)IsSSIS", + image->width, + image->height, + decoder->imageCount, + decoder->alphaPresent ? "RGBA" : "RGB", + NULL == icc ? Py_None : icc, + NULL == exif ? Py_None : exif, + irot_imir_to_exif_orientation(image), + NULL == xmp ? Py_None : xmp + ); + + Py_XDECREF(xmp); + Py_XDECREF(exif); + Py_XDECREF(icc); + + return ret; +} + +PyObject * +_decoder_get_frame(AvifDecoderObject *self, PyObject *args) { + PyObject *bytes; + PyObject *ret; + Py_ssize_t size; + avifResult result; + avifRGBImage rgb; + avifDecoder *decoder; + avifImage *image; + uint32_t frame_index; + + decoder = self->decoder; + + if (!PyArg_ParseTuple(args, "I", &frame_index)) { + return NULL; + } + + result = avifDecoderNthImage(decoder, frame_index); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to decode frame %u: %s", + frame_index, + avifResultToString(result) + ); + return NULL; + } + + image = decoder->image; + + avifRGBImageSetDefaults(&rgb, image); + + rgb.depth = 8; + rgb.format = decoder->alphaPresent ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB; + + result = avifRGBImageAllocatePixels(&rgb); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Pixel allocation failed: %s", + avifResultToString(result) + ); + return NULL; + } + + Py_BEGIN_ALLOW_THREADS; + result = avifImageYUVToRGB(image, &rgb); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Conversion from YUV failed: %s", + avifResultToString(result) + ); + avifRGBImageFreePixels(&rgb); + return NULL; + } + + if (rgb.height > PY_SSIZE_T_MAX / rgb.rowBytes) { + PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size"); + return NULL; + } + + size = rgb.rowBytes * rgb.height; + + bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size); + avifRGBImageFreePixels(&rgb); + + ret = Py_BuildValue( + "SKKK", + bytes, + decoder->timescale, + decoder->imageTiming.ptsInTimescales, + decoder->imageTiming.durationInTimescales + ); + + Py_DECREF(bytes); + + return ret; +} + +/* -------------------------------------------------------------------- */ +/* Type Definitions */ +/* -------------------------------------------------------------------- */ + +// AvifEncoder methods +static struct PyMethodDef _encoder_methods[] = { + {"add", (PyCFunction)_encoder_add, METH_VARARGS}, + {"finish", (PyCFunction)_encoder_finish, METH_NOARGS}, + {NULL, NULL} /* sentinel */ +}; + +// AvifEncoder type definition +static PyTypeObject AvifEncoder_Type = { + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifEncoder", + .tp_basicsize = sizeof(AvifEncoderObject), + .tp_dealloc = (destructor)_encoder_dealloc, + .tp_methods = _encoder_methods, +}; + +// AvifDecoder methods +static struct PyMethodDef _decoder_methods[] = { + {"get_info", (PyCFunction)_decoder_get_info, METH_NOARGS}, + {"get_frame", (PyCFunction)_decoder_get_frame, METH_VARARGS}, + {NULL, NULL} /* sentinel */ +}; + +// AvifDecoder type definition +static PyTypeObject AvifDecoder_Type = { + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifDecoder", + .tp_basicsize = sizeof(AvifDecoderObject), + .tp_dealloc = (destructor)_decoder_dealloc, + .tp_methods = _decoder_methods, +}; + +/* -------------------------------------------------------------------- */ +/* Module Setup */ +/* -------------------------------------------------------------------- */ + +static PyMethodDef avifMethods[] = { + {"AvifDecoder", AvifDecoderNew, METH_VARARGS}, + {"AvifEncoder", AvifEncoderNew, METH_VARARGS}, + {"decoder_codec_available", _decoder_codec_available, METH_VARARGS}, + {"encoder_codec_available", _encoder_codec_available, METH_VARARGS}, + {"codec_versions", _codec_versions, METH_NOARGS}, + {NULL, NULL} +}; + +static int +setup_module(PyObject *m) { + if (PyType_Ready(&AvifDecoder_Type) < 0 || PyType_Ready(&AvifEncoder_Type) < 0) { + return -1; + } + + PyObject *d = PyModule_GetDict(m); + PyObject *v = PyUnicode_FromString(avifVersion()); + PyDict_SetItemString(d, "libavif_version", v ? v : Py_None); + Py_XDECREF(v); + + return 0; +} + +PyMODINIT_FUNC +PyInit__avif(void) { + PyObject *m; + + static PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + .m_name = "_avif", + .m_size = -1, + .m_methods = avifMethods, + }; + + m = PyModule_Create(&module_def); + if (setup_module(m) < 0) { + Py_DECREF(m); + return NULL; + } + +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + + return m; +} diff --git a/wheels/dependency_licenses/AOM.txt b/wheels/dependency_licenses/AOM.txt new file mode 100644 index 00000000000..3a2e46c264b --- /dev/null +++ b/wheels/dependency_licenses/AOM.txt @@ -0,0 +1,26 @@ +Copyright (c) 2016, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. 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. + +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. diff --git a/wheels/dependency_licenses/DAV1D.txt b/wheels/dependency_licenses/DAV1D.txt new file mode 100644 index 00000000000..875b138ecf6 --- /dev/null +++ b/wheels/dependency_licenses/DAV1D.txt @@ -0,0 +1,23 @@ +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. 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. + +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 OWNER 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. diff --git a/wheels/dependency_licenses/LIBAVIF.txt b/wheels/dependency_licenses/LIBAVIF.txt new file mode 100644 index 00000000000..350eb9d15ce --- /dev/null +++ b/wheels/dependency_licenses/LIBAVIF.txt @@ -0,0 +1,387 @@ +Copyright 2019 Joe Drago. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. 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. + +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. + +------------------------------------------------------------------------------ + +Files: src/obu.c + +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. 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. + +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 OWNER 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. + +------------------------------------------------------------------------------ + +Files: third_party/iccjpeg/* + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +The Unix configuration script "configure" was produced with GNU Autoconf. +It is copyright by the Free Software Foundation but is freely distributable. +The same holds for its supporting scripts (config.guess, config.sub, +ltmain.sh). Another support script, install-sh, is copyright by X Consortium +but is also freely distributable. + +The IJG distribution formerly included code to read and write GIF files. +To avoid entanglement with the Unisys LZW patent, GIF reading support has +been removed altogether, and the GIF writer has been simplified to produce +"uncompressed GIFs". This technique does not use the LZW algorithm; the +resulting GIF files are larger than usual, but are readable by all standard +GIF decoders. + +We are required to state that + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +------------------------------------------------------------------------------ + +Files: contrib/gdk-pixbuf/* + +Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. 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. + +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. + +------------------------------------------------------------------------------ + +Files: android_jni/gradlew* + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ + +Files: third_party/libyuv/* + +Copyright 2011 The LibYuv Project Authors. 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 of Google 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. diff --git a/wheels/dependency_licenses/LIBYUV.txt b/wheels/dependency_licenses/LIBYUV.txt new file mode 100644 index 00000000000..c911747a6b5 --- /dev/null +++ b/wheels/dependency_licenses/LIBYUV.txt @@ -0,0 +1,29 @@ +Copyright 2011 The LibYuv Project Authors. 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 of Google 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. diff --git a/wheels/dependency_licenses/RAV1E.txt b/wheels/dependency_licenses/RAV1E.txt new file mode 100644 index 00000000000..3d6c825c4eb --- /dev/null +++ b/wheels/dependency_licenses/RAV1E.txt @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2017-2023, the rav1e contributors +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. + +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. diff --git a/wheels/dependency_licenses/SVT-AV1.txt b/wheels/dependency_licenses/SVT-AV1.txt new file mode 100644 index 00000000000..532a982b3ff --- /dev/null +++ b/wheels/dependency_licenses/SVT-AV1.txt @@ -0,0 +1,26 @@ +Copyright (c) 2019, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. 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. + +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. diff --git a/winbuild/build.rst b/winbuild/build.rst index aae78ce1237..3c20c7d179f 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -61,6 +61,7 @@ Run ``build_prepare.py`` to configure the build:: --no-imagequant skip GPL-licensed optional dependency libimagequant --no-fribidi, --no-raqm skip LGPL-licensed optional dependency FriBiDi + --no-avif skip optional dependency libavif Arguments can also be supplied using the environment variables PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 2e9e187192e..e4901859ea3 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,6 +116,7 @@ def cmd_msbuild( "HARFBUZZ": "11.0.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.17", + "LIBAVIF": "1.2.1", "LIBIMAGEQUANT": "4.3.4", "LIBPNG": "1.6.47", "LIBWEBP": "1.5.0", @@ -378,6 +379,26 @@ def cmd_msbuild( ], "bins": [r"*.dll"], }, + "libavif": { + "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip", + "filename": f"libavif-{V['LIBAVIF']}.zip", + "license": "LICENSE", + "build": [ + f"{sys.executable} -m pip install meson", + *cmds_cmake( + "avif_static", + "-DBUILD_SHARED_LIBS=OFF", + "-DAVIF_LIBSHARPYUV=LOCAL", + "-DAVIF_LIBYUV=LOCAL", + "-DAVIF_CODEC_AOM=LOCAL", + "-DAVIF_CODEC_DAV1D=LOCAL", + "-DAVIF_CODEC_RAV1E=LOCAL", + "-DAVIF_CODEC_SVT=LOCAL", + ), + cmd_xcopy("include", "{inc_dir}"), + ], + "libs": ["avif.lib"], + }, } @@ -683,6 +704,11 @@ def main() -> None: action="store_true", help="skip LGPL-licensed optional dependency FriBiDi", ) + parser.add_argument( + "--no-avif", + action="store_true", + help="skip optional dependency libavif", + ) args = parser.parse_args() arch_prefs = ARCHITECTURES[args.architecture] @@ -723,6 +749,8 @@ def main() -> None: disabled += ["libimagequant"] if args.no_fribidi: disabled += ["fribidi"] + if args.no_avif or args.architecture != "AMD64": + disabled += ["libavif"] prefs = { "architecture": args.architecture, From 81412212016a70eb160460e26dc552a0f8a8c153 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Apr 2025 08:35:19 +1100 Subject: [PATCH 1598/2374] Allow cmake<4 when building libtiff --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e4901859ea3..b45148ee89d 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -235,6 +235,7 @@ def cmd_msbuild( "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DWebP_LIBRARY=libwebp", '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", ) ], "headers": [r"libtiff\tiff*.h"], From 348bf6550d3937d14bbd04251c12bed6dfed9eec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Apr 2025 16:33:55 +1100 Subject: [PATCH 1599/2374] Allow cmake<4 when building libavif --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b45148ee89d..e118cd99422 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -395,6 +395,7 @@ def cmd_msbuild( "-DAVIF_CODEC_DAV1D=LOCAL", "-DAVIF_CODEC_RAV1E=LOCAL", "-DAVIF_CODEC_SVT=LOCAL", + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", ), cmd_xcopy("include", "{inc_dir}"), ], From 5c76e7ec17813eefaa1fdd8948e0165ee644e11f Mon Sep 17 00:00:00 2001 From: wiredfool Date: Tue, 1 Apr 2025 07:10:45 +0100 Subject: [PATCH 1600/2374] Image -> Arrow support (#8330) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .ci/install.sh | 3 + .github/workflows/macos-install.sh | 3 + .github/workflows/test-windows.yml | 4 + Tests/test_arrow.py | 164 ++++++++++++++++ Tests/test_pyarrow.py | 112 +++++++++++ docs/reference/Image.rst | 3 + docs/reference/arrow_support.rst | 88 +++++++++ docs/reference/block_allocator.rst | 3 + docs/reference/internal_design.rst | 1 + pyproject.toml | 5 + setup.py | 1 + src/PIL/Image.py | 80 ++++++++ src/_imaging.c | 115 +++++++++++ src/libImaging/Arrow.c | 299 +++++++++++++++++++++++++++++ src/libImaging/Arrow.h | 48 +++++ src/libImaging/Imaging.h | 38 ++++ src/libImaging/Storage.c | 199 ++++++++++++++++++- 17 files changed, 1165 insertions(+), 1 deletion(-) create mode 100644 Tests/test_arrow.py create mode 100644 Tests/test_pyarrow.py create mode 100644 docs/reference/arrow_support.rst create mode 100644 src/libImaging/Arrow.c create mode 100644 src/libImaging/Arrow.h diff --git a/.ci/install.sh b/.ci/install.sh index 83d5df01cf6..ba32eab04ea 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -36,6 +36,9 @@ python3 -m pip install -U pytest python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma +# optional test dependency, only install if there's a binary package. +# fails on beta 3.14 and PyPy +python3 -m pip install --only-binary=:all: pyarrow || true if [[ $(uname) != CYGWIN* ]]; then python3 -m pip install numpy diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 099f4a582b0..94e3d5d085e 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -30,6 +30,9 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma python3 -m pip install numpy +# optional test dependency, only install if there's a binary package. +# fails on beta 3.14 and PyPy +python3 -m pip install --only-binary=:all: pyarrow || true # libavif pushd depends && ./install_libavif.sh && popd diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 0c3f44e96ef..bf8ec2f2cdf 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -88,6 +88,10 @@ jobs: run: | python3 -m pip install PyQt6 + - name: Install PyArrow dependency + run: | + python3 -m pip install --only-binary=:all: pyarrow || true + - name: Install dependencies id: install run: | diff --git a/Tests/test_arrow.py b/Tests/test_arrow.py new file mode 100644 index 00000000000..b86c77b9aa8 --- /dev/null +++ b/Tests/test_arrow.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +import pytest + +from PIL import Image + +from .helper import hopper + + +@pytest.mark.parametrize( + "mode, dest_modes", + ( + ("L", ["I", "F", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]), + ("I", ["L", "F"]), # Technically I;32 can work for any 4x8bit storage. + ("F", ["I", "L", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]), + ("LA", ["L", "F"]), + ("RGB", ["L", "F"]), + ("RGBA", ["L", "F"]), + ("RGBX", ["L", "F"]), + ("CMYK", ["L", "F"]), + ("YCbCr", ["L", "F"]), + ("HSV", ["L", "F"]), + ), +) +def test_invalid_array_type(mode: str, dest_modes: list[str]) -> None: + img = hopper(mode) + for dest_mode in dest_modes: + with pytest.raises(ValueError): + Image.fromarrow(img, dest_mode, img.size) + + +def test_invalid_array_size() -> None: + img = hopper("RGB") + + assert img.size != (10, 10) + with pytest.raises(ValueError): + Image.fromarrow(img, "RGB", (10, 10)) + + +def test_release_schema() -> None: + # these should not error out, valgrind should be clean + img = hopper("L") + schema = img.__arrow_c_schema__() + del schema + + +def test_release_array() -> None: + # these should not error out, valgrind should be clean + img = hopper("L") + array, schema = img.__arrow_c_array__() + del array + del schema + + +def test_readonly() -> None: + img = hopper("L") + reloaded = Image.fromarrow(img, img.mode, img.size) + assert reloaded.readonly == 1 + reloaded._readonly = 0 + assert reloaded.readonly == 1 + + +def test_multiblock_l_image() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in single channel mode + size = (4096, 2 * block_size // 4096) + img = Image.new("L", size, 128) + + with pytest.raises(ValueError): + (schema, arr) = img.__arrow_c_array__() + + +def test_multiblock_rgba_image() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + + with pytest.raises(ValueError): + (schema, arr) = img.__arrow_c_array__() + + +def test_multiblock_l_schema() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in single channel mode + size = (4096, 2 * block_size // 4096) + img = Image.new("L", size, 128) + + with pytest.raises(ValueError): + img.__arrow_c_schema__() + + +def test_multiblock_rgba_schema() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + + with pytest.raises(ValueError): + img.__arrow_c_schema__() + + +def test_singleblock_l_image() -> None: + Image.core.set_use_block_allocator(1) + + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, 2 * (block_size // 4096)) + img = Image.new("L", size, 128) + assert img.im.isblock() + + (schema, arr) = img.__arrow_c_array__() + assert schema + assert arr + + Image.core.set_use_block_allocator(0) + + +def test_singleblock_rgba_image() -> None: + Image.core.set_use_block_allocator(1) + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + assert img.im.isblock() + + (schema, arr) = img.__arrow_c_array__() + assert schema + assert arr + Image.core.set_use_block_allocator(0) + + +def test_singleblock_l_schema() -> None: + Image.core.set_use_block_allocator(1) + block_size = Image.core.get_block_size() + + # check a 2 block image in single channel mode + size = (4096, 2 * block_size // 4096) + img = Image.new("L", size, 128) + assert img.im.isblock() + + schema = img.__arrow_c_schema__() + assert schema + Image.core.set_use_block_allocator(0) + + +def test_singleblock_rgba_schema() -> None: + Image.core.set_use_block_allocator(1) + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + assert img.im.isblock() + + schema = img.__arrow_c_schema__() + assert schema + Image.core.set_use_block_allocator(0) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py new file mode 100644 index 00000000000..ece9f8f2657 --- /dev/null +++ b/Tests/test_pyarrow.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from typing import Any # undone + +import pytest + +from PIL import Image + +from .helper import ( + assert_deep_equal, + assert_image_equal, + hopper, +) + +pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") + +TEST_IMAGE_SIZE = (10, 10) + + +def _test_img_equals_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None +) -> None: + assert img.height * img.width == len(arr) + px = img.load() + assert px is not None + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): + if mask: + for ix, elt in enumerate(mask): + pixel = px[x, y] + assert isinstance(pixel, tuple) + assert pixel[ix] == arr[y * img.width + x].as_py()[elt] + else: + assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) + + +# really hard to get a non-nullable list type +fl_uint8_4_type = pyarrow.field( + "_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4) +).type + + +@pytest.mark.parametrize( + "mode, dtype, mask", + ( + ("L", pyarrow.uint8(), None), + ("I", pyarrow.int32(), None), + ("F", pyarrow.float32(), None), + ("LA", fl_uint8_4_type, [0, 3]), + ("RGB", fl_uint8_4_type, [0, 1, 2]), + ("RGBA", fl_uint8_4_type, None), + ("RGBX", fl_uint8_4_type, None), + ("CMYK", fl_uint8_4_type, None), + ("YCbCr", fl_uint8_4_type, [0, 1, 2]), + ("HSV", fl_uint8_4_type, [0, 1, 2]), + ), +) +def test_to_array(mode: str, dtype: Any, mask: list[int] | None) -> None: + img = hopper(mode) + + # Resize to non-square + img = img.crop((3, 0, 124, 127)) + assert img.size == (121, 127) + + arr = pyarrow.array(img) + _test_img_equals_pyarray(img, arr, mask) + assert arr.type == dtype + + reloaded = Image.fromarrow(arr, mode, img.size) + + assert reloaded + + assert_image_equal(img, reloaded) + + +def test_lifetime() -> None: + # valgrind shouldn't error out here. + # arrays should be accessible after the image is deleted. + + img = hopper("L") + + arr_1 = pyarrow.array(img) + arr_2 = pyarrow.array(img) + + del img + + assert arr_1.sum().as_py() > 0 + del arr_1 + + assert arr_2.sum().as_py() > 0 + del arr_2 + + +def test_lifetime2() -> None: + # valgrind shouldn't error out here. + # img should remain after the arrays are collected. + + img = hopper("L") + + arr_1 = pyarrow.array(img) + arr_2 = pyarrow.array(img) + + assert arr_1.sum().as_py() > 0 + del arr_1 + + assert arr_2.sum().as_py() > 0 + del arr_2 + + img2 = img.copy() + px = img2.load() + assert px # make mypy happy + assert isinstance(px[0, 0], int) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index bc3758218db..a3ba8cfd8e1 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -79,6 +79,7 @@ Constructing images .. autofunction:: new .. autofunction:: fromarray +.. autofunction:: fromarrow .. autofunction:: frombytes .. autofunction:: frombuffer @@ -370,6 +371,8 @@ Protocols .. autoclass:: SupportsArrayInterface :show-inheritance: +.. autoclass:: SupportsArrowArrayInterface + :show-inheritance: .. autoclass:: SupportsGetData :show-inheritance: diff --git a/docs/reference/arrow_support.rst b/docs/reference/arrow_support.rst new file mode 100644 index 00000000000..4a5c45e8624 --- /dev/null +++ b/docs/reference/arrow_support.rst @@ -0,0 +1,88 @@ +.. _arrow-support: + +============= +Arrow Support +============= + +`Arrow `__ +is an in-memory data exchange format that is the spiritual +successor to the NumPy array interface. It provides for zero-copy +access to columnar data, which in our case is ``Image`` data. + +The goal with Arrow is to provide native zero-copy interoperability +with any Arrow provider or consumer in the Python ecosystem. + +.. warning:: Zero-copy does not mean zero allocation -- the internal + memory layout of Pillow images contains an allocation for row + pointers, so there is a non-zero, but significantly smaller than a + full-copy memory cost to reading an Arrow image. + + +Data Formats +============ + +Pillow currently supports exporting Arrow images in all modes +**except** for ``BGR;15``, ``BGR;16`` and ``BGR;24``. This is due to +line-length packing in these modes making for non-continuous memory. + +For single-band images, the exported array is width*height elements, +with each pixel corresponding to the appropriate Arrow type. + +For multiband images, the exported array is width*height fixed-length +four-element arrays of uint8. This is memory compatible with the raw +image storage of four bytes per pixel. + +Mode ``1`` images are exported as one uint8 byte/pixel, as this is +consistent with the internal storage. + +Pillow will accept, but not produce, one other format. For any +multichannel image with 32-bit storage per pixel, Pillow will accept +an array of width*height int32 elements, which will then be +interpreted using the mode-specific interpretation of the bytes. + +The image mode must match the Arrow band format when reading single +channel images. + +Memory Allocator +================ + +Pillow's default memory allocator, the :ref:`block_allocator`, +allocates up to a 16 MB block for images by default. Larger images +overflow into additional blocks. Arrow requires a single continuous +memory allocation, so images allocated in multiple blocks cannot be +exported in the Arrow format. + +To enable the single block allocator:: + + from PIL import Image + Image.core.set_use_block_allocator(1) + +Note that this is a global setting, not a per-image setting. + +Unsupported Features +==================== + +* Table/dataframe protocol. We support a single array. +* Null markers, producing or consuming. Null values are inferred from + the mode, e.g. RGB images are stored in the first three bytes of + each 32-bit pixel, and the last byte is an implied null. +* Schema negotiation. There is an optional schema for the requested + datatype in the Arrow source interface. We ignore that + parameter. +* Array metadata. + +Internal Details +================ + +Python Arrow C interface: +https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html + +The memory that is exported from the Arrow interface is shared -- not +copied, so the lifetime of the memory allocation is no longer strictly +tied to the life of the Python object. + +The core imaging struct now has a refcount associated with it, and the +lifetime of the core image struct is now divorced from the Python +image object. Creating an arrow reference to the image increments the +refcount, and the imaging struct is only released when the refcount +reaches zero. diff --git a/docs/reference/block_allocator.rst b/docs/reference/block_allocator.rst index 1abe5280fbf..f4d27e24e57 100644 --- a/docs/reference/block_allocator.rst +++ b/docs/reference/block_allocator.rst @@ -1,3 +1,6 @@ + +.. _block_allocator: + Block Allocator =============== diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index 99a18e9ea99..0411779535b 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -9,3 +9,4 @@ Internal Reference block_allocator internal_modules c_extension_debugging + arrow_support diff --git a/pyproject.toml b/pyproject.toml index 780a938a32c..8564192154d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,10 @@ optional-dependencies.fpx = [ optional-dependencies.mic = [ "olefile", ] +optional-dependencies.test-arrow = [ + "pyarrow", +] + optional-dependencies.tests = [ "check-manifest", "coverage>=7.4.2", @@ -67,6 +71,7 @@ optional-dependencies.tests = [ "pytest-timeout", "trove-classifiers>=2024.10.12", ] + optional-dependencies.typing = [ "typing-extensions; python_version<'3.10'", ] diff --git a/setup.py b/setup.py index 9d69b1d6ea6..5ecd6b8160a 100644 --- a/setup.py +++ b/setup.py @@ -65,6 +65,7 @@ def get_version() -> str: _LIB_IMAGING = ( "Access", "AlphaComposite", + "Arrow", "Resample", "Reduce", "Bands", diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 60850f4ffaf..233df592c33 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -577,6 +577,14 @@ def size(self) -> tuple[int, int]: def mode(self) -> str: return self._mode + @property + def readonly(self) -> int: + return (self._im and self._im.readonly) or self._readonly + + @readonly.setter + def readonly(self, readonly: int) -> None: + self._readonly = readonly + def _new(self, im: core.ImagingCore) -> Image: new = Image() new.im = im @@ -728,6 +736,16 @@ def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]: new["shape"], new["typestr"] = _conv_type_shape(self) return new + def __arrow_c_schema__(self) -> object: + self.load() + return self.im.__arrow_c_schema__() + + def __arrow_c_array__( + self, requested_schema: object | None = None + ) -> tuple[object, object]: + self.load() + return (self.im.__arrow_c_schema__(), self.im.__arrow_c_array__()) + def __getstate__(self) -> list[Any]: im_data = self.tobytes() # load image first return [self.info, self.mode, self.size, self.getpalette(), im_data] @@ -3201,6 +3219,18 @@ def __array_interface__(self) -> dict[str, Any]: raise NotImplementedError() +class SupportsArrowArrayInterface(Protocol): + """ + An object that has an ``__arrow_c_array__`` method corresponding to the arrow c + data interface. + """ + + def __arrow_c_array__( + self, requested_schema: "PyCapsule" = None # type: ignore[name-defined] # noqa: F821, UP037 + ) -> tuple["PyCapsule", "PyCapsule"]: # type: ignore[name-defined] # noqa: F821, UP037 + raise NotImplementedError() + + def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: """ Creates an image memory from an object exporting the array interface @@ -3289,6 +3319,56 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) +def fromarrow(obj: SupportsArrowArrayInterface, mode, size) -> Image: + """Creates an image with zero-copy shared memory from an object exporting + the arrow_c_array interface protocol:: + + from PIL import Image + import pyarrow as pa + arr = pa.array([0]*(5*5*4), type=pa.uint8()) + im = Image.fromarrow(arr, 'RGBA', (5, 5)) + + If the data representation of the ``obj`` is not compatible with + Pillow internal storage, a ValueError is raised. + + Pillow images can also be converted to Arrow objects:: + + from PIL import Image + import pyarrow as pa + im = Image.open('hopper.jpg') + arr = pa.array(im) + + As with array support, when converting Pillow images to arrays, + only pixel values are transferred. This means that P and PA mode + images will lose their palette. + + :param obj: Object with an arrow_c_array interface + :param mode: Image mode. + :param size: Image size. This must match the storage of the arrow object. + :returns: An Image object + + Note that according to the Arrow spec, both the producer and the + consumer should consider the exported array to be immutable, as + unsynchronized updates will potentially cause inconsistent data. + + See: :ref:`arrow-support` for more detailed information + + .. versionadded:: 11.2.0 + + """ + if not hasattr(obj, "__arrow_c_array__"): + msg = "arrow_c_array interface not found" + raise ValueError(msg) + + (schema_capsule, array_capsule) = obj.__arrow_c_array__() + _im = core.new_arrow(mode, size, schema_capsule, array_capsule) + if _im: + return Image()._new(_im) + + msg = "new_arrow returned None without an exception" + raise ValueError(msg) + + def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile: """Creates an image instance from a QImage image""" from . import ImageQt diff --git a/src/_imaging.c b/src/_imaging.c index 330a7eef401..72f12214390 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -230,6 +230,93 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { return PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE); } +/* -------------------------------------------------------------------- */ +/* Arrow HANDLING */ +/* -------------------------------------------------------------------- */ + +PyObject * +ArrowError(int err) { + if (err == IMAGING_CODEC_MEMORY) { + return ImagingError_MemoryError(); + } + if (err == IMAGING_ARROW_INCOMPATIBLE_MODE) { + return ImagingError_ValueError("Incompatible Pillow mode for Arrow array"); + } + if (err == IMAGING_ARROW_MEMORY_LAYOUT) { + return ImagingError_ValueError( + "Image is in multiple array blocks, use imaging_new_block for zero copy" + ); + } + return ImagingError_ValueError("Unknown error"); +} + +void +ReleaseArrowSchemaPyCapsule(PyObject *capsule) { + struct ArrowSchema *schema = + (struct ArrowSchema *)PyCapsule_GetPointer(capsule, "arrow_schema"); + if (schema->release != NULL) { + schema->release(schema); + } + free(schema); +} + +PyObject * +ExportArrowSchemaPyCapsule(ImagingObject *self) { + struct ArrowSchema *schema = + (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema)); + int err = export_imaging_schema(self->image, schema); + if (err == 0) { + return PyCapsule_New(schema, "arrow_schema", ReleaseArrowSchemaPyCapsule); + } + free(schema); + return ArrowError(err); +} + +void +ReleaseArrowArrayPyCapsule(PyObject *capsule) { + struct ArrowArray *array = + (struct ArrowArray *)PyCapsule_GetPointer(capsule, "arrow_array"); + if (array->release != NULL) { + array->release(array); + } + free(array); +} + +PyObject * +ExportArrowArrayPyCapsule(ImagingObject *self) { + struct ArrowArray *array = + (struct ArrowArray *)calloc(1, sizeof(struct ArrowArray)); + int err = export_imaging_array(self->image, array); + if (err == 0) { + return PyCapsule_New(array, "arrow_array", ReleaseArrowArrayPyCapsule); + } + free(array); + return ArrowError(err); +} + +static PyObject * +_new_arrow(PyObject *self, PyObject *args) { + char *mode; + int xsize, ysize; + PyObject *schema_capsule, *array_capsule; + PyObject *ret; + + if (!PyArg_ParseTuple( + args, "s(ii)OO", &mode, &xsize, &ysize, &schema_capsule, &array_capsule + )) { + return NULL; + } + + // ImagingBorrowArrow is responsible for retaining the array_capsule + ret = + PyImagingNew(ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule) + ); + if (!ret) { + return ImagingError_ValueError("Invalid Arrow array mode or size mismatch"); + } + return ret; +} + /* -------------------------------------------------------------------- */ /* EXCEPTION REROUTING */ /* -------------------------------------------------------------------- */ @@ -3655,6 +3742,10 @@ static struct PyMethodDef methods[] = { /* Misc. */ {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, + /* arrow */ + {"__arrow_c_schema__", (PyCFunction)ExportArrowSchemaPyCapsule, METH_VARARGS}, + {"__arrow_c_array__", (PyCFunction)ExportArrowArrayPyCapsule, METH_VARARGS}, + {NULL, NULL} /* sentinel */ }; @@ -3722,6 +3813,11 @@ _getattr_unsafe_ptrs(ImagingObject *self, void *closure) { ); } +static PyObject * +_getattr_readonly(ImagingObject *self, void *closure) { + return PyLong_FromLong(self->image->read_only); +} + static struct PyGetSetDef getsetters[] = { {"mode", (getter)_getattr_mode}, {"size", (getter)_getattr_size}, @@ -3729,6 +3825,7 @@ static struct PyGetSetDef getsetters[] = { {"id", (getter)_getattr_id}, {"ptr", (getter)_getattr_ptr}, {"unsafe_ptrs", (getter)_getattr_unsafe_ptrs}, + {"readonly", (getter)_getattr_readonly}, {NULL} }; @@ -3983,6 +4080,21 @@ _set_blocks_max(PyObject *self, PyObject *args) { Py_RETURN_NONE; } +static PyObject * +_set_use_block_allocator(PyObject *self, PyObject *args) { + int use_block_allocator; + if (!PyArg_ParseTuple(args, "i:set_use_block_allocator", &use_block_allocator)) { + return NULL; + } + ImagingMemorySetBlockAllocator(&ImagingDefaultArena, use_block_allocator); + Py_RETURN_NONE; +} + +static PyObject * +_get_use_block_allocator(PyObject *self, PyObject *args) { + return PyLong_FromLong(ImagingDefaultArena.use_block_allocator); +} + static PyObject * _clear_cache(PyObject *self, PyObject *args) { int i = 0; @@ -4104,6 +4216,7 @@ static PyMethodDef functions[] = { {"fill", (PyCFunction)_fill, METH_VARARGS}, {"new", (PyCFunction)_new, METH_VARARGS}, {"new_block", (PyCFunction)_new_block, METH_VARARGS}, + {"new_arrow", (PyCFunction)_new_arrow, METH_VARARGS}, {"merge", (PyCFunction)_merge, METH_VARARGS}, /* Functions */ @@ -4190,9 +4303,11 @@ static PyMethodDef functions[] = { {"get_alignment", (PyCFunction)_get_alignment, METH_VARARGS}, {"get_block_size", (PyCFunction)_get_block_size, METH_VARARGS}, {"get_blocks_max", (PyCFunction)_get_blocks_max, METH_VARARGS}, + {"get_use_block_allocator", (PyCFunction)_get_use_block_allocator, METH_VARARGS}, {"set_alignment", (PyCFunction)_set_alignment, METH_VARARGS}, {"set_block_size", (PyCFunction)_set_block_size, METH_VARARGS}, {"set_blocks_max", (PyCFunction)_set_blocks_max, METH_VARARGS}, + {"set_use_block_allocator", (PyCFunction)_set_use_block_allocator, METH_VARARGS}, {"clear_cache", (PyCFunction)_clear_cache, METH_VARARGS}, {NULL, NULL} /* sentinel */ diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c new file mode 100644 index 00000000000..33ff2ce779a --- /dev/null +++ b/src/libImaging/Arrow.c @@ -0,0 +1,299 @@ + +#include "Arrow.h" +#include "Imaging.h" +#include + +/* struct ArrowSchema* */ +/* _arrow_schema_channel(char* channel, char* format) { */ + +/* } */ + +static void +ReleaseExportedSchema(struct ArrowSchema *array) { + // This should not be called on already released array + // assert(array->release != NULL); + + if (!array->release) { + return; + } + if (array->format) { + free((void *)array->format); + array->format = NULL; + } + if (array->name) { + free((void *)array->name); + array->name = NULL; + } + if (array->metadata) { + free((void *)array->metadata); + array->metadata = NULL; + } + + // Release children + for (int64_t i = 0; i < array->n_children; ++i) { + struct ArrowSchema *child = array->children[i]; + if (child->release != NULL) { + child->release(child); + child->release = NULL; + } + // UNDONE -- should I be releasing the children? + } + + // Release dictionary + struct ArrowSchema *dict = array->dictionary; + if (dict != NULL && dict->release != NULL) { + dict->release(dict); + dict->release = NULL; + } + + // TODO here: release and/or deallocate all data directly owned by + // the ArrowArray struct, such as the private_data. + + // Mark array released + array->release = NULL; +} + +int +export_named_type(struct ArrowSchema *schema, char *format, char *name) { + char *formatp; + char *namep; + size_t format_len = strlen(format) + 1; + size_t name_len = strlen(name) + 1; + + formatp = calloc(format_len, 1); + + if (!formatp) { + return IMAGING_CODEC_MEMORY; + } + + namep = calloc(name_len, 1); + if (!namep) { + free(formatp); + return IMAGING_CODEC_MEMORY; + } + + strncpy(formatp, format, format_len); + strncpy(namep, name, name_len); + + *schema = (struct ArrowSchema){// Type description + .format = formatp, + .name = namep, + .metadata = NULL, + .flags = 0, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &ReleaseExportedSchema + }; + return 0; +} + +int +export_imaging_schema(Imaging im, struct ArrowSchema *schema) { + int retval = 0; + + if (strcmp(im->arrow_band_format, "") == 0) { + return IMAGING_ARROW_INCOMPATIBLE_MODE; + } + + /* for now, single block images */ + if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + return IMAGING_ARROW_MEMORY_LAYOUT; + } + + if (im->bands == 1) { + return export_named_type(schema, im->arrow_band_format, im->band_names[0]); + } + + retval = export_named_type(schema, "+w:4", ""); + if (retval != 0) { + return retval; + } + // if it's not 1 band, it's an int32 at the moment. 4 uint8 bands. + schema->n_children = 1; + schema->children = calloc(1, sizeof(struct ArrowSchema *)); + schema->children[0] = (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema)); + retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel"); + if (retval != 0) { + free(schema->children[0]); + schema->release(schema); + return retval; + } + return 0; +} + +static void +release_const_array(struct ArrowArray *array) { + Imaging im = (Imaging)array->private_data; + + if (array->n_children == 0) { + ImagingDelete(im); + } + + // Free the buffers and the buffers array + if (array->buffers) { + free(array->buffers); + array->buffers = NULL; + } + if (array->children) { + // undone -- does arrow release all the children recursively? + for (int i = 0; i < array->n_children; i++) { + if (array->children[i]->release) { + array->children[i]->release(array->children[i]); + array->children[i]->release = NULL; + free(array->children[i]); + } + } + free(array->children); + array->children = NULL; + } + // Mark released + array->release = NULL; +} + +int +export_single_channel_array(Imaging im, struct ArrowArray *array) { + int length = im->xsize * im->ysize; + + /* for now, single block images */ + if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + return IMAGING_ARROW_MEMORY_LAYOUT; + } + + if (im->lines_per_block && im->lines_per_block < im->ysize) { + length = im->xsize * im->lines_per_block; + } + + MUTEX_LOCK(&im->mutex); + im->refcount++; + MUTEX_UNLOCK(&im->mutex); + // Initialize primitive fields + *array = (struct ArrowArray){// Data description + .length = length, + .offset = 0, + .null_count = 0, + .n_buffers = 2, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im + }; + + // Allocate list of buffers + array->buffers = (const void **)malloc(sizeof(void *) * array->n_buffers); + // assert(array->buffers != NULL); + array->buffers[0] = NULL; // no nulls, null bitmap can be omitted + + if (im->block) { + array->buffers[1] = im->block; + } else { + array->buffers[1] = im->blocks[0].ptr; + } + return 0; +} + +int +export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { + int length = im->xsize * im->ysize; + + /* for now, single block images */ + if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + return IMAGING_ARROW_MEMORY_LAYOUT; + } + + if (im->lines_per_block && im->lines_per_block < im->ysize) { + length = im->xsize * im->lines_per_block; + } + + MUTEX_LOCK(&im->mutex); + im->refcount++; + MUTEX_UNLOCK(&im->mutex); + // Initialize primitive fields + // Fixed length arrays are 1 buffer of validity, and the length in pixels. + // Data is in a child array. + *array = (struct ArrowArray){// Data description + .length = length, + .offset = 0, + .null_count = 0, + .n_buffers = 1, + .n_children = 1, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im + }; + + // Allocate list of buffers + array->buffers = (const void **)calloc(1, sizeof(void *) * array->n_buffers); + if (!array->buffers) { + goto err; + } + // assert(array->buffers != NULL); + array->buffers[0] = NULL; // no nulls, null bitmap can be omitted + + // if it's not 1 band, it's an int32 at the moment. 4 uint8 bands. + array->n_children = 1; + array->children = calloc(1, sizeof(struct ArrowArray *)); + if (!array->children) { + goto err; + } + array->children[0] = (struct ArrowArray *)calloc(1, sizeof(struct ArrowArray)); + if (!array->children[0]) { + goto err; + } + + MUTEX_LOCK(&im->mutex); + im->refcount++; + MUTEX_UNLOCK(&im->mutex); + *array->children[0] = (struct ArrowArray){// Data description + .length = length * 4, + .offset = 0, + .null_count = 0, + .n_buffers = 2, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im + }; + + array->children[0]->buffers = + (const void **)calloc(2, sizeof(void *) * array->n_buffers); + + if (im->block) { + array->children[0]->buffers[1] = im->block; + } else { + array->children[0]->buffers[1] = im->blocks[0].ptr; + } + return 0; + +err: + if (array->children[0]) { + free(array->children[0]); + } + if (array->children) { + free(array->children); + } + if (array->buffers) { + free(array->buffers); + } + return IMAGING_CODEC_MEMORY; +} + +int +export_imaging_array(Imaging im, struct ArrowArray *array) { + if (strcmp(im->arrow_band_format, "") == 0) { + return IMAGING_ARROW_INCOMPATIBLE_MODE; + } + + if (im->bands == 1) { + return export_single_channel_array(im, array); + } + + return export_fixed_pixel_array(im, array); +} diff --git a/src/libImaging/Arrow.h b/src/libImaging/Arrow.h new file mode 100644 index 00000000000..0b285fe8053 --- /dev/null +++ b/src/libImaging/Arrow.h @@ -0,0 +1,48 @@ +#include +#include + +// Apache License 2.0. +// Source apache arrow project +// https://arrow.apache.org/docs/format/CDataInterface.html + +#ifndef ARROW_C_DATA_INTERFACE +#define ARROW_C_DATA_INTERFACE + +#define ARROW_FLAG_DICTIONARY_ORDERED 1 +#define ARROW_FLAG_NULLABLE 2 +#define ARROW_FLAG_MAP_KEYS_SORTED 4 + +struct ArrowSchema { + // Array type description + const char *format; + const char *name; + const char *metadata; + int64_t flags; + int64_t n_children; + struct ArrowSchema **children; + struct ArrowSchema *dictionary; + + // Release callback + void (*release)(struct ArrowSchema *); + // Opaque producer-specific data + void *private_data; +}; + +struct ArrowArray { + // Array data description + int64_t length; + int64_t null_count; + int64_t offset; + int64_t n_buffers; + int64_t n_children; + const void **buffers; + struct ArrowArray **children; + struct ArrowArray *dictionary; + + // Release callback + void (*release)(struct ArrowArray *); + // Opaque producer-specific data + void *private_data; +}; + +#endif // ARROW_C_DATA_INTERFACE diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 0fc191d158b..234f9943c5a 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -20,6 +20,8 @@ extern "C" { #define M_PI 3.1415926535897932384626433832795 #endif +#include "Arrow.h" + /* -------------------------------------------------------------------- */ /* @@ -104,6 +106,21 @@ struct ImagingMemoryInstance { /* Virtual methods */ void (*destroy)(Imaging im); + + /* arrow */ + int refcount; /* Number of arrow arrays that have been allocated */ + char band_names[4][3]; /* names of bands, max 2 char + null terminator */ + char arrow_band_format[2]; /* single character + null terminator */ + + int read_only; /* flag for read-only. set for arrow borrowed arrays */ + PyObject *arrow_array_capsule; /* upstream arrow array source */ + + int blocks_count; /* Number of blocks that have been allocated */ + int lines_per_block; /* Number of lines in a block have been allocated */ + +#ifdef Py_GIL_DISABLED + PyMutex mutex; +#endif }; #define IMAGING_PIXEL_1(im, x, y) ((im)->image8[(y)][(x)]) @@ -161,6 +178,7 @@ typedef struct ImagingMemoryArena { int stats_reallocated_blocks; /* Number of blocks which were actually reallocated after retrieving */ int stats_freed_blocks; /* Number of freed blocks */ + int use_block_allocator; /* don't use arena, use block allocator */ #ifdef Py_GIL_DISABLED PyMutex mutex; #endif @@ -174,6 +192,8 @@ extern int ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max); extern void ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size); +extern void +ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator); extern Imaging ImagingNew(const char *mode, int xsize, int ysize); @@ -187,6 +207,15 @@ ImagingDelete(Imaging im); extern Imaging ImagingNewBlock(const char *mode, int xsize, int ysize); +extern Imaging +ImagingNewArrow( + const char *mode, + int xsize, + int ysize, + PyObject *schema_capsule, + PyObject *array_capsule +); + extern Imaging ImagingNewPrologue(const char *mode, int xsize, int ysize); extern Imaging @@ -700,6 +729,13 @@ _imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence); extern Py_ssize_t _imaging_tell_pyFd(PyObject *fd); +/* Arrow */ + +extern int +export_imaging_array(Imaging im, struct ArrowArray *array); +extern int +export_imaging_schema(Imaging im, struct ArrowSchema *schema); + /* Errcodes */ #define IMAGING_CODEC_END 1 #define IMAGING_CODEC_OVERRUN -1 @@ -707,6 +743,8 @@ _imaging_tell_pyFd(PyObject *fd); #define IMAGING_CODEC_UNKNOWN -3 #define IMAGING_CODEC_CONFIG -8 #define IMAGING_CODEC_MEMORY -9 +#define IMAGING_ARROW_INCOMPATIBLE_MODE -10 +#define IMAGING_ARROW_MEMORY_LAYOUT -11 #include "ImagingUtils.h" extern UINT8 *clip8_lookups; diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 522e9f37557..4fa4ecd1ce4 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -58,19 +58,22 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { /* Setup image descriptor */ im->xsize = xsize; im->ysize = ysize; - + im->refcount = 1; im->type = IMAGING_TYPE_UINT8; + strcpy(im->arrow_band_format, "C"); if (strcmp(mode, "1") == 0) { /* 1-bit images */ im->bands = im->pixelsize = 1; im->linesize = xsize; + strcpy(im->band_names[0], "1"); } else if (strcmp(mode, "P") == 0) { /* 8-bit palette mapped images */ im->bands = im->pixelsize = 1; im->linesize = xsize; im->palette = ImagingPaletteNew("RGB"); + strcpy(im->band_names[0], "P"); } else if (strcmp(mode, "PA") == 0) { /* 8-bit palette with alpha */ @@ -78,23 +81,36 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 4; /* store in image32 memory */ im->linesize = xsize * 4; im->palette = ImagingPaletteNew("RGB"); + strcpy(im->band_names[0], "P"); + strcpy(im->band_names[1], "X"); + strcpy(im->band_names[2], "X"); + strcpy(im->band_names[3], "A"); } else if (strcmp(mode, "L") == 0) { /* 8-bit grayscale (luminance) images */ im->bands = im->pixelsize = 1; im->linesize = xsize; + strcpy(im->band_names[0], "L"); } else if (strcmp(mode, "LA") == 0) { /* 8-bit grayscale (luminance) with alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ im->linesize = xsize * 4; + strcpy(im->band_names[0], "L"); + strcpy(im->band_names[1], "X"); + strcpy(im->band_names[2], "X"); + strcpy(im->band_names[3], "A"); } else if (strcmp(mode, "La") == 0) { /* 8-bit grayscale (luminance) with premultiplied alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ im->linesize = xsize * 4; + strcpy(im->band_names[0], "L"); + strcpy(im->band_names[1], "X"); + strcpy(im->band_names[2], "X"); + strcpy(im->band_names[3], "a"); } else if (strcmp(mode, "F") == 0) { /* 32-bit floating point images */ @@ -102,6 +118,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 4; im->linesize = xsize * 4; im->type = IMAGING_TYPE_FLOAT32; + strcpy(im->arrow_band_format, "f"); + strcpy(im->band_names[0], "F"); } else if (strcmp(mode, "I") == 0) { /* 32-bit integer images */ @@ -109,6 +127,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 4; im->linesize = xsize * 4; im->type = IMAGING_TYPE_INT32; + strcpy(im->arrow_band_format, "i"); + strcpy(im->band_names[0], "I"); } else if (strcmp(mode, "I;16") == 0 || strcmp(mode, "I;16L") == 0 || strcmp(mode, "I;16B") == 0 || strcmp(mode, "I;16N") == 0) { @@ -118,12 +138,18 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 2; im->linesize = xsize * 2; im->type = IMAGING_TYPE_SPECIAL; + strcpy(im->arrow_band_format, "s"); + strcpy(im->band_names[0], "I"); } else if (strcmp(mode, "RGB") == 0) { /* 24-bit true colour images */ im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "X"); } else if (strcmp(mode, "BGR;15") == 0) { /* EXPERIMENTAL */ @@ -132,6 +158,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; + /* not allowing arrow due to line length packing */ + strcpy(im->arrow_band_format, ""); } else if (strcmp(mode, "BGR;16") == 0) { /* EXPERIMENTAL */ @@ -140,6 +168,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; + /* not allowing arrow due to line length packing */ + strcpy(im->arrow_band_format, ""); } else if (strcmp(mode, "BGR;24") == 0) { /* EXPERIMENTAL */ @@ -148,32 +178,54 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 3; im->linesize = (xsize * 3 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; + /* not allowing arrow due to line length packing */ + strcpy(im->arrow_band_format, ""); } else if (strcmp(mode, "RGBX") == 0) { /* 32-bit true colour images with padding */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "X"); } else if (strcmp(mode, "RGBA") == 0) { /* 32-bit true colour images with alpha */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "A"); } else if (strcmp(mode, "RGBa") == 0) { /* 32-bit true colour images with premultiplied alpha */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "a"); } else if (strcmp(mode, "CMYK") == 0) { /* 32-bit colour separation */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "C"); + strcpy(im->band_names[1], "M"); + strcpy(im->band_names[2], "Y"); + strcpy(im->band_names[3], "K"); } else if (strcmp(mode, "YCbCr") == 0) { /* 24-bit video format */ im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "Y"); + strcpy(im->band_names[1], "Cb"); + strcpy(im->band_names[2], "Cr"); + strcpy(im->band_names[3], "X"); } else if (strcmp(mode, "LAB") == 0) { /* 24-bit color, luminance, + 2 color channels */ @@ -181,6 +233,10 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "L"); + strcpy(im->band_names[1], "a"); + strcpy(im->band_names[2], "b"); + strcpy(im->band_names[3], "X"); } else if (strcmp(mode, "HSV") == 0) { /* 24-bit color, luminance, + 2 color channels */ @@ -188,6 +244,10 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "H"); + strcpy(im->band_names[1], "S"); + strcpy(im->band_names[2], "V"); + strcpy(im->band_names[3], "X"); } else { free(im); @@ -218,6 +278,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { break; } + // UNDONE -- not accurate for arrow MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingDefaultArena.stats_new_count += 1; MUTEX_UNLOCK(&ImagingDefaultArena.mutex); @@ -238,8 +299,18 @@ ImagingDelete(Imaging im) { return; } + MUTEX_LOCK(&im->mutex); + im->refcount--; + + if (im->refcount > 0) { + MUTEX_UNLOCK(&im->mutex); + return; + } + MUTEX_UNLOCK(&im->mutex); + if (im->palette) { ImagingPaletteDelete(im->palette); + im->palette = NULL; } if (im->destroy) { @@ -270,6 +341,7 @@ struct ImagingMemoryArena ImagingDefaultArena = { 0, 0, 0, // Stats + 0, // use_block_allocator #ifdef Py_GIL_DISABLED {0}, #endif @@ -302,6 +374,11 @@ ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max) { return 1; } +void +ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator) { + arena->use_block_allocator = use_block_allocator; +} + void ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size) { while (arena->blocks_cached > new_size) { @@ -396,11 +473,13 @@ ImagingAllocateArray(Imaging im, ImagingMemoryArena arena, int dirty, int block_ if (lines_per_block == 0) { lines_per_block = 1; } + im->lines_per_block = lines_per_block; blocks_count = (im->ysize + lines_per_block - 1) / lines_per_block; // printf("NEW size: %dx%d, ls: %d, lpb: %d, blocks: %d\n", // im->xsize, im->ysize, aligned_linesize, lines_per_block, blocks_count); /* One extra pointer is always NULL */ + im->blocks_count = blocks_count; im->blocks = calloc(sizeof(*im->blocks), blocks_count + 1); if (!im->blocks) { return (Imaging)ImagingError_MemoryError(); @@ -487,6 +566,58 @@ ImagingAllocateBlock(Imaging im) { return im; } +/* Borrowed Arrow Storage Type */ +/* --------------------------- */ +/* Don't allocate the image. */ + +static void +ImagingDestroyArrow(Imaging im) { + // Rely on the internal Python destructor for the array capsule. + if (im->arrow_array_capsule) { + Py_DECREF(im->arrow_array_capsule); + im->arrow_array_capsule = NULL; + } +} + +Imaging +ImagingBorrowArrow( + Imaging im, + struct ArrowArray *external_array, + int offset_width, + PyObject *arrow_capsule +) { + // offset_width is the number of char* for a single offset from arrow + Py_ssize_t y, i; + + char *borrowed_buffer = NULL; + struct ArrowArray *arr = external_array; + + if (arr->n_children == 1) { + arr = arr->children[0]; + } + if (arr->n_buffers == 2) { + // buffer 0 is the null list + // buffer 1 is the data + borrowed_buffer = (char *)arr->buffers[1] + (offset_width * arr->offset); + } + + if (!borrowed_buffer) { + return (Imaging + )ImagingError_ValueError("Arrow Array, exactly 2 buffers required"); + } + + for (y = i = 0; y < im->ysize; y++) { + im->image[y] = borrowed_buffer + i; + i += im->linesize; + } + im->read_only = 1; + Py_INCREF(arrow_capsule); + im->arrow_array_capsule = arrow_capsule; + im->destroy = ImagingDestroyArrow; + + return im; +} + /* -------------------------------------------------------------------- * Create a new, internally allocated, image. */ @@ -529,11 +660,17 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { Imaging ImagingNew(const char *mode, int xsize, int ysize) { + if (ImagingDefaultArena.use_block_allocator) { + return ImagingNewBlock(mode, xsize, ysize); + } return ImagingNewInternal(mode, xsize, ysize, 0); } Imaging ImagingNewDirty(const char *mode, int xsize, int ysize) { + if (ImagingDefaultArena.use_block_allocator) { + return ImagingNewBlock(mode, xsize, ysize); + } return ImagingNewInternal(mode, xsize, ysize, 1); } @@ -558,6 +695,66 @@ ImagingNewBlock(const char *mode, int xsize, int ysize) { return NULL; } +Imaging +ImagingNewArrow( + const char *mode, + int xsize, + int ysize, + PyObject *schema_capsule, + PyObject *array_capsule +) { + /* A borrowed arrow array */ + Imaging im; + struct ArrowSchema *schema = + (struct ArrowSchema *)PyCapsule_GetPointer(schema_capsule, "arrow_schema"); + + struct ArrowArray *external_array = + (struct ArrowArray *)PyCapsule_GetPointer(array_capsule, "arrow_array"); + + if (xsize < 0 || ysize < 0) { + return (Imaging)ImagingError_ValueError("bad image size"); + } + + im = ImagingNewPrologue(mode, xsize, ysize); + if (!im) { + return NULL; + } + + int64_t pixels = (int64_t)xsize * (int64_t)ysize; + + // fmt:off // don't reformat this + if (((strcmp(schema->format, "I") == 0 // int32 + && im->pixelsize == 4 // 4xchar* storage + && im->bands >= 2) // INT32 into any INT32 Storage mode + || // (()||()) && + (strcmp(schema->format, im->arrow_band_format) == 0 // same mode + && im->bands == 1)) // Single band match + && pixels == external_array->length) { + // one arrow element per, and it matches a pixelsize*char + if (ImagingBorrowArrow(im, external_array, im->pixelsize, array_capsule)) { + return im; + } + } + if (strcmp(schema->format, "+w:4") == 0 // 4 up array + && im->pixelsize == 4 // storage as 32 bpc + && schema->n_children > 0 // make sure schema is well formed. + && schema->children // make sure schema is well formed + && strcmp(schema->children[0]->format, "C") == 0 // Expected format + && strcmp(im->arrow_band_format, "C") == 0 // Expected Format + && pixels == external_array->length // expected length + && external_array->n_children == 1 // array is well formed + && external_array->children // array is well formed + && 4 * pixels == external_array->children[0]->length) { + // 4 up element of char into pixelsize == 4 + if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) { + return im; + } + } + // fmt: on + ImagingDelete(im); + return NULL; +} + Imaging ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn) { /* allocate or validate output image */ From a7537b1b06490ef3dfbf0bf1c48a0b8c9aa36940 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 30 Mar 2025 07:31:17 +1100 Subject: [PATCH 1601/2374] Only change readonly if saved filename matches opened filename --- Tests/test_image.py | 9 +++++++++ src/PIL/Image.py | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index c2e850c36c7..7e6118d5280 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -258,6 +258,15 @@ def test_readonly_save(self, tmp_path: Path) -> None: assert im.readonly im.save(temp_file) + def test_save_without_changing_readonly(self, tmp_path: Path) -> None: + temp_file = tmp_path / "temp.bmp" + + with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: + assert im.readonly + + im.save(temp_file) + assert im.readonly + def test_dump(self, tmp_path: Path) -> None: im = Image.new("L", (10, 10)) im._dump(str(tmp_path / "temp_L.ppm")) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 233df592c33..c62d7a8a3dd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2540,8 +2540,13 @@ def save( msg = f"unknown file extension: {ext}" raise ValueError(msg) from e + from . import ImageFile + # may mutate self! - self._ensure_mutable() + if isinstance(self, ImageFile.ImageFile) and filename == self.filename: + self._ensure_mutable() + else: + self.load() save_all = params.pop("save_all", None) self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} From f205a45f44b237374533d5f41db65d75da474a45 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Apr 2025 19:10:11 +1100 Subject: [PATCH 1602/2374] Added release notes for #8330 --- docs/releasenotes/11.2.0.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index 2c1c761e5d2..de3db3c84fa 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -81,6 +81,28 @@ DXT5, BC2, BC3 and BC5 are supported:: Other Changes ============= +Arrow support +^^^^^^^^^^^^^ + +`Arrow `__ is an in-memory data exchange format that is the +spiritual successor to the NumPy array interface. It provides for zero-copy access to +columnar data, which in our case is ``Image`` data. + +To create an image with zero-copy shared memory from an object exporting the +arrow_c_array interface protocol:: + + from PIL import Image + import pyarrow as pa + arr = pa.array([0]*(5*5*4), type=pa.uint8()) + im = Image.fromarrow(arr, 'RGBA', (5, 5)) + +Pillow images can also be converted to Arrow objects:: + + from PIL import Image + import pyarrow as pa + im = Image.open('hopper.jpg') + arr = pa.array(im) + Reading and writing AVIF images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 867c4772c22455207d6f1982bfbe130b423b53a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Apr 2025 20:19:40 +1100 Subject: [PATCH 1603/2374] Do not import type checking --- Tests/test_image_array.py | 3 ++- Tests/test_numpy.py | 2 +- Tests/test_qt_image_qapplication.py | 3 ++- docs/dater.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index eb2309e0ffe..25cb99c43ef 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest from packaging.version import parse as parse_version @@ -13,6 +13,7 @@ im = hopper().resize((128, 100)) +TYPE_CHECKING = False if TYPE_CHECKING: import numpy.typing as npt diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index c4ad19d23eb..ef54deeeb2b 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,7 +1,6 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING import pytest @@ -9,6 +8,7 @@ from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature +TYPE_CHECKING = False if TYPE_CHECKING: import numpy import numpy.typing as npt diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 0ed9fbfa5cb..82a3e074120 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,7 +1,7 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING, Union +from typing import Union import pytest @@ -9,6 +9,7 @@ from .helper import assert_image_equal_tofile, assert_image_similar, hopper +TYPE_CHECKING = False if TYPE_CHECKING: import PyQt6 import PySide6 diff --git a/docs/dater.py b/docs/dater.py index f9fb0c1da6e..c0302b55c35 100644 --- a/docs/dater.py +++ b/docs/dater.py @@ -8,8 +8,8 @@ import re import subprocess -from typing import TYPE_CHECKING +TYPE_CHECKING = False if TYPE_CHECKING: from sphinx.application import Sphinx From 1103e82d17311dac9ec2f32cb7e738fda8c64271 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Apr 2025 11:14:58 +1100 Subject: [PATCH 1604/2374] Include filename in state --- Tests/test_pickle.py | 1 + src/PIL/ImageFile.py | 4 ++++ src/PIL/JpegImagePlugin.py | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 70661ecc17a..1c48cb743e0 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -81,6 +81,7 @@ def test_pickle_jpeg() -> None: unpickled_image = pickle.loads(pickle.dumps(image)) # Assert + assert unpickled_image.filename == "Tests/images/hopper.jpg" assert len(unpickled_image.layer) == 3 assert unpickled_image.layers == 3 diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index c5d6383a59f..bcb7d462eba 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -252,8 +252,12 @@ def get_format_mimetype(self) -> str | None: return Image.MIME.get(self.format.upper()) return None + def __getstate__(self) -> list[Any]: + return super().__getstate__() + [self.filename] + def __setstate__(self, state: list[Any]) -> None: self.tile = [] + self.filename = state[5] super().__setstate__(state) def verify(self) -> None: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index cc1d54b93ba..96952884118 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -403,8 +403,8 @@ def __getstate__(self) -> list[Any]: return super().__getstate__() + [self.layers, self.layer] def __setstate__(self, state: list[Any]) -> None: + self.layers, self.layer = state[6:] super().__setstate__(state) - self.layers, self.layer = state[5:] def load_read(self, read_bytes: int) -> bytes: """ From 8dbbce624f7ce9ad85eb50075d9e3dfdcb0fbfd4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Apr 2025 12:16:25 +1100 Subject: [PATCH 1605/2374] Compare absolute path of filename --- src/PIL/Image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c62d7a8a3dd..807814c021e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2543,7 +2543,9 @@ def save( from . import ImageFile # may mutate self! - if isinstance(self, ImageFile.ImageFile) and filename == self.filename: + if isinstance(self, ImageFile.ImageFile) and os.path.abspath( + filename + ) == os.path.abspath(self.filename): self._ensure_mutable() else: self.load() From 7e15c54cad753c30974005230699e888e8902b6c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 2 Apr 2025 23:53:14 +1100 Subject: [PATCH 1606/2374] Use multibuild build_github (#8861) --- .github/workflows/wheels-dependencies.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2f2e75b6cb4..accd99901fa 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -80,11 +80,7 @@ function build_pkg_config { function build_zlib_ng { if [ -e zlib-stamp ]; then return; fi - fetch_unpack https://github.com/zlib-ng/zlib-ng/archive/$ZLIB_NG_VERSION.tar.gz zlib-ng-$ZLIB_NG_VERSION.tar.gz - (cd zlib-ng-$ZLIB_NG_VERSION \ - && ./configure --prefix=$BUILD_PREFIX --zlib-compat \ - && make -j4 \ - && make install) + build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat if [ -n "$IS_MACOS" ]; then # Ensure that on macOS, the library name is an absolute path, not an From f4cd5e750217c8d0c0d5cb5378a49c651d5aa7d8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Apr 2025 18:44:45 +1100 Subject: [PATCH 1607/2374] Assert image type --- Tests/test_file_avif.py | 9 +++++++++ Tests/test_file_fli.py | 2 ++ Tests/test_file_gif.py | 3 +++ Tests/test_file_jpeg.py | 26 +++++++++++++++++++++----- Tests/test_file_mpo.py | 8 ++++++++ Tests/test_file_png.py | 30 ++++++++++++++++++++++-------- Tests/test_file_webp_metadata.py | 2 ++ 7 files changed, 67 insertions(+), 13 deletions(-) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 392a4bbd5b0..069a48940af 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -14,6 +14,7 @@ from PIL import ( AvifImagePlugin, + GifImagePlugin, Image, ImageDraw, ImageFile, @@ -240,6 +241,7 @@ def test_save_single_frame(self, tmp_path: Path) -> None: with Image.open("Tests/images/chi.gif") as im: im.save(temp_file) with Image.open(temp_file) as im: + assert isinstance(im, AvifImagePlugin.AvifImageFile) assert im.n_frames == 1 def test_invalid_file(self) -> None: @@ -595,10 +597,12 @@ def test_n_frames(self) -> None: """ with Image.open(TEST_AVIF_FILE) as im: + assert isinstance(im, AvifImagePlugin.AvifImageFile) assert im.n_frames == 1 assert not im.is_animated with Image.open("Tests/images/avif/star.avifs") as im: + assert isinstance(im, AvifImagePlugin.AvifImageFile) assert im.n_frames == 5 assert im.is_animated @@ -609,11 +613,13 @@ def test_write_animation_P(self, tmp_path: Path) -> None: """ with Image.open("Tests/images/avif/star.gif") as original: + assert isinstance(original, GifImagePlugin.GifImageFile) assert original.n_frames > 1 temp_file = tmp_path / "temp.avif" original.save(temp_file, save_all=True) with Image.open(temp_file) as im: + assert isinstance(im, AvifImagePlugin.AvifImageFile) assert im.n_frames == original.n_frames # Compare first frame in P mode to frame from original GIF @@ -633,6 +639,7 @@ def test_write_animation_RGBA(self, tmp_path: Path) -> None: def check(temp_file: Path) -> None: with Image.open(temp_file) as im: + assert isinstance(im, AvifImagePlugin.AvifImageFile) assert im.n_frames == 4 # Compare first frame to original @@ -705,6 +712,7 @@ def test_timestamp_and_duration(self, tmp_path: Path) -> None: ) with Image.open(temp_file) as im: + assert isinstance(im, AvifImagePlugin.AvifImageFile) assert im.n_frames == 5 assert im.is_animated @@ -734,6 +742,7 @@ def test_seeking(self, tmp_path: Path) -> None: ) with Image.open(temp_file) as im: + assert isinstance(im, AvifImagePlugin.AvifImageFile) assert im.n_frames == 5 assert im.is_animated diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 81df1ab0b80..ff80c92f7e2 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -43,6 +43,7 @@ def test_sanity() -> None: def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) with Image.open(animated_test_file_with_prefix_chunk) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) assert im.mode == "P" assert im.size == (320, 200) assert im.format == "FLI" @@ -50,6 +51,7 @@ def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None: assert im.is_animated palette = im.getpalette() + assert palette is not None assert palette[3:6] == [255, 255, 255] assert palette[381:384] == [204, 204, 12] assert palette[765:] == [252, 0, 0] diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 20d58a9dda4..3d07159f37c 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -280,6 +280,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None: im.save(out, save_all=True) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 5 @@ -1357,8 +1358,10 @@ def test_palette_save_all_P(tmp_path: Path) -> None: with Image.open(out) as im: # Assert that the frames are correct, and each frame has the same palette + assert isinstance(im, GifImagePlugin.GifImageFile) assert_image_equal(im.convert("RGB"), frames[0].convert("RGB")) assert im.palette is not None + assert im.global_palette is not None assert im.palette.palette == im.global_palette.palette im.seek(1) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 79f0ec1a809..9164882d306 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -334,8 +334,10 @@ def test_exif_gps(self, tmp_path: Path) -> None: # Reading with Image.open("Tests/images/exif_gps.jpg") as im: - exif = im._getexif() - assert exif[gps_index] == expected_exif_gps + assert isinstance(im, JpegImagePlugin.JpegImageFile) + exif_data = im._getexif() + assert exif_data is not None + assert exif_data[gps_index] == expected_exif_gps # Writing f = tmp_path / "temp.jpg" @@ -344,8 +346,10 @@ def test_exif_gps(self, tmp_path: Path) -> None: hopper().save(f, exif=exif) with Image.open(f) as reloaded: - exif = reloaded._getexif() - assert exif[gps_index] == expected_exif_gps + assert isinstance(reloaded, JpegImagePlugin.JpegImageFile) + exif_data = reloaded._getexif() + assert exif_data is not None + assert exif_data[gps_index] == expected_exif_gps def test_empty_exif_gps(self) -> None: with Image.open("Tests/images/empty_gps_ifd.jpg") as im: @@ -372,6 +376,7 @@ def test_exif_equality(self) -> None: exifs = [] for i in range(2): with Image.open("Tests/images/exif-200dpcm.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) exifs.append(im._getexif()) assert exifs[0] == exifs[1] @@ -405,13 +410,17 @@ def test_exif_rollback(self) -> None: } with Image.open("Tests/images/exif_gps.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) exif = im._getexif() + assert exif is not None for tag, value in expected_exif.items(): assert value == exif[tag] def test_exif_gps_typeerror(self) -> None: with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + # Should not raise a TypeError im._getexif() @@ -491,7 +500,9 @@ def getsampling( def test_exif(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) info = im._getexif() + assert info is not None assert info[305] == "Adobe Photoshop CS Macintosh" def test_get_child_images(self) -> None: @@ -676,11 +687,13 @@ def test_load_16bit_qtables(self) -> None: def test_save_multiple_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) im2 = self.roundtrip(im, qtables="keep") assert im.quantization == im2.quantization def test_save_single_16bit_qtable(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) im2 = self.roundtrip(im, qtables={0: im.quantization[0]}) assert len(im2.quantization) == 1 assert im2.quantization[0] == im.quantization[0] @@ -889,7 +902,10 @@ def test_ifd_offset_exif(self) -> None: # in contrast to normal 8 with Image.open("Tests/images/exif-ifd-offset.jpg") as im: # Act / Assert - assert im._getexif()[306] == "2017:03:13 23:03:09" + assert isinstance(im, JpegImagePlugin.JpegImageFile) + exif = im._getexif() + assert exif is not None + assert exif[306] == "2017:03:13 23:03:09" def test_multiple_exif(self) -> None: with Image.open("Tests/images/multiple_exif.jpg") as im: diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 73838ef4484..c5d3142e895 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -120,9 +120,11 @@ def test_ignore_frame_size() -> None: # Ignore the different size of the second frame # since this is not a "Large Thumbnail" image with Image.open("Tests/images/ignore_frame_size.mpo") as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) assert im.size == (64, 64) im.seek(1) + assert im.mpinfo is not None assert ( im.mpinfo[0xB002][1]["Attribute"]["MPType"] == "Multi-Frame Image: (Disparity)" @@ -155,7 +157,9 @@ def test_reload_exif_after_seek() -> None: @pytest.mark.parametrize("test_file", test_files) def test_mp(test_file: str) -> None: with Image.open(test_file) as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) mpinfo = im._getmp() + assert mpinfo is not None assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 @@ -164,7 +168,9 @@ def test_mp_offset() -> None: # This image has been manually hexedited to have an IFD offset of 10 # in APP2 data, in contrast to normal 8 with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) mpinfo = im._getmp() + assert mpinfo is not None assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 @@ -180,7 +186,9 @@ def test_mp_no_data() -> None: @pytest.mark.parametrize("test_file", test_files) def test_mp_attribute(test_file: str) -> None: with Image.open(test_file) as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) mpinfo = im._getmp() + assert mpinfo is not None for frame_number, mpentry in enumerate(mpinfo[0xB002]): mpattr = mpentry["Attribute"] if frame_number: diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0f0886ab8dc..13e1c80f47c 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -671,6 +671,9 @@ def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None: im.save(out, bits=4, save_all=save_all) with Image.open(out) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) + assert reloaded.png is not None + assert reloaded.png.im_palette is not None assert len(reloaded.png.im_palette[1]) == 48 def test_plte_length(self, tmp_path: Path) -> None: @@ -681,6 +684,9 @@ def test_plte_length(self, tmp_path: Path) -> None: im.save(out) with Image.open(out) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) + assert reloaded.png is not None + assert reloaded.png.im_palette is not None assert len(reloaded.png.im_palette[1]) == 3 def test_getxmp(self) -> None: @@ -702,13 +708,17 @@ def test_getxmp(self) -> None: def test_exif(self) -> None: # With an EXIF chunk with Image.open("Tests/images/exif.png") as im: - exif = im._getexif() - assert exif[274] == 1 + assert isinstance(im, PngImagePlugin.PngImageFile) + exif_data = im._getexif() + assert exif_data is not None + assert exif_data[274] == 1 # With an ImageMagick zTXt chunk with Image.open("Tests/images/exif_imagemagick.png") as im: - exif = im._getexif() - assert exif[274] == 1 + assert isinstance(im, PngImagePlugin.PngImageFile) + exif_data = im._getexif() + assert exif_data is not None + assert exif_data[274] == 1 # Assert that info still can be extracted # when the image is no longer a PngImageFile instance @@ -717,8 +727,10 @@ def test_exif(self) -> None: # With a tEXt chunk with Image.open("Tests/images/exif_text.png") as im: - exif = im._getexif() - assert exif[274] == 1 + assert isinstance(im, PngImagePlugin.PngImageFile) + exif_data = im._getexif() + assert exif_data is not None + assert exif_data[274] == 1 # With XMP tags with Image.open("Tests/images/xmp_tags_orientation.png") as im: @@ -740,8 +752,10 @@ def test_exif_save(self, tmp_path: Path) -> None: im.save(test_file, exif=im.getexif()) with Image.open(test_file) as reloaded: - exif = reloaded._getexif() - assert exif[274] == 1 + assert isinstance(reloaded, PngImagePlugin.PngImageFile) + exif_data = reloaded._getexif() + assert exif_data is not None + assert exif_data[274] == 1 @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 7543d22da07..3de412b832b 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -22,11 +22,13 @@ def test_read_exif_metadata() -> None: file_path = "Tests/images/flower.webp" with Image.open(file_path) as image: + assert isinstance(image, WebPImagePlugin.WebPImageFile) assert image.format == "WEBP" exif_data = image.info.get("exif", None) assert exif_data exif = image._getexif() + assert exif is not None # Camera make assert exif[271] == "Canon" From 2d452c82e5f78fb25d0271a4f4534622235176da Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 3 Apr 2025 21:17:54 +1100 Subject: [PATCH 1608/2374] Removed condition that is always true (#8862) --- src/_avif.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/_avif.c b/src/_avif.c index eabd9958e10..31228678706 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -568,9 +568,7 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) { } end: - if (&rgb) { - avifRGBImageFreePixels(&rgb); - } + avifRGBImageFreePixels(&rgb); if (!self->first_frame) { avifImageDestroy(frame); } From 8691112a2cba76b7bcf1aaa0f4824ff9063f2f7b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 13:23:36 +0300 Subject: [PATCH 1609/2374] Update scientific-python/upload-nightly-action action to v0.6.2 (#8865) --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 2a8594f4961..3b1be9a96b1 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -245,7 +245,7 @@ jobs: path: dist merge-multiple: true - name: Upload wheels to scientific-python-nightly-wheels - uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1 + uses: scientific-python/upload-nightly-action@b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} From 9f4195752d2231b34909c95fa70d716d4c664491 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 3 Apr 2025 21:24:37 +1100 Subject: [PATCH 1610/2374] Added type hints (#8867) --- src/PIL/Image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 807814c021e..88ea6f3b53e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3326,7 +3326,9 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) -def fromarrow(obj: SupportsArrowArrayInterface, mode, size) -> Image: +def fromarrow( + obj: SupportsArrowArrayInterface, mode: str, size: tuple[int, int] +) -> Image: """Creates an image with zero-copy shared memory from an object exporting the arrow_c_array interface protocol:: From 61d3dd9e83aee05fb19e28274bcc20a8fb8148f6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Apr 2025 22:12:54 +1100 Subject: [PATCH 1611/2374] Updated xz to 5.8.1, except on Windows x86 --- .github/workflows/wheels-dependencies.sh | 16 +--------------- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index accd99901fa..04bce62fbbe 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -42,7 +42,7 @@ HARFBUZZ_VERSION=11.0.0 LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 -XZ_VERSION=5.8.0 +XZ_VERSION=5.8.1 TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 ZLIB_VERSION=1.3.1 @@ -53,20 +53,6 @@ LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 LIBAVIF_VERSION=1.2.1 -if [[ $MB_ML_VER == 2014 ]]; then - function build_xz { - if [ -e xz-stamp ]; then return; fi - yum install -y gettext-devel - fetch_unpack https://tukaani.org/xz/xz-$XZ_VERSION.tar.gz - (cd xz-$XZ_VERSION \ - && ./autogen.sh --no-po4a \ - && ./configure --prefix=$BUILD_PREFIX \ - && make -j4 \ - && make install) - touch xz-stamp - } -fi - function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi # This essentially duplicates the Homebrew recipe diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e118cd99422..e8e3aacc248 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -122,7 +122,7 @@ def cmd_msbuild( "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.7.0", - "XZ": "5.6.4", + "XZ": "5.6.4" if struct.calcsize("P") == 4 else "5.8.1", "ZLIBNG": "2.2.4", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) From 9f654ff748919d92ac0710a573e239b18c82e806 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Fri, 4 Apr 2025 09:41:11 -0400 Subject: [PATCH 1612/2374] Fixed conversion of AVIF image rotation property to EXIF orientation (#8866) --- Tests/test_file_avif.py | 3 ++- src/_avif.c | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 392a4bbd5b0..bd87947c014 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -309,7 +309,7 @@ def test_exif(self) -> None: assert exif[274] == 3 @pytest.mark.parametrize("use_bytes", [True, False]) - @pytest.mark.parametrize("orientation", [1, 2]) + @pytest.mark.parametrize("orientation", [1, 2, 3, 4, 5, 6, 7, 8]) def test_exif_save( self, tmp_path: Path, @@ -327,6 +327,7 @@ def test_exif_save( if orientation == 1: assert "exif" not in reloaded.info else: + assert reloaded.getexif()[274] == orientation assert reloaded.info["exif"] == exif_data def test_exif_without_orientation(self, tmp_path: Path) -> None: diff --git a/src/_avif.c b/src/_avif.c index 31228678706..7e7bee7031b 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -59,7 +59,7 @@ irot_imir_to_exif_orientation(const avifImage *image) { return axis ? 7 // 90 degrees anti-clockwise then swap left and right. : 5; // 90 degrees anti-clockwise then swap top and bottom. } - return 6; // 90 degrees anti-clockwise. + return 8; // 90 degrees anti-clockwise. } if (angle == 2) { if (imir) { @@ -75,7 +75,7 @@ irot_imir_to_exif_orientation(const avifImage *image) { ? 5 // 270 degrees anti-clockwise then swap left and right. : 7; // 270 degrees anti-clockwise then swap top and bottom. } - return 8; // 270 degrees anti-clockwise. + return 6; // 270 degrees anti-clockwise. } } if (imir) { From 1ba32fce487cba432fd56c694651f15e0b32e25f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Apr 2025 15:44:46 +1100 Subject: [PATCH 1613/2374] Updated harfbuzz to 11.0.1 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index accd99901fa..06c968d6744 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -38,7 +38,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.3 -HARFBUZZ_VERSION=11.0.0 +HARFBUZZ_VERSION=11.0.1 LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e118cd99422..cf6dd066180 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ def cmd_msbuild( "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "11.0.0", + "HARFBUZZ": "11.0.1", "JPEGTURBO": "3.1.0", "LCMS2": "2.17", "LIBAVIF": "1.2.1", From 1db27be6a0a56eefb08f7f7ed5064b2af875a6fd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Apr 2025 16:09:12 +1100 Subject: [PATCH 1614/2374] Use same URL as wheels-dependencies.sh --- winbuild/build_prepare.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index cf6dd066180..5806d88dadd 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -349,8 +349,8 @@ def cmd_msbuild( "libs": [r"..\target\release\imagequant_sys.lib"], }, "harfbuzz": { - "url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip", - "filename": f"harfbuzz-{V['HARFBUZZ']}.zip", + "url": f"https://github.com/harfbuzz/harfbuzz/releases/download/{V['HARFBUZZ']}/FILENAME", + "filename": f"harfbuzz-{V['HARFBUZZ']}.tar.xz", "license": "COPYING", "build": [ *cmds_cmake( @@ -514,8 +514,8 @@ def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None: msg = "Attempted Path Traversal in Zip File" raise RuntimeError(msg) zf.extractall(sources_dir) - elif filename.endswith((".tar.gz", ".tgz")): - with tarfile.open(file, "r:gz") as tgz: + elif filename.endswith((".tar.gz", ".tar.xz")): + with tarfile.open(file, "r:xz" if filename.endswith(".xz") else "r:gz") as tgz: for member in tgz.getnames(): member_abspath = os.path.abspath(os.path.join(sources_dir, member)) member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) @@ -776,7 +776,7 @@ def main() -> None: for k, v in DEPS.items(): if "dir" not in v: - v["dir"] = re.sub(r"\.(tar\.gz|zip)", "", v["filename"]) + v["dir"] = re.sub(r"\.(tar\.gz|tar\.xz|zip)", "", v["filename"]) prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"]) print() From 82bccf70a0113617492b163f30e2bf31294c0a09 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 6 Apr 2025 11:10:05 +1000 Subject: [PATCH 1615/2374] Added XZ_CLMUL_CRC:BOOL=OFF to allow Windows x86 to use xz 5.8.1 --- winbuild/build_prepare.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e8e3aacc248..f40312506ef 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -122,7 +122,7 @@ def cmd_msbuild( "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.7.0", - "XZ": "5.6.4" if struct.calcsize("P") == 4 else "5.8.1", + "XZ": "5.8.1", "ZLIBNG": "2.2.4", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) @@ -181,7 +181,11 @@ def cmd_msbuild( "filename": f"xz-{V['XZ']}.tar.gz", "license": "COPYING", "build": [ - *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), + *cmds_cmake( + "liblzma", + "-DBUILD_SHARED_LIBS:BOOL=OFF" + + (" -DXZ_CLMUL_CRC:BOOL=OFF" if struct.calcsize("P") == 4 else ""), + ), cmd_mkdir(r"{inc_dir}\lzma"), cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), ], From f6eb2e7fa50fc707aca36efd3d0dccd64edf1979 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:17:00 +0000 Subject: [PATCH 1616/2374] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.9.9 → v0.11.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.9...v0.11.4) - [github.com/pre-commit/mirrors-clang-format: v19.1.7 → v20.1.0](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.7...v20.1.0) - [github.com/python-jsonschema/check-jsonschema: 0.31.2 → 0.32.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.2...0.32.1) - [github.com/woodruffw/zizmor-pre-commit: v1.4.1 → v1.5.2](https://github.com/woodruffw/zizmor-pre-commit/compare/v1.4.1...v1.5.2) - [github.com/abravalheri/validate-pyproject: v0.23 → v0.24.1](https://github.com/abravalheri/validate-pyproject/compare/v0.23...v0.24.1) --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ff947d4111..66cf6d118e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.9 + rev: v0.11.4 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v19.1.7 + rev: v20.1.0 hooks: - id: clang-format types: [c] @@ -50,14 +50,14 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.31.2 + rev: 0.32.1 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.4.1 + rev: v1.5.2 hooks: - id: zizmor @@ -72,7 +72,7 @@ repos: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.23 + rev: v0.24.1 hooks: - id: validate-pyproject additional_dependencies: [trove-classifiers>=2024.10.12] From a5a8ece5d2211e3121ba0508d9ca78d4afd90e90 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:17:33 +0000 Subject: [PATCH 1617/2374] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/libImaging/QuantHash.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libImaging/QuantHash.h b/src/libImaging/QuantHash.h index 0462cfd4968..d75d55ce0e4 100644 --- a/src/libImaging/QuantHash.h +++ b/src/libImaging/QuantHash.h @@ -20,8 +20,12 @@ typedef uint32_t HashVal_t; typedef uint32_t (*HashFunc)(const HashTable *, const HashKey_t); typedef int (*HashCmpFunc)(const HashTable *, const HashKey_t, const HashKey_t); -typedef void (*IteratorFunc)(const HashTable *, const HashKey_t, const HashVal_t, void *); -typedef void (*IteratorUpdateFunc)(const HashTable *, const HashKey_t, HashVal_t *, void *); +typedef void (*IteratorFunc)( + const HashTable *, const HashKey_t, const HashVal_t, void * +); +typedef void (*IteratorUpdateFunc)( + const HashTable *, const HashKey_t, HashVal_t *, void * +); typedef void (*ComputeFunc)(const HashTable *, const HashKey_t, HashVal_t *); typedef void (*CollisionFunc)( const HashTable *, HashKey_t *, HashVal_t *, HashKey_t, HashVal_t From 8c4510cb236597df034ab3cec97d4ec6ca3ba9a1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 7 Apr 2025 22:25:12 +0300 Subject: [PATCH 1618/2374] Fix clang-format: Configuration file(s) do(es) not support C --- .clang-format | 1 - src/_imagingcms.c | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.clang-format b/.clang-format index 143dde82c2c..f01a1151ae7 100644 --- a/.clang-format +++ b/.clang-format @@ -11,7 +11,6 @@ ColumnLimit: 88 DerivePointerAlignment: false IndentGotoLabels: false IndentWidth: 4 -Language: Cpp PointerAlignment: Right ReflowComments: true SortIncludes: false diff --git a/src/_imagingcms.c b/src/_imagingcms.c index ea2f7018634..f93c1613b47 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1402,8 +1402,8 @@ static struct PyGetSetDef cms_profile_getsetters[] = { {"colorant_table_out", (getter)cms_profile_getattr_colorant_table_out}, {"intent_supported", (getter)cms_profile_getattr_is_intent_supported}, {"clut", (getter)cms_profile_getattr_is_clut}, - {"icc_measurement_condition", (getter)cms_profile_getattr_icc_measurement_condition - }, + {"icc_measurement_condition", + (getter)cms_profile_getattr_icc_measurement_condition}, {"icc_viewing_condition", (getter)cms_profile_getattr_icc_viewing_condition}, {NULL} From 8b7d72440e5d8a1acbe3f4692003d6c6eabd2205 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Apr 2025 20:15:45 +1000 Subject: [PATCH 1619/2374] Specify both C and Cpp --- .clang-format | 21 +++++++++++++++++++++ .pre-commit-config.yaml | 1 + 2 files changed, 22 insertions(+) diff --git a/.clang-format b/.clang-format index f01a1151ae7..1871d1f7a7b 100644 --- a/.clang-format +++ b/.clang-format @@ -1,5 +1,26 @@ # A clang-format style that approximates Python's PEP 7 # Useful for IDE integration +Language: C +BasedOnStyle: Google +AlwaysBreakAfterReturnType: All +AllowShortIfStatementsOnASingleLine: false +AlignAfterOpenBracket: BlockIndent +BinPackArguments: false +BinPackParameters: false +BreakBeforeBraces: Attach +ColumnLimit: 88 +DerivePointerAlignment: false +IndentGotoLabels: false +IndentWidth: 4 +PointerAlignment: Right +ReflowComments: true +SortIncludes: false +SpaceBeforeParens: ControlStatements +SpacesInParentheses: false +TabWidth: 4 +UseTab: Never +--- +Language: Cpp BasedOnStyle: Google AlwaysBreakAfterReturnType: All AllowShortIfStatementsOnASingleLine: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66cf6d118e0..140ce33bead 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,6 +44,7 @@ repos: - id: check-json - id: check-toml - id: check-yaml + args: [--allow-multiple-documents] - id: end-of-file-fixer exclude: ^Tests/images/ - id: trailing-whitespace From 179ae9d395d5ad87e7d9f5db6e93a2cdd69af9ce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Apr 2025 22:05:29 +1000 Subject: [PATCH 1620/2374] Disable building harfbuzz tests --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 06c968d6744..7f1f22a3e3d 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -107,7 +107,7 @@ function build_harfbuzz { local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ - && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled) + && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled -Dtests=disabled) (cd $out_dir/build \ && meson install) touch harfbuzz-stamp From 6b5f8d768d2d388f2c9475f6ae9ca3780dafb8c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Apr 2025 13:55:02 +1000 Subject: [PATCH 1621/2374] Do not include libavif in wheels --- .github/workflows/wheels-dependencies.sh | 41 --- .github/workflows/wheels.yml | 7 +- Tests/check_wheel.py | 8 +- docs/releasenotes/11.2.0.rst | 5 +- wheels/dependency_licenses/AOM.txt | 26 -- wheels/dependency_licenses/DAV1D.txt | 23 -- wheels/dependency_licenses/LIBAVIF.txt | 387 ----------------------- wheels/dependency_licenses/LIBYUV.txt | 29 -- wheels/dependency_licenses/RAV1E.txt | 25 -- wheels/dependency_licenses/SVT-AV1.txt | 26 -- 10 files changed, 5 insertions(+), 572 deletions(-) delete mode 100644 wheels/dependency_licenses/AOM.txt delete mode 100644 wheels/dependency_licenses/DAV1D.txt delete mode 100644 wheels/dependency_licenses/LIBAVIF.txt delete mode 100644 wheels/dependency_licenses/LIBYUV.txt delete mode 100644 wheels/dependency_licenses/RAV1E.txt delete mode 100644 wheels/dependency_licenses/SVT-AV1.txt diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index accd99901fa..395db86b6cb 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -51,7 +51,6 @@ LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 -LIBAVIF_VERSION=1.2.1 if [[ $MB_ML_VER == 2014 ]]; then function build_xz { @@ -113,45 +112,6 @@ function build_harfbuzz { touch harfbuzz-stamp } -function build_libavif { - if [ -e libavif-stamp ]; then return; fi - - python3 -m pip install meson ninja - - if [[ "$PLAT" == "x86_64" ]] || [ -n "$SANITIZER" ]; then - build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03 - fi - - # For rav1e - curl https://sh.rustup.rs -sSf | sh -s -- -y - . "$HOME/.cargo/env" - if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then - yum install -y perl - if [[ "$MB_ML_VER" == 2014 ]]; then - yum install -y perl-IPC-Cmd - fi - fi - - local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz) - (cd $out_dir \ - && CMAKE_POLICY_VERSION_MINIMUM=3.5 cmake \ - -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \ - -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \ - -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_SHARED_LIBS=OFF \ - -DAVIF_LIBSHARPYUV=LOCAL \ - -DAVIF_LIBYUV=LOCAL \ - -DAVIF_CODEC_AOM=LOCAL \ - -DAVIF_CODEC_DAV1D=LOCAL \ - -DAVIF_CODEC_RAV1E=LOCAL \ - -DAVIF_CODEC_SVT=LOCAL \ - -DENABLE_NASM=ON \ - -DCMAKE_MODULE_PATH=/tmp/cmake/Modules \ - . \ - && make install) - touch libavif-stamp -} - function build { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then @@ -186,7 +146,6 @@ function build { build_tiff fi - build_libavif build_libpng build_lcms2 build_openjpeg diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3b1be9a96b1..40d3dc7e882 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -157,14 +157,9 @@ jobs: # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images - & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} + & python.exe winbuild\build_prepare.py -v --no-imagequant --no-avif --architecture=${{ matrix.cibw_arch }} shell: pwsh - - name: Update rust - if: matrix.cibw_arch == 'AMD64' - run: | - rustup update - - name: Build wheels run: | setlocal EnableDelayedExpansion diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 582fc92c2f3..8ba40ba3fbe 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,7 +1,6 @@ from __future__ import annotations import platform -import struct import sys from PIL import features @@ -10,7 +9,7 @@ def test_wheel_modules() -> None: - expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"} + expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} # tkinter is not available in cibuildwheel installed CPython on Windows try: @@ -20,11 +19,6 @@ def test_wheel_modules() -> None: except ImportError: expected_modules.remove("tkinter") - # libavif is not available on Windows for x86 and ARM64 architectures - if sys.platform == "win32": - if platform.machine() == "ARM64" or struct.calcsize("P") == 4: - expected_modules.remove("avif") - assert set(features.get_supported_modules()) == expected_modules diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index de3db3c84fa..ed41c21164c 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -106,5 +106,6 @@ Pillow images can also be converted to Arrow objects:: Reading and writing AVIF images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Pillow can now read and write AVIF images. If you are building Pillow from source, this -will require libavif 1.0.0 or later. +Pillow can now read and write AVIF images. However, due to concern over size, this +functionality is not included in our prebuilt wheels. You will need to build Pillow +from source with libavif 1.0.0 or later. diff --git a/wheels/dependency_licenses/AOM.txt b/wheels/dependency_licenses/AOM.txt deleted file mode 100644 index 3a2e46c264b..00000000000 --- a/wheels/dependency_licenses/AOM.txt +++ /dev/null @@ -1,26 +0,0 @@ -Copyright (c) 2016, Alliance for Open Media. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. 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. - -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. diff --git a/wheels/dependency_licenses/DAV1D.txt b/wheels/dependency_licenses/DAV1D.txt deleted file mode 100644 index 875b138ecf6..00000000000 --- a/wheels/dependency_licenses/DAV1D.txt +++ /dev/null @@ -1,23 +0,0 @@ -Copyright © 2018-2019, VideoLAN and dav1d authors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. 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. - -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 OWNER 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. diff --git a/wheels/dependency_licenses/LIBAVIF.txt b/wheels/dependency_licenses/LIBAVIF.txt deleted file mode 100644 index 350eb9d15ce..00000000000 --- a/wheels/dependency_licenses/LIBAVIF.txt +++ /dev/null @@ -1,387 +0,0 @@ -Copyright 2019 Joe Drago. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this -list of conditions and the following disclaimer. - -2. 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. - -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. - ------------------------------------------------------------------------------- - -Files: src/obu.c - -Copyright © 2018-2019, VideoLAN and dav1d authors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. 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. - -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 OWNER 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. - ------------------------------------------------------------------------------- - -Files: third_party/iccjpeg/* - -In plain English: - -1. We don't promise that this software works. (But if you find any bugs, - please let us know!) -2. You can use this software for whatever you want. You don't have to pay us. -3. You may not pretend that you wrote this software. If you use it in a - program, you must acknowledge somewhere in your documentation that - you've used the IJG code. - -In legalese: - -The authors make NO WARRANTY or representation, either express or implied, -with respect to this software, its quality, accuracy, merchantability, or -fitness for a particular purpose. This software is provided "AS IS", and you, -its user, assume the entire risk as to its quality and accuracy. - -This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding. -All Rights Reserved except as specified below. - -Permission is hereby granted to use, copy, modify, and distribute this -software (or portions thereof) for any purpose, without fee, subject to these -conditions: -(1) If any part of the source code for this software is distributed, then this -README file must be included, with this copyright and no-warranty notice -unaltered; and any additions, deletions, or changes to the original files -must be clearly indicated in accompanying documentation. -(2) If only executable code is distributed, then the accompanying -documentation must state that "this software is based in part on the work of -the Independent JPEG Group". -(3) Permission for use of this software is granted only if the user accepts -full responsibility for any undesirable consequences; the authors accept -NO LIABILITY for damages of any kind. - -These conditions apply to any software derived from or based on the IJG code, -not just to the unmodified library. If you use our work, you ought to -acknowledge us. - -Permission is NOT granted for the use of any IJG author's name or company name -in advertising or publicity relating to this software or products derived from -it. This software may be referred to only as "the Independent JPEG Group's -software". - -We specifically permit and encourage the use of this software as the basis of -commercial products, provided that all warranty or liability claims are -assumed by the product vendor. - - -The Unix configuration script "configure" was produced with GNU Autoconf. -It is copyright by the Free Software Foundation but is freely distributable. -The same holds for its supporting scripts (config.guess, config.sub, -ltmain.sh). Another support script, install-sh, is copyright by X Consortium -but is also freely distributable. - -The IJG distribution formerly included code to read and write GIF files. -To avoid entanglement with the Unisys LZW patent, GIF reading support has -been removed altogether, and the GIF writer has been simplified to produce -"uncompressed GIFs". This technique does not use the LZW algorithm; the -resulting GIF files are larger than usual, but are readable by all standard -GIF decoders. - -We are required to state that - "The Graphics Interchange Format(c) is the Copyright property of - CompuServe Incorporated. GIF(sm) is a Service Mark property of - CompuServe Incorporated." - ------------------------------------------------------------------------------- - -Files: contrib/gdk-pixbuf/* - -Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this -list of conditions and the following disclaimer. - -2. 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. - -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. - ------------------------------------------------------------------------------- - -Files: android_jni/gradlew* - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - ------------------------------------------------------------------------------- - -Files: third_party/libyuv/* - -Copyright 2011 The LibYuv Project Authors. 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 of Google 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. diff --git a/wheels/dependency_licenses/LIBYUV.txt b/wheels/dependency_licenses/LIBYUV.txt deleted file mode 100644 index c911747a6b5..00000000000 --- a/wheels/dependency_licenses/LIBYUV.txt +++ /dev/null @@ -1,29 +0,0 @@ -Copyright 2011 The LibYuv Project Authors. 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 of Google 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. diff --git a/wheels/dependency_licenses/RAV1E.txt b/wheels/dependency_licenses/RAV1E.txt deleted file mode 100644 index 3d6c825c4eb..00000000000 --- a/wheels/dependency_licenses/RAV1E.txt +++ /dev/null @@ -1,25 +0,0 @@ -BSD 2-Clause License - -Copyright (c) 2017-2023, the rav1e contributors -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. - -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. diff --git a/wheels/dependency_licenses/SVT-AV1.txt b/wheels/dependency_licenses/SVT-AV1.txt deleted file mode 100644 index 532a982b3ff..00000000000 --- a/wheels/dependency_licenses/SVT-AV1.txt +++ /dev/null @@ -1,26 +0,0 @@ -Copyright (c) 2019, Alliance for Open Media. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. 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. - -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. From c8d98d56a02e0729f794546d6f270b3cea5baecf Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 10 Apr 2025 16:21:48 +1000 Subject: [PATCH 1622/2374] Added avif to config settings (#8875) --- docs/installation/building-from-source.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 9f953e718c3..9ba389b6699 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -284,14 +284,16 @@ Build Options * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, ``-C lcms=disable``, ``-C webp=disable``, - ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. + ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``, + ``-C avif=disable``. Disable building the corresponding feature even if the development libraries are present on the building machine. * Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, ``-C lcms=enable``, ``-C webp=enable``, - ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. + ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``, + ``-C avif=enable``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Tcl and Tk must be used together. From 75d3f1d3bdaee8b4e44ac1c437866e63ff577069 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Apr 2025 18:41:12 +1000 Subject: [PATCH 1623/2374] Assert palette is not None --- Tests/test_file_gif.py | 2 ++ Tests/test_image.py | 1 + Tests/test_image_quantize.py | 1 + Tests/test_image_transform.py | 1 + 4 files changed, 5 insertions(+) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 20d58a9dda4..7ce6b7c8cd8 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -224,6 +224,7 @@ def test_optimize_if_palette_can_be_reduced_by_half() -> None: out = BytesIO() im.save(out, "GIF", optimize=optimize) with Image.open(out) as reloaded: + assert reloaded.palette is not None assert len(reloaded.palette.palette) // 3 == colors @@ -1359,6 +1360,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None: # Assert that the frames are correct, and each frame has the same palette assert_image_equal(im.convert("RGB"), frames[0].convert("RGB")) assert im.palette is not None + assert im.global_palette is not None assert im.palette.palette == im.global_palette.palette im.seek(1) diff --git a/Tests/test_image.py b/Tests/test_image.py index 7e6118d5280..2a1911517d2 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -673,6 +673,7 @@ def test_remap_palette(self) -> None: im_remapped = im.remap_palette(list(range(256))) assert_image_equal(im, im_remapped) assert im.palette is not None + assert im_remapped.palette is not None assert im.palette.palette == im_remapped.palette.palette # Test illegal image mode diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 0ca7ad86e33..6d313cb8cb3 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -70,6 +70,7 @@ def test_quantize_no_dither() -> None: converted = image.quantize(dither=Image.Dither.NONE, palette=palette) assert converted.mode == "P" assert converted.palette is not None + assert palette.palette is not None assert converted.palette.palette == palette.palette.palette diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 77916929bb5..0429eb99d83 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -48,6 +48,7 @@ def test_palette(self) -> None: im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] ) assert im.palette is not None + assert transformed.palette is not None assert im.palette.palette == transformed.palette.palette def test_extent(self) -> None: From 7b459a8524a1aa7b5180cd868df3a78c6fc8ff4b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Apr 2025 19:33:17 +1000 Subject: [PATCH 1624/2374] Improved reading XPM images --- Tests/test_file_xpm.py | 2 +- src/PIL/XpmImagePlugin.py | 42 ++++++++++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 73c62a44d53..b604f07f5a2 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -17,7 +17,7 @@ def test_sanity() -> None: assert im.format == "XPM" # large error due to quantization->44 colors. - assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) + assert_image_similar(im.convert("RGB"), hopper("RGB"), 23) def test_invalid_file() -> None: diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 3c932c41b84..304f5836170 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -53,24 +53,20 @@ def _open(self) -> None: self._size = int(m.group(1)), int(m.group(2)) - pal = int(m.group(3)) + palette_length = int(m.group(3)) bpp = int(m.group(4)) - if pal > 256 or bpp != 1: + if palette_length > 256 or bpp != 1: msg = "cannot read this XPM file" raise ValueError(msg) # # load palette description - palette = [b"\0\0\0"] * 256 + palette = {} - for _ in range(pal): - s = self.fp.readline() - if s.endswith(b"\r\n"): - s = s[:-2] - elif s.endswith((b"\r", b"\n")): - s = s[:-1] + for _ in range(palette_length): + s = self.fp.readline().rstrip() c = s[1] s = s[2:-2].split() @@ -82,7 +78,6 @@ def _open(self) -> None: if rgb == b"None": self.info["transparency"] = c elif rgb.startswith(b"#"): - # FIXME: handle colour names (see ImagePalette.py) rgb = int(rgb[1:], 16) palette[c] = ( o8((rgb >> 16) & 255) + o8((rgb >> 8) & 255) + o8(rgb & 255) @@ -99,9 +94,12 @@ def _open(self) -> None: raise ValueError(msg) self._mode = "P" - self.palette = ImagePalette.raw("RGB", b"".join(palette)) + self.palette = ImagePalette.raw("RGB", b"".join(palette.values())) - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), "P")] + palette_keys = tuple(palette.keys()) + self.tile = [ + ImageFile._Tile("xpm", (0, 0) + self.size, self.fp.tell(), (palette_keys,)) + ] def load_read(self, read_bytes: int) -> bytes: # @@ -114,11 +112,31 @@ def load_read(self, read_bytes: int) -> bytes: return b"".join(s) +class XpmDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + assert self.fd is not None + self.fd.readline() # Read '/* pixels */' + + data = bytearray() + palette_keys = self.args[0] + dest_length = self.state.xsize * self.state.ysize + while len(data) < dest_length: + s = self.fd.readline().rstrip()[1:] + s = s[: -1 if s.endswith(b'"') else -2] + for key in s: + data += o8(palette_keys.index(key)) + self.set_as_raw(bytes(data)) + return -1, 0 + + # # Registry Image.register_open(XpmImageFile.format, XpmImageFile, _accept) +Image.register_decoder("xpm", XpmDecoder) Image.register_extension(XpmImageFile.format, ".xpm") From 89ac20d2b9690bed2a10195b8acaf405aeaf2fbc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Apr 2025 19:38:13 +1000 Subject: [PATCH 1625/2374] Allow more than 1 character per pixel --- Tests/images/hopper_bpp2.xpm | 390 +++++++++++++++++++++++++++++++++++ Tests/test_file_xpm.py | 7 +- src/PIL/XpmImagePlugin.py | 15 +- 3 files changed, 405 insertions(+), 7 deletions(-) create mode 100644 Tests/images/hopper_bpp2.xpm diff --git a/Tests/images/hopper_bpp2.xpm b/Tests/images/hopper_bpp2.xpm new file mode 100644 index 00000000000..fe97b83baf1 --- /dev/null +++ b/Tests/images/hopper_bpp2.xpm @@ -0,0 +1,390 @@ +/* XPM */ +static const char *hopper[] = { +/* columns rows colors chars-per-pixel */ +"128 128 256 2 ", +" c #0C0C0D", +". c #0A0708", +"X c #1C0A04", +"o c #120B0C", +"O c #170808", +"+ c #0B110D", +"@ c #16120C", +"# c #0D0D12", +"$ c #0D0D1A", +"% c #070A16", +"& c #120D13", +"* c #120E1A", +"= c #1A0C16", +"- c #0D1114", +"; c #0D121B", +": c #091518", +"> c #131215", +", c #14131B", +"< c #1A141C", +"1 c #1B191D", +"2 c #191517", +"3 c #250906", +"4 c #390904", +"5 c #27150A", +"6 c #250A18", +"7 c #251719", +"8 c #361410", +"9 c #342215", +"0 c #0C0C24", +"q c #0C0D2B", +"w c #060927", +"e c #130D24", +"r c #150D2A", +"t c #0C1225", +"y c #0C122C", +"u c #061227", +"i c #151422", +"p c #1A1522", +"a c #1C1B23", +"s c #13132C", +"d c #19172A", +"f c #0C0D35", +"g c #130E37", +"h c #0D1436", +"j c #131333", +"k c #13143C", +"l c #191838", +"z c #241926", +"x c #231B38", +"c c #2E1226", +"v c #372628", +"b c #292538", +"n c #362B37", +"m c #2F2A2F", +"M c #1A2233", +"N c #4C150D", +"B c #740F10", +"V c #512916", +"C c #793419", +"Z c #6D2C13", +"A c #4E1524", +"S c #741624", +"D c #4E332E", +"F c #6F3629", +"G c #574438", +"H c #744831", +"J c #775A2E", +"K c #0E1444", +"L c #141443", +"P c #1B1A44", +"I c #14144B", +"U c #1A1B4C", +"Y c #181747", +"T c #1B1B53", +"R c #181955", +"E c #0F0E44", +"W c #231C46", +"Q c #231C56", +"! c #1C234E", +"~ c #272547", +"^ c #2E2F52", +"/ c #2E3765", +"( c #483947", +") c #742D4A", +"_ c #364970", +"` c #534A51", +"' c #6E534D", +"] c #756654", +"[ c #53556D", +"{ c #6B5B69", +"} c #746B71", +"| c #5E616A", +" . c #880C15", +".. c #881217", +"X. c #8D0D0F", +"o. c #8B3218", +"O. c #8C3828", +"+. c #AC2F30", +"@. c #9A1825", +"#. c #CE202B", +"$. c #8A452A", +"%. c #974A2B", +"&. c #884934", +"*. c #954B35", +"=. c #995539", +"-. c #895736", +";. c #A75738", +":. c #A84E30", +">. c #996839", +",. c #B6683B", +"<. c #AE6835", +"1. c #A35419", +"2. c #D26D19", +"3. c #CC712E", +"4. c #CD6922", +"5. c #A83152", +"6. c #985845", +"7. c #8A5748", +"8. c #AE5A46", +"9. c #916A4F", +"0. c #A96647", +"q. c #B76947", +"w. c #BA744A", +"e. c #B97757", +"r. c #AB6F53", +"t. c #8D736D", +"y. c #B27669", +"u. c #91566F", +"i. c #C56B4A", +"p. c #C8764B", +"a. c #C87856", +"s. c #D47A59", +"d. c #C96E53", +"f. c #C77C64", +"g. c #D17969", +"h. c #D45D68", +"j. c #C52A46", +"k. c #D58932", +"l. c #B38355", +"z. c #968775", +"x. c #BA8667", +"c. c #B38C74", +"v. c #AB9C73", +"b. c #C9845A", +"n. c #D7855B", +"m. c #D39454", +"M. c #E28C5B", +"N. c #F7B251", +"B. c #C78867", +"V. c #D98866", +"C. c #D8956A", +"Z. c #C79878", +"A. c #D89876", +"S. c #CD8C70", +"D. c #E38A68", +"F. c #E5956A", +"G. c #E79776", +"H. c #ED9176", +"J. c #D6A371", +"K. c #E8A379", +"L. c #F3A677", +"P. c #D8A05D", +"I. c #3D65AB", +"U. c #3F67B2", +"Y. c #3B5C9C", +"T. c #506796", +"R. c #72748D", +"E. c #446AAE", +"W. c #4869A9", +"Q. c #4166B2", +"!. c #436BB3", +"~. c #496EB4", +"^. c #476DB9", +"/. c #4A71B6", +"(. c #4C73BA", +"). c #4772B6", +"_. c #5176BC", +"`. c #547BBD", +"'. c #577BB7", +"]. c #5572A9", +"[. c #6B7CAA", +"{. c #505B8C", +"}. c #557CC1", +"|. c #4C73C2", +" X c #897987", +".X c #9F7593", +"XX c #C46B87", +"oX c #5981BF", +"OX c #5884BD", +"+X c #768AB9", +"@X c #7288B5", +"#X c #5C83C3", +"$X c #5D8AC5", +"%X c #6186C5", +"&X c #648AC6", +"*X c #6B8DC6", +"=X c #668BC9", +"-X c #6B8ECA", +";X c #6586C6", +":X c #738DC7", +">X c #6D91CB", +",X c #6C94C6", +" b R.DXPXLXHXHXHXHXCX~ / T.Y.T.T.W.T.W.E.Q.I.E.I.I.E.E.I.I.I.I.I.Y.I.Q.^.Q.E.E.E.E.Q.Q.~.U.U.U.U.U.U.Q.Q.U.U.U.U.U.U.Q.Q.Q.Q.U.U.U.Q.~.~.Q.U.Q.~.^._._._._._._._._.(.(.(.", +"L k f L L k y h T R I L U U L U R R T L E E E R I R I U l XuX' fXV v [ / P h z V Z.G a y l [ 7XCXHXJXHXHXCXb ! {.{.T.{._ _ {.T.W.W.T.T.W.I.I.U.U.I.I.E.W.I.I.Q.Q.E.E.E.Q.Q.~.~.~.U.U.U.U.Q.Q.Q.Q.U.U.U.U.U.Q.~.~.~.U.U.U.U.U.Q.~.Q.Q.Q.~.~._._._.'.`._._._._._.", +"L k f L L k 0 h T T I E U U L T T T U L h h E U R R E U W R.{ D pXF z l L U ^ p F fXD i P W Y ~ n CXHXHXHXFX8Xl W ~ ~ l ^ b b ^ ^ / [ T.W._.U.^.U.U.Q.E.W.W.~.^.E.E.E.~.~.Q.~.~.~.~.~.~.~.~.Q.Q.Q.Q.U.U.U.U.U.U.~.Q.U.U.U.U.U.U.^.~.~.Q.~.~._._.`.`.`.`._._.|.|.", +"k k f L L k 0 h U T L h I U I T T T U k h k E U Q I E U k ` m ' hXV z k U I Q d V fX( j L U W W z VXLX8X XuX( z b x d ` X X` n n b b ! {.W.~.I.Q.Q.Q.E.W.].~.I.~.~.~.~.~.~.~.Q.~.~.~.~.~.~.~.~.~.Q.Q.Q.U.U.U.U.U.Q.U.U.~.~.Q.Q.Q.Q.Q.Q.Q.~._._._._._._._._.|.|.", +"k k f L L k q j U T L h U U I T R T U k h h E I E R I I k b p ' Z.V z k ! T U p H Z.c k U L U W n CXCXn z = c c v 7X8X` 8XPX} c R.tX` n b / {.].W.~.~.E.W.E.~.^.~.~.~.~.^.~.~.~.~.~.E.E.~.~.~.~.~.~.~.~.~.~.U.U.~.U.~.~.~.~.~.Q.U.Q.~.~.E.~.~./././.(.(.(.(.(.(.", +"h k h P L k q j U T L h U U I T R T U h h h E E I R I E k d p ' dXV x P U L L z J fXv l L L P j n IX` m = = 7 ' HXLXKXCX7XKXtXrXLXKXJXqXv n ^ {.T.T.T.W.].E.Q.^.~.~.~.~.^.^.~.~.E.E.E.E.E.~.~.~.~.~.~.~.~.~.U.U.~.~.~.~.U.U.U.U.Q.Q.~.~.E.~.~.E.~.~.~./././.(.(.", +"j k k P Y k q h U U k h U R I R R T U h h h E E R R E I Y d d ' Z.V p L ! ! Y = H Z.v j h ! l b n iXtX7Xa p t.LXZX0XHXPXKXKXPXLXLXCXbXAXVXn m 8XVXeX[.T.W.W.^.^.~.~.~.^.^.^.^.~.E.E.E.E.E.~.U.U.~.~.~.~.~.~.~.~.~.~.~.U.I.I.I.~.~.Q.Q.~.~.~.~.E.~.~.~./.(.(.(.(.", +"j k k U P k 0 y L U k h U R I R T Q T E f E E E E I I I f k z ` Z.V d U T L P z >.B.c j l l l } IXKXKX8Xp ` t.` t.' G ] tXIXIXwX] ' z.t.` { c n PXKXLXUX+X].E.~.~.~.~.~.^.^.^.~.Q.E.E.E.E.U.U.U.~.~.~.~.~.~.~.~.~.~.I.I.I.I.~.~.~.Q.Q.Q.~.~.~.~.~.~.~./.(.(.(.(.", +"j k R.~ k k q q Y T k f Q T I I I Q I L E L E E R I I E f d x ` dXV d T T T U z -.Z.c b s b CXLXKXLXKX} z 7 7 z.hXSXSXz.AXbXmXbX0XJXmXmX` 1 n b iXKXLXLXLXDX@XT.].W.E.(.U.|.^.^.~.~.W.E.~.~.~.~.~.~.U.~.~.~.~.~.~.U.I.I.I.U.~.~.Q.~.~.~.~.E.~.~.~.~.~.~.~.~.^.^.", +"j ~ DX[ W q s q Q I f h Q L R R R R I I L f E I I I I L P d x ` dXV d R R T Y z -.Z.7 0 ` GXLXKXJXLXJX` 7 7 7 t.hXMXmXJXLXJXJXJXJXSXSXmX' n z b 7XKXKXPXKXLXPXBX].T.W./.^.^.U.|.~.~.~.~.~.~.~.^.~.U.U.~.~.~.~.~.~.~.U.U.I.U.~.~.~.~.~.~.E.E.~.~.~.~.~.~.~.~.^.^.", +"r x DXIX~ s $ b L U L L Y Q L I I I T L f L U E T T L k k d x ` dXV x T R U L p 9.Z.7 | KXKXLXLXJXSXZXD v 7 7 D mXMXmXSXZXZXCXZXAXZXdXmXG v n n 7XKXLXKXKXKXLXKXDX[.T.]./.I.}.U.~.~.~.~.~.~.~.U.~.U.U.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.U.~.~.~.~.^.^.^.^.", +"r b CXPX X[ iX[ Y U L k P [.~ k U T U L f f f L I U U k f d x ` dXV z T T U L z 9.x.D LXHXJXJXZXqXqXmXD @ 7 7 9 ] mXbXJXKXKXKXLXJXJXMXv.9 7 7 7 } HXKXKXHXLXJXLXKXDX[.T.W.(.~.^.Q.Q.E.E.E.~.U.U.~.U.U.U.~.U.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.U.~.~.~.~.~.^.^.^.", +"d x VXKXPXGXCX` P U L Y ~ BX| l k P k k k P w k h L L P j d d ( dXZ z P ! L k z 9.B.mXJXJXSX9Xz.t.D 5 5 5 7 7 9 hXmXv.mXLXKXKXKXJXSXv.mX] v c v D t.xXZXJXJXJXLXPXKXUXT.W.E.~.~.Q.Q.E.E.E.E.Q.Q.~.I.I.~.~.E.E.~.~.~.~.~.~.~.~.~.~.~.~.I.~.~.U.U.~.~.~.~.^.^.^.(.", +"x 8XGXPXHXHXtXb k U U k l CXtXd b ~ | {.j q k f P / h k k d d ( dXF < k ! L k z 7.zXSXJXSXt.] V 3 3 X 5 @ 2 c 7 z.v.bXSXAXKXLXLXZXmXhXMX' 7 n 7 9 3 8 ] qXZXJXLXLXKXPX@X].W.I.^.~.~.~.E.E.~.~.~.E.I.E.~.~.E.E.~.~.~.~.~.~.~.~.~.~.~.~.~.~.U.U.U.~.~.~.~.^.^.(.(.", +"uXGXHXLXJXAX} & W Q g g ~ DXCX` [ VXDX[ s j s y ^ eX~ j j d l ( pXF 7 k ! L L z 7.nXJXAX] D 3 3 3 X ' ] 7 1 = 9 t.SXSXMX9XZXJXJXxXmXSXSXJ v v 7 9 ] 9 9 5 ' 9XxXJXHXGXDX{.'.).~.~.~.E.E.E.E.~.~.~.~.~.~.~.E.I.~.~.~.~.~.~.I.I.~.~.~.~.~.~.~.U.~.U.U.~.~.^.(.(.(.", +"iXFXPXLXLXLXyX( k W k ~ b CXGXFXPXGXtXl l s 0 j ^ DX` d d d x D pXF z P T L P z ' AXAXz.5 X 3 9.] 9 5 v.5 G ` 9 J hXhXhXmX9X' ] qXhXhXMX] 9 D 9 G z.5 ] t.8 8 G wXHXPXIX[.T.W.].~.~.~.~.E.E.~.~.~.~.~.~.~.E.E.~.I.~.~.~.~.I.I.I.~.~.~.~.U.U.U.~.U.U.U.~.^.(.(.(.", +"d n } LXLXCXVX[ W W d ` tXHXAXAXHXIX^ x j l w s ` GX7X7 n } ~ D dXH p k ! P k l ` xX8X2 @ 5 7 gXbXhXhXv.hXmXSXmXMX9.J 5 5 V 9 9 9 V G dXhXSXSXdXhXl.MXMXdXV J V 9 wXLXFXtXR.T.T.W.~.^.~./.E.E.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.).~.~.~.^.^.^.^.~.^.(.(.(.", +"d d [ LXVX( ^ ~ k ^ 7XFXLXHXJXHXAX} x l k w l i ` GXCX8XbX Xx v Z.H z k ! P d d i . & @ . 2 7 v z.v.V dXmXdXZ.mXSXSXSXbXt.` 7 D ] bXJXSXSXSXMXSXSXl.hXMXhXmXMXV 5 v xXxX} ^ ! {.W.~.^.U.).E.E.E.~.~.E.E.~.~.~.E.~.~.~.~.~.~.~.~.~.~.).).).).(.(.(.(.(.(.(.(.(.(.", +"f ~ ` PXR.l l j Y ~ { uXFXLXJXHXFXuX~ W f f d a } HXZXxXyXn d n Z.H 7 j P l j r p & o @ @ @ o 7 X 9 D ] V 5 hXbXqXv.] G D ` n ` G ' 0X9XmXmXz.G 9.9.3 8 ] hXgX9XwXv D z > $ l ! W.~.^.U.).E.~.E.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.^.~.)./.(././.(.(.(.(.(.(.(.(.(.(.", +"g W ^ DX( l l q L W r b ` CXLXLXPXPXR.k k ^ | 8XCXHXbXxX{ < d v Z.J 7 j l d r * . > 2 o . @ 2 = 7 X X 5 D 5 5 5 9 9 9 @ 7 7 2 v 7 7 v 9 v V D G qXgXxXD 3 3 ' z.D 9 2 > 1 # d u Y.W.~.~.).E.W.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.^.~.)././././.(.(.(.(.(.(.(.(.(.(.", +"U U / eXP P l f L L d r b CXPXR. XUXDX/ k ~ ` 7XCXZXZXyXv p k v B.-.o d l i * * & o o 2 @ . & . < & o 7 o 7 2 @ @ @ 7 2 v < z < z 7 2 7 9 7 5 9 ] z.ZXCX` 7 5 X @ @ & . o # % u _ W.E.E.E.E.E././././././.~.~.~.~.~.~.~.~.~.~.~././.(.(./.(.(.(.(.(.(.(.(.(.(.(.", +"f U ~ / L U k f U Y k x d DXVX~ x W {.[ f d 2 7 t.ZXZXxX} x k z x.-.3 d a $ & & . 2 o . @ . # p # , & . > & o z 7 2 2 2 o & < & < . > 7 > 7 7 7 v ' m 7 @ 2 . @ . + . > . > % y _ W.E.E.E.E.~./././.(.(././.~.~.(.(.~./.~.~.~.~.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"I T P P ^ ~ k k L U q l ~ DX{.W W Q Q ~ d * * o { HXxXVXCX^ q z x.>.5 i , # & & > o > . + + > . # . * * . < & . o o 2 7 & 2 2 2 > > < > 2 7 o o @ . @ . o . . + . . # & . . ; u / ].W././././.(././.(.(././././.(.(.(.(.(.(././.(.(.(.(.(.(.(.(.(.(.(.(.(.(.}.}.", +"E Q L P Q Q P f f L k k ^ BXU ~ P W T Y j i * * XFX` b 7XR.l 7 l.>.7 , # # < o o . > . - - - $ # # . & , . & . o o o . . o . o o . . . . . o o o @ . . . . 2 . + . - . > > . w _ ].W./././.~.(./.(.(.(.(././.(.(.(.(.(.(.(.(.(.(./.(.(.).).(.(.(.(.(.(.(.(.(.}.", +"I U U U T Q k h L L h P Q / T L U T T U j 0 0 r 7XuXd r d ^ l < r.0.5 ; - - - & . > # # - + % # . . + . # # . . . . . . . . . . & # . . o o o o . o o . . . . . + . # # . > ; w _ ].]./.E.(.(.}.(.(.(.(./././././.(.(.(.(.(.(.(././././.(.(.(.(._.(.(.(.(.(.(.(.", +"L U U U T Q k q k L k P U Q U U U T T U k q q r X( j d 0 t y 7 r.0.@ ; + - - & . > & . . - - , - + + + . . + > . . . . . . . . . . . . o o o o o o o . . . . . . . % . . . - t / ].].]./.`.(.|.(.(._._.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"L U U T T Q k q k k k P I I I T R R T U L f q f / L W q w s s 2 0.r.X ; % - # # > > . # > + . . . . . @ @ o . o o o o o o o o o o o o o o o X X o o o o o . # # - . # > o . # w ^ ].].'./.`.(.}.(._.`.`.`._._._.`.`.`.`._.(.(.(._._.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"L U I T R Q j q k k h L I I R T R R T U L f q f E T U f j j 0 7 0.l.X ; % - # . . # . . . . o @ X X X X X X X 3 3 3 3 3 o o 3 3 o o 3 3 3 3 3 3 3 3 X X o X o o . . . & o o - % ! ].].'./.(.(.}.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.(.`.`._.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"k U U T R Q k q l k f L T I T T R R T U k f 0 q I R E L q q q 7 >.l.X , % + . . . & = o @ X X X 3 4 N N N V V V V N N N N N N N N N V V F F H H H H F V 8 3 3 3 o . & o . . > % P ].].'./.'._.}.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.(.(.(._._.`.(.(.(.(.(.(._._.", +"k P U T R Q k q k k k L T I T T R R T U k q 0 q I I T f w j j 6 >.r.3 - . + + . > . X @ X X 3 N F 6.r.y.y.y.y.y.y.r.r.0.6.7.6.7.7.6.6.0.r.y.B.B.y.y.x.x.y.7.V 3 X . & & @ . # % h ].].'.'.'.`.}.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`._._._._.(._._.}.", +"k P U T Q Q k q f l k L T E T T R R R U j 0 0 y k E I L k j q 7 0.<.3 # . + . . o o X X 3 9 ' c.Z.A.aXaXaXaXjXjXjXA.A.A.S.B.S.B.S.S.A.A.G.K.K.G.A.C.C.Z.A.Z.r.' 9 o X o o @ - ; u ].].'.'.'.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.}.`.`.`.`.`._._._.}.}.", +"d W U R R Q k q f k g L T I Q R T R R U j 0 0 y k L I I f f f 6 r.>.3 # . . . . X 7 7 8 G t.pXaXaXA.A.fXzXzXzXlXzXjXzXlXzXzXzXzXkXzXkXzXlXlXzXjXK.K.K.A.A.K.Z.c.t.G 5 3 X o . t u '.'.'.'.'.'.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.`.}.}.`.`.`.`.`.`.}.}.}.}.", +"` b U Q R Q k q q l L L L T T R T T R U k 0 0 t j U I E L f f 7 r.r.X . . @ o o v ' F H y.Z.fXfXK.jXjXzXzXzXzXlXcXlXzXlXzXzXnXcXzXlXlXlXzXnXnXcXlXjXzXA.jXA.J.B.c.t.-.D 3 X & % K '.'.].'.'.oX`.oX`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.}.`.}.}.}.#X#X", +"xXz W Q I _ ~ y j j f U I I U U U R T U k q 0 t k U I L L f f = 0.l.X @ @ . . 9 ' ' H 0.B.A.fXfXjXjXzXzXzXlXcXcXzXzXlXnXcXzXzXzXcXcXlXcXzXzXzXzXzXcXjXA.G.A.C.B.Z.y.e.-.D o @ % K '.'./.'.'.'.oXoXoX`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.}.}.}.}.}.}.}.}.}.", +"HX7Xx ^ eXBXM $ l x Y U R R I R I I T U k q 0 y k U I I k j j = 0.l.X > o o 7 D ' H 7.y.B.A.fXfXjXzXzXzXzXzXjXjXzXzXlXnXlXzXzXzXjXzXzXsXsXD.B.e.x.x.S.A.B.B.S.Z.Z.c.l.e.' 7 @ , ! [.'.`.'.'.oX#XoXoXoXoXoXoXoXoXoXoXoXoXoX`.`.`.`.}.}.#X#X}.#X#X}.}.}.#X}.#X}.}.", +"AXZX{ CXPX| d 0 ` R.d Y U R I U E L R U k q q y L U U U k h l o r.l.X > . o v D H H r.x.l.B.A.A.B.B.B.e.e.e.B.S.jXzXzXlXzXzXcXzXfXjXjXD.D.a.q.e.r.e.Z.A.jXA.Z.Z.Z.c.e.e.9.G 5 # ^ [.'.`.'.'.'.'.oXoXoXoX#X#XoXoX#XoXoXoXoXoXoXoX#X#X#X#X#X#X#X#X}.}.#X#X#X#X#X#X", +"ZXHXHXGXVXb i i ` FX^ W Y g ~ P k L U I k q q q L U I U k q y @ >.l.X $ & 7 9 F ' 7.r.e.x.Z.A.A.A.V.a.a.a.f.B.A.A.jXzXzXzXzXlXzXzXfXA.D.D.8.*.=.*.6.r.B.fXaXfXc.c.Z.B.9.t.` v 7 ^ @XoX'.'.#X%X}.#XoXoXoX#X#X#XoXoXoXoXoX#X#X#XoX#X#X#X#X#X#X}.}.#X#X}.}.#X#X$X$X", +"HXAXZXFX{ x b # { FXrXx x {.eX^ k L U L k q 0 q f L E E f 0 t @ >.<.X , & 7 G 7.7.9.r.x.Z.Z.S.S.B.e.0.6.6.0.0.0.B.A.jXK.A.jXlXzXjXfXA.g.i.:.%.*.O.Z F Z Z F F F H ' -.7.t.` v c [ @X@X'.oX=X#X#XoXoXoX#X#X#X#XoX#XoXoX#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X;X;X", +"ZXAXZXGX` b & & ` ZXHX} uXGX8Xl k L U L j r r 0 r W Y f q 0 i 5 w.>.5 , . 9 7.9.-.-.9.c.c.x.B.B.x.r.9.7.7.6.r.y.r.B.A.jXG.jXlXlXzXK.B.e.0.=.C N Z &.6.*.&.H N V t.' V F ' { v v [ @XoX'.oX#X`.#X#X#XoX#X#X#X#XoX#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X$X", +"AXJXHXFXIX^ x = ] ZXAXAXCXVXb j k L U Y d i & & x ^ ~ f q q i X 0.<.5 & 7 D 9.9.-.J H ' G V V N 4 8 N N N N V Z $.r.G.lXlXzXlXnXlXK.b.0.C C 6.B.r.y.y.S.r.c.SX9.8 V A D ` ' D D R.eX+X%X$XoXoX$X#X#X#X#X#X#X#XoX&X#X#X#X#X#X#X#X#X#X#X#X%X%X#X#X#X#X#X#X#X#X#X#X", +"gXAXCXFXPXuXz D bXAXSXZXCX{ b k L L Y W r ` n v 7XtXx j w r r 3 0.>.8 2 9 ] y.9.H F 8 D D V N 0.cXaXy.r.r.B.S.*.O.Z <.n.kXkXL.L.F.p.;.0.jXy.9.V N N F F r.r.c.MXD V ' F D ' u.' XR.+X%X$X$XoX#X#X#X#X#X#X#X#X#X#X#X%X&X&X%X#X#X#X#X#X%X%X%X#X#X%X#X#X#X#X#X;X;X", +"z.ZX` ( { eX{ n CXZXZXZXxXm x k L U Y W r ` } z.iX` r w r r 0 3 0.>.8 D G 9.r.-.F V 3 8 3 N r.y.y.7.F V V N &.B.a.w.p.p.F.F.F.b.p.,.,.q.0.6.V H N V V N C $.H C H F V N V H r.9XgXrX[.*X&XOX&X=X%X%X%X#X#X#X#X#X&X&X&X&X&X&X%X%X%X%X%X%X%X%X%X#X#X#X#X%X%X;X=X=X", +"z.xXx ~ f x x z t.ZXAXAXCX} x Y U T U k p v ] ] } z r r 0 r s 6 r.r.8 D 9.r.6.C V V D D 8 7.9.r.' V N N N N Z F 0.;.;.s.n.p.a.p.3.1.p.p.;.Z O.V 4 N N N B o.o.%.0.-.H H &.r.f.6.y.yX@X:X'.oX-X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X%X%X%X%X#X%X%X%X&X=X=X=X", +"} } k W U k 0 & v CXAXCXGXCX~ f L T U k z D t.] 9 o p d w r d = >.l.N D c.e.0.6.F H V F V H F 6.V N V 4 4 N &.6.C O.%.s.i.F.zXkXF.n.M.s.;.i.8.=.&.O.F O.%.%.;.q.B.e.b.w.;.;.q.o.Z 0XeX:X[.@X-X'.&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X%X=X=X=X=X=X%X%X", +"[ ( Q L I U r * 7 CX8Xv } iXR.j L T U k r ( } t.{ . r r s 0 r 6 >.w.N ' y.$.$.=.6.6.6.r.6.$.O.O.O.C O.O.=.=.=.w.d.n.n.M.:.G.lXlXL.,.H.H.D.d.q.g.a.q.0.,.q.d.s.p.a.b.C.w.<.;.d.s.Z t. X[.:X;X;X=X&X&X&X&X&X&X&X&X=X=X=X=X&X&X&X&X&X&X&X=X=X=X=X=X&X=X=X=X=X;X;X%X", +"~ k L T T U 0 $ b CX` x x x ^ k L L U P d ( ' } 7X` r r s s 0 o 0.w.4 7.6.O.8.8.0.6.B.B.0.;.o.o.o.o.:.s.G.V.s.V.s.H.kXF.%.G.lXlXlXF.H.L.D.s.H.G.kXzXsXD.s.D.F.n.V.V.V.w.<.n.V.a.O.y. X[.:X;X;X2X=X=X=X=X=X=X=X=X=X=X=X=X=X&X&X&X&X&X=X=X=X=X=X=X%X=X=X=X=X=X=X=X", +"f k Y U L k 0 $ b 7Xj W Q L k q L U U Y r p 2 7 [ } d d 0 s t o r.r.4 F &.o.q.8.=.;.a.C.w.w.:.O.O.;.d.sXsXkXH.s.i.L.lXs.q.kXlXzXlXK.a.kXkXV.a.G.L.H.D.M.p.D.F.V.A.A.A.B.q.D.kXV.%.y.eXoX@X*X$X=X=X=X=X=X=X=X=X-X=X=X=X=X=X=X&X&X&X&X=X=X=X=X=X=X=X=X=X=X=X=X=X=X", +"k j L k P P q 0 x ^ Y Q R R L k h U U k j * # * p x k l q 0 t @ >.w.8 V 6.C a.g.q.%.w.n.p.p.d.q.:.i.i.V.s.i.q.d.lXkXH.,.V.lXlXlXzXlXs.G.K.lXkXn.n.i.p.i.p.p.G.C.B.V.B.B.a.q.V.A.=..X@X*XoX-X=X$X=X=X=X=X>X-X-X>X-X-X-X-X-X=X=X&X-X-X-X=X=X=X&X&X=X=X=X=X=X=X=X=X", +"j k L U U k q 0 j j Y U U L L L Y W L k y w ; $ 0 q h k q y y o >.w.4 8 r.O.V.s.8.%.d.s.n.p.n.q.%.i.p.a.a.f.sXlXlXzXF.q.kXlXnXcXnXnXjXB.zXlXlXnXlXkXG.V.V.G.L.F.V.n.b.b.8.o.i.D.r.t.@X+X@X*X-X>X>X-X*X*X*X-X*X*X*X*X-X>X-X*X&X*X-X-X-X-X=X=X&X&X=X=X=X=X=X=X=X=X", +"j k L U U k q 0 j f k Y Y k k L U W L k y u t 0 w j f f k y s 3 0.l.3 V r.=.D.s.:.%.i.i.n.n.V.q.,.s.G.kXlXnXnXcXlXnXb.C.zXlXcXnXnXlXnXS.G.zXlXlXlXlXzXK.K.K.F.V.V.n.,.C.d.o.;.S.c.8XeX:X@X;X-X=X>X-X*X*X*X-X*X*X>X-X-X-X-X*X*X*X-X-X-X-X=X=X=X=X=X=X=X=X=X-X-X=X", +"j k L L P j 0 0 j k L P Y k k L U U L h y 0 r r f f f f k w i 5 >.w.V 9XcXe.V.V.%.q.s.i.p.n.b.w.:.,.L.nXnXnXnXnXnXlXq.jXlXlXcXnXnXlXlXjXq.sXzXzXlXlXK.F.G.F.V.n.V.p.w.C.sX8.q.f.9X8X+X*X*X-X2X-X>X-X-X-X-X-X-X-X>X-X-X-X-X-X*X>X-X-X-X-X-X=X=X=X=X=X-X-X-X-X-X-X", +"h k L L P j 0 0 j k Y U P Y k Y Y U k h y w r r j r j f y t r X C C.c.hXcXA.K.p.w.A.C.q.<.p.a.b.p.i.F.lXcXnXlXzXjXq.q.G.kXlXcXlXnXnXnXzX0.:.s.H.G.G.G.K.kXkXF.n.a.e.b.C.kXD.q.y.0XeX+X*X*X-X-X=X>X>X-X-X-X-X-X-X>X-X-X-X>X-X-X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"h k L L k s $ $ y j L Y Y L L Y Y U k h y 0 r r r f k r r d & 9 w.C.c.hXcXA.G.n.C.jXG.q.<.p.w.b.D.i.p.D.kXkXV.i.;.:.D.H.kXlXnXcXcXlXlXlXsXi.8.8.:.;.a.G.G.G.F.a.a.a.l.C.kXs.q.S.8X@X:X>X-X-X>X=X>X-X-X-X-X>X-X-X-X-X*X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"k k L L l t $ $ y k L Y L L L Y Y P k h y 0 r r r f f s r # 7 t.J.P.c.hXMXfXC.G.C.K.K.a.q.i.w.p.F.M.i.:.%.;.;.:.o.s.kXH.kXzXcXlXlXlXzXzXsXH.8.H.H.H.sXkXD.V.V.a.b.e.B.A.G.q.V.B.8X@X*X-X-X-X2X2X>X>X-X-X>X>X>X-X-X-X-X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"k L L L l t % $ y k L I P P L L L Y h h y y s q r r r s * & ' hXK.J.x.hXcXx.B.K.C.A.J.b.i.i.p.b.a.F.D.s.V.H.V.i.:.H.D.V.G.sXzXzXjXG.sXV.s.s.:.q.H.kXkXH.D.n.n.V.b.e.B.A.b.V.G.c.eX:X>X-X&X*X*X&X>X>X-X-X>X>X>X>X>X-X>X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"k k L k j t # ; y k L I L L L k g L f h y y s 0 q r s r * D wXgXJ.P.x.cXMXl.b.C.jXA.C.e.q.i.a.b.p.n.V.H.sXkXV.%.o.;.O.%.q.q.f.B.B.f.8.:.Z O.B O.s.G.H.D.H.V.V.C.B.e.B.0.C.K.s.pXeX&X=X-X,X,XX>X-X-X>X>X>X>X>X>X>X>X>X-X-X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"f P k f ~ 0 $ ; j w I T L L f L Y P k y y y y % 0 w y i ` 0XgXhXP.P.x.cXhXl.x.A.b.b.%.w.p.i.a.n.p.n.V.D.G.V.:.o.V.q.Z Z C o.$.$.$.O.=.o.%.g.:.B :.s.g.s.V.n.V.C.V.B.e.q.q.w.A.9X+X-X=X>X*X*X*X,X*X>X>X>X>X-X>X>XX>X-X-X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X2X3X3X", +"d j f W s s $ % s k g L L k h g g k f y t t t t w y t | tXwXgXfXP.P.B.cXhXc.x.A.C.B.C.K.V.i.p.n.n.a.F.V.V.i.o.i.G.G.V.V.V.0.o.N C 8.g.V.g.D.i.Z B ;.d.s.s.s.V.C.B.C.kXG.K.C.fX0X+X=X=X-X*X*X*X:X*X>X>X>X>X-X>X>X>X>X>X-X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X>X>X", +"p d W / d $ i i 0 k P U P k L k g f q q i i $ i $ d 8XiXrXgXgXfXP.P.B.cXhXZ.x.S.jXA.jXkXsXa.p.n.F.n.n.V.i.O.:.D.H.D.H.V.H.H.g.q.a.V.s.V.s.a.V.q.B o.:.,.i.s.V.C.V.C.jXzXC.fXgX8X+X*X-X>X,X*X:X,X*X>X>X>X-X-X>X>X>X-X-X*X*X*X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X>X", +"( m X[ d s $ $ l q j k k k h f g k j 0 i , & * x 7XiXtXyXqXgXdXP.P.B.cXhXZ.r.e.jXzXjXlXjXa.b.V.C.n.a.p.%.o.s.D.H.H.G.H.G.G.sXH.sXH.V.V.D.V.H.s.O.B O.;.a.V.s.b.b.A.zXjXK.dX0X8X+X*X-X>X,X:X:X,X*X>X>X>X-X-X-X>X-X-X-X*X*X*X*X*X-X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"qX0X8Xx r r * $ 0 s j j j f g k g j j r * & 6 D XrXuXyXyXgXgXfXP.P.B.zXhXZ.9.=.A.jXjXzXr.e.V.b.F.b.n.<.o.:.s.D.G.sXsXkXsXkXlXzXzXsXsXD.H.G.G.V.s.O.o.:.i.s.V.C.B.B.zXjXB.c.7XeX+X*X-X>X>X*X*X-X-X>X>X>X-X-X-X>X-X-X*X*X*X*X-X-X-X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X", +"mXqX} z p z $ * b ` b j l w k k h f s i = 6 A u.rXrXyXxXqXqXxXfXJ.P.B.zXhXZ.-.H >.B.A.0.N -.e.C.C.C.n.:.o.p.d.H.G.sXG.kXsXkXlXzXzXkXsXsXsXG.G.V.V.i.o.d.n.V.F.b.B.*.r.B.e.9XeX@X+X*X-X-X-X*X*X-X-X-X>X>X-X-X-X>X-X-X-X-X-X-X-X-X*X*X*X*X-X-X*X*X-X-X-X-X-X-X-X-X", +"hXqXD z z e * ( R.[ d $ d s q q h h j r 6 c A u..XvXxXyXqXqXxXdXP.P.x.hXzXZ.7.H -.0.0.Z 4 H w.V.V.G.V.,.:.s.s.D.s.a.q.d.i.d.H.H.g.s.i.s.s.a.s.D.s.V.;.s.H.F.G.V.e.C F 6.fXwX+X+X+X+X-X-X-X*X-X-X-X-X>X-X-X*X-X>X>X>X-X-X-X-X>X>X*X*X*X*X*X*X*X*X-X-X-X-X-X-X-X-X", +"bXqX( z = p & ` } p d d 0 s q k q h j $ 6 c A &.y.gXxXxXyXqXgXhXP.P.B.fXzXZ.9.H H =.x.9.V V e.C.C.F.kXs.i.d.s.d.%.o.o.o.o.o.:.:.o.o.o.o.o.o.o.:.q.d.D.F.kXF.n.B.B.C *.c.c.z.eX+X:X:X-X5X-X-X>X>X-X-X>X-X-X-X-X>X>X>X>X-X-X>X>X>X-X*X*X*X*X*X-X*X*X-X-X-X-X-X-X-X", +"ZXxX.Xz z = . ` n x d s l q q h f j s r = A S S r.9XgXyXxXqXgXfXC.F.m.fXfXe.6.H F V D D V N 0.b.V.G.L.L.i.i.:.O.o.%.:.;.8.8.+.+.:.d.H.D.s.D.n.a.i.s.n.L.L.F.S.Z.e.H -.y.qXtX@X,X*X-X5X5X5X>X>X>X>X,X,X,X,X,X-X-XX,X*X>X>X*X*X-X-X-X>X-X-X-X*X*X-X-X-X-X-X-X2X", +"0XyXuX( = p . { ^ * d j j h y s r j s r 6 A S B 6.pXgXyXgXgXgXfXF.F.m.jXZ.0.>.;.Z N 3 9 v V =.b.b.n.G.L.V.p.a.p.s.s.g.H.sXsXH.sXsXH.G.sXH.D.s.n.V.p.F.L.F.n.A.B.9.H 9.c.yXtX+X+X-X2X5X5X5X-X>X>X>X>X,X,X>X>X-X-X>X,X,X*X*X*X*X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X>X", +"` = { } z & < 7X{ * d r q j 0 s s r y r 6 A S B ..g.gXgXgXgXgXfXF.P.B.fXZ.>.;.,.<.Z N N N N F w.p.n.L.F.n.p.M.n.s.n.n.V.H.sXsXH.H.D.H.V.d.p.n.F.F.p.F.L.n.n.B.x.-.H 9.0XtXeX+XX>X>X*X,X>X>X>X>X>X>X>X-X-X-X*X*X,X*X*X*X*X,X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X>X", +"z p r z r * & } Xx d r r s 0 s s s t * 6 N S B B r.gXgXgXgXhXfXP.P.B.J.Z.0.,.<.3.3.<.Z N N Z 0.a.b.V.F.M.3.n.M.s.p.p.p.i.q.8.;.:.:.8.q.p.D.F.V.V.a.M.M.M.a.B.r.J -.t.8X7X@X:XX>X,X,X,X>X>XX>X>X>X>X-X-X*X*X*X*X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"i d r s r * $ < } b d r d r 0 d y s i & 3 N B B B O.aXgXgXgXhXA.P.P.l.J.Z.0.,.<.3.2.k.k.<.Z N *.w.p.p.F.n.p.n.n.F.D.n.d.,.;.;.;.;.:.;.<.p.n.n.p.V.V.n.n.s.a.y.7.7.9.} 8XeX:XX>X>X>X>XX>X>X>X>X>XX*X*X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"y q j q $ r * * b x d r d q i i t i = 6 N B B B B ..h.aXgXgXhXZ.P.m.b.J.A.>.<.<.3.2.2.2.3.1.B *.q.n.p.p.,.n.D.s.p.s.s.a.V.G.G.G.D.V.V.a.p.p.p.n.C.V.s.D.i.a.y.H 9.t.} eX+X1XX>X>X,X>X>XX>X>X>X>XX>X*X>X>X>X*X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"l w h l s $ . & $ i s q j q y 0 t * 6 A B B B B B .+.aXaXaXgXZ.P.m.b.K.A.<.,.3.4.2.2.2.4.1...O.d.w.q.a.n.n.D.F.F.D.V.H.zXzXzXzXzXL.kXkXL.L.L.L.C.a.n.d.s.q.B.' } 8XeX+X+X,X,X,X,X,X,X>X2X3X3X3XXX>X>X>X>X>X>X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"y l h s $ $ * & i 0 j f h q t ; , o 4 A S .B B B .. .g.aXaXgXZ.m.P.m.G.C.q.3.4.4.2.2.2.4.4.o.o.s.q.p.p.V.a.n.V.s.D.D.D.L.lXlXlXzXH.kXkXL.L.F.L.B.b.a.;.a.e.c.t.} eXeX+X>X>X,X,X,X,X,X2X2X3X3X3XX>XX>X>X>XX>X>X>X-X-X-X-X-X-X-X-X-X-X-X-X", +"y h j s r $ # # i t q y h q t , = c A S . .B B .X...+.aXpXpXx.k.N.m.n.n.,.p.4.4.2.2.2.4.3.Z B e.b.q.w.p.b.p.n.n.M.n.n.F.F.L.kXH.G.kXzXkXL.C.C.e.0.;.q.q.r.y.t. XeX:X1X3X2X1X,X,X,X,X>X2X2X3X3XX>XX>X>X>XX>X>X>X-X-X-X-X-X-X-X-X-X-X-X", +"y s s s r $ # # $ d l q q q i = c A S ..X.X.B B .X...@.f.S.aXe.k.N.k.p.p.<.3.4.3.2.2.2.3.,.Z F A.b.;.;.w.p.,.p.n.n.n.p.n.n.F.V.V.D.G.A.F.F.n.e.0.$.%.o.%.x.y.D 7X[.+X5X3X3X-X>X,X,X,X,X>X>XX>X>X>XX>X>X-X-X-X-X-X-X-X-X-X-X-X-X", +"r s s i r $ # # ; d j q r d = 6 A S S ..X.X. .X. .....B :.S.aXe.k.N.k.<.<.<.3.<.2.2.2.k.<.$.8 gXaXB.r.%.$.%.%.;.a.n.b.a.s.e.e.q.e.g.B.f.a.a.q.=.F C Z C r.Z.t.o ^ R.+X5X3X6X6X2X>X,X,X,X>X>XX>X>XX>X-X-X-X-X-X-X-X-X-X-X-X-X", +"r s s r r $ # - ; i q r r 6 6 A S S ....X.X. . . ..... .o.g.S.b.k.N.k.<.<.1.3.3.k.2.2.3.;.4 9 AXc.S.f.*.Z N Z C =.0.q.0.=.$.&.=.6.6.6.=.=.%.%.o.Z N Z r.B.Z.t.X < M T.:X5X3X3X4X3XX>X>X>XX>X-X-X-X-X-X-X-X-X-X-X-X-X", +"s s s r r $ # - ; i r r 6 6 N S S .X.X.X.X. .B . . . .B i.V.b.k.N.k.<.<.:.3.3.3.4.k.<.4 X t.bX9.S.f.e.$.N N N Z F F Z V N V F F V N N Z Z C o.Z Z 0.B.B.Z.t.@ > $ y {.NX3X3X-X3X3XX>X>X>X>XX>X>X-X-X-X-X-X-X-X-X-X-X-X", +"j j j r 0 $ # ; i p p 6 8 A S .. .X.X.X.X. . .B B B ..B X.:.D.,.k.N.k.1.:.%.p.<.,.<.-.8 X X wXgXH S.f.f.r.C C Z Z Z N 4 4 8 8 4 4 4 4 4 N C $.$.F 6.f.a.Z.Z.t.o . - $ y [ +X1X6X3X4X4XXX>X>XX>X>X>X-X>X>X>X>X2X2X2X2X", +"j j j q 0 $ # ; , * c c A S .. . .X.X.X.X. .B B B B .. .X.+.D.;.k.N.N.1.1.%.w.%.0.V 3 3 = & xXgXZ A.B.e.r.*.&.&.&.H V N 8 8 3 3 3 4 N V F =.6.*.*.0.e.B.B.pXz.X < # # ; % ^ @X,X>X3X4X3X1XX>XX>XX>X>X>X>X>X>X>X2X2X2X2X", +"j j j q 0 $ # - , z ( ) S S ..X. .X.X.X.X.X.B B B S B ..X.X.g.;.m.N.P.1.%.$.r.H 3 3 @ o * . CXmXV C.B.e.e.;.$.6.=.7.F V N 8 8 8 4 N V F &.6.6.*.6.f.e.B.B.mXt.o # , & . - + h _ @X:X1XNX1XX>XX>X>X>X>X2X2X2X", +"s s d w r $ # . 7 } vXXXS ..X.X.X.X.X.X.X.B B B B B B X.....f.;.m.L.P.-.Z N 3 X o o # # # & CXZXF e.B.e.e.;.;.=.6.6.&.F Z V N 8 N N Z C =.6.*.*.0.f.e.B.hXZX} ; # # & & # # ; u w ! {.+XNX,X,X4X1XXX3X4X4X1XX*X*X*X-X.Z 3 X X . . # # # # > @ CXZXF 0.A.e.0.q.;.=.;.=.O.O.C Z V V F C O.%.6.;.%.;.e.e.B.Z.SXAX| % # # # # # # # # ; t u h _ +XNX+XX>X4X4XX , i t t y h h ^ T.+X+X+X1X1XX>X>X # # # # # # # # & # # i d i r t h w h {.eXNX1X1X,X,X1X1X1XX4XX,X,X & o XFXHXt.&.f.a.,.w.<.q.0.=.&.*.*.=.8.;.:.;.0.q.;.;.e.0.pXAXZXPXVXo & # # # # # # # # & & < < , * , a i d y y l / T.+XNX1X1X1X,X,X1X4XX>X>X-X5X > > + + + . . . & # # | CXHXZXF 0.s.,.<.w.w.;.=.&.=.*.;.8.;.:.;.w.q.;.0.r.r.ZXAXGXGX} = o # # # # # # # # & & & < < < , < , i i i t y h h ^ _ ].+X1XNX1X,X1XX2X>X-X-X5X5X5X5X", +"s q q 0 , o 9 9XgXgXxXyXxXCXgXD 3 o o @ + + + . - . # - > # , . > . . . @ . . . . . . # # , | IXHXHXt.F e.a.,.<.w.;.=.&.=.%.;.8.;.;.;.q.;.=.=.-.hXAXHXGXGX` o & . # # . . . # # & < < * , < < > < < , , i d s y h l h h ! [ @XNX1X1X1X1XX-X3X2X>X-X5X-X5X-X", +"j q s 0 * o 9 9XgXxXxXyXtX X7 o o o . + . . . . & # # - - - - + + . . . o o . . > . # . # # ` IXFXLXZXF =.e.p.<.w.q.=.*.*.o.;.;.;.=.,.q.;.*.F c.ZXJXHXGXFX< & . . . # . . # # # # # * , i i , , < < < p i i i i s d l l y q l ! T.NX1X,X,X1X*XX>X2X2X-X-X5X5X", +"y y r r = o 5 9XqXbXwX7 2 2 . 2 & & & > # # & o & & # + + . . . . . . . o & & & # # , > - # n GXFXHXJXmX&.r.w.p.w.e.=.$.=.C $.$.$.=.%.w.;.C 9.ZXHXHXLXGX7X& $ # . . # . . . # # * , , , i i i i < < < < < p p p a i i d d s s j h ! T.NX1X,X,X*XX2X2X-X-X5X5X", +"0 s r r p & @ qXbXyX7 2 o & > & & = & & & # & # . . . . o o o o . . . # # & * * * * * , # & z FXPXHXLXAXZX6.a.s.e.e.6.*.=.C $.*.$.O.0.0.$.7.AXHXKXPXPXGXD . $ # . . . # # # # # * ; ; ; i i i i , < p p < < < < z < < p a p i r y h L _ @XeX1X,XX-X-X-X-X", +"i i $ r * & o wXxX` o < . # # # = = & & & & # - . . . . o o o o # # # # $ $ $ * * * * & . & 2 tXFXLXPXLXJXcX6.g.A.e.r.0.%.C *.&.$.&.e.0.H SXHXKXKXLXPXCX2 . # # & # # # . # # - # $ ; ; i t i i i i i i < < < < 7 < < < < < < p d s y h h ^ [.1X:X5X5X5X-X-X-X-X", +"p = * * * & o 0XyXo 7 . $ 1 # > & & & & & > - - . . . . o o o o # # # $ $ $ $ $ $ # * * & & & ` FXGXKXKXLXJXpX6.B.B.b.e.=.$.r.&.&.y.r.H SXLXLXKXKXKXLX X& & & & > # # # # - - - # - ; i i i i i i i i i , , < < < < < < < < p a a x i t y h / NX:X5X5X5X-X-X-X-X", +"v = * i # & = 0X' o . 1 , . & > - - & & & > & - . . . . . . . . # # # # # # # # # # & & , & < < xXFXPXKXKXLXJXc.r.B.A.B.e.0.y.0.r.y.7.AXLXLXKXKXKXKXPXn , > & > > > # # - - > > > , , i i i i i i i i i i , p < p , < , p i , , & & z i t y u [.:X > . + - & & & & & & # # # # . . . . . o o . . . . + + + + . - # p * t.GXPXKXKXKXJXJXx.e.B.B.x.e.y.e.e.c.AXLXLXKXKXKXKXKXtX# , > > > , ; # # - > > > > > > , , , i i i i i i i i i p i i , i i i t $ 7 o 7 > $ s u _ :X5X5X5X5X-X-X-X", +"yXD & > # # o } o X > > - # o @ + + o o & & & & . # # # # # # . o o o o . . . . + . . . - # , p ` GXGXKXKXLXLXJXAXx.Z.fXMXnXcXcXcX9XAXKXKXKXKXKXKXLX| , , # < & , ; # - - > > > > > > > , , , i i i i i i i i i , i i i * t i i = 2 & > a % y K :X5X5X5X-X-X-X-X", +"xX8X3 o o o 2 n o o > > > > & & > - # & o . & # . . . # # # # . # o & o . . . . . . # # # & , , m IXGXLXKXKXKXKXLXJXHXAXAXZXxXwXD 5 D FXKXKXKXKXLXCX, , 1 > . > > > > > > > , , , , , , , , , , i i i i i i , , , , , , , , , , # > , , > - $ q @X:X5X3X-X>X*X>X", +"xXqXG X o o = z & & o > > & & # & # # . # # # # . . # # # # # . . # # - # # # # # # # # # # & & < tXPXPXLXKXKXKXKX8X` ` n v z < < < & m CXKXKXKXKX} , - > > > > > > > > , , , , , , , i , , , , i i i i i i , , i i i i , , * $ $ ; , , > & # 0 {.:X3X-X>X,X*X # # # . . . - - - # # # . . . . # # # # # # # # # # # # # & < } FXPXKXLXKXLX} < . a > & < > & > z > m IXKXLXGXb 1 , > < > > , , , , , , , , , , i i i i , , p i p p p p i i p p i , , * $ $ $ , , , & & - t ! 1X3X*X>X*X>X>X", +"gXgXgXt.3 7 7 & & & o & & & # # # - # # # # # . > > > # . . . # . . # # # # # # . # # # # # # & > ` FXKXKXKXLX{ > & 1 1 < a < & 1 o . . a n GXKX8X< , 1 > < > > , , , , , , , , p p p p i , , , i i i p p p i i i i , , * * * $ $ , * * & & - t h +X*X > # # . . . . . . - - # # # # . # # # # # # & , b CXPXLXKX| # & , , . > , > 1 > 2 < < > & ` GXn , > , > > < > , , , , , , , , a a p p p , , , , i i i i i i i i i , , , , * $ , * * * & # # $ q {.+XX3X-X", +"hXpXgX0X' 7 X o o o o & & & & & # # # - > # # . > > # # # # . . . # - # # # . . # # # # # # # # & a R.PXKX| , , , # & & & > & > & & & & . z # ` 1 # 1 > > > 1 > < < , , , , , , p p p p p i , , , i i i i i i i i i i i , , , * * * * * & # # $ q ! 1X:X,X > > > . . # . . . - # # . . . # # # # # # # # # # # # # & # a ` PX} , # # & . > > . > > # > 2 < & a . , 1 a > z , > , < > , , , , , , , , p p p p i , , , i i i i i i i p i i i , i , , * $ $ * * & # # $ t u [.+X,X > > . . . . . . - - # . . . # . # # # # . # # # # # > , , # z | & , > . # # > # . # . > & & < > # a , 1 , , , , & 1 > > , , , , , , , , p p p p i , , , , , , , , , , i , * , * , , , , $ $ * & & # # # % u _ +X,X-X3X3X", +"fXpXpX9Xt.9 X o o . . # # # & > # . . . . # # - > # . # # # . . . . . . # # # # # # # # # # # > , , a # - 1 . > 1 > > > < . > m 7 z m , , , , < , , , , , , , > , , , , , , i p p i , i p p , , , , , * * , , i , , , , , , , * $ $ $ # & & & - ; u P +X1X5X3X2X", +"fXpXpX9Xt.5 X o o . # # # # # # # - . . # # > > # . . # # # # . . . . . # # # # # # # # # # # & , , 1 > # # . > # # # 8XtXCXCXCXCXCXiX7Xa > , > , , , , , , , > , > > > , , , , , , , i p p < , , , , * * , , i i , , , , , * * $ $ $ # # # # # # % y [.:X > > - # . . . # # # # # . . # # # # # # # # # # # # # ; , , & , 1 & 1 , | CXPXKXKXKXKXKXPXIX| 1 > # > > > , , , , > , , > > & > , , , , , p p p , , , , , * * , , , i , , , * * * $ , , ; - # # # # # % % [ +X1X3X3X", +"fXdXpX9Xt.3 o o o # # # # # # . - - - - # > > > # . . . . # - # # # # # # # # # # & # # # # # # # ; ; , > , 1 # > 1 uXIXKXKXLXKXKXKXPXIX} 1 # , > > > , , , > > , , > > , , , , , , , p p i , , , , , * * * , , , , , * * * * $ , # # # # # # # # # % ^ :X1X2X3X", +"fXdX9X9Xt.X o o o # . . # # # # . . . # # # # # - # . . . # # # # - - - # # # # # # # # # # # # , # # , , > < a < { CXLXKXKXKXKXKXKXPXGX( > , ; , , , , , , , < , , , , , , , , , i p p i , & # , , , * $ $ * , , , * * * * * $ # # # # # $ $ # o . $ P NX1X3X2X", +"fXdXpX9X' X o o # # . . # # # & . . # # # # # # > > # . . # # # - - # - # # # # # # # # # # # # # ; , , a 1 > > n tXFXKXKXKXKXKXKXPXGXtX, , a # , , , , , , , < , , , , , , , , p p p i , * # # , , * * $ $ * , , * * * * * * # # # . # # $ * # o . ; u +X1X3X2X", +"fXpXpX9XG X o o # # # # # # # # . . . . - # # # > > - . # . . . . . # # # # # # # # # # # # # # # , , # , , & m 7XCXLXKXKXKXKXKXKXPXCX` , , 1 , , , 1 1 , , , , , < , , , , i i p p i , * $ & & , , , , * $ * , , , * * * * $ # # # . . # $ * & o . # w [.1X @ - . > > - # # . . . . . # # # # # # # # # # # # # & , # , a a # m 7XCXGXKXKXLXKXKXKXPXCXuX< 1 a , i 1 1 1 1 , > > , 1 1 1 < 1 , p a p p < , $ # & * , , , , , ; , , , , , * & & # # # # . # # $ $ # & & # y {.1X & # . . # # # . . > . 1 @ . @ > . . > . . . . . 2 & . # # > . > > & # # # # & , # a , a , 7XCXGXKXLXKXKXKXKXLXCXVX( z a 1 , i a 1 , , > > > > 1 - 1 , , 1 , , , & = o , # * = < & , $ , , ; , , - ; & & & # # $ # & # # # $ $ ; # $ t [ NX4X3X", +"aXpXpX9X9 X & # & & & o . . # # > . . 1 . . 1 . @ @ o 2 . o > > . . . & , < . 1 & & & # # # # # # , , , i M R.CXFXKXKXKXKXKXPXIXrX} z < < a , 1 1 1 , < , > > > < > < a 1 , a < < = = = . & = 6 7 & a # - % ; , , # # # & & & - # # # & # # $ $ $ $ $ 0 _ NX4X3X", +"fXZ.9X9X5 X & # # # & o . . # # . o 8X8X8X} } | ` ` n m 2 > o . 2 & > & . < . # # & # # # # # # # 1 # , , a ` iXIXPXLXKXKXLXCX7X} n , , , a , , < < < < < > > , , a < < , , < = 6 6 6 c 6 c 6 6 6 6 , ; t : & & = * & & & & . # # # # & # # $ $ $ ; * 0 ^ NX4X4X", +"fXZ.9X9XX o & # # . o o # . # # 7 . 8XxXKXKXLX7X7X7XrXuXiXxXtXrX8X X{ ` & . 1 > # & # # # # # # # , # , , , a 7XVXHXKXLXLXFXrX{ ` i i a i a i , i < p < < , , , * x p p p < 1 ` D ) ) ) ) ) ) ) ) u.2 + + ; o = & & o > * # # - # # # o # # $ $ $ ; * 0 ! NX3X4X", +"dXZ.9Xz.X o & # . # o o & # # # . z 8XCXLXKXCXt.8X7X7XrXtXiXxXxXiXiXtXtX{ > . . & & > # # # # # & # , a i i , | iXFXLXHXZX7X} } n t i i a $ i p i p p p p , , , d r * < z & o 8X5.5.5.5.5.5.5.5.5.vX7 + ; , = = 2 > @ > $ % . . # # # & # # $ $ # ; # $ l +X & & & & & # # - > , # , , i a , m uXVXZXFXtX X8X8Xi t i i a * p p p p p i i 1 , , ; i x = < < 6 0X5.5.5.5.5.5.5.5.5.vXv @ = = 8 3 3 3 X 2 , % # . # # # & & & ; ; ; - , $ h [.1X4X", +"pXpXpX' o o . # & & o o . . # # < D yXCXCXVXFX7X8XuXVXCXCXCXVXCXCXVXxXiXVX7X` . & & & & # & > > , , , , , i a & 8XCXFXxX8XrXVX` , i p , p p p p i a i i , 1 , , 1 > 6 7 { rXu.u.u.) .Xu.u.u.XXu..X7X' t.u.7.O.@.O.7.9.] ( r o o # # & & > > ; ; ; , < , y {.1X4X", +"pXpX9XD o o . & > o o o . . # # & ` XuXFXFX7Xt.8XrXrXrXtXuXuXiXiXuXyXtXuXtX7Xz & & & & # # > > # , # a i * a < ` VXxXrX8X8X7X= < * < * * p , , i a i 1 , , , > @ 5 A ' yXPXvXu..X} uX X.Xu.vX{ BX} ] cXXXh.+.j.+.h.pX9X' = o = & - & > > > ; ; - # , $ y [ 1X4X", +"dX9X9X9 o > # # . . o o . . # # & n { } } } X7X7X7X7XrXtXtXuXuXtXtXtXtXrXrX7Xz # & # & # # # # > , ; ; , , , < & uXyX} ` 8Xv & < * * * * , , , i , , , < < < 7 4 V O.u.vXPXvXu..X{ VX X7Xt.vX} BXR.9.aXg.h.j.#.#.h.g.pX' z & > & > > > > $ ; ; ; > , $ y / 1X4X", +"dXpX9X9 @ > . # # . o o o o & > . & & # . . < z m n n ` ` { } } 8X7X8X7X7X7X7Xz # # # # # # # # > > , , , * , * < ' 8X` D { & = , , , , , , , , ; ; , < = 6 6 8 Z *.8.8.5.h.5.u. XrX8X X X7Xu.t.8X} ' 8.5.j.#.#.#.#.+.r.A & . > # > > > , ; ; ; ; > > , y ^ NXNX", +"pXpXz.5 @ > . - . . o o o . # # # # . # & & # # & & & # . . # & & & 2 z m n n & # . # # # # # > > & # # $ * * & = 7 ` D ( c & & # - - - , , 1 1 1 1 , 2 6 c A F 0.G.G.D.+.j.5.XXrXVX` 7XiXGX( { uXrXu.5.j.#.#.#.#.#.+.h.c * # > # & > > , , ; ; ; - > i t h NX4X", +"pXpXt.X @ > . > . . . o . . # # . . . # # - # # # # # # # # # & & . o & . . # & # # . # # # # & > & # # $ $ # & & & 7 ( D . < & # - - - - ; 1 1 1 1 , = 6 c D F e.K.A.B.+.+.5.u.rXVX| 7XrXCX' t.uX.Xu.5.j.+.+.+.+.+.+.y.v ; . . # , > > , ; ; ; ; > > i t u NX4X", +"pX9X] X o & . # # . o o o . # # - > > . . . . # . . # # # # # # o o 2 . # < & . # # # # # # # # & & # # # # # # & & > m < > # , - - - - - - , , 1 , > 2 7 c A A F H F D A A A A ( ( a M b ` c v n m c 4 N N 4 4 N 4 N N O . - , ; , > & & $ ; ; ; , , , t u NX1X", +"pX9X' X o & . . . . o o o o . # # + - > > > > # # > > > > > > & > & < . . & . # # # # # # # # # & & & # # # # - > & > & . < # - - - - - > , < 1 , ; - > < 7 6 6 6 7 o 7 6 6 7 z z < i a z < , 1 < < 6 c o 2 @ @ 7 3 3 X = . - - ; , * & * $ ; ; ; , , * t u @X1X", +"pX9XD X o . . . . . . . . . . . > . . . . . # & # & # # . . . . . . > & , . . > # # # # # # # # & # # # # - - , & , > > # - , # - - , * < < p < , ; ; - , > < < p < z z a < = 7 z z a , p x d - , < < z < , , p = < z = = o . # , * * * * $ ; ; ; - , # t u {.NX", +"pXc.D X o . # # . . o o o o # & # . @ > - > & & & & & & & & & & > . > . > # , # # # # . # # # # & . # # # ; ; , , . # > . - , , , , , < z 6 = * # ; : : : , 1 1 i i p p ; a 7 7 < < < z z = i i , 1 < & , p i r r r * * o & & > $ * , , * , ; ; ; # , # i q _ NX" +}; diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index b604f07f5a2..7b04293018f 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -17,7 +17,12 @@ def test_sanity() -> None: assert im.format == "XPM" # large error due to quantization->44 colors. - assert_image_similar(im.convert("RGB"), hopper("RGB"), 23) + assert_image_similar(im.convert("RGB"), hopper(), 23) + + +def test_read_bpp2() -> None: + with Image.open("Tests/images/hopper_bpp2.xpm") as im: + assert_image_similar(im.convert("RGB"), hopper(), 11) def test_invalid_file() -> None: diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 304f5836170..fcee142e3c2 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -56,7 +56,7 @@ def _open(self) -> None: palette_length = int(m.group(3)) bpp = int(m.group(4)) - if palette_length > 256 or bpp != 1: + if palette_length > 256: msg = "cannot read this XPM file" raise ValueError(msg) @@ -68,8 +68,8 @@ def _open(self) -> None: for _ in range(palette_length): s = self.fp.readline().rstrip() - c = s[1] - s = s[2:-2].split() + c = s[1 : bpp + 1] + s = s[bpp + 1 : -2].split() for i in range(0, len(s), 2): if s[i] == b"c": @@ -98,7 +98,9 @@ def _open(self) -> None: palette_keys = tuple(palette.keys()) self.tile = [ - ImageFile._Tile("xpm", (0, 0) + self.size, self.fp.tell(), (palette_keys,)) + ImageFile._Tile( + "xpm", (0, 0) + self.size, self.fp.tell(), (bpp, palette_keys) + ) ] def load_read(self, read_bytes: int) -> bytes: @@ -120,12 +122,13 @@ def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int self.fd.readline() # Read '/* pixels */' data = bytearray() - palette_keys = self.args[0] + bpp, palette_keys = self.args dest_length = self.state.xsize * self.state.ysize while len(data) < dest_length: s = self.fd.readline().rstrip()[1:] s = s[: -1 if s.endswith(b'"') else -2] - for key in s: + for i in range(0, len(s), bpp): + key = s[i : i + bpp] data += o8(palette_keys.index(key)) self.set_as_raw(bytes(data)) return -1, 0 From 395bd6bd12138ec5b6b97ff172758446895cc9c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Apr 2025 18:56:15 +1000 Subject: [PATCH 1626/2374] Allow more than 256 colours --- Tests/images/hopper_rgb.xpm | 11174 +++++++++++++++++++++++++ Tests/test_file_xpm.py | 8 +- docs/handbook/image-file-formats.rst | 3 +- src/PIL/XpmImagePlugin.py | 41 +- 4 files changed, 11207 insertions(+), 19 deletions(-) create mode 100644 Tests/images/hopper_rgb.xpm diff --git a/Tests/images/hopper_rgb.xpm b/Tests/images/hopper_rgb.xpm new file mode 100644 index 00000000000..063833b3a35 --- /dev/null +++ b/Tests/images/hopper_rgb.xpm @@ -0,0 +1,11174 @@ +/* XPM */ +static char *dummy[]={ +"128 128 11043 3", +".F0 c #000000", +".Jj c #000001", +"#94 c #000002", +".F5 c #000003", +"ax0 c #000004", +".Gy c #000005", +".HF c #000006", +".Mc c #000007", +"#G5 c #000008", +".F2 c #00000b", +".HC c #00000d", +".Dj c #000010", +"#g# c #000012", +".7l c #00001d", +"#aa c #00001e", +".qa c #000028", +".LB c #00002d", +".KU c #000100", +"aPY c #000102", +".KG c #000103", +".KV c #000104", +".KW c #000106", +"abf c #000107", +"acJ c #000108", +".EX c #00010e", +"#ve c #000114", +"#D2 c #000121", +".LC c #000127", +".Hv c #000200", +".Jo c #000201", +"aD6 c #000204", +"abc c #000205", +".SU c #000206", +".QD c #00020b", +"axP c #00020e", +".Hp c #000210", +"#7j c #000214", +"#Fs c #00021e", +"#eC c #000220", +"a.O c #000223", +".4f c #00022e", +".2z c #000230", +".Jn c #000300", +".Ky c #000304", +".Jk c #000305", +".Rr c #000307", +".IN c #000308", +"a#G c #00030a", +"aua c #000313", +".BL c #000314", +"#hQ c #000321", +".N5 c #000332", +".Kz c #000405", +".KX c #000407", +"aIk c #000408", +".KS c #00040a", +"aP7 c #00040c", +"#92 c #00040d", +"aCO c #00040f", +".Di c #000412", +"avW c #000417", +"#hF c #00041b", +"#kY c #000421", +"#mm c #000422", +".4e c #000436", +".T5 c #000437", +"ab# c #000507", +"aJL c #000508", +".KR c #00050a", +"az2 c #00050b", +"az4 c #00050f", +"#tr c #000512", +"#nM c #000522", +"aN2 c #000609", +"#uZ c #000616", +"#3T c #000617", +"atm c #00061f", +"#A5 c #00062b", +"#hW c #00062d", +"a#D c #000701", +"aPD c #000709", +".Jl c #00070a", +".IX c #00070f", +"#2x c #000715", +"#tb c #000716", +"#Fx c #000717", +"avo c #00071d", +".BN c #00071f", +"axg c #000720", +"#86 c #000722", +"#On c #000728", +"#Cy c #000729", +"arw c #000730", +"abe c #000809", +"#2w c #00080c", +".Jm c #00080d", +"#5D c #000818", +".A# c #00081d", +"axQ c #000820", +".IW c #00090f", +".IY c #000911", +"#ts c #00091b", +"asO c #000927", +"asc c #00092b", +"az3 c #000a11", +"awR c #000a25", +"asN c #000a26", +"#Rr c #000a2b", +"aOL c #000a2c", +"acE c #000b00", +"#tt c #000b1c", +"akR c #000b24", +"akS c #000b27", +"#87 c #000b29", +"aPH c #000b2d", +"aM1 c #000c2e", +"#D4 c #000c2f", +"#Cx c #000c30", +"atn c #000d28", +"aCB c #000e0a", +".Sa c #000f33", +"#7m c #000f35", +"#tq c #001020", +".TG c #001032", +"aiF c #001034", +"adG c #001232", +".QE c #001439", +".Vi c #00143a", +"#2y c #001c36", +"aAn c #010000", +".H0 c #010001", +"azp c #010002", +"adp c #010003", +".I. c #010008", +"#k3 c #010018", +".2A c #010020", +".lR c #010037", +".l2 c #01003f", +".IZ c #010107", +"awn c #01010a", +"#wx c #010124", +".DB c #01012f", +"acM c #010202", +"asx c #010204", +".Uv c #01020b", +"#x4 c #01020d", +".FX c #010301", +"as7 c #010304", +"aQD c #01030a", +"#7h c #010311", +"Qta c #010332", +"aK0 c #010405", +".Kq c #010406", +".Kk c #010409", +".Mh c #01040b", +".Mi c #010410", +"#5C c #010414", +"#K. c #010420", +".SH c #010424", +".ht c #010428", +".fZ c #010429", +"aPV c #010507", +".Kr c #010509", +"#8n c #01050e", +"ayT c #010513", +"#kL c #010518", +"#jl c #01051a", +"#p# c #010523", +".IU c #010609", +"#7i c #010615", +"#mc c #010616", +"awo c #010618", +".Xt c #010622", +"#qx c #010623", +"#rX c #010624", +"aM. c #01070a", +"#rP c #010714", +"#qo c #010715", +"#VG c #010718", +"akQ c #01071d", +"ab6 c #010720", +"aIb c #010723", +".qc c #010731", +"#Rs c #010823", +"aGQ c #010824", +".IV c #01090d", +"#7k c #01091f", +"#Rp c #010929", +"acF c #010b10", +"#vc c #010d1c", +"#5F c #010d25", +".Go c #020000", +"agL c #020002", +"aai c #020003", +"aj6 c #020004", +"#Ls c #02000a", +"#7R c #020015", +"#eW c #020022", +".oS c #020032", +"#95 c #020102", +"aup c #020109", +"#x3 c #020113", +"#Fr c #020124", +".Oh c #02012f", +".oR c #020132", +".Ei c #020200", +".Kn c #020203", +".QC c #020207", +".I5 c #020209", +"ay4 c #020210", +"#PV c #020218", +"acm c #02021b", +"#M3 c #020225", +"#hT c #02022d", +"a#J c #020303", +"asA c #020304", +".KB c #020305", +"aL3 c #020306", +"aeQ c #020307", +"abg c #020309", +".Gz c #02030a", +"#3i c #02030c", +"avV c #02030e", +"#c5 c #020317", +".#Y c #020332", +"as4 c #020404", +"#93 c #020406", +".Jp c #020408", +"aMj c #02040b", +"#8m c #02040d", +"#ZI c #02040f", +".Ju c #020412", +"#wL c #020413", +"#Cs c #02041b", +"#Ly c #020420", +".Re c #020425", +".PJ c #02042a", +".5O c #020430", +"#2v c #020506", +".Kx c #020507", +".KQ c #020509", +".Jq c #02050a", +".SI c #020523", +".Lw c #020525", +"#nR c #020528", +".ew c #020529", +".bq c #02052a", +".Ip c #020534", +".Jr c #020608", +"a#H c #02060c", +"axf c #020613", +".Y2 c #020616", +"#o2 c #020617", +"#nB c #02061a", +"#SS c #020626", +"a#C c #020703", +".SV c #02070c", +"aD7 c #020710", +"#nC c #020713", +"#He c #020716", +"#jx c #020733", +"#o3 c #020810", +"ayR c #020818", +"a.M c #020820", +"#Ro c #020822", +"#hU c #020835", +"aP8 c #02090f", +"adF c #020924", +"#to c #020926", +"#SR c #02092b", +"ahw c #020a2a", +".eJ c #020a4b", +".iX c #020a4c", +"#vb c #020b20", +"#Rq c #020b2f", +"#D5 c #020c2d", +"#A4 c #020c32", +"ajL c #020d2a", +".Gq c #030000", +"azg c #030001", +".HO c #030002", +".H2 c #030003", +".I6 c #030005", +"ayY c #030006", +".F1 c #030010", +".HB c #030013", +".SM c #030037", +"aJP c #030103", +"abb c #030105", +"ami c #030108", +"axq c #030109", +"aAF c #03010b", +"az6 c #03010e", +"ay5 c #030111", +".nt c #03013b", +".QB c #030202", +"a#K c #030203", +"arQ c #030204", +"asF c #030205", +"aj7 c #030207", +"awP c #030208", +"avC c #03020a", +"#wN c #030214", +"#wu c #03021d", +"#c4 c #030221", +"#Lx c #030225", +".km c #030244", +".KF c #030305", +"aN9 c #030306", +"#8p c #030307", +"aeP c #030308", +".I3 c #030309", +".I4 c #03030a", +"#1l c #030312", +".FR c #030316", +"#P1 c #030326", +"#A2 c #030327", +".LS c #030400", +".FY c #030402", +"acL c #030404", +".KD c #030406", +"a.E c #030408", +"ahj c #030409", +".2e c #03040a", +"#43 c #03040c", +"#Yg c #03040f", +"aAb c #030412", +".bn c #030433", +"a#I c #030504", +"aOV c #030508", +"a#F c #03050d", +"avn c #030512", +"ae2 c #03051e", +".Lx c #030523", +".J9 c #030526", +".IA c #03052b", +".MH c #03052c", +".Sy c #030535", +".GY c #030539", +"aOY c #030608", +".Oo c #03060a", +".M2 c #03060b", +"#mb c #03061e", +"#kK c #030621", +"#jk c #030623", +"#hE c #030625", +".#1 c #03062a", +"Qte c #03062b", +".Lj c #03062c", +"aPW c #030709", +"aCJ c #03070a", +".PV c #03070c", +"aNh c #03070d", +"#1. c #03070e", +"aL4 c #03070f", +"aAa c #030714", +"am7 c #030718", +"#eD c #030719", +"#Oz c #03071d", +".Im c #03072d", +".Lk c #030731", +".5N c #030738", +".Op c #03080d", +"#SO c #03081c", +"aa9 c #030900", +"#qp c #03090e", +"#91 c #030912", +"#85 c #03091f", +"ajK c #030922", +".hE c #03093b", +"acD c #030a00", +"#rQ c #030a0d", +"aiD c #030a27", +"aav c #030b28", +"#jy c #030b36", +"aPw c #030c0e", +"ayP c #030c16", +"alW c #030d23", +"aiE c #030f30", +"#tp c #031026", +"aBf c #031114", +"#vd c #03111d", +"adI c #031539", +".xN c #040000", +".ET c #040001", +".KH c #040002", +".Oq c #040003", +".HU c #040004", +".Wj c #040005", +".HJ c #040007", +".HI c #040008", +"ayJ c #04000f", +"#MX c #040014", +".vH c #04001e", +"#jw c #040027", +"#IE c #040028", +".Gu c #040101", +".HP c #040102", +"aAD c #040105", +"aei c #040108", +"acn c #040111", +".Me c #040201", +".Ji c #040202", +".FD c #040203", +"aJN c #040204", +".F8 c #040205", +"ax9 c #040206", +"avU c #040208", +"ai3 c #040209", +"#8l c #04020a", +"avm c #04020b", +"aAE c #04020c", +"axM c #04020f", +".5K c #040212", +".0O c #040225", +".LM c #040304", +"arR c #040305", +"#8q c #040306", +".H7 c #040307", +"aqZ c #040308", +"av5 c #04030b", +"#Ub c #040312", +".Hq c #040314", +"aFs c #040315", +"aaQ c #04031d", +"#Ox c #040326", +".Q7 c #04032a", +".Px c #04032b", +".nw c #04033c", +".LR c #040400", +"acN c #040402", +"ann c #040405", +"anm c #040406", +".KP c #040407", +"aq0 c #040408", +"aBp c #040409", +".Mb c #04040a", +".EV c #04040b", +"aCM c #04040c", +"auN c #04040d", +"aCN c #04040e", +"#Rl c #04040f", +"#SM c #040413", +"aD8 c #040416", +".N2 c #04042b", +".DI c #040432", +".hH c #040447", +".KT c #040500", +"abk c #040505", +".KE c #040507", +"#3S c #040509", +"aBq c #04050a", +"aA# c #04050b", +"atl c #04050c", +"aeR c #04050d", +"#9Z c #04050f", +".Ea c #04051c", +"#tn c #04052a", +".cY c #040534", +".MT c #040535", +".df c #040547", +"abl c #040604", +"abj c #040607", +"aJK c #040608", +"aOX c #040609", +".Vh c #04060b", +"aI. c #04060d", +"#81 c #04060f", +"alS c #040618", +".MP c #040624", +".IB c #040627", +".Lv c #04062c", +".Ht c #040704", +".Hz c #040705", +".IT c #040709", +"aCK c #04070c", +".NA c #040712", +"#wv c #040717", +"aQE c #04071d", +"#Ha c #040722", +".Sx c #040732", +".Kw c #040809", +".IO c #04080d", +"aO# c #04080e", +"awQ c #040817", +".FQ c #040818", +"aiC c #040822", +".Q8 c #040833", +".2y c #04083c", +"abd c #04090b", +"aBo c #04090d", +"aKS c #040911", +".VT c #040933", +".g. c #04093b", +"ayy c #040a0a", +".Ks c #040a0e", +"aFt c #040a27", +".eG c #040a3c", +"ab. c #040b0d", +"aBh c #040b10", +"#IK c #040b21", +"aOd c #040c12", +"ab7 c #040c27", +".gb c #040c4e", +"ahx c #040d2f", +"aqL c #040e3e", +"aCC c #04100d", +"#88 c #041032", +"ajM c #041434", +"adH c #041738", +".EP c #050000", +"#2O c #050001", +".KI c #050002", +"aDl c #050004", +"#pf c #050009", +"#6e c #05000b", +"#yc c #050020", +".87 c #050024", +"amJ c #050106", +"al9 c #050107", +"#zt c #05010f", +"aaR c #050112", +".Ay c #050123", +"#u7 c #05013e", +"ail c #050206", +".J# c #050207", +"afv c #050209", +"#ZG c #05020a", +"#09 c #05020b", +"axN c #050211", +"#2L c #050221", +".Sw c #05022a", +"aN3 c #050304", +".Gw c #050305", +".Mf c #050307", +"ale c #050309", +"awI c #05030a", +"av6 c #05030b", +".3X c #05030c", +"awN c #05030e", +"a#E c #05030f", +"axd c #050310", +"aAc c #050313", +".5J c #050318", +"#qz c #05031d", +".2s c #050323", +".0V c #050326", +".Ed c #050403", +"alc c #050404", +"anl c #050406", +"arf c #050407", +"#6I c #050408", +"#44 c #050409", +".HH c #05040a", +".H9 c #05040b", +".I# c #05040c", +"awl c #05040d", +"awM c #05040e", +"#W8 c #05040f", +"aDB c #050411", +"#Rk c #050413", +"#Z0 c #050418", +".uj c #05041d", +"aaO c #05041f", +".4g c #050422", +".rw c #050426", +"#zz c #05042d", +"#u2 c #050430", +".kn c #050446", +".Ej c #050503", +"aee c #050504", +".O8 c #050506", +".KC c #050507", +"apj c #050508", +"apk c #050509", +"ajw c #05050a", +"aFp c #05050b", +".I2 c #05050c", +"#Rm c #05050d", +"#SN c #05050e", +"#YA c #05050f", +"#Z1 c #050510", +"aGP c #050515", +"#ST c #05051f", +".Fn c #050538", +"Qtx c #050548", +"asB c #050607", +"ay0 c #050609", +"ahi c #05060a", +"ay1 c #05060b", +"aqK c #05060c", +"asa c #05060e", +"#hX c #050624", +"#x6 c #050626", +"#Cv c #050628", +"#o4 c #05062d", +"#rR c #05062e", +"#eL c #05062f", +".Rg c #05063e", +".FV c #050705", +".IR c #050706", +"as6 c #050708", +"aOT c #050709", +"ayZ c #05070b", +"aGO c #05070e", +"aeS c #05070f", +"#zu c #05071f", +"#IG c #050723", +".K. c #050725", +".2v c #05072c", +".at c #050730", +".et c #050736", +".iR c #05073f", +"aA. c #05080c", +"aMk c #05080e", +"aJy c #050814", +"aJz c #050815", +"aPG c #05081e", +"ab5 c #050820", +".PK c #050826", +"#ms c #050827", +"aNi c #05090f", +"aFq c #050911", +"#90 c #050912", +"#Rt c #05091b", +"aOK c #05091f", +".iV c #05093f", +".iW c #050943", +"aD5 c #050a08", +"ayO c #050a13", +"#Hb c #050a20", +"#mr c #050a30", +"#zC c #050a34", +".da c #050a3c", +".g# c #050a3f", +".hF c #050a43", +".Kt c #050b11", +"#hP c #050b2d", +".bC c #050b3d", +"ayB c #050c11", +"#Uc c #050c1e", +"am8 c #050c20", +"#k1 c #050c35", +"#vf c #050d1e", +".a# c #050d4f", +"#5E c #050e22", +"QtH c #050e37", +"aD0 c #050f09", +"#tc c #050f16", +".BM c #050f25", +"#Hd c #050f27", +"aQB c #051112", +".W6 c #05193e", +".A9 c #060000", +".KJ c #060002", +".Nv c #060004", +"aMd c #060005", +".HT c #060008", +"#7S c #060009", +"#bK c #06000e", +"##9 c #06000f", +"#AW c #060010", +".7m c #06001c", +".xt c #060026", +".HV c #060103", +"ax4 c #060104", +"aNb c #060105", +"#8k c #060106", +".lM c #060125", +"#3k c #060205", +"ahh c #060206", +"aqj c #060207", +".HL c #060208", +"ak4 c #060209", +"aj0 c #06020a", +".k. c #060229", +".CE c #060303", +".O7 c #060304", +"#45 c #060305", +"awm c #060307", +".I8 c #060308", +"aef c #06030a", +".Zf c #060320", +".Ze c #060329", +".Wb c #06032d", +".Uo c #06032f", +".Gv c #060404", +"ai2 c #060405", +"aK2 c #060406", +"aPP c #060407", +"atd c #060409", +"aeg c #06040b", +"atA c #06040c", +"awk c #06040d", +"ay# c #06040e", +"a.G c #060410", +"avk c #060411", +"a.H c #060413", +"#Yz c #060418", +"#W7 c #060419", +".4# c #06041a", +".2t c #06041c", +".0P c #06041e", +".sU c #060420", +".oT c #060436", +".lS c #06043d", +".l1 c #060443", +"acP c #060500", +"agO c #060505", +".LP c #060506", +"api c #060507", +"ano c #060508", +"amg c #060509", +"anp c #06050a", +"avl c #06050b", +".I0 c #06050c", +"avB c #06050d", +"aAG c #06050e", +"awL c #060510", +"asM c #060513", +"aCR c #06051a", +".4. c #060523", +".MU c #06052f", +".nu c #06053e", +".nz c #060543", +".l3 c #060544", +".ko c #060547", +".LU c #060601", +"acO c #060603", +"ank c #060607", +"aqY c #060608", +".TE c #060609", +".S. c #06060a", +"aim c #06060b", +"aGM c #06060c", +".Kl c #06060d", +"aqJ c #06060e", +"#VF c #06060f", +"aog c #060610", +"ay2 c #060611", +"#IL c #060612", +"asb c #060614", +"aIa c #060616", +"a#b c #060620", +"#Ka c #060626", +"#Cw c #06062a", +".Ri c #060641", +"acR c #060705", +"abm c #060706", +"abh c #06070b", +"aH8 c #06070c", +"art c #06070e", +"af7 c #06070f", +".S# c #060710", +"aBr c #060713", +"#82 c #060714", +"#hR c #060722", +"#nD c #06072e", +"#qq c #06072f", +".0U c #060738", +".GR c #06073c", +".PO c #06073f", +".hy c #060740", +".f4 c #060741", +".c6 c #060742", +".U. c #060747", +".FU c #060806", +".Ny c #06080a", +"abi c #06080b", +".Nz c #06080f", +"af8 c #060810", +".P. c #060812", +"amY c #06081a", +"amZ c #06081b", +"ajJ c #06081d", +"#mn c #060823", +".IG c #060830", +".hU c #060831", +".PI c #060834", +"#qv c #060836", +".GT c #06083d", +".eA c #060841", +".8X c #060845", +".Js c #06090d", +"aMl c #06090f", +"aMm c #060910", +".KY c #060912", +"ay3 c #060917", +"aM0 c #06091f", +".IC c #060927", +"Qtf c #06092d", +".Oc c #060935", +".PA c #060938", +".Kv c #060a0b", +"aPZ c #060a0c", +"aNT c #060a0d", +"aCL c #060a0f", +"aL5 c #060a1f", +"#M4 c #060a20", +"#ta c #060a26", +"#g. c #060a27", +"#IJ c #060a29", +".br c #060a2d", +".eH c #060a44", +"aGN c #060b11", +"aJA c #060b12", +"#rW c #060b33", +"Qtr c #060b3d", +".db c #060b40", +".Ku c #060c12", +"aKi c #060c14", +"#qw c #060c34", +"aO7 c #060d13", +"aNm c #060d14", +"#ZJ c #060d1e", +"alV c #060d21", +"#PT c #060d2d", +"#7l c #06102c", +"a.N c #06102f", +"aOc c #061117", +"#tu c #06111f", +"#Fw c #06122c", +"aBg c #061719", +".Jf c #070000", +".EC c #070002", +".Eo c #070004", +".El c #070009", +"#1W c #07000c", +"ahK c #070017", +"aD3 c #070100", +".M5 c #070101", +".KK c #070102", +"abp c #070103", +"aq# c #070104", +"aqi c #070106", +"aK8 c #070107", +"aK7 c #070108", +"#Ow c #070127", +".Er c #070200", +".Gs c #070202", +".sF c #070203", +".M4 c #070204", +"aNc c #070206", +"aM# c #070208", +".Wh c #07020a", +".Uu c #07020b", +"aER c #070212", +"#Or c #070213", +"##0 c #070228", +"#mp c #07022a", +".Je c #070303", +".Jd c #070304", +"amf c #070306", +"af6 c #070307", +".HK c #070309", +"#2N c #070311", +"#mt c #070318", +".XG c #070323", +".sJ c #070326", +"#.E c #070327", +"#wH c #07032c", +".PW c #070401", +".LZ c #070402", +".KO c #070405", +"auq c #070406", +".H4 c #070407", +"aec c #070408", +".I7 c #070409", +"ar0 c #07040a", +"ai4 c #07040b", +"aCi c #070410", +"avT c #070411", +"aqH c #070414", +"#6d c #07041a", +"#2M c #07041d", +".Fq c #070433", +".GZ c #070437", +"#y. c #070438", +"#wD c #07043b", +"aJM c #070505", +"aed c #070506", +"aK1 c #070507", +"azn c #070508", +"ax8 c #070509", +"ark c #07050a", +"#3j c #07050b", +"auM c #07050c", +"aun c #07050d", +"auL c #07050e", +"#9Y c #07050f", +"auK c #070510", +"au# c #070511", +"au. c #070512", +"aCQ c #070513", +".HD c #070514", +".4a c #070515", +"#bt c #070518", +"#VE c #070519", +"#Ua c #07051a", +"#VI c #070520", +"#9w c #070521", +"#7Q c #070522", +".XF c #07052d", +"#x9 c #070537", +".kd c #070541", +".Ef c #070605", +"abn c #070606", +".LL c #070608", +"arX c #070609", +".Mg c #07060b", +".XR c #07060c", +".IP c #07060d", +"acH c #07060e", +"azs c #07060f", +"awj c #070611", +"asL c #070612", +"as# c #070613", +"apQ c #070614", +"atk c #070615", +"#pe c #07061e", +"#LA c #070621", +".sV c #070622", +".5I c #070624", +"#II c #07062c", +".LV c #070701", +"abX c #070706", +"awx c #070708", +".H6 c #070709", +"apl c #07070b", +"aky c #07070c", +"apN c #07070e", +"aoc c #07070f", +"#YB c #070710", +"aH9 c #070711", +"aru c #070712", +"arv c #070713", +"ao5 c #070714", +"aoh c #070715", +"#nE c #070732", +".iL c #07073a", +".T9 c #070740", +".bv c #070742", +".Ra c #070743", +"aD4 c #070804", +"acK c #070809", +"#8o c #07080c", +".O9 c #07080e", +"aLn c #07080f", +"af9 c #070812", +"aCP c #070813", +"#nN c #070823", +"#nS c #070824", +"#pd c #070828", +".fW c #070837", +".ho c #07083a", +".Of c #07083d", +".#5 c #070843", +"Qtj c #070844", +".bH c #07084a", +".FZ c #070907", +".IS c #070909", +"as5 c #07090a", +".Kp c #07090b", +"aOI c #070910", +"aeT c #070912", +".EW c #070914", +"#W9 c #070919", +"#D0 c #070922", +"#qy c #070923", +"#rY c #070924", +"#MU c #070926", +"#gg c #070932", +".rB c #070935", +"#o9 c #070937", +".Sz c #07093c", +".Ul c #070946", +".Um c #070948", +".Hy c #070a07", +"aNj c #070a10", +"aPm c #070a12", +".Jt c #070a13", +"am0 c #070a1d", +"#rO c #070a25", +"#P2 c #070a26", +"#qn c #070a27", +".c2 c #070a2d", +".ex c #070a2e", +"#zD c #070a31", +".Iz c #070a35", +".R. c #070a39", +"aOG c #070b0f", +"aO8 c #070b11", +"am1 c #070b1f", +"#Rn c #070b20", +"#Ft c #070b22", +".JY c #070b35", +".2x c #070b3d", +".bD c #070b45", +"aOH c #070c14", +"ayD c #070c15", +"a.L c #070c23", +"#va c #070c28", +"#Oo c #070c2a", +"Qts c #070c41", +"#p. c #070d34", +".iY c #070d4f", +".Dk c #070f22", +"aO6 c #071017", +"ayQ c #07101c", +"#hV c #07103c", +"ayS c #07111e", +"aNl c #071319", +"#89 c #071437", +".P# c #071d45", +".M6 c #080000", +".zu c #080001", +"abq c #080003", +"aps c #080004", +"aFW c #080005", +".SW c #080006", +"ayN c #08000a", +"#Ix c #08000e", +".ml c #080010", +".7e c #08001c", +"#s7 c #080025", +"am# c #080101", +".HS c #08010c", +"#Ot c #08010d", +"#J2 c #08010f", +".9. c #080110", +".ES c #080201", +".A5 c #080202", +"al# c #080203", +"a#M c #080204", +"aEe c #080206", +"aGX c #080207", +"aKR c #080208", +"agy c #08021d", +"#ab c #08021e", +".HX c #080302", +".Jc c #080305", +"azl c #080306", +"aMa c #080309", +"aiS c #08030e", +"#zs c #080315", +".n# c #080321", +".Oi c #080327", +"#.F c #080328", +".L0 c #080402", +"aPO c #080403", +"awO c #080406", +"atI c #080407", +".HM c #080408", +"aeb c #080409", +"aMb c #08040a", +".Hb c #080410", +"aHt c #080415", +".XH c #08041f", +".V7 c #080426", +".lO c #080428", +"#hS c #08042c", +"#u6 c #08043f", +".Km c #080502", +".LY c #080503", +".TD c #080504", +"a#L c #080506", +".HN c #080507", +".HY c #080508", +"aea c #080509", +".J. c #08050a", +"ald c #08050c", +"aBe c #08050e", +".3W c #080512", +"at9 c #080513", +"apM c #080515", +".HA c #08051a", +".V8 c #080521", +"#wI c #080527", +"acQ c #080600", +".R9 c #080605", +"aJO c #080608", +"azm c #080609", +"aPS c #08060a", +"aoR c #08060b", +"afy c #08060c", +"afx c #08060d", +"auo c #08060e", +"avj c #080611", +"avS c #080612", +"aap c #080613", +"apL c #080615", +"#Rj c #08061a", +"#r2 c #080624", +".B8 c #08062e", +".MQ c #080631", +"#u3 c #080636", +".l7 c #080648", +"aa8 c #080700", +".Eg c #080706", +".Nx c #080708", +".LN c #080709", +"aow c #08070b", +"anq c #08070c", +"aEB c #08070d", +"aHj c #08070e", +"atB c #08070f", +"azr c #080710", +"avi c #080711", +"awK c #080712", +"apO c #080713", +"aof c #080714", +"as. c #080715", +"aqI c #080716", +"aBs c #08071d", +".5P c #080724", +"#v# c #08072b", +".2r c #080730", +".0N c #080733", +"#eB c #080735", +".LQ c #080803", +"acS c #080806", +"aw1 c #08080a", +"aO. c #08080b", +"amM c #08080d", +".I1 c #08080e", +"aO0 c #080810", +".Wi c #080811", +"aod c #080813", +"apP c #080816", +"#Kb c #08081b", +"#wK c #08081c", +"#M5 c #08081d", +"#ew c #080831", +"asz c #08090b", +"alG c #08090d", +"avN c #08090f", +"ahk c #080911", +"acj c #080923", +"aaP c #080925", +"acl c #080926", +"#jn c #080934", +".hp c #080938", +".fV c #08093b", +"#dc c #080943", +".PD c #080944", +"aOF c #080a0e", +"aJB c #080a11", +"ain c #080a12", +"aio c #080a13", +"ag. c #080a14", +"#wM c #080a16", +".4b c #080a2d", +".iM c #080a39", +".N6 c #080a3e", +".T4 c #080a41", +"##1 c #080a47", +".Hw c #080b08", +".Hu c #080b09", +"aLo c #080b12", +"aQt c #080b14", +".KZ c #080b18", +"aas c #080b24", +".CA c #080b26", +"#o1 c #080b29", +".f0 c #080b2e", +".hu c #080b2f", +".vU c #080b32", +".SG c #080b36", +".J8 c #080b37", +".KA c #080c0d", +"aKj c #080c13", +"#84 c #080c1e", +"Qtt c #080c46", +"#Hc c #080d30", +".Z# c #080d45", +"#tv c #080e18", +"adE c #080e28", +"#nL c #080e35", +"#SQ c #080f2e", +"aCA c #081011", +"#Om c #081133", +".bR c #08123a", +"agl c #081339", +"aPx c #081717", +"#5G c #081736", +"a#N c #090003", +"#D7 c #090013", +".Eb c #090016", +".Rl c #09001b", +"#VJ c #09001e", +"aLc c #090107", +"aBl c #09010a", +"#zF c #09011e", +"#4x c #090203", +"arU c #090207", +"#9W c #09020d", +"ayH c #090214", +"##R c #09021a", +".CB c #09021d", +"#nP c #090229", +".Gl c #090301", +".Eq c #090302", +"aCG c #090305", +"arV c #090308", +"aK6 c #090309", +"aFn c #09030b", +".Up c #09031e", +"#bL c #09031f", +"adX c #090324", +".bY c #090325", +"#r1 c #090326", +".QA c #090404", +"aAo c #090406", +".Gb c #090407", +".Jb c #090409", +".Ja c #09040a", +".Uw c #09040c", +"#9X c #09040e", +"adY c #09041e", +"#qC c #090428", +".IQ c #090502", +".LW c #090503", +"abo c #090506", +".M3 c #090507", +"aq8 c #090508", +"ae# c #090509", +"afu c #09050a", +".BJ c #09050b", +"#4w c #090511", +"ayU c #090514", +"#PW c #090517", +"#Ln c #09051d", +".Ui c #090523", +".Uh c #090528", +".V6 c #090530", +"#wE c #09053e", +".L1 c #090603", +".LX c #090604", +"acT c #090607", +"#6J c #090608", +".Nw c #09060a", +".Dg c #09060b", +"arl c #09060c", +"afz c #09060d", +"aLl c #09060e", +"aLm c #09060f", +".5u c #090612", +"#SU c #090616", +"#9x c #090619", +".Zg c #09061e", +"#1k c #090621", +"#yb c #09062c", +".Ug c #090631", +".T6 c #090632", +".VW c #090636", +".oH c #09063a", +"acC c #090700", +".Md c #090701", +"aM8 c #090709", +".F7 c #09070a", +"aba c #09070b", +"anO c #09070c", +"aj9 c #09070e", +"atE c #09070f", +"aJx c #090710", +"aEC c #090711", +"awi c #090712", +"avR c #090713", +"auI c #090714", +"ao4 c #090716", +".2u c #09071a", +"#ZZ c #090721", +"a#. c #090722", +".XL c #09072f", +"#Fu c #090733", +".nk c #09073d", +"aw0 c #09080a", +"ay. c #09080b", +"aQA c #09080d", +"#ZH c #090810", +"aDn c #090811", +"aFr c #090813", +"aOJ c #090814", +"aoe c #090816", +"ay6 c #09081f", +"a## c #090824", +"#9v c #090826", +"#J9 c #09082b", +"#x8 c #090833", +"#zA c #090834", +"#y# c #090836", +"axp c #09090b", +"atb c #09090c", +"alb c #09090f", +".GB c #090914", +"aF6 c #090918", +"#OA c #09091b", +"adB c #09091e", +".39 c #090930", +"#qk c #090936", +"#js c #09093a", +".gv c #09093b", +".Fm c #09093c", +".Ko c #090a0b", +"aaj c #090a0c", +"ayv c #090a0d", +"avd c #090a0f", +"auB c #090a10", +"aOa c #090a11", +".TF c #090a12", +"aip c #090a15", +"ahl c #090a16", +"amQ c #090a17", +"#83 c #090a1a", +"ae1 c #090a22", +"ack c #090a26", +"#Cu c #090a29", +".Kd c #090a2d", +".hM c #090a30", +"#mq c #090a33", +"#hH c #090a35", +"#oY c #090a36", +".W# c #090a49", +".gd c #090a4c", +"aCI c #090b0d", +"akz c #090b13", +"amP c #090b16", +"#Ru c #090b18", +".A. c #090b19", +"#nA c #090b2d", +"#nK c #090b39", +".iQ c #090b41", +"#VH c #090c21", +"#Oy c #090c28", +".xH c #090c31", +".Lu c #090c38", +".J0 c #090c3b", +".V0 c #090c52", +"aPX c #090d0f", +".EY c #090d1b", +"#PU c #090d28", +".XA c #090d48", +"aQa c #090e0c", +"aNU c #090e13", +"aPE c #090e15", +"#IH c #090e24", +"aau c #090e29", +"aP9 c #090f14", +"#SP c #090f28", +"#ml c #090f37", +".5D c #090f3e", +".0K c #090f44", +"#vg c #09111f", +"ab8 c #09122f", +"ayx c #091311", +"aLr c #09131a", +".Aa c #091731", +".M. c #0a0000", +"#8j c #0a0003", +"#ye c #0a000c", +"aco c #0a0104", +"aBi c #0a010c", +".zy c #0a0203", +"aiT c #0a0206", +".On c #0a020a", +"ayK c #0a0214", +"#IB c #0a0216", +"aak c #0a030d", +".HR c #0a0310", +"adZ c #0a0313", +".za c #0a0329", +"#kZ c #0a032a", +".0b c #0a0400", +".ER c #0a0402", +".A8 c #0a0404", +"acU c #0a0407", +".F9 c #0a0408", +"as9 c #0a0409", +"aK5 c #0a040a", +".89 c #0a0420", +"afi c #0a0423", +"#ID c #0a042a", +".Jh c #0a0504", +"aN1 c #0a0505", +"ame c #0a0506", +"ahT c #0a0507", +"azi c #0a0508", +"am. c #0a0509", +"aM9 c #0a050a", +"aMc c #0a050b", +".86 c #0a0520", +"#Rf c #0a052b", +".k# c #0a052c", +"#v. c #0a0530", +".ks c #0a0545", +"axe c #0a0605", +".HW c #0a0606", +"ar# c #0a0609", +"agN c #0a060a", +"agM c #0a060b", +"aJw c #0a060f", +"a.F c #0a0612", +".Ar c #0a0628", +"#dl c #0a062a", +"#H. c #0a062e", +".CH c #0a0706", +"#96 c #0a0709", +"ahU c #0a070b", +".I9 c #0a070c", +"agP c #0a070e", +"aao c #0a0713", +"aob c #0a0717", +"a#c c #0a0719", +"#r3 c #0a071a", +".0Q c #0a071c", +"#Yy c #0a0721", +"#W6 c #0a0722", +"#4u c #0a0726", +".sZ c #0a0734", +".H1 c #0a080a", +"aPR c #0a080b", +"aOU c #0a080c", +"anN c #0a080d", +"aN5 c #0a080e", +"ai6 c #0a080f", +"atD c #0a0810", +"aDm c #0a0812", +"atj c #0a0813", +"at5 c #0a0814", +"at6 c #0a0815", +"aoa c #0a0817", +"apK c #0a0818", +"#9u c #0a0824", +"#6c c #0a0825", +".ry c #0a082a", +".Zl c #0a082d", +"#zB c #0a0836", +".G2 c #0a0838", +"#f7 c #0a0849", +".kk c #0a084b", +"adq c #0a0907", +".Ee c #0a0908", +"arS c #0a090b", +".Gx c #0a090c", +"ai1 c #0a090e", +"avh c #0a0910", +"avA c #0a0911", +"avQ c #0a0913", +"auH c #0a0914", +"akL c #0a0919", +"#P3 c #0a091a", +"#MW c #0a0920", +".sT c #0a0925", +"#u0 c #0a0930", +".IF c #0a0933", +"#ex c #0a093a", +".dx c #0a0941", +".H5 c #0a0a0a", +"atT c #0a0a10", +"#Yf c #0a0a11", +"acG c #0a0a12", +"aDD c #0a0a17", +"akD c #0a0a19", +"a.I c #0a0a1b", +"#jA c #0a0a26", +"a#a c #0a0a27", +"#H# c #0a0a2d", +"#nw c #0a0a35", +".Uj c #0a0a3a", +".es c #0a0a3d", +".gw c #0a0a3e", +".XK c #0a0a41", +".VY c #0a0a46", +".LT c #0a0b05", +".FW c #0a0b09", +"asw c #0a0b0c", +"ayw c #0a0b0d", +"azM c #0a0b0f", +"ave c #0a0b10", +"a.D c #0a0b11", +".Dh c #0a0b15", +".0A c #0a0b16", +"ag# c #0a0b17", +"amR c #0a0b18", +"akC c #0a0b1a", +"amX c #0a0b1b", +"alR c #0a0b1d", +"#zw c #0a0b28", +"#zy c #0a0b2f", +".do c #0a0b31", +"#l8 c #0a0b35", +"Qt# c #0a0b3c", +".Zk c #0a0b3f", +".c5 c #0a0b46", +".bu c #0a0b47", +"aOW c #0a0c0e", +"acI c #0a0c13", +"alH c #0a0c14", +"alI c #0a0c15", +"aDy c #0a0c18", +"#uY c #0a0c28", +".MJ c #0a0c40", +".hx c #0a0c43", +".W. c #0a0c48", +"#gh c #0a0c49", +".Py c #0a0d38", +".Ll c #0a0d39", +".kp c #0a0d50", +"aP0 c #0a0e10", +"ayA c #0a0e12", +"aQC c #0a0e16", +".MI c #0a0e38", +".i7 c #0a0e3d", +".hG c #0a0e4b", +".qd c #0a0f3c", +"#Yh c #0a1023", +"aD9 c #0a102f", +"#kX c #0a1038", +".35 c #0a1040", +".2n c #0a1042", +"#PS c #0a1437", +"aQd c #0a171a", +".Os c #0b0000", +".PZ c #0b0001", +".M# c #0b0003", +".Nu c #0b0004", +"aLj c #0b0005", +"#Cn c #0b0013", +"#0B c #0b0107", +"aFo c #0b010c", +".Jg c #0b0200", +"aCF c #0b0203", +".M1 c #0b020a", +"aaS c #0b0306", +".PU c #0b030b", +"aiQ c #0b0314", +"aIS c #0b040e", +"akN c #0b0410", +"#P0 c #0b0417", +".oY c #0b0433", +".kt c #0b0437", +".Df c #0b0505", +".O6 c #0b0508", +"aK4 c #0b050b", +"az9 c #0b050e", +"##S c #0b0515", +"ayG c #0b0517", +".Wc c #0b051f", +"#bO c #0b0521", +".FB c #0b0524", +"#G9 c #0b052a", +"#wG c #0b0534", +"axO c #0b0605", +".Gn c #0b0606", +"ax6 c #0b0609", +"ak5 c #0b060a", +"al8 c #0b060d", +"#MY c #0b0617", +"ahL c #0b0618", +"#Lp c #0b061c", +".yR c #0b0622", +".nf c #0b0624", +".lH c #0b062a", +"#bM c #0b062b", +"#VA c #0b062c", +"#t. c #0b0634", +".Gr c #0b0706", +"aq9 c #0b070a", +"af5 c #0b070b", +"ajv c #0b070c", +"asr c #0b070d", +".F3 c #0b0714", +"#bs c #0b0724", +"#J8 c #0b072f", +".nA c #0b0742", +".ge c #0b0743", +"#ti c #0b0745", +".CI c #0b0808", +"alF c #0b080c", +"a.C c #0b080d", +"aoT c #0b080e", +"aeh c #0b080f", +"aEZ c #0b0812", +"axL c #0b0815", +"alO c #0b0817", +"ao# c #0b0818", +"#LB c #0b081f", +"#VD c #0b0822", +"#U# c #0b0823", +"#k0 c #0b0831", +".HZ c #0b090b", +"#2u c #0b090c", +"ai0 c #0b090d", +"aoS c #0b090e", +"ahV c #0b090f", +"atQ c #0b0910", +"au2 c #0b0911", +"avP c #0b0912", +"auG c #0b0913", +"an1 c #0b0914", +"an0 c #0b0915", +"at7 c #0b0916", +"ao. c #0b0919", +"#uX c #0b092f", +".p8 c #0b0933", +"#ya c #0b0934", +".Zd c #0b0938", +".Uf c #0b093d", +".lU c #0b0948", +"aCH c #0b0a0b", +"abW c #0b0a0c", +".H8 c #0b0a0f", +"avg c #0b0a10", +"auF c #0b0a11", +".HG c #0b0a12", +"anS c #0b0a13", +"anT c #0b0a14", +"anU c #0b0a15", +"aDC c #0b0a17", +"alP c #0b0a19", +"ab3 c #0b0a1e", +".Kb c #0b0a35", +"#kS c #0b0a3b", +"#jr c #0b0a3f", +".kc c #0b0a42", +"#f6 c #0b0a47", +".l4 c #0b0a49", +"anQ c #0b0b0f", +"anR c #0b0b10", +"aj8 c #0b0b11", +"aP1 c #0b0b12", +".GA c #0b0b14", +"aEO c #0b0b19", +"alK c #0b0b1a", +"ajz c #0b0b1b", +"ab4 c #0b0b21", +".Ke c #0b0b22", +".5H c #0b0b31", +"#kN c #0b0b36", +".V9 c #0b0b3a", +"#jo c #0b0b3b", +".Wa c #0b0b43", +".f6 c #0b0b49", +"asy c #0b0c0d", +"azN c #0b0c0e", +"aA0 c #0b0c10", +"amh c #0b0c11", +"awf c #0b0c12", +"aKg c #0b0c13", +"akA c #0b0c16", +"ajx c #0b0c17", +"alJ c #0b0c18", +"ahm c #0b0c19", +"ajy c #0b0c1a", +"#MV c #0b0c26", +"#ma c #0b0c30", +"#jm c #0b0c33", +"#hy c #0b0c34", +"#kG c #0b0c35", +".cX c #0b0c3e", +".Rf c #0b0c42", +".ez c #0b0c46", +".ML c #0b0c47", +".eC c #0b0c48", +".#4 c #0b0c49", +".hA c #0b0c4b", +".Hx c #0b0d0b", +"as3 c #0b0d0e", +"aKh c #0b0d14", +"aQn c #0b0d15", +"aCd c #0b0d18", +"#D1 c #0b0d2a", +".Li c #0b0d3a", +"#mk c #0b0d3b", +".PB c #0b0d40", +".f3 c #0b0d44", +".7a c #0b0d4a", +"aLp c #0b0e15", +".0B c #0b0e16", +"ae3 c #0b0e28", +".iO c #0b0e34", +".hs c #0b0e35", +".JZ c #0b0e3a", +".Io c #0b0e3b", +".ga c #0b0e4b", +"aMn c #0b0f15", +"aat c #0b0f29", +".Ih c #0b0f3b", +".eI c #0b0f4c", +"azO c #0b1012", +"aLs c #0b1017", +"#ww c #0b1019", +".E# c #0b1024", +"#Ue c #0b102c", +"#k2 c #0b1033", +"#Fv c #0b1035", +"#jv c #0b1038", +".uw c #0b1043", +".s7 c #0b1044", +".V2 c #0b104c", +".de c #0b1053", +"aQ# c #0b1110", +"#hO c #0b1139", +"aD1 c #0b120a", +"auO c #0b122a", +"#jz c #0b1237", +"aL6 c #0b183a", +".Qx c #0c0000", +"a#x c #0c0001", +".R7 c #0c0004", +"ad0 c #0c0005", +"aH6 c #0c0009", +"#1V c #0c0011", +"#nO c #0c0026", +"aiU c #0c0100", +".Rt c #0c0106", +"#Kc c #0c010c", +".Dd c #0c0200", +"abZ c #0c020a", +"ayV c #0c0211", +".FS c #0c0214", +"aHw c #0c0215", +"amc c #0c0301", +".PY c #0c0307", +".LJ c #0c030b", +"akM c #0c030d", +"#9V c #0c030e", +"#zr c #0c031c", +"#bF c #0c0320", +".z# c #0c032f", +".YA c #0c0400", +"agI c #0c0403", +".KN c #0c0404", +".XS c #0c040a", +".Rq c #0c040c", +"#Os c #0c0410", +"#MZ c #0c0411", +"#J1 c #0c0413", +".7n c #0c0414", +"#eG c #0c0427", +"adW c #0c042a", +"#uV c #0c042e", +".an c #0c0435", +"aC2 c #0c0507", +"aqf c #0c0509", +"apw c #0c050a", +"aBk c #0c050d", +"aHv c #0c0517", +"#6a c #0c0522", +".B9 c #0c0533", +"a#B c #0c0605", +".B. c #0c0606", +"#6K c #0c0607", +".Ep c #0c0608", +"aAA c #0c0609", +".Ma c #0c060a", +"aBC c #0c060b", +"aK3 c #0c060c", +"aA3 c #0c060e", +"aKf c #0c0610", +"aJv c #0c0611", +".Ca c #0c0629", +".sI c #0c0635", +".Gt c #0c0706", +"agJ c #0c0707", +"ae. c #0c0709", +"azj c #0c070a", +".G. c #0c070b", +"am5 c #0c0710", +"aiR c #0c0715", +"#eX c #0c0723", +".A3 c #0c0726", +"#dm c #0c072c", +"#Yw c #0c072d", +".Gp c #0c0807", +"#8r c #0c080a", +"aNa c #0c080c", +"amI c #0c080d", +"ap5 c #0c080e", +"aBn c #0c080f", +"aIT c #0c0811", +"QtO c #0c081b", +"#Rg c #0c082b", +"#VB c #0c082c", +"#pc c #0c082d", +".0H c #0c082f", +".p2 c #0c083a", +"ax3 c #0c090a", +"aN6 c #0c090d", +"aqp c #0c090f", +"afw c #0c0910", +"aO1 c #0c0912", +".FC c #0c0916", +"an9 c #0c0919", +"#4v c #0c0921", +"#Ri c #0c0923", +"#7P c #0c0927", +"#ZY c #0c0928", +"ax1 c #0c0a0b", +"aIl c #0c0a0c", +"aPQ c #0c0a0d", +"ala c #0c0a0e", +".F6 c #0c0a0f", +"ai5 c #0c0a11", +"aPF c #0c0a12", +"aoZ c #0c0a13", +"ao0 c #0c0a14", +"anZ c #0c0a15", +"apI c #0c0a16", +"at4 c #0c0a17", +"ao2 c #0c0a19", +"an8 c #0c0a1a", +"ahv c #0c0a1f", +"#zE c #0c0a2d", +".Pw c #0c0a39", +".oU c #0c0a3c", +".V5 c #0c0a3d", +".nj c #0c0a40", +".LK c #0c0b0d", +"aoV c #0c0b0f", +"aoW c #0c0b10", +"aoX c #0c0b11", +"aoY c #0c0b12", +"apH c #0c0b13", +"azG c #0c0b15", +"aqD c #0c0b16", +"aNV c #0c0b17", +"aaq c #0c0b19", +"aF7 c #0c0b1a", +"#wJ c #0c0b26", +"#qD c #0c0b2a", +".rv c #0c0b2d", +".N1 c #0c0b39", +"#nx c #0c0b3a", +".XE c #0c0b3c", +"#ny c #0c0b3e", +"#l9 c #0c0b3f", +"aN7 c #0c0c0f", +"auE c #0c0c10", +"avO c #0c0c11", +"atS c #0c0c12", +"aGg c #0c0c14", +"ayE c #0c0c17", +"aga c #0c0c1a", +"amS c #0c0c1b", +"#zx c #0c0c2d", +"#D3 c #0c0c2e", +"#hz c #0c0c39", +".MG c #0c0c3a", +"#kT c #0c0c3b", +"#hI c #0c0c3c", +"#kH c #0c0c3f", +".N8 c #0c0c48", +".Iu c #0c0c4c", +"aA1 c #0c0d10", +"azP c #0c0d11", +"avf c #0c0d12", +"auC c #0c0d13", +"aEY c #0c0d14", +"amO c #0c0d16", +"akB c #0c0d18", +"aeU c #0c0d19", +"aeV c #0c0d1b", +"aiq c #0c0d1c", +"agj c #0c0d24", +"#hG c #0c0d34", +"#kM c #0c0d35", +".MB c #0c0d3a", +"#f5 c #0c0d40", +".PM c #0c0d43", +".Lm c #0c0d45", +".Ln c #0c0d49", +"Qti c #0c0d4b", +".kq c #0c0d52", +"amN c #0c0e16", +"aCc c #0c0e19", +"#kW c #0c0e3c", +"Qtm c #0c0e42", +".SJ c #0c0e47", +".SK c #0c0e4b", +".VZ c #0c0e50", +"aOZ c #0c0f11", +".fY c #0c0f35", +".2w c #0c0f3a", +".N4 c #0c0f3b", +".dq c #0c0f3e", +".dc c #0c0f4c", +"aPU c #0c1011", +"#DX c #0c101d", +"a.K c #0c1026", +".go c #0c103f", +".bE c #0c104d", +".Ub c #0c105d", +".5E c #0c1141", +".rK c #0c1147", +".oV c #0c1244", +"ab9 c #0c1633", +"aaw c #0c1635", +"a.P c #0c183a", +"ahy c #0c183d", +".L8 c #0d0000", +".Nt c #0d0003", +".Qy c #0d0004", +"azX c #0d000c", +".FK c #0d0011", +"aJJ c #0d0100", +"afk c #0d0107", +"aze c #0d0201", +".xQ c #0d0204", +"alT c #0d0208", +"abY c #0d020b", +".M7 c #0d0300", +".O4 c #0d0302", +"az7 c #0d0310", +"acB c #0d0400", +"al. c #0d0403", +".2H c #0d0406", +".EA c #0d0407", +".O5 c #0d0408", +"ahN c #0d0409", +"aph c #0d040a", +".Rs c #0d040b", +".ST c #0d040c", +"aIR c #0d040e", +".H# c #0d0434", +"#6L c #0d0504", +"#97 c #0d0506", +"aK9 c #0d050b", +".Kj c #0d050d", +"ayX c #0d050f", +".Gf c #0d0510", +"ajY c #0d0511", +"#dn c #0d0515", +"#hY c #0d0519", +"aAB c #0d060a", +"aqh c #0d060b", +".En c #0d060d", +".HQ c #0d0613", +"#PX c #0d0614", +"#bP c #0d0615", +"ayL c #0d0618", +".MW c #0d0619", +".R8 c #0d0707", +".5R c #0d0709", +"aAm c #0d070b", +"arc c #0d070c", +"akG c #0d0714", +"#bE c #0d0732", +"#wF c #0d0738", +"aIj c #0d0808", +"ajE c #0d0817", +"agz c #0d081e", +"afj c #0d0820", +".lN c #0d082c", +"#bN c #0d082d", +".j9 c #0d082f", +".VQ c #0d083c", +"#tj c #0d0847", +"apz c #0d090d", +"aq1 c #0d090e", +"ak3 c #0d0911", +"aiy c #0d0919", +"#Yx c #0d0928", +"#VC c #0d0929", +"#ZX c #0d092c", +".rn c #0d0931", +".VU c #0d0932", +".Sv c #0d0938", +".Sr c #0d0939", +".Fo c #0d093b", +".oX c #0d0940", +".CD c #0d0a0a", +"atG c #0d0a0c", +"aO9 c #0d0a0d", +"are c #0d0a0e", +"azE c #0d0a14", +"alN c #0d0a17", +"an3 c #0d0a1a", +"#Rh c #0d0a29", +"#c0 c #0d0a35", +".Q6 c #0d0a38", +".Q1 c #0d0a39", +"#wC c #0d0a3e", +"#u5 c #0d0a42", +"aN4 c #0d0b0d", +".EU c #0d0b0f", +"aoU c #0d0b10", +"aqw c #0d0b13", +"aqx c #0d0b14", +"aqy c #0d0b15", +".Kf c #0d0b16", +"arp c #0d0b17", +"at3 c #0d0b18", +"amU c #0d0b19", +"ao3 c #0d0b1a", +"an7 c #0d0b1b", +"#A6 c #0d0b2c", +"#nQ c #0d0b32", +".q# c #0d0b34", +".uo c #0d0b36", +".Pr c #0d0b39", +".G1 c #0d0b3c", +".oQ c #0d0b3d", +".nh c #0d0b40", +".lZ c #0d0b4a", +".Hc c #0d0c0a", +".LO c #0d0c0e", +"aqt c #0d0c10", +"aqu c #0d0c11", +"aqv c #0d0c12", +"arm c #0d0c13", +"arn c #0d0c14", +"aro c #0d0c15", +"awh c #0d0c16", +"ao1 c #0d0c17", +"aGf c #0d0c18", +"aEP c #0d0c1a", +"akK c #0d0c1c", +"#Oq c #0d0c21", +"#Cz c #0d0c2a", +"#A3 c #0d0c34", +"#u1 c #0d0c35", +"#eK c #0d0c3c", +"#ql c #0d0c3e", +"#hA c #0d0c3f", +"#kR c #0d0c40", +".kl c #0d0c4e", +"aOB c #0d0d07", +"atU c #0d0d13", +"aEX c #0d0d17", +"aGe c #0d0d1a", +"aeW c #0d0d1c", +"#wy c #0d0d31", +"#rT c #0d0d3c", +"#kO c #0d0d3d", +"#mg c #0d0d3e", +".bm c #0d0d40", +".SL c #0d0d4b", +".Lo c #0d0d4c", +".iT c #0d0d4d", +"aCh c #0d0e12", +"atW c #0d0e13", +"auD c #0d0e14", +"aIU c #0d0e15", +"aDH c #0d0e16", +"aDz c #0d0e1a", +"agb c #0d0e1c", +"air c #0d0e1d", +"akP c #0d0e1f", +".zo c #0d0e30", +"#jj c #0d0e36", +"#rN c #0d0e37", +"#hD c #0d0e38", +".qe c #0d0e40", +".c8 c #0d0e47", +".ab c #0d0e50", +"aFm c #0d0f0b", +"as8 c #0d0f10", +"aNk c #0d0f16", +"aQk c #0d0f17", +"aEN c #0d0f1c", +"adC c #0d0f26", +"#zv c #0d0f29", +"#A0 c #0d0f2a", +".eT c #0d0f35", +".ev c #0d0f36", +"#f4 c #0d0f3b", +".ux c #0d0f3d", +".2q c #0d0f3e", +".#7 c #0d0f44", +".XJ c #0d0f4c", +".l6 c #0d0f51", +"a.J c #0d1023", +"aiB c #0d1028", +".s9 c #0d103f", +".PH c #0d1042", +".0S c #0d1045", +".a. c #0d104d", +"Qtu c #0d104e", +"aQb c #0d110d", +"aLq c #0d1117", +"#DY c #0d1121", +".4d c #0d1141", +".qn c #0d114a", +"#Lz c #0d1228", +"adD c #0d122a", +".A2 c #0d1232", +".36 c #0d1243", +".bG c #0d1255", +"aQ. c #0d1315", +"Qtw c #0d1355", +"aMq c #0d141a", +"Qtv c #0d1657", +".dp c #0d173f", +".Hs c #0e0000", +"aDL c #0e0008", +"azW c #0e000e", +".L7 c #0e0100", +"#nU c #0e0105", +"#0A c #0e010a", +"aC1 c #0e0202", +".KL c #0e0301", +"au1 c #0e0305", +"aGK c #0e030d", +"amb c #0e0401", +".KM c #0e0403", +"a#w c #0e0404", +".xO c #0e0406", +"aGY c #0e0408", +"az8 c #0e040f", +"aiP c #0e0415", +"#pb c #0e042a", +"#8t c #0e0506", +".Qz c #0e0508", +"apv c #0e0509", +"acV c #0e050a", +"#eZ c #0e050c", +"#Z2 c #0e0516", +"#mo c #0e052c", +".Rk c #0e0531", +".SN c #0e0532", +".CQ c #0e0600", +"#46 c #0e0604", +".Ex c #0e0606", +".zx c #0e0607", +"arb c #0e060c", +"#qE c #0e060d", +".IM c #0e060e", +"#gs c #0e0616", +"afh c #0e0629", +".TC c #0e0707", +"aAz c #0e070b", +"aal c #0e0712", +".Wd c #0e0716", +"#yd c #0e0720", +".PP c #0e072f", +".qg c #0e0734", +".W5 c #0e0805", +"afs c #0e0808", +"#8s c #0e0809", +".Or c #0e080b", +"asD c #0e080d", +"ahM c #0e0814", +"ahq c #0e0819", +"#gj c #0e0833", +".t9 c #0e0836", +"#rU c #0e083f", +".Vg c #0e090a", +"aBV c #0e090e", +"#2t c #0e0913", +"ajZ c #0e0915", +"ajD c #0e0917", +".hT c #0e0922", +"#4t c #0e0928", +".b1 c #0e0935", +"#eJ c #0e0942", +"aq6 c #0e0a0d", +"amL c #0e0a0e", +"aP2 c #0e0a13", +".5Q c #0e0a17", +"#6b c #0e0a28", +"#th c #0e0a44", +".kg c #0e0a50", +".kh c #0e0a52", +"ar. c #0e0b0d", +"alE c #0e0b0f", +"arj c #0e0b11", +"ahW c #0e0b12", +"aLt c #0e0b13", +"auJ c #0e0b18", +"an4 c #0e0b1b", +"adA c #0e0b1c", +"agf c #0e0b1f", +"ahu c #0e0b20", +".oI c #0e0b3c", +".Rj c #0e0b40", +".nn c #0e0b42", +".YB c #0e0c08", +"apG c #0e0c11", +"awe c #0e0c12", +"atP c #0e0c13", +"ar5 c #0e0c14", +"ar6 c #0e0c15", +"ar7 c #0e0c16", +"anY c #0e0c17", +".HE c #0e0c18", +"at8 c #0e0c19", +"an2 c #0e0c1b", +"apJ c #0e0c1c", +".uk c #0e0c25", +".oM c #0e0c36", +".T8 c #0e0c40", +".ns c #0e0c46", +"ar2 c #0e0d11", +"ar3 c #0e0d12", +"ar4 c #0e0d13", +"asJ c #0e0d14", +"asK c #0e0d15", +"aBX c #0e0d16", +"atg c #0e0d17", +"anX c #0e0d18", +"aEW c #0e0d19", +"aDE c #0e0d1b", +"amV c #0e0d1c", +"amW c #0e0d1d", +"#X. c #0e0d21", +"agi c #0e0d24", +".ui c #0e0d25", +".LD c #0e0d27", +".sS c #0e0d29", +"#te c #0e0d3b", +"#md c #0e0d3d", +"#nG c #0e0d3e", +"#oZ c #0e0d40", +"#hL c #0e0d41", +".lQ c #0e0d42", +".e3 c #0e0d46", +".Eh c #0e0e0d", +"atf c #0e0e12", +"atX c #0e0e13", +"atV c #0e0e14", +"aCg c #0e0e15", +"azK c #0e0e16", +"aAZ c #0e0e17", +"aAY c #0e0e18", +"aCf c #0e0e19", +"aDG c #0e0e1a", +"agc c #0e0e1e", +"#D6 c #0e0e2b", +"#IF c #0e0e31", +"#qm c #0e0e38", +"#nH c #0e0e3c", +"#mh c #0e0e3d", +".#X c #0e0e41", +".Un c #0e0e48", +".SC c #0e0e4f", +"awg c #0e0f15", +"ayt c #0e0f16", +"aAV c #0e0f19", +"alQ c #0e0f20", +".38 c #0e0f3c", +"Qt. c #0e0f41", +".GX c #0e0f44", +".by c #0e0f46", +".MK c #0e0f47", +"#eN c #0e0f49", +".Rh c #0e0f4b", +".iZ c #0e0f51", +"azR c #0e1016", +".c1 c #0e1037", +".5G c #0e103c", +"#hN c #0e103e", +".XI c #0e104a", +"aIW c #0e1119", +".bp c #0e1138", +".Q9 c #0e113d", +".Pz c #0e113e", +".Ob c #0e1143", +".Iy c #0e1144", +".0T c #0e1148", +".l5 c #0e1151", +"aMo c #0e1219", +".N3 c #0e123d", +".Za c #0e124a", +".XB c #0e124d", +".o5 c #0e124e", +".Uc c #0e1250", +".2o c #0e1345", +".0L c #0e1348", +"#Ud c #0e142b", +"awp c #0e1a37", +".FT c #0f0000", +".Ru c #0f0001", +"a#A c #0f0002", +"#Cm c #0f0017", +"#rZ c #0f0022", +"au0 c #0f0101", +"#8i c #0f0102", +".L6 c #0f0200", +"#gm c #0f0201", +"acA c #0f0300", +"axZ c #0f0303", +"aCE c #0f0305", +"aHC c #0f0308", +"adr c #0f030b", +"ayM c #0f030d", +"ayW c #0f0310", +"#r0 c #0f0327", +"aj4 c #0f0401", +".CL c #0f0402", +".CK c #0f0404", +"atz c #0f0406", +".01 c #0f0407", +"ap4 c #0f0408", +"agB c #0f0409", +".Hr c #0f0414", +"agw c #0f0423", +"#qB c #0f0429", +".f# c #0f050b", +"ab0 c #0f050d", +"aPv c #0f050e", +".EQ c #0f0600", +"aj1 c #0f0605", +".zv c #0f0608", +"ady c #0f0609", +"aMe c #0f060c", +"#PY c #0f0612", +"#Fk c #0f0613", +"#M6 c #0f061a", +".Oj c #0f061e", +"#6# c #0f0620", +"#eP c #0f0623", +".AB c #0f062a", +".DV c #0f0635", +"ak6 c #0f0707", +".Ey c #0f0709", +"al7 c #0f070d", +"aBm c #0f0710", +"aE0 c #0f0711", +"#4s c #0f0724", +"agx c #0f0725", +"#bp c #0f073e", +".02 c #0f080b", +".Gi c #0f080d", +".PX c #0f080e", +".LF c #0f080f", +"aKe c #0f0812", +"#SV c #0f0813", +".XN c #0f0815", +".Ho c #0f0817", +"ayI c #0f081a", +".A6 c #0f0909", +".A7 c #0f090a", +"aq. c #0f090c", +".0X c #0f0910", +"azD c #0f0914", +"akF c #0f0915", +"#Lr c #0f091b", +"#7O c #0f0922", +"#a. c #0f0925", +"#Uf c #0f0927", +"#2K c #0f0928", +"#J7 c #0f092f", +".s0 c #0f0933", +"#qt c #0f0940", +".Gm c #0f0a06", +"ax5 c #0f0a0d", +".G# c #0f0a0e", +"aNd c #0f0a0f", +"aQj c #0f0a15", +"akH c #0f0a18", +"aHu c #0f0a1b", +".xv c #0f0a22", +"#a# c #0f0a2e", +"#tm c #0f0a36", +"#o8 c #0f0a40", +".l8 c #0f0a48", +"aAC c #0f0b0f", +"arO c #0f0b10", +"arP c #0f0b11", +"aO2 c #0f0b14", +"aan c #0f0b16", +".4h c #0f0b19", +".2B c #0f0b1a", +"aiz c #0f0b1c", +".zr c #0f0b2f", +".VV c #0f0b36", +"amH c #0f0c10", +"aqs c #0f0c12", +"auA c #0f0c13", +".F4 c #0f0c17", +"alM c #0f0c19", +"amT c #0f0c1a", +"ajF c #0f0c1b", +"an6 c #0f0c1c", +"#rV c #0f0c40", +"#tg c #0f0c42", +"ax7 c #0f0d11", +"anP c #0f0d12", +"axc c #0f0d17", +"anW c #0f0d18", +"anV c #0f0d19", +"aqG c #0f0d1c", +"an5 c #0f0d1d", +".7c c #0f0d42", +".nl c #0f0d43", +".nq c #0f0d47", +".ki c #0f0d50", +"aN8 c #0f0e11", +"atc c #0f0e12", +"atY c #0f0e14", +"atZ c #0f0e15", +"at0 c #0f0e16", +"at1 c #0f0e17", +"aAT c #0f0e18", +"ati c #0f0e19", +"aCe c #0f0e1a", +"aDF c #0f0e1b", +"aGb c #0f0e1d", +"aHA c #0f0e1e", +"#nF c #0f0e42", +"#m. c #0f0e43", +".Ue c #0f0e49", +".l0 c #0f0e4d", +"awJ c #0f0f15", +"azL c #0f0f16", +"azJ c #0f0f18", +"aEV c #0f0f1c", +"ahn c #0f0f1e", +"#o0 c #0f0f3a", +".DF c #0f0f3d", +".MS c #0f0f40", +".f5 c #0f0f4c", +".MM c #0f0f4e", +"aPc c #0f1014", +"axa c #0f1015", +"ayu c #0f1016", +"aAW c #0f101a", +".BK c #0f101c", +"agk c #0f1028", +"#Ct c #0f102b", +".gm c #0f1036", +"Qtb c #0f103d", +".0M c #0f1042", +".8Y c #0f104a", +".eB c #0f104b", +".hz c #0f104d", +"az1 c #0f1119", +".hr c #0f113b", +".#Z c #0f113d", +".0R c #0f113e", +".rM c #0f1142", +"#eM c #0f114f", +"ayz c #0f1214", +"#MT c #0f1230", +".#0 c #0f1238", +"Qtd c #0f1239", +".c0 c #0f123b", +".5F c #0f1241", +".37 c #0f1243", +".SF c #0f1245", +".V3 c #0f124f", +"#K# c #0f132a", +".nH c #0f1351", +".V1 c #0f135b", +".f9 c #0f1449", +"aPb c #0f1719", +"ayC c #0f171e", +"am9 c #0f192e", +"aMp c #0f1b21", +".N# c #100000", +".Wk c #100001", +"a#y c #100002", +"aGL c #10000e", +"#P4 c #100010", +"#Ye c #100100", +"azV c #100110", +"#gn c #100200", +".L5 c #100201", +"avz c #100202", +".Ec c #100205", +"aQr c #10020f", +"#OB c #100214", +"#DU c #100217", +".SO c #100224", +"aM7 c #100300", +"aot c #100306", +".CC c #10030b", +"#CA c #10031c", +"#2s c #100402", +".Zs c #100409", +"aJu c #10040c", +"#mu c #10040d", +"ajX c #100410", +"#gl c #100411", +"ahI c #10041d", +".xP c #100507", +"aIQ c #100511", +".A4 c #100512", +"#LC c #100516", +"#zq c #100520", +".CN c #100600", +"#JZ c #100617", +"ahJ c #10061e", +"#X# c #100620", +"afg c #10062a", +".i0 c #100635", +".EN c #100702", +".De c #100704", +"apu c #10070b", +"aMi c #10070d", +"aA2 c #100711", +"#k4 c #100715", +"aG. c #100719", +"#jB c #10071a", +"#.z c #100723", +"#3l c #100806", +"amd c #100807", +"#.H c #10080f", +"aOA c #100907", +"aP. c #10090b", +"aDZ c #10090d", +"a#d c #10090e", +".XQ c #100910", +"aPq c #100915", +"#.G c #100918", +"#s9 c #100937", +"#u9 c #100939", +"aMZ c #100a0a", +".B# c #100a0b", +".4i c #100a0d", +".2C c #100a0e", +"aix c #100a1a", +".vI c #100a25", +".yY c #100a26", +".Y5 c #100a29", +"#Fp c #100a30", +"##3 c #100a35", +"#nI c #100a41", +".Gc c #100b0e", +"aN. c #100b10", +"#08 c #100b18", +"aES c #100b1a", +"aF8 c #100b1c", +".bI c #100b47", +"agK c #100c0e", +"alD c #100c10", +"aPr c #100c14", +"alL c #100c1a", +"ais c #100c1b", +"#9t c #100c26", +".88 c #100c30", +"#M2 c #100c34", +".oF c #100c43", +".ac c #100c48", +".IJ c #100d0e", +"au6 c #100d0f", +"aik c #100d11", +"avc c #100d14", +"aOb c #100d15", +"#YC c #100d1d", +".MV c #100d2c", +"#bB c #100d35", +".Lz c #100d38", +"#qu c #100d40", +".kC c #100d47", +"anM c #100e13", +"ax# c #100e15", +"ar9 c #100e19", +"aqC c #100e1a", +"aqF c #100e1d", +"aqE c #100e1e", +".xK c #100e35", +".B7 c #100e36", +".Y8 c #100e3f", +"#rS c #100e44", +".mg c #100e46", +".kr c #100e53", +"aJQ c #100f11", +"asG c #100f13", +"axb c #100f14", +"azt c #100f18", +"axK c #100f19", +"ar8 c #100f1a", +"aEU c #100f1d", +"#x7 c #100f36", +"#td c #100f38", +".DH c #100f3d", +"#kI c #100f44", +"#jh c #100f45", +".MD c #100f4a", +"axE c #101016", +"ays c #101018", +"aPl c #101019", +"aAX c #10101b", +"aGd c #10101e", +"#db c #101038", +"#nz c #10103d", +"#o7 c #10103e", +"#qs c #10103f", +"#o6 c #101041", +".bZ c #101042", +".PL c #101043", +".Le c #10104a", +".iS c #10104e", +"aCb c #10111c", +".Kc c #10113b", +"Qtl c #101148", +"Qtk c #10114a", +".bw c #10114b", +"#Op c #10122c", +"Qtc c #10123b", +".bo c #10123e", +".qp c #101246", +".c7 c #10124b", +".Ud c #10124f", +".Xz c #101253", +"aPn c #10131b", +".bS c #101342", +".J7 c #101345", +".2p c #101346", +".SD c #101353", +".md c #101354", +".hC c #101451", +".d# c #10154a", +".Cz c #10172f", +".rz c #101839", +"asd c #101f42", +".Na c #110000", +"a#z c #110002", +".Ot c #110200", +"#pa c #110227", +"aa7 c #110300", +".5V c #110302", +".L4 c #110303", +"aA5 c #11030b", +"#jC c #11030f", +"#zG c #110319", +"ahO c #110402", +"aL2 c #110404", +".TB c #110407", +".gE c #110409", +"aLk c #11040b", +"aGi c #11040d", +"#G3 c #110410", +"azT c #110413", +"#.l c #110428", +".O3 c #110500", +"aPy c #11050c", +"#yf c #11050f", +"adu c #11060c", +"#2I c #110720", +"#u8 c #11073b", +"#ad c #11080f", +"#gt c #110810", +".zs c #11081a", +"#4r c #110821", +".80 c #110825", +"adV c #11082f", +".EF c #110906", +"ani c #11090f", +"#PZ c #110916", +".ku c #110929", +"#bq c #11093f", +".Ew c #110a09", +"arT c #110a0f", +"apx c #110a10", +"aIX c #110a11", +".Em c #110a13", +".Zn c #110a15", +"#eY c #110a1a", +"#Ou c #110a1d", +"#Ov c #110a28", +".Ha c #110a29", +".Ba c #110b0c", +"aam c #110b16", +"aET c #110b1b", +"ayF c #110b1c", +".rD c #110b36", +"aNg c #110c11", +"ahr c #110c1e", +"#eV c #110c31", +"#mi c #110c42", +".Q3 c #110c48", +".eL c #110c49", +"aft c #110d10", +"akx c #110d11", +"amK c #110d12", +"apm c #110d13", +"aOs c #110d17", +".pS c #110d1b", +".LE c #110d1c", +".DX c #110d1d", +"age c #110d20", +".ox c #110d23", +".Ps c #110d49", +".CF c #110e0d", +".CG c #110e0e", +"atK c #110e10", +".H3 c #110e11", +"aqr c #110e14", +"akI c #110e1c", +"aeX c #110e23", +".DN c #110e32", +".G0 c #110e41", +"#u4 c #110e42", +".no c #110e45", +".NX c #110e49", +"anL c #110f14", +"axA c #110f16", +"axJ c #110f18", +"ars c #110f1a", +"at2 c #110f1b", +"akJ c #110f1e", +"agg c #110f24", +"aeY c #110f25", +".B2 c #110f37", +".vX c #110f38", +".o8 c #110f43", +"#.x c #110f44", +"#hB c #110f46", +"av8 c #111018", +"ayb c #111019", +"aqB c #11101b", +"aGc c #11101f", +".Od c #11103f", +".ka c #111043", +"#jp c #111044", +"#jq c #111045", +"#o5 c #111046", +".np c #111049", +".dy c #11104a", +"axF c #111117", +"aI# c #11111a", +"aC. c #11111c", +".kI c #111132", +"#m# c #111140", +".Fi c #111144", +".f7 c #11114f", +"azH c #11121b", +"aC# c #11121d", +".Zc c #111247", +".#6 c #111249", +".bx c #11124a", +".PN c #11124b", +".Y1 c #111327", +".cZ c #11133f", +".iP c #111346", +".Zi c #11134b", +".kA c #111355", +".Lt c #111446", +".f8 c #111451", +".Lq c #111454", +".cb c #111539", +".In c #11153f", +".eE c #111552", +".MO c #111555", +".#9 c #11164b", +"Qtq c #11164c", +".aa c #111659", +".zq c #11183c", +".bF c #11195b", +".vW c #111a44", +"adJ c #112650", +".NB c #112752", +"#41 c #120000", +".Ns c #120105", +"aKb c #12020a", +"#Z3 c #120217", +".Bc c #120300", +".Dc c #120400", +".L3 c #120405", +".L2 c #120406", +".eX c #120423", +".r2 c #120500", +"#42 c #120505", +"aHy c #120519", +"aOS c #120602", +"ak9 c #120806", +"#6H c #120808", +"adv c #12080c", +".dL c #12081c", +".CS c #120900", +"aOk c #120907", +".ED c #12090b", +".Zr c #12090e", +"#go c #120910", +"aKd c #120913", +"aQe c #120915", +".8W c #12092d", +".AL c #120936", +"Qtz c #120937", +"#J6 c #120a28", +".C. c #120a37", +".Ev c #120b09", +"aEA c #120b10", +".Gg c #120b14", +"aP3 c #120b15", +"akE c #120b17", +"ajC c #120b18", +"agA c #120b19", +"aiw c #120b1a", +"#IC c #120b28", +"ab2 c #120c13", +"aO3 c #120c15", +".up c #120c36", +"#.y c #120c37", +"#kU c #120c43", +".Gd c #120d0f", +".jb c #120d20", +".XM c #120d24", +".Y6 c #120d32", +"aiZ c #120e11", +"as1 c #120e14", +"aPf c #120e17", +"aQh c #120e19", +".0W c #120e20", +"#Lw c #120e36", +"#c1 c #120e44", +".lW c #120e50", +"aq7 c #120f11", +"ate c #120f15", +"amj c #120f16", +"aAS c #120f1a", +"aht c #120f23", +".T7 c #120f3e", +"#nJ c #120f43", +".ni c #120f45", +"#ey c #120f4a", +"azo c #121013", +"aPT c #121014", +"aoQ c #121015", +"axI c #121018", +"arr c #12101b", +"aqA c #12101c", +"agh c #121026", +".qb c #121039", +".qq c #121041", +"#dd c #121044", +"axH c #121117", +"av7 c #121119", +"aFX c #12111a", +"ath c #12111c", +"aar c #12111f", +"aHB c #121120", +".A0 c #12112f", +".rN c #121140", +"#qr c #121143", +"#kP c #121145", +"#me c #121146", +"#kQ c #121147", +"azI c #12121b", +"ae0 c #121229", +".Og c #121247", +".XD c #121249", +".e2 c #12124a", +"##2 c #12124d", +".bs c #121250", +".hB c #121252", +".o7 c #12134a", +".N7 c #12134b", +".eD c #12134e", +".c3 c #12134f", +".V4 c #121350", +".eu c #121440", +".Zh c #121444", +"Qtn c #121447", +".Iq c #121448", +".Zj c #12144e", +".jc c #121537", +".oW c #12154a", +".d. c #121552", +".Rc c #121555", +"aQc c #121611", +".eV c #121645", +".bA c #121653", +".J5 c #121656", +"aQm c #12171e", +"aIV c #12171f", +".xJ c #121a42", +".A1 c #121b37", +".gn c #121b44", +".zp c #121c3e", +".xI c #121e42", +".vV c #121f45", +"agm c #121f46", +".YC c #122549", +".Ow c #130000", +"aQy c #13000b", +"aH7 c #13000f", +"arJ c #130100", +"#DS c #130114", +".Vf c #130200", +"aIH c #13020b", +"#Fy c #13030f", +"QtK c #130424", +"azU c #130514", +"#AU c #13051d", +".pk c #130600", +"aqX c #130608", +"acz c #130700", +"aPN c #130702", +"aP6 c #130712", +".PQ c #130723", +".xS c #13080a", +".CU c #130903", +".gD c #130910", +".Az c #130936", +".eM c #130938", +".CR c #130a00", +"am4 c #130a0f", +".7o c #130a11", +".9# c #130a12", +".MX c #130a16", +".rF c #130a1f", +"#2J c #130a26", +".s1 c #130a2c", +".gy c #130a2f", +".gf c #130a38", +"afr c #130b09", +"#bQ c #130b12", +"aOq c #130b16", +"ajB c #130b18", +"aiv c #130b19", +".Uq c #130b1b", +"#df c #130b27", +"#G8 c #130b29", +".gp c #130b3b", +".Gj c #130c0e", +"aoJ c #130c12", +"aPs c #130c15", +"aO4 c #130c16", +"aQs c #130c18", +"#Lt c #130c1a", +"ahp c #130c1c", +"#Fo c #130c2a", +"#ge c #130c4f", +"asC c #130d12", +"aDk c #130d13", +".FP c #130d1d", +".zb c #130d29", +"#jt c #130d44", +".Ge c #130e10", +"aN# c #130e13", +"aQo c #130e16", +"#wt c #130e32", +".p1 c #130e44", +".dg c #130e4a", +"atH c #130f12", +"aeO c #130f13", +"ayp c #130f19", +"aiA c #130f21", +"#x2 c #130f29", +"#gr c #130f33", +".j6 c #130f36", +"ax2 c #131011", +"atF c #131012", +"ajG c #131020", +".rC c #13103f", +".oG c #131045", +"#eA c #13104c", +".lV c #131051", +"apF c #131116", +"atR c #131117", +"axD c #131118", +"atC c #131119", +"aya c #13111b", +"aqz c #13111c", +".q. c #13113b", +".MR c #131140", +"#mj c #131144", +"#gi c #131146", +"#mf c #131147", +".nv c #13114b", +"axG c #131217", +"awy c #13121a", +".t. c #13123e", +"#kJ c #131243", +"#hJ c #131246", +"#hK c #131248", +".Ek c #131312", +"ayr c #13131c", +"aCa c #13131e", +"#wz c #13133a", +".uy c #13133e", +".Oe c #131345", +".Z. c #13134a", +"#bC c #13134e", +".#2 c #131353", +".iU c #131354", +".Ld c #131447", +".Xy c #131453", +".fX c #131541", +".JW c #131548", +".hv c #131549", +".PC c #13154c", +".Zb c #13154d", +".XC c #13154f", +"#Cr c #131629", +".am c #131645", +".Ii c #131648", +".PG c #13164e", +".Oa c #13164f", +"Qto c #131653", +".O. c #131656", +".PF c #131657", +".SE c #13174f", +".Rd c #131754", +".Ua c #131760", +".eK c #13185b", +"aBj c #14000e", +"axo c #140100", +".Nb c #140200", +"#8h c #140201", +"aIO c #14030c", +"#DT c #14041a", +"#6G c #140500", +"aHz c #140519", +".Bd c #140600", +"aLu c #14060f", +"aQz c #140610", +"#Fj c #140611", +".AV c #140618", +".ds c #140625", +"#eR c #140706", +".2G c #140708", +"ap3 c #14070a", +"#1m c #140719", +"#Ug c #14071e", +"ads c #140810", +"aIP c #140813", +"#eQ c #140815", +"aPh c #140816", +"aHx c #14081b", +"#tk c #14083d", +"aH5 c #14090d", +"aCm c #140912", +"aP5 c #140914", +"aA4 c #140a11", +".zz c #140b0d", +"aou c #140b11", +"ab1 c #140b13", +"aP4 c #140b15", +"aO5 c #140b16", +"ajA c #140b17", +"#YD c #140b1f", +"#gk c #140b27", +"##N c #140b2e", +".vF c #140b36", +".FA c #140b3b", +".Gh c #140c14", +".oZ c #140c2c", +".dw c #140c35", +"aqe c #140d11", +".vl c #140d22", +".DW c #140d2f", +".rk c #140d47", +"ayo c #140e18", +"#Iy c #140e1c", +"#gq c #140e2a", +".dz c #140e40", +"azk c #140f12", +".z9 c #140f15", +"aQf c #140f1a", +"#nT c #140f1e", +"Qty c #140f4b", +"aBW c #141014", +"#Lo c #141027", +".Ax c #141032", +".j5 c #141036", +"#gf c #14104e", +"ar1 c #141116", +"ak. c #141118", +"ajH c #141122", +"au3 c #14121a", +"arq c #14121d", +"aeZ c #141229", +".NW c #141246", +"ayq c #14131c", +"ajI c #141325", +"#ji c #141345", +"#hC c #141346", +".Y9 c #141348", +"aB9 c #14141f", +"aDA c #141420", +".DC c #141442", +".MC c #141447", +".nJ c #14144e", +".7b c #14144f", +".N9 c #141452", +".J4 c #141454", +".bQ c #14153b", +".GU c #14154a", +".Uk c #14154b", +".J2 c #14154d", +".c9 c #14154e", +".c4 c #141550", +".bt c #141552", +".MN c #141555", +".hq c #141642", +".J1 c #141649", +".#8 c #14164a", +".f1 c #14164c", +".ny c #141653", +".Ix c #141750", +".Lr c #141754", +".O# c #141755", +".Iv c #141757", +".U# c #14175d", +".J6 c #141855", +"aQg c #141c23", +"aub c #141c35", +".dd c #141c5e", +".P0 c #150000", +"aC0 c #150100", +"aQv c #150110", +".N. c #150200", +"aQw c #150210", +".Nc c #150300", +"#Vh c #150303", +"#mv c #150407", +"anh c #150505", +"aPB c #15050e", +"aMr c #15050f", +"azY c #150514", +"#qA c #150529", +".r1 c #150600", +"#ZF c #150706", +"apf c #150708", +".PR c #15071c", +"aFh c #150806", +"aLd c #15080f", +"aNn c #150811", +"aiX c #150905", +"adt c #150910", +"aPt c #150a14", +"#JY c #150a1b", +"aG# c #150a1d", +"aP# c #150b0b", +"az5 c #150b1d", +"adx c #150c0f", +"#G4 c #150c19", +"#uU c #150c2c", +".uq c #150c2e", +"#ac c #150d1d", +".xL c #150d22", +".5C c #150d26", +".rE c #150d2e", +"#gc c #150d34", +"#6f c #150e0d", +"aF9 c #150f20", +"#Lq c #150f23", +".yW c #150f2b", +"at# c #151015", +"aPd c #151017", +"adz c #151021", +".zc c #151025", +".Xw c #151039", +".DK c #15103f", +".DJ c #151041", +".Ss c #151045", +"ahs c #151123", +".Q2 c #151145", +"azq c #151216", +".Aw c #151233", +"#Fq c #151239", +"#kV c #151246", +"#ez c #151252", +"arZ c #151318", +".II c #15131c", +"aHk c #15131d", +"#wB c #151343", +"#bD c #151348", +".nr c #15134d", +"au4 c #15141c", +"aED c #15141d", +".ug c #15142d", +"#f8 c #151450", +"asH c #151518", +".mf c #151551", +".#3 c #151555", +"Qtg c #151556", +".iN c #151643", +"#f9 c #151644", +".hw c #15164b", +".bz c #15164c", +".R# c #15164e", +".ey c #151750", +".5L c #15183a", +".Ls c #151850", +".Iw c #151856", +".nx c #151953", +".gc c #151b5d", +".hN c #151f47", +".Uy c #160000", +"#Xa c #16001b", +".Rv c #160100", +".Ut c #160300", +".L9 c #160303", +"aDM c #16030b", +".rZ c #160400", +".R6 c #160404", +"#Hf c #16040e", +".SP c #16041e", +"azZ c #160515", +"aKc c #16060f", +"aPC c #160610", +".Ux c #160709", +".qE c #160800", +"aDJ c #160814", +".4l c #160908", +"aCl c #160914", +".Ct c #16091a", +".vv c #160924", +"aIY c #160a0f", +"aGa c #160a1d", +"agH c #160b09", +".EB c #160c12", +"#wp c #160c22", +".zw c #160d0f", +"aiu c #160d1a", +".qi c #160d21", +"##4 c #160d2a", +".Cj c #160d3c", +".t7 c #160d49", +"aOj c #160e09", +".Ez c #160e10", +"aqg c #160e14", +"aGh c #160e15", +".8V c #160e25", +".m. c #160e2e", +".qh c #160e2f", +"#c9 c #160e35", +".eW c #160e3f", +"aqa c #160f13", +".Xu c #160f31", +"#.w c #160f34", +"aqc c #161014", +"at. c #161015", +".C# c #161038", +"#eO c #16103b", +".gx c #16103c", +"aOu c #16111a", +"aPg c #16111c", +".E. c #161125", +".Zm c #161126", +".T2 c #16114d", +"ara c #161215", +"aPk c #16121c", +".p3 c #16123e", +"au5 c #161315", +"axB c #16131a", +".Cy c #16132c", +".Xx c #161341", +".oJ c #161342", +".qf c #161346", +"#ju c #161347", +"aqq c #161419", +"awH c #16141b", +"axs c #16141c", +"azF c #16141d", +".je c #161430", +".oN c #161446", +"#hM c #161447", +"ayc c #16151e", +".rx c #161536", +"#wA c #161540", +".kb c #16154a", +".VX c #16154b", +".Fj c #161649", +".kB c #161652", +".It c #161655", +".i6 c #16173d", +".au c #161741", +".f2 c #16174e", +".me c #161751", +".Ij c #161756", +"#DZ c #16182d", +".hD c #161a54", +"aCS c #161c3c", +".P1 c #170000", +".Ng c #170100", +".Nh c #170200", +"aGj c #17020e", +"#A7 c #170217", +"#Uh c #170311", +"aBB c #170400", +"#It c #170511", +"aos c #170605", +"aKk c #17060d", +"ap2 c #170706", +"#k5 c #170810", +".i5 c #170911", +".uv c #170a17", +".s6 c #170a18", +".hI c #170a26", +".SX c #170b11", +"aQq c #170b16", +"aCk c #170b18", +".r4 c #170c02", +".1O c #170c0a", +".jk c #170c11", +"#Iw c #170c19", +".H. c #170c46", +"adw c #170d11", +"#wq c #170d28", +"##Q c #170e33", +"aCj c #170f1c", +".e1 c #170f38", +".xs c #170f3a", +".Eu c #17100d", +"anF c #171015", +".x# c #171028", +".34 c #17102d", +"azS c #17111b", +".x. c #171128", +".7k c #17112d", +".rm c #171141", +"aFl c #17120b", +".Ga c #171215", +".as c #17122b", +".ne c #171230", +"#de c #17123c", +".Kg c #171316", +"aOr c #17131d", +"aqk c #171418", +"aEQ c #171423", +".ro c #171432", +".Y7 c #17143f", +"#c3 c #171442", +"axC c #17151c", +"axr c #17151d", +".B6 c #17153d", +"#tf c #171547", +".lX c #171554", +".ME c #171555", +".uh c #17162e", +".Lg c #171654", +".Lf c #171655", +"am6 c #171724", +"#wO c #171728", +".p7 c #171731", +".Ik c #171755", +".JX c #171756", +"Qth c #171757", +"#A1 c #171838", +".nI c #171850", +".PE c #171856", +".o6 c #17194e", +".SA c #171950", +".eF c #171b55", +".eU c #172049", +"#40 c #180000", +"#LD c #180008", +".tj c #180200", +"aQx c #180211", +"aQu c #180212", +"aOR c #180300", +"#VK c #18031f", +".Ov c #180400", +"#zH c #180414", +".Wg c #180500", +"aCD c #18050b", +"#8g c #180600", +"#3h c #180602", +"aE2 c #180713", +".eO c #180912", +".k7 c #180a11", +".vT c #180a16", +"#hZ c #180a18", +".bU c #180a29", +"aOi c #180b07", +"aPz c #180b12", +".Cs c #180b1a", +".to c #180c03", +".Ok c #180c1c", +"#JX c #180c1e", +"#AV c #180c23", +".yZ c #180c36", +".M8 c #180d0a", +".xR c #180d0f", +"apg c #180d11", +".7# c #180e31", +"ahS c #180f0d", +".dh c #180f3d", +".m# c #18101f", +"alU c #18111a", +".vY c #181128", +".nB c #181141", +"aPj c #18121d", +".p0 c #18124b", +".St c #181252", +".Q4 c #181253", +"aNf c #181318", +"agd c #181326", +".Q5 c #181352", +".Pu c #181353", +".Pt c #181354", +"azf c #181415", +"aox c #18141a", +".hW c #181437", +".Fp c #181445", +".NZ c #181453", +".NY c #181454", +"akO c #181523", +".0J c #181542", +".DY c #18161a", +".c. c #181623", +".G5 c #181642", +".lY c #181656", +".oP c #181749", +"aPo c #181821", +"aAU c #181822", +".b0 c #18184b", +".Lp c #181858", +".rr c #181924", +".Xs c #181a3b", +"#AZ c #181b32", +".qo c #181b4d", +".bB c #181c56", +"ac. c #182d51", +"aiG c #182d55", +"a#v c #190000", +".Om c #190100", +"#1U c #190108", +".Nf c #190300", +".Nr c #190407", +"aCz c #190411", +"as0 c #190502", +".zA c #190600", +"asq c #190603", +"aqW c #190604", +".Ou c #190702", +"aIZ c #19080d", +"aHs c #19080f", +"aE3 c #190813", +".Bo c #19090c", +"aOe c #190910", +".03 c #190a0d", +"agC c #190b08", +"am2 c #190b0c", +".gh c #190b14", +".rJ c #190b1b", +".nX c #190c03", +"am3 c #190c0f", +".AU c #190c1b", +".5W c #190d0d", +".CW c #190d0e", +".v3 c #190d10", +"aPa c #190e0e", +".dK c #190f23", +".VO c #190f32", +"ait c #19101d", +".s2 c #191026", +".EI c #191108", +".G7 c #19113a", +".i8 c #191142", +"#eH c #191145", +".sG c #19114c", +"aC3 c #191214", +".dI c #19131c", +".8Z c #19133e", +".e4 c #191346", +".7d c #19143e", +".DL c #191440", +"#d# c #191444", +"avD c #191518", +".DM c #19153e", +"atJ c #191619", +"azQ c #19171d", +"aDI c #191723", +"aAH c #191821", +".G6 c #191843", +".LA c #191846", +"#x5 c #191a37", +".Fh c #191a4c", +".jd c #191b3f", +"ae4 c #191c37", +".4c c #191c44", +".rL c #191c4c", +"Qtp c #191d57", +".0c c #192a4d", +".Mj c #192f5c", +"#XI c #1a0000", +"aOy c #1a0008", +"aKl c #1a0109", +".M0 c #1a0200", +"aFg c #1a0309", +".Ne c #1a0400", +"#IM c #1a040b", +"ap1 c #1a0501", +"#YE c #1a051f", +"aFA c #1a0600", +"aE4 c #1a0712", +"aPi c #1a0718", +".kD c #1a0923", +"aJ7 c #1a0a12", +".mh c #1a0a23", +".xG c #1a0b16", +".h3 c #1a0c0f", +".dn c #1a0c14", +".D5 c #1a0c1e", +".qm c #1a0c1f", +".ao c #1a0c2b", +".xg c #1a0c36", +".C1 c #1a0e0a", +".v2 c #1a0e10", +"aOv c #1a0e18", +".CJ c #1a0f10", +".xM c #1a0f11", +".T0 c #1a0f3a", +"##P c #1a0f3c", +".G9 c #1a0f47", +"aiY c #1a100d", +"apr c #1a1015", +"#tl c #1a1042", +".BI c #1a1111", +"#s8 c #1a113b", +"ayn c #1a121d", +".kv c #1a1220", +"#x1 c #1a1232", +".t8 c #1a1249", +".gu c #1a1336", +".xr c #1a1337", +"ad9 c #1a1416", +"aqd c #1a1417", +".yX c #1a1430", +".rl c #1a144a", +".Es c #1a1510", +"apy c #1a151a", +"aPe c #1a151d", +"ass c #1a161c", +".lL c #1a163a", +".kf c #1a175a", +"ata c #1a181b", +"aBY c #1a1822", +".B3 c #1a1840", +".oO c #1a184a", +"azu c #1a1922", +".Rb c #1a1a59", +"QtP c #1a1d3f", +".s8 c #1a1d4a", +".hO c #1a1e4d", +".Np c #1b0000", +".ti c #1b0400", +".Zq c #1b0600", +"aD2 c #1b0702", +"aGW c #1b0801", +"auZ c #1b0804", +".M9 c #1b0900", +"aOz c #1b090d", +".nK c #1b0b22", +".Rm c #1b0b23", +".Db c #1b0d00", +".hJ c #1b0d16", +".zj c #1b0d1f", +".Hk c #1b0e21", +".v4 c #1b1012", +"aoI c #1b1015", +".Fy c #1b1145", +".CP c #1b1206", +".zt c #1b1314", +"#J5 c #1b1327", +".vG c #1b133c", +".hP c #1b1344", +"aj5 c #1b1413", +"aqb c #1b1418", +".vE c #1b1436", +".l9 c #1b1446", +"ard c #1b151a", +".0G c #1b1536", +".2c c #1b1625", +"#c2 c #1b1650", +"arK c #1b181d", +"atM c #1b181f", +".Fr c #1b1846", +"aJR c #1b191b", +".Fv c #1b193f", +".lP c #1b1a4e", +".SB c #1b1b5a", +".J3 c #1b1c58", +".sY c #1b1d46", +".Il c #1b1d53", +".c# c #1b1f43", +".dU c #1b264a", +"apR c #1b265d", +".yt c #1b2c49", +".tg c #1c0000", +".Nq c #1c0304", +"aLv c #1c030d", +"#M7 c #1c0314", +".LI c #1c0400", +".2F c #1c0500", +"aOC c #1c0506", +".00 c #1c0600", +".Nd c #1c0601", +".uE c #1c0700", +"aI0 c #1c070b", +".rY c #1c0902", +".zn c #1c0c14", +".o9 c #1c0c20", +".vt c #1c0d35", +".qD c #1c0e05", +".o4 c #1c0e24", +".gg c #1c0f2a", +".0a c #1c1006", +".CY c #1c1014", +"aDK c #1c1019", +".uD c #1c1100", +".C4 c #1c1104", +".CT c #1c1209", +".CM c #1c120d", +"#bu c #1c1212", +"aMf c #1c1219", +"aOp c #1c121e", +"#.n c #1c1224", +".ad c #1c1241", +".CO c #1c1309", +".EO c #1c130d", +"apt c #1c1318", +"aNC c #1c131f", +".2j c #1c132b", +".AK c #1c133e", +".EM c #1c140e", +"aov c #1c151c", +"aQp c #1c151e", +"aho c #1c1525", +".DR c #1c1633", +".ng c #1c1835", +"aPp c #1c1923", +"arW c #1c1a1e", +".N0 c #1c1a52", +".p9 c #1c1b44", +".MF c #1c1b52", +".T3 c #1c1b56", +".Lh c #1c1c53", +".Fl c #1c1d4f", +".GV c #1c1d52", +"QtI c #1c204f", +"aQl c #1c2128", +".Ni c #1d0000", +"#CB c #1d0316", +"#0z c #1d070a", +"#3g c #1d0800", +".Cx c #1d0907", +".rX c #1d0a03", +"#Fi c #1d0a14", +".AZ c #1d0b0e", +"#Ck c #1d0b17", +".Br c #1d0d04", +".qr c #1d0d1f", +".pl c #1d0f06", +".D6 c #1d0f23", +".qG c #1d1007", +".C0 c #1d110f", +".h2 c #1d1218", +"aOt c #1d121f", +"#J0 c #1d1323", +"##O c #1d133f", +"#.o c #1d141e", +".EH c #1d150d", +".nC c #1d1525", +".0F c #1d1530", +".sH c #1d154c", +"#gd c #1d154f", +"#Fn c #1d1629", +"#.v c #1d1631", +"#eI c #1d1652", +"aBD c #1d1719", +"#br c #1d1742", +".Su c #1d1851", +".As c #1d193b", +".Pv c #1d1951", +"asI c #1d1b20", +".2d c #1d1b26", +".sK c #1d1b33", +".rs c #1d1b3d", +".Fs c #1d1b46", +".hV c #1d1e48", +".GS c #1d1e53", +".VS c #1d1f4d", +".cd c #1d2245", +"aKT c #1d2b4d", +".Ab c #1d2e4b", +"#A8 c #1e000f", +"#VL c #1e0013", +".Nj c #1e0100", +".Rw c #1e0300", +"aOD c #1e0608", +"aJt c #1e0708", +"aOE c #1e070a", +"aI1 c #1e070b", +".D9 c #1e0902", +"aOw c #1e0914", +"#DR c #1e0b14", +"aQi c #1e0d1d", +".Bb c #1e0f07", +".gF c #1e0f12", +".i1 c #1e0f18", +".rO c #1e0f1d", +".nG c #1e0f28", +"aPA c #1e1018", +".i9 c #1e1030", +".tn c #1e1108", +"#zp c #1e1129", +".mu c #1e1208", +".v5 c #1e1214", +".C5 c #1e1304", +"aoH c #1e1318", +".vM c #1e1319", +".EE c #1e1515", +"aJ6 c #1e151b", +"#gp c #1e1726", +".xu c #1e1737", +".Fw c #1e173c", +".Gk c #1e1817", +".na c #1e1937", +"azh c #1e1a1c", +".pT c #1e1a28", +"asE c #1e1b1f", +"agQ c #1e1b22", +".0I c #1e1b46", +".lT c #1e1c58", +".DG c #1e1e4c", +".aE c #1e1f2e", +"#AX c #1e2134", +"#WM c #1e2234", +".ce c #1e2246", +".fn c #1e2345", +"#3U c #1e283d", +".O1 c #1f0000", +".O2 c #1f0100", +"aOf c #1f060e", +"aE5 c #1f0613", +".Rp c #1f0700", +"asZ c #1f0701", +"arI c #1f0a06", +"aHD c #1f0c14", +".pA c #1f0d0c", +".SY c #1f0e10", +".TA c #1f0e12", +".r0 c #1f0f07", +"#Cl c #1f0f23", +"#pg c #1f100f", +"aCn c #1f1017", +".t# c #1f101c", +".mc c #1f102b", +".kR c #1f1109", +"aLe c #1f1219", +".bP c #1f121a", +".Hj c #1f1224", +"#.m c #1f1230", +".Xo c #1f152a", +".bJ c #1f1544", +"#7T c #1f1613", +".o0 c #1f1627", +".ur c #1f162d", +"#M1 c #1f172b", +".z. c #1f173e", +"QtJ c #1f1747", +"aL. c #1f181d", +".3V c #1f1829", +"#d. c #1f1848", +".Xv c #1f193d", +"#ga c #1f1a1d", +".lI c #1f1a3e", +"aq2 c #1f1b21", +".p4 c #1f1c42", +"#cZ c #1f1d40", +".oL c #1f1d47", +".nm c #1f1d54", +".ke c #1f1d5c", +"#t# c #1f1e46", +".GW c #1f2055", +".Ir c #1f2058", +".ca c #1f2346", +"aKm c #200109", +".Nk c #200400", +".yn c #200800", +"#Ui c #200810", +"aBd c #200817", +"#1n c #200b21", +".kz c #20102c", +"aE1 c #20121e", +".eS c #20131b", +"aPu c #20131d", +".C2 c #20140d", +"ak2 c #20141b", +".kH c #201630", +".e5 c #201641", +".EG c #201812", +".VP c #201845", +".dr c #201849", +".yS c #201a36", +"#uW c #201a45", +".5t c #201b2b", +".xq c #201c2f", +".kj c #201f61", +"#9. c #203057", +".No c #210100", +"aA7 c #21020e", +"#D8 c #210416", +".SS c #210900", +".FO c #210a00", +"aKZ c #210b03", +".pB c #210d17", +"aum c #210e09", +".Bq c #211109", +"afl c #21110d", +".Bn c #211111", +"#AT c #211122", +".uL c #21120a", +".Be c #211305", +".CZ c #211413", +".62 c #21141b", +"#9y c #211610", +".dB c #21161c", +".dA c #211642", +".2i c #211728", +"#.D c #211929", +".AC c #211935", +".rc c #211d23", +".j8 c #211d44", +"#Kd c #220107", +".Ve c #220300", +"aOg c #22040b", +"#zI c #220915", +".5U c #220a00", +"asp c #220b06", +"aAl c #220f08", +".gq c #221433", +".CX c #221619", +"#wr c #22183b", +".AA c #221842", +".f. c #221920", +"azC c #221a26", +".Et c #221b18", +".oy c #221d33", +".DD c #22214f", +".Fk c #222255", +"#1# c #22364e", +"aFk c #230000", +"aB. c #23000d", +".Nl c #230600", +"ang c #230a02", +".Hn c #230b00", +".BD c #230f00", +".ja c #231108", +".2I c #231112", +"az0 c #23121e", +".kS c #23130b", +".FL c #231529", +".Bg c #231602", +".Bl c #231606", +".D4 c #231624", +"afq c #231814", +".G8 c #231949", +"aIG c #231a20", +".DS c #231c40", +"atN c #232027", +".K0 c #233867", +"#Rv c #24000b", +"a#u c #240100", +"#6F c #240200", +"#0y c #240300", +".P2 c #240600", +"aMs c #240612", +"aOo c #240615", +"aNo c #240a14", +"aty c #24110d", +".mw c #24140c", +".fi c #24141b", +"aNH c #241423", +".tm c #24150d", +".Bm c #241510", +".vu c #241539", +"aNG c #241624", +".qF c #24170e", +"aLi c #24171e", +".Bh c #241802", +".r3 c #24180e", +"a#e c #24180f", +".jC c #24181a", +"aoG c #24191e", +".fa c #24191f", +".TZ c #241937", +"aHi c #241b21", +".7. c #241b30", +".b2 c #241b40", +".hX c #241c38", +"aNe c #241f24", +".u. c #241f41", +".Cb c #24203c", +"atL c #242123", +".G3 c #242250", +".kJ c #242642", +".dT c #242a42", +".cg c #242c4d", +"aa6 c #250000", +"aB# c #25000f", +"aBc c #250013", +"#YF c #25001b", +"#1T c #250100", +"aOh c #25020a", +".7s c #250500", +"aA6 c #250613", +".Nn c #250900", +".Nm c #250901", +".tK c #250e00", +".mM c #251524", +"#eS c #25170e", +".xC c #251729", +".BH c #251814", +".kQ c #251a10", +"##8 c #251c24", +"aL# c #251e24", +"aJS c #251e25", +".xf c #251e36", +"aw2 c #25232b", +".dS c #252536", +".cf c #25294d", +"#9U c #260000", +"#Xb c #260019", +"#OC c #260112", +".9m c #260200", +".uF c #260300", +"aKn c #26040c", +"aA9 c #260412", +"aOl c #260610", +"aA8 c #260613", +"aGk c #260f1c", +".e0 c #26111e", +"ad1 c #261611", +".k9 c #261628", +"#.k c #26193a", +".Bi c #261a02", +".Hg c #261a21", +".CV c #261b18", +".VN c #261b30", +".y0 c #261b41", +".Fz c #261c53", +".T1 c #261d54", +"#c6 c #261e1f", +".gC c #261e27", +"#bo c #261f4d", +"aNE c #26202b", +".rj c #262128", +".2m c #262145", +".0z c #262333", +".dR c #262430", +".aD c #262634", +"ajN c #26395c", +".O0 c #270503", +".3t c #27151e", +".Bp c #27161c", +".nY c #271810", +".AW c #27192d", +"agG c #271a16", +".gl c #271a22", +".Bj c #271b05", +"#Lv c #271f33", +".gT c #272034", +".y9 c #27223b", +"aEE c #27262f", +"aBt c #272d4e", +".sW c #27304c", +".EZ c #273f6d", +"#VM c #280009", +"aBb c #280211", +".9k c #280300", +"#Z4 c #280923", +".z0 c #280f00", +".Da c #281a0d", +".mv c #281a11", +".xD c #281a2e", +"#yg c #281b2e", +".eN c #281b37", +"#bx c #281f37", +".bT c #282050", +".AM c #282146", +"aAy c #282226", +"aoK c #282429", +".nd c #282441", +"apE c #28262b", +".DQ c #282643", +".hY c #282939", +".GC c #283e69", +".Ox c #290000", +"#8f c #290700", +"aJI c #29140c", +".mN c #291929", +".Bt c #291a0c", +".qC c #291a11", +".Cu c #291c2f", +".C3 c #291d13", +"#.j c #291e33", +".Fx c #29204c", +".5B c #292138", +"aND c #29222d", +"apA c #29262a", +"atO c #29262d", +".u# c #29273c", +".fm c #292b40", +".cc c #292d51", +".fo c #29315f", +".Ia c #293d66", +"aOn c #2a0616", +".v. c #2a0d00", +".u7 c #2a1200", +".wh c #2a1408", +".qU c #2a141a", +".BC c #2a1601", +".zB c #2a1705", +"#2P c #2a1719", +".Yz c #2a1809", +".fb c #2a1a21", +".dj c #2a1b24", +"aaT c #2a1d12", +".tp c #2a1f15", +"aEf c #2a2124", +".e9 c #2a222a", +"QtZ c #2a2b3b", +"avX c #2a3857", +".1P c #2a395a", +"#2z c #2a4a6c", +"#0T c #2b0000", +".6A c #2b0600", +".nN c #2b1508", +".XT c #2b1718", +".Bk c #2b1f0a", +"#xY c #2b1f2f", +".C6 c #2b2111", +"#M0 c #2b2330", +".gU c #2b2543", +"axt c #2b2931", +".rA c #2b3158", +".ul c #2b354d", +".ys c #2b364d", +"aJC c #2b385a", +".Jv c #2b4070", +"#0U c #2c0000", +"#M8 c #2c000b", +"#P5 c #2c0011", +"#jF c #2c0100", +".th c #2c0300", +"aFi c #2c0502", +"#CC c #2c0513", +"aOm c #2c0514", +"aBa c #2c0615", +"aLw c #2c0813", +".sj c #2c1100", +".u4 c #2c1212", +".kG c #2c1319", +".x8 c #2c1519", +".uJ c #2c1710", +".4m c #2c1717", +"#nV c #2c1915", +"acp c #2c1e12", +".uO c #2c2218", +"#4y c #2c231d", +"aJT c #2c252c", +".Ck c #2c2548", +".e6 c #2c262f", +"aDx c #2c2e3a", +"#7n c #2c3f69", +"#9T c #2d0000", +"aOx c #2d111d", +".qV c #2d171e", +"##T c #2d201f", +"##Z c #2d2748", +".gz c #2d2b38", +".IH c #2d2d43", +"ahz c #2d3f66", +"#2b c #2e0303", +"#4Z c #2e0d00", +".SZ c #2e1613", +".sh c #2e161a", +".zF c #2e1d00", +".ob c #2e1e20", +".nW c #2e2218", +".ii c #2e2232", +".dJ c #2e2438", +".EJ c #2e2620", +".Is c #2e2f6a", +".5M c #2e3158", +"aax c #2e3a5b", +"aFj c #2f0000", +"#LE c #2f0005", +"#0S c #2f0100", +".tF c #2f1617", +".mk c #2f1715", +"aIi c #2f1911", +".gt c #2f1a21", +".qT c #2f1b17", +".R5 c #2f1c17", +"akw c #2f2b2f", +"ato c #2f4465", +".Dl c #2f4a7c", +"aa5 c #300002", +"aNJ c #300314", +".Oy c #300400", +".9l c #300e00", +".tI c #301600", +".sk c #301707", +".sf c #301a13", +".wg c #301c06", +".hS c #301d1b", +".k8 c #302131", +".pm c #302219", +".qH c #30231a", +"ajW c #30232d", +".Xp c #302848", +"#eE c #302a2c", +".gV c #302a4f", +"aAR c #302b37", +"awz c #302d30", +".al c #303a62", +".ch c #303b60", +".aJ c #304475", +"#HA c #310000", +"#1S c #310400", +"aI2 c #310a00", +"#Z. c #310e00", +".tG c #311600", +".z1 c #311800", +"ak8 c #312623", +"aLa c #312a2f", +".8U c #312b2e", +"#2a c #320604", +"#2c c #320909", +".7t c #321700", +"aNI c #321827", +".wk c #321921", +".Zt c #321a1a", +".1N c #32221c", +".zk c #322438", +".Cr c #322630", +".EL c #322923", +"aIm c #323033", +"ari c #323035", +".B4 c #323058", +"#da c #32305c", +"aql c #323134", +".Wl c #331b0d", +".3s c #331d21", +"azd c #331f18", +"aIN c #33222b", +".xT c #33282a", +".vm c #332b40", +"anj c #332d34", +".vD c #332f40", +"Qt9 c #335e9d", +".#g c #3365a8", +".#f c #3367a8", +".#e c #3368a8", +"#2d c #340806", +"aNp c #340d19", +".7z c #341304", +".v# c #341608", +".wl c #341f08", +"#.A c #342835", +"Qt0 c #343748", +"aQF c #344d7b", +".#h c #3463a8", +"#8e c #350000", +"#Zo c #350100", +"a#t c #350103", +"aDY c #350a1e", +"#h2 c #350b00", +".7r c #350c00", +"#XH c #350d00", +"aMY c #351717", +".si c #351806", +".vP c #352739", +".xh c #35284c", +"anE c #352e33", +"#Vi c #35374b", +".d6 c #355d9e", +".aW c #3565aa", +"#Zn c #360000", +"#9S c #360005", +"#Xc c #360011", +"#SW c #360213", +"#0R c #360703", +".Rx c #361609", +"#3f c #361c0c", +".Bu c #362716", +".zi c #362938", +".0E c #362c41", +"aLb c #362f35", +".Ly c #36325a", +"#Zt c #370000", +"#Zp c #370500", +"#0V c #370600", +".9c c #370c00", +".wj c #371e22", +".yo c #37231c", +".Bv c #372916", +".aj c #372931", +".7h c #372a20", +"al6 c #372b2f", +"aoF c #372c31", +"#Fm c #372f3c", +"aAx c #373134", +"aJU c #373137", +"afA c #37343b", +".DE c #373664", +"ac# c #374d71", +".#i c #3763ac", +".aX c #3766ab", +"#9R c #38040a", +"#Zq c #380700", +".OM c #380800", +"#9Q c #38080c", +"#Wf c #380900", +"#h3 c #380b00", +"aL1 c #380e0e", +".04 c #381b1a", +".48 c #381d2d", +".tD c #382017", +".pz c #382a14", +".mL c #382a2f", +".ih c #382b37", +".us c #382e3e", +".dQ c #382e42", +".dH c #38313a", +".VR c #38376a", +"#YG c #390018", +"#Zs c #390300", +"#Zr c #390400", +"aa4 c #390608", +"#HB c #39100c", +".OZ c #39130f", +"aHE c #39202b", +".49 c #392236", +"#TV c #392626", +".z7 c #392822", +".Bs c #392a1f", +".FJ c #392c3b", +".jD c #392f31", +".Ci c #393060", +"#ws c #393157", +"#80 c #39353b", +"aAd c #393e60", +"#5H c #394b71", +".aK c #394e7f", +".#a c #395ea5", +".## c #3960a4", +".yy c #3966b3", +".wL c #3966b4", +".Oz c #3a0e02", +".P3 c #3a1a0c", +".tE c #3a2121", +".tJ c #3a220e", +".uI c #3a231d", +".wi c #3a2320", +".7g c #3a2d2c", +".vN c #3a2d38", +"aMh c #3a3137", +"alX c #3a465d", +"Qt3 c #3a5a93", +".lk c #3a5fab", +".aT c #3a62ad", +".m2 c #3a64b8", +".aV c #3a6bb0", +"#e2 c #3b0200", +"#0Q c #3b0a03", +".OK c #3b0b03", +"#D9 c #3b101d", +".nZ c #3b2b23", +".xe c #3b344b", +".cq c #3b62ad", +".a1 c #3b63a9", +".su c #3b64a9", +".cm c #3b64ac", +".oq c #3b67bc", +"#dq c #3c0400", +"aNR c #3c0408", +".OL c #3c0c04", +"#2e c #3c0d0a", +"#2# c #3c0e0b", +"#.K c #3c0f00", +"aMt c #3c121f", +".sg c #3c2427", +"#Cj c #3c2a28", +".od c #3c2a37", +".oc c #3c2b37", +"anG c #3c363b", +"#bn c #3c3759", +".At c #3c385a", +"adK c #3c517e", +"Qt2 c #3c5b95", +".ct c #3c64a6", +".cs c #3c64a9", +".cr c #3c64ac", +".#j c #3c65b0", +".#B c #3c66b0", +".cJ c #3c66b1", +"##j c #3d0000", +"#Rw c #3d0113", +".58 c #3d0900", +".OJ c #3d0d04", +"#VN c #3d0d12", +"#Wg c #3d180b", +".u2 c #3d2419", +".zO c #3d292c", +".zE c #3d2c12", +".b3 c #3d2d30", +".z8 c #3d3130", +".8O c #3d3336", +"aFB c #3d3437", +".xd c #3d364e", +".Av c #3d395b", +".aS c #3d62aa", +".#A c #3d65b2", +".#C c #3d66b1", +".vj c #3d67ae", +".#F c #3d67b1", +".a9 c #3d67b2", +".r# c #3d69b8", +"#Zu c #3e0000", +"aNO c #3e0001", +"aNK c #3e0012", +"#gw c #3e0300", +"#Zm c #3e0600", +"#Z5 c #3e0b28", +"aNS c #3e181b", +"#FW c #3e1d1d", +"#dg c #3e3340", +"#gb c #3e3749", +".t6 c #3e3936", +"ahg c #3e3b3f", +"aIc c #3e507a", +".dV c #3e507f", +".BO c #3e5d93", +".oo c #3e64af", +".a8 c #3e66b2", +".#z c #3e66b3", +".a0 c #3e67ad", +".#G c #3e67b2", +".lx c #3e68af", +".lw c #3e68b0", +".m9 c #3e68b1", +".#D c #3e68b2", +".#E c #3e68b3", +".ly c #3e69b0", +".ln c #3e69c1", +".Af c #3e6ab4", +".Dp c #3e6cab", +"#CZ c #3f0200", +"aa3 c #3f0b0d", +"#ag c #3f0f01", +"#A9 c #3f1823", +"axY c #3f2b25", +".8K c #3f3147", +".63 c #3f3333", +"#G7 c #3f3744", +".Ka c #3f3b64", +".un c #3f4268", +".g3 c #3f63af", +".jX c #3f64b1", +".jY c #3f65b0", +".a2 c #3f66ad", +".#o c #3f66ae", +".cp c #3f66b3", +".#n c #3f67ad", +".#y c #3f67b3", +".#x c #3f67b4", +".#H c #3f68b3", +".lv c #3f69b0", +".m6 c #3f69b1", +".lq c #3f69b2", +".#I c #3f69b3", +".lu c #3f6ab1", +".wN c #3f6bae", +".E4 c #3f6dab", +".Uz c #401615", +"#HC c #401a14", +"aym c #403541", +".5s c #40374b", +".dY c #4061a7", +".d2 c #4061ae", +".#d c #4062b0", +".fJ c #4065b3", +".a4 c #4067ae", +".cy c #4067af", +".#p c #4067b0", +".#k c #4067b4", +".#m c #4068ae", +".cI c #4068b3", +".a7 c #4068b4", +".#w c #4068b5", +".cu c #4069a9", +".jS c #4069af", +".wU c #4069b0", +".eg c #4069b3", +".cH c #4069b4", +".jT c #406aaf", +".iy c #406ab0", +".os c #406ab1", +".m5 c #406ab2", +".ls c #406ab3", +".eh c #406ab4", +".fI c #406ab5", +".or c #406bb2", +".lp c #406bb3", +".yA c #406caf", +".pO c #406cbe", +"aNQ c #410003", +"#UN c #410900", +"#bT c #410d01", +".6C c #411912", +".wB c #41281c", +".wm c #412b13", +"aNA c #41313f", +"#Co c #413647", +".3u c #414e6d", +".#b c #4165af", +".#L c #4166b2", +".ba c #4166b3", +".#K c #4166b4", +".#t c #4167b5", +".a3 c #4168ae", +".cx c #4168af", +".cz c #4168b1", +".#q c #4168b3", +".#v c #4168b5", +".d9 c #4169b0", +".ef c #4169b3", +".cG c #4169b4", +".a6 c #4169b5", +".#u c #4169b6", +".#. c #416aab", +".jU c #416aaf", +".iz c #416ab0", +".yD c #416ab1", +".ha c #416ab2", +".fH c #416ab3", +".ee c #416ab4", +".cF c #416ab5", +".aU c #416ab7", +".lz c #416bb2", +".pP c #416bb3", +".lr c #416bb4", +".hb c #416bb5", +".lC c #416bb6", +"Qt8 c #416caa", +".m7 c #416cb3", +".ou c #416cb4", +".aY c #416eb3", +".Do c #416fb0", +"aNN c #420004", +"#Hg c #420c17", +"#0W c #420d05", +"#Fz c #420d1a", +".59 c #420e00", +".7D c #420e02", +".ON c #42130a", +"aNq c #421320", +".OA c #42160a", +"aKQ c #421714", +".ym c #422604", +".x9 c #422b32", +".qB c #42332b", +".pj c #42352c", +"##5 c #423643", +"arY c #424245", +"#AY c #424559", +".d1 c #4261b1", +".jZ c #4267b1", +".cM c #4267b2", +".cL c #4267b3", +".fK c #4267b4", +".b# c #4267b5", +".a5 c #4268b3", +".#s c #4268b5", +".#l c #4268b6", +".cw c #4269b0", +".e. c #4269b2", +".cA c #4269b4", +".#r c #4269b5", +".cC c #4269b6", +".d8 c #426ab0", +".wW c #426ab2", +".fG c #426ab3", +".ed c #426ab4", +".cE c #426ab5", +".cD c #426ab6", +".wS c #426bb1", +".wV c #426bb2", +".fF c #426bb3", +".m4 c #426bb4", +".ec c #426bb5", +".lD c #426bb6", +".yE c #426cb2", +".lA c #426cb3", +".m8 c #426cb4", +".lt c #426cb5", +".hc c #426cb6", +".ov c #426cb7", +".lm c #426cc2", +".Dq c #426daf", +".wX c #426db2", +".ot c #426db4", +".sw c #426db5", +".E3 c #426fae", +"#9P c #430103", +"#OD c #430513", +"aLx c #431622", +".6B c #431d14", +".rW c #432215", +".eR c #432409", +".7A c #432418", +"ape c #432c26", +".bX c #432e35", +".tf c #433121", +".Bf c #433624", +"aLg c #43363d", +"aMg c #433940", +"#r4 c #433a3f", +".qj c #433a44", +"aGJ c #433b39", +"ado c #433f48", +"awd c #434047", +".ej c #4368b2", +".ei c #4368b3", +".hd c #4368b4", +".cK c #4368b5", +".#J c #4368b6", +".yw c #4369ae", +".iA c #4369b2", +".eb c #4369b6", +".fA c #436ab2", +".fB c #436ab3", +".e# c #436ab5", +".cB c #436ab6", +".h# c #436ab7", +".d5 c #436bac", +".d7 c #436bb1", +".yF c #436bb3", +".lo c #436bb5", +".yB c #436cb0", +".wR c #436cb2", +".wT c #436cb3", +".yG c #436cb4", +".jV c #436cb7", +".lB c #436db6", +".lE c #436db7", +".vk c #436db8", +".wY c #436eb4", +".E5 c #436fb0", +".BS c #4370b1", +".Ah c #4370b2", +".tV c #4370ba", +".wM c #4372b9", +"#Zl c #440a00", +"#IN c #440a12", +".OI c #44140c", +".9n c #44180a", +".44 c #441c0f", +".qW c #443124", +"aLh c #44373f", +"#bG c #443946", +".rG c #443a47", +".s3 c #443a49", +"ast c #444046", +"ay7 c #44496c", +".aP c #44629b", +".aQ c #4464a2", +".Dn c #4468ab", +".yC c #4469b1", +".cN c #4469b3", +".#M c #4469b4", +".fL c #4469b5", +".b. c #4469b7", +".j0 c #446ab1", +".iB c #446ab2", +".he c #446ab3", +".ea c #446ab6", +".fE c #446ab7", +".jR c #446ab8", +".fz c #446bb2", +".g6 c #446bb3", +".g7 c #446bb4", +".fC c #446bb6", +".h. c #446bb8", +".yH c #446cb4", +".tU c #446daf", +".wQ c #446db3", +".Aj c #446db4", +".BX c #446db5", +".ra c #446db8", +".tW c #446eb6", +".pQ c #446eb7", +".lF c #446eb8", +".n. c #446eb9", +".yx c #446eba", +".aZ c #446fb4", +".yI c #446fb5", +".vi c #4471b9", +".yz c #4472b9", +"#ay c #450000", +"aNP c #450003", +"#M9 c #450109", +"#I9 c #451200", +".OO c #45150d", +"aFf c #451528", +".9Z c #451809", +"#HD c #451a09", +".41 c #451b02", +"#6O c #451c01", +"#FX c #451e0c", +"aK# c #45353d", +".vQ c #45374b", +".Bw c #453822", +"aNB c #453845", +".d0 c #4562b5", +"Qt4 c #45649e", +"Qt6 c #45659e", +".jW c #4569b7", +".iE c #456baf", +".iD c #456bb1", +".iC c #456bb2", +".fM c #456bb3", +".fD c #456bb7", +".ix c #456bb8", +".j1 c #456caf", +".Jz c #456cb2", +".fy c #456cb3", +".it c #456cb4", +".iu c #456cb5", +".g8 c #456cb7", +".g9 c #456cb8", +".jQ c #456cb9", +".co c #456cba", +".E2 c #456db0", +".wO c #456db1", +".g5 c #456db3", +".Dr c #456db4", +".Ai c #456eb2", +".BW c #456eb4", +".Ak c #456eb5", +".Al c #456eb6", +".BT c #456fb2", +".wZ c #456fb5", +".lG c #456fb9", +".w0 c #4570b5", +".m3 c #4571c8", +".GH c #4573af", +".9O c #460500", +".6. c #461200", +"#3D c #461803", +"#zJ c #462b34", +".zM c #46342b", +".BG c #463730", +".nV c #463b31", +"#ZK c #465470", +".ci c #465580", +".aL c #465b8c", +".dZ c #4662b7", +".E1 c #4667a7", +".aR c #4669ac", +".fw c #4669b2", +".#c c #4669b5", +".hf c #466cb3", +".ek c #466cb4", +".bb c #466cb5", +".hh c #466db0", +".BU c #466db4", +".jN c #466db5", +".jO c #466db6", +".iv c #466db7", +".jP c #466db8", +".iw c #466db9", +".m1 c #466dbc", +".g4 c #466eb4", +".Am c #466eb6", +".BV c #466fb5", +".BY c #466fb6", +".BZ c #466fb7", +".r. c #466fb8", +".yJ c #4670b6", +".w6 c #4670b8", +".ow c #4670ba", +".wK c #4670be", +".w1 c #4671b6", +".w5 c #4671b8", +".sv c #4673bf", +".BR c #4674b7", +".If c #4675b1", +"#Hz c #470b00", +".OH c #47170f", +".40 c #471901", +".90 c #47190a", +".8a c #47231c", +".5X c #473131", +".di c #473a56", +"aJV c #474047", +"#bm c #47435c", +"ao6 c #47518e", +"arx c #475680", +".aI c #475b8c", +".aF c #475b8d", +".ck c #47659f", +".fv c #4766b1", +".Ac c #4769a3", +".q9 c #476aa9", +".d3 c #476ab3", +".jJ c #476bb0", +".wP c #476bb4", +".fx c #476cb5", +".iF c #476db1", +".hg c #476db3", +".BQ c #476db4", +".ir c #476dba", +".j2 c #476eb2", +".jM c #476eb5", +".is c #476fb5", +".Ae c #476fb9", +".cv c #4770af", +".Du c #4770b6", +".Dv c #4770b7", +".Dx c #4770b8", +".tX c #4770bb", +".yL c #4771b7", +".w4 c #4771b8", +".pR c #4771bb", +".yK c #4772b7", +".Dy c #4772b8", +".w3 c #4772b9", +".cn c #4772bd", +".GG c #4773b1", +".GI c #4774b4", +".Ag c #4775ba", +"a#s c #480209", +".57 c #480400", +"#KC c #480700", +"#Fl c #484453", +".li c #4866a7", +"Qt5 c #4867a1", +".g2 c #4868b0", +".st c #486aa4", +".iq c #486bb5", +".yv c #486ca9", +".wJ c #486eac", +".hi c #486eb2", +".fN c #486eb5", +".cO c #486eb6", +".iG c #486fb4", +".jL c #4870b6", +".Dw c #4870b8", +".Ds c #4871b7", +".Dt c #4871b8", +".GN c #4871b9", +".w2 c #4872b8", +".yO c #4872ba", +".rb c #4872bc", +".tY c #4872bd", +".yM c #4873b8", +".An c #4873b9", +"aNL c #490010", +"#E. c #49131d", +".OG c #491910", +".6H c #492f4b", +"#xX c #493d47", +".vJ c #493f3c", +"aPI c #496190", +".Ad c #496daf", +".#N c #496eb8", +".j3 c #496fb5", +".iH c #496fb6", +".K4 c #4970b4", +".hj c #4970b5", +".Fa c #4971b9", +".E9 c #4972b8", +".E8 c #4972b9", +".F# c #4972ba", +".Dz c #4973b9", +".w7 c #4973ba", +".Ap c #4973bb", +".sx c #4973bd", +".Fb c #4974b9", +".yN c #4974ba", +".yP c #4974bb", +".Ie c #4975b2", +"#Ke c #4a0a0f", +".6# c #4a1500", +"#J# c #4a1603", +"#0P c #4a160e", +".OB c #4a1e12", +".xW c #4a3519", +".dv c #4a3542", +".Y3 c #4a415a", +"#tw c #4a4348", +"aAI c #4a4852", +".yu c #4a5e7f", +".tS c #4a6085", +".dW c #4a639d", +".Ic c #4a69a5", +".vg c #4a6a9d", +".GE c #4a6aa7", +".hk c #4a70b6", +".iI c #4a70b7", +".hl c #4a70b8", +".hm c #4a70b9", +".JS c #4a71b8", +".K6 c #4a71b9", +".Lc c #4a72b8", +".MA c #4a72b9", +".GM c #4a72ba", +".E6 c #4a73b9", +".F. c #4a73bb", +".JK c #4a74b8", +".Ao c #4a74ba", +".Aq c #4a74bb", +".tZ c #4a74be", +".Fc c #4a75ba", +".Fe c #4a75bb", +".w8 c #4a75bc", +"aNM c #4b020d", +"#SX c #4b0617", +"#.0 c #4b0c00", +"#3C c #4b1804", +"#CD c #4b1b25", +".OP c #4b1c12", +"#1o c #4b2741", +".gk c #4b2c11", +"#G2 c #4b3a44", +".uM c #4b3e35", +".o1 c #4b424a", +"aNF c #4b424e", +"aAw c #4b4549", +"ax. c #4b494f", +".Dm c #4b69a3", +".m0 c #4b6eb7", +".iJ c #4b70ba", +".fP c #4b71b5", +".fO c #4b71b8", +".bc c #4b71b9", +".JB c #4b71ba", +".Mp c #4b72b8", +".Mn c #4b73b5", +".JR c #4b73ba", +".GL c #4b73bb", +".vh c #4b74b5", +".JL c #4b74b9", +".E7 c #4b74ba", +".GJ c #4b74bb", +".JJ c #4b75b8", +".My c #4b75b9", +".Ff c #4b75bb", +".B0 c #4b75bc", +".yQ c #4b75bd", +".Fd c #4b76bb", +".B1 c #4b76bd", +"#.2 c #4c0a00", +"#I8 c #4c1100", +"#XY c #4c1200", +"aNr c #4c1826", +"#2f c #4c1913", +"#.R c #4c1c08", +".9d c #4c2a1d", +"#FV c #4c2a2e", +".W4 c #4c2e1f", +".u3 c #4c3231", +".pa c #4c3720", +".zC c #4c3a25", +".bM c #4c3d3a", +".8J c #4c414a", +".nD c #4c4449", +".pZ c #4c4856", +"aej c #4c4a51", +"a.Q c #4c5a7e", +".aG c #4c6192", +".aN c #4c679b", +".fu c #4c67b3", +".q8 c #4c689a", +".BP c #4c6dac", +".Jy c #4c6fb5", +".d4 c #4c71b6", +".el c #4c71b9", +".hn c #4c71bc", +".jK c #4c71bd", +".GF c #4c72b3", +".fQ c #4c72b6", +".j4 c #4c72bb", +".NF c #4c73b3", +".JT c #4c73ba", +".NT c #4c74ba", +".JV c #4c74bb", +".JI c #4c75b8", +".JM c #4c75b9", +".JN c #4c75ba", +".GK c #4c75bb", +".JF c #4c76b7", +".JE c #4c76b8", +".JH c #4c76b9", +".JO c #4c76ba", +".GO c #4c76bc", +".w9 c #4c76bd", +".DA c #4c77be", +"#ax c #4d0700", +"#Bs c #4d0d00", +"#KB c #4d0e00", +".4T c #4d0f00", +"#J. c #4d1c0c", +"#0x c #4d2004", +".Qw c #4d2113", +".ma c #4d4549", +".AD c #4d475b", +".fl c #4d4c55", +".jq c #4d4e45", +".gZ c #4d5e9a", +".ip c #4d6dae", +".fT c #4d72ba", +".#V c #4d72bc", +".Id c #4d73b3", +".fR c #4d73b9", +".fS c #4d73ba", +".#W c #4d73be", +".Mz c #4d74bb", +".NH c #4d75b8", +".JU c #4d75bb", +".JQ c #4d75bc", +".ll c #4d75c6", +".L# c #4d76b8", +".JG c #4d76b9", +".JP c #4d76ba", +".Mx c #4d76bb", +".JD c #4d77b8", +".L. c #4d77b9", +".Lb c #4d77ba", +".NS c #4d77bb", +".Fg c #4d77be", +"#Xd c #4e0912", +"aNz c #4e1721", +".OF c #4e1e16", +"QtL c #4e2400", +".7C c #4e2419", +".OY c #4e241f", +".wA c #4e2f0b", +".jl c #4e3f41", +"aBU c #4e4a4e", +".av c #4e4a6d", +".sR c #4e4c68", +".mm c #4e4e65", +".fp c #4e588f", +"avp c #4e5c7e", +".fr c #4e60af", +".pM c #4e6ca7", +".pN c #4e73b8", +".#O c #4e73bc", +".fU c #4e73be", +".#U c #4e74bc", +".iK c #4e74be", +".NU c #4e75bc", +".Pd c #4e76b3", +".NV c #4e76bc", +".K9 c #4e77b9", +".La c #4e77ba", +".Mw c #4e77bc", +".K8 c #4e78b9", +".Mr c #4e78ba", +".Mu c #4e78bb", +".Mv c #4e78bc", +".GP c #4e78bf", +".GQ c #4e79c0", +"#Z6 c #4f031d", +"#Uj c #4f0b1c", +"#aq c #4f1f07", +"a.# c #4f2513", +"#6E c #4f2615", +".ky c #4f2813", +"#.T c #4f2814", +".wy c #4f300c", +".6G c #4f3148", +".TY c #4f4359", +"aoE c #4f444a", +".dP c #4f4458", +".yT c #4f4965", +"aGR c #4f608b", +".E0 c #4f6aa1", +".aO c #4f6ba1", +".dX c #4f6daf", +"Qt1 c #4f6fa8", +".on c #4f70b3", +".bj c #4f75be", +".bk c #4f75bf", +".QY c #4f76bd", +".Pp c #4f77bd", +".Mt c #4f78bb", +".NR c #4f78bc", +".NQ c #4f78bd", +".Mq c #4f79ba", +".NM c #4f79bb", +".Ms c #4f79bc", +".Pm c #4f79bd", +".op c #4f79ca", +"#9O c #500409", +"#.Z c #501200", +"#3E c #501500", +"#.L c #50291e", +".mb c #502a14", +".7B c #502e23", +"#DQ c #503d36", +"aHl c #503f46", +".oa c #504330", +".81 c #504451", +"#x0 c #504665", +"aEz c #50494e", +"ap6 c #504c52", +"ai7 c #504d54", +"#Yi c #505b79", +".gY c #505e96", +".aH c #506596", +".cj c #506699", +".Jx c #506dae", +".ep c #5075bf", +".bl c #5075c0", +".K7 c #5075c1", +".bd c #5076bd", +".#P c #5076be", +".Pf c #5077b8", +".em c #5077ba", +".QX c #5077be", +".Pq c #5078be", +".NG c #5079b9", +".NP c #5079bc", +".QR c #5079bd", +".Pn c #5079be", +".NL c #507abb", +".NK c #507abc", +".NO c #507abd", +".Po c #507abe", +".Ig c #507ac1", +".5d c #507dbb", +"#XQ c #510900", +"#.1 c #511000", +".9v c #511305", +"acy c #512320", +".OC c #512519", +".dm c #513217", +"aPM c #513327", +".BB c #513d28", +".Bx c #51442d", +"#DV c #514456", +".5. c #515d7b", +".aM c #516596", +".fs c #5165b3", +".ft c #5167b5", +"Qt7 c #5171aa", +".K3 c #5174b8", +".eq c #5176c0", +".#T c #5177bd", +".#Q c #5177be", +".cU c #5177c0", +".cV c #5177c1", +".#R c #5178bb", +".en c #5178bc", +".QZ c #5178bf", +".QK c #5179b7", +".So c #5179bf", +".TV c #5179c0", +".Pe c #517ab7", +".VF c #517aba", +".NJ c #517abc", +".NN c #517abd", +".QS c #517abe", +".QT c #517abf", +".Pj c #517bbc", +".Pi c #517bbd", +".Pl c #517bbe", +".Sm c #517bbf", +"#L4 c #520300", +".73 c #52130a", +"#.Q c #521d09", +".6a c #521e07", +"#jG c #522512", +".6D c #522928", +".ai c #523318", +".jA c #524140", +"aK. c #52424a", +".uN c #52473d", +".vw c #524756", +"aGZ c #52484c", +"#c8 c #524a64", +".85 c #524b5b", +".sO c #52506c", +"#3V c #525f79", +"#9# c #52648b", +".mX c #526ba1", +".io c #526ca3", +".K2 c #526fb0", +".mZ c #5272b4", +".Vl c #5275b6", +".cl c #5276b8", +".er c #5277c2", +".#S c #5278bd", +".eo c #5278be", +".be c #5278bf", +".cT c #5278c0", +".cW c #5278c2", +".JA c #5279bf", +".Sn c #5279c0", +".VE c #527abb", +".Mo c #527abd", +".K5 c #527abf", +".Q0 c #527ac0", +".Sp c #527ac1", +".VD c #527bbb", +".Pk c #527bbe", +".QU c #527bbf", +".QV c #527bc0", +".QJ c #527cb7", +".Ph c #527cbd", +".QP c #527cbe", +".QQ c #527cbf", +".QW c #527cc0", +"#P6 c #530a1a", +"#Et c #531705", +"#0X c #53180d", +"#0O c #531d13", +".nF c #532d15", +".i4 c #533419", +"axn c #533a30", +".xw c #534846", +".7f c #534855", +"arg c #535256", +"#Cq c #535666", +".mY c #536eaa", +".lj c #5375bb", +".JC c #5377c4", +".bf c #5379bd", +".bi c #5379bf", +".cP c #537abd", +".TT c #537ac1", +".VC c #537bbc", +".TU c #537bc2", +".TK c #537cb3", +".QI c #537cb6", +".Sg c #537cb8", +".Vw c #537cbb", +".TS c #537cc0", +".TR c #537cc1", +".TL c #537db4", +".Sf c #537db6", +".QO c #537dbe", +".QN c #537dbf", +".TQ c #537dc1", +".6O c #5380bf", +".5e c #5383c1", +"#8d c #540007", +".OE c #54271c", +".OD c #54281c", +".Zu c #543424", +".Tz c #544040", +".zQ c #544233", +".xa c #544d65", +".ak c #54567c", +"axR c #54668b", +".in c #546995", +".pL c #546c9b", +".ol c #546c9d", +".lh c #5470ac", +".W9 c #5477b8", +".Mm c #5477ba", +".X# c #5478b3", +".bg c #547abe", +".bh c #547abf", +".cS c #547ac0", +".QL c #547bbd", +".cQ c #547bbf", +".TW c #547bc2", +".Xh c #547cbb", +".VB c #547cbc", +".Sq c #547cc3", +".Se c #547db6", +".Vv c #547dbc", +".VA c #547dbd", +".Sl c #547dbf", +".QM c #547ebf", +".Sk c #547ec0", +"#X3 c #551506", +"#XX c #551904", +".9t c #551b0c", +"aLy c #552330", +".OQ c #55261b", +"#ah c #55281f", +"aLN c #553d4e", +"aIL c #55434c", +"ahR c #554843", +"aLf c #55484f", +".Hf c #554a4c", +".dM c #554b5f", +"#dk c #554e5e", +".sN c #555758", +".lg c #5570aa", +".Ml c #5572b0", +".TI c #5574a7", +".NE c #5578b8", +".Vn c #557ab4", +".NI c #557ac2", +".cR c #557bc0", +".Sh c #557cbc", +".Vz c #557dbe", +".VH c #557dbf", +".Vs c #557ebb", +".Xg c #557ebc", +".Vu c #557ebd", +".Vy c #557ebe", +".VG c #557ebf", +".Sj c #557ec0", +".Xd c #557fbc", +".Si c #557fc0", +".TP c #557fc1", +".8o c #5587bc", +"aNw c #560a13", +"#LF c #560b0f", +"aNy c #560b1b", +".74 c #56140b", +".9u c #561a0b", +".55 c #561d0c", +"#5l c #56200c", +"#.S c #562d18", +"aeo c #563726", +"aNu c #56392a", +".XU c #563b2c", +".ar c #564341", +"QtN c #56443c", +".zX c #564628", +".fe c #56464d", +".DU c #564c80", +"aAv c #564f53", +".Cl c #565165", +"aB8 c #565561", +"agn c #566790", +".wI c #566b8e", +".g0 c #566cac", +".GD c #5670a3", +".g1 c #5671b6", +".ND c #5674af", +".TJ c #567bb2", +".0h c #567cb9", +".Pg c #567cc0", +".TX c #567dc4", +".Xb c #567eb5", +".0o c #567ebd", +".Xj c #567ebe", +".Xi c #567ebf", +".VI c #567ec0", +".TM c #567fb9", +".Vr c #567fbc", +".Vt c #567fbd", +".Vx c #567fbe", +".TO c #567fc1", +".0j c #5681ba", +".8n c #5689bb", +"##k c #571002", +".9Q c #571105", +"#X4 c #571401", +"#XZ c #571f0d", +".dt c #572806", +"#FU c #572b27", +".o3 c #573116", +".8# c #57342c", +".zP c #574349", +".zL c #574636", +"aoD c #574c51", +"amk c #574c58", +"#7g c #575359", +".oC c #575369", +".Pb c #5775ae", +".Sc c #5776aa", +".tT c #5778ad", +".0f c #577aba", +".Pc c #577bb8", +".Vm c #577bba", +".X. c #577cbb", +".0g c #577cbc", +".5c c #577dbe", +".Vp c #577eb4", +".YN c #577fbe", +".Xk c #577fc1", +".0i c #5780ba", +".Vq c #5780bd", +".Xc c #5780be", +".Xf c #5780bf", +".VJ c #5780c2", +".0m c #5781be", +".1W c #5784bf", +".8l c #5787bf", +"#YH c #58081a", +"##l c #580d00", +"#WG c #581000", +"#Zv c #581502", +"#3e c #58371d", +"aOQ c #583b2f", +"aF5 c #58484f", +".i2 c #584946", +".0C c #584d5b", +"aFV c #584f55", +".kw c #585053", +".nO c #585151", +"aJW c #585158", +"avb c #58565c", +".q7 c #586b90", +".QG c #5876ad", +".1S c #587bba", +".YH c #587db9", +".1T c #587dbd", +".Xa c #587fb6", +".TN c #587fbe", +".YO c #5880c0", +".YP c #5880c1", +".Xl c #5880c2", +".Xe c #5881be", +".YK c #5881bf", +".YM c #5881c0", +".YQ c #5881c1", +".0r c #5881c3", +"a#r c #59040f", +"#XR c #590e00", +"#av c #591400", +".9M c #591803", +"#gx c #591a16", +".9Y c #592e1e", +".9j c #592f1d", +".0# c #594534", +"aLJ c #594555", +"aCo c #59464d", +".D# c #594b3e", +".gB c #59545e", +"ae5 c #59678f", +".K1 c #5971a8", +".om c #5975af", +".Vk c #597ab4", +".Sd c #597eb6", +".1Z c #5981c0", +".YR c #5981c2", +".YT c #5981c3", +".0k c #5982bf", +".0l c #5982c0", +".YL c #5982c1", +".YS c #5982c2", +".VK c #5982c3", +".0s c #5982c4", +".3B c #5988c4", +"#8# c #5a0009", +".9R c #5a1205", +"#au c #5a1501", +".56 c #5a1b0c", +"#3B c #5a200c", +".6b c #5a260f", +"aMu c #5a2635", +"#2. c #5a2a24", +"#k8 c #5a2f10", +".bO c #5a3b20", +"#Is c #5a4753", +".mt c #5a5147", +".Ib c #5a72a4", +".Mk c #5a72a7", +".Jw c #5a72aa", +".W8 c #5a7ab4", +".5b c #5a7cbb", +".QH c #5a7eb8", +".15 c #5a82c2", +".0p c #5a82c3", +".YU c #5a82c4", +".1X c #5a83c0", +".3F c #5a83c1", +".0n c #5a83c2", +".0q c #5a83c3", +".0t c #5a83c4", +".VL c #5a83c5", +".1V c #5a84bf", +".1Y c #5a84c1", +".6P c #5a8ac9", +"#Wn c #5b1000", +"aI3 c #5b1814", +"#Eu c #5b1e06", +".72 c #5b2611", +"#5m c #5b2915", +".ap c #5b3000", +".7y c #5b3323", +".ql c #5b3618", +"aor c #5b4139", +".x7 c #5b4542", +".lK c #5b577a", +"#vh c #5b5e68", +".lf c #5b6f7b", +".mW c #5b72a6", +".ss c #5b739e", +".NC c #5b74a7", +".YF c #5b7ebe", +".Vo c #5b81b8", +".3y c #5b81c1", +".YI c #5b82bb", +".1U c #5b83c0", +".3G c #5b83c2", +".3M c #5b83c3", +".14 c #5b83c4", +".3N c #5b83c5", +".YJ c #5b84bc", +".3C c #5b84c1", +".10 c #5b84c2", +".3E c #5b84c3", +".13 c #5b84c4", +".3D c #5b85c2", +".3A c #5b87c3", +".8m c #5b8ec1", +"#R0 c #5c0d00", +"aGV c #5c4032", +".zN c #5c4847", +".zR c #5c4a3a", +"#eT c #5c535a", +"amG c #5c595d", +".p6 c #5c5b77", +".jp c #5c5d55", +".Pa c #5c75a5", +".QF c #5c76a3", +".3x c #5c7fbe", +".YG c #5c81c0", +".5h c #5c84c3", +".5k c #5c84c4", +".12 c #5c84c5", +".16 c #5c84c6", +".5f c #5c85c2", +".3H c #5c85c3", +".3J c #5c85c4", +".11 c #5c85c5", +".5l c #5c85c6", +".6V c #5c85c7", +".6N c #5c86c5", +"#OE c #5d0812", +"#24 c #5d0a00", +"#az c #5d1703", +"#.Y c #5d200e", +"#1Z c #5d2700", +"#ao c #5d2c13", +".1M c #5d4539", +".3U c #5d5265", +".DT c #5d5483", +".rt c #5d5c7d", +".gX c #5d69a0", +".wH c #5d708f", +".Sb c #5d77a2", +".8p c #5d85c1", +".3I c #5d85c4", +".3L c #5d85c6", +".17 c #5d85c7", +".6Q c #5d86c3", +".5i c #5d86c4", +".5j c #5d86c5", +".3K c #5d86c6", +".9P c #5e1a10", +".Vd c #5e3325", +"#8w c #5e3822", +".dk c #5e4e4b", +".gi c #5e4f4c", +".vZ c #5e5254", +".vs c #5e576c", +"aAu c #5e595b", +"aay c #5e6c8e", +"aca c #5e749b", +".TH c #5e79a3", +".5g c #5e86c5", +".6T c #5e86c6", +".6U c #5e86c7", +".18 c #5e86c8", +".6S c #5e87c6", +".5m c #5e87c9", +".99 c #5e8abf", +"#ht c #5e91d1", +"aNx c #5f0517", +"#Uk c #5f0c1c", +"#X2 c #5f2115", +".9s c #5f2616", +"aNv c #5f312b", +".rI c #5f3a19", +".2J c #5f3e3c", +".S0 c #5f4139", +".hK c #5f504d", +"acW c #5f545a", +".oE c #5f5b71", +"aIn c #5f5d5f", +"#bd c #5f7fbf", +".8G c #5f85c5", +".8t c #5f87c3", +".3O c #5f87c9", +".3z c #5f88c6", +"#.. c #5f8bc0", +"##m c #601202", +"#XS c #601401", +"##i c #601905", +"#Zk c #602312", +".3k c #603016", +".OR c #603225", +"#49 c #603415", +"awZ c #60463c", +".u5 c #604a2c", +"#qF c #60504b", +".mK c #605545", +".k6 c #605647", +"#WN c #606986", +".fq c #606ca8", +".0e c #607eb7", +".8F c #6085c5", +".8u c #6086c4", +".8E c #6086c5", +"#.f c #6086c6", +"#.d c #6087c4", +".6M c #6087c7", +".8q c #6088c4", +".6R c #6089c7", +".3P c #6089ca", +"#Rx c #61091a", +"aKo c #611d08", +"#3F c #612209", +"#5k c #612411", +".OX c #61342e", +".8c c #613b34", +".tL c #614b3c", +".eP c #61524f", +"#dh c #615453", +".EK c #615953", +".gW c #615b84", +".wG c #616d85", +".YE c #6180b9", +".8k c #6186bf", +".8v c #6186c5", +".8D c #6186c6", +".8w c #6187c5", +"#.e c #6187c6", +".8C c #6187c7", +"#.a c #6188c4", +".8r c #6189c5", +"#hs c #618eca", +"#vR c #621400", +".6E c #623c42", +".s5 c #623d1b", +"aHF c #624554", +".wn c #624d33", +"aH4 c #625251", +".gG c #625254", +".Y0 c #626078", +".W7 c #627baa", +".Vj c #627cab", +"##D c #6282be", +".8x c #6287c6", +".8B c #6287c7", +".8y c #6288c6", +".8A c #6288c7", +".8z c #6288c8", +".8s c #6289c5", +"#.b c #6289c6", +"#.c c #628ac6", +"#rD c #628cc6", +"#X5 c #631d05", +"#X0 c #632b1a", +"#FT c #632c18", +"#ap c #63361c", +".8d c #633c36", +".uu c #633f1b", +".va c #634437", +"aJ8 c #63535c", +".8N c #635763", +"aDj c #635d62", +".AN c #635e76", +".pK c #63759a", +"asP c #63799c", +"##E c #6386bf", +"#bj c #6388c7", +"#.g c #6388c8", +"##J c #6389c7", +"##I c #6389c8", +"##K c #6389c9", +"#nu c #638aca", +"##H c #638bc7", +"#fW c #638bc8", +"#rC c #6391cf", +"#fX c #6391d2", +"#s1 c #6393d2", +"#9N c #640711", +"##n c #641603", +"#Zj c #642715", +"#a8 c #642722", +".45 c #643c34", +"#4Y c #643d1a", +".05 c #643f2e", +"aMK c #645160", +"#Iu c #645360", +".2b c #645b6b", +"#IA c #645d6a", +".2k c #645d7b", +"arN c #646066", +"aDo c #64636c", +".ru c #646384", +"axh c #64769c", +"#ba c #647cb9", +"#em c #6485c3", +".6L c #6486c4", +"#bi c #6489c8", +"#bl c #6489c9", +"#bh c #648ac8", +"#cY c #648ac9", +"#bk c #648aca", +"#rG c #648bc6", +"#be c #648bc7", +"#bf c #648cc8", +".98 c #648ec7", +"#N. c #650c0f", +"#KD c #65210f", +"#.3 c #652212", +"#XW c #652710", +".3i c #652712", +".9X c #653a2a", +".9e c #65462d", +".jn c #65504e", +"aj3 c #655a56", +".y1 c #655b7a", +"#5B c #656168", +".Fu c #65638a", +"#7o c #6579a6", +"aiH c #657da8", +".1R c #6582ba", +"#cU c #658ac9", +"#f1 c #658aca", +"#cV c #658bc9", +"#cX c #658bca", +"#cW c #658bcb", +"#bg c #658cc8", +"#cR c #658dc9", +"#kA c #658dce", +"#GG c #6593c4", +"a#q c #660714", +"#WF c #662209", +"#CY c #662c18", +".vS c #664531", +".6I c #66728f", +".im c #667696", +"aOM c #667fad", +".5a c #6681b7", +"#hq c #668abd", +"##G c #668bc7", +"#jb c #668bc8", +"#jc c #668bc9", +"#jd c #668bca", +"#f2 c #668bcb", +"apV c #668cc4", +"#er c #668cca", +"#et c #668ccb", +"#eu c #668ccc", +"#cS c #668dca", +"#oV c #668dcb", +"aoo c #668ec6", +"#cT c #668eca", +"#s0 c #6690cc", +"#38 c #6691d0", +"#8. c #670209", +"#IO c #670b16", +"#VO c #670f1e", +"#Wm c #671f08", +".75 c #67231a", +"#2g c #672d25", +"aN0 c #67493d", +"aJX c #676067", +".uV c #676250", +"aBT c #676267", +"#G6 c #676271", +"aq5 c #676368", +"aFu c #6778a3", +".0d c #677eaa", +"#ky c #6788c3", +"#Ol c #678ac8", +"#rE c #678bc1", +"#ja c #678bc8", +"#kF c #678bc9", +"#je c #678bca", +"#j# c #678cc9", +"#jf c #678cca", +"#es c #678ccb", +"#ev c #678ccc", +"#hw c #678dcb", +"#f3 c #678dcd", +"#eq c #678eca", +"#qd c #678fca", +"#eo c #678fcb", +"anc c #6790ca", +"and c #6791c8", +"#qc c #6791ce", +"#s2 c #6792cc", +"#.# c #6794cc", +"#Hh c #680d1b", +".3f c #682612", +"#e3 c #682b27", +"afX c #683115", +".3j c #68311a", +"aNs c #683326", +".UB c #68362e", +"#3o c #683a1a", +".3r c #684c48", +"aKa c #685760", +".bL c #685962", +".AT c #685c67", +".YD c #6880ae", +"#cP c #6884c3", +"#fU c #6887bd", +"#2A c #6888bc", +"#iX c #6889c5", +"#MS c #688bc9", +"#rF c #688cc4", +"#j. c #688cc9", +"#kz c #688cca", +"#jg c #688ccb", +"#i5 c #688dc7", +"#kE c #688dca", +"#l6 c #688dcb", +"#hv c #688dcc", +"#hx c #688dcd", +"aom c #688ec6", +"#kC c #688ec8", +"al3 c #688fca", +"#ep c #688fcb", +"#GC c #688fce", +"#fY c #6890cc", +"al4 c #6891c8", +"#GF c #6892c5", +"#Im c #6894c7", +"#23 c #690800", +"#SY c #690b1b", +"#cJ c #692219", +"#62 c #692d19", +".9i c #693a28", +"aeH c #693e29", +".xF c #694832", +".ag c #695957", +".8L c #695a75", +".69 c #696160", +".AJ c #696283", +"au7 c #696668", +".xp c #696768", +"#bb c #6984c0", +"#kx c #6988c3", +"#cQ c #6989cb", +"#l0 c #698ac5", +"#GV c #698bc9", +"#GU c #698cc8", +"#GT c #698cc9", +"#Ca c #698cca", +"#zk c #698dc7", +"#AI c #698dc9", +"#i9 c #698dca", +"#DG c #698dcb", +"#l7 c #698dcc", +"#i2 c #698ec8", +"#i8 c #698ecb", +"#nv c #698ecc", +"al2 c #698ecd", +"#i1 c #698fc8", +"#oW c #698fc9", +"akZ c #698fca", +"#Cb c #698fce", +"ap# c #6990c9", +"#fZ c #6990cc", +"#f0 c #6990cd", +"ak0 c #6991c8", +"#hu c #6991cd", +"#iZ c #6991d4", +"#T5 c #6994c8", +"#SE c #6996c8", +"#FA c #6a0f20", +"#Xe c #6a1d20", +"#X9 c #6a270e", +"#X1 c #6a2d23", +".OS c #6a3c2e", +".bV c #6a3d13", +".jx c #6a4e4b", +"arH c #6a5149", +".tC c #6a5637", +"aMG c #6a5b69", +"#bJ c #6a6269", +"adL c #6a80b0", +".3w c #6a87bd", +"#Ym c #6a87d4", +"#iW c #6a88c3", +"#DM c #6a8bc9", +"#DN c #6a8bca", +"#uH c #6a8cc9", +"#Cg c #6a8cca", +"#xU c #6a8dc8", +"#GS c #6a8dc9", +"#GW c #6a8dca", +"#GY c #6a8dcb", +"#GN c #6a8ec5", +"#ut c #6a8ec6", +"##F c #6a8ec7", +"#wm c #6a8ec8", +"#nt c #6a8eca", +"#i7 c #6a8ecb", +"#kD c #6a8ecc", +"#iY c #6a8ecd", +"#i3 c #6a8fc9", +"#i6 c #6a8fcc", +"#oX c #6a8fcd", +"#qi c #6a8fce", +"#DH c #6a8fcf", +"#kB c #6a90c9", +"#l4 c #6a90ca", +"#AJ c #6a90cd", +"ap. c #6a91c9", +"#uu c #6a91ca", +"#Ii c #6a91cf", +"#l3 c #6a91d2", +"#Il c #6a93c7", +"#SF c #6a98cb", +"#4O c #6b0702", +"#Kf c #6b0c12", +"#WH c #6b2000", +"#XT c #6b270d", +"#3A c #6b2916", +"ac1 c #6b2b16", +"#19 c #6b3831", +".Zv c #6b422e", +".se c #6b593c", +"#by c #6b6483", +".ok c #6b7eaa", +"#1a c #6b86ab", +"#uI c #6b8cc9", +"#AO c #6b8cca", +"#AP c #6b8ccb", +"#fV c #6b8dc6", +"asS c #6b8dc7", +"#uG c #6b8dc9", +"#uL c #6b8dca", +"#zl c #6b8dcb", +"ajT c #6b8dcd", +"aqP c #6b8ec3", +"#nr c #6b8ec9", +"#GR c #6b8eca", +"#Cc c #6b8ecb", +"#GX c #6b8ecc", +"#Io c #6b8fc6", +"#GM c #6b8fc7", +"#wf c #6b8fc8", +"#uE c #6b8fc9", +"ajU c #6b8fca", +"#GQ c #6b8fcb", +"#rI c #6b8fcc", +"#rJ c #6b8fcd", +"#qj c #6b8fce", +"ajV c #6b90c8", +"#xO c #6b90c9", +"#i0 c #6b90ca", +"apW c #6b90cc", +"#rK c #6b90cd", +"#rL c #6b90ce", +"#Fa c #6b90d0", +"#T2 c #6b90d2", +"#hr c #6b91c9", +"#rB c #6b91cb", +"#zg c #6b91cd", +"#wg c #6b92cc", +"#MI c #6b94cc", +"#Ob c #6b95ca", +"#T6 c #6b97cb", +"#Ra c #6b97ce", +"#R# c #6b98cc", +"#Z7 c #6c081a", +"#25 c #6c0a02", +"#8c c #6c0a15", +"#Wo c #6c1f09", +"#Vc c #6c2000", +"#aw c #6c2712", +".4U c #6c2a19", +"#61 c #6c2d18", +"#5o c #6c2e14", +"#1p c #6c314e", +"#0E c #6c3512", +".54 c #6c3925", +".3m c #6c442b", +".6F c #6c4a59", +"aLO c #6c5163", +".C7 c #6c5e51", +"abr c #6c6166", +"aB7 c #6c6468", +"av9 c #6c686b", +"ap9 c #6c686e", +".vf c #6c81a3", +"#cO c #6c84c1", +"#el c #6c88c3", +"#Cd c #6c8cc7", +"#Ce c #6c8dc7", +"#DJ c #6c8dc8", +"#uJ c #6c8dca", +"#xV c #6c8dcb", +"#xW c #6c8dcc", +"aiN c #6c8dce", +"anb c #6c8dd0", +"#uF c #6c8eca", +"#uK c #6c8ecb", +"#uM c #6c8ecc", +"#ns c #6c8fc8", +"#zj c #6c8fca", +"#AK c #6c8fcb", +"#GP c #6c8fcc", +"#DI c #6c8fcd", +"#GL c #6c90c7", +"#GK c #6c90c8", +"aaN c #6c90c9", +"#wl c #6c90ca", +"#uD c #6c90cb", +"#Iq c #6c90cc", +"#rM c #6c90cf", +"#PG c #6c91c3", +"#uv c #6c91c8", +"#4l c #6c91c9", +"#i4 c #6c91cb", +"#oS c #6c91cc", +"aqQ c #6c92ca", +"#xP c #6c92cd", +"#JR c #6c92cf", +"#JT c #6c93ca", +"#qg c #6c93cf", +"#JQ c #6c93d0", +"#Vr c #6c94cb", +"#Lh c #6c94ce", +"#T4 c #6c95c9", +"#JU c #6c95ca", +"#MJ c #6c95cc", +"#Oc c #6c96cc", +"#1i c #6c96cd", +"#37 c #6c96d5", +"#SD c #6c97c8", +"#SG c #6c97cd", +"#PK c #6c98cd", +"#8a c #6d0d18", +"#y1 c #6d0e00", +"#ub c #6d1700", +"aCp c #6d2238", +"abv c #6d3b2c", +"#8x c #6d3f29", +".3l c #6d4226", +"aGl c #6d5564", +".5v c #6d5d57", +".2f c #6d6064", +".uW c #6d695a", +".jr c #6d6e65", +".6K c #6d88bd", +"#qb c #6d8dc4", +"#AL c #6d8dc6", +"#DK c #6d8dc9", +"al1 c #6d8dd0", +"akY c #6d8dd1", +"#AM c #6d8ec7", +"#l1 c #6d8ec9", +"#Fc c #6d8eca", +"#uN c #6d8ecc", +"#uO c #6d8ecd", +"#uw c #6d8fc3", +"#ux c #6d8fc4", +"#JS c #6d8fc9", +"#Cf c #6d8fcb", +"#DL c #6d8fcc", +"#uP c #6d8fcd", +"asi c #6d8fcf", +"aiO c #6d90c9", +"#AN c #6d90ca", +"#uz c #6d90cb", +"#GO c #6d90cc", +"#Ip c #6d90cd", +"#Fb c #6d90ce", +"#qe c #6d91c8", +"#wh c #6d91c9", +"#56 c #6d91ca", +"#oU c #6d91cb", +"#Lj c #6d91cc", +"#JV c #6d91cd", +"#4k c #6d92c9", +"#9q c #6d92ca", +"#l5 c #6d92cc", +"#Li c #6d92cd", +"aaM c #6d93ca", +"#rH c #6d93cd", +"#s5 c #6d93ce", +"#en c #6d93d5", +"aug c #6d93dd", +"#Vq c #6d94cc", +"#Vt c #6d95cb", +"#MK c #6d95cd", +"#Rb c #6d95d0", +"#Vs c #6d96cc", +"#PI c #6d97cb", +"#Od c #6d97ce", +"#PJ c #6d98cc", +"#PL c #6d98cf", +"#R. c #6d99cc", +"#4P c #6e0000", +"#P7 c #6e0f19", +"#1q c #6e1933", +"#vS c #6e2203", +"#KA c #6e2d03", +"#XV c #6e2d15", +"#FS c #6e2f10", +".UA c #6e413e", +".9W c #6e4434", +"aNt c #6e4939", +"aFz c #6e5244", +".sl c #6e5a4b", +".FE c #6e6462", +"#Lu c #6e6773", +".jf c #6e677c", +"aAt c #6e6a6b", +"as2 c #6e6a70", +"#DW c #6e727e", +"auP c #6e7da0", +"akT c #6e7e9b", +".1Q c #6e83ae", +"#fT c #6e84b0", +"#C# c #6e8bc2", +"#DF c #6e8bc4", +"#bc c #6e8bc8", +"#AH c #6e8cc2", +"adU c #6e8ccc", +"agt c #6e8dce", +"ajS c #6e8dd1", +"#PE c #6e8eba", +"#nq c #6e8ec8", +"#Fd c #6e8eca", +"ahG c #6e8ecb", +"aiM c #6e8ed1", +"#wi c #6e8fc5", +"#zh c #6e8fc7", +"ahH c #6e8fc9", +"#Ik c #6e8fca", +"#Ij c #6e8fcb", +"#wn c #6e8fcd", +"#uQ c #6e8fce", +"#wj c #6e90c5", +"#4m c #6e90cb", +"#Fe c #6e90cd", +"#wo c #6e90ce", +"arC c #6e90d1", +"#uy c #6e91c7", +"#oT c #6e91c9", +"#Lk c #6e91cb", +"#uA c #6e91cc", +"#Lm c #6e91cd", +"#Ok c #6e91ce", +"#Ir c #6e91cf", +"#GH c #6e92c9", +"#In c #6e92ca", +"#xQ c #6e92cb", +"#uC c #6e92cc", +"#PR c #6e92ce", +"#l2 c #6e92cf", +"#Vu c #6e93c9", +"a.7 c #6e93ca", +"#qh c #6e93cd", +"#oR c #6e93cf", +"#4j c #6e94ca", +"#55 c #6e94cb", +"#T7 c #6e94cc", +"#SH c #6e94cd", +"#ML c #6e95ce", +"#Q7 c #6e96c2", +"#Q6 c #6e97c6", +"#4d c #6e97cb", +"#5Y c #6e97cd", +"#4e c #6e98cb", +"#5Z c #6e98cd", +"#Oe c #6e98d0", +"#1B c #6f0100", +"a#p c #6f0c19", +"#2T c #6f1719", +"aCq c #6f233a", +"#Y. c #6f290c", +".9S c #6f2918", +"#Hy c #6f2b02", +".46 c #6f4b49", +".zm c #6f4c32", +".yr c #6f7281", +"#ej c #6f7eb0", +"#lZ c #6f8ac1", +"#F# c #6f8cc5", +"#us c #6f8dbf", +"adT c #6f8dcc", +"afd c #6f8dce", +"#Ih c #6f8ebb", +"agu c #6f8ecb", +"adS c #6f8ecc", +"ahF c #6f8ed1", +"afc c #6f8ed2", +"ao9 c #6f8fc7", +"#xR c #6f90c6", +"#xS c #6f90c7", +"#GE c #6f90cb", +"#9r c #6f90cc", +"#uR c #6f90ce", +"#uS c #6f90cf", +"#zi c #6f91ca", +"#Oa c #6f91cc", +"#uT c #6f91cf", +"#wk c #6f92c9", +"#uB c #6f92cd", +"#W5 c #6f92ce", +"#JW c #6f92cf", +"#GJ c #6f93ca", +"#MR c #6f93cb", +"#Ll c #6f93cc", +"#U. c #6f93cf", +"#s3 c #6f94c9", +"#9p c #6f94cb", +"#s6 c #6f94ce", +"#MM c #6f94cf", +"#T3 c #6f94d4", +"#Yu c #6f95c7", +"a.6 c #6f95cb", +"#54 c #6f95cc", +"auT c #6f95de", +"#9l c #6f96ce", +"#PM c #6f96d1", +"#4c c #6f97cc", +"#SB c #6f97ce", +"#9m c #6f98d0", +"#50 c #6f99ce", +"#7C c #6f99d0", +"#Q9 c #6f9acc", +"#6w c #700005", +"#79 c #70030c", +"#L3 c #702202", +"#X6 c #70280c", +"aMv c #703746", +".XW c #704436", +".47 c #705158", +".BE c #705c47", +"ak1 c #706166", +".jB c #706263", +"aIq c #706e70", +"QtG c #707197", +"aoi c #707bba", +"#DD c #7086b3", +"aqN c #7087b8", +"#zf c #708dc2", +"afe c #708dcb", +"#we c #708ec0", +"#JP c #708ec7", +"aci c #708ecd", +"apU c #708fc5", +"agv c #708fc9", +"ach c #708fce", +"#1c c #7090c7", +".97 c #7090c9", +"#GD c #7090cd", +"#57 c #7091cd", +"adP c #7091cf", +"#7J c #7091d0", +"#xT c #7092ca", +"agr c #7092cc", +"#4n c #7092ce", +"avs c #7092d4", +"#MN c #7093cf", +"#Vz c #7093d0", +"#WZ c #7094c7", +"#MQ c #7094cb", +"#qf c #7094cc", +"#W4 c #7094d0", +"#PH c #7095c5", +"#PF c #7095c6", +"#WY c #7095c9", +"adR c #7095cc", +"#7y c #7095ce", +"#WX c #7096cc", +"#9o c #7096cd", +"#WV c #7096cf", +"#5Q c #7096d0", +"af# c #7096d2", +"#1h c #7097cf", +"#Of c #7097d1", +"#7B c #7098d0", +"#OF c #710c10", +"#LG c #710d10", +"#9H c #711215", +"##g c #711f07", +"#Zi c #712911", +"#5j c #712e1a", +".4S c #713013", +".OT c #714233", +"aJs c #714942", +".Yy c #71533e", +".rV c #715849", +".zK c #71614a", +".30 c #71656f", +"aHT c #716973", +"#eU c #716a79", +"aIp c #716f71", +"#Vj c #717692", +"#xN c #718ec2", +"aff c #718ec9", +"al0 c #718ed2", +"akX c #718ed3", +"#7N c #718fce", +"#9s c #718fcf", +"aiL c #718fd3", +"#7L c #7190ce", +"#7M c #7190cf", +"ahE c #7190d3", +"afb c #7190d4", +"#MP c #7191ce", +"ahD c #7192cb", +"#MO c #7192ce", +"ags c #7192cf", +"acg c #7192d0", +"afa c #7192d2", +"#33 c #7193ca", +"#7w c #7193cb", +"ash c #7193cd", +"#53 c #7193d1", +"av0 c #7193d3", +"#W0 c #7194c7", +"#7E c #7194cb", +"#SL c #7194d0", +"awV c #7194d4", +"#Sz c #7194d7", +"#PQ c #7195cc", +"#Vy c #7195cd", +"#7x c #7195ce", +"#WU c #7195d0", +"#WT c #7195d1", +"#WW c #7196ce", +"avt c #7196de", +"#Q5 c #7197c6", +"#7K c #7197cd", +"adQ c #7197ce", +"apa c #7197d1", +"#ZU c #7198cc", +"#4b c #7198ce", +"aon c #7198d0", +"#SA c #7198d4", +"#5X c #7199d0", +"#SC c #719bcc", +"#2H c #719cd5", +"#VP c #720d1c", +"#6C c #721819", +"#To c #72270a", +".3h c #72301c", +"#5n c #72361c", +"#am c #72361f", +".9V c #72371d", +".XV c #72493d", +".Ry c #724d3e", +"aDv c #72666c", +".zd c #726866", +".YX c #726870", +".te c #726a55", +"##M c #726b7f", +"#sZ c #728cbb", +"a.9 c #728fcf", +"aiJ c #7290cb", +"aue c #7290cc", +"a.8 c #7290cf", +"#6. c #7290d0", +"#MH c #7291d2", +"#GB c #7292b8", +"#58 c #7292cf", +"#O# c #7293cb", +"aaK c #7293d0", +"a.5 c #7293d1", +"aaL c #7293d2", +"#Vv c #7294ca", +"#SK c #7294cc", +"aaJ c #7294d0", +"#Og c #7294d1", +"#Rc c #7294d2", +"#Vp c #7294de", +"#s4 c #7295cc", +"#Vx c #7295cd", +"#34 c #7295cf", +"#Yn c #7295d1", +"auS c #7295d9", +"#GI c #7296cd", +"#W3 c #7296ce", +"av1 c #7296db", +"#Yt c #7297ca", +"#Q8 c #7298c1", +"#2G c #7299d3", +"#5R c #7299d4", +"#7A c #729ad2", +"#7D c #729cd3", +"#0f c #730202", +"#1E c #730307", +"#Kh c #730b07", +"a#o c #730f1c", +"#sE c #731d04", +".OW c #73433c", +".91 c #734436", +".OU c #734536", +"aJ# c #735468", +"aww c #735950", +".uP c #73602c", +"#AS c #736368", +"QtA c #736682", +".D3 c #736772", +".kP c #736a60", +"apq c #736a6f", +"aJY c #736c73", +".j7 c #736f96", +"aIo c #737173", +".96 c #737eaa", +".le c #738590", +"#9c c #738ab7", +"asf c #7390bc", +"ajR c #7390d2", +"aiK c #7391ce", +"#59 c #7391d0", +"#Lg c #7391d2", +"#Vn c #7391d9", +"a.V c #7392c2", +"#SI c #7393cf", +"#4o c #7393d0", +"asg c #7394c7", +"#ZO c #7394cf", +"adO c #7394d1", +"#PN c #7394d2", +"#W1 c #7395cd", +"#4g c #7395ce", +"#4h c #7395cf", +"#2F c #7395d0", +"#Yo c #7395d1", +"#52 c #7395d2", +"#4i c #7395d3", +"#ZV c #7396cd", +"#1j c #7396ce", +"aEa c #7396da", +"aCV c #7396db", +"az. c #7396de", +"#Oj c #7397ce", +"#W2 c #7397cf", +"aBw c #7397dd", +"aAg c #7397de", +"#4a c #7398d0", +"aqR c #7398d4", +"#7z c #7399d1", +"#36 c #739bd8", +"#1C c #74000a", +"#LH c #740205", +"#Kg c #74020a", +"#IP c #740210", +"#YI c #74131c", +"#Nx c #741903", +"#RZ c #741f00", +"#t1 c #742408", +"#gz c #742611", +"aCr c #74263d", +"#I7 c #742f00", +"#bU c #744139", +".7x c #744837", +"aGz c #745361", +".uH c #745b56", +".tk c #746059", +"#6g c #746a62", +"#.C c #746b72", +"#bA c #747097", +"QtQ c #74769a", +".sr c #74829d", +"#N9 c #748bb2", +"#C. c #748bbd", +"aok c #748cc7", +"ana c #748ed1", +"ajP c #748fcb", +"#Oi c #748fd0", +"akW c #748fd1", +"ajQ c #7490ce", +"ahB c #7491c3", +"#Oh c #7491d0", +"aaF c #7492c5", +"#2B c #7492c7", +"#4q c #7492d2", +"#1b c #7493c1", +"a.W c #7493c4", +"aol c #7493cd", +"#4p c #7493d2", +"atq c #7494c5", +"agq c #7495cd", +"#T8 c #7495cf", +"#Yp c #7495d0", +"#7I c #7495d2", +"#T1 c #7495da", +"#7v c #7496cd", +"#4f c #7496ce", +"#Yq c #7496cf", +"a.4 c #7496d0", +"#39 c #7496d1", +"#51 c #7496d2", +"a.3 c #7497cf", +"#5P c #7497d0", +"arA c #7497d1", +"arB c #7497d5", +"#ZW c #7498cf", +"#Yv c #7498d0", +"#9k c #749ad1", +"#5W c #749ad2", +"#35 c #749ad5", +"#Na c #750301", +"#4N c #750605", +"#t0 c #752307", +"aCs c #75253d", +"#b8 c #752811", +"#E3 c #753112", +"#3G c #753118", +".3g c #75321f", +"#0Y c #753325", +"#Ja c #753c23", +".52 c #754931", +"ajp c #754f40", +".AY c #755032", +".8b c #75514a", +".wz c #755732", +"#cM c #7582b8", +"ahA c #758bb7", +"#cN c #758bc7", +"#AG c #758cbd", +"#DE c #758cbf", +"#kw c #758cc3", +"#ur c #758dba", +"#ek c #758ec7", +"akU c #758ecb", +"#5K c #758fc3", +"akV c #758fce", +"#E9 c #7590c0", +"asQ c #7591bb", +"auR c #7592cd", +"#WR c #7592df", +"aaG c #7593c8", +"axS c #7593cc", +"#32 c #7594cb", +"atr c #7595d7", +"asR c #7596c7", +"axT c #7596d4", +"a.2 c #7597cf", +"#7F c #7597d0", +"#4. c #7597d1", +"#9n c #7597d2", +"#Yr c #7598cf", +"#4# c #7598d1", +"auf c #7598de", +"#Ys c #7599cd", +"a.0 c #7599cf", +"aFw c #7599db", +"a.1 c #759ad1", +"#5V c #759ad3", +"#0g c #76000b", +"#Hi c #760214", +"#OH c #760400", +"#Ul c #760c1b", +"#N# c #760d0d", +"#xx c #761500", +"#sG c #761902", +"#vZ c #762004", +"#Vb c #762d10", +"ah9 c #763213", +".6p c #763417", +"#XU c #76341a", +"#0N c #763f33", +".4Z c #76402b", +"#Ep c #76461c", +"#N4 c #764737", +"aid c #764a36", +"azB c #766c79", +"avM c #76737a", +".Ce c #767383", +"#b# c #7685be", +"aoj c #7686c4", +"awS c #7689b0", +".5# c #7689b1", +"#iV c #768bc1", +"#5I c #768cb8", +"avq c #768cba", +"#F. c #768cc0", +"an. c #768dca", +"#wd c #768ebb", +"#ze c #768ebd", +"alY c #768eca", +"an# c #768ecd", +"alZ c #768ece", +"#oQ c #768fc2", +".jI c #7691bc", +"#PO c #7692d3", +"arz c #7694c3", +"aqO c #7694c7", +"#Sy c #7694da", +"aaH c #7695cb", +"#WS c #7695e5", +"#Vw c #7696cd", +"#ZP c #7696d1", +"aws c #7696d4", +"#7H c #7697d3", +"#Vo c #7697e1", +"aaI c #7698d0", +"adN c #7698d1", +"#7G c #7698d2", +"#ZT c #7699d0", +"#9i c #769ad0", +"#5S c #769eda", +"#FB c #770317", +"#Q. c #770500", +"#6x c #77050d", +"#9M c #770a18", +"aCx c #77294b", +"#Zh c #772f17", +"#3z c #77301c", +"#as c #773420", +"#6D c #773a30", +"#Eo c #77411b", +".Wo c #77463a", +".uz c #77494a", +"aHL c #775a73", +".z2 c #775e42", +".u1 c #776141", +"aGI c #77635e", +".wf c #776545", +".qS c #77664d", +"##7 c #776a60", +"#.u c #77707c", +"#J3 c #77717f", +"aAQ c #77727e", +"asv c #777379", +".pC c #777469", +"aoP c #77747a", +"aJh c #77757b", +"#TX c #777da3", +".3v c #778ab4", +".8j c #778ab5", +"#WP c #778cc5", +"auQ c #778dbd", +"##C c #778dc7", +"ae8 c #778ebe", +"#xM c #778fbd", +"acf c #7790c3", +"#3Z c #7792c8", +"ae9 c #7793c7", +"#T0 c #7793d8", +"aaE c #7794c5", +"#2E c #7794cf", +"ats c #7795dc", +"#5N c #7796cb", +"axj c #7796d0", +"a.X c #7797c9", +"ahC c #7797ca", +"asj c #7797da", +"#5O c #7798ce", +"#9j c #779bd2", +"#5U c #779bd5", +"#6v c #780007", +"#1A c #780201", +"#LI c #780402", +"#4U c #780c0b", +"#P8 c #780c10", +"#Xf c #780d19", +"#dE c #782309", +"aCu c #78243d", +"#Vd c #782d09", +"ag5 c #782f10", +"#Zz c #78371d", +"#CU c #78421c", +"##t c #784225", +".06 c #784e36", +".Wm c #784f49", +".y. c #78634f", +"#PA c #786667", +"##L c #78727d", +".sE c #787474", +"#N7 c #78767e", +".oK c #7876a2", +"#B9 c #7885ab", +"#TY c #7885b8", +"aqM c #7887b7", +"acb c #788eb8", +"awT c #7892c5", +"awr c #7892c8", +"#Rd c #7892d5", +"#WQ c #7892d7", +"avZ c #7894cc", +"avr c #7894ce", +"#5M c #7895c8", +"#1d c #7896ce", +"#1g c #7896cf", +"awU c #7897d3", +"asT c #7897dc", +"#ZS c #7898d0", +"#9h c #789bd1", +"#YQ c #790304", +"#OG c #790b09", +"#8b c #791421", +"aCt c #79273f", +"#e5 c #792e18", +"#Zg c #79331b", +"#60 c #793620", +"#Tp c #793627", +"#5p c #79381e", +"#.P c #793e1d", +"#63 c #79402d", +"##u c #794125", +".8e c #79504b", +"aML c #796374", +".82 c #796c6a", +"aJZ c #797379", +"apn c #79757b", +"#Le c #7985ad", +"ao7 c #7989c4", +"ary c #798fba", +"#PP c #7990d3", +"#Lf c #7991c9", +"#Vm c #7992d3", +"#hp c #7993bd", +"#Yl c #7993d9", +"#Q4 c #7994bd", +"#MG c #7995d0", +"#SJ c #7995d4", +"aId c #7996ce", +"aGS c #7996cf", +"aFv c #7996d0", +"#31 c #7997cd", +"#ZR c #7997d1", +"#ZQ c #7998d2", +"a.Z c #799cd0", +"aGT c #799ddf", +"#YR c #7a010c", +"#RC c #7a0401", +"#P9 c #7a0805", +"#26 c #7a0907", +"#v0 c #7a1900", +"#t2 c #7a2c0f", +"afP c #7a2d0d", +"#X7 c #7a3011", +"#8J c #7a3620", +".P4 c #7a3b2b", +".9N c #7a3e2a", +"#an c #7a432c", +".hQ c #7a4e1c", +".W3 c #7a503d", +".43 c #7a5343", +"aM6 c #7a5c50", +"aHU c #7a6971", +".Y4 c #7a728c", +".vn c #7a7388", +"#Cp c #7a7e8c", +"#qa c #7a87af", +"#np c #7a8ab8", +"#9a c #7a8db6", +"#rA c #7a8fbc", +"#TZ c #7a8fcd", +"aud c #7a91c1", +"#9d c #7a94c2", +"#7r c #7a94c3", +"#5L c #7a96c9", +"aE# c #7a96d2", +"#O. c #7a98ca", +"#7u c #7a9acf", +"a.Y c #7a9ccf", +"#5T c #7a9dd7", +"#1D c #7b0b13", +"aCv c #7b253f", +"##o c #7b2f19", +"aGF c #7b2f2f", +"a#n c #7b3233", +"#Zw c #7b331d", +".76 c #7b372a", +"#.4 c #7b3828", +".9U c #7b3d25", +"#Pz c #7b4d40", +".Zw c #7b503b", +"#L# c #7b522d", +".3n c #7b533a", +".3o c #7b533c", +"aKC c #7b5f71", +".zD c #7b6a52", +".bK c #7b6e89", +"ama c #7b7270", +"#3R c #7b777e", +".jo c #7b7c74", +"#Vl c #7b8dc4", +"#9b c #7b90bb", +"avY c #7b90bc", +"#Sx c #7b91d4", +"apT c #7b92c8", +"axi c #7b95c6", +"atp c #7b97bf", +"#9f c #7b97c8", +"aCU c #7b97d4", +"#T9 c #7b98d4", +"aNX c #7b9bd0", +"#YU c #7c0102", +"#0e c #7c0202", +"#S3 c #7c0302", +"#22 c #7c0704", +"#0h c #7c0a12", +"#Ry c #7c0b19", +"#1r c #7c0d1f", +"#b9 c #7c2f18", +"#a7 c #7c350f", +".6o c #7c351a", +"#Dx c #7c3819", +"#E# c #7c4149", +".53 c #7c4e37", +".8g c #7c524d", +".Cw c #7c5633", +".jw c #7c605b", +".u8 c #7c624c", +"aKF c #7c6974", +"#3W c #7c8dae", +"aE. c #7c8dba", +"#JO c #7c90bf", +"#Re c #7c91d6", +"#7q c #7c94c3", +"aJD c #7c95c3", +"#30 c #7c98d0", +"#1f c #7c98d2", +"aON c #7c9cd1", +"af. c #7c9cd4", +"#9g c #7c9ed4", +"#Uu c #7d0302", +"#Xm c #7d0305", +"#O4 c #7d1700", +"aCw c #7d2640", +"#Wp c #7d2e19", +"#5i c #7d3621", +"#65 c #7d3a1f", +"#8K c #7d3a26", +".3e c #7d3c28", +".UC c #7d4638", +"#JJ c #7d4c2c", +".Wn c #7d5149", +".4n c #7d5956", +"aJi c #7d5d62", +".tH c #7d624a", +"#N5 c #7d6657", +".x6 c #7d685d", +"#Q1 c #7d6e7a", +"#MC c #7d6f68", +"#Iv c #7d6f7c", +"#GA c #7d8fab", +"#MF c #7d92c1", +"#PD c #7d93b6", +"#3Y c #7d95c5", +"aiI c #7d97c4", +"#ZM c #7d97ce", +"#2D c #7d98d1", +"#7s c #7d99ca", +"aBv c #7d99d7", +"#1e c #7d9ad3", +"aM3 c #7d9cd2", +"aIe c #7da1e2", +"#Uq c #7e0303", +"#Xn c #7e030d", +"#78 c #7e0410", +"#YS c #7e0810", +"#IQ c #7e221d", +".7O c #7e2d08", +"#b7 c #7e3119", +"##p c #7e371d", +"#Ib c #7e4426", +"a#S c #7e4938", +"aKG c #7e4956", +".tl c #7e6c65", +"aIK c #7e6c75", +"#.B c #7e7066", +"aIr c #7e7d7f", +"##B c #7e7faa", +"#lY c #7e86b0", +"#Sw c #7e8cc4", +"awq c #7e92bb", +"#ZL c #7e93bd", +"ao8 c #7e96cf", +"aNW c #7e97c5", +"ace c #7e97c8", +"aaA c #7e99c3", +"aaD c #7e9ac8", +"#2C c #7e9ad1", +"#7t c #7e9cd0", +"#RB c #7f0203", +"#VV c #7f0304", +"#0j c #7f0405", +"#6p c #7f0607", +"#6y c #7f0610", +"#Xg c #7f0b16", +"#Va c #7f3a22", +"#Dy c #7f3a28", +"aGq c #7f3a5b", +"#0M c #7f463a", +".Qv c #7f5142", +"#MA c #7f6043", +"#2Q c #7f6667", +".qA c #7f6f67", +".fh c #7f6f76", +"#bH c #7f7270", +".vO c #7f7281", +"aG5 c #7f7679", +".fj c #7f7a75", +"aAs c #7f7b7c", +"aur c #7f7c7f", +"azv c #7f7e87", +"#Ig c #7f92b5", +"a.U c #7f94bd", +"agp c #7f98c9", +"#9e c #7f9bcb", +"aAf c #7f9bda", +"aJE c #7f9ed4", +"aKV c #7f9fd5", +"#0i c #800e12", +"#EF c #80290f", +"#aC c #802a07", +"ag9 c #803314", +"#h5 c #80331f", +"#X8 c #803413", +"#e4 c #803e31", +"#Gv c #804025", +"#Bn c #804b24", +".OV c #804e46", +".eY c #80522f", +"aGn c #805f72", +".pn c #806a4c", +"alf c #80727f", +".8i c #807c8e", +"#WO c #808eb9", +"auc c #808fb4", +".6J c #8092ba", +"ae7 c #8093c0", +"ajO c #8094b9", +"ay9 c #809cdb", +"aPJ c #809fd5", +"#Q# c #810000", +"#4H c #810200", +"#S2 c #810304", +"#73 c #81030c", +"#Nb c #810403", +"#VW c #81040c", +"#OJ c #810708", +"#Xo c #81070e", +"#YK c #810810", +"#VQ c #810a17", +"#SZ c #810b19", +"#Xr c #810d07", +"#Qb c #811016", +"#EQ c #81240b", +"#aU c #81311d", +"aey c #813212", +"#c. c #81351d", +"#8I c #813c25", +".4V c #813e2d", +"#C0 c #81422a", +".gr c #81542b", +".51 c #81553c", +"#La c #815d3e", +"aDw c #81767b", +"arL c #817d83", +"#PC c #81879d", +"#DC c #818b9c", +"#Vk c #818cb5", +"#sY c #818db0", +"aaz c #8190b2", +"#7p c #8198c6", +"aKU c #8199c8", +"aaB c #819cc6", +"#ZN c #819fdd", +"#4T c #820005", +"#Ur c #820309", +"#VZ c #820707", +"#4Q c #82080c", +"#yI c #822905", +"#t3 c #823719", +"#aT c #823824", +"#1R c #823917", +"afG c #825740", +".7u c #82623d", +".u9 c #826653", +".zY c #82694d", +"#di c #82746b", +".dO c #82778b", +"aju c #827e83", +".sX c #8288aa", +"#ME c #828dac", +"#N8 c #828ea6", +"apS c #8292c8", +"a.R c #8293b8", +"aaC c #829dc9", +"#OI c #830200", +"#9L c #830a1b", +"#Z8 c #830c14", +"#YJ c #83181b", +"#dD c #832a14", +"#dC c #833117", +"#b6 c #83311d", +"ajm c #83442b", +"#dr c #834b45", +".4p c #83573d", +".3p c #835c4a", +"#xZ c #837790", +"aC8 c #837c7e", +".K# c #837da3", +"abV c #837e86", +"aBS c #837f83", +"#ho c #838197", +"#Sv c #8389b6", +"ase c #8399c0", +"#Yk c #8399d4", +"acc c #839ac7", +"aQG c #83a3d8", +"#6u c #84000a", +"#Z9 c #840104", +"#Up c #840305", +"#S4 c #840308", +"#4M c #840609", +"#9I c #840616", +"#Xp c #84080d", +"#Xq c #840909", +"#29 c #84110f", +"#t9 c #843516", +"#vQ c #843518", +"#5h c #843d25", +"#dt c #844027", +"ajj c #844528", +"#mw c #844913", +"aha c #844a2c", +"aHR c #844f70", +".50 c #84573f", +"##6 c #847776", +".Hi c #847786", +"aBR c #847f84", +".yV c #847f9b", +".oB c #848096", +"#Su c #8485aa", +"a.T c #8498c1", +"acd c #849ccc", +"#Xl c #850406", +"#RD c #850408", +"#Qa c #850709", +"#4E c #851318", +"#xy c #852508", +"#A# c #852f09", +"#dF c #853117", +"#b5 c #853a23", +"#WE c #853c13", +"#6Y c #853e24", +"#6Z c #853f28", +"#66 c #854026", +"#64 c #854329", +"#bW c #85492e", +"##v c #854c2f", +"#ar c #854c36", +"#6P c #855339", +"aHP c #855575", +".ta c #85574f", +".D8 c #855d35", +"aJf c #856377", +"#Yd c #856b61", +".mx c #85746c", +".dN c #857a8f", +"aAp c #858082", +"ay8 c #8595c6", +"ago c #859ac7", +"#27 c #860206", +"#RA c #860308", +"#0. c #860309", +"#6z c #860311", +"#1z c #860402", +"#VU c #860406", +"#VX c #86060d", +"#6q c #86070a", +"#9J c #860718", +"#Nc c #861414", +"#F8 c #862807", +"##h c #863b25", +"#US c #863c1d", +"a.l c #863e27", +"#3y c #863e28", +"ai# c #863f24", +"#FR c #864418", +"#b4 c #86462b", +".R4 c #86583b", +"av4 c #866c63", +".zZ c #866d51", +".jm c #867372", +".rU c #867966", +".b9 c #86838f", +"#iU c #8685a9", +"#oP c #8689a9", +"ae6 c #8695bf", +"#74 c #87040f", +"#6A c #870412", +"#VY c #87050b", +"#YL c #87070e", +"#YT c #871115", +"#6B c #87121b", +"aGC c #874349", +"#Wv c #87462a", +".9w c #87473a", +"#Zb c #874d31", +".Vc c #875242", +".9h c #875330", +".XX c #875745", +"ahb c #875a43", +"#3d c #875f39", +"akq c #876357", +"aKD c #87687b", +"#ZE c #876e64", +"#ph c #87736a", +".dF c #877e86", +"#E8 c #8796ab", +"#Yj c #8797c3", +"aAe c #8797c8", +"aCT c #8798c6", +"#3X c #879cc5", +"adM c #879ccd", +"aL7 c #879fce", +"#0d c #880403", +"#S1 c #880409", +"#Rz c #880711", +"#9K c #88091a", +"#Um c #880a17", +"#V0 c #881d14", +"#Au c #882c0d", +"#TR c #883c18", +"a.k c #884228", +"#XP c #88422d", +"#bX c #884325", +"aia c #88452a", +"#2h c #88463b", +".9r c #885140", +"#JI c #885530", +".rP c #885b45", +".uG c #885b5d", +".8f c #885f59", +"aEd c #886c5f", +"aJd c #886d7f", +"#.p c #887977", +".xB c #887b8a", +".Hd c #887e7b", +"#ei c #887e9d", +"#J4 c #88808d", +"ad8 c #888180", +"#fS c #88839c", +".Xq c #8883a9", +".rd c #88848a", +".Cf c #888496", +"aKE c #888993", +"#AF c #8889a4", +"#Q3 c #8893b2", +"#5J c #88a0d2", +"aM2 c #88a1cf", +"#1u c #890104", +"#Xh c #890711", +"#aV c #893520", +"ag6 c #894021", +".9T c #894631", +"#15 c #894723", +".mi c #895531", +"aHJ c #895a75", +"aGB c #895c61", +"aso c #896d65", +"ap0 c #897068", +"#XJ c #89736d", +"#bI c #897b71", +"aBE c #898486", +"#q# c #8986a0", +"#kv c #898bb1", +"#JN c #898daa", +"#77 c #8a0414", +"#4I c #8a0505", +"#Us c #8a050c", +"#YV c #8a0d09", +"#72 c #8a1219", +"#iP c #8a2f17", +"#ES c #8a3116", +"#pG c #8a3210", +"#BA c #8a330d", +"#eg c #8a3e2f", +"#t7 c #8a3f20", +".3c c #8a4328", +"#aA c #8a432f", +"#8G c #8a4428", +".1z c #8a4a32", +".4Y c #8a4d3a", +".kE c #8a5536", +"##s c #8a5537", +"#.U c #8a5847", +"#Y9 c #8a5b31", +".2L c #8a5f45", +".07 c #8a5f46", +".2K c #8a614f", +"aGH c #8a625d", +"aGo c #8a6378", +"aHN c #8a6581", +"#MB c #8a705b", +"aKz c #8a7484", +".we c #8a7952", +".h4 c #8a797a", +"#cL c #8a7ea3", +"aJ0 c #8a8288", +"#MD c #8a8894", +"#rz c #8a90b0", +"aBu c #8a9aca", +"#S0 c #8b0711", +"#6n c #8b0c13", +"aI4 c #8b2d3a", +"#ER c #8b3015", +"#E4 c #8b4836", +".Qh c #8b4939", +"#.X c #8b4e3c", +"#CV c #8b592e", +".4q c #8b5f45", +"#B. c #8b616a", +".qX c #8b796c", +"aG0 c #8b8184", +"#b. c #8b83ac", +".33 c #8b849e", +"#wP c #8b8d9b", +"#TW c #8b8dab", +"#4S c #8c0008", +"#2X c #8c0402", +"#YP c #8c0406", +"#0# c #8c0409", +"#Uo c #8c040a", +"aGD c #8c2c36", +"afQ c #8c3f1f", +"#1P c #8c4120", +"#t6 c #8c4222", +"#8H c #8c462c", +".3d c #8c472b", +"#02 c #8c4c32", +"#Ww c #8c4d33", +"aa2 c #8c5554", +"#Bm c #8c5a39", +"#Mz c #8c5d49", +"a#O c #8c8185", +".ij c #8c8193", +".DZ c #8c827f", +"aqo c #8c898f", +"#28 c #8d0005", +"#S5 c #8d040b", +"#75 c #8d0513", +"#1G c #8d0605", +"#1F c #8d1113", +"#EH c #8d3219", +"#EG c #8d341b", +".7N c #8d3d17", +"#Zx c #8d422a", +"#t4 c #8d4324", +".6c c #8d4827", +"##q c #8d482c", +"#UM c #8d491f", +"#Bt c #8d4b31", +".P5 c #8d4f3f", +"#18 c #8d5950", +"#Eq c #8d5b38", +".6z c #8d644c", +"aKw c #8d6e81", +"asY c #8d7167", +"aJb c #8d7586", +"aFY c #8d7d84", +"#N6 c #8d7e77", +".ae c #8d809b", +"#PB c #8d848d", +"#RE c #8e030a", +"#Xk c #8e0408", +"#VR c #8e0812", +"#2U c #8e141a", +"#71 c #8e2428", +"aGw c #8e315a", +"#Zy c #8e4127", +"#TP c #8e4428", +"#WD c #8e451e", +"#5q c #8e4930", +".4X c #8e4d3c", +".7M c #8e4f44", +".7w c #8e5e3a", +".5Y c #8e6965", +"#2r c #8e766c", +"aKA c #8e7889", +"#St c #8e7a7b", +"aF2 c #8e7e85", +".83 c #8e8077", +"aEK c #8e8086", +"#zd c #8e8394", +"aC4 c #8e878a", +"aAq c #8e898b", +"#Q2 c #8e899c", +"a.S c #8ea0c8", +"#6t c #8f020d", +"#VT c #8f0409", +"#YM c #8f050b", +"#6r c #8f060e", +"#21 c #8f0709", +"#Un c #8f0710", +"#0k c #8f0c0a", +"#vI c #8f3311", +"#u. c #8f3e21", +"ag8 c #8f4325", +"a.m c #8f4430", +"a.j c #8f4a2d", +"ac9 c #8f4c29", +"abE c #8f4c2a", +"ajl c #8f5032", +"aGG c #8f514e", +"#Gw c #8f543b", +"#HE c #8f5b3a", +".FN c #8f653a", +".3q c #8f6d61", +"aL9 c #8f7266", +"#Lb c #8f7361", +"#no c #8f879f", +"#1v c #900406", +"#76 c #900515", +"#4R c #900710", +"#EP c #903118", +"#BW c #903416", +"#ss c #903514", +"aGu c #903961", +"#aD c #903b17", +"#4D c #903d3e", +"a#2 c #90482d", +"#UR c #90492a", +"#aS c #904935", +"a#1 c #904c2c", +"aGp c #904d6d", +".4A c #90553f", +"ah2 c #905642", +"#17 c #905a51", +"#CT c #905e3d", +".qs c #906341", +"atx c #90776d", +".Ty c #907b77", +".C8 c #908275", +"aoC c #90858a", +"aJ5 c #90878d", +".AO c #908c9b", +"aIs c #908e90", +"aGE c #91202e", +"#6m c #912c2e", +"#EM c #912d16", +"#kq c #91361e", +"#su c #913a1b", +"afS c #913f20", +"aKp c #914235", +"a#3 c #91442d", +"#t5 c #914829", +"#Wl c #914c34", +"#0K c #914d2e", +".4B c #915640", +".4o c #916451", +".1K c #916b51", +"#.M c #916c56", +".jz c #917c7a", +".gL c #918482", +"aG6 c #91858b", +"ak7 c #918785", +".jj c #918a94", +"aAr c #918c8e", +".mO c #919898", +".il c #919eb6", +"#RF c #92050b", +"#Xi c #92050d", +"#1H c #921108", +"#S7 c #921817", +"#EI c #92341c", +"#F7 c #923913", +"aGs c #924569", +"#fQ c #924632", +"ai. c #924d2f", +"a.i c #924e2e", +".Qg c #925141", +"#Wx c #92533a", +"aeG c #925737", +"#.O c #925f3b", +".2M c #92674c", +".j. c #92682f", +".42 c #926d5d", +"#WL c #92766d", +"avy c #92786f", +".ff c #928289", +"aEM c #92848b", +".ID c #928cb1", +"aw3 c #928f91", +"#VS c #93050d", +"#1y c #930605", +"#2Y c #930606", +"#4L c #93070d", +"#Ut c #930c12", +"#1s c #930e17", +"#Qv c #932705", +"#Qw c #93281a", +"#EJ c #93331c", +"#5t c #934329", +"afR c #934625", +"#8N c #934a2f", +"#0v c #934b20", +"#0L c #934d2f", +"#TS c #935132", +"#8L c #93523e", +".Qf c #935241", +".Qi c #935544", +"#Es c #935947", +"#b1 c #935b3d", +"aep c #936149", +"#Vg c #93766e", +"aul c #937a70", +".7i c #938a92", +"#.h c #938c87", +"axu c #939199", +".B5 c #9391b9", +".oe c #939b94", +"#YO c #940407", +"#Xj c #94040a", +"#6s c #94040f", +"#0c c #940505", +"#6o c #94050f", +"#0a c #940609", +"#4J c #94070b", +"#sF c #943a22", +"#sv c #944122", +".1x c #944a30", +"#6X c #944e32", +"ah8 c #945031", +"aMO c #945067", +".4W c #945140", +".Qj c #945745", +"aCy c #945c76", +"agX c #945f47", +"afY c #946e5a", +"aGA c #94747b", +".0. c #94755e", +".8h c #94807d", +".zW c #948467", +".v1 c #94888b", +".uC c #948a77", +".aw c #948ca8", +"#Ld c #9492a6", +"#YN c #950409", +"#1t c #950507", +"#UT c #95492b", +"#5g c #954f35", +"#B3 c #955132", +"#2l c #95553b", +"#zY c #956442", +"#4X c #956940", +".1L c #957560", +".tq c #958254", +"aHp c #95848c", +"#eF c #958f9e", +"#EO c #96341c", +"#tX c #963918", +"#aX c #964228", +"#gD c #964826", +"afN c #964a29", +"#1O c #964b1e", +"#hm c #964b34", +"#WC c #964d2b", +"#8O c #964d31", +"#8M c #964d33", +".1y c #964e34", +"#zZ c #96603a", +"#Ic c #966147", +".4r c #966950", +".Hm c #966b3e", +".SR c #966f47", +"a#R c #966f61", +"#iT c #967781", +"auY c #967c72", +"#zK c #967e90", +"#lX c #968495", +"#Lc c #968686", +"#zo c #968897", +"aFI c #968a90", +".Hh c #968a95", +"#E7 c #968e85", +"aBJ c #969192", +"af4 c #969296", +"#4F c #970812", +"#S6 c #970c13", +"#O5 c #973227", +"#EN c #97341c", +"#xe c #973e1a", +"#Hj c #97453f", +"#3J c #97462d", +"#2k c #974735", +"#UU c #97492c", +"#0t c #974c24", +"#tK c #974f27", +"adi c #975328", +"a#0 c #975532", +"ac8 c #975630", +"##y c #975b40", +"#Dz c #975d49", +"#b3 c #976041", +".Wp c #97614f", +".nL c #97663e", +"#En c #976644", +".9f c #977450", +"#4z c #978484", +"#oO c #978899", +"aEL c #978990", +".vL c #978c8e", +".dE c #978e95", +"aIt c #979097", +"#Iz c #9791a0", +"#B8 c #9799a3", +"#0b c #980607", +"#1w c #980708", +"aH0 c #983237", +"#y0 c #98371a", +"#At c #98381b", +"#y2 c #983b1d", +"#aW c #98432b", +"abF c #984830", +"#TQ c #984a28", +"#0s c #984d1b", +"ag4 c #984f2f", +"ag7 c #984f30", +"#WB c #985032", +"#0Z c #98503f", +"#WA c #985239", +"#8F c #985435", +".Qe c #985646", +"abD c #985732", +"#ds c #985b4d", +"ajd c #986456", +".5Z c #986a57", +".IL c #987048", +".4k c #98733e", +".jy c #987f7c", +"aBK c #989094", +"aJ1 c #989095", +".2l c #9893b5", +"#2V c #99010c", +"#4K c #99080f", +"aMP c #99354f", +"#sH c #993b25", +"#Ny c #993d36", +"#EE c #994328", +"#L5 c #994940", +"#01 c #994b36", +"#dQ c #994c25", +"#t8 c #994c2d", +"ag3 c #995031", +"#3H c #995037", +"#67 c #995137", +"#3x c #995139", +"#gy c #995348", +".6q c #995c3e", +"#I3 c #99643a", +"#Bo c #996538", +"#cK c #996b75", +"#Q0 c #996c60", +".kT c #998880", +"aHq c #998890", +".fd c #998990", +".xz c #998d94", +"aEm c #998f94", +"#bw c #99909e", +"aJ2 c #999197", +"aIu c #999299", +".Cm c #99969f", +"#1x c #9a0807", +"#2Z c #9a080b", +"#20 c #9a080d", +"#aE c #9a4823", +"#7. c #9a4c30", +"#lc c #9a4d29", +"afO c #9a4d2c", +".Qk c #9a5d4a", +"#.V c #9a5f4e", +"aic c #9a644c", +"#CE c #9a656c", +"#eh c #9a6a6f", +"aac c #9a6f56", +"#JK c #9a7058", +"apd c #9a7e75", +"#Fh c #9a838a", +".r5 c #9a8560", +"aJ9 c #9a8a92", +".xi c #9a8eaa", +"aD# c #9a9196", +"#LJ c #9b3634", +"aKv c #9b385f", +"#fc c #9b3d25", +"#fd c #9b4024", +"ad# c #9b4a31", +"aex c #9b4c2c", +"#at c #9b513f", +".4R c #9b553a", +"#aB c #9b5541", +"#dx c #9b5638", +"#Wu c #9b583a", +"#0J c #9b593a", +".1A c #9b5a42", +".4z c #9b604a", +"#gu c #9b6634", +".xU c #9b866e", +"aIM c #9b8992", +"aLK c #9b8a99", +".kX c #9b8f69", +"#AE c #9b9294", +"aFH c #9b9295", +".8H c #9b9482", +".h1 c #9b949d", +"aBZ c #9b99a3", +".wF c #9b9eae", +"aMW c #9c3941", +"#jM c #9c4926", +"#3K c #9c4931", +"#jL c #9c4b28", +".3b c #9c5238", +".2X c #9c5a44", +"aji c #9c5d3f", +"#k6 c #9c632c", +"#ae c #9c6433", +"##r c #9c6749", +"#5. c #9c684b", +".2D c #9c6c31", +"#FY c #9c6c44", +".Ro c #9c754c", +"#xL c #9c858d", +".k2 c #9c908f", +"aC9 c #9c9398", +"aJ3 c #9c9499", +"aAP c #9c9699", +"ayd c #9c9aa4", +"aMC c #9d2e3f", +"#Ml c #9d4828", +"aGt c #9d4a70", +"a#4 c #9d4b37", +"aew c #9d4f2e", +"#ca c #9d5139", +"#c# c #9d513a", +"adg c #9d532c", +"#Wq c #9d5330", +"#jI c #9d533e", +"#V# c #9d5626", +".2W c #9d5b45", +"#We c #9d612e", +".71 c #9d634d", +".7E c #9d6356", +"#bR c #9d6635", +"ac0 c #9d6c5e", +".9g c #9d704a", +".Yx c #9d745b", +".Ki c #9d764d", +"abQ c #9d7760", +".2E c #9d7845", +".u6 c #9d866a", +"aDN c #9d868f", +".BF c #9d8974", +"aMH c #9d909e", +"#yh c #9d94a4", +"aEy c #9d969b", +"aDW c #9e315e", +"aKu c #9e3b5d", +"#cb c #9e533b", +"#5e c #9e542c", +"ad. c #9e5536", +"#2i c #9e5647", +"#ZA c #9e593b", +"#Wz c #9e5949", +"#bZ c #9e5b42", +"#CX c #9e674c", +"#0w c #9e6942", +".We c #9e6d3c", +".XO c #9e6e3a", +".92 c #9e6e60", +".2N c #9e7156", +"aHK c #9e849c", +".Xn c #9e949c", +"aD. c #9e9599", +"aFU c #9e959b", +"aBL c #9e969a", +"aIF c #9e969c", +"aIv c #9e969d", +"auz c #9e9ca3", +"#2W c #9f0004", +"aE7 c #9f2953", +"aMA c #9f2b33", +"#Gd c #9f3f2c", +"#rh c #9f432c", +"#sr c #9f4532", +"#fb c #9f482b", +"#h9 c #9f4a27", +"#aR c #9f4e38", +"#lk c #9f5125", +"#ch c #9f522b", +"#2j c #9f5242", +"#Y4 c #9f5422", +"#3v c #9f542d", +"aa# c #9f5430", +"afW c #9f5a36", +"#CW c #9f6b46", +".7q c #9f6b4b", +".Ol c #9f6c3a", +".MY c #9f6c3b", +"#Ea c #9f6f81", +"a.x c #9f7059", +"aHO c #9f7492", +".5T c #9f7a44", +".S1 c #9f7b6d", +"aCZ c #9f8375", +"#07 c #9f867c", +"aKy c #9f8798", +"aG4 c #9f9699", +"#OK c #a0363a", +"#F9 c #a04121", +"aeC c #a04925", +"aev c #a05131", +"#bY c #a05338", +"#Wy c #a05b4c", +"a.h c #a05c3b", +"#al c #a05d3f", +"#h0 c #a06730", +".9b c #a06c4c", +"ajo c #a06f5c", +".0Y c #a07038", +"#4W c #a07144", +".p. c #a07148", +"#Ss c #a08179", +".qz c #a08774", +"aJ4 c #a0989e", +"#dj c #a0989f", +".uf c #a09fb8", +"#4G c #a1010e", +"aMB c #a11e31", +"aDO c #a1274c", +"aLS c #a13149", +"#Gc c #a1402d", +"#q5 c #a14725", +"a#7 c #a14b2f", +"#ua c #a14c2f", +"#sD c #a14f35", +"#XA c #a1541a", +"#69 c #a15438", +"#5s c #a1543a", +"#8P c #a1573b", +"adh c #a15b31", +"#16 c #a15d3a", +".2Y c #a15f49", +"#Hw c #a16339", +"#b0 c #a16348", +"#FQ c #a1653c", +"akl c #a1674a", +"#Hv c #a16d46", +"#Sr c #a16f5b", +".Zx c #a1735c", +"aJ. c #a17f94", +"#TU c #a1827a", +"#ku c #a18693", +"aB6 c #a1999d", +".h7 c #a19b95", +"#0l c #a2261c", +"aE8 c #a22a55", +"#EK c #a24029", +"#oj c #a24e2b", +"#3u c #a25430", +"#00 c #a25642", +"#1N c #a25719", +"#e8 c #a25737", +"#0r c #a25815", +"#Hx c #a25c30", +"#a5 c #a25c36", +"#6W c #a25c3f", +"#5f c #a25c41", +"#3# c #a25e4c", +"ac7 c #a26139", +"#14 c #a2613d", +"#ak c #a26646", +"#do c #a26d3b", +".PS c #a26f3d", +"aMN c #a27c8e", +".Us c #a27f56", +"aKB c #a28a9b", +"aJc c #a28a9c", +".8M c #a294aa", +"aMI c #a295a2", +"aG7 c #a2969b", +".2h c #a296a2", +"aj2 c #a29793", +"#wR c #a2989a", +"#If c #a2a1b4", +"aLZ c #a3282d", +"aKs c #a33c50", +"#KV c #a34b2e", +"afT c #a34f2d", +"#oy c #a35030", +"#9G c #a35050", +"a.p c #a35338", +"a.q c #a35438", +"abN c #a35832", +".9L c #a35b42", +"#Ts c #a35e34", +"#dA c #a36540", +"#Ia c #a36747", +"akn c #a3694c", +".LG c #a3703e", +"#CF c #a3788a", +".zS c #a3917e", +".uK c #a39189", +".fc c #a39299", +".zJ c #a39478", +"aDu c #a3979d", +"aIw c #a39ca3", +"aLE c #a41e35", +"aLD c #a42633", +"aFd c #a4416c", +"#pC c #a4432a", +"aKH c #a44655", +"#ET c #a44c30", +"#yH c #a44d28", +"a#8 c #a44f32", +"afU c #a4502a", +"#oz c #a45131", +"#m7 c #a45338", +"aeA c #a45534", +"#du c #a4563c", +"#6U c #a4572f", +"a.n c #a45846", +"#Y3 c #a4591f", +"#3w c #a45d44", +"ah# c #a4603f", +".2Z c #a4634d", +".P6 c #a46556", +".Ql c #a46753", +"#jH c #a46a5c", +"#Ku c #a46c3f", +".7I c #a46c5f", +"#XG c #a46f3c", +"#iS c #a46f64", +".R3 c #a47356", +".08 c #a4765c", +"#fR c #a47776", +".PT c #a47d54", +".Z9 c #a47d60", +".XP c #a48054", +"aKY c #a4867a", +"#JL c #a4867d", +"anf c #a4877d", +"#nn c #a48890", +"#zn c #a49699", +"aG8 c #a4989e", +".IE c #a4a0c7", +".G4 c #a4a2cf", +"aDQ c #a5274e", +"aI9 c #a5497e", +"#Av c #a54d2d", +"#Mm c #a5512e", +"#dG c #a55137", +"#V9 c #a55619", +"afM c #a55838", +"#5r c #a55b42", +"aLR c #a55d73", +"#8E c #a56040", +"abC c #a5633c", +"a#Z c #a5633e", +".Qd c #a56353", +"#XL c #a56850", +"#L. c #a5755d", +".LH c #a57d55", +"#zc c #a59089", +".tM c #a59183", +"aF3 c #a5959c", +"aIE c #a59ca2", +".js c #a5a59d", +".rq c #a5a5b4", +"#Uv c #a64338", +"#pB c #a64729", +"#Aa c #a64c29", +"#Mh c #a64f32", +"abG c #a6503c", +"aeB c #a65232", +"#5u c #a6543b", +"a.o c #a6553b", +"#aY c #a65639", +"#So c #a65836", +"aeF c #a65b34", +"ag2 c #a65d3d", +"#03 c #a66244", +".20 c #a6644e", +"#Br c #a66852", +".7p c #a66d3d", +".Rn c #a67341", +"aGx c #a6738e", +"##z c #a67975", +"#.N c #a67c5a", +".7v c #a67e56", +".w. c #a6906e", +"aHr c #a6959c", +".95 c #a69aab", +"azA c #a69ca8", +"aID c #a69da3", +"#vi c #a69ea2", +"aBQ c #a69fa3", +"aDP c #a72c52", +"aI5 c #a7485f", +"#Nw c #a74b1e", +"#F6 c #a74f29", +"a#6 c #a74f34", +"#Y# c #a75e3a", +"aGr c #a75e80", +"#dB c #a76040", +"#a4 c #a7633c", +"#.W c #a76455", +"#I4 c #a76539", +".6w c #a76b5a", +"#h4 c #a76b5e", +".4C c #a76c56", +"#jD c #a76e37", +".Kh c #a77442", +".Qu c #a77565", +".Ur c #a77747", +"aGy c #a77c92", +".wu c #a78863", +".yg c #a78a69", +"anD c #a79ba1", +"aDa c #a79ea3", +".uU c #a7a088", +"aDR c #a82850", +"aLF c #a83048", +"aKO c #a8312d", +"#Ge c #a84931", +"aKq c #a84a49", +"#xf c #a84d2b", +"#Mi c #a85035", +"#Mk c #a85334", +"#5d c #a85a34", +"#3I c #a85a42", +"#sC c #a85b3f", +"#68 c #a85e43", +"ah7 c #a86345", +".Zp c #a88456", +"#tx c #a89190", +"afp c #a89b96", +"ahX c #a8a5ac", +".h0 c #a8a5b1", +".jv c #a8a8a0", +"aE9 c #a92e5a", +"aMV c #a92f39", +"aKr c #a9434d", +"#EL c #a94730", +"#xz c #a94d2e", +"#KW c #a95133", +"#Mj c #a95137", +"aez c #a95a39", +"#0n c #a95b3b", +"a.t c #a95e3c", +"#KE c #a96252", +"#Zf c #a9664d", +"#z4 c #a9664e", +".2V c #a96751", +"ajk c #a96a4c", +"#Y8 c #a96d33", +"akg c #a96f52", +".7J c #a96f62", +"ako c #a97059", +".Vb c #a97060", +".9a c #a97140", +"a.a c #a9715d", +".9o c #a97365", +".SQ c #a97644", +"#yt c #a97755", +"#h1 c #a9784a", +".Zo c #a97943", +"#s# c #a98161", +"#7Y c #a98178", +"aKx c #a98d9f", +"aHo c #a9989f", +".gO c #a99c9a", +".tu c #a99e86", +"ad7 c #a9a09e", +"aIx c #a9a2a9", +".7j c #a9a2b1", +".ri c #a9a5ab", +"aF. c #aa2b58", +"#ie c #aa4a24", +"#tW c #aa4e2d", +"abJ c #aa4f33", +"#oi c #aa5532", +".7X c #aa5b2b", +"#vT c #aa5d3e", +"aaa c #aa613b", +".Qc c #aa6858", +".ZZ c #aa6c4f", +".IK c #aa7746", +"#hn c #aa7f7b", +".MZ c #aa835a", +"akf c #aa8b81", +".xV c #aa957b", +"aiV c #aa9c97", +".qQ c #aa9d6e", +"#7U c #aa9f94", +"aHf c #aaa1a7", +".y8 c #aaa7af", +".Xr c #aaaacf", +"#uc c #ab4a2d", +"#aO c #ab4e33", +"abI c #ab4e34", +"#y3 c #ab5333", +"#Bz c #ab572f", +"#aQ c #ab5741", +"#1J c #ab5b3b", +"#UV c #ab5c3f", +"#cG c #ab5e33", +"#Y2 c #ab6118", +"a.g c #ab6744", +".6s c #ab6859", +".77 c #ab6959", +"abw c #ab6b55", +".78 c #ab6e5a", +"#e0 c #ab7644", +"#b2 c #ab7757", +".5S c #ab7b3d", +"#a9 c #ab8594", +".Wf c #ab875d", +"aEn c #aba1a6", +"aEl c #aba3a5", +".oD c #aba6bd", +"awA c #aba7aa", +"alC c #aba7ab", +"#G. c #ac4d2f", +"ada c #ac523e", +"##c c #ac5426", +"#kr c #ac583f", +"a.r c #ac5e40", +"#FC c #ac6159", +"#Y6 c #ac632b", +"#Tt c #ac643b", +"#Wt c #ac6647", +"#TO c #ac664e", +"#R3 c #ac6737", +"#OV c #ac6a36", +".Qb c #ac6a5a", +"#sT c #ac6c46", +"#No c #ac6d3b", +"alu c #ac7659", +"#Id c #ac8373", +".0Z c #ac8757", +"asn c #ac8d80", +"aBA c #ac9083", +"aME c #ac96a7", +".qJ c #ac9983", +"#zL c #ac99a8", +".zT c #ac9a85", +"aG9 c #aca0a6", +"aH. c #aca1a6", +"aHh c #aca4aa", +"aus c #aca9ab", +"#KU c #ad523b", +"abK c #ad5537", +"#C6 c #ad5631", +"a#5 c #ad5947", +"aMD c #ad5964", +"adf c #ad5d39", +"#I6 c #ad622a", +"#8Q c #ad6244", +"abz c #ad6431", +"#6V c #ad6439", +".6n c #ad6449", +"#Kz c #ad652f", +"#B4 c #ad6753", +"#LV c #ad7242", +".4y c #ad725c", +"##w c #ad7357", +"#gv c #ad7657", +".XY c #ad7963", +".4j c #ad7c40", +"akp c #ad7c69", +".Z8 c #ad8161", +"#B6 c #ad8266", +"#lW c #ad8584", +"aPL c #ad897b", +"aE6 c #ad8f9d", +"#GZ c #ad949b", +"#r5 c #ad9d95", +"#DB c #ada194", +".vx c #ada5a6", +"aEw c #ada6ac", +"aDh c #ada7ac", +"aoy c #ada9af", +"#Gz c #ada9b4", +".fk c #adaaaa", +"#O3 c #ae470f", +"#gK c #ae4f33", +"#ow c #ae573f", +"#A. c #ae5a32", +"#3t c #ae5b3a", +"#UE c #ae5d24", +"#.7 c #ae5d38", +"#8S c #ae6142", +".1w c #ae6148", +"a.s c #ae6242", +"#8R c #ae6243", +"a.u c #ae6341", +"#dw c #ae6348", +"abO c #ae663e", +"#a6 c #ae6841", +"#7a c #ae6b4b", +"#Sq c #ae6b4d", +"#0I c #ae6e4e", +"aib c #ae7056", +".9p c #ae7163", +".4D c #ae745d", +"#.I c #ae7645", +"alv c #ae7a64", +"#aj c #ae7b5d", +"#Gx c #ae7e6c", +"#6k c #ae7e79", +"#tF c #ae8266", +"#Z# c #aea09e", +"#JM c #aea0a9", +"aFC c #aea4a7", +"aEg c #aea5a8", +"aDb c #aea5aa", +"aIC c #aea5ab", +"aIB c #aea6ac", +"#yj c #aea9af", +"#bz c #aea9cd", +"awG c #aeabb2", +"aJg c #aebdc2", +"aMS c #af1d30", +"aKI c #af2a39", +"aDS c #af2b54", +"aF# c #af2c5a", +"aKt c #af4a66", +"#st c #af5534", +"##f c #af583c", +"#hj c #af6045", +"#WI c #af6641", +"#Wr c #af6643", +"#8D c #af6738", +"a#X c #af6932", +"ah6 c #af6a4b", +"aMX c #af6d71", +"#3N c #af7048", +"#8U c #af7456", +"#9D c #af9084", +"QtC c #af9f9c", +"aHe c #afa6ac", +"aIy c #afa7ae", +"aDi c #afa9ae", +"aFa c #b02959", +"#pF c #b04e3d", +"#v1 c #b05033", +"#Dq c #b05436", +"#70 c #b05c5b", +"#3L c #b06541", +".4Q c #b0674c", +"ac5 c #b06b30", +".4E c #b06c4c", +"#Ve c #b06f50", +"afH c #b07054", +".Qm c #b0735e", +"akk c #b07659", +"#dp c #b0795a", +"alo c #b0795d", +"#4C c #b07977", +"#wc c #b08f90", +"aeI c #b09081", +"#AD c #b0947b", +"aGm c #b095a5", +".z6 c #b09c93", +"#B7 c #b09d8b", +"#AR c #b0a097", +"aHa c #b0a4aa", +"aBP c #b0a9ad", +"aMR c #b1243c", +"aDU c #b12752", +"aGv c #b1567f", +"aeD c #b15830", +"#iO c #b1593d", +"#.6 c #b15e3a", +"#7# c #b16145", +"#L2 c #b16332", +"#ec c #b1633a", +"aa. c #b16341", +"aKP c #b1635e", +"#Nz c #b16737", +"ah. c #b16746", +"#Sn c #b1674a", +"#ip c #b16c49", +"#UQ c #b16d4c", +"a#Y c #b16e48", +".P7 c #b17263", +"#bS c #b17b5c", +".09 c #b18165", +".Rz c #b18a78", +"aOP c #b18d7f", +"aLQ c #b18d9e", +"arG c #b19387", +"ad2 c #b19d93", +".qy c #b19f8d", +"acq c #b1a08c", +".sm c #b1a193", +"ahP c #b1a29b", +"al5 c #b1a2a4", +".vb c #b1a38a", +"#zm c #b1a39f", +".gS c #b1aab3", +".sy c #b1adad", +"avE c #b1aeb0", +"axz c #b1afb6", +".mV c #b1b9c6", +"aDT c #b22a55", +"#RG c #b23836", +"#pE c #b2503e", +"#pD c #b2513b", +"#G# c #b25237", +"#Gb c #b2523c", +"#QW c #b25937", +"#cw c #b25a40", +"#Mg c #b25c3c", +"#h8 c #b25f3d", +".7P c #b2623d", +"#m6 c #b26448", +"#fp c #b2653e", +"#XE c #b2660f", +"#Xz c #b26627", +"#Qm c #b26d39", +"#XO c #b26e58", +"#a3 c #b27048", +"#Gu c #b27156", +"#nW c #b27740", +"als c #b27b5f", +"#yu c #b27c56", +"#FP c #b27f59", +".p# c #b28e6d", +"avx c #b29181", +"#Ba c #b294a3", +"#Bd c #b2a39b", +"aEJ c #b2a5ab", +"aH# c #b2a6ac", +"#98 c #b2a7a9", +"aFJ c #b2a7ac", +"aHg c #b2a9af", +"#wQ c #b2b2ba", +"#lT c #b35a42", +"#xA c #b35b3b", +"#dv c #b35d46", +"#Mn c #b35e39", +"#sw c #b36447", +".9K c #b36546", +"#id c #b3663b", +"#cF c #b3683c", +"#1Q c #b36949", +"#kn c #b36a44", +"#Kv c #b36b3e", +"#Ws c #b36b4a", +"#f# c #b37047", +"#.J c #b37f5f", +".W2 c #b3806b", +"#uq c #b38d8a", +"auX c #b39283", +"aAk c #b39789", +"aqV c #b3978d", +"afm c #b3a097", +"#zM c #b3a5b0", +"#9z c #b3a698", +"aIz c #b3abb2", +"#yi c #b3adb9", +"aFc c #b4295a", +"#e7 c #b45843", +"abH c #b45948", +"add c #b45a3c", +"#KT c #b45a41", +"#gQ c #b46040", +"#ld c #b46440", +"#vP c #b46448", +"a.d c #b4663f", +"#8C c #b4683d", +"a#V c #b46840", +"aby c #b4693f", +"#YY c #b46949", +"ac4 c #b46c37", +"#Px c #b46d54", +"abA c #b46e35", +".Qa c #b47262", +"#Jb c #b47557", +"ajn c #b47a63", +".7H c #b47e6f", +"ams c #b48064", +".1H c #b48969", +"awv c #b49383", +"aJH c #b4968a", +".z5 c #b49c80", +"#vj c #b49c9a", +"aMM c #b4a1b0", +"a#f c #b4a594", +".61 c #b4a5b6", +"agF c #b4a6a0", +"aiW c #b4a7a3", +".gN c #b4a7a5", +"#zN c #b4aaad", +".31 c #b4aabb", +".t0 c #b4afad", +".hZ c #b4b4c2", +"aFb c #b52b5c", +"aMU c #b52f39", +"#fe c #b55b3f", +"#xw c #b55e43", +"#n8 c #b5633c", +"#Tw c #b56640", +"#oM c #b5684d", +"a.e c #b56a3c", +"#V. c #b56d40", +"#U3 c #b5714f", +"abB c #b5734b", +"#13 c #b57752", +"ajh c #b57759", +"#af c #b58060", +"#bV c #b5806f", +"a.w c #b58267", +"#r8 c #b58366", +"#JH c #b58469", +"#kt c #b5857d", +".2O c #b5866b", +"#my c #b58869", +"aH3 c #b59591", +".wC c #b59f97", +".ph c #b5a99f", +"aEo c #b5aaaf", +"aIA c #b5aeb5", +"#pT c #b65c44", +"#KX c #b65e3e", +".9H c #b66330", +"#5c c #b66342", +"#6T c #b66542", +"a#9 c #b66545", +"#UL c #b66625", +"#UD c #b6662d", +"#V8 c #b66829", +"#fM c #b66847", +".7Y c #b66941", +"#Qy c #b66a40", +"#ll c #b66b3f", +"#cc c #b66b53", +".6d c #b67150", +"#z5 c #b67154", +"#Y7 c #b6732e", +".6t c #b67364", +"#C1 c #b67655", +".P8 c #b67768", +"#uo c #b67d4c", +"#Er c #b68167", +"#qJ c #b68468", +"#jE c #b68557", +"#B# c #b693a5", +"#E6 c #b6947e", +"auk c #b69585", +"awY c #b69586", +"azc c #b69a8c", +"aLH c #b69bad", +"#Be c #b6a290", +"agD c #b6a49c", +".pg c #b6aba1", +".y2 c #b6adc3", +"aEx c #b6aeb4", +"aEv c #b6afb4", +".t5 c #b6b2af", +"aHY c #b72a40", +"adc c #b7593d", +"#KY c #b7603d", +"#Tf c #b76330", +"#cv c #b7634a", +"abM c #b76845", +"#mW c #b76846", +"#Np c #b76936", +"#jS c #b76a3f", +"#lC c #b76c50", +"#Xy c #b76d20", +"a.v c #b76d4a", +"a#W c #b76e3d", +"ag1 c #b76e4f", +".ZY c #b77155", +"abP c #b77249", +"#5w c #b7734f", +"aHW c #b7747e", +"ac6 c #b7754c", +".ZH c #b7785c", +".UD c #b77e67", +"#N3 c #b77e6a", +".9q c #b78070", +"#k7 c #b78457", +".Zy c #b7856a", +"#TT c #b78672", +".Yw c #b7886c", +"atw c #b79687", +"asX c #b79689", +"aHG c #b798a9", +"aJe c #b798ac", +"axX c #b79b8e", +"#Ef c #b7a48d", +"aG1 c #b7adb1", +".dG c #b7b0b9", +"auy c #b7b5bc", +".l. c #b7b8bd", +".jH c #b7cce7", +"aMQ c #b83a54", +"#YW c #b84538", +"#jT c #b86339", +"#e6 c #b8634a", +"#mI c #b8643e", +"#mH c #b86540", +"#5b c #b86548", +"#lU c #b8664d", +"#mV c #b86743", +"#cu c #b86950", +"#I5 c #b86c3f", +"#sg c #b86c43", +"#LW c #b86d3c", +"a.f c #b8703e", +"#5v c #b8704d", +".ZX c #b87055", +"#Sm c #b8715a", +".70 c #b8765c", +".1B c #b8765e", +"#Ev c #b8795a", +".Q# c #b8796a", +"#0H c #b87a59", +"#2S c #b87d7d", +"#e1 c #b88162", +"#8V c #b88165", +"#3p c #b88263", +"alt c #b88266", +".Zz c #b88364", +".4s c #b88a71", +"#vr c #b88b70", +"alw c #b88d7d", +"aNZ c #b89385", +"axm c #b89788", +"#G0 c #b8a0a8", +"#G1 c #b8a3ad", +"#CK c #b8a58f", +"aaU c #b8a894", +"aLP c #b8a9b7", +".zI c #b8aa8b", +".zh c #b8acb7", +".py c #b8ad83", +"aDf c #b8b2b7", +"aAJ c #b8b6c0", +".q6 c #b8c3db", +"aMT c #b92435", +"adb c #b95a49", +"#Ki c #b9605d", +"ade c #b96443", +"#tZ c #b9654a", +"#4V c #b96a5f", +"#Sp c #b96c49", +"#sx c #b96d51", +".3a c #b96d54", +"#Tu c #b96f47", +".Z0 c #b97a5d", +".P9 c #b97a6b", +"#T# c #b97d60", +".W1 c #b9826d", +".Qt c #b98371", +"#w1 c #b98766", +"#k9 c #b98d7a", +".j# c #b99872", +"#yl c #b9a399", +".r6 c #b9a584", +"#Ci c #b9a796", +"#CJ c #b9a99e", +".af c #b9aab3", +"#zO c #b9aca7", +"#Za c #b9acaa", +".v6 c #b9aeb0", +"#KS c #ba6045", +"#aP c #ba6148", +"#LX c #ba6432", +"#RQ c #ba6435", +"#.5 c #ba6743", +"#gC c #ba6749", +"#Kw c #ba6839", +"#gV c #ba6843", +"#OW c #ba6934", +"#pu c #ba6a43", +"#n7 c #ba6a44", +"#nl c #ba6a51", +"aeu c #ba6b4a", +"#qT c #ba6d44", +"afK c #ba6d4d", +"#8T c #ba6d4e", +"#ls c #ba6e4b", +"agY c #ba7158", +"#un c #ba723f", +"aab c #ba734c", +"#l# c #ba755e", +".1f c #ba775f", +"#Dw c #ba794b", +".ZI c #ba7a5e", +".Q. c #ba7b6b", +".Qn c #ba7d66", +".Qr c #ba7e6a", +".4x c #ba7f69", +"aln c #ba8467", +"alr c #ba8468", +"amA c #ba9385", +".qt c #ba9778", +"#7X c #ba9d94", +"a#i c #baa695", +".Cq c #baaeb5", +"#yk c #baafad", +"aEs c #bab0b4", +"aHd c #bab1b7", +"aBM c #bab2b6", +".p5 c #bab8d8", +".sP c #bab9d4", +"aLY c #bb1c26", +"#Ga c #bb5b43", +"#ri c #bb5e48", +"#F5 c #bb653f", +"#Th c #bb681e", +"#ND c #bb693b", +".9J c #bb6943", +"#UG c #bb6b1e", +"#p8 c #bb6b4f", +"aet c #bb6c4b", +"#fa c #bb6f4c", +"#tL c #bb6f4e", +"#V7 c #bb7024", +"#L6 c #bb723e", +"#Gm c #bb723f", +"#R4 c #bb7344", +".Yl c #bb7b5e", +".Qs c #bb816e", +".1. c #bb8769", +"#ai c #bb8f7b", +"aHM c #bb9ab5", +"##A c #bba1b2", +"a#j c #bba493", +"#6h c #bba7a4", +"aIJ c #bba9b2", +"aaY c #bbaa97", +"aDg c #bbb5ba", +".yq c #bbb5bc", +"aHS c #bbb5c2", +"aLT c #bc2641", +"aDV c #bc305c", +"#cx c #bc6246", +"#ox c #bc634b", +"aeE c #bc663b", +"#Da c #bc674d", +"afI c #bc684b", +"#xo c #bc684d", +"#ov c #bc684f", +"afV c #bc6c45", +"#aZ c #bc6d4e", +"#UC c #bc6e2b", +"#qU c #bc6e45", +"#mE c #bc744d", +"#Mx c #bc7454", +"#N2 c #bc7458", +"#QY c #bc755d", +"#sR c #bc7a50", +"#a1 c #bc7c53", +".ZG c #bc7c61", +".Qq c #bc7e6a", +"akm c #bc8265", +"#My c #bc836a", +"#E5 c #bc8371", +"#tC c #bc8a5d", +".93 c #bc8b7d", +".1J c #bc9274", +"aoq c #bc9e93", +"a#g c #bca493", +"afn c #bcaca5", +"ad6 c #bcb0ad", +"aHc c #bcb3b9", +"aBN c #bcb5b9", +".ik c #bcc6da", +"#mN c #bd6e44", +"#gH c #bd6e48", +"#pw c #bd7045", +"##. c #bd704a", +"#U4 c #bd705e", +"#XC c #bd732f", +"#U1 c #bd7552", +"ac2 c #bd7555", +"#gE c #bd764e", +"#U2 c #bd7755", +".2U c #bd7b65", +"#Zd c #bd8065", +"##x c #bd8166", +"#12 c #bd825c", +"#QZ c #bd8574", +".1G c #bd8e70", +"aIg c #bd998a", +"aM5 c #bd998b", +"#6j c #bd9d98", +"aIh c #bd9f93", +"#Gy c #bda19c", +".uX c #bda594", +"#9C c #bda597", +"#zP c #bda89a", +"#Bb c #bda8b2", +"#0D c #bdb0aa", +"aEq c #bdb3b8", +"aDd c #bdb5b9", +"aEu c #bdb5bb", +"#lf c #be6743", +"#C5 c #be6a42", +".9z c #be6d48", +"aL0 c #be6e6f", +"#sh c #be7147", +"afL c #be7150", +"#cE c #be7347", +"#0u c #be744d", +".4P c #be745a", +"#TN c #be7843", +"#gG c #be794d", +"#p4 c #be7950", +"ah5 c #be7a5b", +"#Uy c #be7e61", +"#Ze c #be7e63", +".4u c #be8671", +"#Py c #be8674", +"#sa c #be895c", +".Yv c #be8a6e", +"#vo c #be8d60", +"#I# c #be8d6f", +".Z7 c #be8f6e", +"#qM c #be9374", +"aad c #be9784", +"aBz c #be9a86", +"aAj c #be9a87", +"#DP c #beac95", +"aLL c #beacbb", +"aaX c #bead99", +"aHb c #beb2b7", +"aDc c #beb5b9", +"aFT c #beb6bc", +".DP c #bebcd9", +"aKN c #bf1f1f", +"aes c #bf5b3a", +"aLz c #bf614a", +"#Qo c #bf662d", +".9I c #bf6b3e", +"abL c #bf6c4b", +"#Qn c #bf6e37", +"#tN c #bf6f4a", +"#pv c #bf7047", +"#8B c #bf704b", +"#8A c #bf7051", +"#O8 c #bf7145", +"#Qz c #bf7148", +"#eb c #bf7249", +"#W# c #bf7327", +"#ct c #bf735a", +"#XB c #bf7436", +"aeq c #bf785b", +"#R1 c #bf7e4c", +"ajg c #bf8062", +"#7b c #bf8160", +".Qo c #bf836b", +"#RK c #bf8468", +".Hl c #bf8553", +"#tG c #bf8d6c", +"#vs c #bf8e6c", +"#pk c #bf8e71", +"#Eb c #bf97a5", +"asW c #bf9988", +"aJG c #bf9a8c", +"aCY c #bf9b88", +"apZ c #bfa196", +"#FH c #bfa89d", +"#Ie c #bfa8a7", +"#FF c #bfaaa4", +".x0 c #bfac86", +"aEF c #bfb1b7", +"QtF c #bfb2ba", +"aEr c #bfb5ba", +".6W c #bfb89d", +".0y c #bfb8c9", +".oj c #bfd1f9", +"#Wb c #c06701", +"#cr c #c0684f", +"#cm c #c06942", +"#RS c #c06b24", +"#W. c #c0722e", +"#tM c #c07251", +"#O7 c #c07447", +"#qV c #c07449", +"#sk c #c07548", +"ag0 c #c07757", +"#04 c #c07955", +"#e9 c #c07c56", +"#rs c #c07d53", +"#Bu c #c07e5b", +"#WJ c #c08060", +"#5x c #c08560", +"#FZ c #c08952", +"alq c #c08a6e", +"aCX c #c0967d", +"aEc c #c0967e", +"aKX c #c09c8e", +"#Ee c #c0afa3", +".zG c #c0b091", +".AS c #c0b4bb", +"aDp c #c0b5ba", +".o. c #c0b78f", +"aBO c #c0b9bd", +"#vJ c #c16443", +"#Dc c #c1684e", +"#vY c #c16b4f", +"#xB c #c16d4c", +"#F4 c #c16e46", +"#vz c #c1724d", +"#aG c #c1734d", +"#O6 c #c17649", +"a#U c #c17657", +"#lQ c #c17a54", +"#Tr c #c17f54", +"#oI c #c18057", +"#XN c #c18069", +".Yn c #c18362", +".Qp c #c1856c", +".FM c #c18858", +".6x c #c18c78", +".Tx c #c1936c", +"#Bl c #c19479", +"aBy c #c1977f", +".nM c #c19a7d", +"azb c #c19d89", +"aFy c #c19d8a", +"QtE c #c1a387", +"#CH c #c1a6ae", +"#Hn c #c1a89c", +".xX c #c1ad8f", +"#Hm c #c1ada5", +"acu c #c1b4a0", +".v0 c #c1b5b8", +".zg c #c1b5bb", +"aEp c #c1b7bc", +"aFS c #c1b9be", +".84 c #c1b9c0", +".Cc c #c1bdd4", +"#Nq c #c26935", +"#Aw c #c26e4c", +"#oA c #c2704f", +"#ih c #c2714e", +"#6S c #c27153", +"#o. c #c2754a", +"#lP c #c2754e", +"a.c c #c27557", +"#5a c #c27559", +"##a c #c2764f", +"#nm c #c27a5f", +"#2m c #c27f60", +".1g c #c27f67", +".6v c #c28071", +"#dy c #c28361", +".7L c #c28478", +".7K c #c28579", +"#0G c #c28866", +".6r c #c28868", +"#K9 c #c2886c", +"#FD c #c28880", +"#11 c #c28a63", +"#Nl c #c28c4f", +"#7c c #c28c6c", +".Z6 c #c29271", +"#w0 c #c2967a", +".1I c #c29777", +".mj c #c29983", +"av3 c #c29a85", +"aM4 c #c29a8b", +"#2R c #c29d9d", +"aqT c #c29e8e", +"#Ec c #c2a4ac", +"#IU c #c2aaa2", +"#r6 c #c2ab9c", +"#7V c #c2aca5", +".By c #c2ae99", +"#FG c #c2afa8", +"#CI c #c2b0af", +"aII c #c2b0b9", +"ad3 c #c2b1aa", +"anC c #c2b5bb", +"aEt c #c2b7bc", +".D2 c #c2b7bd", +"aDe c #c2b9bd", +".5A c #c2b9cb", +"aBF c #c2bcbe", +"#EU c #c36245", +"#gM c #c3674a", +"#KR c #c36a4c", +"#xd c #c36c46", +"#Db c #c36d52", +"#oh c #c36e59", +"#3s c #c36f52", +"aH1 c #c36f70", +"#Te c #c37040", +"#n9 c #c3734a", +"#3r c #c37559", +"#Y5 c #c37946", +"#ng c #c37951", +"#U8 c #c37956", +"#ZB c #c37c56", +".1e c #c38068", +".ZF c #c38367", +"#Yb c #c38565", +"#sS c #c3875d", +".Va c #c38778", +".XZ c #c38c70", +"#wX c #c39164", +"#zX c #c3967b", +"#CS c #c3977b", +"#ZD c #c39781", +".kF c #c39989", +"awX c #c39b86", +"avw c #c39b87", +"aL8 c #c39b8b", +"axl c #c39c87", +"aNY c #c39c8c", +"axW c #c39f8c", +"#Hl c #c3aba5", +"#4A c #c3abaa", +"#7W c #c3aca4", +"ahQ c #c3b5af", +"ayl c #c3b7c4", +".VM c #c3b8c5", +"aEh c #c3bbbe", +"azw c #c3c2cb", +"aHX c #c45b6b", +"#gJ c #c4624a", +"#h# c #c46942", +"#OX c #c46b34", +"#cy c #c46c4d", +"#R7 c #c47548", +"#xp c #c47556", +".ZW c #c47960", +"#R5 c #c47a4c", +"#fu c #c47b56", +"#yA c #c47b5c", +"#6l c #c47b79", +"#U9 c #c47c53", +"#2n c #c47d59", +".6u c #c48071", +"#Wk c #c48168", +"a#T c #c4826b", +".Ym c #c48867", +"#8y c #c48871", +"#2p c #c48968", +"#HF c #c48a5d", +"akj c #c48a6d", +".Wq c #c48b72", +".D7 c #c48c5f", +"#qN c #c48c61", +"#Ej c #c49366", +"#5y c #c49574", +"aAi c #c49a82", +".S2 c #c49b8a", +"arE c #c49e8c", +".94 c #c4a6a3", +"#IV c #c4a99c", +"#wS c #c4ada7", +"#Ed c #c4afae", +"aF1 c #c4b4bb", +"#.i c #c4bcc1", +".8T c #c4bdaf", +".oz c #c4c0d6", +"QtY c #c4c2cf", +"#gI c #c56b4d", +"#KK c #c57141", +"#N1 c #c5714e", +"#D# c #c57156", +"#pt c #c57850", +"#Tv c #c57851", +"ac3 c #c57a4f", +"#sB c #c57b5e", +"#lV c #c57b61", +"#oH c #c57e55", +"#Xu c #c57e5f", +"ah3 c #c57f6c", +"#g2 c #c5825c", +".4w c #c58372", +"#6Q c #c5876d", +".4v c #c58774", +"#LS c #c59057", +".Cv c #c59068", +".R2 c #c59274", +"#CP c #c59466", +"#3O c #c5946f", +"#AC c #c59472", +"#nY c #c59679", +"aKW c #c59d8d", +"aOO c #c59e8e", +"acx c #c59e98", +".eZ c #c5a191", +"#4B c #c5a29f", +"#CM c #c5a884", +"aJa c #c5aabc", +".z4 c #c5ac90", +"acs c #c5b09b", +".Bz c #c5b19d", +".ws c #c5b28c", +"#Bc c #c5b6b7", +"aFK c #c5b9bf", +"aFO c #c5babf", +".vr c #c5bed3", +".xc c #c5bed6", +".kK c #c5c4d4", +"#Tj c #c66002", +"#q4 c #c66554", +"#Nd c #c6686a", +"#jN c #c66f4c", +"#gP c #c67051", +"#Mo c #c6734b", +"#cA c #c6734f", +"#QA c #c6754c", +"#lj c #c6794e", +"#hi c #c6795e", +"#w8 c #c67a58", +"#ii c #c67c5b", +"#sy c #c67d63", +"#ij c #c68363", +".Yk c #c68367", +".1C c #c6846c", +"#Wi c #c6866c", +"ajf c #c68769", +"#rt c #c6895f", +".2Q c #c68e74", +"#OS c #c68f50", +"#10 c #c69068", +"alp c #c69074", +"#FM c #c69164", +".Yu c #c69175", +"#Hs c #c69263", +"aHQ c #c693b3", +"#yq c #c69467", +"#Bi c #c69567", +"#Gt c #c69576", +"#ys c #c6997e", +"#Yc c #c69984", +"aza c #c69c84", +"auW c #c69e89", +"asl c #c69e8b", +"ajc c #c69e8f", +"asm c #c6a292", +"apY c #c6a496", +"#Kl c #c6a79f", +"#Eg c #c6ae8e", +"ad4 c #c6b6af", +".FH c #c6bbc1", +".pY c #c6c2d0", +"aye c #c6c4ce", +"#Di c #c7674e", +"#vH c #c76d4a", +"#ha c #c76e44", +"#jO c #c76e4a", +"#ig c #c76f4b", +"#BX c #c76f4f", +"#KZ c #c7704a", +"#iQ c #c7715a", +"#Tn c #c7723e", +"#QV c #c77250", +"#oF c #c77750", +"#oG c #c77751", +"#K4 c #c7794d", +"#cH c #c7794f", +"#si c #c77b50", +"### c #c77b54", +"#px c #c77c50", +"#U7 c #c77c5e", +"#sf c #c77d55", +"#K8 c #c77e5b", +"#2o c #c7825c", +"#E2 c #c78558", +"#XF c #c7863f", +".ZE c #c7886c", +"#B5 c #c78973", +"akh c #c78d70", +".V# c #c78f68", +".79 c #c79078", +".AX c #c79471", +".2P c #c79478", +".4t c #c7947d", +"#zU c #c79568", +".Z5 c #c79674", +"#WK c #c79984", +"axk c #c79a81", +"#Kt c #c79b6e", +"#Em c #c79b7f", +"axV c #c79d85", +"aHI c #c79eb6", +"aPK c #c79f8f", +".aq c #c7a585", +"apb c #c7a698", +"#zQ c #c7a893", +"aaV c #c7ae9b", +".wo c #c7b396", +".yp c #c7b9b8", +"#0C c #c7bcbc", +"aB0 c #c7bec3", +"aFR c #c7bec4", +".DO c #c7c4e5", +"aLU c #c81831", +"#Tm c #c8681e", +"#pA c #c86947", +"#Gf c #c86a51", +"#g9 c #c86f4c", +"#EB c #c8714c", +"#cd c #c8714e", +"#gR c #c87554", +".9y c #c87651", +"#NU c #c8774b", +"#NT c #c8774d", +"#p2 c #c87851", +"#Tx c #c87852", +".9A c #c87853", +"#H5 c #c87946", +"#JB c #c87949", +"#QU c #c87b5a", +".3# c #c87d63", +"#KF c #c87f4c", +"#nh c #c8845d", +"#Az c #c88464", +"#a2 c #c8865e", +"#v7 c #c88a5f", +"#p5 c #c88a60", +"#XM c #c88a72", +".2R c #c88b71", +".Z4 c #c89272", +"#w2 c #c8936c", +"#I0 c #c89462", +"#3c c #c89a6a", +"#up c #c89b81", +"#I2 c #c89d73", +"#06 c #c89e87", +"asV c #c89f8b", +"auj c #c8a08c", +"aJF c #c8a090", +".bW c #c8a48e", +".hR c #c8a686", +"#xK c #c8a898", +"#Km c #c8a89a", +"#9A c #c8b1a5", +".v9 c #c8b293", +"ak# c #c8b8c5", +"aFQ c #c8c0c6", +"##Y c #c8c2da", +".ld c #c8d8e2", +"#Xs c #c96253", +"#ED c #c96c4b", +"#ob c #c96f48", +"aer c #c96f50", +"#LZ c #c97024", +"#HN c #c97048", +"#3. c #c97166", +"#Mf c #c97350", +"#kp c #c97357", +"#mD c #c97547", +"#yG c #c9754d", +"#mJ c #c9774f", +"#UF c #c97834", +"#aF c #c97853", +"#Td c #c9793f", +".7W c #c97a44", +"#NB c #c97c4c", +"#w9 c #c97c52", +"#Jc c #c97f4e", +"#sW c #c98265", +"#QT c #c98364", +"#tJ c #c98553", +".RO c #c98569", +"#ix c #c98666", +"#V3 c #c98668", +"#R2 c #c98755", +"#ry c #c9896b", +".ZD c #c9896d", +"#y8 c #c98a5e", +"#JG c #c98e6e", +".W0 c #c9907b", +".ZA c #c9916e", +"#Bp c #c9926c", +"#Kr c #c9955f", +"atu c #c99c83", +"#CG c #c9a4b3", +".FI c #c9bdc8", +"#8u c #c9bfbf", +"aah c #c9c2c7", +".Cg c #c9c3df", +"aw. c #c9c5c8", +".rp c #c9c8de", +".tR c #c9d4e7", +"aKM c #ca1115", +"#gL c #ca6c50", +"#if c #ca6d48", +"#S8 c #ca6e61", +"#BB c #ca704d", +"##b c #ca7342", +"#lg c #ca744f", +"#Jh c #ca7548", +"#nf c #ca7752", +"#QX c #ca7758", +"#ef c #ca7952", +"#NC c #ca7b4c", +"#um c #ca7b60", +"#o# c #ca7f53", +"#qW c #ca8054", +"abx c #ca8162", +"#cB c #ca8255", +"#3M c #ca835d", +"#q. c #ca886a", +".Yq c #ca8969", +".9x c #ca8a7d", +"#Hk c #ca8a83", +"aje c #ca8b6d", +".ZJ c #ca8b6f", +"#Zc c #ca8f73", +".WZ c #ca936d", +".Yt c #ca9571", +"amt c #ca9579", +"#Qg c #ca9a7c", +"#Vf c #ca9a85", +"awW c #ca9d84", +"atv c #caa28d", +"aIf c #caa292", +"#zb c #caa483", +"apc c #caab9e", +"#9B c #cab3a6", +"aHn c #cab9c0", +"aF4 c #cabac1", +".ig c #cabcc3", +".dC c #cabfc5", +".YY c #cac2d5", +"aux c #cac7ce", +"#O2 c #cb6211", +"#BV c #cb6b4e", +"#v2 c #cb6e50", +"##e c #cb714e", +"#i. c #cb7350", +"#HL c #cb754c", +"#EA c #cb774f", +"#NV c #cb7b4c", +"#UW c #cb7b50", +"#Ky c #cb7c3d", +"#vD c #cb7c50", +"#tQ c #cb7d51", +"#KG c #cb804d", +"#qS c #cb8058", +"#IR c #cb807b", +"#L7 c #cb814e", +"#he c #cb8265", +"#cC c #cb8356", +"#sz c #cb8369", +"#05 c #cb845f", +"#tI c #cb8850", +"#gF c #cb885c", +".21 c #cb8968", +"#9F c #cb8a86", +"#B2 c #cb8b5e", +"#a0 c #cb8b62", +"#dz c #cb906b", +"#wa c #cb9567", +".zl c #cb9a7b", +".Tw c #cb9b74", +".hL c #cb9d7c", +"aEb c #cb9d82", +"aui c #cb9e85", +".kx c #cba085", +".nE c #cba183", +".o2 c #cba281", +"#IX c #cba68e", +".du c #cba797", +"#LN c #cba798", +"apX c #cba899", +"#ym c #cbaa99", +"#Hp c #cbac94", +"#FJ c #cbb098", +"aLI c #cbb3c4", +"act c #cbbca6", +".AE c #cbc6d2", +"aq3 c #cbc7cd", +"aut c #cbc8ca", +"aij c #cbc8cc", +"#Qu c #cc5e1e", +"aLG c #cc677c", +"#Nv c #cc6f2c", +"#BC c #cc704e", +"#oc c #cc724f", +"#Pv c #cc7450", +"#gA c #cc745c", +"#ib c #cc7750", +"#xu c #cc775a", +"#NK c #cc7954", +".9B c #cc7c57", +"#vA c #cc7d50", +"#vU c #cc7e5f", +"#L8 c #cc7f4d", +"#y6 c #cc7f5b", +"#fL c #cc805e", +"#hh c #cc8064", +"#Qx c #cc8156", +"#UZ c #cc8159", +"#sA c #cc8366", +"#Ya c #cc845f", +"#oN c #cc876b", +".4F c #cc8867", +".Yo c #cc8b6b", +".2S c #cc8d73", +"#uk c #cc8e63", +"#Gs c #cc916d", +"#8W c #cc9d85", +".eQ c #cc9f7d", +"aFx c #cc9f84", +".S3 c #cca18e", +".rH c #cca37d", +".qk c #cca37f", +".s4 c #cca47c", +"QtM c #ccab85", +"#IW c #ccab98", +"#IT c #ccaca7", +"#Ho c #ccb09d", +".qI c #ccb698", +"#qG c #ccb7aa", +"ad5 c #ccbeb8", +".AR c #ccc1c3", +".FG c #ccc1c4", +"aFF c #ccc3c6", +"aC5 c #ccc5c7", +".Ft c #cccaf2", +"aKL c #cd0a13", +"aKJ c #cd2935", +"aJm c #cd2e2a", +"#Qp c #cd6820", +"#RT c #cd6917", +"afJ c #cd6e50", +"#fl c #cd7250", +"#KP c #cd7452", +"#KQ c #cd7454", +"#ff c #cd7458", +"agZ c #cd775e", +"#cz c #cd7855", +"#xn c #cd795e", +"#O9 c #cd7c51", +"#fy c #cd7d5a", +"#dK c #cd7e62", +"#mK c #cd7f54", +"#U5 c #cd806c", +"#RN c #cd813e", +"#sj c #cd8256", +"#U0 c #cd845e", +".4O c #cd866b", +"#Uz c #cd8823", +"#3q c #cd8a6d", +".1h c #cd8a72", +".2T c #cd8b75", +"#Ay c #cd8d60", +"#C2 c #cd8d67", +".Yr c #cd906f", +".Ys c #cd9673", +"#OP c #cd9c7f", +".ah c #cda07e", +"aCW c #cda085", +"awu c #cda087", +"#Ko c #cda28a", +".ut c #cda47b", +"#yn c #cda490", +"#LM c #cda49d", +"#ty c #cda59f", +".gs c #cda993", +"#FE c #cdaaa3", +"aop c #cdaea1", +".wx c #cdaf8a", +"a#h c #cdb7a6", +"aaZ c #cdb7a7", +".BA c #cdbaa5", +"aMF c #cdbac9", +"#Ch c #cdbca1", +".qY c #cdbdb2", +".sc c #cdbe8c", +"aEG c #cdbfc5", +"aoL c #cdcace", +".sQ c #cdcce8", +"aLX c #ce1c28", +"aJo c #ce1f1a", +"#OZ c #ce6915", +"#fE c #ce744c", +"#y4 c #ce7a59", +"#ne c #ce7f57", +"#QF c #ce8053", +"#p3 c #ce8059", +"#iN c #ce805f", +"#B1 c #ce825e", +"#Tc c #ce833a", +"#UB c #ce8431", +"#V6 c #ce852a", +"a.b c #ce8871", +"#Qj c #ce9655", +".1# c #ce9676", +"#Qe c #ce9b8c", +"#ON c #ce9e8f", +"#LU c #ce9f6f", +"axU c #cea085", +"aGU c #cea186", +"auV c #cea188", +"#Ng c #cea394", +"#vk c #cea79f", +"aqU c #ceada0", +"#Eh c #ceb28d", +"#6i c #ceb6b2", +".uY c #ceb995", +"afo c #cebfb8", +".uS c #cec29c", +"aFP c #cec2c8", +".xA c #cec2cc", +"aFN c #cec3c8", +"#bv c #cec4c9", +".pU c #cec9d7", +".lJ c #cec9ed", +".aC c #cecbd7", +".ua c #cecdd6", +".pJ c #cedaf8", +"aLV c #cf0f26", +"#OY c #cf6a1f", +"#Ns c #cf6f1d", +"#cq c #cf745a", +"#Dd c #cf745b", +"#i# c #cf7552", +"#KO c #cf7651", +"#C7 c #cf7652", +"aMw c #cf7662", +"#M# c #cf7a4a", +"#cs c #cf7a61", +"#jU c #cf7c53", +"#NJ c #cf7c55", +"#oL c #cf7c61", +"#dH c #cf7c62", +"#XD c #cf7e1e", +"#yF c #cf7e54", +"#NS c #cf7e57", +"#.8 c #cf7f59", +".9F c #cf825b", +"#mL c #cf8358", +"#Pt c #cf8360", +"#rr c #cf845c", +"#iM c #cf8560", +"#j3 c #cf8561", +"#yB c #cf865e", +"#hf c #cf8669", +"#Wd c #cf873f", +".6m c #cf886d", +"#z7 c #cf895e", +".RA c #cf8d77", +"#xE c #cf9165", +"#RI c #cf9178", +"#ZC c #cf9372", +".Z3 c #cf9476", +".1E c #cf967b", +"#Ni c #cf9f83", +"#LP c #cfa087", +".bN c #cfa180", +".i3 c #cfa280", +"ask c #cfa691", +"#wb c #cfa791", +"#zR c #cfaa90", +"#Kn c #cfaa96", +"arF c #cfac9d", +"#FI c #cfb5a2", +"acr c #cfb6a1", +"aaW c #cfbaa6", +".xZ c #cfbc98", +".uQ c #cfbe8d", +"aEk c #cfc7c9", +".k1 c #cfcbbc", +"axv c #cfccce", +"QtR c #cfcde9", +"aKK c #d01721", +"#Nt c #d0711c", +"#ud c #d07154", +"#Gl c #d0744d", +"#Jw c #d07653", +"#dZ c #d0785a", +"#RR c #d07b3f", +"#qX c #d07b51", +"#Tg c #d07c3d", +"#hb c #d07c4d", +"#Mr c #d07d50", +"#NH c #d07d53", +"#u# c #d07d60", +"#Wc c #d07e24", +"#fo c #d07e5a", +"#vW c #d07e61", +"#D. c #d07e62", +"#tO c #d07f51", +"#TB c #d07f52", +"#xr c #d07f61", +"#ic c #d08056", +"#xC c #d0805e", +"#UY c #d08259", +"#ul c #d08a6a", +".1d c #d08c74", +"#vv c #d08d5c", +"aJr c #d08d80", +".RZ c #d09270", +"#Ql c #d09561", +"aH2 c #d09695", +"#0F c #d09775", +"#z0 c #d09969", +"#RJ c #d0997c", +"#n1 c #d0997e", +"#pn c #d0a083", +"avv c #d0a38a", +"arD c #d0a895", +".my c #d0bc8f", +".vq c #d0c8dd", +".kZ c #d0c9b2", +"auv c #d0cdd0", +".um c #d0d7f5", +"#RY c #d17138", +"#UJ c #d17211", +"#d6 c #d17654", +"#Dl c #d1775c", +"#Qc c #d1777b", +"#Jx c #d17852", +"#pH c #d17b59", +"#le c #d17d59", +"#v4 c #d17d5c", +"#Pw c #d17d5d", +"#M. c #d17f4f", +"#mM c #d18055", +"#Gn c #d1814d", +"#ed c #d18159", +"#y5 c #d1815e", +"#km c #d1825d", +"#R6 c #d18456", +"#rv c #d18666", +"#hg c #d1866a", +"#U6 c #d1866d", +"#sQ c #d1875f", +"#TM c #d18a58", +"#iF c #d18a64", +"#V4 c #d18c1f", +".RM c #d18d71", +".Z1 c #d18f73", +"#sX c #d19274", +"#po c #d1956b", +".xE c #d1a085", +"az# c #d1a489", +"#wU c #d1a495", +"#Hu c #d1a57e", +"#DA c #d1ab93", +"#wT c #d1aca0", +"#Bf c #d1b59b", +"aHm c #d1c0c8", +".b4 c #d1c3c6", +"aFM c #d1c6cb", +".Cn c #d1c7c4", +"#c7 c #d1c9d3", +".Ch c #d1c9f1", +".Au c #d1cdef", +"auw c #d1cfd5", +"aHZ c #d2304a", +"#EV c #d27255", +"#Nu c #d27423", +"#LY c #d27628", +"##d c #d2784f", +"#rg c #d27961", +"#V1 c #d27966", +"#HM c #d27a51", +"#v3 c #d27a5a", +"#Mw c #d27d57", +"#rf c #d27d64", +"#NI c #d27f56", +"#fk c #d28062", +"#TG c #d2806b", +"#L1 c #d28143", +"aMz c #d28177", +"#R8 c #d28255", +"#UX c #d28257", +".7Q c #d2825d", +"#xa c #d28357", +"#.9 c #d2845e", +"#NA c #d28757", +"#HG c #d2875a", +"#V2 c #d2886e", +".RN c #d28d72", +"#S9 c #d28f77", +"#Wj c #d29177", +"#Ew c #d2936e", +".ZC c #d29870", +".UE c #d29879", +"#mx c #d29d71", +".7G c #d29e8f", +"#UO c #d2a098", +"#Qf c #d2a38a", +"aBx c #d2a58a", +"aqS c #d2ad9b", +"#3P c #d2b096", +"#Bg c #d2b192", +"a#k c #d2b1a4", +".yj c #d2b594", +".v8 c #d2bb9e", +".ty c #d2bbab", +".64 c #d2c0bd", +"aFZ c #d2c2c9", +"##U c #d2c6c6", +"#1X c #d2c6c7", +"au8 c #d2ced1", +"aLW c #d30c20", +"#Tk c #d36b0c", +"#O0 c #d36b0f", +"#UI c #d3710a", +"#Dj c #d3745b", +"#sI c #d3765a", +"#h. c #d37853", +"#Dm c #d37a5f", +"#NG c #d37b4f", +"#m8 c #d37f66", +"#TA c #d38051", +"#xt c #d38063", +".9G c #d3814a", +"#NR c #d3815d", +"#NQ c #d3815e", +"#re c #d38267", +"#vV c #d38365", +"#tR c #d38459", +"#dL c #d38468", +"#cI c #d3855a", +"#lN c #d3855d", +"#vy c #d38667", +".1v c #d3866d", +"#Dv c #d38763", +".Yp c #d39071", +"#qP c #d3986e", +"#I. c #d39876", +".R0 c #d39978", +"#nX c #d39c70", +"#vt c #d39d77", +"#Nf c #d39f99", +"#IS c #d3a19c", +"aAh c #d3a68b", +"adj c #d3b09b", +"a.. c #d3b1a0", +".tN c #d3bfb2", +".x5 c #d3c0ac", +".ax c #d3c0c0", +"#XK c #d3c1c0", +".uR c #d3c397", +".6Y c #d3c7c9", +"#6M c #d3c9c7", +".e7 c #d3cdd6", +"axy c #d3d0d7", +"#RX c #d46a1d", +"#RU c #d46c15", +"#C8 c #d47957", +"#Jj c #d47b50", +"#gO c #d47d5e", +"#QC c #d47e57", +"#NZ c #d47f58", +"#BY c #d4805f", +"#Mq c #d48154", +"#P. c #d48157", +"#xb c #d48359", +"#uh c #d48462", +"#vB c #d48553", +"#E0 c #d48764", +"#HH c #d4885b", +"#sl c #d4885d", +".ZV c #d4886f", +"#Jd c #d48959", +"#Ms c #d48d66", +"#Ta c #d48e32", +"#z6 c #d48e69", +"#Ps c #d48e6d", +"#xF c #d48f6f", +".RL c #d48f74", +"#ka c #d49071", +"#xI c #d49467", +".RY c #d49470", +"#pq c #d4956c", +".WY c #d49a75", +"aki c #d49a7d", +"#OU c #d49c68", +".X0 c #d49c79", +"#Nn c #d4a16f", +".Tv c #d4a179", +".1F c #d4a184", +"#Kk c #d4aaa6", +".wt c #d4b590", +"ane c #d4b5a9", +".wp c #d4c0a0", +".kU c #d4c18d", +"#DO c #d4c2a2", +"acv c #d4c3b2", +"##W c #d4cbd2", +"aw9 c #d4d2d8", +"aJn c #d52b26", +"#O1 c #d56d0f", +"#Qq c #d56d1a", +"#HO c #d57856", +"#Dr c #d57c5d", +"#uf c #d57d5d", +"#g8 c #d57e5b", +"#yZ c #d57e62", +"#RP c #d58156", +"#Xt c #d58168", +"#lh c #d5825a", +"#xs c #d58265", +"#fP c #d58464", +"#jR c #d5855b", +"#iL c #d58560", +"#li c #d5865c", +"#Mt c #d5875e", +"#Ax c #d58865", +"#Uw c #d58872", +"#y7 c #d58965", +".1u c #d58970", +"#g7 c #d58a63", +"#E1 c #d58a66", +"#p9 c #d58a6f", +".ZU c #d58b71", +"#8z c #d58d74", +".RQ c #d59175", +".RR c #d59275", +".1i c #d5927a", +".RB c #d5937c", +"#5# c #d59478", +".S5 c #d5967b", +"#OM c #d59993", +"aMy c #d59a82", +"#zT c #d59c60", +"#3a c #d59e62", +".R1 c #d59e7f", +"#tH c #d59f79", +"#vl c #d5a499", +".vR c #d5a58b", +"#OO c #d5a78f", +"#LO c #d5ac96", +".yh c #d5b896", +"agE c #d5c6bf", +"aFL c #d5cacf", +".e8 c #d5cdd6", +".gA c #d5d2de", +"apB c #d5d4d7", +".sM c #d5d5da", +".sL c #d5d5e2", +"#HP c #d67958", +"#Gi c #d67959", +"#Dk c #d67a60", +"#q0 c #d67c5d", +"#L0 c #d68139", +"#Me c #d6815b", +"#NL c #d68260", +"#OL c #d68285", +"#q8 c #d68465", +"#By c #d6855b", +"#x. c #d68958", +"#ui c #d68965", +"#Xx c #d68e31", +".RP c #d69277", +"#Qd c #d6948f", +".S6 c #d6977c", +"#Bq c #d69c7f", +"#CO c #d69d61", +"awt c #d6a68b", +".dl c #d6a887", +"#9E c #d6a89f", +"asU c #d6ab96", +"#Ks c #d6ac7b", +".z3 c #d6bda2", +".8P c #d6c6c3", +"anK c #d6d4d9", +"#gB c #d77864", +"#yJ c #d77b59", +"#Jk c #d77c58", +"#dM c #d77d5b", +"#dY c #d77e5f", +"#vG c #d7805b", +"#Kx c #d7833d", +"#sO c #d7855f", +"#yE c #d7885d", +"#TD c #d78c62", +"#iG c #d78d66", +"#JF c #d78d67", +"#K3 c #d78f64", +"#NX c #d7916d", +"#vu c #d7965f", +"#Nk c #d79753", +"a#m c #d79895", +"#mB c #d79980", +"#yp c #d79e62", +".Tu c #d7a177", +"av2 c #d7a78c", +"#LT c #d7aa76", +"QtD c #d7aa88", +"#Nh c #d7ab94", +".y# c #d7c2ad", +".uZ c #d7c590", +"#47 c #d7ceca", +".n6 c #d7cfcd", +".jg c #d7dbec", +"#Qr c #d86d0d", +"#UH c #d8750c", +"#Dh c #d8765e", +"#aL c #d87e54", +"#ce c #d8815e", +"#Mu c #d88258", +"#jQ c #d8835c", +"#cf c #d8845f", +"#Mp c #d8855a", +"#rp c #d88760", +"#NW c #d88859", +"#rq c #d88860", +"#gW c #d88c65", +"#ks c #d88c72", +"#H9 c #d88e65", +"#H4 c #d88f5e", +"#yC c #d88f61", +".3. c #d89176", +"#vw c #d8926a", +"#j4 c #d8926f", +"#n4 c #d8946c", +"#Tq c #d8976c", +".1j c #d89979", +"#7Z c #d89c97", +".ZB c #d89e79", +"#Ei c #d89f62", +".Wr c #d89f7e", +"avu c #d8a88d", +".gj c #d8ab89", +"#mz c #d8ac99", +".zH c #d8c8a7", +"#AQ c #d8c8b7", +".5r c #d8cbe1", +"#3m c #d8ceca", +".AP c #d8cecb", +"aC7 c #d8d1d4", +".nb c #d8d4f1", +".vC c #d8d7d4", +"#Qt c #d96a12", +"#RV c #d96d11", +"aJk c #d96d70", +"#As c #d9785b", +"#lb c #d97f53", +"#KN c #d9815b", +"#jP c #d9825d", +"#Pu c #d98560", +"#yU c #d98568", +"#ro c #d98661", +"#la c #d98664", +"#Pe c #d98960", +"#xq c #d9896b", +"#lM c #d98c64", +"#v5 c #d98c68", +"#ea c #d98d63", +"#e# c #d98e65", +"#lR c #d98e6c", +"#6R c #d98e74", +"#TF c #d99068", +"#Xv c #d99325", +"#oJ c #d9946f", +"ah4 c #d99476", +"#Sl c #d9955d", +".RS c #d99678", +".1c c #d9967e", +"#sb c #d99965", +"#Hr c #d99a65", +".S4 c #d99b7f", +".1b c #d99c7b", +".V. c #d99e78", +".1a c #d99e7d", +"#wW c #d9a064", +"#pj c #d9a176", +"#tz c #d9a79e", +"auU c #d9a98e", +"#nZ c #d9ae9b", +"#CL c #d9c0a1", +".ve c #d9e2f1", +"#jK c #da7c51", +"#HQ c #da7c5e", +"#tV c #da805d", +"#Jy c #da8159", +"#yV c #da8569", +"#og c #da856f", +"#Tz c #da8655", +"#vO c #da876c", +"#z9 c #da895f", +"#sm c #da8b64", +"#rw c #da8b6f", +"#F0 c #da8f64", +"#Ne c #da9393", +".Yj c #da9478", +"#y9 c #da9576", +".RX c #da9874", +"#OR c #da9a54", +".U9 c #da9c76", +"#T. c #da9f83", +".Tt c #daa075", +"#LL c #daa2a0", +"#FO c #daac88", +"#wZ c #dab096", +"#yr c #dab097", +"#vq c #dab197", +".w# c #dac5a0", +".ye c #dac7a6", +".sd c #daca9a", +".px c #dacfa3", +".68 c #dad4bf", +"aAO c #dad4d8", +"anr c #dad6dc", +"#Qs c #db6d09", +"#EC c #db7f5d", +"#q1 c #db7f64", +"#H2 c #db805a", +"#Jl c #db805d", +"#ia c #db825d", +"#Go c #db834d", +"#QD c #db835d", +"#lO c #db8561", +"#Sa c #db8857", +"#RO c #db8a56", +"#ee c #db8a62", +"#cg c #db8a64", +"#Pd c #db8b60", +"#p1 c #db8b64", +"#fN c #db8b6b", +"#ko c #db8e6c", +"#e. c #db9167", +"#sU c #db9271", +"#ni c #db9370", +".1t c #db9479", +"#v8 c #db9575", +"#se c #db986e", +".RC c #db9882", +"#LR c #db995a", +"#Bv c #db9970", +".WV c #db9976", +"#Ux c #db997e", +"#IZ c #db9a63", +"#f. c #db9b71", +"#FL c #db9c68", +"#vn c #dba265", +"#tB c #dba266", +"#za c #dba27b", +"auh c #dbab90", +"#xJ c #dbac84", +"att c #dbac91", +"#tE c #dbb198", +".ie c #dbcac7", +".pi c #dbcfc5", +"abs c #dbcfd4", +".xy c #dbd0d2", +"aFD c #dbd1d4", +"aFE c #dbd2d5", +".n7 c #dbd5d5", +".mn c #dbdcef", +"#Nr c #dc7b2c", +"#Do c #dc7b5f", +"#aN c #dc7d5d", +"#Mv c #dc8258", +"#QE c #dc835d", +"#sq c #dc836e", +"#xv c #dc866a", +"#xc c #dc8961", +"#C4 c #dc8b61", +"#fH c #dc8e5d", +"#kk c #dc8e66", +"#LK c #dc8e8d", +"#KH c #dc8f5d", +"#n3 c #dc8f5f", +"#Ex c #dc8f62", +"#iR c #dc8f75", +"#v6 c #dc916d", +"#ru c #dc9c76", +"#qQ c #dc9d73", +"#sd c #dc9f74", +"#Qk c #dca56c", +"#Nm c #dcac76", +"#FN c #dcae87", +"#I1 c #dcb184", +"#Ek c #dcb191", +"#zW c #dcb298", +"acw c #dcc1b5", +".0D c #dcd2e2", +".k0 c #dcd7c5", +"aAK c #dcd7da", +"azz c #dcd8db", +"aJq c #dd7467", +"#Tl c #dd761d", +"#Wa c #dd8014", +"#Gk c #dd815c", +"#Jm c #dd8161", +"#ue c #dd8163", +"#Jv c #dd8363", +"#yX c #dd876b", +"#Ty c #dd8857", +"#d9 c #dd8b60", +"#Bx c #dd8f63", +"#fK c #dd916f", +"#TK c #dd926a", +"#mF c #dd926c", +"#RL c #dd9740", +"#Kq c #dd9c60", +"#Kj c #dd9d9a", +".WX c #dd9f7b", +"#ik c #dd9f81", +"#pi c #dda06a", +".rQ c #ddbba4", +".pp c #ddcab4", +".r7 c #ddccaf", +"#1Y c #ddcec6", +"ayk c #ddd0dd", +".8I c #ddd4cd", +".mJ c #ddd5b1", +"auu c #ddd9dc", +"aJp c #de4c43", +"#Ti c #de7a1e", +"#h7 c #de7d54", +"#Gh c #de8063", +"#cp c #de8064", +"#aM c #de815b", +"#nk c #de886e", +"#NE c #de895c", +"#ug c #de8968", +"#tY c #de896e", +"#QB c #de8a62", +"#vX c #de8a6e", +"#vN c #de8a6f", +"#HK c #de8b60", +"#tT c #de8b63", +"#tS c #de8d63", +"#p0 c #de8d67", +"#tP c #de8e5a", +"#QG c #de8f63", +"#vC c #de9064", +"#TC c #de9065", +"#NY c #de916c", +"#fJ c #de9371", +"#fI c #de9471", +"#TL c #de9567", +"#cD c #de9569", +".6e c #de9978", +"#Qh c #de9a4d", +".4G c #de9a79", +".RD c #de9b85", +".RE c #de9c86", +"#UP c #de9d7c", +".UF c #dea57f", +".UG c #dea77c", +"#OT c #deaa72", +"amz c #dead98", +"#Ht c #deb289", +"aHV c #debac1", +".gK c #ded1cf", +"acX c #ded1d8", +".uT c #ded4b6", +".5z c #ded4e0", +"aB5 c #ded5da", +".uc c #dedfda", +"aJl c #df5454", +"#C9 c #df8262", +"#fD c #df845f", +"#h6 c #df8464", +"#H3 c #df855d", +"#UK c #df8631", +"#iK c #df8663", +"#BT c #df876c", +"#xk c #df876d", +"#vM c #df886e", +"#K7 c #df895f", +"#Md c #df8a63", +"#rn c #df8a67", +"#vF c #df8b63", +"#fx c #df8c6a", +"#ok c #df8d6c", +"#hl c #df8f74", +"#hk c #df9075", +"aFe c #df91b3", +"#B0 c #df926e", +"#hd c #df945f", +"#TE c #df956d", +"#YZ c #df982e", +"#Sk c #df9964", +"#iE c #df9974", +"#p6 c #df9c77", +".RT c #df9c7d", +".RF c #df9d87", +"#wV c #dfa15b", +"#r7 c #dfa57a", +".X2 c #dfa67c", +".Wt c #dfa77d", +"a#l c #dfb0a8", +"#Bk c #dfb59b", +"#r9 c #dfb6a2", +"#Fg c #dfc5cb", +"aG3 c #dfd5d8", +"aEi c #dfd6d9", +".k5 c #dfd8b5", +"#.t c #dfd8d6", +".pX c #dfdbe9", +"awB c #dfdcde", +"aek c #dfdce3", +"avL c #dfdde4", +".jt c #dfe0d7", +"#Jn c #e08466", +"#xi c #e0856c", +"#Dn c #e0886d", +"#Ar c #e0896d", +"#Ma c #e08a5b", +"#S# c #e08b59", +"#P# c #e08b61", +"#Jg c #e08d60", +"#sN c #e08d68", +"#BZ c #e08f6d", +"#dJ c #e09074", +".7V c #e09258", +"#mO c #e0926b", +"#io c #e09773", +"#kb c #e0977a", +"#FK c #e09c63", +".RJ c #e09c80", +".U7 c #e09d7a", +".22 c #e09e7e", +".WW c #e09f7c", +"#Qi c #e0a159", +"#vm c #e0a15b", +".1D c #e0a188", +".S7 c #e0a286", +".Ts c #e0a376", +".ZR c #e0a384", +"#qI c #e0a77b", +"#CQ c #e0b494", +".id c #e0cdc6", +"aMJ c #e0d1df", +".tt c #e0d2b3", +".D. c #e0d2c5", +".wE c #e0d9e0", +"aeN c #e0dde1", +"#1I c #e17a64", +"#Dp c #e18164", +"#xg c #e18464", +"#EW c #e18567", +"#K6 c #e18759", +"#gN c #e1876a", +"#RH c #e1887a", +"#Mb c #e1895a", +"#KL c #e18b5c", +"#JE c #e18b5d", +"#yT c #e18c70", +"#Pc c #e19165", +"#QH c #e19167", +".9D c #e1936d", +"#TJ c #e19371", +"#yD c #e19467", +"#xD c #e19571", +"#Kp c #e19657", +"#uj c #e19672", +"#JA c #e1986a", +"#rx c #e1997d", +"#qR c #e19c73", +".25 c #e19f7f", +".RG c #e19f89", +"#w4 c #e1a172", +"#yo c #e1a25c", +"#iw c #e1a281", +"#tA c #e1a35d", +"#CR c #e1b89e", +"ah1 c #e1bba7", +".yi c #e1c5a3", +".qK c #e1d1c2", +"aDt c #e1d6db", +".xj c #e1d7e8", +".o# c #e1d8b1", +".y3 c #e1d9e5", +".nc c #e1dcfa", +"azx c #e1dee1", +"apD c #e1dee4", +"ava c #e1dfe6", +"#RW c #e2741a", +"#BU c #e28164", +"#pz c #e2845f", +"#sL c #e28a6a", +"#An c #e28a6f", +"#kl c #e28b67", +"#K2 c #e28c61", +"#q7 c #e28c6c", +"#xm c #e28c71", +"#pZ c #e28f6c", +"#NP c #e2906f", +"#TH c #e29079", +"#oB c #e2916e", +"#vE c #e29267", +"#sP c #e2926a", +"#mG c #e2926d", +"#EZ c #e2926f", +".9C c #e2936d", +"#q9 c #e29376", +".9E c #e2946e", +"#x# c #e29568", +"#LQ c #e29754", +"#dR c #e29870", +"#z# c #e29c7e", +"#n2 c #e29e77", +".RK c #e29e82", +".4I c #e29f7e", +".RH c #e2a08a", +"#CN c #e2a45e", +"amu c #e2ae92", +"#pl c #e2b8a5", +".v7 c #e2cbaf", +".sa c #e2cdbe", +".n1 c #e2ceac", +".6Z c #e2d4e4", +"#.q c #e2d5cf", +".Cp c #e2d7d9", +"ael c #e2ddde", +"agR c #e2dfe6", +"arh c #e2e2e5", +".ju c #e2e3da", +"aLC c #e37d79", +"#q3 c #e38370", +"#Gj c #e38663", +"#YX c #e3866e", +"#R9 c #e38b57", +"#jJ c #e38b6a", +"#K5 c #e38c5e", +"#fF c #e38c61", +"#yY c #e38d71", +"#d1 c #e39072", +"#L9 c #e39463", +"#jV c #e3956d", +"#w. c #e39571", +"#n# c #e39573", +"#Du c #e39672", +"#lJ c #e39873", +"#rc c #e3997c", +"#g3 c #e39a75", +"#Y1 c #e39b41", +".RW c #e3a07e", +".RU c #e3a180", +"#zS c #e3a55f", +".X9 c #e3a687", +".Ws c #e3aa83", +"#l. c #e3ad9e", +"#wY c #e3b797", +"#vp c #e3b898", +"#El c #e3b99f", +"#qK c #e3baa6", +".u0 c #e3d19d", +".sb c #e3d1b1", +".zU c #e3d2ba", +".5w c #e3d4d0", +".n4 c #e3d6c8", +".qZ c #e3d6cb", +"aG2 c #e3dadd", +".8S c #e3dbc6", +".k4 c #e3ddb8", +"anH c #e3dee3", +"avF c #e3dfe2", +"ap7 c #e3dfe5", +"amm c #e3e2e8", +".ub c #e3e4e4", +"#Jt c #e4896e", +"#od c #e48c6b", +"#S. c #e48d59", +"#KM c #e48d5e", +"#Ji c #e48d61", +"#pS c #e48d74", +"#K0 c #e48e66", +"#yR c #e48e73", +"#Ds c #e4906f", +"#sn c #e49370", +"#BE c #e49377", +"#Sc c #e49668", +"#na c #e49673", +"#F1 c #e4986e", +"aI8 c #e498c4", +"#0o c #e49b38", +"aJj c #e49da2", +"#w# c #e49f6e", +".4H c #e4a080", +".RV c #e4a17f", +".24 c #e4a282", +".WU c #e4a381", +".UI c #e4a583", +"#Bh c #e4a660", +".X1 c #e4ab84", +"#Bj c #e4b898", +"#tD c #e4b998", +"#7d c #e4ba9f", +"#qL c #e4bfaa", +".n2 c #e4d3b6", +"a#P c #e4d8dc", +"aDq c #e4d9de", +"aka c #e4dae2", +"apo c #e4e0e6", +"awc c #e4e1e8", +"ayf c #e4e2e4", +"aqm c #e4e3e7", +"#yO c #e58e74", +"#NO c #e59074", +"#Ac c #e59277", +"#KJ c #e59363", +"#oE c #e5956e", +"#kj c #e59770", +"#aH c #e59972", +"#IY c #e59b60", +"#ps c #e59d75", +"#Hq c #e59e64", +"#OQ c #e59f54", +".4N c #e5a084", +".1s c #e5a386", +".Tr c #e5a577", +".UJ c #e5a584", +".UH c #e5a684", +".Wu c #e5a887", +".X8 c #e5a889", +".Ye c #e5a98a", +".7F c #e5b1a2", +"#zV c #e5ba9a", +"aEI c #e5d8de", +".re c #e5e1e7", +".tQ c #e5e6e8", +"#pU c #e68b73", +"#yL c #e68d74", +"#EX c #e68e6e", +"#vL c #e68e75", +"#NF c #e68f63", +"#yW c #e69074", +"#dW c #e6926f", +"#dP c #e69570", +"#Dt c #e69673", +"#dV c #e69772", +"#fz c #e69874", +"#Nj c #e69e56", +"#qO c #e6a46f", +".Tq c #e6a576", +".Wv c #e6a988", +"#sc c #e6ad81", +"#s. c #e6c2ad", +"aa0 c #e6c4b9", +".wd c #e6d5ab", +"aF0 c #e6d6dd", +"anB c #e6dae0", +".D1 c #e6dbde", +".D0 c #e6dcdb", +".n5 c #e6ddd5", +".h6 c #e6e0da", +".t1 c #e6e2df", +"aeJ c #e6e3e7", +".ue c #e6e4fd", +".y7 c #e6e5e1", +".mU c #e6eefa", +"#Dg c #e7846c", +"#Gg c #e7896e", +"#yK c #e78a69", +"#H1 c #e78c68", +"#xh c #e78c73", +"#fC c #e78d6b", +"#qZ c #e78f6a", +"#sM c #e7916e", +"#NM c #e79373", +"#fj c #e79476", +"aDX c #e794b7", +"#dI c #e7967b", +"#Bw c #e79a6d", +"#lL c #e79a73", +"#1K c #e79c3f", +"#iH c #e79c74", +"#n6 c #e79c75", +"#Gr c #e79d72", +"#pp c #e7a06d", +"#yz c #e7a085", +".23 c #e7a484", +"#pr c #e7a57c", +".ZK c #e7a98a", +".WR c #e7af8f", +".n9 c #e7dbc4", +"#3n c #e7ddd7", +".Co c #e7dddc", +".nU c #e7ded4", +".b8 c #e7e3ed", +".mR c #e7eef3", +".jE c #e7f1eb", +"#0m c #e8846d", +"#Ju c #e88d6f", +"#N0 c #e88f68", +"#d7 c #e88f6a", +"#BS c #e89075", +"#tU c #e8916b", +"#m9 c #e89379", +"#TI c #e8987c", +"#Ey c #e8996d", +"#iJ c #e89a72", +"#n. c #e89b79", +"#p7 c #e89c7c", +"#mX c #e89d7c", +".7Z c #e89f7f", +".Yi c #e8a086", +".29 c #e8a488", +".RI c #e8a489", +"#AB c #e8a68c", +".UK c #e8a988", +"#w3 c #e8aa75", +"#qH c #e8ab75", +".U5 c #e8ab85", +"#3b c #e8b47c", +"#n0 c #e8bba9", +".yl c #e8cba9", +".qu c #e8d5ba", +".5q c #e8d8e8", +".66 c #e8dcc5", +".2a c #e8dce6", +".8R c #e8ddca", +".32 c #e8dff5", +"alg c #e8e0e9", +".YZ c #e8e3fb", +"af3 c #e8e4e8", +".AI c #e8e4f5", +"aeM c #e8e5e9", +"awF c #e8e5ec", +"#HR c #e98b6f", +"#Pb c #e99068", +"#yM c #e99077", +"#Pa c #e99169", +"#Aq c #e99276", +"#K1 c #e99368", +"#qY c #e9936b", +"#fG c #e99768", +"#BG c #e9977b", +"#Sb c #e99869", +"#Jf c #e9996a", +"#rd c #e99d80", +"#rb c #e9a284", +".ZT c #e9a488", +".Ww c #e9ab8a", +".X7 c #e9ad8d", +"#pm c #e9c1ad", +"afF c #e9cfbf", +".n0 c #e9d4b0", +".po c #e9d4b9", +".zV c #e9d8be", +".qR c #e9dbae", +"aEH c #e9dbe1", +".2g c #e9dce3", +".sn c #e9ddd0", +"a.B c #e9dfe1", +"aFG c #e9e0e3", +".0x c #e9e0eb", +"##X c #e9e2f0", +"afB c #e9e7ee", +"#py c #ea8c65", +"#q2 c #ea8c76", +"#Jr c #ea8c77", +"#HX c #ea8d73", +"#oa c #ea8f67", +"#Ao c #ea9277", +"#gU c #ea9370", +"#d8 c #ea956c", +"#z8 c #ea9c6f", +"#sV c #ea9d80", +"#lK c #ea9e78", +"#ci c #ea9f78", +"#r. c #ea9f84", +"#1M c #eaa051", +"#vx c #eaa07f", +"#0q c #eaa14d", +".Yh c #eaa287", +".6l c #eaa589", +"#ft c #eaa780", +"#yx c #eaaa87", +".Z2 c #eaaa8e", +"#z2 c #eaad8e", +"#yv c #eaaf7c", +".WQ c #eaaf90", +"#z1 c #eab087", +"amy c #eab69a", +".wc c #ead6ab", +".tz c #ead6b3", +"ai8 c #ead9e4", +"ahY c #eadae3", +".C9 c #eadccf", +".0u c #eaddc8", +".YW c #eadfd9", +"#48 c #eadfdb", +".zf c #eadfe1", +"aEj c #eae2e5", +".tv c #eae3d2", +"QtS c #eae3f8", +"#De c #eb8e76", +"#d5 c #eb9070", +"#Js c #eb9076", +"#JC c #eb9362", +"#aK c #eb9367", +"#xl c #eb947a", +"#Sf c #eb957e", +"#Mc c #eb966e", +"#Pf c #eb9a74", +"#oC c #eb9a75", +"#hc c #eb9d6a", +"#lD c #eb9d81", +"#Sj c #eba372", +"#kg c #eba37d", +".4J c #eba787", +".U8 c #eba985", +".26 c #eba989", +".1k c #ebab8b", +".1r c #ebad8f", +".X3 c #ebaf8f", +".Yc c #ebb192", +".WJ c #ebb294", +"#mA c #ebbaa9", +"#2q c #ebc1ab", +"ai9 c #ebe0e6", +".He c #ebe1e0", +"abU c #ebe1ea", +".rT c #ebe5cf", +".h8 c #ebe5df", +".sz c #ebe6e7", +"au9 c #ebe8eb", +".sq c #ebf1fc", +".jG c #ebfcff", +"#Ab c #ec8f6e", +"#xj c #ec9279", +"#Jz c #ec936a", +"#pW c #ec9577", +"#NN c #ec977a", +"#C3 c #ec9d71", +"#HI c #ec9e72", +"#ol c #ec9f7f", +"#w7 c #eca285", +"#iq c #ecab89", +".Th c #ecac88", +".S8 c #ecad92", +".WH c #ecb395", +"abu c #ecc8bd", +".mz c #ecd9af", +".8Q c #ecddd3", +".r9 c #ece0d2", +"afC c #ece3e6", +".67 c #ece4c7", +".yU c #ece7ff", +"ans c #ece8ed", +"axw c #ece9ec", +".Cd c #ece9fc", +"#Jo c #ed9075", +"#q6 c #ed9574", +"#Ap c #ed957a", +"#rm c #ed9675", +"aLB c #ed9988", +"#pR c #ed9b80", +"#pJ c #ed9d7d", +".7U c #ed9f79", +"#Pr c #eda06e", +"#QR c #eda06f", +"#iI c #eda078", +"#mC c #eda27d", +"aMx c #eda890", +".Tb c #edad89", +"#z3 c #edad94", +".Wx c #edb08f", +".UW c #edb192", +".WS c #edb28d", +".Y. c #edb293", +".8. c #edb99f", +".pf c #eddccb", +".mA c #edddb7", +"QtB c #eddee7", +"#8Z c #ede1df", +"anz c #ede1e6", +"#6N c #ede2df", +".mH c #ede3cf", +".vK c #ede3e2", +".mI c #ede5bf", +"aml c #ede6f0", +"aii c #ede9ed", +"aw8 c #edebf2", +".xo c #edede2", +".vB c #edeede", +"#HU c #ee8f7a", +"#co c #ee9071", +"#Ag c #ee997e", +"#Ae c #ee9a7f", +"#Sg c #ee9a81", +"#Pq c #eea070", +"#nb c #eea07c", +"#Je c #eea171", +"#pK c #eea284", +"#lI c #eea47f", +"#w5 c #eeaa85", +".U6 c #eead89", +".ZS c #eead90", +".Te c #eeae8a", +".UR c #eeb698", +"aen c #eedcd3", +".2# c #eee1df", +"anA c #eee2e8", +"#.s c #eee6dc", +"avK c #eeebf2", +".ud c #eeedff", +".lc c #eefaff", +"#BD c #ef9271", +"#sK c #ef9576", +"#H6 c #ef9763", +"#yQ c #ef997e", +"#BI c #ef9a7f", +"#mU c #ef9c78", +"#oD c #ef9f79", +"#KI c #efa06f", +"#xG c #efa289", +".UL c #efaf8e", +".1n c #efaf8f", +".U0 c #efaf92", +".X6 c #efb293", +".X4 c #efb393", +".WI c #efb798", +"a.y c #efc8b7", +".xx c #efe5e4", +".b7 c #efe8f0", +".sD c #efeaeb", +"aeL c #efebf0", +"awb c #efecf3", +".q4 c #eff1fa", +"#sJ c #f09477", +"#vK c #f0977d", +"#yP c #f0997f", +"#lS c #f09b7f", +"#ou c #f0a085", +"#F2 c #f0a279", +"#lm c #f0a87f", +"#lt c #f0a886", +"#lB c #f0aa8c", +".Tf c #f0b08c", +".Yf c #f0b193", +".WC c #f0b799", +".UU c #f0b89a", +".wa c #f0dbb4", +"#7f c #f0e1db", +".3Y c #f0e1e1", +"#99 c #f0e5e7", +".aB c #f0e9f2", +".rh c #f0ebf1", +".rg c #f0ecf2", +"#yN c #f1997f", +"#d0 c #f19b7d", +"#fh c #f19c7e", +"aLA c #f19c85", +"#d2 c #f1a083", +"#QM c #f1a27d", +"#QS c #f1a472", +"#g4 c #f1a480", +"#ki c #f1a57e", +"#kh c #f1a780", +"#n5 c #f1aa83", +".Tk c #f1ad89", +".Tj c #f1ad8a", +".Yg c #f1ad91", +"#j5 c #f1af8e", +".1l c #f1b191", +".1m c #f1b192", +"#ir c #f1b393", +".ZL c #f1b495", +".Yd c #f1b697", +".UT c #f1b99b", +"#Ff c #f1d5da", +".5n c #f1decd", +".60 c #f1e2f8", +"agT c #f1e6e7", +".oA c #f1ecff", +".rf c #f1edf3", +"af1 c #f1eef2", +".AH c #f1eff5", +"#H0 c #f29674", +"#HY c #f2967a", +"#BR c #f2987d", +"#dN c #f29a77", +"#fB c #f29b7a", +"#pV c #f29b7d", +"#fg c #f29b7e", +"#yS c #f29d81", +"#F3 c #f2a179", +"#BF c #f2a185", +"aI6 c #f2a5c0", +"#xH c #f2a786", +"#jW c #f2aa84", +"#ra c #f2ab91", +"#iD c #f2ae89", +".1q c #f2b292", +".WP c #f2b295", +".S9 c #f2b498", +".ZO c #f2b596", +".Y# c #f2b798", +".WD c #f2b99b", +".WE c #f2ba9b", +".US c #f2ba9c", +"amx c #f2bea2", +".U3 c #f2c09e", +"aa1 c #f2c3be", +"aaf c #f2dcda", +".x1 c #f2dfb8", +"adm c #f2e3ec", +".3T c #f2e3f0", +".gH c #f2e4e2", +"#8v c #f2e6e6", +".0w c #f2e7e7", +"adn c #f2eaf6", +".vA c #f2edd5", +".y6 c #f2eee4", +"asu c #f2eef4", +".pW c #f2eefb", +"axx c #f2eff6", +".pD c #f2f0e6", +"#HT c #f3947d", +"#Df c #f3967e", +"#H8 c #f39c6c", +"#oK c #f3a486", +"#fq c #f3aa82", +".Tl c #f3af8a", +".Ti c #f3af8d", +".28 c #f3b191", +"#mT c #f3b296", +".Tc c #f3b38f", +"#yw c #f3b68a", +".Wy c #f3b695", +".ZM c #f3b697", +".X5 c #f3b797", +"#g0 c #f3ba92", +"amv c #f3bfa3", +".3Q c #f3e2ce", +"#5A c #f3e3da", +".if c #f3e3e4", +".n3 c #f3e4ce", +".2. c #f3e4d6", +"aag c #f3e5e9", +".b5 c #f3e7eb", +"aDs c #f3e8ed", +".xb c #f3ecff", +".vz c #f3edda", +"ahe c #f3eff3", +".pc c #f3f2e8", +"alh c #f3f2f6", +".og c #f3f9f6", +"#rj c #f4997c", +"#Al c #f49a7f", +"#so c #f4a083", +"#Af c #f4a084", +"#Ad c #f4a185", +"#HJ c #f4a479", +"#Pl c #f4a480", +"#Po c #f4a579", +"#kf c #f4ad88", +".Ta c #f4b48f", +".Tg c #f4b490", +".1o c #f4b495", +".T. c #f4b69a", +".Ya c #f4b99a", +"abR c #f4d3c3", +".ww c #f4d5b0", +"alx c #f4d9d2", +"#3Q c #f4e2d8", +"abT c #f4e2e7", +".3S c #f4e3e5", +".AQ c #f4e9e8", +".kY c #f4eacc", +"aB1 c #f4ebf0", +".xl c #f4ede7", +".h9 c #f4efe8", +".sA c #f4f0f0", +"af0 c #f4f0f4", +"aoA c #f4f0f5", +"aoz c #f4f0f6", +"av# c #f4f1f8", +".vd c #f4f3ef", +".la c #f4f9ff", +".lb c #f4fdff", +"#pY c #f5a17f", +"#nj c #f5a386", +"#Pn c #f5a67d", +"#fv c #f5a684", +"#nd c #f5a780", +"#QQ c #f5a879", +"#r# c #f5ad92", +".4M c #f5b296", +".UM c #f5b594", +"#j6 c #f5b797", +".T# c #f5b79b", +".ZQ c #f5b899", +".tb c #f5d2c6", +"agW c #f5d5c0", +".wv c #f5d6b2", +"#8Y c #f5ddd3", +"akr c #f5ded8", +"aLM c #f5e0f0", +".65 c #f5e6da", +"any c #f5e9ee", +".YV c #f5eadc", +"#.r c #f5eae0", +"QtW c #f5eaef", +"afD c #f5ebea", +".b6 c #f5ebf1", +".6X c #f5ecdd", +".vy c #f5efe4", +".xn c #f5f1de", +".y5 c #f5f1eb", +"ahf c #f5f1f5", +"akb c #f5f3f5", +"awE c #f5f3fa", +".jF c #f5ffff", +"#Am c #f69d82", +"#Gq c #f69f6d", +"#pX c #f6a080", +"#Pp c #f6a87a", +".7T c #f6a882", +"#v9 c #f6a88e", +"#Sd c #f6a97e", +"#mY c #f6af8f", +".ZN c #f6b99a", +".U4 c #f6bb95", +".WG c #f6bd9e", +".UQ c #f6bea0", +"#j0 c #f6c1a1", +".xY c #f6e2c1", +"a.A c #f6e4e1", +".r8 c #f6e7d1", +".ze c #f6ebeb", +"##V c #f6ecee", +"QtX c #f6eff9", +"aAL c #f6f0f3", +".pG c #f6f1ef", +"aeK c #f6f2f6", +"arM c #f6f2f8", +".oi c #f6f3fe", +"apC c #f6f5f9", +".q5 c #f6fbff", +".mT c #f6ffff", +"#HS c #f7997f", +"#HW c #f79a81", +"#HZ c #f79b7c", +"#cn c #f79c79", +"#fm c #f79e7b", +"#Pm c #f7a781", +"#QN c #f7a982", +"#nc c #f7a984", +"#Si c #f7ac81", +"#om c #f7ad8e", +"#pL c #f7ae92", +"#w6 c #f7af91", +"#AA c #f7af9a", +".6h c #f7b291", +".6f c #f7b292", +".UZ c #f7b699", +".Td c #f7b793", +"ajq c #f7dbd0", +".wq c #f7e3c1", +".yd c #f7e4c4", +".n8 c #f7e8e4", +".3Z c #f7eaee", +"agS c #f7eaf0", +".Xm c #f7ecec", +".qx c #f7efdc", +".tP c #f7f0e5", +"aBI c #f7f2f4", +".t2 c #f7f3f0", +"app c #f7f3f9", +"aw5 c #f7f4f7", +"alz c #f7f5f9", +".mP c #f7fefe", +"#HV c #f89985", +"#Jq c #f89a84", +"#BP c #f89c82", +"#d4 c #f8a082", +"#BJ c #f8a287", +"#QI c #f8a883", +"#g6 c #f8ab86", +"#Se c #f8ac81", +"#z. c #f8ae96", +"#RM c #f8af60", +"#aJ c #f8af87", +"#m5 c #f8b092", +".6g c #f8b392", +".Tm c #f8b48e", +".Tp c #f8b58b", +".Wz c #f8ba99", +"#lp c #f8c4a4", +".3R c #f8e7dc", +".ts c #f8e8c2", +".kW c #f8e9bd", +"alm c #f8eae3", +".5y c #f8ebf2", +".FF c #f8eded", +"aDr c #f8edf2", +".vc c #f8eedd", +".nT c #f8f0e6", +".gR c #f8f1f0", +".t4 c #f8f3f0", +".sC c #f8f3f4", +"aAM c #f8f3f6", +".kL c #f8f3fa", +"avI c #f8f4f7", +".AF c #f8f4fa", +"amF c #f8f5f9", +"ap8 c #f8f5fa", +".oh c #f8f7fe", +"aqn c #f8f8fb", +"amp c #f8fbf6", +"amD c #f8fefe", +"amC c #f8ffff", +"#fi c #f9a688", +"#fO c #f9a988", +"#pQ c #f9aa8f", +"#lu c #f9b494", +"#pM c #f9b499", +".WO c #f9b69a", +"#fs c #f9b991", +".UN c #f9b998", +"#m1 c #f9b99d", +".UX c #f9bb9d", +".WT c #f9bc98", +"#il c #f9bda0", +"amw c #f9c5a9", +"afZ c #f9dcce", +".5p c #f9e7eb", +".fg c #f9e8ef", +".aA c #f9eef4", +".td c #f9efd9", +".dD c #f9eff5", +"aB4 c #f9f1f5", +"ahZ c #f9f2ed", +".vo c #f9f2ff", +"aAN c #f9f4f7", +"azy c #f9f5f8", +"af2 c #f9f5f9", +"avJ c #f9f6fd", +"alj c #f9fcf7", +"amE c #f9fcf8", +"amo c #f9fefb", +".mQ c #f9ffff", +"#JD c #fa9f6d", +"#sp c #faa38a", +"#cl c #faa67d", +"#pI c #faa686", +"#Pi c #faa889", +"#in c #faaf89", +"#pP c #fab093", +"#iy c #fab395", +"#V5 c #fab44d", +".To c #fab68d", +"#yy c #fab69b", +".Tn c #fab78f", +".WA c #fabd9c", +".Yb c #fabfa1", +"#it c #fac0a1", +".UP c #fac2a4", +"#5z c #fadac3", +"abS c #fae0dc", +".5x c #faecec", +".0v c #faede0", +".vp c #faf3ff", +"ant c #faf6fc", +".AG c #faf7f9", +"anJ c #faf7fd", +"aoM c #faf9fc", +"aoN c #fafafd", +"alB c #fafcfb", +"amn c #fafeff", +".of c #fafffc", +"#BO c #fb9e84", +"#Jp c #fb9e85", +"#H7 c #fb9f6b", +"#oe c #fba487", +"#of c #fba58c", +"#fA c #fba685", +"#Ah c #fba78c", +"#BH c #fba88d", +".7R c #fbac86", +"#Sh c #fbac88", +"#QP c #fbad81", +"#kc c #fbaf92", +".6k c #fbb89c", +".27 c #fbb999", +".UO c #fbbb9a", +".WL c #fbbd9f", +".WB c #fbbe9d", +".ZP c #fbbe9f", +".WK c #fbbfa0", +"#iu c #fbc19f", +".WF c #fbc2a3", +".UV c #fbc3a5", +"ahc c #fbdbca", +"adk c #fbddd0", +".yk c #fbdebc", +".uB c #fbe6d9", +"anx c #fbeef4", +"aem c #fbf2f0", +".xk c #fbf3f8", +".ia c #fbf6ef", +".t3 c #fbf6f4", +"aq4 c #fbf7fd", +"aw7 c #fbf8fa", +"ayh c #fbf9fb", +".jh c #fbfdff", +".mS c #fbffff", +"#BQ c #fca187", +"#rl c #fca384", +"#dX c #fca484", +"#fw c #fcaa88", +"#Ez c #fcab81", +"#QO c #fcae85", +"#1L c #fcb258", +"#gX c #fcb48c", +"#dS c #fcb58c", +"#os c #fcb597", +"#mS c #fcba9d", +"#op c #fcba9e", +"#jX c #fcbb97", +"#g1 c #fcbf98", +"#j7 c #fcc0a1", +".tA c #fcebb7", +".x2 c #fcecc5", +"QtV c #fcecee", +".so c #fcf3e7", +".nS c #fcf5ea", +"akt c #fcf5f2", +"aC6 c #fcf5f7", +".pu c #fcf5fc", +".nQ c #fcf6f2", +".xm c #fcf7e8", +"awC c #fcf8fb", +"aih c #fcf8fc", +".pV c #fcf8ff", +"avG c #fcf9fb", +"awD c #fcf9fc", +"akv c #fcf9fd", +"ayi c #fcfafc", +"aj# c #fcfbf2", +"anI c #fcfcff", +"akc c #fcfef9", +"ali c #fcffff", +"#rk c #fda385", +"#QK c #fdac8d", +"#Tb c #fdb660", +"#Xw c #fdb74e", +".4L c #fdb999", +".WM c #fdbb9e", +".UY c #fdbc9f", +".1p c #fdbd9e", +"#lz c #fdbe9d", +"#k. c #fdc09f", +"#is c #fdc2a3", +"aie c #fdddcc", +"a.z c #fde0d7", +"adl c #fde5e4", +".tc c #fde8d6", +".19 c #fdeed8", +"afE c #fdeee6", +".az c #fdeff2", +".s. c #fdf5f3", +"aB3 c #fdf5f9", +".mD c #fdf6e5", +".tx c #fdf8ef", +".mq c #fdf9ed", +"aw# c #fdf9fc", +"ajt c #fdf9fd", +"anv c #fdf9ff", +"aw4 c #fdfafc", +"alk c #fdfbf4", +"ayj c #fdfbfd", +"aoO c #fdfbff", +"alA c #fdffff", +"#d3 c #feaa8d", +"#Pj c #feac8e", +"#UA c #feb758", +"#on c #feb89b", +"#ke c #feb995", +"#or c #feb99a", +".4K c #feba9a", +"#mZ c #feba9c", +"#iC c #febb96", +"#ly c #fec09f", +"#lw c #fec0a2", +"#iv c #fec2a0", +".yf c #fee2c0", +"acY c #fee5de", +".wb c #fee9c0", +".yb c #feebd0", +".tB c #feedba", +".k3 c #fef5e1", +".tw c #fef8ec", +".i# c #fef8f1", +".y4 c #fef9fa", +".sB c #fefafa", +"aw6 c #fefafd", +"anu c #fefaff", +"av. c #fefbfd", +"ayg c #fefcfe", +"awa c #fefcff", +".nP c #fefeff", +"#BN c #ffa288", +"#Ak c #ffa68b", +"#Gp c #ffa872", +"#BL c #ffa88e", +"#BM c #ffa88f", +"#gT c #ffaa88", +"#dO c #ffab87", +"#EY c #ffad8c", +"#Aj c #ffad92", +"#lF c #ffad93", +"#fn c #ffae8b", +"#QJ c #ffae8d", +"#Pk c #ffaf8c", +"#gS c #ffaf8d", +"#lE c #ffaf95", +"#lr c #ffb08c", +"#Pg c #ffb08d", +"#Ph c #ffb08f", +"#BK c #ffb096", +"#QL c #ffb395", +".7S c #ffb48e", +"#iA c #ffb499", +"#j1 c #ffb58f", +"#Ai c #ffb59a", +"#aI c #ffb68e", +"#lH c #ffb693", +"#iz c #ffb699", +"#dU c #ffb790", +"#im c #ffb892", +"#mP c #ffb994", +"#g5 c #ffb995", +"#0p c #ffba5c", +"#pO c #ffba9c", +".6j c #ffbb9b", +"#pN c #ffbb9c", +"#ot c #ffbb9e", +"#kd c #ffbba0", +"#ck c #ffbc94", +"#lG c #ffbc99", +".WN c #ffbca0", +"#iB c #ffbca1", +"#Y0 c #ffbd58", +".6i c #ffbd9d", +"#gZ c #ffbe94", +"#cj c #ffbe96", +"#lv c #ffbe9f", +"#m0 c #ffbea1", +"#fr c #ffbf96", +"#lq c #ffbf9a", +"#mQ c #ffbf9d", +"#lA c #ffbf9f", +"#oq c #ffc0a1", +"#dT c #ffc197", +"#mR c #ffc1a2", +"#m4 c #ffc2a3", +"#gY c #ffc39a", +"#j2 c #ffc39e", +"#m3 c #ffc3a3", +"#k# c #ffc4a4", +".U1 c #ffc4a5", +"#oo c #ffc4a7", +"#m2 c #ffc5a5", +"#j9 c #ffc6a4", +"#lo c #ffc7a4", +"#lx c #ffc7aa", +"#jY c #ffc8a6", +"#j8 c #ffc9aa", +"#ln c #ffcaa3", +".U2 c #ffcaaa", +"#jZ c #ffcdac", +"aI7 c #ffd2f3", +".6y c #ffdbc4", +"amr c #ffdcc0", +".uA c #ffddd8", +"#8X c #ffdfcd", +"#Wh c #ffdfdd", +"aae c #ffe0d6", +"#7e c #ffe6d5", +"abt c #ffe7de", +"aHH c #ffe7fb", +"a#Q c #ffe8db", +"ahd c #ffebde", +"acZ c #ffebe1", +".wr c #ffecc8", +".rR c #ffecd4", +"amB c #ffece7", +".tr c #ffedc3", +"agV c #ffeddf", +".x3 c #ffeecb", +".yc c #ffeed1", +"aif c #ffeee1", +".qP c #ffefd2", +".ic c #ffefe5", +".ay c #ffeff0", +"QtU c #fff0ef", +"ake c #fff1e7", +".5o c #fff2e9", +"ajb c #fff3e3", +".wD c #fff3f2", +"aly c #fff3f3", +".kV c #fff4c2", +".pw c #fff4da", +".pq c #fff4e4", +"QtT c #fff4f2", +".nR c #fff5eb", +"ajs c #fff5ec", +".pr c #fff5ed", +".ps c #fff5f4", +".mB c #fff6d7", +".pe c #fff6e8", +".qO c #fff6eb", +".pt c #fff6fa", +"aoB c #fff6fc", +".x4 c #fff7db", +".rS c #fff7df", +"ah0 c #fff7e5", +"all c #fff7ef", +".gJ c #fff7f6", +".mC c #fff8e1", +"ajr c #fff8ef", +".q0 c #fff8f0", +".mG c #fff8f6", +".h5 c #fff8f7", +".mp c #fff8f8", +".tO c #fff9e8", +".ms c #fff9ee", +".pv c #fff9f1", +"aks c #fff9f6", +".gI c #fff9f7", +"aBH c #fff9fb", +".qM c #fff9fd", +"anw c #fff9ff", +".ya c #fffae2", +".qv c #fffae3", +"aja c #fffaed", +".q1 c #fffaf4", +".q2 c #fffaf5", +".qL c #fffaf9", +".pI c #fffafd", +"aB2 c #fffafe", +".kO c #fffbef", +"akd c #fffbf0", +".gQ c #fffbf3", +".ib c #fffbf5", +".gM c #fffbf9", +".s# c #fffbfb", +".pH c #fffbfc", +"aBG c #fffbfd", +".qN c #fffbff", +".qw c #fffce8", +".pb c #fffcee", +".mE c #fffcf0", +".sp c #fffcf6", +"avH c #fffcfe", +".mo c #fffcff", +".kN c #fffdf0", +".gP c #fffdf1", +".pE c #fffdf5", +"aig c #fffdff", +".mr c #fffef3", +".pd c #fffef4", +"amq c #fffef8", +".pF c #fffef9", +"aj. c #fffefc", +".ji c #fffeff", +".kM c #fffff4", +".mF c #fffff7", +"agU c #fffff8", +"aku c #fffffa", +".q3 c #fffffb", +".i. c #fffffc", +".l# c #ffffff", +"Qt.Qt#QtaQtbQtcQtdQteQtfQtgQthQtiQtjQtkQtlQtmQtnQtoQtpQtqQtrQtrQtsQttQtuQtvQtwQtxQtyQtzQtAQtBQtCQtDQtEQtFQtGQtHQtIQtJQtKQtLQtMQtNQtOQtPQtQQtRQtSQtTQtUQtVQtWQtXQtYQtZQt0Qt1Qt2Qt3Qt4Qt5Qt6Qt7Qt5Qt8Qt9.#..##.#a.#b.#c.#d.#e.#f.#g.#h.#i.#j.#k.#l.#m.#n.#o.#p.#q.#r.#s.#t.#u.#v.#w.#x.#y.#z.#A.#A.#B.#C.#D.#E.#E.#D.#C.#B.#D.#D.#F.#F.#F.#G.#H.#I.#J.#K.#L.#M.#N.#O.#P.#Q.#R.#S.#S.#T.#U.#V.#V.#W", +".#XQt#.#Y.#ZQtc.#0.#1Qtf.#2.#3.#4.#5Qtk.#6.#7.#8QtoQtp.#9QtrQtrQtsQtt.a..a#.aa.ab.ac.ad.ae.af.ag.ah.ai.aj.ak.al.am.an.ao.ap.aq.ar.as.at.au.av.aw.ax.ay.az.aA.aB.aC.aD.aE.aF.aG.aH.aI.aJ.aK.aL.aM.aN.aO.aP.aQ.aR.aS.aT.aU.aV.aW.aX.aY.aZ.a0.a1.a2.a3.a4.#o.#p.a5.#r.#s.#t.#u.#u.a6.a7.#x.#y.#z.a8.#D.#G.a9.#F.#F.a9.#G.#D.#E.#D.#F.#F.#F.#G.#E.#I.b..b#.ba.a5.bb.bc.bd.be.bf.bg.bh.bi.#P.bj.bk.bl", +".bmQt#.bn.boQtc.bp.bq.br.bs.bt.bu.bv.bw.bx.by.bz.bA.bB.#9.bC.bCQts.bD.bE.bF.bG.bH.bI.bJ.bK.bL.bM.bN.bO.bP.bQ.bR.bS.bT.bU.bV.bW.bX.bY.bZ.b0.b1.b2.b3.b4.b5.b6.b7.b8.b9.c..c#.ca.c#.cb.cc.cd.ce.cf.cg.ch.ci.cj.ck.cl.cm.cn.co.cp.cq.cr.cs.ct.cu.cv.cw.cx.cy.cz.cA.cB.cC.#v.cD.cE.cF.cG.cH.cI.#H.#y.cH.#I.a9.cJ.cJ.a9.#I.cH.#I.#E.#D.#F.#F.#G.#E.#I.b..cK.cL.cM.cN.cO.#U.bi.cP.cQ.cR.cS.cT.cU.cV.cW", +".cXQt#.cY.cZ.c0.c1.bq.c2.c3.c4.c5.c6.bw.c7.c8.c9.d..bB.d#.da.da.db.bD.dc.dd.de.df.dg.dh.di.dj.dk.dl.dm.dn.do.dp.dq.dr.ds.dt.du.dv.dw.dx.dy.dz.dA.dB.dC.dD.dE.dF.dG.dH.dI.dJ.dK.dL.dM.dN.dO.dP.dQ.dR.dS.dT.dU.dV.dW.dX.dY.dZ.d0.d1.d2.d3.d4.d5.d6.d7.d8.d9.e..e#.ea.eb.#l.ec.ec.ed.ed.ee.ef.ef.eg.eh.#I.#E.#D.#D.#E.#I.eh.eh.#I.#D.a9.#F.#D.#E.#I.b#.cK.cK.ei.ej.ek.el.bd.em.en.#S.eo.#P.ep.eq.er", +".esQt#.et.eu.c0.ev.ew.ex.c9.ey.ez.eA.eB.eB.eC.eD.eE.eF.d#.eG.eG.db.eH.eI.eJ.eK.ab.eL.eM.eN.eO.eP.eQ.eR.eS.eT.eU.eV.eW.eX.eY.eZ.e0.e1.e2.e3.e4.e5.e6.e7.e8.e9.f..f#.fa.dB.fb.fc.fd.fe.ff.fg.fh.fi.fj.fk.fl.fm.fn.fo.fp.fq.fr.fs.ft.fu.fv.fw.fx.cO.fy.fz.fA.fB.fC.fD.fE.eb.fF.fF.fF.fG.fG.fH.fH.fH.#I.#I.eh.fI.fI.eh.#I.#I.fI.eh.#I.#D.a9.#D.#D.#H.fJ.fK.fL.#M.cN.fM.fN.fO.fP.fQ.fR.fS.fT.#V.fU.bk", +".fVQt#.fW.fX.c0.fY.fZ.f0.f1.f2.f3.f4.eB.f5.f6.f7.f8.eF.f9.g..g..g#.eH.ga.gb.gc.gd.ge.gf.gg.gh.gi.gj.gk.gl.gm.gn.go.gp.gq.gr.gs.gt.gu.gv.gw.gx.gy.gz.gA.gB.gC.gD.gE.gF.gG.gH.gI.gJ.gK.gL.gM.gN.gO.gP.gQ.gR.gS.gT.gU.gV.gW.gX.gY.gZ.g0.g1.g2.g3.b#.g4.g5.g6.g7.g8.g9.h..h#.ha.ha.ha.ha.ha.ha.ha.ha.#I.eh.hb.hc.hc.hb.eh.#I.hb.fI.#I.#D.#G.#D.#D.#E.fJ.#K.hd.#M.he.fM.hf.hg.hh.hi.hj.hk.hl.hm.hn.fU", +".hoQt#.hp.hq.hr.hs.ht.hu.hv.hw.hx.hy.eB.hz.hA.hB.hC.hD.f9.g..hE.g#.hF.hGQtvQtw.hH.ge.eM.hI.hJ.hK.hL.gk.dn.hM.hN.hO.hP.eX.hQ.hR.hS.hT.hU.hV.hW.hX.hY.hZ.h0.h1.h2.h3.h4.h5.h6.h7.h8.h9.i..i#.ia.ib.ic.id.ie.if.ig.ih.ii.ij.ik.il.im.in.io.ip.iq.ir.is.g4.it.iu.iv.iw.ix.fE.iy.iy.iz.iz.iz.iz.iz.iz.hb.hb.hb.hb.hb.hb.hb.hb.hc.hb.cH.#E.#D.#D.#D.#E.b#.#K.ba.cM.iA.iB.iC.iD.iE.iF.iG.iH.iI.iJ.#V.iK", +".iLQt#.iM.iN.hr.iO.ht.hu.iP.hv.iQ.iR.eB.iS.iT.iU.hC.hD.f9.hE.hE.iV.iW.hG.iX.iY.iZ.eL.i0.gg.i1.i2.i3.i4.i5.i6.eU.i7.i8.i9.j..j#.ja.jb.jc.jd.je.jf.jg.jh.ji.jj.jk.jl.jm.jn.jo.jp.jq.jr.js.jt.ju.jv.jw.jx.jy.jz.jA.jB.jC.jD.jE.jF.jG.jH.jIQt1.jJ.jK.jL.jM.jN.jO.jP.iw.jQ.jR.jS.jS.jS.jS.jT.jU.iz.iz.jV.hc.hb.eh.eh.hb.hc.jV.hc.hb.eh.#E.#D.#D.#D.#D.jW.fK.jX.jY.jZ.iB.iC.j0.j1.j2.j3.iI.bc.j4.fU.bl", +".j5.j6.j7.j8.j6.j9.k..k#.ka.kb.kc.kd.ke.kf.kg.kh.ki.kj.kk.kl.km.kl.kn.ko.kp.kq.kr.ks.kt.ku.kv.kw.kx.ky.kz.kA.kB.kB.kC.kD.kE.kF.kG.kH.kI.kJ.kK.kL.kM.kN.kO.kP.kQ.kR.kS.kT.kU.kV.kW.kX.kY.kZ.k0.k1.k2.k3.k4.k5.k6.k7.k8.k9.l..l#.la.lb.lc.ld.le.lf.lg.lh.li.lj.lk.ll.lm.ln.lo.lo.lo.lo.lo.lo.lo.lo.lp.lq.lq.lr.lr.ls.ls.lt.lu.lv.lw.lx.ly.lu.lz.lA.lp.lt.lB.lt.ls.lq.ls.lt.hc.lC.hb.hb.lD.lE.lF.lG", +".lH.lI.lJ.lK.lL.lM.lN.lO.lP.lQ.lR.lS.lT.lU.lV.lW.lX.lY.lZ.l0.l1.l2.l3.l4.l5.l6.l7.l8.l9.m..m#.ma.kx.mb.mc.md.me.mf.mg.mh.mi.mj.mk.ml.mm.mn.mo.mp.mq.mr.ms.mt.mu.mv.mw.mx.my.mz.mA.mB.mC.mD.mE.mF.mG.mH.mI.mJ.mK.mL.mM.mN.mO.mP.mQ.mR.mS.mT.mU.mV.mW.mX.mY.mZ.m0.m1.m2.m3.m4.m4.m4.m4.m4.m4.m4.m4.lp.lq.lq.lr.lr.ls.ls.lt.m5.lu.lv.lv.m6.m5.m7.m8.lq.ls.ls.m5.m6.m9.m9.lq.hc.lC.hb.hb.hc.lE.n..lG", +".n#.na.nb.nc.nd.ne.nf.ng.nh.ni.nj.nk.nl.nm.nn.no.np.nq.nr.ns.nt.nu.nv.nw.nx.ny.nz.nA.nB.m..nC.nD.nE.nF.nG.nH.nI.nJ.no.nK.nL.nM.nN.nO.nP.l#.nQ.nR.nS.nT.nU.nV.nW.nX.nY.nZ.n0.n1.n2.n3.n4.n5.n6.n7.n8.n9.o..o#.oa.ob.oc.od.oe.of.og.l#.l#.oh.ji.oi.oj.ok.ol.om.on.oo.op.oq.fH.fH.fH.fH.fH.fH.fH.fH.lp.lq.lq.lr.lr.ls.ls.lt.lz.or.m5.os.m5.lz.lA.ot.lt.ou.lr.lr.lr.lr.lp.ls.hc.lC.lC.lC.ov.lF.lG.ow", +".ox.oy.oz.oA.oB.oC.oD.oE.oF.oG.oH.oI.oJ.oK.oL.oM.oN.oO.oP.oQ.oR.oS.oT.oU.oV.oW.e3.oX.oY.oZ.o0.o1.o2.o3.o4.o5.o6.o7.o8.o9.p..p#.pa.pb.pc.pd.pe.pf.pg.ph.pi.pj.pk.nX.pl.pm.pn.po.pp.pq.pr.ps.pt.pu.pv.pw.px.py.pz.pA.pB.pB.pC.pD.pE.pF.pG.pH.pI.mo.mT.pJ.pK.pL.pM.pN.aU.pO.ef.ef.ef.ef.ef.ef.ef.ef.lp.lq.lq.lr.lr.ls.ls.lt.m7.m7.pP.pP.pP.m7.lA.lA.pQ.lt.lr.lr.lB.lB.lt.ls.hc.hc.lD.hc.lE.n..ow.pR", +".pS.pT.pU.pV.pW.pX.pY.pZ.p0.p1.p2.p3.p4.p5.p6.p7.p8.p9.q..q#.oM.q..qa.qb.qc.qd.qe.qf.qg.qh.qi.qj.qk.ql.qm.qn.qo.qp.qq.qr.qs.qt.qu.qv.qw.qx.qy.qz.qA.qB.qC.qD.qE.qF.qG.qH.qI.po.qJ.qK.pv.qL.qM.qN.qO.qP.qQ.qR.qS.qT.qU.qV.qW.qX.qY.qZ.q0.q1.q2.q3.q4.q5.q6.q7.q8.q9.r..r#.ef.ef.ef.ef.ef.ef.ef.ef.lp.lq.lq.lr.lr.ls.ls.lt.m7.m7.m7.lA.m7.m7.pP.lz.lt.ls.lq.lq.lr.lr.ls.m9.hc.hc.hc.ov.ra.lG.pR.rb", +".rc.rd.re.rf.rg.rh.ri.rj.rk.rl.rm.rn.ro.rp.rq.rr.rs.rs.rt.ru.rv.rw.rx.ry.rz.rA.rB.rC.rD.rE.rF.rG.rH.rI.rJ.rK.rL.rM.rN.rO.rP.rQ.rR.rS.rT.rU.rV.rW.rX.rY.rZ.r0.r1.r2.r3.r4.r5.r6.r7.r8.r9.q0.s..s#.sa.sb.sc.sd.se.sf.sg.sh.si.sj.sk.sl.sm.sn.so.gP.sp.l#.sq.sr.ss.st.su.sv.fH.fH.fH.fH.fH.fH.fH.fH.lp.lq.lq.lr.lr.ls.ls.lt.lz.pP.m7.lA.m7.lz.os.lu.sw.lt.lr.lr.ou.ou.lr.ls.hc.hc.hc.lE.lF.ow.rb.sx", +".sy.sz.sA.sB.sC.sD.sE.sF.sG.sH.sI.sJ.sK.sL.sM.sN.sO.sP.sQ.sR.sS.sT.sU.sV.sW.sX.sY.sZ.s0.s1.s2.s3.s4.s5.s6.s7.s8.s9.t..t#.ta.tb.tc.td.te.tf.tg.th.ti.tj.tk.tl.tm.tn.to.tp.tq.tr.ts.tt.tu.tv.tw.tx.ty.tz.tA.tB.tC.tD.tE.tF.tG.tH.tI.tJ.tK.tL.tM.tN.tO.tP.tQ.tR.tS.tT.tU.tV.m4.m4.m4.m4.m4.m4.m4.m4.lp.lq.lq.lr.lr.ls.ls.lt.os.or.pP.m7.lz.os.lv.lx.lB.pQ.pQ.tW.sw.lt.lt.lB.hc.hc.ov.lE.lG.tX.tY.tZ", +".t0.t1.t2.t2.t3.t4.t5.t6.t7.t8.t9.u..u#.ua.ub.uc.ud.ue.uf.ug.uh.ui.uj.uk.ul.um.un.uo.up.uq.ur.us.ut.uu.uv.uw.s8.ux.uy.t#.uz.uA.uB.uC.uD.uE.uF.uG.uH.uI.uJ.uK.uL.uM.uN.uO.uP.uQ.uR.uS.uT.uU.uV.uW.uX.uY.uZ.u0.u1.u2.u3.u4.u5.u6.u7.u8.u9.v..v#.va.vb.vc.vd.ve.vf.vg.vh.vi.lo.lo.lo.lo.lo.lo.lo.lo.lp.lq.lq.lr.lr.ls.ls.lt.lu.os.lz.lz.or.lu.lx.vj.ls.lt.lB.lt.ls.lq.ls.lt.hc.hc.ov.vk.lG.pR.sx.tZ", +".vl.vm.vn.vo.vp.vq.vr.vs.vt.vu.vv.vw.vx.vy.vz.vA.vB.vC.vD.vE.vF.vG.vH.vI.vJ.vK.vL.vM.vN.vO.vP.vQ.vR.vS.vT.vU.vV.vW.vX.vY.vZ.v0.v1.v2.v3.v4.v5.v6.v7.v8.v9.w..w#.wa.wb.wc.wd.we.wf.wg.wh.wi.wj.wk.wl.wm.wn.wo.wp.wq.wr.ws.wt.wu.wv.ww.wx.wy.wz.wA.wB.wC.wD.wE.wF.wG.wH.wI.wJ.cx.wK.wL.wM.wN.wO.wP.wQ.wR.iz.wS.wR.wT.wS.wU.wV.wV.wV.wV.wW.wW.wW.wW.wX.wY.wZ.w0.wZ.w0.w1.w2.w3.w4.w5.w6.w4.w7.w8.w9", +".x..x#.xa.xb.xc.xd.xe.xf.xg.xh.xi.xj.xk.xl.xm.xn.xo.xp.xq.xr.xs.xt.xu.xv.xw.xx.xy.xz.xA.xB.xC.xD.xE.xF.xG.xH.xI.xJ.xK.xL.xM.xN.xO.xP.xQ.xR.xS.xT.xU.xV.xW.xX.xY.xZ.x0.x1.x2.x3.x4.x5.x6.x7.x8.x9.y..y#.ya.yb.yc.yd.ye.x3.yf.yg.yh.yi.yj.yk.yl.ym.yn.yo.yp.yq.yr.ys.yt.yu.yv.yw.yx.yy.yz.yA.yB.yC.wQ.wS.iz.yD.yE.wR.wS.jS.ha.ha.wW.fF.yF.yG.yG.yH.yI.yJ.yK.yK.yL.yL.yM.yN.w4.w4.w4.w3.yO.yP.w8.yQ", +".yR.yS.yT.yU.yV.yW.yX.yY.yZ.y0.y1.y2.y3.y4.y5.y6.y7.y8.y9.z..z#.za.zb.zc.zd.ze.zf.zg.zh.zi.zj.zk.zl.zm.zn.zo.zp.zq.zr.zs.zt.zu.zv.zw.zx.zv.zy.zz.zA.zB.zC.zD.zE.zF.zG.zH.zI.zJ.zK.zL.zM.zN.zO.zP.zQ.zR.zS.zT.zU.zV.zW.zX.zY.zZ.z0.z1.z2.z3.z4.z5.z6.z7.z8.z9.A..A#.Aa.Ab.Ac.Ad.Ae.Af.Ag.Ah.Ai.iB.wQ.wR.wS.wS.wR.wR.wS.iz.ha.wV.fF.yG.Aj.Ak.Al.Am.w0.yL.w2.w2.yK.yK.An.Ao.w5.w4.yO.Ap.Aq.w8.w8.w8", +".Ar.As.At.Au.Av.Aw.Ax.Ay.Az.AA.AB.AC.AD.AE.AF.AG.AH.AI.AJ.AK.AL.AM.AN.AO.AP.AQ.AR.AS.AT.AU.AV.AW.AX.AY.AZ.A0.A1.A2.A3.A4.A5.A6.A7.A8.A9.B..B#.Ba.Bb.Bc.Bd.Be.Bf.Bg.Bh.Bi.Bj.Bk.Bl.qE.Bm.Bn.Bo.Bp.Bq.Br.Bs.Bt.Bu.Bv.Bw.Bx.By.Bz.BA.BB.BC.BD.BE.BF.BG.BH.BI.BJ.BK.BL.BM.BN.BO.BP.BQ.ou.BR.BS.BT.BU.BV.BW.BW.BW.BW.BW.wQ.wQ.Aj.BX.BX.Ak.Al.Am.BY.BZ.wZ.w1.yK.yK.w1.w1.yK.An.w6.w3.Ap.B0.w9.B1.B0.w8", +".B2.B3.B4.B5.B6.B3.B7.B8.B9.C..C#.Ca.Cb.Cc.Cd.Ce.Cf.Cg.Ch.Ci.Cj.Ck.Cl.Cm.Cn.Co.Cp.Cq.Cr.Cs.Ct.Cu.Cv.Cw.Cx.Cy.Cz.CA.CB.CC.CD.CE.CE.CF.CG.CH.CI.CE.CJ.CK.CL.CM.CN.CO.CP.CQ.CR.CS.CT.CU.CV.CW.CX.CY.CZ.C0.C1.C2.C3.C4.C5.C6.C7.C8.C9.D..D#.Da.Db.Dc.Dd.De.Df.Dg.Dh.Di.Dj.Dk.Dl.Dm.Dn.jU.Do.Dp.Dq.Dr.Ds.Dt.w2.Ds.yL.Du.Du.Dv.Dt.Dw.Dx.Dv.BZ.BZ.BY.Am.w1.yK.yM.w2.yL.yL.Dy.Dz.w4.yO.Aq.w9.DA.DA.w9.yQ", +".DB.DC.DD.DE.DF.DG.DH.DI.DJ.DK.DL.DM.DN.DO.DP.DQ.DR.DS.DT.DU.DV.DW.DX.DY.DZ.D0.D1.D2.D3.D4.D5.D6.D7.D8.D9.E..E#.Ea.Eb.Ec.Ed.Ee.Ef.Eg.Eh.Ei.Ej.Ek.El.Em.En.Eo.Ep.Eq.Er.Es.Et.Eu.Ev.Ew.Ex.Ey.Ez.EA.EB.EC.ED.EE.EF.EG.EH.EI.EJ.EK.EL.EM.EN.EO.EP.EQ.ER.ES.ET.EU.EV.EW.EX.EY.EZ.E0.E1.E2.E3.E4.E5.yL.E6.E7.E7.E7.E8.E9.E9.E6.F..F..F#.Fa.Dt.Dx.BZ.BZ.Fb.Fc.Fd.Fe.yN.Dz.Ao.Ff.w7.yP.w8.B1.DA.Fg.Fg.Fg", +"Qt..Fh.Fi.Fj.Fk.Fl.Fm.Fn.Fo.Fp.Fq.Fr.Fs.Ft.Fu.Fv.Fw.Fx.Fy.Fz.FA.FB.FC.FD.FE.FF.FG.FH.FI.FJ.FK.FL.FM.FN.FO.FP.FQ.FR.FS.FT.FU.FV.FW.FX.FY.FU.FZ.F0.F1.F2.F3.F4.F5.F6.F7.F8.F9.G..G#.Ga.Gb.Gc.Gd.Ge.Gf.Gg.Gh.Gi.Gj.Gk.Gl.Gm.Gn.Go.Gp.Gq.Gr.Gs.Gq.Gt.Gu.Gv.Gw.Gx.Gy.Gz.GA.GB.GC.GD.GE.GF.GG.GH.GI.Fb.E6.GJ.GK.GJ.E6.E9.E6.E7.GL.F..F..GM.F#.Fa.GN.GN.Fc.Fd.GO.Fd.Ao.Fb.Fc.Fd.w8.w8.w8.B0.w9.Fg.GP.GQ", +".GR.GS.GT.GU.GV.GW.GX.GY.GZ.G0.G1.G2.G3.G4.G5.G6.G7.G8.G9.H..H#.Ha.Hb.Hc.Hd.He.Hf.Hg.Hh.Hi.Hj.Hk.Hl.Hm.Hn.Ho.Hp.Hq.Hr.Hs.FU.Ht.Hu.Hv.Hw.Hx.Hy.Hz.HA.HB.HC.HD.HE.HF.HG.HH.HI.HJ.HK.HL.HM.HN.HO.HP.HQ.HR.HS.HT.HU.HV.HW.HX.HY.HZ.F0.H0.H1.H2.H3.H4.H5.F0.H6.H7.H8.H9.I..I#.Ia.Ib.Ic.Id.Ie.If.GI.yK.E6.E7.GK.E7.E9.Dt.E8.E7.F..F..F..F..F..F..F..F..Dz.Ao.Fe.Ao.An.w2.An.yN.w9.yQ.w8.w8.yQ.Fg.GQ.Ig", +".Ih.Ii.bw.Ij.Ik.Il.Ih.Im.In.Io.Ip.Iq.Ir.Is.It.Iu.Iv.Iw.Ix.Iy.Iz.IA.IB.IC.ID.IE.IF.IG.zo.IH.II.IJ.IK.IL.Hn.IM.IN.IO.IP.IQ.IR.IS.IT.IU.IV.IW.IX.IY.IZ.I0.I1.I2.I3.I4.I5.HF.I6.I7.I8.I8.I9.J..J#.I7.Ja.Ja.Jb.Gb.Jc.Jd.Je.Je.Jf.Jg.Jh.Ji.Jj.Jk.Jl.Jm.Jn.Jo.Jp.Jq.Jr.Js.Jt.Ju.Jv.Jw.Jx.Jy.Jz.JA.JB.JC.JD.JD.JD.JD.JE.JF.JF.JF.JG.JH.JH.JH.JI.JJ.JJ.JJ.JK.JK.JL.JL.JM.JN.JO.JP.JQ.JR.JS.JS.JT.JU.JU.JV", +".Io.JW.bw.JX.Ik.Il.Io.Im.JY.JZ.J0.J1.J2.J3.f7.J4.J5.J6.Ix.J7.J8Qte.J9.K..K#.Ka.Kb.Kc.Kd.Ke.Kf.Kg.Kh.Ki.yn.Kj.Kk.IO.Kl.Km.Kn.Ko.Kp.Kq.Kr.Ks.Kt.Ku.Kv.Kw.Kx.Jk.Ky.Kz.Kw.KA.KB.KC.KD.KB.KE.KF.KG.KB.KH.KH.KI.KJ.KK.ES.Gl.ER.KL.KM.KN.KO.KP.KQ.KR.KS.KT.KU.KV.KW.F5.KX.KY.KZ.K0.K1.K2.K3.K4.K5.K6.K7.K8.K8.K9.L..JD.JD.L#.JE.La.Lb.Lb.Lb.JG.JH.JH.JH.JO.JN.JN.JN.JN.JN.JN.JM.JU.JR.JS.Lc.JT.JU.JU.JV", +".Io.Ld.Le.Lf.Lg.Lh.Li.Lj.Lk.Ll.J0.iP.Lm.Ln.Lo.Lp.Lq.Lr.Ls.Lt.Lu.Lv.Lw.Lx.Ly.Lz.LA.LB.LC.LD.LE.LF.LG.LH.LI.LJ.Jq.IO.I0.Km.LK.LL.LM.LN.LO.LP.F0.F0.LQ.LR.LS.LQ.LT.LQ.LU.LV.LW.LX.LY.LZ.LY.LY.L0.L1.L2.L3.L4.L5.L6.L7.L8.L8.L9.FT.M..M#.Ma.I9.Mb.Mc.Md.Me.Mf.Mg.KD.Jk.Mh.Mi.Mj.Mk.Ml.Mm.Mn.Mo.Mp.eq.Mq.Mq.Mq.Mr.K8.K8.K8.K8.Ms.Ms.Mt.Mt.Mu.Mu.La.La.Mv.Mw.Mw.Mx.JP.JN.JN.My.Mz.JT.MA.MA.JT.Mz.JU.Mz", +".MB.MC.MD.ME.lX.MF.MG.MH.MI.Ll.Ip.MJ.MK.ML.MM.MN.MO.Lr.Ix.J7.J8Qte.Lw.MP.MQ.MR.MS.MT.MU.MV.MW.MX.MY.MZ.M0.M1.M2.IO.I2.Km.H4.M3.M4.M5.M6.M6.M7.M8.M9.N..N#.Na.Nb.Nc.Nb.Nb.Nd.Ne.Nf.tj.Ng.Nh.Nf.Nf.Ni.Ni.Nj.Nk.Nl.Nm.Nm.Nn.No.Ni.Np.Nq.Nr.Ns.Nt.Nu.Jf.Gq.Nv.Nw.Nx.Ny.Nz.NA.NB.NC.ND.NE.NF.NG.NH.NI.NJ.NK.NK.NL.NL.NM.NM.Mq.NN.NN.NO.NO.NP.NP.Ms.Ms.NQ.NQ.NR.Mv.Mw.Mw.NS.NS.JU.JV.JT.JR.NT.Mz.NU.NV", +".MG.NW.NX.NY.NZ.N0.N1.N2.N3.N4.N5.N6.N7.N8.N9.J4.O..O#.Oa.Ob.Oc.bq.J9.K..Od.Oe.Of.Og.Oh.Oi.Oj.Ok.Ol.MZ.Om.On.Oo.Op.H9.L0.Oq.Or.zv.Os.Ot.Ou.Ov.Ow.Ox.Oy.Oz.OA.OB.OC.OD.OE.OF.OG.OH.OI.OJ.OK.OL.OM.ON.OO.OP.OQ.OR.OS.OT.OU.OV.OW.OX.OY.OZ.O0.O1.O2.O3.O4.O5.O6.O7.O8.O9.P..P#.Pa.Pb.Pc.Pd.Pe.Pf.Pg.Ph.Ph.Ph.Ph.Pi.Pj.Pj.Pj.Pk.Pk.Pl.Pl.Pl.NN.NN.NO.NQ.NQ.NQ.Pm.Pn.Pn.Po.Po.NV.NU.NU.JU.JU.NU.Pp.Pq", +".Pr.NW.Ps.Pt.Pu.Pv.Pw.Px.Py.Pz.PA.PB.PC.PD.PEQth.PF.O#.PG.PH.PI.PJ.J9.PK.PL.PM.PN.PO.oS.PP.PQ.PR.PS.PT.LI.PU.Kr.PV.H9.PW.PX.PY.PZ.Na.P0.P1.P2.P3.P4.P5.P6.P7.P8.P9.Q..Q#.Qa.Qb.Qc.Qd.Qe.Qf.Qg.Qh.Qi.Qj.Qk.Ql.Qm.Qn.Qo.Qp.Qq.Qr.Qs.Qt.Qu.Qv.Qw.Ox.N#.Qx.Qy.Qz.QA.QB.QC.QD.QE.QF.QG.QH.QI.QJ.QK.QL.QM.QM.QN.QO.QO.QO.QP.QP.QQ.QQ.QQ.Pk.Pl.Pl.Pl.NN.Pm.QR.Pn.QS.QT.QU.QV.QW.QX.QX.QX.Pp.QY.Pp.QZ.Q0", +".Q1.Q2.Q3.Q4.Q5.Pv.Q6.Q7.Q8.Q9.R..MJ.R#.Ra.Rb.#3.Rc.Rd.Oa.Ob.rB.bq.Re.MP.Rf.Rg.Rh.Ri.Rj.Rk.Rl.Rm.Rn.Ro.Rp.Rq.Rr.PV.I2.PW.Rs.Rt.Ru.Rv.Rw.Rx.Ry.Rz.RA.RB.RC.RD.RE.RF.RG.RH.RI.RJ.RK.RJ.RL.RM.RN.RO.RP.RQ.RR.RS.RT.RU.RV.RW.RX.RY.RZ.R0.R1.R2.R3.R4.R5.R6.M6.R7.R8.R9.S..S#.Sa.Sb.Sc.Sd.Se.Sf.Sg.Sh.Si.Si.Sj.Sk.QM.QM.QM.Sl.QQ.QQ.QP.Pk.Pl.Pl.Pl.NN.Sm.QU.QU.QV.QV.QV.QV.QV.QZ.Sn.Q0.So.Pq.QZ.Sp.Sq", +".Sr.Ss.Q3.Q4.St.Su.Sv.Sw.Sx.Io.Sy.Sz.SA.c5.SB.SC.SDQto.SE.SF.SG.bq.SH.SI.f3.SJ.SK.SL.SM.SN.SO.SP.SQ.SR.SS.ST.SU.SV.I2.PW.SW.SX.SY.SZ.S0.S1.S2.S3.S4.S5.S6.S7.S8.S9.T..T#.Ta.Tb.Tc.Td.Te.Tf.Tg.Th.Ti.Tj.Tk.Tl.Tm.Tn.To.Tp.Tq.Tr.Ts.Tt.Tu.Tv.Tw.Tx.Ty.Tz.TA.TB.TC.TD.TE.TF.TG.TH.TI.TJ.TK.TL.TM.TN.TO.TP.Si.Si.Si.Sk.Sk.QM.QQ.QQ.Pk.Pk.Pl.Pl.NN.NN.TQ.TQ.TR.TS.QV.QV.Sm.QT.Sn.TT.TU.TT.So.TV.TW.TX", +".TY.TZ.T0.T1.T2.T3.T4.T5.T6.T7.T8.T9.U..U#.Ua.Ub.Uc.nH.Ud.Ue.Uf.Ug.Uh.Ui.Uj.Uk.Ul.Um.Un.Uo.Up.Uq.Ur.Us.Ut.Uu.Uv.Uw.Ux.Uy.Uz.UA.UB.UC.UD.UE.UF.UG.UH.UI.UJ.UK.UL.UM.UN.UO.UP.UQ.UR.US.UT.UU.UV.UP.UW.UX.UY.UZ.U0.U1.U2.U3.U4.U5.U6.U7.U8.U9.V..V#.Va.Vb.Vc.Vd.Ve.Vf.Vg.Vh.Vi.Vj.Vk.Vl.Vm.Vn.Vo.Vp.Vq.Vr.Vs.Vr.Vq.Vq.Vt.Vs.Vu.Vv.Vw.Vv.Vu.Vx.Vu.Vv.Vy.Vz.VA.VB.VC.VD.VE.VF.VG.VH.VH.VH.VI.VJ.VK.VL", +".VM.VN.VO.VP.VQ.VR.VS.VT.VU.VV.VW.VX.VY.VZ.V0.V1.V2.V3.V4.dy.V5.V6.V7.V8.V9.GU.W..W#.Wa.Wb.Wc.Wd.We.Wf.Wg.Wh.Wi.Wj.Wk.Wl.Wm.Wn.Wo.Wp.Wq.Wr.Ws.Wt.Wu.Wv.Ww.Wx.Wy.Wz.WA.WB.WC.WD.WE.WF.WG.WH.WI.WJ.WK.WL.WM.WN.WO.WP.WQ.WR.WS.WT.WU.WV.WW.WX.WY.WZ.W0.W1.W2.W3.W4.Nb.W5.Jj.W6.W7.W8.W9.X..X#.Xa.Xb.Xc.Vt.Xd.Vr.Vq.Xe.Vq.Vr.Xf.Vt.Xg.Xg.Vt.Vt.Xg.Xh.Xi.Xi.Xi.Xj.Vy.Vy.Vy.Vy.VI.VI.Xk.Xk.Xl.Xl.Xl.Xl", +".Xm.Xn.Xo.Xp.Xq.Xr.Xs.Xt.Xu.Xv.Xw.Xx.nh.bw.Xy.Xz.XA.XB.XC.XD.XE.XF.XG.XH.MB.Ld.XI.XJ.XK.XL.XM.XN.XO.XP.Ov.XQ.XR.XS.XT.XU.XV.XW.XX.XY.XZ.X0.X1.X2.X3.X4.X5.X5.X6.X7.X8.X9.Y..Y#.Ya.Yb.Ya.Yc.Yd.Y..Ye.Yf.Yg.Yh.Yi.Yj.Yk.Yl.Ym.Yn.Yo.Yp.Yq.Yr.Ys.Yt.Yu.Yv.Yw.Yx.Yy.Yz.YA.YB.YC.YD.YE.YF.YG.YH.YI.YJ.Xe.Vq.Vr.Vq.Xe.YK.Xe.Vt.YL.YM.YN.Vx.YN.Vx.Xg.Xh.YO.YP.YP.YQ.YQ.YR.YR.YS.Xk.VJ.YT.YU.YU.YT.VJ.TO", +".YV.YW.YX.YY.YZ.Y0.Y1.Y2.Y3.Y4.Y5.Y6.Y7.Y8.Y9.Z..Z#.Za.Zb.Zc.Zd.Ze.Zf.Zg.hr.Zh.Zi.Zj.Zk.Zl.Zm.Zn.Zo.Zp.Zq.Zr.Wj.Zs.Zt.Zu.Zv.Zw.Zx.Zy.Zz.ZA.ZB.ZC.ZD.ZE.ZF.ZG.ZH.ZI.ZF.ZJ.ZK.ZL.ZM.ZN.ZO.ZO.ZP.ZQ.ZR.ZS.ZT.ZU.ZV.ZW.ZX.ZY.ZZ.Z0.Z1.Z1.Z2.Z3.Z4.Z5.Z6.Z7.Z8.Z9.0..0#.0a.0b.0c.0d.0e.0f.0g.0h.0i.0j.0k.Xe.Vq.Xe.0k.0l.0k.0m.0n.YL.YK.YK.YM.YM.Xf.0o.0p.0p.0p.0p.0q.0q.0q.0q.Xl.0r.0s.YU.0t.YU.0s.YT", +".0u.0v.0w.0x.0y.0z.0A.0B.0C.0D.0E.0F.0G.0H.0I.0J.0K.0L.oW.0M.0N.0O.0P.0Q.ev.0R.0S.0T.0U.0V.0W.0X.0Y.0Z.00.01.02.03.04.05.06.07.08.09.1..1#.1a.1b.1c.1d.1e.1f.1f.1g.1h.1i.1j.1k.1l.1m.1n.1o.1p.1q.1r.1s.1t.1u.1v.1w.1x.1y.1z.1A.1B.1C.1D.1E.1F.1G.1H.1I.1J.1K.1L.1M.1N.1O.1P.1Q.1R.1S.1T.1U.1V.1W.1X.0k.YK.0k.1X.1Y.1X.0k.0n.YL.1Z.YL.0n.10.0n.0l.11.12.13.14.0q.15.YS.YS.0s.YT.YT.0s.0t.16.17.18", +".19.2..2#.2a.2b.2c.2d.2e.2f.2g.2h.2i.2j.2k.2l.2m.2n.2o.2p.2q.2r.2s.2t.2u.2v.2w.2x.2y.2z.2A.2B.2C.2D.2E.2F.2G.2H.2I.2J.2K.2L.2M.2N.2O.2P.2Q.2R.2S.2T.2U.2V.2W.2X.2Y.2Z.20.21.22.23.24.25.26.27.28.U0.29.3..3#.3a.3b.3c.3d.3e.3f.3g.3h.3i.3j.3k.3l.3m.3n.3o.3p.3q.3r.3s.3t.3u.3v.3w.3x.3y.3z.3A.3B.10.1X.0l.1X.3C.3D.3C.1X.3E.0n.3F.3G.3H.3I.3J.3E.3K.3L.11.13.3M.0p.YS.YR.3N.0t.YU.YU.3N.17.3O.3P", +".3Q.3R.3S.3T.3U.3V.3W.3X.i2.3Y.3Z.30.31.32.33.34.35.36.37.38.39.4..4#.4a.4b.4c.4d.4e.4f.4g.4h.4i.4j.4k.FO.4l.A9.4m.4n.4o.4p.4q.4r.4s.4t.4u.4v.4w.4x.4y.4z.4A.4B.4z.4C.4D.4E.4F.4G.4H.4I.4J.4K.4L.4M.4N.4O.4P.4Q.4R.4S.4T.4U.4V.4W.4X.4Y.4Z.40.41.42.43.44.45.46.47.48.49.5..5#.5a.5b.5c.YM.5d.5e.5f.3C.1X.3C.5f.5f.10.3F.5g.3I.5h.3H.5i.5j.3J.3E.3K.3K.3L.11.11.5k.13.13.16.16.16.5l.17.18.5m.3O", +".5n.5o.5p.5q.5r.5s.5t.5u.5v.5w.5x.5y.5z.5A.5B.5C.5D.5E.5F.5G.5H.5I.5J.5K.5L.5MQtI.5N.5O.5P.5Q.5R.5S.5T.5U.5V.5W.5X.5Y.5Z.50.51.52.53.54.55.56.57.58.59.6..6..6..6#.6a.6b.6c.6d.6e.6f.6g.6h.6i.6j.6k.6l.6m.6n.6o.6p.6q.6r.6s.6t.6u.6v.6w.6x.6y.6z.6A.6B.6C.6D.6E.6F.6G.6H.6I.6J.6K.6L.6M.6N.6O.6P.6Q.10.3C.3C.5f.6Q.5f.3C.6R.6S.5j.5i.5j.5j.5h.0n.3L.3L.3K.3K.3K.6T.6U.6U.16.17.18.3O.3O.18.17.6V", +".6W.6X.6Y.6Z.60.61.62.63.64.65.66.67.68.69.7..7#.7a.7b.7c.7d.7e.7f.7g.7h.7i.7j.7k.zr.7l.7m.7n.7o.7p.7q.7r.7s.7t.7u.7v.7w.7x.7y.7z.7A.7B.7C.7D.7E.7F.7G.7H.7I.7J.7K.7L.7M.7N.7O.7P.7Q.7R.7S.7T.7U.7V.7W.7X.7Y.7Z.70.71.72.73.74.75.76.77.78.79.8..8#.8a.8b.8c.8d.8e.8f.8g.8h.8i.8j.8k.8l.8m.8n.8o.8p.8p.8p.8p.8p.8p.8p.8p.8q.8q.8r.8s.8s.8r.8q.8t.8u.8u.8u.8v.8w.8x.8y.8y.8z.8A.8B.8C.8D.8E.8F.8G", +".8H.8I.8J.8K.8L.8M.8N.8O.8P.8Q.8R.8S.8T.8U.8V.8W.8X.8Y.T8.8Z.80.81.82.83.84.85.86.87.88.89.9..9#.9a.9b.9c.9d.9e.9f.9g.9h.9i.9j.9k.9l.9m.9n.9o.9p.9q.9r.9s.9t.9u.9v.9w.9x.9y.9z.9A.9B.9C.9D.9E.9F.9G.9H.9I.9J.9K.9L.9M.9N.9O.9P.9Q.9R.9S.9T.9U.9V.9W.9X.9Y.9Z.90.91.92.93.94.95.96.97.98.99#..#.#.8t.8t.8t.8t.8t.8t.8t.8t#.a.8r#.b#.c#.c.8r.8q#.d.8x#.e#.e.8w.8w.8w.8w.8w.8E#.f#.f#.e.8B.8A.8z#.g", +"#.h#.i#.j#.k#.l#.m#.n#.o#.p#.q#.r#.s#.t#.u#.v#.w.SK.7b#.x#.y#.z#.A.82#.B#.C#.D.86#.E#.F.89#.G#.H#.I#.J#.K#.L#.M#.N#.O#.P#.Q#.R#.S#.T.7r#.U#.V#.W#.X#.Y#.Z#.0#.1#.2#.3#.4#.5#.6#.7#.8#.9##.#####a##b##c##d##e##f##g##h##i##j##k##l##m##n##o##p##q##r##s##t##u##v##w##x##y##z##A##B##C##D##E##F##G.8r.8r.8r.8r.8r.8r.8r.8r#.c#.c##H##H##H#.c.8r.8q##I##J##J.8y.8y.8w.8w.8v.8D#.e.8C.8B.8z#.g##K##K", +"##L##M##N##O##P##Q##R##S##T##U##V##W##X##Y##Z##0##1##2.NW##3##4##5##6##7##8##9#a.#a##aa#ab#ac#ad#ae#af#ag#ah#ai#aj#ak#al#am#an#ao#ap#aq#ar#as#at#au#av#aw#ax#ay#az#aA#aB#aC#aD#aE#aF#aG#aH#aI#aJ#aK#aL#aM#aN#aO#aP#aQ#aR#aS#aT#aU#aV#aW#aX#aY#aZ#a0#a1#a2#a3#a4#a5#a6#a7#a8#a9#b.#b##ba#bb#bc#bd##H##H##H##H##H##H##H##H#be#bf#bf#bg#bf##H#.c.8r#bh#bh#bi##J##J#bj.8y.8y#bk#bk#bl##K#.g.8z.8A.8B", +"#bm#bn#bo#bp#bq#br#bs#bt#bu#bv#bw#bx#by#bz#bA#bB.Ud#bC#bD#bE#bF#bG#bH#bI#bJ#bK#bL#bM#bN#bO#bP#bQ#bR#bS#bT#bU#bV#bW#bX#bY#bZ#b0#b1#b2#b3#b4#b5#b6#b7#b8#b9#c.#c##ca#cb#cc#cd#ce#cf#cg#ch#ci#cj#ck#cl#cm#cn#co#cp#cq#cr#cs#ct#cu#cv#cw#cx#cy#cz#cA#cB#cC#cD#cE#cF#cG#cH#cI#cJ#cK#cL#cM#cN#cO#cP#cQ#cR#cR#cR#cR#cR#cR#cR#cR#cR#cS#cT#cT#cR#bf##H#.c##J##I#bh#bh#bh#cU#cV#cV#cW#cW#cX#bk#cY##K##I#.g", +"#cZ#c0#c1.dg#c2#c3#c4#c5#c6#c7#c8#c9#d.#d##da#db.7a#dc#dd#de#df#dg#dh#di#dj#dk#ab#dl#dm#a.#dn#.H#do#dp#dq#dr#ds#dt#du#dv#dw#dx#dy#dz#dA#dB#dC#dD#dE#dF#dG#dH#dI#dJ#dK#dL#dM#dN#dO#dP#dQ#dR#dS#dT#dU#dV#dW#dX#dY#dZ#d0#d1#d2#d3#d4#d5#d6#d7#d8#d9#e.#e##ea#eb#ec#ed#ee#ef#eg#eh#ei#ej#ek#el#em#en#cT#cT#cT#cT#cT#cT#cT#cT#eo#eo#ep#eo#eq#cR#bf##H#bh#bh#bh#cV#cX#er#er#es#bl#cY#bk#cX#cW#et#eu#ev", +"#ew#ex#ey#ez#eA#eB#eC#eD#eE#eF#eG#eH#eI#eJ#eK#eL#eM#eN.oG#eO#eP#eQ#eR#eS#eT#eU.yY#eV#eW#eX#eY#eZ#e0#e1#e2#e3#e4#e5#e6#e7#aY#e8#e9#f.#f##fa#fb#fc#fd#fe#ff#fg#fh#fi#fj#fk#fl#fm#fn#fo#fp#fq#fr.To#fs#ft#fu#fv#fw#fx#fy#fz#fA#fB#fC#fD#fE#fF#fG#fH#fI#fJ#fK#fL#fM#fN#fO#fP#fQ#fR#fS#fT#fU#fV#fW#fX#eo#eo#eo#eo#eo#eo#eo#eo#fY#fZ#f0#fY#eo#cT#bg#be#er#er#er#er#er#er#er#er#f1#f1#cW#f2#eu#ev#f3#f3", +"#f4#f5#f6#f7#f8#f9#g.#g##ga#gb#gc#gd#ge#gf.gv#gg#gh.c4#gi#gj#gk#gl#gm#gn#go#gp#gq#gr#dl#ab#gs#gt#gu#gv#gw#gx#gy#gz#gA#gB#gC#gD#gE#gF#gG#gH#gI#gJ#gK#gL#gM#gN#gO#gP#gQ#gR#gS#gT#gU#gV#gW#gX#gY#gZ#g0#g1#g2#g3#g4#g5#g6#g7#g8#g9#h.#h##ha#hb#hc#hd#he#hf#hg#hh#hi#hj#hk#hl#hm#hn#ho#hp#hq#hr#hs#ht#eo#eo#eo#eo#eo#eo#eo#eo#hu#hu#hu#hu#fY#cT#cR#bf#hv#hw#hw#es#er#er#cV#cV#hx#hx#f3#ev#eu#f2#cW#cX", +"#hy#hz#hA#hB#hC#hD#hE#hF#hG#hH#hI#hJ#hK#hL.XE.DF.p1#hM#hN#hO#hP#hQ#hR.uj#hS#hT#hU#hV#hW#hX#hY#hZ#h0#h1#h2#h3#h4#h5#h6#h7#h8#h9#i.#i##ia#ib#ic#id#ie#if#ig#ih#ii#ij#ik#il#im#in#io#ip#iq#ir#is#it#iu#iv#iw#ix#iy#iz#iA#iB#iC#iD#iE#iF#iG#iH#iI#iJ#iK#iL#iM#iN#iO#iP#iQ#iR#iS#iT#iU#iV#iW#iX#iY#iZ#i0#i1#i2#i2#i3#i0#i3#i2#i3#i2#i3#i4#i4#i2#i5#i2#i6#i7#i8#i9#j.#j##ja#jb#jc#jd#je#je#jf#es#jg#jg", +"#hy#hz#hA#jh#ji#jj#jk#jl#jm#jn#jo#jp#jq#jr#js.V9#jt#ju.ux#jv#hP#hQ#hR.uj#jw.IF#jx#jy#jz#jA#jB#jC#jD#jE#jF#jG#jH#jI#jJ#jK#jL#jM#jN#jO#jP#jQ#jR#jS#jT#jU#jV#jW#jX#jY#jZ#j0#j1#j2#j3#j4#j5#j6#j7#j8#j9#k.#k##ka#kb#kc#kd#iA#ke#g5#kf#kg#kh#ki#kj#kk#kl#km#kn#ko#kp#kq#kr#ks#kt#ku#kv#kw#kx#ky#kz#kA#i0#i3#i2#i2#kB#i0#i3#i2#i4#i3#i2#i0#i3#kC#i2#i3#i6#kD#i8#i9#kE#j##kF#ja#je#je#je#jf#jg#jg#jg#jg", +"#kG.MG#kH#kI#kJ#hG#kK#kL#kM#kN#kO#kP#kQ#kR#kS#kT#kU#kV#kW#kX#hP#kY#hR.uj#kZ#k0.Lk#k1#k2#k3#k4#k5#k6#k7#k8#k9#l.#l##la#lb#lc#ld#le#lf#lg#lh#li#lj#lk#ll#lm#ln#j2#lo#jZ#lp#lq#lr#ls#lt#lu#lv#lw#lx#ly#lz#lA#lB#lC#lD#lE#lF#lG#lH#lI#lJ#lK#lL#lM#lN#lO#lP#lQ#lR#lS#lT#lU#lV#lW#lX#lY#lZ#l0#l1#l2#l3#i0#i3#i2#i3#i0#i0#l4#i2#l5#i0#i2#i3#i2#i5#i1#i4#i6#kD#i7#i8#kE#kE#j.#j##jf#jf#jg#jg#jg#l6#l7#l7", +"#l8.MG#l9#m.#m##ma#mb#mc#hy#kN#md#me#mf#m.#mg#mh#mi#mj#mk#ml#hP#mm#mn.uj#mo#mp#mq#mr#ms#mt#mu#mv#mw#mx#my#mz#mA#mB#mC#mD#mE#mF#mG#mH#mI#mJ#mK#mL#mM#mN#mO#mP#mQ#mR#mS#mT#mU#mV#mW#mX#mY#mZ#m0#m1#m2#m3#m4#m5#m6#m7#m8#m9#n.#n##na#nb#nc#nd#jV#ne#nf#ng#nh#ni#nj#nk#nl#nm#nn#no#np#nq#nr#ns#nt#nu#i4#i0#i3#i3#i0#i4#i0#i3#l5#i0#i3#i0#i0#i2#i3#i4#i6#i6#i7#i8#i8#i9#kE#kE#jg#jg#l6#l7#l7#l7#nv#nv", +"#nw#nx#ny#hL#nz#nA#nB#nC#nD#nE.Uj#nF#jh#nF#nG#nH#nI#nJ#nK#nL#hP#nM#nN.uj#nO#nP#nQ#nR#nS#nT#nU#nV#nW#nX#nY#nZ#n0#n1#n2#n3#n4#n5#n6#n7#n8#n9#o.#o##oa#ob#oc#od#oe#of#og#oh#oi#oj#ok#ol#om#on#oo#op#oq#or#os#ot#ou#ov#ow#ox#oy#oz#oA#oB#oC#oD#oE#oF#oG#oH#oI#oJ#oK#oL#oM#oN#oO#oP#oQ#oR#oS#oT#oU#oV#i4#i0#i3#i0#i4#i4#i4#kB#i4#i0#i0#l5#l5#i3#oW#i0#i6#i6#i6#kD#i7#i7#i8#i8#l7#l7#l7#nv#iY#iY#iY#oX", +"#oY#jo#ny#oZ#o0#o1#o2#o3#o4#nE#jo#nF#o5#jp#o6#o7#o8.G0#o9#p.#hP#p##nN.uj#pa#pb#pc#pd#pe#pf#pg#ph#pi#pj#pk#pl#pm#pn#po#pp#pq#pr#ps#pt#pu#pv#pw#px#py#pz#pA#pB#pC#pD#pE#pF#pG#pH#pI#pJ#pK#pL.WN#pM#pN#pO#pP#pQ#pR#pS#pT#pU#pV#pW#pX#pY#pZ#p0#p1#p2#p3#p4#p5#p6#p7#p8#p9#q.#q##qa#qb#qc#qd#qe#qf#qg#l5#i4#i0#i0#i4#l5#i4#i0#i4#i0#i4#qh#qh#i0#i3#kB#i6#i6#i6#i6#i6#i6#i6#i6#iY#iY#iY#iY#qi#qj#qj#qj", +"#qk#jo#ny#ql#qm#qn#qo#qp#qq#hH#kO#kI#kQ#hJ#qr#qs#qt#qu#qv#qw#hP#qx#qy#qz#qA#qB#qC#qD.5J#qE#qF#qG#qH#qI#qJ#qK#qL#qM#qN#qO#qP#qQ#qR#qS#qT#qU#qV#qW#qX#qY#qZ#q0#q1#q2#q3#q4#q5#q6#q7#q8#q9#r.#r##ra#rb#rc#rd#re#rf#rg#rh#ri#rj#rk#rl#rm#rn#ro#rp#rq#rr#rs#rt#ru#rv#rw#rx#ry#rz#rA#rB#rC#rD#rE#rF#rG#l5#i4#i0#i4#l5#l5#i4#i0#rH#i4#i4#qh#l5#i0#i0#i4#i6#i6#rI#rI#rI#rJ#rK#rK#oX#oX#qj#qj#qj#rL#rM#rM", +"#qk#jo#ny#ql#rN#rO#rP#rQ#rR#jn#hI.lQ#rS#kI.MS#rT#rU#rV#qv#rW#hP#rX#rY#qz#rZ#r0#r1#r2#r3#r4#r5#r6#pi#r7#r8#r9#s.#s##sa#sb#sc#sd#se#sf#sg#sh#si#sj#sk#sl#sm#sn#so#sp#sq#sr#ss#st#su#sv#sw#sx#sy#sz#sA#sB#sC#sD#sE#sF#sG#sH#sI#sJ#sK#sL#sM#sN#sO#sP#sQ#sR#sS#sT#sU#sV#sW#sX#sY#sZ#s0#s1#s2#s3#s4#s5#l5#i4#i0#i4#l5#rH#l5#i4#s6#l5#i0#i4#i4#i0#i4#qh#i6#i6#rI#rJ#rK#rK#rK#rK#qj#qj#qj#qj#rL#rM#rM#rM", +"#s7#s8#s9#t.#t##ta#tb#tc#td.qa#te#tf#tg#th#ti#tj#tk#tl#tm#tn#to#tp#tq#tr#ts#tt#tu#tv#tw#tx#ty#tz#tA#tB#tC#tD#tE#tF#tG#tH#tI#tJ#tK#tL#tM#tN#tO#tP#tQ#tR#tS#tT#tU#tV#tW#tX#tY#tZ#t0#t1#t2#t3#t4#t5#t6#t7#t8#t9#u.#u##ua#ub#uc#ud#ue#uf#ug#uh#ui#uj#uk#ul#um#tN#un#uo#up#uq#ur#us#ut#uu#uv#uw#ux#uy#uz#uA#uB#uC#uz#uD#oU#uC#uB#uA#uz#uE#nr#nr#nr#uE#uF#uG#uH#uI#uJ#uK#uL#uH#uM#uN#uO#uP#uQ#uR#uS#uT", +"#uU.8W#uV#uW#uX#uY#uZ.HF#u0#u1#u2#u3#u4#u5#u6#u7#u8#u9#v.#v##va#vb#vc#vd#ve#vf#vg#vh#vi#vj#vk#vl#vm#vn#vo#vp#vq#vr#vs#vt#vu#vv#vw#vx#vy#vz#vA#vB#vC#vD#vE#vF#vG#vH#vI#vJ#vK#vL#vM#vN#vO#vP#vQ#vR#vS#vT#vU#vV#vW#vX#vY#vZ#v0#v1#v2#v3#v4#uh#v5#v6#v7#v8#v9#w.#w##wa#wb#wc#wd#we#wf#wg#wh#wi#wj#wk#uz#uA#uC#uA#uz#wl#oU#uC#uC#oU#wl#nr#wm#wm#wm#nr#uF#uG#uH#uI#uJ#uK#uL#uH#uM#uM#uN#uP#wn#wo#uR#uS", +"#wp#wq#wr#ws#wt#wu#wv#ww#wx#wy#wz#wA#wB#wC#wD#wE#wF#wG#wH#wI#wJ#wK#wL#wM#wN#wO#wP#wQ#wR#wS#wT#wU#wV#wW#wX#wY#wZ#w0#w1#w2#w3#w4#w5#w6#w7#w8#w9#x.#x##xa#xb#xc#xd#xe#xf#xg#xh#xi#xj#xk#xl#xm#xn#xo#xp#xq#xr#xs#xt#xu#xv#xw#xx#xy#xz#xA#xB#xC#v5#xD#xE#xF#xG#xH#xI#xJ#xK#xL#xM#xN#xO#xP#xQ#xR#xS#xT#wl#oU#uC#uA#uz#wl#oU#uC#oU#wl#uE#wm#xU#xU#wm#nr#uF#uG#uH#uI#uJ#uK#uL#uH#xV#xW#uM#uO#uP#uQ#wo#uR", +"#xX#xY#xZ#x0#x1#x2#x3#x4#x5#x6.zo#x7#x8.Zd#x9#y.#y##ya#yb#yc#yd.zs#ye#yf#yg#yh#yi#yj#yk#yl#ym#yn#yo#yp#yq#tD#yr#ys#yt#yu#yv#yw#yx#yy#yz#yA#yB#yC#yD#yE#yF#yG#yH#yI#yJ#yK#yL#yM#yN#yO#xl#yP#yQ#yR#yS#yT#yU#yV#yW#yX#yY#yZ#y0#y1#y2#y3#y4#y5#y6#y7#y8#y9#z.#z##za#zb#zc#zd#ze#zf#uE#zg#oU#zh#zh#zi#wl#oU#uA#uA#wl#zj#uz#uA#uD#zj#nr#xU#zk#xU#wm#nr#uF#uG#uH#uI#uJ#uK#uL#uH#zl#zl#xV#uM#uO#uP#wn#uQ", +"#zm#zn#zo#zp#zq#zr#zs#zt#zu#zv#zw#zx#zy#zz#zA#zB#zC#zD#zE#zF#zG#zH#zI#zJ#zK#zL#zM#zN#zO#zP#zQ#zR#zS#zT#zU#zV#zW#zX#zY#zZ#z0#z1#z2#z3#z4#z5#z6#z7#z8#xa#z9#A.#A##Aa#yJ#Ab#Ac#Ad#Ae#Af#Ag#Ah#Ai#Aj#Ak#Al#Am#An#Ao#Ap#Aq#Ar#As#At#Au#Av#Aw#fy#Ax#xD#Ay#Az#AA#AB#AC#AD#AE#AF#AG#AH#AI#AJ#AK#AL#AM#AN#zj#uz#uA#oU#wl#zj#wl#uA#wl#zj#nr#xU#xU#wm#nr#uE#uF#uG#uH#uI#uJ#uK#uL#uH#AO#AP#zl#xV#uM#uO#uP#uP", +"#AQ#AR#AS#AT#AU#AV#AW.7n#AX#AY#AZ#A0#A1#A2#u0#A3#A4#A5#A6#zq#A7#A8#A9#B.#B##Ba#Bb#Bc#Bd#Be#Bf#Bg#Bh#zT#Bi#Bj#Bk#Bl#Bm#Bn#Bo#Bp#Bq#Br#Bs#Bt#Bu#Bv#Bw#Bx#By#Bz#BA#BB#BC#BD#BE#BF#BG#BH#BI#BJ#BK#BL#BM#BN#BO#BP#BQ#BR#BS#BT#BU#BV#BW#BX#BY#BZ#B0#B1#B2#B3#B4#B5#B6#B7#B8#B9#C.#C##Ca#Cb#Cc#Cd#Ce#Cf#zj#uz#oU#oU#zj#uE#wl#oU#uz#wl#uE#nr#nr#uE#wl#uz#uF#uG#uH#uI#uJ#uK#uL#uH#Cg#Cg#AO#zl#xV#uM#uN#uO", +"#Ch#Ci#Cj#Ck#Cl#Cm#Cn#Co#Cp#Cq#Cr#Cs#Ct#Cu#Cv#Cw#Cx#Cy#Cz#CA#CB#CC#CD#CE#CF#CG#CH#CI#CJ#CK#CL#CM#CN#CO#CP#CQ#CR#CS#CT#CU#CV#CW#CX#CY#CZ#C0#C1#C2#Bx#C3#C4#C5#C6#C7#C8#C9#D.#D##Da#Db#Dc#Dd#De#Df#Dg#Dh#Di#Dj#Dk#Dl#Dm#Dn#Do#Dp#Dq#Dr#Ds#Dt#Du#Dv#Dw#Dx#Dy#Dz#DA#DB#DC#DD#DE#DF#DG#DH#DI#DJ#DK#DL#uE#wl#oU#uz#zj#uE#wl#oU#oU#uz#wl#zj#zj#wl#oU#uA#uF#uG#uH#uI#uJ#uK#uL#uH#DM#DN#Cg#AP#zl#xW#uM#uN", +"#DO#DP#DQ#DR#DS#DT#DU#DV#DW#DX#DY#DZ#D0#D1#D2#D3#D4#D5#D6#D7#D8#D9#E.#E##Ea#Eb#Ec#Ed#Ee#Ef#Eg#Eh#wV#Ei#Ej#Ek#El#Em#En#Eo#Ep#Eq#Er#Es#Et#Eu#Ev#Ew#Ex#Ey#Ez#EA#EB#C7#EC#ED#EE#EF#EG#EH#EI#EJ#EK#EL#EM#EN#EO#EP#EQ#ER#ES#ET#EU#EV#EW#EX#EY#EZ#E0#E1#E2#E3#E4#E5#E6#E7#E8#E9#F.#F##kD#Fa#Fb#Fc#Fd#Fe#uE#wl#oU#uz#zj#uE#zj#uz#uA#oU#uz#wl#uD#oU#uC#uB#uF#uG#uH#uI#uJ#uK#uL#uH#DM#DM#Cg#AO#zl#xV#uM#uM", +"#Ff#Fg#Fh#Fi#Fj#Fk.9.#Fl#Fm#Fn#Fo#Fp#Fq#Fr#Fs#Ft#Fu#Fv#Fw#Fx#Fy#Fz#FA#FB#FC#FD#FE#FF#FG#FH#FI#FJ#FK#FL#FM#FN#FO#FP#FQ#FR#FS#FT#FU#FV#FW#FX#FY#FZ#F0#F1#F2#F3#F4#F5#F6#F7#F8#F9#G.#G##Ga#Gb#Gc#Gd#Ge#Gf#Gg#Gh#Gi#Gj#Gk#Gl#Gm#Gn#Go#Gp#Gq#Gr#Gs#Gt#Gu#Gv#Gw#Gx#Gy#Gz#GA#GB#GC#qj#uQ#GD#GE#uv#GF#GG#GH#GH#GH#wh#wh#qe#qe#qe#GI#GJ#GK#GL#GL#GK#GM#GN#GO#GO#GP#GQ#GR#GS#GT#GU#GV#GW#GX#GX#GY#Cc#GP#Fb", +"#GZ#G0#G1#G2#G3#G4#G5#G6#G7#hY#G8#G9#H.#H##Ha#Hb.2r#Hc#Hd#He#Hf#Hg#Hh#Hi#Hj#Hk#ty#Hl#Hm#Hn#Ho#Hp#Hq#Hr#Hs#Ht#Hu#Hv#Hw#Hx#Hy#Hz#HA#HB#HC#HD#HE#HF#HG#HH#HI#HJ#HK#HL#HM#HN#HO#HP#HQ#HR#HS#HT#HU#HV#HW#HX#HY#HZ#H0#H1#H2#H3#H4#H5#H6#H7#H8#H9#I.#I##Ia#Ib#Ic#Id#Ie#If#Ig#Ih#Ii#rM#uP#Ij#Ik#uv#Il#Im#In#GH#GH#GH#wh#qe#qe#qe#In#GH#qe#GL#Io#Io#GL#qe#Ip#GO#Iq#AK#GR#GS#AI#GU#Ca#Cc#DI#DI#GX#uM#Ip#Ir", +"#Is#It#Iu#Iv#Iw#Ix#Iy#Iz#IA#IB#IC#ID#IE#IF#IG#IH#II#IJ#IK#IL#IM#IN#IO#IP#IQ#IR#IS#IT#IU#IV#IW#IX#IY#IZ#I0#I1#I2#I3#I4#I5#I6#I7#I8#I9#J.#J##Ja#Jb#Jc#Jd#Je#Jf#Jg#Jh#Ji#Jj#Jk#Jl#Jm#Jn#Jo#Jp#Jq#Jr#Js#Jt#Ju#Jv#Jw#Jx#Jy#Jz#JA#JB#JC#JD#JE#JF#JG#JH#JI#JJ#JK#JL#JM#JN#JO#JP#JQ#JR#Ip#Cf#JS#wh#JT#JU#GJ#GJ#In#GH#GH#GH#wh#wh#GL#qe#wh#GL#GN#GN#GL#In#JV#GO#Iq#AK#GR#GS#GS#GT#GY#GX#Ip#Ip#DI#DI#Fb#JW", +"#JX#JY#JZ#J0#J1#J1#J2#J3#J4#J5#J6#J7#J8#J9#K.#K##Ka#rY#Kb#Kc#Kd#Ke#Kf#Kg#Kh#Ki#Kj#Kk#Kl#Km#Kn#Ko#Kp#Kq#Kr#Ks#Kt#Ku#Kv#Kw#Kx#Ky#Kz#KA#KB#KC#KD#KE#KF#KG#KH#KI#KJ#KK#KL#KM#KN#KO#KP#KQ#KR#KS#KT#KU#KV#KW#KX#KY#KZ#K0#K1#K2#K3#K4#K5#K6#K7#K8#K9#L.#L##La#Lb#Lc#Ld#Le#Lf#Lg#Lh#Lh#Li#Lj#Lk#uC#Ll#Ll#GJ#GJ#GJ#GJ#In#GH#GH#GH#qe#qe#qe#GL#ut#ut#GL#qe#Lm#JV#GO#AK#GR#nt#GS#AI#Ca#Cc#DI#DI#GX#GX#Ip#Ir", +"#Ln#Lo#Lp#Lq#Lr##S#Ls#Lt#Lu#Lv#J6#Fp#Lw#Lx#Ly#Lz#LA#LB#LC#LD#LE#LF#LG#LH#LI#LJ#LK#LL#LM#LN#LO#LP#LQ#LR#LS#LT#LU#LV#LW#LX#LY#LZ#L0#L1#L2#L3#L4#L5#L6#L7#L8#L9#M.#M##Ma#Mb#Mc#Md#Me#Mf#Mg#Mh#Mi#Mj#Mk#Ml#Mm#Mn#Mo#Mp#Mq#Mr#Ms#Mt#Mu#Mv#Mw#Mx#My#Mz#MA#MB#MC#MD#ME#MF#MG#MH#MI#MJ#MK#ML#MM#MN#MO#MP#MQ#MQ#MR#GJ#GJ#GJ#In#In#qf#GJ#GK#GM#GL#GL#GM#Io#Lm#Lm#GO#GP#GQ#GR#GS#GS#MS#Ca#GY#Cc#GY#GY#GX#DI", +"#MT#MU#MV#MW#MX#MY#.G#MZ#M0#M1#IC#ID#M2#M3#rO#M4#M5#M6#M7#M8#M9#N.#N##Na#Nb#Nc#Nd#Ne#Nf#Ng#Nh#Ni#Nj#Nk#Nl#Nm#Nn#No#Np#Nq#Nr#Ns#Nt#Nu#Nv#Nw#Nx#Ny#Nz#NA#NB#NC#ND#NE#NF#NG#NH#NI#NJ#NK#NL#NM#NN#NO#NP#NQ#NR#NS#NT#NU#NV#NW#NX#NY#NZ#N0#N1#N2#N3#N4#N5#N6#N7#N8#N9#O.#O##Oa#Ob#Oc#Od#Oe#Of#Og#Oh#Oi#qf#MQ#MQ#MQ#MR#GJ#GJ#GJ#Oj#MQ#qe#GK#qe#wh#GL#ut#Ok#Lm#Ip#GO#AK#GR#nt#GS#Ol#GT#GW#GY#Ca#Ca#zl#GP", +"#Om#On#Oo#Op#Oq#Or.I.#Os#Ot#Ou#Ov#Ow.0H#Ox#Oy#Oz#OA#OB#OC#OD#OE#OF#OG#OH#OI#OJ#OK#OL#OM#ON#OO#OP#OQ#OR#OS#OT#OU#OV#OW#OX#OY#OZ#O0#O1#O2#O3#O4#O5#O6#O7#O8#O9#P.#P##Pa#Pb#Pc#Pd#Pe#Pf#Pg#Ph#Pi#Pj#Pk#Pl#Pm#Pn#Po#Pp#Pq#Pr#Ps#Pt#Pu#Pv#Pw#Px#Py#Pz#PA#PB#PC#PD#PE#PF#PG#PH#PI#PJ#PK#PL#PM#PN#PO#PP#PQ#qf#qf#MQ#MQ#MR#MR#MR#GI#PQ#GJ#GH#GH#GH#GH#GH#PR#Lm#JV#GO#AK#GR#GR#GW#MS#Ca#GY#GY#GY#GY#GX#DI", +"#A5#PS#PT#PU#PV#PW#PX#PY#PZ#P0#Ov#ID#J8#P1#P2#Hb#P3#P4#P5#P6#P7#P8#P9#Q.#Q##Qa#Qb#Qc#Qd#Qe#Qf#Qg#Qh#Qi#Qj#Qk#Ql#Qm#Qn#Qo#Qp#Qq#Qr#Qs#Qt#Qu#Qv#Qw#Qx#Qy#Qz#QA#QB#QC#QD#QE#QF#QG#QH#oE#QI#QJ#QK#QL#Pg#QM#QN#QO#QP#QQ#QR#QS#QT#QU#QV#QW#QX#QY#QZ#Q0#Q1#Q2#Q3#Q4#Q5#Q6#Q7#Q8#Q9#R.#R##Ra#Rb#Rc#Rd#Re#PQ#PQ#qf#qf#MQ#MQ#MQ#MR#MQ#qf#PQ#MR#GH#GH#MQ#GI#PR#Lm#Lm#GO#AK#Cc#GR#nt#Ca#GY#GX#GX#Cc#GX#DI#Fb", +"#Rf#Rg#Rh#Ri#Rj#Rk#Rl#Rm#Rn#Ro#Rp#Rq#Rr#Rs#Rt#Ru#Rv#Rw#Rx#Ry#Rz#RA#RB#RC#RD#RE#RF#RG#RH#RI#RJ#RK#RL#RM#RN#RO#RP#RQ#RR#RS#RT#RU#RV#RW#RX#RY#RZ#R0#R1#R2#R3#R4#R5#R6#R7#R8#R9#S.#S##Sa#Sb#Sc#Sd#Se#Sf#Sg#nj#Sh#Si#Sj#Sk#Sl#Sm#Sn#So#Sp#Sq#Sr#Ss#St#Su#Sv#Sw#Sx#Sy#Sz#SA#SB#SC#SD#SE#SF#SG#SH#SI#SJ#SK#SK#SK#SK#SK#SK#SK#SK#In#GJ#qf#PQ#PQ#MQ#GJ#GH#SL#MN#JW#Lm#GO#GQ#GR#GS#GP#GP#GP#GP#GP#GP#GP#GP", +"#Rf#Rg#Rh#Ri#Rj#SM#Rl#SN#SO#SP#SQ#SR#SS#ST#SU#SV#SW#SX#SY#SZ#S0#S1#S2#S3#S4#S5#S6#S7#S8#S9#T.#T##Ta#Tb#Tc#Td#Te#Tf#Tg#Th#Ti#Tj#Tk#Tl#Tm#Tn#To#Tp#Tq#Tr#Ts#Tt#Tu#Tv#Tw#Tx#Ty#Tz#TA#TB#TC#TD#TE#TF#TG#TH#TI#TJ#TK#TL#TM#TN#TO#TP#TQ#TR#TS#TT#TU#TV#TW#TX#TY#TZ#T0#T1#T2#T3#T4#Il#T5#T6#Oc#T7#T8#T9#s4#s4#s4#s4#s4#s4#s4#s4#MQ#MQ#qf#qf#qf#MQ#MR#GJ#SL#MN#U.#Lm#GO#AK#GR#nt#DI#DI#DI#DI#DI#DI#DI#DI", +"#Rf#Rg#Rh#U##Ua#Ub#Rl#SN#Uc#Ud#Ue#pd#Uf#Ug#Uh#Ui#Uj#Uk#Ul#Um#Un#Uo#Up#Uq#Ur#Us#Ut#Uu#Uv#Uw#Ux#Uy#Uz#UA#UB#UC#UD#UE#UF#UG#UH#UI#UJ#UK#UL#UM#UN#UO#UP.21#UQ#UR#US#UT#UU#UV#UW#UX#UY#UZ#U0#U1#U2#U3#U4#U5#U6#U7#U8#U9#V.#V##Va#Vb#Vc#Vd#Ve#Vf#Vg#Vh#Vi#Vj#Vk#Vl#Vm#Vn#Vo#Vp#Vq#Vr#MJ#Vs#Vt#Vu#Vv#Vw#Vx#Vx#Vx#Vx#Vx#Vx#Vx#Vx#Vy#PQ#qf#MQ#MR#MQ#MQ#MQ#SL#Vz#U.#PR#JV#Iq#AK#Cc#DI#DI#DI#DI#DI#DI#DI#DI", +"#VA#VB#VC#VD#VE#Ub#Rl#VF#VG#VH#VI#VJ#VK#VL#VM#VN#VO#VP#VQ#VR#VS#VT#VU#VV#VW#VX#VY#VZ#V0#V1#V2#V3#V4#V5#V6#V7#V8#V9#W.#W##Wa#Wb#Wc#Wd#We#Wf#Wg#Wh#Wi#Wj#Wk#Wl#Wm#Wn#Wo#Wp#Wq#Wr#Ws#Wt#Wu#Wv#Ww#Wx#Wy#Wz#aB#WA#WB#WC#WD#WE#WF#WG#WH#WI#WJ#WK#WL.N#.BK#WM#WN#WO#WP#WQ#WR#WS#WT#WU#WV#WW#WX#WY#WZ#W0#W1#W1#W1#W1#W1#W1#W1#W1#W2#W3#PQ#MR#GJ#MR#qf#PQ#SL#W4#MN#W5#Lm#GO#Iq#AK#Fb#Fb#Fb#Fb#Fb#Fb#Fb#Fb", +"#VA#VB#VC#W6#W7#Ub#W8#VF#W9#X.#X##Xa#Xb#Xc#Xd#Xe#Xf#Xg#Xh#Xi#Xj#Xk#Xl#Xm#Xn#Xo#Xp#Xq#Xr#Xs#Xt#Xu#Xv#Xw#Xx#Xy#Xz#XA#XB#XC#XD#XE#XF#XG#XH#XI#XJ#XK#XL#XM#XN#XO#XP#XQ#XR#XS#XT#XU#XV#XW#XX#XY#XZ#X0#X1#X2#X3#X4#X5#X6#X7#X8#X9#Y.#Y##Ya#Yb#Yc#Yd#Ye#Yf#Yg#Yh#Yi#Yj#Yk#Yl#Ym#Yn#Yo#Yp#Yq#Yr#Ys#Yt#Yu#W1#W1#W1#W1#W1#W1#W1#W1#Yv#W2#GI#MQ#MR#MQ#qf#PQ#SL#SL#MN#U.#W5#Lm#JV#GO#Fb#Fb#Fb#Fb#Fb#Fb#Fb#Fb", +"#Yw.lN#Yx#Yy#Yz#Ub#YA#YB#YC#YD#YE#YF#YG#YH#YI#YJ#YK#YL#YM#YN#YO#YP#S2#YQ#YR#YS#YT#YU#YV#YW#YX#YY#YZ#Y0#Y1#Y2#Y3#Y4#Y5#Y6#Y7#Y8#Y9#Z..Om.FT#Z##Za#Zb#Zc#Zd#Ze#Zf#Zg#Zh#Zi#Zj#Zk#Zl#Zm#Zn#Zo#Zp#Zq#Zr#Zs#Zt#Zu#Zv#Zw#Zx#Zy#Zz#ZA#K8#ZB#ZC#ZD#ZE#ZF#ZG#ZH#ZI#ZJ#ZK#ZL#ZM#ZN#ZO#ZP#ZQ#ZR#ZS#ZT#ZU#T4#ZV#ZV#ZV#ZV#ZV#ZV#ZV#ZV#Yv#ZW#Oj#GI#PQ#qf#qf#qf#SL#SL#W4#MN#JW#W5#PR#Lm#Ir#Ir#Ir#Ir#Ir#Ir#Ir#Ir", +"#Yw#ZX#ZY#ZZ#Z0#Ub#Z1#YB#Z2#Z3#Z4#Z5#Z6#Z7#Z8#Z9#0.#0##0a#0b#0c#0d#0e#0f#0g#0h#0i#0j#0k#0l#0m#0n#0o#0p#0q#0r#0s#0t#0u#0v#0w#0x#0y#0z#0A#0B#0C#0D#0E#0F#0G#0H#0I#0J#0K#0L#0M#0N#0O#0P#0Q#0R#0S#0T#0U#0V#0W#0X#0Y#0Z#00#01#02#03#04#05.Ym#06#07.L8#08#09.XR#1..HC#1##1a#1b#1c#1d#1e#1f#1g#Yq#1h#1i#1j#1j#1j#1j#1j#1j#1j#1j#ZW#ZW#ZW#W2#W3#PQ#MQ#GJ#SL#SL#W4#MN#MN#JW#W5#W5#Ir#Ir#Ir#Ir#Ir#Ir#Ir#Ir", +"#pc#ZX#ZY#1k.5J#1l#Z1#YB#1m#1n#1o#1p#1q#1r#1s#1t#1u#1v#1w#1x#1y#1z#1A#1B#1C#1D#1E#1F#1G#1H#1I#1J#1K#1L#1M#1N#1O#1P#1Q#1R#1S#1T#1U#1V#hY#1W#1X#1Y#1Z#10#11#12#13#14#15#16#17#18#19#2.#2##2a#2b#2c#2d#2e#2f#2g#2h#2i#2j#2k#2l#2m#2n#2o#2p#2q#2r#2s##9#2t#2u#2v#2w#2x#2y#2z#2A#2B#2C#2D#2E#2F#2G#2H#1j#1j#1j#1j#1j#1j#1j#1j#W2#ZW#Yv#Yv#W2#Vy#MQ#In#SL#SL#SL#Vz#MN#MN#U.#JW#JW#JW#JW#JW#JW#JW#JW#JW", +"#2I#2J#2K#2L#2M#2N#2O.Jf#2P#2Q#2R#2S#2T#2U#2V#2W#2X#2Y#2Z#20#21#22#23#24#25#26#27#28#YM#29#3.#3##3a#3b#3c#3d#3e#3f#3g#3h#3i#Rm#3j#3k.B.#3l#3m#3n#3o#3p#3q#3r#3s#3t#3u#3v#3w#3x#3y#3z#3A#3B#3C#3D#XY#3E#3F#3G#3H#3I#3J#3K#3L#3M#3N#3O#3P#3Q#3R#ve#3S#3S#3S#3S#3S#3S#3S#3S#3T#3U#3V#3W#3X#3Y#3Z#30#31#32#33#34#35#36#37#38#39#4.#4##4a#4b#4c#4d#4e#4f#1j#4g#4h#2F#PN#4i#4i#4j#4k#4l#4m#4n#4o#4p#4q", +"#4r#4s#4t#4u#4v#4w#4x#4y#4z#4A#4B#4C#4D#4E#4F#4G#4H#4I#4J#4K#4L#4M#4N#4O#4P#4Q#4R#4S#4T#4U#4V.6w#4W#4X#4Y#4Z#40#41.Hs#42#43.I4#44#45.B.#46#47#48#49#5.#5##5a#5b#5c#5d#5e#5f#5g#5h#5i#5j#5k#5l#5m#5n#5o#5p#5q#5r#5s#5t#5u#5v#5w#5x#5y#5z#5A#5B#5C#3S#3S#3S#3S#3S#3S#3S#3S#5D#5E#5F#5G#5H#5I#5J#5K#5L#5M#5N#5O#5P#5Q#5R#5S#5T#5U#5V#5W#5X#5Y#5Z#50#s4#W1#Yq#39#51#52#Rc#53#54#55#56#Lk#57#58#59#6.", +"#6##6a#6b#6c#6d#6e#6f#6g#6h#6i#6j#6k#6l#6m#6n#6o#6p#6q#6r#6s#6t#6u#6v#6w#6x#6y#6z#6A#6B#6C#6D#6E#6F.7s.Np#41#6G#6H.F9.HK.O9.Mb#6I#6J#6K#6L#6M#6N#6O#6P#6Q#6R#6S#6T#6U#6V#6W#6X#6Y#6Z#60#61#62#63#64#65#66#67#68#69#7.#7##7a#7b#7c#7d#7e#7f#7g#7h#3S#3S#3S#3S#3S#3S#3S#3S#7i#7j#7k#7l#Rr#7m#7n#7o#7p#7q#7r#7s#7t#7u#7v#7w#5P#7x#7y#7z#7A#7B#7C#7D#7E#W1#7F#7G#7H#7I#53#7J#7K#54#In#Lk#57#7L#7M#7N", +"#7O.yR#7P#7Q#7R#7S#7T#7U#7V#7W#7X#7Y#7Z#70#71#72#73#74#75#76#77#78#79#8.#8##8a#8b#8c#8d#8e#8f#8g.N.#8h#8i#8j#8k#8l#8m#8n#8o#8p#8q#8r#8s#8t#8u#8v#8w#8x#8y#8z#8A#8B#8C#8D#8E#8F#8G#8H#8I#8J#8K#8L#8M#8N#8O#8P#8Q#8R#8S#8T#8U#8V#8W#8X#8Y#8Z#80#81#3S#3S#3S#3S#3S#3S#3S#3S#82#83#84#85#86#87#88#89#9.#9##9a#9b#9c#9d#9e#9f#9g#9h#9i#9j#9k#9l#MK#9m#s4#1j#7F#9n#9n#52#53#7J#9o#9p#9q#4m#9r#7L#9s#7N", +"#9t#9u#9v#9w#9x.LF#9y#9z#9A#9B#9C#9D#9E#9F#9G#9H#9I#9J#9K#9L#9M#9N#9O#9P#9Q#9R#9S#9T#9U.Np.Jg.Hv#9V#9W#9X#9Y#9Z#90#91#92#93#94#95#96.Ep#97#98#99a..a.#a.aa.ba.ca.da.ea.fa.ga.ha.ia.ja.ka.la.ma.na.oa.pa.qa.ra.sa.ta.ua.va.wa.xa.ya.za.Aa.Ba.Ca.Da.Ea.Ea.Ea.Ea.Ea.Ea.Ea.Ea.Fa.Ga.Ha.Ia.Ja.Ka.La.Ma.Na.Oa.Pa.Qa.Ra.Sa.Ta.Ua.Va.Wa.Xa.Ya.Za.0#ZWa.1a.2a.3#Yqa.4#2F#Oga.5#53a.6a.7#wh#AN#9r#MPa.8a.9", +"a#.a##a#aa#ba#ca#da#ea#fa#ga#ha#ia#ja#ka#la#ma#na#oa#pa#qa#ra#sa#ta#ua#va#wa#xa#ya#za#Aa#Ba#Ca#Da#Ea#E#YAa#Fa#Ga#H.SV.SUa#Ia#Ja#Ka#La#Ma#Na#Oa#Pa#Qa#Ra#Sa#Ta#Ua#Va#Wa#Xa#Ya#Za#0a#1a#2a#3a#4a#5a#6a#7a#8a#9aa.aa#aaaaabaacaadaaeaafaagaahaaiaaja.Ea.Ea.Ea.Ea.Ea.Ea.Ea.EaakaalaamaanaaoaapaaqaaraasaataauaavaawaaxaayaazaaAaaBaaCaaDaaEaaFaaGaaHaaI#7F#Yq#4haaJaaKa.5aaL#55aaMaaN#AN#9r#MPa.8#9s", +"aaOaaP#zwaaQaaRaaSaaTaaUaaVaaWaaXaaYaaZaa0aa1aa2aa3aa4aa5aa6.Npaa7aa8aa9ab.ab#.S.abaabbabcabdabeabfabgabhabiabjabk.FVablablabmabnaboabpabqabrabsabtabuabvabwabxabyabzabAabBabCabDabE#WBabFabGabHabIabJabKabLabMabNabOabPabQabRabSabTabUabVabWabXa.Ea.Ea.Ea.Ea.Ea.Ea.Ea.EabYabZab0ab1.Gh.XQ.0Xab2#VEab3ab4ab5ab6ab7ab8ab9ac.ac#acaacbaccacdaceacfa.2a.2#7F#39#Yo#Ogacg#7J#WX#55#wh#AN#9r#GDachaci", +"acjackaclacmacnacoacpacqacracsactacuacvacwacxacyacz.O3acAacBacCacDacEacEacF.KW.KSacGacH.I2acIacJacKacLacMacNacOacPacPacQ.JnacRacSacTacUacVacWacXacYacZac0ac1ac2ac3ac4ac5ac6ac7ac8ac9ad.ad#adaadb#G#adcaddadeadfadgadhadiadjadkadladmadnadoadpadqa.Ea.Ea.Ea.Ea.Ea.Ea.Ea.EadradsadtaduadvadwadxadyadzadA#RjadBadCadDadEadFadGadHadI.ViadJadKadLadM#ZVa.3adN#7G#9nadOadP#woadQadR#In#AN#IjadSadTadU", +"adVadWadXadYadZad0ad1ad2ad3ad4ad5ad6ad7ad8ad9ae.ae#ae#aeaaeb.HM.HMaecaecaedaedaedaedaedaedaedaedaeeaeeaeeaeeaeeaeeaeeaeeaefaegaehaeiaefaefaejaekaelaemaenaeoaepaeqaeraesaetaeuaevaewaexaeyaezaeAaeBaeCaeDaeEaeFaeGaeHaeIaeJaeKaeLaeMaeNaeOaecaea#3Sa.EaePaeQaeQaePa.E#3SaeRaeSaeT.0AaeUaeVaeWaeW.LE.LE.LE.LE.LE.LE.LE.LEaeXaeYaeZae0ae1ae2ae3ae4ae5ae6ae7ae8ae9af.#wlaf##Ll#MNafaafbafcafdafeaff", +"afgafhafiafj.Hoafkaflafmafnafoafpafqafrafs.KIaft.Nwafuafuae#aeaaebaeb.HMaedaedaedaedaedaedaedaed.O8.O8.O8.O8.O8.O8.O8.O8afvafvafwafxafyafzafAafBafCafDafEafFafGafHafIafJafKafLafMafNafOafPafQafRafSafTafUafVafWafXafYafZaf0af1af2af3af4af5af6af6#3S#3Sa.EaeQaeQa.E#3S#3Saf7af8af9ag.ag#agaagbagc.LE.LE.LE.LE.LE.LE.LE.LEagdageagfaggaghagiagjagkaglagmagnagoagp#2Bagq#nsagragsafaafbafcagtaguagv", +"agwagxagyagzagAagBagCagDagEagFagGagHagIagJagKagLagMagMagNagN.Nwafuae#ae#agOagOagOagOagOagOagOagO#44#44#44#44#44#44#44#44afzaegagPafxaefagPagQagRagSagTagUagVagWagXagYagZag0ag1ag2ag3ag4ag5ag6ag7ag8ag9ah.ah#ahaahbahcahd.moaheahfaeJahg.H2ahhaf6ahi#3Sahja.Ea.Eahj#3SahiahkaeTaf9.EWahlahmagbahn.pS.pS.pS.pS.pS.pS.pS.pSahoahpahqahrahsahtahuahvahwahxahyahzahA#7rahBahCahD#58#MHahEahFagtahGahH", +"ahIahJahKahLahMahNahOahPahQahRacAahS.JfahT.F7.Mfaf5af5ahUagMagNagN.Nw.NwagOagOagOagOagOagOagOagO.H9.H9.H9.H9.H9.H9.H9.H9agPafx#3jafzaefahVahWahXahY.ptahZ.mEah0ah1ah2ah3ah4ah5ah6ah7ah8ah9ah8ai.ai#aiaaibaicaidaieaif.nRaigaihaiiaijaikaaiail.HMaimahi#3S#3S#3S#3Sahiaimainainaioaipag#ahmaiqairaisaisaisaisaisaisaisaisaitaiuaivaiwaixaiyaizaiAaiBaiCaiDaiEaiFaiGaiHaiIaiJaiK#LgaiLaiMaiN#uFaiO", +".CtaiPaiQaiRaiSaiTaiUaiVaiWaiXaiY.xN.xNaiZai0ai1af5af5ahUagMagNagN.Nw.Nwai2ai2ai2ai2ai2ai2ai2ai2.H9.H9.H9.H9.H9.H9.H9.H9ai3afzai4ai5ai6ai5afxai7ai8ai9aj.aj#ajaajbajcajdajeajfajgajhajiajjajkajlajmajnajoajpajqajrajs.q0ajt.l#ahfajuajvaecaf6aeaabhabhahiajwajwahiabhabhahkaio.DhajxahmajyajyajzaisaisaisaisaisaisaisaisajAajAajBajCajDajEajFajGajHajIajJajKajLajMajNajOajPajQajRaiLajSajTajUajV", +"ajWajXajYajZaj0.F9aj1aj2aj3aj4.Jgaj5.HVaj6aj7aj8agMagMagNagN.Nwafuae#ae#ai2ai2ai2ai2ai2ai2ai2ai2#44#44#44#44#44#44#44#44aeiaj9aegagPaj9ai6aehak.ak#akaakbakcagUakdakeakfakgakhakiakjakkaklakmaknakoakpakqakraksaktaku.ibakvaigaiiakwakxajvahUafu#8oakyabhahiahiabhaky#8oainakzakAakBaeUajyakCakDaisaisaisaisaisaisaisaisakEakFakGakHakIakJakKakLakMakNakOakPakQakRakSakTakUakVakWakXakY#iYakZak0", +"ak1ak2.IMak3ak4ak5ak6ak7ak8ak9al.al#.HNalaalbabf.Nwafuafuae#aeaaebaeb.HMalcalcalcalcalcalcalcalc.O8.O8.O8.O8.O8.O8.O8.O8aefafzaldaleaefai4afwaj9alfalgalhalialjalkallalmalnaloalpalqalralsaltalualvalwalxalyalzalAalBaku.moaihalCagNalDafualEalFalG#8oabhabhabhabh#8oalGalHalIakAajxalJahmalKakCalLalLalLalLalLalLalLalLalMalNalOalPagcalQalRalS.CYalTalUa#E.DjalValWalXalYalZakWal0al1al2al3al4", +"al5al6al7al8al9am.am#amaambamcamdameamfamgamh.Nzae#ae#aeaaeb.HM.HMaecaecalcalcalcalcalcalcalcalcaeeaeeaeeaeeaeeaeeaeeaeeamiaegafwai4aldaegafzamjamkamlammamnamoampamqakuamramsamtamuamvamwamxamyamzamAamB.l#.mSamCamDamE.jiamFamGamHamIamJamKamLalGamM#8oabhabh#8oamMalGamNamOalIamPamQamRajyamSamTamTamTamTamTamTamTamTamUamVamWamXamYamZam0am1am2am3am4am5am6am7am8am9an.an#anaal0anbal2ancand", +"aneanfang.Nhanh.R7anianjaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankank.KC.KC.O8.O8anlanmannannanoanoanoanoanoanoanoano#44#44anpanp.Mg.Mganqanq.rcanransantanuanv.jianuanwanxanyanzanAanBanCanDanEanFanGanH.mo.ji.l#anIanJanKanLanLanManNanOanPanQanRanR.HGanSanTanUanUanVanWanXanY.KfanZan0an1#YCan2an3an4an5#YCan6an7an8an8an9ao.ao#aoaaobaobaocaodaoeaofaoc.Klaogaohaoiaojaokaolaomaonaoo#ML", +"aopaoqaor.Neaosaotaouaovaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankank.KC.KC.KC.O8anlanmanmannanoanoanoanoanoanoanoano#44#44anpanp.Mg.Mgaowanqaoxaoy.rgaozaoA.l#.AF.qNaoBaoCaoDaoEaoFaoGaoHaoIaoJanF.JbaoKaoLaoM.l#aoNaoOaoPaoQaoRanOaoSaoTaoUaoVaoWaoXaoYaoZao0anZanZao1anYanYanYanYanYanYanYan4an3an9ao2an4an4ao3an9an7an3an8ao.ao#aobao4#SUaocaodaoeaofaf7.Klaogao5ao6ao7ao8ao9#9qap.ap#apa", +"apbapcapdape.Ncapfapgaphaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankankapiapi.KC.O8.O8anlanmanmapjapjapjapjapjapjapjapj#44#44apkanp.S..Mgaplanqapmapnapoaoz.l#appaiganuapqaprapsaptapuahNam4apvapwapxapyapzapAapB.jiapCapDapEapFapGanNanLapGaoUaoWaoWaoXaoYapHao0anZapIanWanWanWanWanWanWanWanW#YCan6apJan6#YC#YCan5apJapJan4an3an9apKao4apLapMapNapOapPaof#VF.KlaogapQapRapSapTapU#GHapV#rHapW", +"apXapYapZap0ap1ap2ap3ap4aeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankankankankapi.KC.O8.O8anlanlapjapjapjapjapjapjapjapj#6I#44#44anpanp.Mg.Mg.Mgap5ap6ap7.moanuanvap8ap9aq.aq#aqaaqbaqcaqdaqeaqfaqgaqhaqiaqjaqkaqlaqmaqnaqoapFaqpaqqapGaqraqsaqpaqtaquaqvaqwaqxaqy.Kf.KfaqzaqAaqBaqCanWanY.KfaqDaqEaqF#YC#YCaqEaqEaqE#YCaqGapJan7an9aoa#SUapMaqHapNapOaqIao5aqJaqKaogapQaqLaqMaqNaqOaqPaqQaqR#Ol", +"aqSaqTaqUaqV.uIaqW.FTaqXaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankankaqYankankapi.KC.O8.O8.O8#6I#6I#6I#6I#6I#6I#6I#6IaqZaq0#44apkanp.S..Mg.Mgaq1aq2aq3.rfaq4.moaq5.BJaq6aftaq7aq8aq9ar.ar#araarbarcardakxarearfargarhariarjarkanPapGaoRanParlaquaquaqvarmarnaro.KfarparqaqzarrarsanWanY.KfapIan7an4an4an4an4an7an4an4aqGapJan7an9aoa#SUapMaqHartaruaqIarvaqJ.I2#VFapQarwarxaryarz#uwarAarBarC", +"arDarEarFarGarHarIarJanhaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankankaqYaqYankankapi.KC.KC.O8arfarfarfarfarfarfarfarfaqZaqZ#44#44anpanp.Mg.Mg.HKarKarLarM.l#arNarOarP.LOarQapiarRanlarS.LN.LOarTarUarVagNaecarWarXarYapFanOarZanOanMar0ar1anNar2ar3ar4ar5ar6ar7anYanYarsarsar8ar9anVanWanXanYan3an4an6an2an7an3an4apJapJan4an3an9apKao4apLapMaqJaruas.as#asa.I2#YAasbascasdaseasfasgashasiasj", +"askaslasmasnasoaspasqarJaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankank.LL.LLaqYankankapi.KC.KC.KP.KP.KP.KP.KP.KP.KP.KPaqZaqZaq0#44apkanp.S..Mgasrassastasuasva.C.HL.HLaswasxasyaszasAaswaswasBaqhasCasDaf5asEasFasGasHaqq.F6asIarjaqsarjaoQanPar3ar3ar4asJasKar7anY.HEarsar8ar9anVanWanYao1.Kfan7an6#YCan6an4ao3an4an5an7an3an8ao.ao#aobao4#SU.KlasLaohas##SN.I2#YAasMasNasOasPasQasRasSaiNasT", +"asUasVasWasXasYasZas0.Hsaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankank.LL.LLaqYaqYankapi.KC.KC.KP.KP.KP.KP.KP.KP.KP.KPaqZaqZaqZ#44apkanpanp.S.as1adpaoxas2a.Cap5arPadpas3as4as5as6as7abjas6as8aqhas9at.at#aaiataatbatcanManPanMaqratdateanOanLatfar3ar4asJasKatganY.HEaqzatharsatianYanZan1atjapKan9an3an8ao#aoaao.an3an8an8an9ao.ao#aoaaobaob.KlasLatkas##SNatl#YAasMatmatnatoatpatq#Ceatrats", +"attatuatvatwatxaty.Hsatzapjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapiankankapi.KC.O8anlanmanmarfarfarfarfarfarfarfarfanpanpanpanpanpanpanpanpatAatBatCatDatDarn.McatEatFatGatHatIatJ.OqatKatLatMatNatOatPatQahWahVatRatSa.DatTaj8atUatVatWatSatfatXatYatZat0at1anWanVatianXanYatiarsat2ar9anYat3at4an0at5at6at7at3at8at7at7at7an0at6at6at5at5at9au.au##9Y.XR.S..TEaqY#YAauaaubaucaudaueaufaug", +"auhauiaujaukaulaum.Hsatzapjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapi.KC.KC.KC.KC.O8.O8.O8.O8arfarfarfarfarfarfarfarfanpanpanpanpanpanpanpanpatEacHarn#ZHaunauoaupauoauqatI.HYaurausautauuauvauwauxauyauzatMauAatQaldaj8atTauBa.DauCatVauDauCauEaoWaoXauF.HGauGauHat5arp.KfanYar9aqBarrar9anYarpapIat6auIauIan0at4auJarpat4apIan0at5auIaapaapau.au.auKauLauM.S..TEaqYauN#wLauOauPauQauRauSauT", +"auUauVauWauXauYauZau0au1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapi.O8.O8.O8.KC.KCapiankank#6I#6I#6I#6I#6I#6I#6I#6IanpanpanpanpanpanpanpanpatAau2arnatBauoau3au4aunau5au6au7au8au9av.aig.ji.mo.moav#avaavbavcaj9ai4avdalbalbaveavfauDauDatWanQ.H8avgavh#ZHaviavjau#anUanZanYar9aqBarsanW.Kfat4at7auIaapaapauIat7at4at3arpat4an0auIaapau.avkau#auK#9YauL.I0avlanpanpavmavnavoavpavqavravsavt", +"avuavvavwavxavy.Utavzau1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapianl.O8.O8.KCankankaqYaqYapjapjapjapjapjapjapjapjanpanpanpanpanpanpanpanpatE.HGavAavhavBaoYatCavC#6JavDavEavFavGavHavI.l#avJavJavKavLavMai5afzavcavNavNavda.DatSatUatVatVaqtavOaoX.HGavPavQatjatjanZ.KfanWar8ar8anWanZatjapIan0avR.3Wau.aapat5at7at4apIat7at6avRavSau.avT#9YauLauLavBavB.I0avlaegavUavVavWavXavYavZav0av1", +"av2avvav3avxav4.Ow.5Vau1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapi.KCapiapiankankankaqYaqYanoanoanoanoanoanoanoanoanpanpanpanpanpanpanpanpau2av5av6av7av8avAav8atCau6av9aw.aw#.l#aig.jiaw#.l#awaawbawcawdafwaweaehalbauBa.DawfatSatUatVawgaquaquaqvarmarnawh.KfarpanYanWar9ar9anYanUawiawjat4an0avRau.au.avSauIat6at6at6at5auIavRaapavSavS.XRauM.I0avBavBawkawlawlawmawnawoawpawqawraws#Sz", +"awtawuav3awvaww#41avzau1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapiawxawx.LLaqYankapi.KC.KCanoanoanoanoanoanoanoanoanpanpanpanpanpanpanpanpatAatBatBapHawyawyar5apHawzawAawBaigawCawD.l#.moawEav#awFawGavcaweawHawIaj8atSatUatUauCauCauDawJaquaquaqvar5asKatganXanWar9arsar8anXanUawKauKawLarpat7auIavSau.aapauIat6auIauIavRaapaapaapavSavS.S..S.avl.I0awkawMawNa#EawOawPawQawRawSawTawUawV", +"awtawWawXawYawZ.Owavzau1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapiaw0aw1.LNawxank.O8anmannamgamgamgamgamgamgamgamganpanpanpanpanpanpanpanpacHarnar5av6atBatBatEaw2aw3au8aw4aw5aig.l#aw6aw7aoOaw8aw9ax.afwax#ak.auAatUawgaxaawgatWauCauDawJasGaxbatYatZat0axcanWanWarsarsatiaqDawiauKauKavj.HEat4at6aapavSaapauIat6at5auIauIaapavSau.avkavk.TE.TEanpavlawlawNa#Eaxdaxearkaxfaxgaxhaxiaxj#4i", +"awtaxkaxlaxmaxnaxoau0au1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapiarSaxpaw1awxankanlarR.KF.TE.TE.TE.TE.TE.TE.TE.TEanpanpanpanpanpanpanpanp#ZHaxqarnaxraxsacHaxtaxuaxvaxw.l#.moaw4aig.l#.l#axxaxyaxzaxAaxBaxCafzaxDawJaxEaxFaxEatVauCauDawJaxGaxGaxHaxIaxJaxKar9atiaqCar9anXan1avjawLavjatjat8at3an0auIaapavRat5an0at7axLat5aapau.avkaxdaxMaqYaqYanpaegawla#EaxdaxNaxO#2uaxPaxQaxRaceaxSaxT", +"axUaxVaxWaxXaxYaxZas9ax0apiapiapiapiapiapiapiapi.KO.KOax1.F0ax2ax3a#La#L.Gbax4ax4ax5.KHax6.Gb.Oqaj6ax7#2uadpax8ax9ay.#8q.XR.XR.I0avlavl.HH.HH.HHaroay#ayaaybaycar7aydayeayfaygayhaigaigayiawDayjaykaylaymaynayoaypavPayqayraysaytayuatWayvaszaywayxayyayzayAayBayCayDayEayFayGahqayHayIayJayKayLayMayNat1ayOayPayQ#2xayRaySayTayUayVayWayXayYayZay0ahiay1aqKasa#VFaogay2ay3ay4ay5ay6ay7ay8ay9az.", +"az#azaazbazcazdaze.Maaeiapiapiapiapiapiapiapiapi.IJ.KOacTazfazgazgazhacTaziazjax5azk.KHazl.Gbaziazm.F0abbaznazoax7azpazq.XR.XR.I0avlavl.H9.HH.HHay#aqyazrazsaztazuazvazwazx.l#avGawCaw6aigazyazzazAazBazCazDazEazFazGazHazIazJazKazLatXazMayvazNazOazPar1azQawgazRaxJazS.CsazTazUazVazWazXazYazZaz0akMaz1az2az3az4alOaz5#wMaz6.HRaz7az8az9.QCaA.#3S#3SaA#.I2#Rm#YA#Z1awLaAaaAbaAcay6aAdaAeaAfaAg", +"aAhaAiaAjaAkaAl.QxaAmafxapiapiapiapiapiapiapiapiaAnaAoaApaAqaAraAsaAtaAuaAvaAwaAxaAyaAzaAA.F9aABareahUaACalEailalEaADafu.XR.XR.I0.I0avlavlavl.H9aAEayaaAFaAGauGaAHaAIaAJaAKaALaAMaig.qNaANaAOaAPaAQaARaASanYaATaAUaAVaAWaAXaAYaAZaytatVamhaA0aA1aqxazSaA2aA2aA3ab2aA4aA5aA6aA7aA8aA9aB.aB#aBaaBbaBcaBdaBeaBfaBgaBhaBiaBjaBkaBlaBkaBmaBn.XR.KkaBoaq0aBpaBqaA#aeR#SN#YA#Z1.MiaBra.HaBsaBtaBuaBvaBw", +"aBxaByaBzaBAaBBaiUaBCaj9apiapiapiapiapiapiapiapiaBD#2OaBEaBFaBGaBHaBIaBJaBKaBL#viaBMaBNaBOaBPaBQaBRaBSaBTaBUaBV.H0aBWaAC.XR.XR.I0.I0.I0.I0avlavlaAGaroawlaBXao0aroaBYaBZaB0aB1aB2aB3aB4aB5aB6aB7aB8aB9aC.aC#aCaaCbaCcaCdaCeaCfaAYaAZaCgatUavfaChaCiaCjaCkaClaCmaA4aCnaCoaCpaCqaCraCsaCtaCuaCvaCwaCxaCyaCzaCAaCBaCCabpaCDaCEaCFaCGaCHaCIaCJaCKaCLa.Eahj.Mb.EVaCMaCN#W8#W8aCOaCPaCQaCRaCSaCTaCUaCV", +"aCWaCXaCYaCZaC0aC1.F9ai4apiapiapiapiapiapiapiapiaC2aC3aC4aC5aC6aBHaC7aC8aC9aD.aD#aDaaDbaDcaDdaDeaDfaDgaDhaDiaDjaDk.HOaDl.I0.I0auM.XR.XR.XR.XR.XRaDmaAEaDmaBXaxcaqyaDnaDoaDpaDqaDraDsaDtaDuaDvaDwaDxaDyaeUaDzaDAaDBaDCaDDaDEaDFaDGanXaDHazKaCgauDaDI.HQayWaDJaDKaDLaDMaDNaDOaDPaDQaDRaDSaDTaDUaDVaDWaDXaDYaDZaD0aD1.EN.RvaD2#6GaD3aD4aD5aD6.I3axqaq0aBp.MbaA#aeR#SN#YA#Z1aD7aeTasLaD8aD9aE.aE#aEa", +"aEbaEcazbaEdaBBaxZaEeawIapiapiapiapiapiapiapiapiacoaEfaEgaEhaEiaEjaEkaElaEmaEnaEoaEpaEqaEraEsaEtaEuaEvaEwaExaEyaEzaEAarU.I0.I0auM.XR.XR.IP.IPaEBar7ay#aqyaECat1aEDar7aEEaEFaEGaEHaEIaEJaEKaELaEMaENamRaEOaEPaEQaERaESaETamVaEUaEVaEWaEXaAZazKaEYaEZaE0aE1aE2aE3aE4aE5aE6aE7aE8aE9aF.aF#aFaaFbaFcaFdaFeaFfaFgaFh.UyaFiaFjaFk#6F#8gaFlaFm#94aFnaFo#3SaBqaFp.I2#Rm#YAawLawLaFqaf8aFraFsaFtaFuaFvaFw", +"aFxaAiaFyaFzaFAaxZaEeauMapiapiapiapiapiapiapiapiadxaFBaFCaFDaFEaFFaFGaFHaFIaFJaFKaFLaFMaFNaFOaFPaFQaFRaFSaFTaFRaFUaFVaFWavl.I0auM.XR.IPaEBaEBaEBaqyanSauGanSaroayaaFXanSaFYaFZaF0aF1aF2aF3aF4aF5aF6aF7#YCaF8aF9aG.aG#aGaaGbaGcaGdaGeaGfaAYaAZaGgaGhaGiaGjaGkaGlaGmaGnaGoaGpaGqaGraGsaGtaGuaGvaGwaGxaGyaGzaGAaGBaGCaGDaGEaGFaGGaGHaGIaGJ.4iaGKaGLahiay1aGM.KlaqJaogawjawjaGNaGOatgaGPaGQaGRaGSaGT", +"aGUaxVaFyaGVaGWaC1aGXagPapiapiapiapiapiapiapiapiaGYaGZaG0aG1aG2aG3aG4aG5aG6aG7aG8aG9aH.aH#aHaaHbaHcaHdaHeaHfaHgaHhaFUaHiavlavlauM.XR.IPaEBaHjaHjay#anSawlaHkaqyaECayaaztaHlaHmaHnaHoaHpaHqaHraHsan7aHtaHuaHvaHwaHxaHyaHzaHAaHBaGcaGeaGfaCfaAZ.GAaHCaHDaHEaHFaHGaHHaHIaHJaHKaHLaHMaHNaHOaHPaHQaHRaHSaHTaHUaHVaHWaHXaHYaHZaH0aH1aH2aH3aH4aH5aH6aH7abhabhaH8apNaocaH9aruaruaGNaI.aI#aIaaIbaIcaIdaIe", +"aIfaIgaIhaIi.L8aIj.NyaIkai2agOagOaedaedabnabnabnaIlaImaInaIoaIpaIqaIraIsaItaIuaIvaIwaIxaIyaIzaIAaIBaHgaHfaICaIDaIEaIFaIG#44#44apkanpanpanp.S..S.acHauoatB#ZHatDavA.HG.HGaIHaIIaIJaIKaILaIMaINaIOaIPaIQaIRaISaITar5aCgaIUaIVaIWarmaIXaIYaIZaI0aI1aI2aI3aI4aI5aI6aI7aI8aI9aJ.aJ#aJaaJbaJcaJdaJeaJfaJgaJhaJiaJjaJkaJlaJmaJnaJoaJpaJqaJraJsaJtaJuaJvaJwatEaJxaviaruaBraJyaJzaJAaJBar5aap#RnaJCaJDaJE", +"aJFaJGaJHaJIaJJaIjaJKaJLai2ai2agOaJMaedaedabnabnaJNaJO.HZ.LLaJPanlaJQaJRaJSaJTaJUaJVaJWaJXaJYaJZaJ0aJ1aJ2aFUaJ3aJ4aJ5aJ6#44#44#44apkanpanpanp.S.acHauoatBatB#ZH#ZHatDavAaJ7aJ8aJ9aK.aK#aKaaKbaKcaKdaKeaKfaJwatDacGaKgaKhaKiaKjaqw.7oaKkaKlaKmaKnaKoaKpaKqaKraKsaKtaKuaKvaKwaKxaKyaKzaKAaKBaKCaKDaKEaKFaKGaKHaKIaKJaKKaKLaKMaKNaKOaKPaKQ.N#aKR.GAaJwatEaJxaviaruaBraJyaJzaKS.Nz.HGat7#M4aKTaKUaKV", +"aKWaKXaKYaKZ.L7.GpaK0.IUai2ai2ai2agOaedaedaedabn.F7apiaJNaJNanlaK1aK2arRaK3aK3aK4aK5aK6aKRaK7aK8aK9aqha#daL.aL#aLaaLbaLc#44#44#44#44anpanpanpanpacHacHauoauoauoauoauoauoaLdaLeaLfaLgaLhaLiaLjaLkaLlaLmatBaocaLnaLoaLpaLqaLraLsaLtaLuaLvaLwaLxaLyaLzaLAaLBaLCaLDaLEaLFaLGaLHaLIaLJaLKaLLaLMaLNaLOaLPaLQaLRaLSaLTaLUaLVaLWaLXaLYaLZaL0aL1aL2aL3.KtaJwatEaJxaviaruaBraJyaJzaL4aGOatEaEWaL5aL6aL7aJE", +"aL8aKXaL9.Nf.L6agJaD6aM.alcai2ai2ai2agOaedaedaedaJN.GwaK1aK1aK1apiaK1aznaM#aM#.HKaMaaMb.Ja.BJaMcarcaMdaMdaBCaqiaMdaGX.HOaq0#44#44#44apkanpanpanpacHacHacHacHaunavBavBavBaphaMeaMfaMgaMhaFWaMiaMeaMjaI.aMkaMlaMmaLoaMnaMoaMpaMqahWaMraMsaMtaMuaMvaMwaMxaMyaMzaMAaMBaMCaMDaMEaMFaMGaMHaMIaMJaMKaMLaMMaMNaMOaMPaMQaMRaMSaMTaMUaMVaMWaMXaMYaMZ.Jk.KRaJwatEaJxaviaruaBraJyaJzaL4.NzauoanVaM0aM1aM2aM3", +"aM4aM5aM6.P0aM7.GraD6aM.alcalc.Gvai2ai2agOaJMaedaJOazmaJO.GwaaiaaiaK2aM8.I7.I7.I7.I7.I7.I7.I7.I7.OqaM9aN..HM.HMaN#aNaaNbaqZaq0aq0#44#44#44apkanpacHacHacHaunavBavBavBavBaf5aNcaNdaNeaNfakx.HUaNgaNhaNiaNjaNjaMmaJBacIaNkaNlaNmatDaNnaNoaNpaNqaNraNsaNtaNuaNvaNwaNxaNyaNzaNAaNBaNCaNDaNEaNFaNGaNH.ihaNIaNJaNKaNLaNMaNNaNOaNPaNQaNRaNS.L8aAnaNTaNUaJwatEaJxaviaruaBraJyaJzaFqaJBatEaNVaL5ascaNWaNX", +"aNYaNZaN0.P0.O3aN1#93aN2aN3aN3alc.Gvai2ai2agOagOaK1aK2aK1.H1aN4.LO.HZazmanNanNanNanNanNanNaN5aN5amIaN6aikaADazpawmailaecaqZaqZaqZaq0#44#44#44apkaunaunaun.IPacHacHacHacHano.TEaN7ano.KPaN8aN9aO.aO#aNiaNjaOaacGapHar5aObaOcaOd.O9.LFaOeaOfaOgaOhaOiaOjaOk.SYaOlaOmaOnaOoaOpaOqaOraEDaHkaOsaynaOtaOuaOvaOwaOxaOyaOzaOAaOBaOCaODaOE.FT.NuaaiaOFaOGaJwatEaJxaviaruaBraJyaJzaOHaOIavAaOJaOKaOLaOMaON", +"aOOaOPaOQaORaOS.QAaOTaIkaN3aN3aN3alcai2ai2ai2agOaN4aM8.GwaJN.GwaK1aK2.Gwanpanpamg#44#6IaqZaqZ.H7azn#94#2uaOU.Nwaaiax8alEaqZaqZaqZaq0#44#44#44#44avBavBaunacHauoatBatBatEaOVaOWaOXaJKaOVaOYaOZ.Kx.NzaLnaO0aO1aO2aO3aO4aO5aO6aO7aO8azMaO9aP.aP#aPaaPbaPcaPdaPeaPfaPgaPhaPiaPjaPkaPlaPmaPnaPoaPpaPqaPraPsaPtaPuaPv.H8aPwaPxaPyaPzaPAaPBaPC.LJajwaPDaJwatEaJxaviaruaBraJyaJzaPEaI.aPFau.aPGaPHaPIaPJ", +"aPKaPLaPM.ZqaPNaPO.Kp.RraN3aN3aN3alcalcai2ai2ai2aJOaM8aM8aPPaJNarR.LNaPQanpanpanpanp.S..S.aimaimaPRano.EUasFaPSai0aPTabbaqZaqZaqZaqZaq0#44#44#44avBavBaunacHatB#ZHatDavAaPUaPVaPWaPXaPYaPZ.KAaP0aGgaP1aO1aP2aP3aP4aP5aP6aP7aP8aP9aQ.aQ#aQaaQbaQcaQdaEYaQeaQfaKSaQgaQhaQiaQjaFXaQkaQlaQmaQnay#aOsaQoaQpaQqaQrayWaQsazGaQtaQuaQvaQwaQxaQyaQzaQAaQBaJwatEaJxaviaruaBraJyaJzaQCaQDaPFavkaQEaOLaQFaQG"}; diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 7b04293018f..04df023cb19 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -20,11 +20,17 @@ def test_sanity() -> None: assert_image_similar(im.convert("RGB"), hopper(), 23) -def test_read_bpp2() -> None: +def test_bpp2() -> None: with Image.open("Tests/images/hopper_bpp2.xpm") as im: assert_image_similar(im.convert("RGB"), hopper(), 11) +def test_rgb() -> None: + with Image.open("Tests/images/hopper_rgb.xpm") as im: + assert im.mode == "RGB" + assert_image_similar(im, hopper(), 16) + + def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index bfa462c04c9..51d829abe8f 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1650,7 +1650,8 @@ handler. :: XPM ^^^ -Pillow reads X pixmap files (mode ``P``) with 256 colors or less. +Pillow reads X pixmap files as P mode images if there are 256 colors or less, and as +RGB images otherwise. .. _xpm-opening: diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index fcee142e3c2..d80ca7ce45e 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -56,10 +56,6 @@ def _open(self) -> None: palette_length = int(m.group(3)) bpp = int(m.group(4)) - if palette_length > 256: - msg = "cannot read this XPM file" - raise ValueError(msg) - # # load palette description @@ -93,15 +89,16 @@ def _open(self) -> None: msg = "cannot read this XPM file" raise ValueError(msg) - self._mode = "P" - self.palette = ImagePalette.raw("RGB", b"".join(palette.values())) + args: tuple[int, dict[bytes, bytes] | tuple[bytes, ...]] + if palette_length > 256: + self._mode = "RGB" + args = (bpp, palette) + else: + self._mode = "P" + self.palette = ImagePalette.raw("RGB", b"".join(palette.values())) + args = (bpp, tuple(palette.keys())) - palette_keys = tuple(palette.keys()) - self.tile = [ - ImageFile._Tile( - "xpm", (0, 0) + self.size, self.fp.tell(), (bpp, palette_keys) - ) - ] + self.tile = [ImageFile._Tile("xpm", (0, 0) + self.size, self.fp.tell(), args)] def load_read(self, read_bytes: int) -> bytes: # @@ -119,17 +116,27 @@ class XpmDecoder(ImageFile.PyDecoder): def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None - self.fd.readline() # Read '/* pixels */' data = bytearray() - bpp, palette_keys = self.args + bpp, palette = self.args dest_length = self.state.xsize * self.state.ysize + if self.mode == "RGB": + dest_length *= 3 + pixel_header = False while len(data) < dest_length: - s = self.fd.readline().rstrip()[1:] - s = s[: -1 if s.endswith(b'"') else -2] + s = self.fd.readline() + if s.rstrip() == b"/* pixels */" and not pixel_header: + pixel_header = True + continue + if not s: + break + s = b'"'.join(s.split(b'"')[1:-1]) for i in range(0, len(s), bpp): key = s[i : i + bpp] - data += o8(palette_keys.index(key)) + if self.mode == "RGB": + data += palette[key] + else: + data += o8(palette.index(key)) self.set_as_raw(bytes(data)) return -1, 0 From 6512a8e371476b91bc5adfbe076ace2c0f076da2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Apr 2025 23:41:45 +1000 Subject: [PATCH 1627/2374] Test not enough image data --- Tests/test_file_xpm.py | 10 ++++++++++ src/PIL/XpmImagePlugin.py | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 04df023cb19..96365d7f441 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,5 +1,7 @@ from __future__ import annotations +from io import BytesIO + import pytest from PIL import Image, XpmImagePlugin @@ -31,6 +33,14 @@ def test_rgb() -> None: assert_image_similar(im, hopper(), 16) +def test_not_enough_image_data() -> None: + with open(TEST_FILE, "rb") as fp: + data = fp.read().split(b"/* pixels */")[0] + with Image.open(BytesIO(data)) as im: + with pytest.raises(ValueError, match="not enough image data"): + im.load() + + def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index d80ca7ce45e..ff216a6c1eb 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -125,11 +125,11 @@ def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int pixel_header = False while len(data) < dest_length: s = self.fd.readline() + if not s: + break if s.rstrip() == b"/* pixels */" and not pixel_header: pixel_header = True continue - if not s: - break s = b'"'.join(s.split(b'"')[1:-1]) for i in range(0, len(s), bpp): key = s[i : i + bpp] From 34efaaddf306f3a435f490ca6082fc80bc7cad37 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Apr 2025 18:57:31 +1000 Subject: [PATCH 1628/2374] Improved type hints --- src/PIL/XpmImagePlugin.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index ff216a6c1eb..3be240fbc1a 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -37,17 +37,18 @@ class XpmImageFile(ImageFile.ImageFile): format_description = "X11 Pixel Map" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(9)): msg = "not an XPM file" raise SyntaxError(msg) # skip forward to next string while True: - s = self.fp.readline() - if not s: + line = self.fp.readline() + if not line: msg = "broken XPM file" raise SyntaxError(msg) - m = xpm_head.match(s) + m = xpm_head.match(line) if m: break @@ -62,10 +63,10 @@ def _open(self) -> None: palette = {} for _ in range(palette_length): - s = self.fp.readline().rstrip() + line = self.fp.readline().rstrip() - c = s[1 : bpp + 1] - s = s[bpp + 1 : -2].split() + c = line[1 : bpp + 1] + s = line[bpp + 1 : -2].split() for i in range(0, len(s), 2): if s[i] == b"c": @@ -74,9 +75,11 @@ def _open(self) -> None: if rgb == b"None": self.info["transparency"] = c elif rgb.startswith(b"#"): - rgb = int(rgb[1:], 16) + rgb_int = int(rgb[1:], 16) palette[c] = ( - o8((rgb >> 16) & 255) + o8((rgb >> 8) & 255) + o8(rgb & 255) + o8((rgb_int >> 16) & 255) + + o8((rgb_int >> 8) & 255) + + o8(rgb_int & 255) ) else: # unknown colour @@ -106,6 +109,7 @@ def load_read(self, read_bytes: int) -> bytes: xsize, ysize = self.size + assert self.fp is not None s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)] return b"".join(s) @@ -124,15 +128,15 @@ def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int dest_length *= 3 pixel_header = False while len(data) < dest_length: - s = self.fd.readline() - if not s: + line = self.fd.readline() + if not line: break - if s.rstrip() == b"/* pixels */" and not pixel_header: + if line.rstrip() == b"/* pixels */" and not pixel_header: pixel_header = True continue - s = b'"'.join(s.split(b'"')[1:-1]) - for i in range(0, len(s), bpp): - key = s[i : i + bpp] + line = b'"'.join(line.split(b'"')[1:-1]) + for i in range(0, len(line), bpp): + key = line[i : i + bpp] if self.mode == "RGB": data += palette[key] else: From af52060e973063b259708a8e91bfbbf13376c247 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Apr 2025 20:45:53 +1000 Subject: [PATCH 1629/2374] Mention that tobytes() with the raw encoder uses Pack.c --- src/PIL/Image.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 88ea6f3b53e..b419405fb7a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -767,18 +767,20 @@ def tobytes(self, encoder_name: str = "raw", *args: Any) -> bytes: .. warning:: - This method returns the raw image data from the internal - storage. For compressed image data (e.g. PNG, JPEG) use - :meth:`~.save`, with a BytesIO parameter for in-memory - data. - - :param encoder_name: What encoder to use. The default is to - use the standard "raw" encoder. - - A list of C encoders can be seen under - codecs section of the function array in - :file:`_imaging.c`. Python encoders are - registered within the relevant plugins. + This method returns raw image data derived from Pillow's internal + storage. For compressed image data (e.g. PNG, JPEG) use + :meth:`~.save`, with a BytesIO parameter for in-memory data. + + :param encoder_name: What encoder to use. + + The default is to use the standard "raw" encoder. + To see how this packs pixel data into the returned + bytes, see :file:`libImaging/Pack.c`. + + A list of C encoders can be seen under codecs + section of the function array in + :file:`_imaging.c`. Python encoders are registered + within the relevant plugins. :param args: Extra arguments to the encoder. :returns: A :py:class:`bytes` object. """ From dce96089614a2bfac112b5baa5f19d87874f526b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Apr 2025 21:59:04 +1000 Subject: [PATCH 1630/2374] Test unknown colour and missing colour key --- Tests/test_file_xpm.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 96365d7f441..de2d9bb793a 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -33,12 +33,23 @@ def test_rgb() -> None: assert_image_similar(im, hopper(), 16) +def test_cannot_read() -> None: + with open(TEST_FILE, "rb") as fp: + data = fp.read().split(b"#")[0] + with pytest.raises(ValueError, match="cannot read this XPM file"): + with Image.open(BytesIO(data)) as im: + pass + with pytest.raises(ValueError, match="cannot read this XPM file"): + with Image.open(BytesIO(data+b"invalid")) as im: + pass + + def test_not_enough_image_data() -> None: with open(TEST_FILE, "rb") as fp: data = fp.read().split(b"/* pixels */")[0] - with Image.open(BytesIO(data)) as im: - with pytest.raises(ValueError, match="not enough image data"): - im.load() + with Image.open(BytesIO(data)) as im: + with pytest.raises(ValueError, match="not enough image data"): + im.load() def test_invalid_file() -> None: From b2945ec2aaf20d4698479c843f0d26ffcaf6222b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Apr 2025 22:07:55 +1000 Subject: [PATCH 1631/2374] Test truncated header --- Tests/test_file_xpm.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index de2d9bb793a..86d86602f17 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -33,14 +33,22 @@ def test_rgb() -> None: assert_image_similar(im, hopper(), 16) -def test_cannot_read() -> None: +def test_truncated_header() -> None: + data = b"/* XPM */" + with pytest.raises(SyntaxError, match="broken XPM file"): + with XpmImagePlugin.XpmImageFile(BytesIO(data)): + pass + + +def test_cannot_read_color() -> None: with open(TEST_FILE, "rb") as fp: data = fp.read().split(b"#")[0] with pytest.raises(ValueError, match="cannot read this XPM file"): - with Image.open(BytesIO(data)) as im: + with Image.open(BytesIO(data)): pass + with pytest.raises(ValueError, match="cannot read this XPM file"): - with Image.open(BytesIO(data+b"invalid")) as im: + with Image.open(BytesIO(data + b"invalid")): pass From 79f834ef6500774f63b49e19f5c150d9ed6f2681 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Apr 2025 22:26:42 +1000 Subject: [PATCH 1632/2374] If pasting an image onto itself at a lower position, copy from bottom --- Tests/test_image_paste.py | 12 ++++++++++++ src/libImaging/Paste.c | 10 ++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 2cff6c89378..5fee8172950 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -124,6 +124,18 @@ def test_image_solid(self, mode: str) -> None: im = im.crop((12, 23, im2.width + 12, im2.height + 23)) assert_image_equal(im, im2) + @pytest.mark.parametrize("y", [10, -10]) + def test_image_self(self, y: int) -> None: + im = self.gradient_RGB + + im_self = im.copy() + im_self.paste(im_self, (0, y)) + + im_copy = im.copy() + im_copy.paste(im_copy.copy(), (0, y)) + + assert_image_equal(im_self, im_copy) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_image_mask_1(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index 86085942a2e..e2ce00ea626 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -44,8 +44,14 @@ paste( xsize *= pixelsize; - for (y = 0; y < ysize; y++) { - memcpy(imOut->image[y + dy] + dx, imIn->image[y + sy] + sx, xsize); + if (imOut == imIn && dy > sy) { + for (y = ysize - 1; y >= 0; y--) { + memcpy(imOut->image[y + dy] + dx, imIn->image[y + sy] + sx, xsize); + } + } else { + for (y = 0; y < ysize; y++) { + memcpy(imOut->image[y + dy] + dx, imIn->image[y + sy] + sx, xsize); + } } } From 81fa4e18c7c11c115c3bed5200b4129f0097b00f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Apr 2025 08:19:18 +1000 Subject: [PATCH 1633/2374] If pasting image to self at lower position with mask, copy from bottom --- Tests/test_image_paste.py | 11 +++++--- src/libImaging/Paste.c | 53 +++++++++++++++++++++++---------------- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 5fee8172950..37e4df10370 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -125,14 +125,17 @@ def test_image_solid(self, mode: str) -> None: assert_image_equal(im, im2) @pytest.mark.parametrize("y", [10, -10]) - def test_image_self(self, y: int) -> None: - im = self.gradient_RGB + @pytest.mark.parametrize("mode", ["L", "RGB"]) + @pytest.mark.parametrize("mask_mode", ["", "1", "L", "LA", "RGBa"]) + def test_image_self(self, y: int, mode: str, mask_mode: str) -> None: + im = getattr(self, "gradient_" + mode) + mask = Image.new(mask_mode, im.size, 0xFFFFFFFF) if mask_mode else None im_self = im.copy() - im_self.paste(im_self, (0, y)) + im_self.paste(im_self, (0, y), mask) im_copy = im.copy() - im_copy.paste(im_copy.copy(), (0, y)) + im_copy.paste(im_copy.copy(), (0, y), mask) assert_image_equal(im_self, im_copy) diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index e2ce00ea626..9942f9c1c61 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -23,6 +23,18 @@ #include "Imaging.h" +#define PREPARE_PASTE_LOOP() \ + int y, y_end, offset; \ + if (imOut == imIn && dy > sy) { \ + y = ysize - 1; \ + y_end = -1; \ + offset = -1; \ + } else { \ + y = 0; \ + y_end = ysize; \ + offset = 1; \ + } + static inline void paste( Imaging imOut, @@ -37,21 +49,14 @@ paste( ) { /* paste opaque region */ - int y; - dx *= pixelsize; sx *= pixelsize; xsize *= pixelsize; - if (imOut == imIn && dy > sy) { - for (y = ysize - 1; y >= 0; y--) { - memcpy(imOut->image[y + dy] + dx, imIn->image[y + sy] + sx, xsize); - } - } else { - for (y = 0; y < ysize; y++) { - memcpy(imOut->image[y + dy] + dx, imIn->image[y + sy] + sx, xsize); - } + PREPARE_PASTE_LOOP(); + for (; y != y_end; y += offset) { + memcpy(imOut->image[y + dy] + dx, imIn->image[y + sy] + sx, xsize); } } @@ -70,12 +75,13 @@ paste_mask_1( ) { /* paste with mode "1" mask */ - int x, y; + int x; + PREPARE_PASTE_LOOP(); if (imOut->image8) { int in_i16 = strncmp(imIn->mode, "I;16", 4) == 0; int out_i16 = strncmp(imOut->mode, "I;16", 4) == 0; - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { UINT8 *out = imOut->image8[y + dy] + dx; if (out_i16) { out += dx; @@ -103,7 +109,7 @@ paste_mask_1( } } else { - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { INT32 *out = imOut->image32[y + dy] + dx; INT32 *in = imIn->image32[y + sy] + sx; UINT8 *mask = imMask->image8[y + sy] + sx; @@ -132,11 +138,12 @@ paste_mask_L( ) { /* paste with mode "L" matte */ - int x, y; + int x; unsigned int tmp1; + PREPARE_PASTE_LOOP(); if (imOut->image8) { - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { UINT8 *out = imOut->image8[y + dy] + dx; UINT8 *in = imIn->image8[y + sy] + sx; UINT8 *mask = imMask->image8[y + sy] + sx; @@ -147,7 +154,7 @@ paste_mask_L( } } else { - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); UINT8 *mask = (UINT8 *)(imMask->image8[y + sy] + sx); @@ -180,11 +187,12 @@ paste_mask_RGBA( ) { /* paste with mode "RGBA" matte */ - int x, y; + int x; unsigned int tmp1; + PREPARE_PASTE_LOOP(); if (imOut->image8) { - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { UINT8 *out = imOut->image8[y + dy] + dx; UINT8 *in = imIn->image8[y + sy] + sx; UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx * 4 + 3; @@ -195,7 +203,7 @@ paste_mask_RGBA( } } else { - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); UINT8 *mask = (UINT8 *)(imMask->image32[y + sy] + sx); @@ -228,11 +236,12 @@ paste_mask_RGBa( ) { /* paste with mode "RGBa" matte */ - int x, y; + int x; unsigned int tmp1; + PREPARE_PASTE_LOOP(); if (imOut->image8) { - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { UINT8 *out = imOut->image8[y + dy] + dx; UINT8 *in = imIn->image8[y + sy] + sx; UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx * 4 + 3; @@ -243,7 +252,7 @@ paste_mask_RGBa( } } else { - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); UINT8 *mask = (UINT8 *)(imMask->image32[y + sy] + sx); From 04909483a70e29cd265446199a8f22c1acdc6db7 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 12 Apr 2025 17:29:06 +1000 Subject: [PATCH 1634/2374] Remove GPL v2 license (#8884) --- wheels/dependency_licenses/FREETYPE2.txt | 345 ----------------------- 1 file changed, 345 deletions(-) diff --git a/wheels/dependency_licenses/FREETYPE2.txt b/wheels/dependency_licenses/FREETYPE2.txt index 93efc612676..8f2345992b0 100644 --- a/wheels/dependency_licenses/FREETYPE2.txt +++ b/wheels/dependency_licenses/FREETYPE2.txt @@ -211,351 +211,6 @@ Legal Terms --- end of FTL.TXT --- --------------------------------------------------------------------------- - - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Library General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Library General -Public License instead of this License. - --------------------------------------------------------------------------- - The following license details are part of `src/bdf/README`: ``` From 07d78002488063f29cd24e49dd38b5fc2f319989 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Apr 2025 19:08:45 +1000 Subject: [PATCH 1635/2374] Removed release notes update --- docs/releasenotes/11.2.0.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index ed41c21164c..de3db3c84fa 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -106,6 +106,5 @@ Pillow images can also be converted to Arrow objects:: Reading and writing AVIF images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Pillow can now read and write AVIF images. However, due to concern over size, this -functionality is not included in our prebuilt wheels. You will need to build Pillow -from source with libavif 1.0.0 or later. +Pillow can now read and write AVIF images. If you are building Pillow from source, this +will require libavif 1.0.0 or later. From 8dafc38371b99616d3fdcc926f4f5a1f7762ae3f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Apr 2025 19:24:35 +1000 Subject: [PATCH 1636/2374] Added 11.2.1 release notes --- docs/releasenotes/11.2.0.rst | 6 ++++++ docs/releasenotes/11.2.1.rst | 11 +++++++++++ docs/releasenotes/index.rst | 1 + 3 files changed, 18 insertions(+) create mode 100644 docs/releasenotes/11.2.1.rst diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index de3db3c84fa..3a7d618e4e5 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -1,6 +1,12 @@ 11.2.0 ------ +.. warning:: + + The release of Pillow 11.2.0 was halted prematurely, due to concern over the size + of Pillow wheels containing libavif. Instead, Pillow 11.2.1 has been released, + without libavif included in the wheels. + Security ======== diff --git a/docs/releasenotes/11.2.1.rst b/docs/releasenotes/11.2.1.rst new file mode 100644 index 00000000000..7d9a4038268 --- /dev/null +++ b/docs/releasenotes/11.2.1.rst @@ -0,0 +1,11 @@ +11.2.1 +------ + +Reading and writing AVIF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The release of Pillow 11.2.0 was halted prematurely, due to concern over the size of +Pillow wheels containing libavif. + +Instead, Pillow 11.2.1's wheels do not contain libavif. If you wish to read and write +AVIF images, you will need to build Pillow from source with libavif 1.0.0 or later. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index be9f623d05d..0d159fc510d 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 11.2.1 11.2.0 11.1.0 11.0.0 From 7a0092f2072a5deca433e2a6b752eced49e4ee16 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 12 Apr 2025 18:56:38 +0300 Subject: [PATCH 1637/2374] Remove incomplete 11.2.0 release, bill as 11.2.1 instead --- docs/deprecations.rst | 2 +- docs/handbook/image-file-formats.rst | 2 +- docs/reference/ImageDraw.rst | 8 +- docs/reference/ImageGrab.rst | 2 +- docs/releasenotes/11.2.0.rst | 116 -------------------------- docs/releasenotes/11.2.1.rst | 117 +++++++++++++++++++++++++-- docs/releasenotes/index.rst | 1 - src/PIL/Image.py | 2 +- 8 files changed, 120 insertions(+), 130 deletions(-) delete mode 100644 docs/releasenotes/11.2.0.rst diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 634cee6894c..7f8e76bccc5 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -186,7 +186,7 @@ ExifTags.IFD.Makernote Image.Image.get_child_images() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. deprecated:: 11.2.0 +.. deprecated:: 11.2.1 ``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow 13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index bfa462c04c9..49431b3d095 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -170,7 +170,7 @@ DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode. in ``P`` mode. -.. versionadded:: 11.2.0 +.. versionadded:: 11.2.1 DXT1, DXT3, DXT5, BC2, BC3 and BC5 pixel formats can be saved:: im.save(out, pixel_format="DXT1") diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index b2f1bdc9314..bd6f6b048ac 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -391,7 +391,7 @@ Methods the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. - .. versionadded:: 11.2.0 ``"justify"`` + .. versionadded:: 11.2.1 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -462,7 +462,7 @@ Methods the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. - .. versionadded:: 11.2.0 ``"justify"`` + .. versionadded:: 11.2.1 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -609,7 +609,7 @@ Methods the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. - .. versionadded:: 11.2.0 ``"justify"`` + .. versionadded:: 11.2.1 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -663,7 +663,7 @@ Methods the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. - .. versionadded:: 11.2.0 ``"justify"`` + .. versionadded:: 11.2.1 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index d59ed0bd6fd..1e827a6764e 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -44,7 +44,7 @@ or the clipboard to a PIL image memory. :param window: HWND, to capture a single window. Windows only. - .. versionadded:: 11.2.0 + .. versionadded:: 11.2.1 :return: An image .. py:function:: grabclipboard() diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst deleted file mode 100644 index 3a7d618e4e5..00000000000 --- a/docs/releasenotes/11.2.0.rst +++ /dev/null @@ -1,116 +0,0 @@ -11.2.0 ------- - -.. warning:: - - The release of Pillow 11.2.0 was halted prematurely, due to concern over the size - of Pillow wheels containing libavif. Instead, Pillow 11.2.1 has been released, - without libavif included in the wheels. - -Security -======== - -Undefined shift when loading compressed DDS images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When loading some compressed DDS formats, an integer was bitshifted by 24 places to -generate the 32 bits of the lookup table. This was undefined behaviour, and has been -present since Pillow 3.4.0. - -Deprecations -============ - -Image.Image.get_child_images() -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 11.2.0 - -``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow -13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The -method uses an image's file pointer, and so child images could only be retrieved from -an :py:class:`PIL.ImageFile.ImageFile` instance. - -API Changes -=========== - -``append_images`` no longer requires ``save_all`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Previously, ``save_all`` was required to in order to use ``append_images``. Now, -``save_all`` will default to ``True`` if ``append_images`` is not empty and the format -supports saving multiple frames:: - - im.save("out.gif", append_images=ims) - -API Additions -============= - -``"justify"`` multiline text alignment -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be -aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`:: - - from PIL import Image, ImageDraw - im = Image.new("RGB", (50, 25)) - draw = ImageDraw.Draw(im) - draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify") - draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify") - -Specify window in ImageGrab on Windows -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When using :py:meth:`~PIL.ImageGrab.grab`, a specific window can be selected using the -HWND:: - - from PIL import ImageGrab - ImageGrab.grab(window=hwnd) - -Check for MozJPEG -^^^^^^^^^^^^^^^^^ - -You can check if Pillow has been built against the MozJPEG version of the -libjpeg library, and what version of MozJPEG is being used:: - - from PIL import features - features.check_feature("mozjpeg") # True or False - features.version_feature("mozjpeg") # "4.1.1" for example, or None - -Saving compressed DDS images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT3, -DXT5, BC2, BC3 and BC5 are supported:: - - im.save("out.dds", pixel_format="DXT1") - -Other Changes -============= - -Arrow support -^^^^^^^^^^^^^ - -`Arrow `__ is an in-memory data exchange format that is the -spiritual successor to the NumPy array interface. It provides for zero-copy access to -columnar data, which in our case is ``Image`` data. - -To create an image with zero-copy shared memory from an object exporting the -arrow_c_array interface protocol:: - - from PIL import Image - import pyarrow as pa - arr = pa.array([0]*(5*5*4), type=pa.uint8()) - im = Image.fromarrow(arr, 'RGBA', (5, 5)) - -Pillow images can also be converted to Arrow objects:: - - from PIL import Image - import pyarrow as pa - im = Image.open('hopper.jpg') - arr = pa.array(im) - -Reading and writing AVIF images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow can now read and write AVIF images. If you are building Pillow from source, this -will require libavif 1.0.0 or later. diff --git a/docs/releasenotes/11.2.1.rst b/docs/releasenotes/11.2.1.rst index 7d9a4038268..5c6d40d9df3 100644 --- a/docs/releasenotes/11.2.1.rst +++ b/docs/releasenotes/11.2.1.rst @@ -1,11 +1,118 @@ 11.2.1 ------ +.. warning:: + + The release of Pillow *11.2.0* was halted prematurely, due to hitting PyPI's + project size limit and concern over the size of Pillow wheels containing libavif. + The PyPI limit has now been increased and Pillow *11.2.1* has been released + instead, without libavif included in the wheels. + To avoid confusion, the incomplete 11.2.0 release has been removed from PyPI. + +Security +======== + +Undefined shift when loading compressed DDS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When loading some compressed DDS formats, an integer was bitshifted by 24 places to +generate the 32 bits of the lookup table. This was undefined behaviour, and has been +present since Pillow 3.4.0. + +Deprecations +============ + +Image.Image.get_child_images() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.2.1 + +``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow +13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The +method uses an image's file pointer, and so child images could only be retrieved from +an :py:class:`PIL.ImageFile.ImageFile` instance. + +API Changes +=========== + +``append_images`` no longer requires ``save_all`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, ``save_all`` was required to in order to use ``append_images``. Now, +``save_all`` will default to ``True`` if ``append_images`` is not empty and the format +supports saving multiple frames:: + + im.save("out.gif", append_images=ims) + +API Additions +============= + +``"justify"`` multiline text alignment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be +aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`:: + + from PIL import Image, ImageDraw + im = Image.new("RGB", (50, 25)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify") + draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify") + +Specify window in ImageGrab on Windows +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When using :py:meth:`~PIL.ImageGrab.grab`, a specific window can be selected using the +HWND:: + + from PIL import ImageGrab + ImageGrab.grab(window=hwnd) + +Check for MozJPEG +^^^^^^^^^^^^^^^^^ + +You can check if Pillow has been built against the MozJPEG version of the +libjpeg library, and what version of MozJPEG is being used:: + + from PIL import features + features.check_feature("mozjpeg") # True or False + features.version_feature("mozjpeg") # "4.1.1" for example, or None + +Saving compressed DDS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT3, +DXT5, BC2, BC3 and BC5 are supported:: + + im.save("out.dds", pixel_format="DXT1") + +Other Changes +============= + +Arrow support +^^^^^^^^^^^^^ + +`Arrow `__ is an in-memory data exchange format that is the +spiritual successor to the NumPy array interface. It provides for zero-copy access to +columnar data, which in our case is ``Image`` data. + +To create an image with zero-copy shared memory from an object exporting the +arrow_c_array interface protocol:: + + from PIL import Image + import pyarrow as pa + arr = pa.array([0]*(5*5*4), type=pa.uint8()) + im = Image.fromarrow(arr, 'RGBA', (5, 5)) + +Pillow images can also be converted to Arrow objects:: + + from PIL import Image + import pyarrow as pa + im = Image.open('hopper.jpg') + arr = pa.array(im) + Reading and writing AVIF images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The release of Pillow 11.2.0 was halted prematurely, due to concern over the size of -Pillow wheels containing libavif. - -Instead, Pillow 11.2.1's wheels do not contain libavif. If you wish to read and write -AVIF images, you will need to build Pillow from source with libavif 1.0.0 or later. +Pillow can now read and write AVIF images when built from source with libavif 1.0.0 +or later. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 0d159fc510d..a116ef056a7 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -15,7 +15,6 @@ expected to be backported to earlier versions. :maxdepth: 2 11.2.1 - 11.2.0 11.1.0 11.0.0 10.4.0 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 88ea6f3b53e..ded40bc5ddc 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3362,7 +3362,7 @@ def fromarrow( See: :ref:`arrow-support` for more detailed information - .. versionadded:: 11.2.0 + .. versionadded:: 11.2.1 """ if not hasattr(obj, "__arrow_c_array__"): From 339bc5db93bd95decf65a59fab273f300db6594d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 12 Apr 2025 19:55:46 +0300 Subject: [PATCH 1638/2374] 11.2.1 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index e93c7887b80..9380e992753 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "11.2.0.dev0" +__version__ = "11.2.1" From f9083264ff561389ee5931598df9bffe269db504 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 12 Apr 2025 20:56:35 +0300 Subject: [PATCH 1639/2374] 11.3.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 9380e992753..ac678c7d26e 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "11.2.1" +__version__ = "11.3.0.dev0" From 529402143826732973463104000fd91d0a271103 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Apr 2025 12:09:46 +1100 Subject: [PATCH 1640/2374] Removed Fedora 40 --- .github/workflows/test-docker.yml | 1 - docs/installation/platform-support.rst | 2 -- 2 files changed, 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 25aef55fb13..9e42ed884ae 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -47,7 +47,6 @@ jobs: centos-stream-10-amd64, debian-12-bookworm-x86, debian-12-bookworm-amd64, - fedora-40-amd64, fedora-41-amd64, gentoo, ubuntu-22.04-jammy-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 9eafad3c4b7..36b9a7bddfc 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -31,8 +31,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ -| Fedora 40 | 3.12 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 41 | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | From c6434dbbbc667d42ad236aa2eeb407a44bbd2174 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 13 Apr 2025 23:00:06 +1000 Subject: [PATCH 1641/2374] Set color table fourth channel to zero for 1 and L mode when saving --- src/PIL/BmpImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 43131cfe2fa..54fc69ab454 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -445,9 +445,9 @@ def _save( image = stride * im.size[1] if im.mode == "1": - palette = b"".join(o8(i) * 4 for i in (0, 255)) + palette = b"".join(o8(i) * 3 + b"\x00" for i in (0, 255)) elif im.mode == "L": - palette = b"".join(o8(i) * 4 for i in range(256)) + palette = b"".join(o8(i) * 3 + b"\x00" for i in range(256)) elif im.mode == "P": palette = im.im.getpalette("RGB", "BGRX") colors = len(palette) // 4 From 4716bb78189108ceffcc80db226fe099cca20448 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 13 Apr 2025 23:59:05 +1000 Subject: [PATCH 1642/2374] Update macOS tested Pillow versions (#8890) --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 36b9a7bddfc..ca810dc2ad3 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -71,7 +71,7 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+============================+==================+==============+ -| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.1.0 |arm | +| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.2.1 |arm | | +----------------------------+------------------+ | | | 3.8 | 10.4.0 | | +----------------------------------+----------------------------+------------------+--------------+ From 8b1777b9997a48cd59a3bddd888bebddd8adc6de Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Mon, 14 Apr 2025 14:51:01 -0400 Subject: [PATCH 1643/2374] Move XV Thumbnails to read only section --- docs/handbook/image-file-formats.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 49431b3d095..46fe8b630db 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1664,6 +1664,11 @@ The :py:meth:`~PIL.Image.open` method sets the following Transparency color index. This key is omitted if the image is not transparent. +XV Thumbnails +^^^^^^^^^^^^^ + +Pillow can read XV thumbnail files. + Write-only formats ------------------ @@ -1769,11 +1774,6 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 5.3.0 -XV Thumbnails -^^^^^^^^^^^^^ - -Pillow can read XV thumbnail files. - Identify-only formats --------------------- From 507fefbce4c3b8c46c6c21483d488884f1b5a8e1 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 15 Apr 2025 21:02:35 +1000 Subject: [PATCH 1644/2374] Python 3.13 is tested on Arch (#8894) --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index ca810dc2ad3..d5634b384a5 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -23,7 +23,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Amazon Linux 2023 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Arch | 3.12 | x86-64 | +| Arch | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ From 6ea7dc8eeafaf7528d2817bdd443de367eca2940 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 16 Apr 2025 17:06:52 +1000 Subject: [PATCH 1645/2374] Add Fedora 42 (#8899) --- .github/workflows/test-docker.yml | 1 + docs/installation/platform-support.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 9e42ed884ae..0b90732eba7 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -48,6 +48,7 @@ jobs: debian-12-bookworm-x86, debian-12-bookworm-amd64, fedora-41-amd64, + fedora-42-amd64, gentoo, ubuntu-22.04-jammy-amd64, ubuntu-24.04-noble-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index d5634b384a5..d751620fd2b 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -33,6 +33,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 41 | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 42 | 3.13 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 13 Ventura | 3.9 | x86-64 | From f630ec097b4d5d2a132d4ade5e5b903452bf27d8 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 16 Apr 2025 21:05:08 +1000 Subject: [PATCH 1646/2374] Build Windows arm64 wheels on arm64 runner (#8898) --- .github/workflows/wheels.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 40d3dc7e882..33e1976f096 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -121,14 +121,17 @@ jobs: windows: if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' name: Windows ${{ matrix.cibw_arch }} - runs-on: windows-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - cibw_arch: x86 + os: windows-latest - cibw_arch: AMD64 + os: windows-latest - cibw_arch: ARM64 + os: windows-11-arm steps: - uses: actions/checkout@v4 with: From 3d4119521c853e1014012f73fdf9b2a8dc137722 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 17 Apr 2025 01:49:57 +1000 Subject: [PATCH 1647/2374] Close file pointer earlier (#8895) --- Tests/test_file_bmp.py | 12 ++++++------ Tests/test_file_jpeg2k.py | 4 ++-- Tests/test_file_libtiff.py | 12 ++++++------ Tests/test_file_libtiff_small.py | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 7576507118b..746b2e18061 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -190,9 +190,9 @@ def test_rle8() -> None: # Signal end of bitmap before the image is finished with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp: data = fp.read(1063) + b"\x01" - with Image.open(io.BytesIO(data)) as im: - with pytest.raises(ValueError): - im.load() + with Image.open(io.BytesIO(data)) as im: + with pytest.raises(ValueError): + im.load() def test_rle4() -> None: @@ -214,9 +214,9 @@ def test_rle4() -> None: def test_rle8_eof(file_name: str, length: int) -> None: with open(file_name, "rb") as fp: data = fp.read(length) - with Image.open(io.BytesIO(data)) as im: - with pytest.raises(ValueError): - im.load() + with Image.open(io.BytesIO(data)) as im: + with pytest.raises(ValueError): + im.load() def test_offset() -> None: diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 4095bfaf2c5..a5365a90d44 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -457,8 +457,8 @@ def test_comment() -> None: # Test an image that is truncated partway through a codestream with open("Tests/images/comment.jp2", "rb") as fp: b = BytesIO(fp.read(130)) - with Image.open(b) as im: - pass + with Image.open(b) as im: + pass def test_save_comment(card: ImageFile.ImageFile) -> None: diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 9916215fb95..1ec39eba588 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -81,7 +81,7 @@ def test_g4_tiff_bytesio(self, tmp_path: Path) -> None: s = io.BytesIO() with open(test_file, "rb") as f: s.write(f.read()) - s.seek(0) + s.seek(0) with Image.open(s) as im: assert im.size == (500, 500) self._assert_noerr(tmp_path, im) @@ -1050,12 +1050,12 @@ def test_old_style_jpeg_orientation(self) -> None: with open("Tests/images/old-style-jpeg-compression.tif", "rb") as fp: data = fp.read() - # Set EXIF Orientation to 2 - data = data[:102] + b"\x02" + data[103:] + # Set EXIF Orientation to 2 + data = data[:102] + b"\x02" + data[103:] - with Image.open(io.BytesIO(data)) as im: - im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + with Image.open(io.BytesIO(data)) as im: + im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") def test_open_missing_samplesperpixel(self) -> None: with Image.open( diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 617e1e89c72..65ba80c2076 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -32,7 +32,7 @@ def test_g4_hopper_bytesio(self, tmp_path: Path) -> None: s = BytesIO() with open(test_file, "rb") as f: s.write(f.read()) - s.seek(0) + s.seek(0) with Image.open(s) as im: assert im.size == (128, 128) self._assert_noerr(tmp_path, im) From ccc4668d4ed27754a09f819462732dc42c584fc0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Apr 2025 08:04:34 +1000 Subject: [PATCH 1648/2374] Updated harfbuzz to 11.1.0 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0df22c4cbe9..d53cf059bab 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -38,7 +38,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.3 -HARFBUZZ_VERSION=11.0.1 +HARFBUZZ_VERSION=11.1.0 LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 3e75c141105..17fc37572e8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ def cmd_msbuild( "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "11.0.1", + "HARFBUZZ": "11.1.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.17", "LIBAVIF": "1.2.1", From cccc07269ae60226f0bb6475970d6fab255538b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Apr 2025 19:23:24 +1000 Subject: [PATCH 1649/2374] Do not justify a single word --- .../multiline_text_justify_single_word.png | Bin 0 -> 2436 bytes Tests/test_imagefont.py | 12 ++++++ src/PIL/ImageDraw.py | 36 +++++++++--------- 3 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 Tests/images/multiline_text_justify_single_word.png diff --git a/Tests/images/multiline_text_justify_single_word.png b/Tests/images/multiline_text_justify_single_word.png new file mode 100644 index 0000000000000000000000000000000000000000..e124e91f5e34d64b002b06f5c66fc72ddb80a31c GIT binary patch literal 2436 zcmY*beLRzEA0J0yB(}_ud7mN7_>h-jBJ%#S6r0e=A!LqpWpoYZ?RWh!4&n+g#Fg+L<5j4t5r#GU+=GKvJekb!n|JNhWUPt9 zc^|#kHd}rgFLU3^f{E8JRg_;!n-Rw$$N!R6_4e#U2Z5kCJ=I%2#>%{yn3%dzS#+Wu zis~bqn=&_Q5o#_&bJF&+0JS885_d(Tw!_WYY<6l#W@_pK4rhf=fsPINA(y8FMck}S zEgcDgW>S}9iBZ(Fz{iY5M4o*(m+Psr&3F;BPU)jU z;U5t#$D5vHtlf=C;8YAqOpJ~7g)J>DWHOmTq0~}-I6$6o4n^Gxta3SBg(~aU(b3uY zJ25soS`USKo~mC^P|#8WL^uFb>PV@ptDBjbvGmt;v+rDfCl+%v){>kRCteYk@$X zJP80N#BWA{m6X_PUO7ikj!VkjnfPPJDkFNu1L9(dQ~Lbn%a>_8PmZt7`up!$9V+xN zu;$$5LW|?R>+XNupL?Mj_8NJIt0^9L2dZ4}3qO^VsO7r7|NPn6s*cT8BgasEVq;?1 zXWrVmRBbOy0y~IVpGDS5wB(}2f>}ismC<-xv@-Y7Mi@cgj=lSnqLNZ#LP7_k)42m{C8iFG~*YPyTz|nw=f0 z3#`)7v9r1P%+#|RV_pCbMMZ%mlGRn7K7?YcB^|Eyoeh41n4}Ivb#<#~v<98q<>i_h z8gaJ}QBhG>+;R1<73E}e-4s+*=A}~qz(B#XS-KLU=nWuqEvW76^Qaex86y`CGzxrj;ip{bdf)!8A#n%l+2s!(YD zEkIO31LWL`7#o`oqNN=VlX>*eOrBth?s7@XR3H$<#l|ungGm+a_4V;nknK7Z zZ=(>vO5)-qBO@b&gF)xduXJD9up>=0={VTiYrx@vIyieZXVp%qVd-=_-b~oj(=$Ab z!D2T7YI}DT7Wy7LRu+7bf71FYF9r>75QiHWpF4?-9YCb8cvDwb zZb!%Z#FGpg8=J<)M!=#)?ZQQp(}d2`7a9G%=OeW|Fm*A zItNp70_v6IWN#dXs;dh{BGK4bLK9-Yib-joC9ypRA4w#(6O&J$Hod4B@jX_yr7v=s zl)(N`aMPYN5l!$m8qTX!ABWHHptd09x@P{H5+5Ibbf>A)16(gG>ZLWq8|rIocXdS; z_kSc>YGeeXQ!M}4$@o)lhpLQ>%*&TAftoQoI;x_gl4M6fmf}>;%?|2IqZ{$}s?B-W zp8huv6W_4*>bl^=!VHe)x($ww&P-3AYGN>%%%!DNts7+CrWrVr_gX!xkllq?(S~+y@-R3j!WyANv;eA<2h9C z0FTED3kzHB#Jn?nydSHOpPvudqN#w-KhQ21zApiC2&{KyWyOq|keT_owl;Pm3xpF8 zoqt)9JMz5TCQkJWTG{Y-e`ZJ{TKUb$NRwOE=#y+{i`}<9TmDbST5fJu=5S>GwU_y> z{HBbjPAM|5S(_|_lH)|;RSjcsxIasdjWoo%TL)g5hoCeviZ9 zXjq+SWwWjCL7$HO_%327OP&>cSXGXFoERKzZEI@;D^v?abtZ#mt}AY(=bvyjed-+E zyrHKvyHYCqXQ^0h{`&Rn3(i?*$Rl$myJ#j{Nm5dh@>QNO5(%MrKq%9H|6N{E!uIK| zZzi<;My1AV)&Ldi None: + im = Image.new("RGB", (185, 65)) + draw = ImageDraw.Draw(im) + draw.multiline_text( + (0, 0), "hey you\nyou are awesome\nthis", font=font, align="justify" + ) + + assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_single_word.png") + + def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e6c7b029830..e865f451631 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -772,24 +772,26 @@ def _prepare_multiline_text( if align == "justify" and width_difference != 0: words = line.split(" " if isinstance(text, str) else b" ") - word_widths = [ - self.textlength( - word, - font, - direction=direction, - features=features, - language=language, - embedded_color=embedded_color, - ) - for word in words - ] - width_difference = max_width - sum(word_widths) - for i, word in enumerate(words): - parts.append(((left, top), word)) - left += word_widths[i] + width_difference / (len(words) - 1) - else: - parts.append(((left, top), line)) + if len(words) > 1: + word_widths = [ + self.textlength( + word, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, + ) + for word in words + ] + width_difference = max_width - sum(word_widths) + for i, word in enumerate(words): + parts.append(((left, top), word)) + left += word_widths[i] + width_difference / (len(words) - 1) + top += line_spacing + continue + parts.append(((left, top), line)) top += line_spacing return font, anchor, parts From b955cee725da2613b34145eea56227c57ec414d4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Apr 2025 19:36:52 +1000 Subject: [PATCH 1650/2374] Do not justify last line --- .../images/multiline_text_justify_last_line.png | Bin 0 -> 3581 bytes .../multiline_text_justify_single_word.png | Bin 2436 -> 0 bytes Tests/test_imagefont.py | 11 +++++++---- src/PIL/ImageDraw.py | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 Tests/images/multiline_text_justify_last_line.png delete mode 100644 Tests/images/multiline_text_justify_single_word.png diff --git a/Tests/images/multiline_text_justify_last_line.png b/Tests/images/multiline_text_justify_last_line.png new file mode 100644 index 0000000000000000000000000000000000000000..bcc1afd72969c4a476cabc5a074154994a222a94 GIT binary patch literal 3581 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 literal 0 HcmV?d00001 diff --git a/Tests/images/multiline_text_justify_single_word.png b/Tests/images/multiline_text_justify_single_word.png deleted file mode 100644 index e124e91f5e34d64b002b06f5c66fc72ddb80a31c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2436 zcmY*beLRzEA0J0yB(}_ud7mN7_>h-jBJ%#S6r0e=A!LqpWpoYZ?RWh!4&n+g#Fg+L<5j4t5r#GU+=GKvJekb!n|JNhWUPt9 zc^|#kHd}rgFLU3^f{E8JRg_;!n-Rw$$N!R6_4e#U2Z5kCJ=I%2#>%{yn3%dzS#+Wu zis~bqn=&_Q5o#_&bJF&+0JS885_d(Tw!_WYY<6l#W@_pK4rhf=fsPINA(y8FMck}S zEgcDgW>S}9iBZ(Fz{iY5M4o*(m+Psr&3F;BPU)jU z;U5t#$D5vHtlf=C;8YAqOpJ~7g)J>DWHOmTq0~}-I6$6o4n^Gxta3SBg(~aU(b3uY zJ25soS`USKo~mC^P|#8WL^uFb>PV@ptDBjbvGmt;v+rDfCl+%v){>kRCteYk@$X zJP80N#BWA{m6X_PUO7ikj!VkjnfPPJDkFNu1L9(dQ~Lbn%a>_8PmZt7`up!$9V+xN zu;$$5LW|?R>+XNupL?Mj_8NJIt0^9L2dZ4}3qO^VsO7r7|NPn6s*cT8BgasEVq;?1 zXWrVmRBbOy0y~IVpGDS5wB(}2f>}ismC<-xv@-Y7Mi@cgj=lSnqLNZ#LP7_k)42m{C8iFG~*YPyTz|nw=f0 z3#`)7v9r1P%+#|RV_pCbMMZ%mlGRn7K7?YcB^|Eyoeh41n4}Ivb#<#~v<98q<>i_h z8gaJ}QBhG>+;R1<73E}e-4s+*=A}~qz(B#XS-KLU=nWuqEvW76^Qaex86y`CGzxrj;ip{bdf)!8A#n%l+2s!(YD zEkIO31LWL`7#o`oqNN=VlX>*eOrBth?s7@XR3H$<#l|ungGm+a_4V;nknK7Z zZ=(>vO5)-qBO@b&gF)xduXJD9up>=0={VTiYrx@vIyieZXVp%qVd-=_-b~oj(=$Ab z!D2T7YI}DT7Wy7LRu+7bf71FYF9r>75QiHWpF4?-9YCb8cvDwb zZb!%Z#FGpg8=J<)M!=#)?ZQQp(}d2`7a9G%=OeW|Fm*A zItNp70_v6IWN#dXs;dh{BGK4bLK9-Yib-joC9ypRA4w#(6O&J$Hod4B@jX_yr7v=s zl)(N`aMPYN5l!$m8qTX!ABWHHptd09x@P{H5+5Ibbf>A)16(gG>ZLWq8|rIocXdS; z_kSc>YGeeXQ!M}4$@o)lhpLQ>%*&TAftoQoI;x_gl4M6fmf}>;%?|2IqZ{$}s?B-W zp8huv6W_4*>bl^=!VHe)x($ww&P-3AYGN>%%%!DNts7+CrWrVr_gX!xkllq?(S~+y@-R3j!WyANv;eA<2h9C z0FTED3kzHB#Jn?nydSHOpPvudqN#w-KhQ21zApiC2&{KyWyOq|keT_owl;Pm3xpF8 zoqt)9JMz5TCQkJWTG{Y-e`ZJ{TKUb$NRwOE=#y+{i`}<9TmDbST5fJu=5S>GwU_y> z{HBbjPAM|5S(_|_lH)|;RSjcsxIasdjWoo%TL)g5hoCeviZ9 zXjq+SWwWjCL7$HO_%327OP&>cSXGXFoERKzZEI@;D^v?abtZ#mt}AY(=bvyjed-+E zyrHKvyHYCqXQ^0h{`&Rn3(i?*$Rl$myJ#j{Nm5dh@>QNO5(%MrKq%9H|6N{E!uIK| zZzi<;My1AV)&Ldi None: - im = Image.new("RGB", (185, 65)) + im = Image.new("RGB", (280, 60)) draw = ImageDraw.Draw(im) draw.multiline_text( - (0, 0), "hey you\nyou are awesome\nthis", font=font, align="justify" + (0, 0), + "hey you you are awesome\nthis\nlooks awkward", + font=font, + align="justify", ) - assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_single_word.png") + assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_last_line.png") def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e865f451631..47ae575c9e6 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -770,7 +770,7 @@ def _prepare_multiline_text( msg = 'align must be "left", "center", "right" or "justify"' raise ValueError(msg) - if align == "justify" and width_difference != 0: + if align == "justify" and width_difference != 0 and idx != len(lines) - 1: words = line.split(" " if isinstance(text, str) else b" ") if len(words) > 1: word_widths = [ From bc05a88ce664dff5eee1bb024e4922d86ad86f96 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Apr 2025 20:56:02 +1000 Subject: [PATCH 1651/2374] Anchor left when justifying words --- .../images/multiline_text_justify_anchor.png | Bin 0 -> 11282 bytes .../multiline_text_justify_last_line.png | Bin 3581 -> 0 bytes Tests/test_imagefont.py | 20 +++++----- src/PIL/ImageDraw.py | 37 ++++++++++-------- 4 files changed, 32 insertions(+), 25 deletions(-) create mode 100644 Tests/images/multiline_text_justify_anchor.png delete mode 100644 Tests/images/multiline_text_justify_last_line.png diff --git a/Tests/images/multiline_text_justify_anchor.png b/Tests/images/multiline_text_justify_anchor.png new file mode 100644 index 0000000000000000000000000000000000000000..6d3fb421d1832e82dcfba255891240cf2aa5b92d GIT binary patch literal 11282 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 ^Ow{8V4_rXU6&3`dV6!`l3PEJl{WwF}! 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 HzUTh}m8~Dn literal 0 HcmV?d00001 diff --git a/Tests/images/multiline_text_justify_last_line.png b/Tests/images/multiline_text_justify_last_line.png deleted file mode 100644 index bcc1afd72969c4a476cabc5a074154994a222a94..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3581 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 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index f99275925aa..fd622c945de 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -267,19 +267,21 @@ def test_render_multiline_text_align( assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) -def test_render_multiline_text_align_justify_last_line( +def test_render_multiline_text_justify_anchor( font: ImageFont.FreeTypeFont, ) -> None: - im = Image.new("RGB", (280, 60)) + im = Image.new("RGB", (280, 240)) draw = ImageDraw.Draw(im) - draw.multiline_text( - (0, 0), - "hey you you are awesome\nthis\nlooks awkward", - font=font, - align="justify", - ) + for xy, anchor in (((0, 0), "la"), ((140, 80), "ma"), ((280, 160), "ra")): + draw.multiline_text( + xy, + "hey you you are awesome\nthis looks awkward\nthis\nlooks awkward", + font=font, + anchor=anchor, + align="justify", + ) - assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_last_line.png") + assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_anchor.png") def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 47ae575c9e6..d35cda6024d 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -702,8 +702,7 @@ def _prepare_multiline_text( font_size: float | None, ) -> tuple[ ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, - str, - list[tuple[tuple[float, float], AnyStr]], + list[tuple[tuple[float, float], str, AnyStr]], ]: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" @@ -753,13 +752,7 @@ def _prepare_multiline_text( left = xy[0] width_difference = max_width - widths[idx] - # first align left by anchor - if anchor[0] == "m": - left -= width_difference / 2.0 - elif anchor[0] == "r": - left -= width_difference - - # then align by align parameter + # align by align parameter if align in ("left", "justify"): pass elif align == "center": @@ -773,6 +766,12 @@ def _prepare_multiline_text( if align == "justify" and width_difference != 0 and idx != len(lines) - 1: words = line.split(" " if isinstance(text, str) else b" ") if len(words) > 1: + # align left by anchor + if anchor[0] == "m": + left -= max_width / 2.0 + elif anchor[0] == "r": + left -= max_width + word_widths = [ self.textlength( word, @@ -784,17 +783,23 @@ def _prepare_multiline_text( ) for word in words ] + word_anchor = "l" + anchor[1] width_difference = max_width - sum(word_widths) for i, word in enumerate(words): - parts.append(((left, top), word)) + parts.append(((left, top), word_anchor, word)) left += word_widths[i] + width_difference / (len(words) - 1) top += line_spacing continue - parts.append(((left, top), line)) + # align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + parts.append(((left, top), anchor, line)) top += line_spacing - return font, anchor, parts + return font, parts def multiline_text( self, @@ -819,7 +824,7 @@ def multiline_text( *, font_size: float | None = None, ) -> None: - font, anchor, lines = self._prepare_multiline_text( + font, lines = self._prepare_multiline_text( xy, text, font, @@ -834,7 +839,7 @@ def multiline_text( font_size, ) - for xy, line in lines: + for xy, anchor, line in lines: self.text( xy, line, @@ -949,7 +954,7 @@ def multiline_textbbox( *, font_size: float | None = None, ) -> tuple[float, float, float, float]: - font, anchor, lines = self._prepare_multiline_text( + font, lines = self._prepare_multiline_text( xy, text, font, @@ -966,7 +971,7 @@ def multiline_textbbox( bbox: tuple[float, float, float, float] | None = None - for xy, line in lines: + for xy, anchor, line in lines: bbox_line = self.textbbox( xy, line, From 3d77723a0c9d245f61e124c58a11e3a1779b3c0d Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 17 Apr 2025 21:42:42 +0100 Subject: [PATCH 1652/2374] Added arrow support for a flat array of 4*uint8 for image32 modes --- Tests/test_pyarrow.py | 66 +++++++++++++++++++++++++++++++++++++--- src/libImaging/Storage.c | 14 +++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index ece9f8f2657..e7f2bc5f909 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -18,18 +18,25 @@ def _test_img_equals_pyarray( - img: Image.Image, arr: Any, mask: list[int] | None + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 ) -> None: - assert img.height * img.width == len(arr) + assert img.height * img.width * elts_per_pixel == len(arr) px = img.load() assert px is not None + if elts_per_pixel > 1 and mask is None: + # have to do element wise comparison when we're comparing + # flattened r,g,b,a to a pixel. + mask = list(range(elts_per_pixel)) for x in range(0, img.size[0], int(img.size[0] / 10)): for y in range(0, img.size[1], int(img.size[1] / 10)): if mask: + pixel = px[x, y] + assert isinstance(pixel, tuple) for ix, elt in enumerate(mask): - pixel = px[x, y] - assert isinstance(pixel, tuple) - assert pixel[ix] == arr[y * img.width + x].as_py()[elt] + if elts_per_pixel == 1: + assert pixel[ix] == arr[y * img.width + x].as_py()[elt] + else: + assert pixel[ix] == arr[(y * img.width + x) * elts_per_pixel + elt].as_py() else: assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) @@ -110,3 +117,52 @@ def test_lifetime2() -> None: px = img2.load() assert px # make mypy happy assert isinstance(px[0, 0], int) + + +UINT_ARR = ( + fl_uint8_4_type, + [1,2,3,4], + 1 +) +UINT = ( + pyarrow.uint8(), + 3, + 4 +) + + + +@pytest.mark.parametrize( + "mode, data_tp, mask", + ( + ("L", (pyarrow.uint8(), 3, 1), None), + ("I", (pyarrow.int32(), 1<<24, 1), None), + ("F", (pyarrow.float32(), 3.14159, 1), None), + ("LA", UINT_ARR, [0, 3]), + ("LA", UINT, [0, 3]), + ("RGB", UINT_ARR, [0, 1, 2]), + ("RGBA", UINT_ARR, None), + ("RGBA", UINT_ARR, None), + ("CMYK", UINT_ARR, None), + ("YCbCr", UINT_ARR, [0, 1, 2]), + ("HSV", UINT_ARR, [0, 1, 2]), + ("RGB", UINT, [0, 1, 2]), + ("RGBA", UINT, None), + ("RGBA", UINT, None), + ("CMYK", UINT, None), + ("YCbCr", UINT, [0, 1, 2]), + ("HSV", UINT, [0, 1, 2]), + ), +) +def test_fromarray(mode: str, + data_tp: tuple, + mask:list[int] | None) -> None: + (dtype, + elt, + elts_per_pixel) = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + arr = pyarrow.array([elt]*(ct_pixels*elts_per_pixel), type=dtype) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_pyarray(img, arr, mask, elts_per_pixel) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 4fa4ecd1ce4..7f8d9c4a044 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -723,6 +723,8 @@ ImagingNewArrow( int64_t pixels = (int64_t)xsize * (int64_t)ysize; // fmt:off // don't reformat this + // stored as a single array, one element per pixel, either single band + // or multiband, where each pixel is an I32. if (((strcmp(schema->format, "I") == 0 // int32 && im->pixelsize == 4 // 4xchar* storage && im->bands >= 2) // INT32 into any INT32 Storage mode @@ -735,6 +737,7 @@ ImagingNewArrow( return im; } } + // Stored as [[r,g,b,a],....] if (strcmp(schema->format, "+w:4") == 0 // 4 up array && im->pixelsize == 4 // storage as 32 bpc && schema->n_children > 0 // make sure schema is well formed. @@ -750,6 +753,17 @@ ImagingNewArrow( return im; } } + // Stored as [r,g,b,a,r,g,b,a....] + if (strcmp(schema->format, "C") == 0 // uint8 + && im->pixelsize == 4 // storage as 32 bpc + && schema->n_children == 0 // make sure schema is well formed. + && strcmp(im->arrow_band_format, "C") == 0 // Expected Format + && 4* pixels == external_array->length) { // expected length + // single flat array, interleaved storage. + if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) { + return im; + } + } // fmt: on ImagingDelete(im); return NULL; From c729d4e2085b96662b89ad09f99327f4516ce4ed Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 17 Apr 2025 22:16:27 +0100 Subject: [PATCH 1653/2374] Test uint32 array creation -> image32 images --- Tests/test_pyarrow.py | 61 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index e7f2bc5f909..92bc4c807e4 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -9,6 +9,7 @@ from .helper import ( assert_deep_equal, assert_image_equal, + is_big_endian, hopper, ) @@ -41,6 +42,34 @@ def _test_img_equals_pyarray( assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) +def _test_img_equals_int32_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 +) -> None: + assert img.height * img.width * elts_per_pixel == len(arr) + px = img.load() + assert px is not None + if mask is None: + # have to do element wise comparison when we're comparing + # flattened rgba in an uint32 to a pixel. + mask = list(range(elts_per_pixel)) + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): + pixel = px[x, y] + assert isinstance(pixel, tuple) + arr_pixel_int = arr[y * img.width + x].as_py() + arr_pixel_tuple = ( + arr_pixel_int % 256, + (arr_pixel_int // 256) % 256, + (arr_pixel_int // 256**2) % 256, + (arr_pixel_int // 256**3), + ) + if is_big_endian(): + arr_pixel_tuple = arr_pixel_tuple[::-1] + + for ix, elt in enumerate(mask): + assert pixel[ix] == arr_pixel_tuple[elt] + + # really hard to get a non-nullable list type fl_uint8_4_type = pyarrow.field( "_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4) @@ -129,7 +158,11 @@ def test_lifetime2() -> None: 3, 4 ) - +INT32 = ( + pyarrow.uint32(), + 0xabcdef45, + 1 +) @pytest.mark.parametrize( @@ -166,3 +199,29 @@ def test_fromarray(mode: str, img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) _test_img_equals_pyarray(img, arr, mask, elts_per_pixel) + + +@pytest.mark.parametrize( + "mode, data_tp, mask", + ( + ("LA", INT32, [0, 3]), + ("RGB", INT32, [0, 1, 2]), + ("RGBA", INT32, None), + ("RGBA", INT32, None), + ("CMYK", INT32, None), + ("YCbCr", INT32, [0, 1, 2]), + ("HSV", INT32, [0, 1, 2]), + ), +) +def test_from_int32array(mode: str, + data_tp: tuple, + mask:list[int] | None) -> None: + (dtype, + elt, + elts_per_pixel) = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + arr = pyarrow.array([elt]*(ct_pixels*elts_per_pixel), type=dtype) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel) From ac500460dfc6ddaa7c0660de3f0233d05e207852 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 17 Apr 2025 22:22:31 +0100 Subject: [PATCH 1654/2374] lint --- Tests/test_pyarrow.py | 44 ++++++++++++++++------------------------ src/libImaging/Storage.c | 10 ++++----- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index 92bc4c807e4..bcdd7ddc9d0 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -9,8 +9,8 @@ from .helper import ( assert_deep_equal, assert_image_equal, - is_big_endian, hopper, + is_big_endian, ) pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") @@ -37,7 +37,10 @@ def _test_img_equals_pyarray( if elts_per_pixel == 1: assert pixel[ix] == arr[y * img.width + x].as_py()[elt] else: - assert pixel[ix] == arr[(y * img.width + x) * elts_per_pixel + elt].as_py() + assert ( + pixel[ix] + == arr[(y * img.width + x) * elts_per_pixel + elt].as_py() + ) else: assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) @@ -169,33 +172,27 @@ def test_lifetime2() -> None: "mode, data_tp, mask", ( ("L", (pyarrow.uint8(), 3, 1), None), - ("I", (pyarrow.int32(), 1<<24, 1), None), + ("I", (pyarrow.int32(), 1 << 24, 1), None), ("F", (pyarrow.float32(), 3.14159, 1), None), ("LA", UINT_ARR, [0, 3]), ("LA", UINT, [0, 3]), ("RGB", UINT_ARR, [0, 1, 2]), ("RGBA", UINT_ARR, None), - ("RGBA", UINT_ARR, None), ("CMYK", UINT_ARR, None), - ("YCbCr", UINT_ARR, [0, 1, 2]), - ("HSV", UINT_ARR, [0, 1, 2]), + ("YCbCr", UINT_ARR, [0, 1, 2]), + ("HSV", UINT_ARR, [0, 1, 2]), ("RGB", UINT, [0, 1, 2]), ("RGBA", UINT, None), - ("RGBA", UINT, None), ("CMYK", UINT, None), - ("YCbCr", UINT, [0, 1, 2]), - ("HSV", UINT, [0, 1, 2]), + ("YCbCr", UINT, [0, 1, 2]), + ("HSV", UINT, [0, 1, 2]), ), ) -def test_fromarray(mode: str, - data_tp: tuple, - mask:list[int] | None) -> None: - (dtype, - elt, - elts_per_pixel) = data_tp +def test_fromarray(mode: str, data_tp: tuple, mask: list[int] | None) -> None: + (dtype, elt, elts_per_pixel) = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] - arr = pyarrow.array([elt]*(ct_pixels*elts_per_pixel), type=dtype) + arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype) img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) _test_img_equals_pyarray(img, arr, mask, elts_per_pixel) @@ -207,21 +204,16 @@ def test_fromarray(mode: str, ("LA", INT32, [0, 3]), ("RGB", INT32, [0, 1, 2]), ("RGBA", INT32, None), - ("RGBA", INT32, None), ("CMYK", INT32, None), - ("YCbCr", INT32, [0, 1, 2]), - ("HSV", INT32, [0, 1, 2]), + ("YCbCr", INT32, [0, 1, 2]), + ("HSV", INT32, [0, 1, 2]), ), ) -def test_from_int32array(mode: str, - data_tp: tuple, - mask:list[int] | None) -> None: - (dtype, - elt, - elts_per_pixel) = data_tp +def test_from_int32array(mode: str, data_tp: tuple, mask: list[int] | None) -> None: + (dtype, elt, elts_per_pixel) = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] - arr = pyarrow.array([elt]*(ct_pixels*elts_per_pixel), type=dtype) + arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype) img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) _test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 7f8d9c4a044..2c57165c1e7 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -754,11 +754,11 @@ ImagingNewArrow( } } // Stored as [r,g,b,a,r,g,b,a....] - if (strcmp(schema->format, "C") == 0 // uint8 - && im->pixelsize == 4 // storage as 32 bpc - && schema->n_children == 0 // make sure schema is well formed. - && strcmp(im->arrow_band_format, "C") == 0 // Expected Format - && 4* pixels == external_array->length) { // expected length + if (strcmp(schema->format, "C") == 0 // uint8 + && im->pixelsize == 4 // storage as 32 bpc + && schema->n_children == 0 // make sure schema is well formed. + && strcmp(im->arrow_band_format, "C") == 0 // Expected Format + && 4 * pixels == external_array->length) { // expected length // single flat array, interleaved storage. if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) { return im; From 00ae9dda35e512e3bdfa69daff1625fd006cff21 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Apr 2025 18:49:11 +1000 Subject: [PATCH 1655/2374] Changed harfbuzz buildtype to minsize --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index d53cf059bab..a4592871f69 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -92,7 +92,7 @@ function build_harfbuzz { local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ - && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled -Dtests=disabled) + && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=minsize -Dfreetype=enabled -Dglib=disabled -Dtests=disabled) (cd $out_dir/build \ && meson install) touch harfbuzz-stamp From cf48bbf0c48f871ecd62fb473611a7e2552580d9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Apr 2025 20:26:03 +1000 Subject: [PATCH 1656/2374] Removed indentation from list --- docs/handbook/concepts.rst | 44 +++++++++++++++--------------- docs/reference/block_allocator.rst | 18 ++++++------ 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 7da1078c14e..fe874a740b4 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -30,35 +30,35 @@ image. Each pixel uses the full range of the bit depth. So a 1-bit pixel has a r INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current release supports the following standard modes: - * ``1`` (1-bit pixels, black and white, stored with one pixel per byte) - * ``L`` (8-bit pixels, grayscale) - * ``P`` (8-bit pixels, mapped to any other mode using a color palette) - * ``RGB`` (3x8-bit pixels, true color) - * ``RGBA`` (4x8-bit pixels, true color with transparency mask) - * ``CMYK`` (4x8-bit pixels, color separation) - * ``YCbCr`` (3x8-bit pixels, color video format) +* ``1`` (1-bit pixels, black and white, stored with one pixel per byte) +* ``L`` (8-bit pixels, grayscale) +* ``P`` (8-bit pixels, mapped to any other mode using a color palette) +* ``RGB`` (3x8-bit pixels, true color) +* ``RGBA`` (4x8-bit pixels, true color with transparency mask) +* ``CMYK`` (4x8-bit pixels, color separation) +* ``YCbCr`` (3x8-bit pixels, color video format) - * Note that this refers to the JPEG, and not the ITU-R BT.2020, standard + * Note that this refers to the JPEG, and not the ITU-R BT.2020, standard - * ``LAB`` (3x8-bit pixels, the L*a*b color space) - * ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space) +* ``LAB`` (3x8-bit pixels, the L*a*b color space) +* ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space) - * Hue's range of 0-255 is a scaled version of 0 degrees <= Hue < 360 degrees + * Hue's range of 0-255 is a scaled version of 0 degrees <= Hue < 360 degrees - * ``I`` (32-bit signed integer pixels) - * ``F`` (32-bit floating point pixels) +* ``I`` (32-bit signed integer pixels) +* ``F`` (32-bit floating point pixels) Pillow also provides limited support for a few additional modes, including: - * ``LA`` (L with alpha) - * ``PA`` (P with alpha) - * ``RGBX`` (true color with padding) - * ``RGBa`` (true color with premultiplied alpha) - * ``La`` (L with premultiplied alpha) - * ``I;16`` (16-bit unsigned integer pixels) - * ``I;16L`` (16-bit little endian unsigned integer pixels) - * ``I;16B`` (16-bit big endian unsigned integer pixels) - * ``I;16N`` (16-bit native endian unsigned integer pixels) +* ``LA`` (L with alpha) +* ``PA`` (P with alpha) +* ``RGBX`` (true color with padding) +* ``RGBa`` (true color with premultiplied alpha) +* ``La`` (L with premultiplied alpha) +* ``I;16`` (16-bit unsigned integer pixels) +* ``I;16L`` (16-bit little endian unsigned integer pixels) +* ``I;16B`` (16-bit big endian unsigned integer pixels) +* ``I;16N`` (16-bit native endian unsigned integer pixels) Premultiplied alpha is where the values for each other channel have been multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)`` diff --git a/docs/reference/block_allocator.rst b/docs/reference/block_allocator.rst index f4d27e24e57..c6be5b7e6ed 100644 --- a/docs/reference/block_allocator.rst +++ b/docs/reference/block_allocator.rst @@ -37,14 +37,14 @@ fresh allocation. This caching of free blocks is currently disabled by default, but can be enabled and tweaked using three environment variables: - * ``PILLOW_ALIGNMENT``, in bytes. Specifies the alignment of memory - allocations. Valid values are powers of 2 between 1 and - 128, inclusive. Defaults to 1. +* ``PILLOW_ALIGNMENT``, in bytes. Specifies the alignment of memory + allocations. Valid values are powers of 2 between 1 and + 128, inclusive. Defaults to 1. - * ``PILLOW_BLOCK_SIZE``, in bytes, K, or M. Specifies the maximum - block size for ``ImagingAllocateArray``. Valid values are - integers, with an optional ``k`` or ``m`` suffix. Defaults to 16M. +* ``PILLOW_BLOCK_SIZE``, in bytes, K, or M. Specifies the maximum + block size for ``ImagingAllocateArray``. Valid values are + integers, with an optional ``k`` or ``m`` suffix. Defaults to 16M. - * ``PILLOW_BLOCKS_MAX`` Specifies the number of freed blocks to - retain to fill future memory requests. Any freed blocks over this - threshold will be returned to the OS immediately. Defaults to 0. +* ``PILLOW_BLOCKS_MAX`` Specifies the number of freed blocks to + retain to fill future memory requests. Any freed blocks over this + threshold will be returned to the OS immediately. Defaults to 0. From 03e7871afdb8f558cddc10cc9013e1db143298d9 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 20 Apr 2025 00:18:01 +0300 Subject: [PATCH 1657/2374] Add `make [-C docs] htmllive` to rebuild and reload HTML files (#8913) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Makefile | 5 +++++ docs/Guardfile | 10 ---------- docs/Makefile | 9 +++++---- pyproject.toml | 1 + 4 files changed, 11 insertions(+), 14 deletions(-) delete mode 100755 docs/Guardfile diff --git a/Makefile b/Makefile index 53164b08a90..5a815245402 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,10 @@ doc html: htmlview: $(MAKE) -C docs htmlview +.PHONY: htmllive +htmllive: + $(MAKE) -C docs htmllive + .PHONY: doccheck doccheck: $(MAKE) doc @@ -43,6 +47,7 @@ help: @echo " docserve run an HTTP server on the docs directory" @echo " html make HTML docs" @echo " htmlview open the index page built by the html target in your browser" + @echo " htmllive rebuild and reload HTML files in your browser" @echo " install make and install" @echo " install-coverage make and install with C coverage" @echo " lint run the lint checks" diff --git a/docs/Guardfile b/docs/Guardfile deleted file mode 100755 index 16a891a730d..00000000000 --- a/docs/Guardfile +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -from livereload.compiler import shell -from livereload.task import Task - -Task.add("*.rst", shell("make html")) -Task.add("*/*.rst", shell("make html")) -Task.add("Makefile", shell("make html")) -Task.add("conf.py", shell("make html")) diff --git a/docs/Makefile b/docs/Makefile index e90af0519c6..4412fc80687 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -20,8 +20,8 @@ help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " htmlview to open the index page built by the html target in your browser" + @echo " htmllive to rebuild and reload HTML files in your browser" @echo " serve to start a local server for viewing docs" - @echo " livehtml to start a local server for viewing docs and auto-reload on change" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @@ -201,9 +201,10 @@ doctest: htmlview: html $(PYTHON) -c "import os, webbrowser; webbrowser.open('file://' + os.path.realpath('$(BUILDDIR)/html/index.html'))" -.PHONY: livehtml -livehtml: html - livereload $(BUILDDIR)/html -p 33233 +.PHONY: htmllive +htmllive: SPHINXBUILD = $(PYTHON) -m sphinx_autobuild +htmllive: SPHINXOPTS = --open-browser --delay 0 +htmllive: html .PHONY: serve serve: diff --git a/pyproject.toml b/pyproject.toml index e8e76796a69..a3ff9723b4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ optional-dependencies.docs = [ "furo", "olefile", "sphinx>=8.2", + "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph", From 4402797b35137666413efcc04fb91064e846cd90 Mon Sep 17 00:00:00 2001 From: Adian Kozlica <105174725+AdianKozlica@users.noreply.github.com> Date: Mon, 21 Apr 2025 04:36:40 +0200 Subject: [PATCH 1658/2374] Add support for Grim in Wayland sessions ImageGrab (#8912) Co-authored-by: Andrew Murray --- Tests/test_imagegrab.py | 1 + docs/reference/ImageGrab.rst | 6 +++--- src/PIL/ImageGrab.py | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 5f51171f197..01fa090dc3a 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -43,6 +43,7 @@ def test_grab_no_xcb(self) -> None: if ( sys.platform not in ("win32", "darwin") and not shutil.which("gnome-screenshot") + and not shutil.which("grim") and not shutil.which("spectacle") ): with pytest.raises(OSError) as e: diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 1e827a6764e..0fd8f68df7c 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -16,9 +16,9 @@ or the clipboard to a PIL image memory. the entire screen is copied, and on macOS, it will be at 2x if on a Retina screen. On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return - a snapshot of the screen, ``gnome-screenshot`` or ``spectacle`` will be used as a - fallback if they are installed. To disable this behaviour, pass ``xdisplay=""`` - instead. + a snapshot of the screen, ``gnome-screenshot``, ``grim`` or ``spectacle`` will be + used as a fallback if they are installed. To disable this behaviour, pass + ``xdisplay=""`` instead. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 4da14f8e4b5..c29350b7a81 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -89,6 +89,8 @@ def grab( if display_name is None and sys.platform not in ("darwin", "win32"): if shutil.which("gnome-screenshot"): args = ["gnome-screenshot", "-f"] + elif shutil.which("grim"): + args = ["grim"] elif shutil.which("spectacle"): args = ["spectacle", "-n", "-b", "-f", "-o"] else: From 8fe7a7aaf89a5f312fe60382c1c5196876f53452 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 21 Apr 2025 17:32:47 +1000 Subject: [PATCH 1659/2374] Update redirected URL --- docs/releasenotes/10.1.0.rst | 2 +- src/PIL/ImageFont.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/releasenotes/10.1.0.rst b/docs/releasenotes/10.1.0.rst index fd556bdf17f..e4efb069e03 100644 --- a/docs/releasenotes/10.1.0.rst +++ b/docs/releasenotes/10.1.0.rst @@ -71,7 +71,7 @@ size and font_size arguments when using default font Pillow has had a "better than nothing" default font, which can only be drawn at one font size. Now, if FreeType support is available, a version of -`Aileron Regular `_ is loaded, which can be +`Aileron Regular `_ is loaded, which can be drawn at chosen font sizes. The following ``size`` and ``font_size`` arguments can now be used to specify a diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ebe510ba990..329c463ff86 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -1093,7 +1093,7 @@ def load_default_imagefont() -> ImageFont: def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: """If FreeType support is available, load a version of Aileron Regular, - https://dotcolon.net/font/aileron, with a more limited character set. + https://dotcolon.net/fonts/aileron, with a more limited character set. Otherwise, load a "better than nothing" font. From d03ce3d235c9a80fb5a336634115a331749af9f8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 21 Apr 2025 11:22:03 +0300 Subject: [PATCH 1660/2374] Docs: remove unused Makefile targets (#8917) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/Makefile | 135 -------------------------------------------------- docs/conf.py | 91 ---------------------------------- docs/make.bat | 123 --------------------------------------------- 3 files changed, 349 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 4412fc80687..1e6c06ede80 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -24,22 +24,7 @@ help: @echo " serve to start a local server for viewing docs" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" .PHONY: clean clean: @@ -69,119 +54,6 @@ singlehtml: @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." -.PHONY: pickle -pickle: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -.PHONY: json -json: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -.PHONY: htmlhelp -htmlhelp: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -.PHONY: qthelp -qthelp: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PillowPILfork.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc" - -.PHONY: devhelp -devhelp: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/PillowPILfork" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PillowPILfork" - @echo "# devhelp" - -.PHONY: epub -epub: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -.PHONY: latex -latex: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -.PHONY: latexpdf -latexpdf: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: text -text: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -.PHONY: man -man: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -.PHONY: texinfo -texinfo: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -.PHONY: info -info: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -.PHONY: gettext -gettext: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -.PHONY: changes -changes: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - .PHONY: linkcheck linkcheck: $(MAKE) install-sphinx @@ -190,13 +62,6 @@ linkcheck: @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." -.PHONY: doctest -doctest: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - .PHONY: htmlview htmlview: html $(PYTHON) -c "import os, webbrowser; webbrowser.open('file://' + os.path.realpath('$(BUILDDIR)/html/index.html'))" diff --git a/docs/conf.py b/docs/conf.py index bfbcf91516f..040301433f9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -229,97 +229,6 @@ # implements a search results scorer. If empty, the default will be used. # html_search_scorer = 'scorer.js' -# Output file base name for HTML help builder. -htmlhelp_basename = "PillowPILForkdoc" - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements: dict[str, str] = { - # The paper size ('letterpaper' or 'a4paper'). - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # 'preamble': '', - # Latex figure (float) alignment - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ( - master_doc, - "PillowPILFork.tex", - "Pillow (PIL Fork) Documentation", - "Jeffrey A. Clark", - "manual", - ) -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, "pillowpilfork", "Pillow (PIL Fork) Documentation", [author], 1) -] - -# If true, show URL addresses after external links. -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - "PillowPILFork", - "Pillow (PIL Fork) Documentation", - author, - "PillowPILFork", - "Pillow is the friendly PIL fork by Jeffrey A. Clark and contributors.", - "Miscellaneous", - ) -] - -# Documents to append as an appendix to all manuals. -# texinfo_appendices = [] - -# If false, no module index is generated. -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# texinfo_no_detailmenu = False - linkcheck_allowed_redirects = { r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*", diff --git a/docs/make.bat b/docs/make.bat index 0ed5ee1a57e..4126f786b8d 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -22,20 +22,7 @@ if "%1" == "help" ( echo. htmlview to open the index page built by the html target in your browser echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled goto end ) @@ -80,107 +67,6 @@ if "%1" == "singlehtml" ( goto end ) -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PillowPILfork.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PillowPILfork.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 @@ -190,13 +76,4 @@ or in %BUILDDIR%/linkcheck/output.txt. goto end ) -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - :end From 348589a367bba81bd9e2d1f4b6280ada91caae2e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 21 Apr 2025 12:03:31 +0300 Subject: [PATCH 1661/2374] Docs: use sentence case for headers (#8914) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/README.rst | 2 +- docs/PIL.rst | 32 +++---- docs/deprecations.rst | 2 +- docs/handbook/concepts.rst | 2 +- docs/handbook/image-file-formats.rst | 4 +- docs/handbook/overview.rst | 6 +- docs/handbook/tutorial.rst | 4 +- .../writing-your-own-image-plugin.rst | 6 +- docs/installation.rst | 10 +- docs/installation/basic-installation.rst | 2 +- docs/installation/building-from-source.rst | 8 +- docs/installation/platform-support.rst | 6 +- docs/installation/python-support.rst | 2 +- docs/reference/ExifTags.rst | 2 +- docs/reference/Image.rst | 6 +- docs/reference/ImageChops.rst | 2 +- docs/reference/ImageCms.rst | 2 +- docs/reference/ImageColor.rst | 4 +- docs/reference/ImageDraw.rst | 8 +- docs/reference/ImageEnhance.rst | 2 +- docs/reference/ImageFile.rst | 2 +- docs/reference/ImageFilter.rst | 2 +- docs/reference/ImageFont.rst | 2 +- docs/reference/ImageGrab.rst | 2 +- docs/reference/ImageMath.rst | 10 +- docs/reference/ImageMorph.rst | 2 +- docs/reference/ImageOps.rst | 2 +- docs/reference/ImagePalette.rst | 2 +- docs/reference/ImagePath.rst | 2 +- docs/reference/ImageQt.rst | 2 +- docs/reference/ImageSequence.rst | 2 +- docs/reference/ImageShow.rst | 4 +- docs/reference/ImageStat.rst | 2 +- docs/reference/ImageTk.rst | 2 +- docs/reference/ImageTransform.rst | 2 +- docs/reference/ImageWin.rst | 2 +- docs/reference/JpegPresets.rst | 2 +- docs/reference/PSDraw.rst | 2 +- docs/reference/PixelAccess.rst | 4 +- docs/reference/TiffTags.rst | 2 +- docs/reference/arrow_support.rst | 10 +- docs/reference/block_allocator.rst | 8 +- docs/reference/c_extension_debugging.rst | 8 +- docs/reference/features.rst | 2 +- docs/reference/internal_design.rst | 2 +- docs/reference/internal_modules.rst | 16 ++-- docs/reference/limits.rst | 6 +- docs/reference/open_files.rst | 6 +- docs/reference/plugins.rst | 92 +++++++++---------- docs/releasenotes/10.0.0.rst | 8 +- docs/releasenotes/10.0.1.rst | 2 +- docs/releasenotes/10.1.0.rst | 6 +- docs/releasenotes/10.2.0.rst | 6 +- docs/releasenotes/10.3.0.rst | 6 +- docs/releasenotes/10.4.0.rst | 4 +- docs/releasenotes/11.0.0.rst | 12 +-- docs/releasenotes/11.1.0.rst | 6 +- docs/releasenotes/11.2.1.rst | 6 +- docs/releasenotes/2.7.0.rst | 6 +- docs/releasenotes/3.0.0.rst | 12 +-- docs/releasenotes/3.1.0.rst | 4 +- docs/releasenotes/3.2.0.rst | 4 +- docs/releasenotes/3.3.0.rst | 4 +- docs/releasenotes/3.3.2.rst | 4 +- docs/releasenotes/3.4.0.rst | 8 +- docs/releasenotes/4.0.0.rst | 2 +- docs/releasenotes/4.1.0.rst | 10 +- docs/releasenotes/4.1.1.rst | 2 +- docs/releasenotes/4.2.0.rst | 12 +-- docs/releasenotes/4.2.1.rst | 2 +- docs/releasenotes/4.3.0.rst | 26 +++--- docs/releasenotes/5.0.0.rst | 22 ++--- docs/releasenotes/5.1.0.rst | 10 +- docs/releasenotes/5.2.0.rst | 6 +- docs/releasenotes/5.3.0.rst | 6 +- docs/releasenotes/5.4.0.rst | 4 +- docs/releasenotes/6.0.0.rst | 8 +- docs/releasenotes/6.1.0.rst | 4 +- docs/releasenotes/6.2.0.rst | 6 +- docs/releasenotes/6.2.1.rst | 4 +- docs/releasenotes/7.0.0.rst | 6 +- docs/releasenotes/7.1.0.rst | 6 +- docs/releasenotes/7.2.0.rst | 2 +- docs/releasenotes/8.0.0.rst | 8 +- docs/releasenotes/8.1.0.rst | 6 +- docs/releasenotes/8.1.1.rst | 2 +- docs/releasenotes/8.2.0.rst | 6 +- docs/releasenotes/8.3.0.rst | 6 +- docs/releasenotes/8.3.2.rst | 2 +- docs/releasenotes/8.4.0.rst | 4 +- docs/releasenotes/9.0.0.rst | 8 +- docs/releasenotes/9.0.1.rst | 2 +- docs/releasenotes/9.1.0.rst | 6 +- docs/releasenotes/9.2.0.rst | 4 +- docs/releasenotes/9.3.0.rst | 4 +- docs/releasenotes/9.4.0.rst | 4 +- docs/releasenotes/9.5.0.rst | 4 +- docs/releasenotes/index.rst | 2 +- docs/releasenotes/template.rst | 8 +- 99 files changed, 313 insertions(+), 313 deletions(-) diff --git a/Tests/README.rst b/Tests/README.rst index 2d014e5a46e..a955ec4fa56 100644 --- a/Tests/README.rst +++ b/Tests/README.rst @@ -1,4 +1,4 @@ -Pillow Tests +Pillow tests ============ Test scripts are named ``test_xxx.py``. Helper classes and functions can be found in ``helper.py``. diff --git a/docs/PIL.rst b/docs/PIL.rst index bdbf1373d88..5225e9644cd 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -1,10 +1,10 @@ -PIL Package (autodoc of remaining modules) +PIL package (autodoc of remaining modules) ========================================== Reference for modules whose documentation has not yet been ported or written can be found here. -:mod:`PIL` Module +:mod:`PIL` module ----------------- .. py:module:: PIL @@ -12,7 +12,7 @@ can be found here. .. autoexception:: UnidentifiedImageError :show-inheritance: -:mod:`~PIL.BdfFontFile` Module +:mod:`~PIL.BdfFontFile` module ------------------------------ .. automodule:: PIL.BdfFontFile @@ -20,7 +20,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.ContainerIO` Module +:mod:`~PIL.ContainerIO` module ------------------------------ .. automodule:: PIL.ContainerIO @@ -28,7 +28,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.FontFile` Module +:mod:`~PIL.FontFile` module --------------------------- .. automodule:: PIL.FontFile @@ -36,7 +36,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.GdImageFile` Module +:mod:`~PIL.GdImageFile` module ------------------------------ .. automodule:: PIL.GdImageFile @@ -44,7 +44,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.GimpGradientFile` Module +:mod:`~PIL.GimpGradientFile` module ----------------------------------- .. automodule:: PIL.GimpGradientFile @@ -52,7 +52,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.GimpPaletteFile` Module +:mod:`~PIL.GimpPaletteFile` module ---------------------------------- .. automodule:: PIL.GimpPaletteFile @@ -60,7 +60,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.ImageDraw2` Module +:mod:`~PIL.ImageDraw2` module ----------------------------- .. automodule:: PIL.ImageDraw2 @@ -69,7 +69,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.ImageMode` Module +:mod:`~PIL.ImageMode` module ---------------------------- .. automodule:: PIL.ImageMode @@ -77,7 +77,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.PaletteFile` Module +:mod:`~PIL.PaletteFile` module ------------------------------ .. automodule:: PIL.PaletteFile @@ -85,7 +85,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.PcfFontFile` Module +:mod:`~PIL.PcfFontFile` module ------------------------------ .. automodule:: PIL.PcfFontFile @@ -93,7 +93,7 @@ can be found here. :undoc-members: :show-inheritance: -:class:`.PngImagePlugin.iTXt` Class +:class:`.PngImagePlugin.iTXt` class ----------------------------------- .. autoclass:: PIL.PngImagePlugin.iTXt @@ -107,7 +107,7 @@ can be found here. :param lang: language code :param tkey: UTF-8 version of the key name -:class:`.PngImagePlugin.PngInfo` Class +:class:`.PngImagePlugin.PngInfo` class -------------------------------------- .. autoclass:: PIL.PngImagePlugin.PngInfo @@ -116,7 +116,7 @@ can be found here. :show-inheritance: -:mod:`~PIL.TarIO` Module +:mod:`~PIL.TarIO` module ------------------------ .. automodule:: PIL.TarIO @@ -124,7 +124,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.WalImageFile` Module +:mod:`~PIL.WalImageFile` module ------------------------------- .. automodule:: PIL.WalImageFile diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 7f8e76bccc5..0490ba439fd 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -155,7 +155,7 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They have been deprecated, and will be removed in Pillow 12 (2025-10-15). -Specific WebP Feature Checks +Specific WebP feature checks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 11.0.0 diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index fe874a740b4..c9d3f5e91cb 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -84,7 +84,7 @@ pixels. .. _coordinate-system: -Coordinate System +Coordinate system ----------------- The Python Imaging Library uses a Cartesian pixel coordinate system, with (0,0) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 46fe8b630db..5ca549c3787 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1222,7 +1222,7 @@ numbers are returned as a tuple of ``(numerator, denominator)``. .. deprecated:: 3.0.0 -Reading Multi-frame TIFF Images +Reading multi-frame TIFF images ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and @@ -1664,7 +1664,7 @@ The :py:meth:`~PIL.Image.open` method sets the following Transparency color index. This key is omitted if the image is not transparent. -XV Thumbnails +XV thumbnails ^^^^^^^^^^^^^ Pillow can read XV thumbnail files. diff --git a/docs/handbook/overview.rst b/docs/handbook/overview.rst index 17964d1c5f3..ab22b9807a1 100644 --- a/docs/handbook/overview.rst +++ b/docs/handbook/overview.rst @@ -13,7 +13,7 @@ processing tool. Let’s look at a few possible uses of this library. -Image Archives +Image archives -------------- The Python Imaging Library is ideal for image archival and batch processing @@ -24,7 +24,7 @@ The current version identifies and reads a large number of formats. Write support is intentionally restricted to the most commonly used interchange and presentation formats. -Image Display +Image display ------------- The current release includes Tk :py:class:`~PIL.ImageTk.PhotoImage` and @@ -36,7 +36,7 @@ support. For debugging, there’s also a :py:meth:`~PIL.Image.Image.show` method which saves an image to disk, and calls an external display utility. -Image Processing +Image processing ---------------- The library contains basic image processing functionality, including point operations, filtering with a set of built-in convolution kernels, and colour space conversions. diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index f1a2849b897..28c0abe4437 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -122,7 +122,7 @@ This means that opening an image file is a fast operation, which is independent of the file size and compression type. Here’s a simple script to quickly identify a set of image files: -Identify Image Files +Identify image files ^^^^^^^^^^^^^^^^^^^^ :: @@ -399,7 +399,7 @@ Applying filters .. image:: enhanced_hopper.webp :align: center -Point Operations +Point operations ^^^^^^^^^^^^^^^^ The :py:meth:`~PIL.Image.Image.point` method can be used to translate the pixel diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 9e7d14c5716..21a9124d781 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -1,6 +1,6 @@ .. _image-plugins: -Writing Your Own Image Plugin +Writing your own image plugin ============================= Pillow uses a plugin model which allows you to add your own @@ -329,7 +329,7 @@ The fields are used as follows: .. _file-codecs: -Writing Your Own File Codec in C +Writing your own file codec in C ================================ There are 3 stages in a file codec's lifetime: @@ -414,7 +414,7 @@ memory and release any resources from external libraries. .. _file-codecs-py: -Writing Your Own File Codec in Python +Writing your own file codec in Python ===================================== Python file decoders and encoders should derive from diff --git a/docs/installation.rst b/docs/installation.rst index b4bf2fa00e8..03f18c195e4 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -3,27 +3,27 @@ Installation ============ -Basic Installation +Basic installation ------------------ .. Note:: This section has moved to :ref:`basic-installation`. Please update references accordingly. -Python Support +Python support -------------- .. Note:: This section has moved to :ref:`python-support`. Please update references accordingly. -Platform Support +Platform support ---------------- .. Note:: This section has moved to :ref:`platform-support`. Please update references accordingly. -Building From Source +Building from source -------------------- .. Note:: This section has moved to :ref:`building-from-source`. Please update references accordingly. -Old Versions +Old versions ------------ .. Note:: This section has moved to :ref:`old-versions`. Please update references accordingly. diff --git a/docs/installation/basic-installation.rst b/docs/installation/basic-installation.rst index 989f72dddde..f66ee8707f1 100644 --- a/docs/installation/basic-installation.rst +++ b/docs/installation/basic-installation.rst @@ -8,7 +8,7 @@ .. _basic-installation: -Basic Installation +Basic installation ================== .. note:: diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 9ba389b6699..c72568b208e 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -8,12 +8,12 @@ .. _building-from-source: -Building From Source +Building from source ==================== .. _external-libraries: -External Libraries +External libraries ------------------ .. note:: @@ -271,7 +271,7 @@ After navigating to the Pillow directory, run:: .. _compressed archive from PyPI: https://pypi.org/project/pillow/#files -Build Options +Build options ^^^^^^^^^^^^^ * Config setting: ``-C parallel=n``. Can also be given @@ -319,7 +319,7 @@ Sample usage:: .. _old-versions: -Old Versions +Old versions ============ You can download old distributions from the `release history at PyPI diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index d751620fd2b..93486d034c8 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -1,6 +1,6 @@ .. _platform-support: -Platform Support +Platform support ================ Current platform support for Pillow. Binary distributions are @@ -9,7 +9,7 @@ should compile and run everywhere platform support is listed. In general, we aim to support all current versions of Linux, macOS, and Windows. -Continuous Integration Targets +Continuous integration targets ------------------------------ These platforms are built and tested for every change. @@ -59,7 +59,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ -Other Platforms +Other platforms --------------- These platforms have been reported to work at the versions mentioned. diff --git a/docs/installation/python-support.rst b/docs/installation/python-support.rst index dd5765b6b70..7daee8afc78 100644 --- a/docs/installation/python-support.rst +++ b/docs/installation/python-support.rst @@ -1,6 +1,6 @@ .. _python-support: -Python Support +Python support ============== Pillow supports these Python versions. diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 06965ead3f0..e6bcd9d59c2 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ExifTags .. py:currentmodule:: PIL.ExifTags -:py:mod:`~PIL.ExifTags` Module +:py:mod:`~PIL.ExifTags` module ============================== The :py:mod:`~PIL.ExifTags` module exposes several :py:class:`enum.IntEnum` diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index a3ba8cfd8e1..e687229000b 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.Image .. py:currentmodule:: PIL.Image -:py:mod:`~PIL.Image` Module +:py:mod:`~PIL.Image` module =========================== The :py:mod:`~PIL.Image` module provides a class with the same name which is @@ -113,7 +113,7 @@ Registering plugins .. autofunction:: register_decoder .. autofunction:: register_encoder -The Image Class +The Image class --------------- .. autoclass:: PIL.Image.Image @@ -261,7 +261,7 @@ method. :: .. automethod:: PIL.Image.Image.load .. automethod:: PIL.Image.Image.close -Image Attributes +Image attributes ---------------- Instances of the :py:class:`Image` class have the following attributes: diff --git a/docs/reference/ImageChops.rst b/docs/reference/ImageChops.rst index 9519361a7e6..505181db6b5 100644 --- a/docs/reference/ImageChops.rst +++ b/docs/reference/ImageChops.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageChops .. py:currentmodule:: PIL.ImageChops -:py:mod:`~PIL.ImageChops` ("Channel Operations") Module +:py:mod:`~PIL.ImageChops` ("channel operations") module ======================================================= The :py:mod:`~PIL.ImageChops` module contains a number of arithmetical image diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 4a1f5a3eed7..238390e75f7 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageCms .. py:currentmodule:: PIL.ImageCms -:py:mod:`~PIL.ImageCms` Module +:py:mod:`~PIL.ImageCms` module ============================== The :py:mod:`~PIL.ImageCms` module provides color profile management diff --git a/docs/reference/ImageColor.rst b/docs/reference/ImageColor.rst index 31faeac788b..68e228dba0f 100644 --- a/docs/reference/ImageColor.rst +++ b/docs/reference/ImageColor.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageColor .. py:currentmodule:: PIL.ImageColor -:py:mod:`~PIL.ImageColor` Module +:py:mod:`~PIL.ImageColor` module ================================ The :py:mod:`~PIL.ImageColor` module contains color tables and converters from @@ -11,7 +11,7 @@ others. .. _color-names: -Color Names +Color names ----------- The ImageColor module supports the following string formats: diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index bd6f6b048ac..6e73233a1ce 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageDraw .. py:currentmodule:: PIL.ImageDraw -:py:mod:`~PIL.ImageDraw` Module +:py:mod:`~PIL.ImageDraw` module =============================== The :py:mod:`~PIL.ImageDraw` module provides simple 2D graphics for @@ -54,7 +54,7 @@ later, you can also use RGB 3-tuples or color names (see below). The drawing layer will automatically assign color indexes, as long as you don’t draw with more than 256 colors. -Color Names +Color names ^^^^^^^^^^^ See :ref:`color-names` for the color names supported by Pillow. @@ -75,7 +75,7 @@ To load a OpenType/TrueType font, use the truetype function in the :py:mod:`~PIL.ImageFont` module. Note that this function depends on third-party libraries, and may not available in all PIL builds. -Example: Draw Partial Opacity Text +Example: Draw partial opacity text ---------------------------------- :: @@ -102,7 +102,7 @@ Example: Draw Partial Opacity Text out.show() -Example: Draw Multiline Text +Example: Draw multiline text ---------------------------- :: diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index 529acad4a91..334d1d4b228 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageEnhance .. py:currentmodule:: PIL.ImageEnhance -:py:mod:`~PIL.ImageEnhance` Module +:py:mod:`~PIL.ImageEnhance` module ================================== The :py:mod:`~PIL.ImageEnhance` module contains a number of classes that can be used diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index 64abd71d156..043559352ab 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageFile .. py:currentmodule:: PIL.ImageFile -:py:mod:`~PIL.ImageFile` Module +:py:mod:`~PIL.ImageFile` module =============================== The :py:mod:`~PIL.ImageFile` module provides support functions for the image open diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index 5f2b6af7c10..1c201cacca4 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageFilter .. py:currentmodule:: PIL.ImageFilter -:py:mod:`~PIL.ImageFilter` Module +:py:mod:`~PIL.ImageFilter` module ================================= The :py:mod:`~PIL.ImageFilter` module contains definitions for a pre-defined set of diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index d9d9cac6e13..8b2f923234b 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageFont .. py:currentmodule:: PIL.ImageFont -:py:mod:`~PIL.ImageFont` Module +:py:mod:`~PIL.ImageFont` module =============================== The :py:mod:`~PIL.ImageFont` module defines a class with the same name. Instances of diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 0fd8f68df7c..f6a2ec5bc03 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageGrab .. py:currentmodule:: PIL.ImageGrab -:py:mod:`~PIL.ImageGrab` Module +:py:mod:`~PIL.ImageGrab` module =============================== The :py:mod:`~PIL.ImageGrab` module can be used to copy the contents of the screen diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index f4e1081e6b7..0ee49b15008 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageMath .. py:currentmodule:: PIL.ImageMath -:py:mod:`~PIL.ImageMath` Module +:py:mod:`~PIL.ImageMath` module =============================== The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”, that @@ -86,7 +86,7 @@ Expression syntax It is not recommended to process expressions without considering this. :py:meth:`lambda_eval` is a more secure alternative. -Standard Operators +Standard operators ^^^^^^^^^^^^^^^^^^ You can use standard arithmetical operators for addition (+), subtraction (-), @@ -102,7 +102,7 @@ an 8-bit image, the result will be a 32-bit floating point image. You can force conversion using the ``convert()``, ``float()``, and ``int()`` functions described below. -Bitwise Operators +Bitwise operators ^^^^^^^^^^^^^^^^^ The module also provides operations that operate on individual bits. This @@ -116,7 +116,7 @@ mask off unwanted bits. Bitwise operators don’t work on floating point images. -Logical Operators +Logical operators ^^^^^^^^^^^^^^^^^ Logical operators like ``and``, ``or``, and ``not`` work @@ -128,7 +128,7 @@ treated as true. Note that ``and`` and ``or`` return the last evaluated operand, while not always returns a boolean value. -Built-in Functions +Built-in functions ^^^^^^^^^^^^^^^^^^ These functions are applied to each individual pixel. diff --git a/docs/reference/ImageMorph.rst b/docs/reference/ImageMorph.rst index d4522a06ae3..30b89a54df5 100644 --- a/docs/reference/ImageMorph.rst +++ b/docs/reference/ImageMorph.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageMorph .. py:currentmodule:: PIL.ImageMorph -:py:mod:`~PIL.ImageMorph` Module +:py:mod:`~PIL.ImageMorph` module ================================ The :py:mod:`~PIL.ImageMorph` module provides morphology operations on images. diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index fcaa3c8f675..1ecff09f000 100644 --- a/docs/reference/ImageOps.rst +++ b/docs/reference/ImageOps.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageOps .. py:currentmodule:: PIL.ImageOps -:py:mod:`~PIL.ImageOps` Module +:py:mod:`~PIL.ImageOps` module ============================== The :py:mod:`~PIL.ImageOps` module contains a number of ‘ready-made’ image diff --git a/docs/reference/ImagePalette.rst b/docs/reference/ImagePalette.rst index 72ccfac7d83..42ce5cb134c 100644 --- a/docs/reference/ImagePalette.rst +++ b/docs/reference/ImagePalette.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImagePalette .. py:currentmodule:: PIL.ImagePalette -:py:mod:`~PIL.ImagePalette` Module +:py:mod:`~PIL.ImagePalette` module ================================== The :py:mod:`~PIL.ImagePalette` module contains a class of the same name to diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index 23544b613a0..5f560634998 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImagePath .. py:currentmodule:: PIL.ImagePath -:py:mod:`~PIL.ImagePath` Module +:py:mod:`~PIL.ImagePath` module =============================== The :py:mod:`~PIL.ImagePath` module is used to store and manipulate 2-dimensional diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 7e67a44d364..88d7b8a2095 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageQt .. py:currentmodule:: PIL.ImageQt -:py:mod:`~PIL.ImageQt` Module +:py:mod:`~PIL.ImageQt` module ============================= The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6 or PySide6 diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index a27b2fb4efc..0d6f394dd6f 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageSequence .. py:currentmodule:: PIL.ImageSequence -:py:mod:`~PIL.ImageSequence` Module +:py:mod:`~PIL.ImageSequence` module =================================== The :py:mod:`~PIL.ImageSequence` module contains a wrapper class that lets you diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst index 5cedede69e6..12c8741cee2 100644 --- a/docs/reference/ImageShow.rst +++ b/docs/reference/ImageShow.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageShow .. py:currentmodule:: PIL.ImageShow -:py:mod:`~PIL.ImageShow` Module +:py:mod:`~PIL.ImageShow` module =============================== -The :py:mod:`~PIL.ImageShow` Module is used to display images. +The :py:mod:`~PIL.ImageShow` module is used to display images. All default viewers convert the image to be shown to PNG format. .. autofunction:: PIL.ImageShow.show diff --git a/docs/reference/ImageStat.rst b/docs/reference/ImageStat.rst index f694663828a..ede1199209c 100644 --- a/docs/reference/ImageStat.rst +++ b/docs/reference/ImageStat.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageStat .. py:currentmodule:: PIL.ImageStat -:py:mod:`~PIL.ImageStat` Module +:py:mod:`~PIL.ImageStat` module =============================== The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or diff --git a/docs/reference/ImageTk.rst b/docs/reference/ImageTk.rst index 134ef565188..3ab72b83dbf 100644 --- a/docs/reference/ImageTk.rst +++ b/docs/reference/ImageTk.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageTk .. py:currentmodule:: PIL.ImageTk -:py:mod:`~PIL.ImageTk` Module +:py:mod:`~PIL.ImageTk` module ============================= The :py:mod:`~PIL.ImageTk` module contains support to create and modify Tkinter diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst index 5b0a5ce49dc..5302799349a 100644 --- a/docs/reference/ImageTransform.rst +++ b/docs/reference/ImageTransform.rst @@ -2,7 +2,7 @@ .. py:module:: PIL.ImageTransform .. py:currentmodule:: PIL.ImageTransform -:py:mod:`~PIL.ImageTransform` Module +:py:mod:`~PIL.ImageTransform` module ==================================== The :py:mod:`~PIL.ImageTransform` module contains implementations of diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst index 4151be4a746..c0b9bd2ba95 100644 --- a/docs/reference/ImageWin.rst +++ b/docs/reference/ImageWin.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageWin .. py:currentmodule:: PIL.ImageWin -:py:mod:`~PIL.ImageWin` Module (Windows-only) +:py:mod:`~PIL.ImageWin` module (Windows-only) ============================================= The :py:mod:`~PIL.ImageWin` module contains support to create and display images on diff --git a/docs/reference/JpegPresets.rst b/docs/reference/JpegPresets.rst index aafae44cf4a..b0a3ba8b532 100644 --- a/docs/reference/JpegPresets.rst +++ b/docs/reference/JpegPresets.rst @@ -1,6 +1,6 @@ .. py:currentmodule:: PIL.JpegPresets -:py:mod:`~PIL.JpegPresets` Module +:py:mod:`~PIL.JpegPresets` module ================================= .. automodule:: PIL.JpegPresets diff --git a/docs/reference/PSDraw.rst b/docs/reference/PSDraw.rst index 3e8512e7aa8..9eed775fc09 100644 --- a/docs/reference/PSDraw.rst +++ b/docs/reference/PSDraw.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.PSDraw .. py:currentmodule:: PIL.PSDraw -:py:mod:`~PIL.PSDraw` Module +:py:mod:`~PIL.PSDraw` module ============================ The :py:mod:`~PIL.PSDraw` module provides simple print support for PostScript diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 1ac3d034b49..9d7cf83b640 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -1,6 +1,6 @@ .. _PixelAccess: -:py:class:`PixelAccess` Class +:py:class:`PixelAccess` class ============================= The PixelAccess class provides read and write access to @@ -40,7 +40,7 @@ Access using negative indexes is also possible. :: -:py:class:`PixelAccess` Class +:py:class:`PixelAccess` class ----------------------------- .. class:: PixelAccess diff --git a/docs/reference/TiffTags.rst b/docs/reference/TiffTags.rst index 7cb7d16ae47..d75a4847897 100644 --- a/docs/reference/TiffTags.rst +++ b/docs/reference/TiffTags.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.TiffTags .. py:currentmodule:: PIL.TiffTags -:py:mod:`~PIL.TiffTags` Module +:py:mod:`~PIL.TiffTags` module ============================== The :py:mod:`~PIL.TiffTags` module exposes many of the standard TIFF diff --git a/docs/reference/arrow_support.rst b/docs/reference/arrow_support.rst index 4a5c45e8624..063046d8c22 100644 --- a/docs/reference/arrow_support.rst +++ b/docs/reference/arrow_support.rst @@ -1,7 +1,7 @@ .. _arrow-support: ============= -Arrow Support +Arrow support ============= `Arrow `__ @@ -18,7 +18,7 @@ with any Arrow provider or consumer in the Python ecosystem. full-copy memory cost to reading an Arrow image. -Data Formats +Data formats ============ Pillow currently supports exporting Arrow images in all modes @@ -43,7 +43,7 @@ interpreted using the mode-specific interpretation of the bytes. The image mode must match the Arrow band format when reading single channel images. -Memory Allocator +Memory allocator ================ Pillow's default memory allocator, the :ref:`block_allocator`, @@ -59,7 +59,7 @@ To enable the single block allocator:: Note that this is a global setting, not a per-image setting. -Unsupported Features +Unsupported features ==================== * Table/dataframe protocol. We support a single array. @@ -71,7 +71,7 @@ Unsupported Features parameter. * Array metadata. -Internal Details +Internal details ================ Python Arrow C interface: diff --git a/docs/reference/block_allocator.rst b/docs/reference/block_allocator.rst index c6be5b7e6ed..5ad9d9fd123 100644 --- a/docs/reference/block_allocator.rst +++ b/docs/reference/block_allocator.rst @@ -1,10 +1,10 @@ .. _block_allocator: -Block Allocator +Block allocator =============== -Previous Design +Previous design --------------- Historically there have been two image allocators in Pillow: @@ -16,7 +16,7 @@ large images and makes one allocation for each scan line of size between one allocation and potentially thousands of small allocations, leading to unpredictable performance penalties around the transition. -New Design +New design ---------- ``ImagingAllocateArray`` now allocates space for images as a chain of @@ -28,7 +28,7 @@ line. This is now the default for all internal allocations. specifically requesting a single segment of memory for sharing with other code. -Memory Pools +Memory pools ------------ There is now a memory pool to contain a supply of recently freed diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst index 5e85869058c..12dca6cf2c9 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -1,5 +1,5 @@ -C Extension debugging on Linux, with gbd/valgrind. -================================================== +C extension debugging on Linux, with GBD/Valgrind +================================================= Install the tools ----------------- @@ -17,7 +17,7 @@ Then ``sudo apt-get install libtiff5-dbgsym`` - There's a bug with the ``python3-dbg`` package for at least Python 3.8 on Ubuntu 20.04, and you need to add a new link or two to make it autoload when - running python: + running Python: :: @@ -49,7 +49,7 @@ Then ``sudo apt-get install libtiff5-dbgsym`` source ~/vpy38-dbg/bin/activate cd ~/Pillow && make install -Test Case +Test case --------- Take your test image, and make a really simple harness. diff --git a/docs/reference/features.rst b/docs/reference/features.rst index c5d89b838d9..381d7830aac 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.features .. py:currentmodule:: PIL.features -:py:mod:`~PIL.features` Module +:py:mod:`~PIL.features` module ============================== The :py:mod:`PIL.features` module can be used to detect which Pillow features are available on your system. diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index 0411779535b..6bba673b9d6 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -1,4 +1,4 @@ -Internal Reference +Internal reference ================== .. toctree:: diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 93fd82cf9df..19f78864d1d 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -1,7 +1,7 @@ -Internal Modules +Internal modules ================ -:mod:`~PIL._binary` Module +:mod:`~PIL._binary` module -------------------------- .. automodule:: PIL._binary @@ -9,7 +9,7 @@ Internal Modules :undoc-members: :show-inheritance: -:mod:`~PIL._deprecate` Module +:mod:`~PIL._deprecate` module ----------------------------- .. automodule:: PIL._deprecate @@ -17,7 +17,7 @@ Internal Modules :undoc-members: :show-inheritance: -:mod:`~PIL._tkinter_finder` Module +:mod:`~PIL._tkinter_finder` module ---------------------------------- .. automodule:: PIL._tkinter_finder @@ -25,7 +25,7 @@ Internal Modules :undoc-members: :show-inheritance: -:mod:`~PIL._typing` Module +:mod:`~PIL._typing` module -------------------------- .. module:: PIL._typing @@ -58,7 +58,7 @@ on some Python versions. See :py:obj:`typing.TypeGuard`. -:mod:`~PIL._util` Module +:mod:`~PIL._util` module ------------------------ .. automodule:: PIL._util @@ -66,7 +66,7 @@ on some Python versions. :undoc-members: :show-inheritance: -:mod:`~PIL._version` Module +:mod:`~PIL._version` module --------------------------- .. module:: PIL._version @@ -78,7 +78,7 @@ on some Python versions. This is the master version number for Pillow, all other uses reference this module. -:mod:`PIL.Image.core` Module +:mod:`PIL.Image.core` module ---------------------------- .. module:: PIL._imaging diff --git a/docs/reference/limits.rst b/docs/reference/limits.rst index a71b514b5aa..d2f8f7d1fab 100644 --- a/docs/reference/limits.rst +++ b/docs/reference/limits.rst @@ -4,7 +4,7 @@ Limits This page is documentation to the various fundamental size limits in the Pillow implementation. -Internal Limits +Internal limits =============== * Image sizes cannot be negative. These are checked both in @@ -25,10 +25,10 @@ Internal Limits is smaller than 2GB, as calculated by ``y*stride`` (so 2Gpx for 'L' images, and .5Gpx for 'RGB' -Format Size Limits +Format size limits ================== * ICO: Max size is 256x256 -* Webp: 16383x16383 (underlying library size limit: +* WebP: 16383x16383 (underlying library size limit: https://developers.google.com/speed/webp/docs/api) diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index 730c8da5b80..0d43cbc730e 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -1,6 +1,6 @@ .. _file-handling: -File Handling in Pillow +File handling in Pillow ======================= When opening a file as an image, Pillow requires a filename, ``os.PathLike`` @@ -36,7 +36,7 @@ have multiple frames. Pillow cannot in general close and reopen a file, so any access to that file needs to be prior to the close. -Image Lifecycle +Image lifecycle --------------- * ``Image.open()`` Filenames and ``Path`` objects are opened as a file. @@ -97,7 +97,7 @@ Complications im6.load() # FAILS, closed file -Proposed File Handling +Proposed file handling ---------------------- * ``Image.Image.load()`` should close the image file, unless there are diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index c789f575700..243d4f353f5 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -1,7 +1,7 @@ Plugin reference ================ -:mod:`~PIL.AvifImagePlugin` Module +:mod:`~PIL.AvifImagePlugin` module ---------------------------------- .. automodule:: PIL.AvifImagePlugin @@ -9,7 +9,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.BmpImagePlugin` Module +:mod:`~PIL.BmpImagePlugin` module --------------------------------- .. automodule:: PIL.BmpImagePlugin @@ -17,7 +17,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.BufrStubImagePlugin` Module +:mod:`~PIL.BufrStubImagePlugin` module -------------------------------------- .. automodule:: PIL.BufrStubImagePlugin @@ -25,7 +25,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.CurImagePlugin` Module +:mod:`~PIL.CurImagePlugin` module --------------------------------- .. automodule:: PIL.CurImagePlugin @@ -33,7 +33,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.DcxImagePlugin` Module +:mod:`~PIL.DcxImagePlugin` module --------------------------------- .. automodule:: PIL.DcxImagePlugin @@ -41,7 +41,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.DdsImagePlugin` Module +:mod:`~PIL.DdsImagePlugin` module --------------------------------- .. automodule:: PIL.DdsImagePlugin @@ -49,7 +49,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.EpsImagePlugin` Module +:mod:`~PIL.EpsImagePlugin` module --------------------------------- .. automodule:: PIL.EpsImagePlugin @@ -57,15 +57,15 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.FitsImagePlugin` Module --------------------------------------- +:mod:`~PIL.FitsImagePlugin` module +---------------------------------- .. automodule:: PIL.FitsImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`~PIL.FliImagePlugin` Module +:mod:`~PIL.FliImagePlugin` module --------------------------------- .. automodule:: PIL.FliImagePlugin @@ -73,7 +73,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.FpxImagePlugin` Module +:mod:`~PIL.FpxImagePlugin` module --------------------------------- .. automodule:: PIL.FpxImagePlugin @@ -81,7 +81,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.GbrImagePlugin` Module +:mod:`~PIL.GbrImagePlugin` module --------------------------------- .. automodule:: PIL.GbrImagePlugin @@ -89,7 +89,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.GifImagePlugin` Module +:mod:`~PIL.GifImagePlugin` module --------------------------------- .. automodule:: PIL.GifImagePlugin @@ -97,7 +97,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.GribStubImagePlugin` Module +:mod:`~PIL.GribStubImagePlugin` module -------------------------------------- .. automodule:: PIL.GribStubImagePlugin @@ -105,7 +105,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.Hdf5StubImagePlugin` Module +:mod:`~PIL.Hdf5StubImagePlugin` module -------------------------------------- .. automodule:: PIL.Hdf5StubImagePlugin @@ -113,7 +113,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.IcnsImagePlugin` Module +:mod:`~PIL.IcnsImagePlugin` module ---------------------------------- .. automodule:: PIL.IcnsImagePlugin @@ -121,7 +121,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.IcoImagePlugin` Module +:mod:`~PIL.IcoImagePlugin` module --------------------------------- .. automodule:: PIL.IcoImagePlugin @@ -129,7 +129,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.ImImagePlugin` Module +:mod:`~PIL.ImImagePlugin` module -------------------------------- .. automodule:: PIL.ImImagePlugin @@ -137,7 +137,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.ImtImagePlugin` Module +:mod:`~PIL.ImtImagePlugin` module --------------------------------- .. automodule:: PIL.ImtImagePlugin @@ -145,7 +145,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.IptcImagePlugin` Module +:mod:`~PIL.IptcImagePlugin` module ---------------------------------- .. automodule:: PIL.IptcImagePlugin @@ -153,7 +153,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.JpegImagePlugin` Module +:mod:`~PIL.JpegImagePlugin` module ---------------------------------- .. automodule:: PIL.JpegImagePlugin @@ -161,7 +161,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.Jpeg2KImagePlugin` Module +:mod:`~PIL.Jpeg2KImagePlugin` module ------------------------------------ .. automodule:: PIL.Jpeg2KImagePlugin @@ -169,7 +169,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.McIdasImagePlugin` Module +:mod:`~PIL.McIdasImagePlugin` module ------------------------------------ .. automodule:: PIL.McIdasImagePlugin @@ -177,7 +177,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.MicImagePlugin` Module +:mod:`~PIL.MicImagePlugin` module --------------------------------- .. automodule:: PIL.MicImagePlugin @@ -185,7 +185,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.MpegImagePlugin` Module +:mod:`~PIL.MpegImagePlugin` module ---------------------------------- .. automodule:: PIL.MpegImagePlugin @@ -193,15 +193,15 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.MpoImagePlugin` Module ----------------------------------- +:mod:`~PIL.MpoImagePlugin` module +--------------------------------- .. automodule:: PIL.MpoImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`~PIL.MspImagePlugin` Module +:mod:`~PIL.MspImagePlugin` module --------------------------------- .. automodule:: PIL.MspImagePlugin @@ -209,7 +209,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PalmImagePlugin` Module +:mod:`~PIL.PalmImagePlugin` module ---------------------------------- .. automodule:: PIL.PalmImagePlugin @@ -217,7 +217,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PcdImagePlugin` Module +:mod:`~PIL.PcdImagePlugin` module --------------------------------- .. automodule:: PIL.PcdImagePlugin @@ -225,7 +225,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PcxImagePlugin` Module +:mod:`~PIL.PcxImagePlugin` module --------------------------------- .. automodule:: PIL.PcxImagePlugin @@ -233,7 +233,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PdfImagePlugin` Module +:mod:`~PIL.PdfImagePlugin` module --------------------------------- .. automodule:: PIL.PdfImagePlugin @@ -241,7 +241,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PixarImagePlugin` Module +:mod:`~PIL.PixarImagePlugin` module ----------------------------------- .. automodule:: PIL.PixarImagePlugin @@ -249,7 +249,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PngImagePlugin` Module +:mod:`~PIL.PngImagePlugin` module --------------------------------- .. automodule:: PIL.PngImagePlugin @@ -260,7 +260,7 @@ Plugin reference :member-order: groupwise -:mod:`~PIL.PpmImagePlugin` Module +:mod:`~PIL.PpmImagePlugin` module --------------------------------- .. automodule:: PIL.PpmImagePlugin @@ -268,7 +268,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PsdImagePlugin` Module +:mod:`~PIL.PsdImagePlugin` module --------------------------------- .. automodule:: PIL.PsdImagePlugin @@ -276,7 +276,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.SgiImagePlugin` Module +:mod:`~PIL.SgiImagePlugin` module --------------------------------- .. automodule:: PIL.SgiImagePlugin @@ -284,7 +284,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.SpiderImagePlugin` Module +:mod:`~PIL.SpiderImagePlugin` module ------------------------------------ .. automodule:: PIL.SpiderImagePlugin @@ -292,7 +292,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.SunImagePlugin` Module +:mod:`~PIL.SunImagePlugin` module --------------------------------- .. automodule:: PIL.SunImagePlugin @@ -300,7 +300,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.TgaImagePlugin` Module +:mod:`~PIL.TgaImagePlugin` module --------------------------------- .. automodule:: PIL.TgaImagePlugin @@ -308,7 +308,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.TiffImagePlugin` Module +:mod:`~PIL.TiffImagePlugin` module ---------------------------------- .. automodule:: PIL.TiffImagePlugin @@ -316,7 +316,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.WebPImagePlugin` Module +:mod:`~PIL.WebPImagePlugin` module ---------------------------------- .. automodule:: PIL.WebPImagePlugin @@ -324,7 +324,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.WmfImagePlugin` Module +:mod:`~PIL.WmfImagePlugin` module --------------------------------- .. automodule:: PIL.WmfImagePlugin @@ -332,7 +332,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.XVThumbImagePlugin` Module +:mod:`~PIL.XVThumbImagePlugin` module ------------------------------------- .. automodule:: PIL.XVThumbImagePlugin @@ -340,7 +340,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.XbmImagePlugin` Module +:mod:`~PIL.XbmImagePlugin` module --------------------------------- .. automodule:: PIL.XbmImagePlugin @@ -348,7 +348,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.XpmImagePlugin` Module +:mod:`~PIL.XpmImagePlugin` module --------------------------------- .. automodule:: PIL.XpmImagePlugin diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 2ea973c5c76..3e2aa84b1ce 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -28,7 +28,7 @@ This threshold can be changed by setting :py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. -Backwards Incompatible Changes +Backwards incompatible changes ============================== Categories @@ -164,7 +164,7 @@ Since Pillow's C API is now faster than PyAccess on PyPy, ``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is similarly deprecated. -API Changes +API changes =========== Added line width parameter to ImageDraw regular_polygon @@ -173,7 +173,7 @@ Added line width parameter to ImageDraw regular_polygon An optional line ``width`` parameter has been added to ``ImageDraw.Draw.regular_polygon``. -API Additions +API additions ============= Added ``alpha_only`` argument to ``getbbox()`` @@ -184,7 +184,7 @@ Added ``alpha_only`` argument to ``getbbox()`` and the image has an alpha channel, trim transparent pixels. Otherwise, trim pixels when all channels are zero. -Other Changes +Other changes ============= 32-bit wheels diff --git a/docs/releasenotes/10.0.1.rst b/docs/releasenotes/10.0.1.rst index 02189d51405..aa17a62e02b 100644 --- a/docs/releasenotes/10.0.1.rst +++ b/docs/releasenotes/10.0.1.rst @@ -11,7 +11,7 @@ This release provides an updated install script and updated wheels to include libwebp 1.3.2, preventing a potential heap buffer overflow in WebP. -Other Changes +Other changes ============= Updated tests to pass with latest zlib version diff --git a/docs/releasenotes/10.1.0.rst b/docs/releasenotes/10.1.0.rst index fd556bdf17f..649c2bdf971 100644 --- a/docs/releasenotes/10.1.0.rst +++ b/docs/releasenotes/10.1.0.rst @@ -1,7 +1,7 @@ 10.1.0 ------ -API Changes +API changes =========== Setting image mode @@ -35,7 +35,7 @@ to be specified, rather than a single number for both dimensions. :: ImageFilter.BoxBlur((2, 5)) ImageFilter.GaussianBlur((2, 5)) -API Additions +API additions ============= EpsImagePlugin.gs_binary @@ -84,7 +84,7 @@ font size for this new builtin font:: draw.multiline_text((0, 0), "test", font_size=24) draw.multiline_textbbox((0, 0), "test", font_size=24) -Other Changes +Other changes ============= Python 3.12 diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 1c6b78b0841..3377487857e 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -53,7 +53,7 @@ The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant for internal use, so there is no replacement. They can each be replaced by a single line of code using builtin functions in Python. -API Changes +API changes =========== Zero or negative font size error @@ -63,7 +63,7 @@ When creating a :py:class:`~PIL.ImageFont.FreeTypeFont` instance, either directl through :py:func:`~PIL.ImageFont.truetype`, if the font size is zero or less, a :py:exc:`ValueError` will now be raised. -API Additions +API additions ============= Added DdsImagePlugin enums @@ -95,7 +95,7 @@ JPEG tables-only streamtype When saving JPEG files, ``streamtype`` can now be set to 1, for tables-only. This will output only the quantization and Huffman tables for the image. -Other Changes +Other changes ============= Added DDS BC4U and DX10 BC1 and BC4 reading diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 2f0437d94f9..6c7d8ea0a11 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -65,7 +65,7 @@ ImageMath.eval() :py:meth:`~PIL.ImageMath.unsafe_eval` instead. See earlier security notes for more information. -API Changes +API changes =========== Added alpha_quality argument when saving WebP images @@ -87,7 +87,7 @@ Negative P1-P3 PPM value error If a P1-P3 PPM image contains a negative value, a :py:exc:`ValueError` will now be raised. -API Additions +API additions ============= Added PerspectiveTransform @@ -97,7 +97,7 @@ Added PerspectiveTransform that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding subclass of :py:class:`~PIL.ImageTransform.Transform`. -Other Changes +Other changes ============= Portable FloatMap (PFM) images diff --git a/docs/releasenotes/10.4.0.rst b/docs/releasenotes/10.4.0.rst index 8d3706be617..84a6091c96d 100644 --- a/docs/releasenotes/10.4.0.rst +++ b/docs/releasenotes/10.4.0.rst @@ -41,7 +41,7 @@ ImageDraw.getdraw hints parameter The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. -API Additions +API additions ============= ImageDraw.circle @@ -51,7 +51,7 @@ Added :py:meth:`~PIL.ImageDraw.ImageDraw.circle`. It provides the same functiona :py:meth:`~PIL.ImageDraw.ImageDraw.ellipse`, but instead of taking a bounding box, it takes a center point and radius. -Other Changes +Other changes ============= Python 3.13 beta diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index c3f18140f42..020fbf7df7d 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -1,7 +1,7 @@ 11.0.0 ------ -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 3.8 @@ -103,7 +103,7 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They have been deprecated, and will be removed in Pillow 12 (2025-10-15). -Specific WebP Feature Checks +Specific WebP feature checks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 11.0.0 @@ -113,7 +113,7 @@ Specific WebP Feature Checks ``True`` if the WebP module is installed, until they are removed in Pillow 12.0.0 (2025-10-15). -API Changes +API changes =========== Default resampling filter for I;16* image modes @@ -122,7 +122,7 @@ Default resampling filter for I;16* image modes The default resampling filter for I;16, I;16L, I;16B and I;16N has been changed from ``Image.NEAREST`` to ``Image.BICUBIC``, to match the majority of modes. -API Additions +API additions ============= Writing XMP bytes to JPEG and MPO @@ -138,7 +138,7 @@ either JPEG or MPO images:: im.info["xmp"] = b"test" im.save("out.jpg") -Other Changes +Other changes ============= Python 3.13 @@ -154,7 +154,7 @@ Support has also been added for the experimental free-threaded mode of :pep:`703 Python 3.13 only supports macOS versions 10.13 and later. -C-level Flags +C-level flags ^^^^^^^^^^^^^ Some compiling flags like ``WITH_THREADING``, ``WITH_IMAGECHOPS``, and other diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index 0d56cb4204b..4888ddf56d3 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -10,7 +10,7 @@ ExifTags.IFD.Makernote ``ExifTags.IFD.Makernote`` has been deprecated. Instead, use ``ExifTags.IFD.MakerNote``. -API Changes +API changes =========== Writing XMP bytes to JPEG and MPO @@ -34,7 +34,7 @@ be used:: second_im.encoderinfo = {"xmp": b"test"} im.save("out.mpo", save_all=True, append_images=[second_im]) -API Additions +API additions ============= Check for zlib-ng @@ -54,7 +54,7 @@ TIFF images can now be saved as BigTIFF using a ``big_tiff`` argument:: im.save("out.tiff", big_tiff=True) -Other Changes +Other changes ============= Reading JPEG 2000 comments diff --git a/docs/releasenotes/11.2.1.rst b/docs/releasenotes/11.2.1.rst index 5c6d40d9df3..f55b0d7d776 100644 --- a/docs/releasenotes/11.2.1.rst +++ b/docs/releasenotes/11.2.1.rst @@ -32,7 +32,7 @@ Image.Image.get_child_images() method uses an image's file pointer, and so child images could only be retrieved from an :py:class:`PIL.ImageFile.ImageFile` instance. -API Changes +API changes =========== ``append_images`` no longer requires ``save_all`` @@ -44,7 +44,7 @@ supports saving multiple frames:: im.save("out.gif", append_images=ims) -API Additions +API additions ============= ``"justify"`` multiline text alignment @@ -86,7 +86,7 @@ DXT5, BC2, BC3 and BC5 are supported:: im.save("out.dds", pixel_format="DXT1") -Other Changes +Other changes ============= Arrow support diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index e9b0995bb9f..a1ddd117873 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -1,13 +1,13 @@ 2.7.0 ----- -Sane Plugin +Sane plugin ^^^^^^^^^^^ The Sane plugin has now been split into its own repo: https://github.com/python-pillow/Sane . -Png text chunk size limits +PNG text chunk size limits ^^^^^^^^^^^^^^^^^^^^^^^^^^ To prevent potential denial of service attacks using compressed text @@ -155,7 +155,7 @@ so the quality was worse compared to other Gaussian blur software. The new implementation does not have this drawback. -TIFF Parameter Changes +TIFF parameter changes ^^^^^^^^^^^^^^^^^^^^^^ Several kwarg parameters for saving TIFF images were previously diff --git a/docs/releasenotes/3.0.0.rst b/docs/releasenotes/3.0.0.rst index 8bc477f7020..dcd8031f588 100644 --- a/docs/releasenotes/3.0.0.rst +++ b/docs/releasenotes/3.0.0.rst @@ -1,7 +1,7 @@ 3.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Several methods that have been marked as deprecated for many releases @@ -18,10 +18,10 @@ have been removed in this release: * ``ImageWin.fromstring()`` * ``ImageWin.tostring()`` -Other Changes +Other changes ============= -Saving Multipage Images +Saving multipage images ^^^^^^^^^^^^^^^^^^^^^^^ There is now support for saving multipage images in the ``GIF`` and @@ -30,10 +30,10 @@ as a keyword argument to the save:: im.save('test.pdf', save_all=True) -Tiff ImageFileDirectory Rewrite +TIFF ImageFileDirectory rewrite ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The Tiff ImageFileDirectory metadata code has been rewritten. Where +The TIFF ImageFileDirectory metadata code has been rewritten. Where previously it returned a somewhat arbitrary set of values and tuples, it now returns bare values where appropriate and tuples when the metadata item is a sequence or collection. @@ -41,7 +41,7 @@ metadata item is a sequence or collection. The original metadata is still available in the TiffImage.tags, the new values are available in the TiffImage.tags_v2 member. The old structures will be deprecated at some point in the future. When -saving Tiff metadata, new code should use the +saving TIFF metadata, new code should use the TiffImagePlugin.ImageFileDirectory_v2 class. LibJpeg and Zlib are required by default diff --git a/docs/releasenotes/3.1.0.rst b/docs/releasenotes/3.1.0.rst index 951819f1956..90f77ff6150 100644 --- a/docs/releasenotes/3.1.0.rst +++ b/docs/releasenotes/3.1.0.rst @@ -22,7 +22,7 @@ not the absolute height of each line. There is also now a default spacing of 4px between lines. -Exif, Jpeg and Tiff Metadata +EXIF, JPEG and TIFF metadata ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There were major changes in the TIFF ImageFileDirectory support in @@ -63,7 +63,7 @@ single item tuples have been unwrapped and return a bare element. The format returned by Pillow 3.0 has been abandoned. A more fully featured interface for EXIF is anticipated in a future release. -Out of Spec Metadata +Out of spec metadata ++++++++++++++++++++ In Pillow 3.0 and 3.1, images that contain metadata that is internally diff --git a/docs/releasenotes/3.2.0.rst b/docs/releasenotes/3.2.0.rst index 3ed8fae574b..20d7d073eee 100644 --- a/docs/releasenotes/3.2.0.rst +++ b/docs/releasenotes/3.2.0.rst @@ -1,7 +1,7 @@ 3.2.0 ----- -New DDS and FTEX Image Plugins +New DDS and FTEX image plugins ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``DdsImagePlugin`` reading DXT1 and DXT5 encoded ``.dds`` images was @@ -18,7 +18,7 @@ Updates to the GbrImagePlugin The ``GbrImagePlugin`` (GIMP brush format) has been updated to fix support for version 1 files and add support for version 2 files. -Passthrough Parameters for ImageDraw.text +Passthrough parameters for ImageDraw.text ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``ImageDraw.multiline_text`` and ``ImageDraw.multiline_size`` take extra diff --git a/docs/releasenotes/3.3.0.rst b/docs/releasenotes/3.3.0.rst index cd6f7e2f93c..9447245c495 100644 --- a/docs/releasenotes/3.3.0.rst +++ b/docs/releasenotes/3.3.0.rst @@ -11,7 +11,7 @@ libimagequant. We cannot distribute binaries due to licensing differences. -New Setup.py options +New setup.py options ^^^^^^^^^^^^^^^^^^^^ There are two new options to control the ``build_ext`` task in ``setup.py``: @@ -43,7 +43,7 @@ This greatly improves both quality and performance in this case. Also, the bug with wrong image size calculation when rotating by 90 degrees was fixed. -Image Metadata +Image metadata ^^^^^^^^^^^^^^ The return type for binary data in version 2 Exif and Tiff metadata diff --git a/docs/releasenotes/3.3.2.rst b/docs/releasenotes/3.3.2.rst index 73156a65dbb..60ffbdcbadb 100644 --- a/docs/releasenotes/3.3.2.rst +++ b/docs/releasenotes/3.3.2.rst @@ -4,7 +4,7 @@ Security ======== -Integer overflow in Map.c +Integer overflow in map.c ^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow prior to 3.3.2 may experience integer overflow errors in map.c @@ -27,7 +27,7 @@ memory without duplicating the image first. This issue was found by Cris Neckar at Divergent Security. -Sign Extension in Storage.c +Sign extension in Storage.c ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow prior to 3.3.2 and PIL 1.1.7 (at least) do not check for diff --git a/docs/releasenotes/3.4.0.rst b/docs/releasenotes/3.4.0.rst index 8a5a7efe350..01ec77a58cc 100644 --- a/docs/releasenotes/3.4.0.rst +++ b/docs/releasenotes/3.4.0.rst @@ -1,7 +1,7 @@ 3.4.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Image.core.open_ppm removed @@ -14,7 +14,7 @@ been removed. If you were using this function, please use Deprecations ============ -Deprecation Warning when Saving JPEGs +Deprecation warning when saving JPEGs ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ JPEG images cannot contain an alpha channel. Pillow prior to 3.4.0 @@ -22,7 +22,7 @@ silently drops the alpha channel. With this release Pillow will now issue a :py:exc:`DeprecationWarning` when attempting to save a ``RGBA`` mode image as a JPEG. This will become an error in Pillow 4.2. -API Additions +API additions ============= New resizing filters @@ -37,7 +37,7 @@ two times shorter window than ``BILINEAR``. It can be used for image reduction providing the image downscaling quality comparable to ``BICUBIC``. Both new filters don't show good quality for the image upscaling. -New DDS Decoders +New DDS decoders ^^^^^^^^^^^^^^^^ Pillow can now decode DXT3 images, as well as the previously supported diff --git a/docs/releasenotes/4.0.0.rst b/docs/releasenotes/4.0.0.rst index 625f237e841..dd97463f6e3 100644 --- a/docs/releasenotes/4.0.0.rst +++ b/docs/releasenotes/4.0.0.rst @@ -1,7 +1,7 @@ 4.0.0 ----- -Python 2.6 and 3.2 Dropped +Python 2.6 and 3.2 dropped ^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow 4.0 no longer supports Python 2.6 and 3.2. We will not be diff --git a/docs/releasenotes/4.1.0.rst b/docs/releasenotes/4.1.0.rst index 80ad9b9fb63..1f809ad18cb 100644 --- a/docs/releasenotes/4.1.0.rst +++ b/docs/releasenotes/4.1.0.rst @@ -15,10 +15,10 @@ Several deprecated items have been removed. ``PIL.ImageDraw.ImageDraw.setfont`` have been removed. -Other Changes +Other changes ============= -Closing Files When Opening Images +Closing files when opening images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The file handling when opening images has been overhauled. Previously, @@ -41,7 +41,7 @@ is specified: the underlying file until we are done with the image. The mapping will be closed in the ``close`` or ``__del__`` method. -Changes to GIF Handling When Saving +Changes to GIF handling when saving ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The :py:class:`PIL.GifImagePlugin` code has been refactored to fix the flow when @@ -57,14 +57,14 @@ saving images. There are two external changes that arise from this: This refactor fixed some bugs with palette handling when saving multiple frame GIFs. -New Method: Image.remap_palette +New method: Image.remap_palette ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The method :py:meth:`PIL.Image.Image.remap_palette()` has been added. This method was hoisted from the GifImagePlugin code used to optimize the palette. -Added Decoder Registry and Support for Python Based Decoders +Added decoder registry and support for Python-based decoders ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There is now a decoder registry similar to the image plugin diff --git a/docs/releasenotes/4.1.1.rst b/docs/releasenotes/4.1.1.rst index 8c8055bfad8..1cbd3853b30 100644 --- a/docs/releasenotes/4.1.1.rst +++ b/docs/releasenotes/4.1.1.rst @@ -1,7 +1,7 @@ 4.1.1 ----- -Fix Regression with reading DPI from EXIF data +Fix regression with reading DPI from EXIF data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Some JPEG images don't contain DPI information in the image metadata, diff --git a/docs/releasenotes/4.2.0.rst b/docs/releasenotes/4.2.0.rst index bc2a45f025f..0ea3de39906 100644 --- a/docs/releasenotes/4.2.0.rst +++ b/docs/releasenotes/4.2.0.rst @@ -1,7 +1,7 @@ 4.2.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Several deprecated items have been removed @@ -17,17 +17,17 @@ Several deprecated items have been removed was shown. From Pillow 4.2.0, the deprecation warning is removed and an :py:exc:`IOError` is raised. -Removed Core Image Function +Removed core Image function ^^^^^^^^^^^^^^^^^^^^^^^^^^^ The unused function ``Image.core.new_array`` was removed. This is an internal function that should not have been used by user code, but it was accessible from the python layer. -Other Changes +Other changes ============= -Added Complex Text Rendering +Added complex text rendering ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow now supports complex text rendering for scripts requiring glyph @@ -36,7 +36,7 @@ dependencies: harfbuzz, fribidi, and raqm. See the :doc:`install documentation <../installation>` for further details. This feature is tested and works on Unix and Mac, but has not yet been built on Windows platforms. -New Optional Parameters +New optional parameters ^^^^^^^^^^^^^^^^^^^^^^^ * :py:meth:`PIL.ImageDraw.floodfill` has a new optional parameter: @@ -47,7 +47,7 @@ New Optional Parameters optional parameter for specifying additional images to create multipage outputs. -New DecompressionBomb Warning +New DecompressionBomb warning ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :py:meth:`PIL.Image.Image.crop` now may raise a DecompressionBomb diff --git a/docs/releasenotes/4.2.1.rst b/docs/releasenotes/4.2.1.rst index 2061f646746..617d51e52c2 100644 --- a/docs/releasenotes/4.2.1.rst +++ b/docs/releasenotes/4.2.1.rst @@ -3,7 +3,7 @@ There are no functional changes in this release. -Fixed Windows PyPy Build +Fixed Windows PyPy build ^^^^^^^^^^^^^^^^^^^^^^^^ A change in the 4.2.0 cycle broke the Windows PyPy build. This has diff --git a/docs/releasenotes/4.3.0.rst b/docs/releasenotes/4.3.0.rst index ea81fc45ea0..87a57799f5a 100644 --- a/docs/releasenotes/4.3.0.rst +++ b/docs/releasenotes/4.3.0.rst @@ -1,7 +1,7 @@ 4.3.0 ----- -API Changes +API changes =========== Deprecations @@ -12,7 +12,7 @@ Several undocumented functions in ImageOps have been deprecated: ``box_blur``. Use the equivalent operations in ``ImageFilter`` instead. These functions will be removed in a future release. -TIFF Metadata Changes +TIFF metadata changes ^^^^^^^^^^^^^^^^^^^^^ * TIFF tags with unknown type/quantity now default to being bare @@ -27,7 +27,7 @@ TIFF Metadata Changes items, as there can be multiple items, one for UTF-8, and one for UTF-16. -Core Image API Changes +Core Image API changes ^^^^^^^^^^^^^^^^^^^^^^ These are internal functions that should not have been used by user @@ -44,10 +44,10 @@ The ``PIL.Image.core.getcount`` methods have been removed, use ``PIL.Image.core.get_stats()['new_count']`` property instead. -API Additions +API additions ============= -Get One Channel From Image +Get one channel from image ^^^^^^^^^^^^^^^^^^^^^^^^^^ A new method :py:meth:`PIL.Image.Image.getchannel` has been added to @@ -56,14 +56,14 @@ return a single channel by index or name. For example, ``getchannel`` should work up to 6 times faster than ``image.split()[0]`` in previous Pillow versions. -Box Blur +Box blur ^^^^^^^^ A new filter, :py:class:`PIL.ImageFilter.BoxBlur`, has been added. This is a filter with similar results to a Gaussian blur, but is much faster. -Partial Resampling +Partial resampling ^^^^^^^^^^^^^^^^^^ Added new argument ``box`` for :py:meth:`PIL.Image.Image.resize`. This @@ -71,14 +71,14 @@ argument defines a source rectangle from within the source image to be resized. This is very similar to the ``image.crop(box).resize(size)`` sequence except that ``box`` can be specified with subpixel accuracy. -New Transpose Operation +New transpose operation ^^^^^^^^^^^^^^^^^^^^^^^ The ``Image.TRANSVERSE`` operation has been added to :py:meth:`PIL.Image.Image.transpose`. This is equivalent to a transpose operation about the opposite diagonal. -Multiband Filters +Multiband filters ^^^^^^^^^^^^^^^^^ There is a new :py:class:`PIL.ImageFilter.MultibandFilter` base class @@ -87,10 +87,10 @@ operation. The original :py:class:`PIL.ImageFilter.Filter` class remains for image filters that can process only single band images, or require splitting of channels prior to filtering. -Other Changes +Other changes ============= -Loading 16-bit TIFF Images +Loading 16-bit TIFF images ^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow now can read 16-bit multichannel TIFF files including files @@ -101,7 +101,7 @@ Pillow now can read 16-bit signed integer single channel TIFF files. The image data is promoted to 32-bit for storage and processing. -SGI Images +SGI images ^^^^^^^^^^ Pillow can now read and write uncompressed 16-bit multichannel SGI @@ -129,7 +129,7 @@ This release contains several performance improvements: falling back to an allocation for each scan line for images larger than the block size. -CMYK Conversion +CMYK conversion ^^^^^^^^^^^^^^^ The basic CMYK->RGB conversion has been tweaked to match the formula diff --git a/docs/releasenotes/5.0.0.rst b/docs/releasenotes/5.0.0.rst index be00a45cd87..2b93e032287 100644 --- a/docs/releasenotes/5.0.0.rst +++ b/docs/releasenotes/5.0.0.rst @@ -1,10 +1,10 @@ 5.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== -Python 3.3 Dropped +Python 3.3 dropped ^^^^^^^^^^^^^^^^^^ Python 3.3 is EOL and no longer supported due to moving testing from nose, @@ -12,7 +12,7 @@ which is deprecated, to pytest, which doesn't support Python 3.3. We will not be creating binaries, testing, or retaining compatibility with this version. The final version of Pillow for Python 3.3 is 4.3.0. -Decompression Bombs now raise Exceptions +Decompression bombs now raise exceptions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow has previously emitted warnings for images that are @@ -31,7 +31,7 @@ separate package, pillow-scripts, living at https://github.com/python-pillow/pillow-scripts. -API Changes +API changes =========== OleFileIO.py @@ -54,7 +54,7 @@ Several image plugins supported a named ``check`` parameter on their nominally private ``_save`` method to preflight if the image could be saved in that format. That parameter has been removed. -API Additions +API additions ============= Image.transform @@ -65,16 +65,16 @@ A new named parameter, ``fillcolor``, has been added to the area outside the transformed area in the output image. This parameter takes the same color specifications as used in ``Image.new``. -GIF Disposal +GIF disposal ^^^^^^^^^^^^ Multiframe GIF images now take an optional disposal parameter to specify the disposal option for changed pixels. -Other Changes +Other changes ============= -Compressed TIFF Images +Compressed TIFF images ^^^^^^^^^^^^^^^^^^^^^^ Previously, there were some compression modes (JPEG, Packbits, and @@ -82,7 +82,7 @@ LZW) that were supported with Pillow's internal TIFF decoder. All compressed TIFFs are now read using the ``libtiff`` decoder, as it implements the compression schemes more correctly. -Libraqm is now Dynamically Linked +Libraqm is now dynamically linked ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The libraqm dependency for complex text scripts is now linked @@ -90,14 +90,14 @@ dynamically at runtime rather than at packaging time. This allows us to release binaries with support for libraqm if it is installed on the user's machine. -Source Layout Changes +Source layout changes ^^^^^^^^^^^^^^^^^^^^^ The Pillow source is now stored within the ``src`` directory of the distribution. This prevents accidental imports of the PIL directory when running Python from the project directory. -Setup.py Changes +Setup.py changes ^^^^^^^^^^^^^^^^ Multiarch support on Linux should be more robust, especially on Debian diff --git a/docs/releasenotes/5.1.0.rst b/docs/releasenotes/5.1.0.rst index 4e3d10ac596..4b80e8521cf 100644 --- a/docs/releasenotes/5.1.0.rst +++ b/docs/releasenotes/5.1.0.rst @@ -1,7 +1,7 @@ 5.1.0 ----- -API Changes +API changes =========== Optional channels for TIFF files @@ -12,22 +12,22 @@ and ``CMYK`` with up to 6 8-bit channels, discarding any extra channels if the content is tagged as UNSPECIFIED. Pillow still does not store more than 4 8-bit channels of image data. -API Additions +API additions ============= -Append to PDF Files +Append to PDF files ^^^^^^^^^^^^^^^^^^^ Images can now be appended to PDF files in place by passing in ``append=True`` when saving the image. -New BLP File Format +New BLP file format ^^^^^^^^^^^^^^^^^^^ Pillow now supports reading the BLP "Blizzard Mipmap" file format used for tiles in Blizzard's engine. -Other Changes +Other changes ============= WebP memory leak diff --git a/docs/releasenotes/5.2.0.rst b/docs/releasenotes/5.2.0.rst index d9b8f0fb7c8..d183378201b 100644 --- a/docs/releasenotes/5.2.0.rst +++ b/docs/releasenotes/5.2.0.rst @@ -1,7 +1,7 @@ 5.2.0 ----- -API Changes +API changes =========== Deprecations @@ -17,7 +17,7 @@ Pillow 6.0.0, and ``PILLOW_VERSION`` will be removed after that. Use ``PIL.__version__`` instead. -API Additions +API additions ============= 3D color lookup tables @@ -75,7 +75,7 @@ TGA file format Pillow can now read and write LA data (in addition to L, P, RGB and RGBA), and write RLE data (in addition to uncompressed). -Other Changes +Other changes ============= Support added for Python 3.7 diff --git a/docs/releasenotes/5.3.0.rst b/docs/releasenotes/5.3.0.rst index 8f276da2407..6adce95b210 100644 --- a/docs/releasenotes/5.3.0.rst +++ b/docs/releasenotes/5.3.0.rst @@ -1,7 +1,7 @@ 5.3.0 ----- -API Changes +API changes =========== Image size @@ -20,7 +20,7 @@ The exceptions to this are: as direct image size setting was previously necessary to work around an issue with tile extents. -API Additions +API additions ============= Added line width parameter to rectangle and ellipse-based shapes @@ -59,7 +59,7 @@ and size, new method ``ImageOps.pad`` pads images to fill a requested aspect ratio and size, filling new space with a provided ``color`` and positioning the image within the new area through a ``centering`` argument. -Other Changes +Other changes ============= Added support for reading tiled TIFF images through LibTIFF. Compressed TIFF diff --git a/docs/releasenotes/5.4.0.rst b/docs/releasenotes/5.4.0.rst index 6d7277c70ea..13b540d600b 100644 --- a/docs/releasenotes/5.4.0.rst +++ b/docs/releasenotes/5.4.0.rst @@ -1,7 +1,7 @@ 5.4.0 ----- -API Changes +API changes =========== APNG extension to PNG plugin @@ -55,7 +55,7 @@ TIFF images can now be saved with custom integer, float and string TIFF tags:: print(im2.tag_v2[37002]) # "custom tag value" print(im2.tag_v2[37004]) # b"custom tag value" -Other Changes +Other changes ============= ImageOps.fit diff --git a/docs/releasenotes/6.0.0.rst b/docs/releasenotes/6.0.0.rst index 5e69f0b6b5a..b788b2eeb71 100644 --- a/docs/releasenotes/6.0.0.rst +++ b/docs/releasenotes/6.0.0.rst @@ -1,7 +1,7 @@ 6.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 3.4 dropped @@ -32,7 +32,7 @@ Removed deprecated VERSION ``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use ``__version__`` instead. -API Changes +API changes =========== Deprecations @@ -137,7 +137,7 @@ loaded, ``Image.MIME["PPM"]`` will now return the generic "image/x-portable-anym The TGA, PCX and ICO formats also now have MIME types: "image/x-tga", "image/x-pcx" and "image/x-icon" respectively. -API Additions +API additions ============= DIB file format @@ -186,7 +186,7 @@ EXIF data can now be read from and saved to PNG images. However, unlike other im formats, EXIF data is not guaranteed to be present in :py:attr:`~PIL.Image.Image.info` until :py:meth:`~PIL.Image.Image.load` has been called. -Other Changes +Other changes ============= Reading new DDS image format diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst index ce3edc5fa9b..761f435f391 100644 --- a/docs/releasenotes/6.1.0.rst +++ b/docs/releasenotes/6.1.0.rst @@ -23,7 +23,7 @@ Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") -API Additions +API additions ============= Image.entropy @@ -61,7 +61,7 @@ file. ``ImageFont.FreeTypeFont`` has four new methods, instead. An :py:exc:`IOError` will be raised if the font is not a variation font. FreeType 2.9.1 or greater is required. -Other Changes +Other changes ============= ImageTk.getimage diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index b851c56fc0e..b37cd7160e7 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -29,7 +29,7 @@ perform operations on it. The CVE is regarding DOS problems, such as consuming large amounts of memory, or taking a large amount of time to process an image. -API Changes +API changes =========== Image.getexif @@ -48,7 +48,7 @@ There has been a longstanding warning that the defaults of ``Image.frombuffer`` may change in the future for the "raw" decoder. The change will now take place in Pillow 7.0. -API Additions +API additions ============= Text stroking @@ -93,7 +93,7 @@ ImageGrab on multi-monitor Windows An ``all_screens`` argument has been added to ``ImageGrab.grab``. If ``True``, all monitors will be included in the created image. -Other Changes +Other changes ============= Removed bdist_wininst .exe installers diff --git a/docs/releasenotes/6.2.1.rst b/docs/releasenotes/6.2.1.rst index 372298fbc2a..0ede05917a4 100644 --- a/docs/releasenotes/6.2.1.rst +++ b/docs/releasenotes/6.2.1.rst @@ -1,7 +1,7 @@ 6.2.1 ----- -API Changes +API changes =========== Deprecations @@ -15,7 +15,7 @@ Python 2.7 reaches end-of-life on 2020-01-01. Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python 2.7, making Pillow 6.2.x the last release series to support Python 2. -Other Changes +Other changes ============= Support added for Python 3.8 diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index ed6026593e6..9504c974a11 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -1,7 +1,7 @@ 7.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 2.7 @@ -78,7 +78,7 @@ bounds of resulting image. This may be useful in a subsequent .. _chain methods: https://en.wikipedia.org/wiki/Method_chaining -API Additions +API additions ============= Custom unidentified image error @@ -124,7 +124,7 @@ now also be loaded at another resolution:: with Image.open("drawing.wmf") as im: im.load(dpi=144) -Other Changes +Other changes ============= Image.__del__ diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index 0dd8669a5b8..c2aeb0f749b 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -35,7 +35,7 @@ out-of-bounds reads via a crafted JP2 file. In ``libImaging/SgiRleDecode.c`` in Pillow through 7.0.0, a number of out-of-bounds reads exist in the parsing of SGI image files, a different issue than :cve:`2020-5311`. -API Changes +API changes =========== Allow saving of zero quality JPEG images @@ -50,7 +50,7 @@ been resolved. :: im = Image.open("hopper.jpg") im.save("out.jpg", quality=0) -API Additions +API additions ============= New channel operations @@ -101,7 +101,7 @@ Passing a different value on Windows or macOS will force taking a snapshot using the selected X server; pass an empty string to use the default X server. XCB support is not included in pre-compiled wheels for Windows and macOS. -Other Changes +Other changes ============= If present, only use alpha channel for bounding box diff --git a/docs/releasenotes/7.2.0.rst b/docs/releasenotes/7.2.0.rst index 91e54da1999..12bafa8ce90 100644 --- a/docs/releasenotes/7.2.0.rst +++ b/docs/releasenotes/7.2.0.rst @@ -1,7 +1,7 @@ 7.2.0 ----- -API Changes +API changes =========== Replaced TiffImagePlugin DEBUG with logging diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index 1fc245c9a3c..d0dde756fc3 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -1,7 +1,7 @@ 8.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 3.5 @@ -44,7 +44,7 @@ Removed Use instead ``product_model`` Unicode :py:attr:`~.CmsProfile.model` ======================== =================================================== -API Changes +API changes =========== ImageDraw.text: stroke_width @@ -67,7 +67,7 @@ Add MIME type to PsdImagePlugin "image/vnd.adobe.photoshop" is now registered as the :py:class:`.PsdImagePlugin.PsdImageFile` MIME type. -API Additions +API additions ============= Image.open: add formats parameter @@ -135,7 +135,7 @@ and :py:meth:`.FreeTypeFont.getbbox` return the bounding box of rendered text. These functions accept an ``anchor`` parameter, see :ref:`text-anchors` for details. -Other Changes +Other changes ============= Improved ellipse-drawing algorithm diff --git a/docs/releasenotes/8.1.0.rst b/docs/releasenotes/8.1.0.rst index 5c399331846..06e6d997484 100644 --- a/docs/releasenotes/8.1.0.rst +++ b/docs/releasenotes/8.1.0.rst @@ -26,7 +26,7 @@ leading to an out-of-bounds write in ``TiffDecode.c``. This potentially affects versions from 6.0.0 to 8.0.1, depending on the version of LibTIFF. This was reported through `Tidelift`_. -:cve:`2020-35655`: SGI Decode buffer overrun +:cve:`2020-35655`: SGI decode buffer overrun ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 byte read overflow in ``SgiRleDecode.c``, where the code was not correctly @@ -64,7 +64,7 @@ Makefile The ``install-venv`` target has been deprecated. -API Additions +API additions ============= Append images to ICO @@ -77,7 +77,7 @@ With this release, a list of images can be provided to the ``append_images`` par when saving, to replace the scaled down versions. This is the same functionality that already exists for the ICNS format. -Other Changes +Other changes ============= Makefile diff --git a/docs/releasenotes/8.1.1.rst b/docs/releasenotes/8.1.1.rst index 690421c2a56..b8ad5a898a0 100644 --- a/docs/releasenotes/8.1.1.rst +++ b/docs/releasenotes/8.1.1.rst @@ -32,7 +32,7 @@ DOS attack. There is an out-of-bounds read in ``SgiRleDecode.c`` since Pillow 4.3.0. -Other Changes +Other changes ============= A crash with the feature flags for libimagequant, libjpeg-turbo, WebP and XCB on diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index 50fe9aa1988..a59560695eb 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -74,7 +74,7 @@ Tk/Tcl 8.4 Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), when Tk/Tcl 8.5 will be the minimum supported. -API Changes +API changes =========== Image.alpha_composite: dest @@ -107,7 +107,7 @@ removed. Instead, ``Image.getmodebase()``, ``Image.getmodetype()``, ``Image.getmodebandnames()``, ``Image.getmodebands()`` or ``ImageMode.getmode()`` can be used. -API Additions +API additions ============= getxmp() for JPEG images @@ -177,7 +177,7 @@ be specified through a keyword argument:: im.save("out.tif", icc_profile=...) -Other Changes +Other changes ============= GIF writer uses LZW encoding diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst index 4ef914f6471..c4624085400 100644 --- a/docs/releasenotes/8.3.0.rst +++ b/docs/releasenotes/8.3.0.rst @@ -33,7 +33,7 @@ dictionary. The ``convert_dict_qtables`` method no longer performs any operations on the data given to it, has been deprecated and will be removed in Pillow 10.0.0 (2023-07-01). -API Changes +API changes =========== Changed WebP default "method" value when saving @@ -73,7 +73,7 @@ through :py:meth:`~PIL.Image.Image.getexif`. This also provides access to the GP EXIF IFDs, through ``im.getexif().get_ifd(0x8825)`` and ``im.getexif().get_ifd(0x8769)`` respectively. -API Additions +API additions ============= ImageOps.contain @@ -100,7 +100,7 @@ format, through the new ``bitmap_format`` argument:: im.save("out.ico", bitmap_format="bmp") -Other Changes +Other changes ============= Added DDS BC5 reading and uncompressed saving diff --git a/docs/releasenotes/8.3.2.rst b/docs/releasenotes/8.3.2.rst index 34ba703f70a..e26a6ceda4c 100644 --- a/docs/releasenotes/8.3.2.rst +++ b/docs/releasenotes/8.3.2.rst @@ -20,7 +20,7 @@ bytes off the end of the allocated buffer from the heap. Present since Pillow 7. This bug was found by Google's `OSS-Fuzz`_ `CIFuzz`_ runs. -Other Changes +Other changes ============= Python 3.10 wheels diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst index bdc8e802082..3bdf77d564d 100644 --- a/docs/releasenotes/8.4.0.rst +++ b/docs/releasenotes/8.4.0.rst @@ -13,7 +13,7 @@ Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular length default, and the size parameter could be used to override that. Pillow 8.3.0 removed the default required length, also removing the need for the size parameter. -API Additions +API additions ============= Added "transparency" argument for loading EPS images @@ -33,7 +33,7 @@ Added WalImageFile class :py:class:`PIL.Image.Image` instance. It now returns a dedicated :py:class:`PIL.WalImageFile.WalImageFile` class. -Other Changes +Other changes ============= Speed improvement when rotating square images diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index fee66b6d0b5..660e5514cf8 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -59,7 +59,7 @@ initializing ``ImagePath.Path``. .. _OSS-Fuzz: https://github.com/google/oss-fuzz -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 3.6 @@ -102,7 +102,7 @@ ImageFile.raise_ioerror has been removed. Use ``ImageFile.raise_oserror`` instead. -API Changes +API changes =========== Added line width parameter to ImageDraw polygon @@ -111,7 +111,7 @@ Added line width parameter to ImageDraw polygon An optional line ``width`` parameter has been added to ``ImageDraw.Draw.polygon``. -API Additions +API additions ============= ImageShow.XDGViewer @@ -132,7 +132,7 @@ Support has been added for the "title" argument in argument will also now be supported, e.g. ``im.show(title="My Image")`` and ``ImageShow.show(im, title="My Image")``. -Other Changes +Other changes ============= Convert subsequent GIF frames to RGB or RGBA diff --git a/docs/releasenotes/9.0.1.rst b/docs/releasenotes/9.0.1.rst index f65e3bcc2ec..5326afe782c 100644 --- a/docs/releasenotes/9.0.1.rst +++ b/docs/releasenotes/9.0.1.rst @@ -21,7 +21,7 @@ While Pillow 9.0 restricted top-level builtins available to :py:meth:`!PIL.ImageMath.eval`, it did not prevent builtins available to lambda expressions. These are now also restricted. -Other Changes +Other changes ============= Pillow 9.0 added support for ``xdg-open`` as an image viewer, but there have been diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 5b83d1e9c56..72749ce8c54 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -94,7 +94,7 @@ The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be re Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. -API Changes +API changes =========== Raise an error when performing a negative crop @@ -137,7 +137,7 @@ On macOS, the last argument may need to be wrapped in quotes, e.g. Therefore ``requirements.txt`` has been removed along with the ``make install-req`` command for installing its contents. -API Additions +API additions ============= Added get_photoshop_blocks() to parse Photoshop TIFF tag @@ -193,7 +193,7 @@ palette. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY -Other Changes +Other changes ============= musllinux wheels diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 6e064734391..a3c9800b683 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -126,7 +126,7 @@ Use instead:: draw = ImageDraw.Draw(im) draw.text((100 / 2, 100 / 2), "Hello world", font=font, anchor="mm") -API Additions +API additions ============= Image.apply_transparency @@ -137,7 +137,7 @@ with "transparency" in ``im.info``, and apply the transparency to the palette in The image's palette mode will become "RGBA", and "transparency" will be removed from ``im.info``. -Other Changes +Other changes ============= Using gnome-screenshot on Linux diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index e5987ce086c..bb1e731fd75 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -28,7 +28,7 @@ This was introduced in Pillow 9.2.0, found with `OSS-Fuzz`_ and fixed by limitin ``SAMPLESPERPIXEL`` to the number of planes that we can decode. -API Additions +API additions ============= Allow default ImageDraw font to be set @@ -65,7 +65,7 @@ The data from :py:data:`~PIL.ExifTags.TAGS` and :py:data:`~PIL.ExifTags.GPS`. -Other Changes +Other changes ============= Python 3.11 wheels diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 37f26a22c05..3b202157dc0 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -20,7 +20,7 @@ Pillow attempted to dereference a null pointer in ``ImageFont``, leading to a crash. An error is now raised instead. This has been present since Pillow 8.0.0. -API Additions +API additions ============= Added start position for getmask and getmask2 @@ -88,7 +88,7 @@ When saving a JPEG image, a comment can now be written from im.save(out, comment="Test comment") -Other Changes +Other changes ============= Added support for DDS L and LA images diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index 501479bb6df..6bf2079c812 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -37,7 +37,7 @@ be removed in Pillow 11 (2024-10-15). This class was only made as a helper to be used internally, so there is no replacement. If you need this functionality though, it is a very short class that can easily be recreated in your own code. -API Additions +API additions ============= QOI file format @@ -71,7 +71,7 @@ If OpenJPEG 2.4.0 or later is available and the ``plt`` keyword argument is present and true when saving JPEG2000 images, tell the encoder to generate PLT markers. -Other Changes +Other changes ============= Added support for saving PDFs in RGBA mode diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index a116ef056a7..5d7b21d593d 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -1,4 +1,4 @@ -Release Notes +Release notes ============= Pillow is released quarterly on January 2nd, April 1st, July 1st and October 15th. diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst index cfc7221a3cc..a453d2a436d 100644 --- a/docs/releasenotes/template.rst +++ b/docs/releasenotes/template.rst @@ -14,7 +14,7 @@ TODO TODO -Backwards Incompatible Changes +Backwards incompatible changes ============================== TODO @@ -28,7 +28,7 @@ TODO TODO -API Changes +API changes =========== TODO @@ -36,7 +36,7 @@ TODO TODO -API Additions +API additions ============= TODO @@ -44,7 +44,7 @@ TODO TODO -Other Changes +Other changes ============= TODO From 58e48745cc7b6c6f7dd26a50fe68d1a82ea51562 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 21 Apr 2025 19:14:08 +1000 Subject: [PATCH 1662/2374] Add list of third-party plugins (#8910) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/handbook/appendices.rst | 1 + docs/handbook/third-party-plugins.rst | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 docs/handbook/third-party-plugins.rst diff --git a/docs/handbook/appendices.rst b/docs/handbook/appendices.rst index 347a8848b37..c20d8bc8bb2 100644 --- a/docs/handbook/appendices.rst +++ b/docs/handbook/appendices.rst @@ -8,4 +8,5 @@ Appendices image-file-formats text-anchors + third-party-plugins writing-your-own-image-plugin diff --git a/docs/handbook/third-party-plugins.rst b/docs/handbook/third-party-plugins.rst new file mode 100644 index 00000000000..a189a5773c7 --- /dev/null +++ b/docs/handbook/third-party-plugins.rst @@ -0,0 +1,18 @@ +Third-party plugins +=================== + +Pillow uses a plugin model which allows users to add their own +decoders and encoders to the library, without any changes to the library +itself. + +Here is a list of PyPI projects that offer additional plugins: + +* :pypi:`DjvuRleImagePlugin`: Plugin for the DjVu RLE image format as defined in the DjVuLibre docs. +* :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library. +* :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL. +* :pypi:`pillow-heif`: Python bindings to libheif for working with HEIF images. +* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implemetation. Python bindings implemented using pybind11. +* :pypi:`pillow-jxl-plugin`: Plugin for JPEG-XL, using Rust for bindings. +* :pypi:`pillow-mbm`: Adds support for KSP's proprietary MBM texture format. +* :pypi:`pillow-svg`: Implements basic SVG read support. Supports basic paths, shapes, and text. +* :pypi:`raw-pillow-opener`: Simple camera raw opener, based on the rawpy library. From 6bf791a3e7b2490bcb34ae9eb44419ee65c3caee Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Apr 2025 10:27:49 +0100 Subject: [PATCH 1663/2374] Use a named tuple for the packed parameters --- Tests/test_pyarrow.py | 56 ++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index bcdd7ddc9d0..822cd18ac29 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any # undone +from typing import Any, NamedTuple import pytest @@ -151,29 +151,37 @@ def test_lifetime2() -> None: assert isinstance(px[0, 0], int) -UINT_ARR = ( - fl_uint8_4_type, - [1,2,3,4], - 1 +class DataShape(NamedTuple): + dtype: Any + elt: Any + elts_per_pixel: int + + +UINT_ARR = DataShape( + dtype=fl_uint8_4_type, + elt=[1, 2, 3, 4], # array of 4 uint 8 per pixel + elts_per_pixel=1, # only one array per pixel ) -UINT = ( - pyarrow.uint8(), - 3, - 4 + +UINT = DataShape( + dtype=pyarrow.uint8(), + elt=3, # one uint8, + elts_per_pixel=4, # but repeated 4x per pixel ) -INT32 = ( - pyarrow.uint32(), - 0xabcdef45, - 1 + +UINT32 = DataShape( + dtype=pyarrow.uint32(), + elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000 + elts_per_pixel=1, # one per pixel ) @pytest.mark.parametrize( "mode, data_tp, mask", ( - ("L", (pyarrow.uint8(), 3, 1), None), - ("I", (pyarrow.int32(), 1 << 24, 1), None), - ("F", (pyarrow.float32(), 3.14159, 1), None), + ("L", DataShape(pyarrow.uint8(), 3, 1), None), + ("I", DataShape(pyarrow.int32(), 1 << 24, 1), None), + ("F", DataShape(pyarrow.float32(), 3.14159, 1), None), ("LA", UINT_ARR, [0, 3]), ("LA", UINT, [0, 3]), ("RGB", UINT_ARR, [0, 1, 2]), @@ -188,7 +196,7 @@ def test_lifetime2() -> None: ("HSV", UINT, [0, 1, 2]), ), ) -def test_fromarray(mode: str, data_tp: tuple, mask: list[int] | None) -> None: +def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: (dtype, elt, elts_per_pixel) = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] @@ -201,15 +209,15 @@ def test_fromarray(mode: str, data_tp: tuple, mask: list[int] | None) -> None: @pytest.mark.parametrize( "mode, data_tp, mask", ( - ("LA", INT32, [0, 3]), - ("RGB", INT32, [0, 1, 2]), - ("RGBA", INT32, None), - ("CMYK", INT32, None), - ("YCbCr", INT32, [0, 1, 2]), - ("HSV", INT32, [0, 1, 2]), + ("LA", UINT32, [0, 3]), + ("RGB", UINT32, [0, 1, 2]), + ("RGBA", UINT32, None), + ("CMYK", UINT32, None), + ("YCbCr", UINT32, [0, 1, 2]), + ("HSV", UINT32, [0, 1, 2]), ), ) -def test_from_int32array(mode: str, data_tp: tuple, mask: list[int] | None) -> None: +def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: (dtype, elt, elts_per_pixel) = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] From ce204f47f45f2ecdc831faac9c1b7ad8192a9fc7 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Apr 2025 10:37:32 +0100 Subject: [PATCH 1664/2374] lint --- src/libImaging/Storage.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 2c57165c1e7..1a9171a0cbb 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -737,7 +737,7 @@ ImagingNewArrow( return im; } } - // Stored as [[r,g,b,a],....] + // Stored as [[r,g,b,a],...] if (strcmp(schema->format, "+w:4") == 0 // 4 up array && im->pixelsize == 4 // storage as 32 bpc && schema->n_children > 0 // make sure schema is well formed. @@ -753,7 +753,7 @@ ImagingNewArrow( return im; } } - // Stored as [r,g,b,a,r,g,b,a....] + // Stored as [r,g,b,a,r,g,b,a,...] if (strcmp(schema->format, "C") == 0 // uint8 && im->pixelsize == 4 // storage as 32 bpc && schema->n_children == 0 // make sure schema is well formed. From bc4b664b7094a311eb516e7eab1b88acf7496b67 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Apr 2025 10:46:45 +0100 Subject: [PATCH 1665/2374] Add integer range tests --- Tests/test_pyarrow.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index 822cd18ac29..6eedcafe72a 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -153,7 +153,10 @@ def test_lifetime2() -> None: class DataShape(NamedTuple): dtype: Any - elt: Any + elt: Any # Strictly speaking, this should be a pixel or pixel component, + # so list[uint8][4], float, int, uint32, uint8, etc. + # But more correctly, it should be exactly the dtype from the + # line above. elts_per_pixel: int @@ -175,6 +178,12 @@ class DataShape(NamedTuple): elts_per_pixel=1, # one per pixel ) +INT32 = DataShape( + dtype=pyarrow.uint32(), + elt=0x12CDEF45, # one packed int + elts_per_pixel=1, # one per pixel +) + @pytest.mark.parametrize( "mode, data_tp, mask", @@ -215,6 +224,12 @@ def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> Non ("CMYK", UINT32, None), ("YCbCr", UINT32, [0, 1, 2]), ("HSV", UINT32, [0, 1, 2]), + ("LA", INT32, [0, 3]), + ("RGB", INT32, [0, 1, 2]), + ("RGBA", INT32, None), + ("CMYK", INT32, None), + ("YCbCr", INT32, [0, 1, 2]), + ("HSV", INT32, [0, 1, 2]), ), ) def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: From 45e24e429f7d443000a6955d228fa00055104414 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Apr 2025 10:54:00 +0100 Subject: [PATCH 1666/2374] Rearrance so black doesn't screw up the formatting --- Tests/test_pyarrow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index 6eedcafe72a..e7fce1e3399 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -153,10 +153,10 @@ def test_lifetime2() -> None: class DataShape(NamedTuple): dtype: Any - elt: Any # Strictly speaking, this should be a pixel or pixel component, - # so list[uint8][4], float, int, uint32, uint8, etc. - # But more correctly, it should be exactly the dtype from the - # line above. + # Strictly speaking, elt should be a pixel or pixel component, so + # list[uint8][4], float, int, uint32, uint8, etc. But more + # correctly, it should be exactly the dtype from the line above. + elt: Any elts_per_pixel: int From 7a48a9fae083697bb30a6bec42c0c73399f3979a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 23 Apr 2025 20:34:53 +1000 Subject: [PATCH 1667/2374] Do not load image more than once --- src/PIL/IptcImagePlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 60ab7c83f37..4336b815450 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -179,6 +179,7 @@ def load(self) -> Image.core.PixelAccess | None: with Image.open(o) as _im: _im.load() self.im = _im.im + self.tile = [] return None From 1e365d8c7282f77f5959ba9b772c5fd1b9aa4315 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 23 Apr 2025 21:10:54 +1000 Subject: [PATCH 1668/2374] Return PixelAccess on first load --- Tests/test_file_ico.py | 1 + Tests/test_file_iptc.py | 3 +++ src/PIL/IcoImagePlugin.py | 2 +- src/PIL/IptcImagePlugin.py | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 5d2ace35e28..0eef7c07a83 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -99,6 +99,7 @@ def test_getpixel(tmp_path: Path) -> None: reloaded.load() reloaded.size = (32, 32) + assert reloaded.load() is not None assert reloaded.getpixel((0, 0)) == (18, 20, 62) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index c6c0c1aab9d..424820ce47d 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -23,6 +23,9 @@ def test_open() -> None: assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] assert_image_equal(im, expected) + with Image.open(f) as im: + assert im.load() is not None + def test_getiptcinfo_jpg_none() -> None: # Arrange diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 55c57f203ab..bd35ac890e6 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -362,7 +362,7 @@ def load(self) -> Image.core.PixelAccess | None: self.info["sizes"] = set(sizes) self.size = im.size - return None + return Image.Image.load(self) def load_seek(self, pos: int) -> None: # Flag the ImageFile.Parser so that it diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 4336b815450..637f67810f5 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -180,7 +180,7 @@ def load(self) -> Image.core.PixelAccess | None: _im.load() self.im = _im.im self.tile = [] - return None + return Image.Image.load(self) Image.register_open(IptcImageFile.format, IptcImageFile) From d8afcb762fdc2a8d8e06deedd903993ab26805d8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 23 Apr 2025 23:09:08 +1000 Subject: [PATCH 1669/2374] Do not update palette for L mode frame --- .../no_palette_with_transparency_after_rgb.gif | Bin 0 -> 16290 bytes Tests/test_file_gif.py | 12 ++++++++++++ src/PIL/GifImagePlugin.py | 9 ++++++--- 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 Tests/images/no_palette_with_transparency_after_rgb.gif diff --git a/Tests/images/no_palette_with_transparency_after_rgb.gif b/Tests/images/no_palette_with_transparency_after_rgb.gif new file mode 100644 index 0000000000000000000000000000000000000000..41357c1471db6ffeff5ac16d717b97e73f24094d GIT binary patch literal 16290 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 b#g&ycnzVQest<+eu%s{I~DmZd$pp4lG=e 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 None: assert im.mode == "RGB" +def test_l_mode_transparency_after_rgb() -> None: + with Image.open("Tests/images/no_palette_with_transparency_after_rgb.gif") as im: + expected = im.convert("RGB") + d = ImageDraw.Draw(expected) + d.rectangle([(0, 0), (64, 128)], fill="#000") + + im.seek(1) + assert im.mode == "RGB" + + assert_image_equal(im, expected) + + def test_palette_not_needed_for_second_frame() -> None: with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im: im.seek(1) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 4392c4cb909..2102614db9f 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -477,8 +477,11 @@ def load_end(self) -> None: self._prev_im = expanded_im assert self._prev_im is not None if self._frame_transparency is not None: - self.im.putpalettealpha(self._frame_transparency, 0) - frame_im = self.im.convert("RGBA") + if self.mode == "L": + frame_im = self.im.convert_transparent("LA", self._frame_transparency) + else: + self.im.putpalettealpha(self._frame_transparency, 0) + frame_im = self.im.convert("RGBA") else: frame_im = self.im.convert("RGB") @@ -487,7 +490,7 @@ def load_end(self) -> None: self.im = self._prev_im self._mode = self.im.mode - if frame_im.mode == "RGBA": + if frame_im.mode in ("LA", "RGBA"): self.im.paste(frame_im, self.dispose_extent, frame_im) else: self.im.paste(frame_im, self.dispose_extent) From 3bd55822cd81930231db9bcb3bdc53deeeb56736 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 24 Apr 2025 13:26:58 +1000 Subject: [PATCH 1670/2374] Handle IPTC TIFF tags with incorrect type --- Tests/test_file_iptc.py | 16 +++++++++++----- src/PIL/IptcImagePlugin.py | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index c6c0c1aab9d..07335c269a9 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -5,7 +5,7 @@ import pytest -from PIL import Image, IptcImagePlugin +from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags from .helper import assert_image_equal, hopper @@ -75,13 +75,19 @@ def test_getiptcinfo_zero_padding() -> None: def test_getiptcinfo_tiff() -> None: - # Arrange + expected = {(1, 90): b"\x1b%G", (2, 0): b"\xcf\xc0"} + with Image.open("Tests/images/hopper.Lab.tif") as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) - # Assert - assert iptc == {(1, 90): b"\x1b%G", (2, 0): b"\xcf\xc0"} + assert iptc == expected + + # Test with LONG tag type + with Image.open("Tests/images/hopper.Lab.tif") as im: + im.tag_v2.tagtype[TiffImagePlugin.IPTC_NAA_CHUNK] = TiffTags.LONG + iptc = IptcImagePlugin.getiptcinfo(im) + + assert iptc == expected def test_getiptcinfo_tiff_none() -> None: diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 60ab7c83f37..9df498e26e3 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -219,7 +219,7 @@ def getiptcinfo( # get raw data from the IPTC/NAA tag (PhotoShop tags the data # as 4-byte integers, so we cannot use the get method...) try: - data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK] + data = im.tag_v2._tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] except KeyError: pass From 225182414c3c5cccc2fa42b3dd47147ef06790f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Apr 2025 17:14:13 +1000 Subject: [PATCH 1671/2374] libavif below 1.0 is not supported --- src/PIL/AvifImagePlugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index b2c5ab15d7e..366e0c864bf 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -16,7 +16,6 @@ # Decoder options as module globals, until there is a way to pass parameters # to Image.open (see https://github.com/python-pillow/Pillow/issues/569) DECODE_CODEC_CHOICE = "auto" -# Decoding is only affected by this for libavif **0.8.4** or greater. DEFAULT_MAX_THREADS = 0 From 4c2227758ec7bda10213220dc022e0e105533391 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 07:09:53 -0400 Subject: [PATCH 1672/2374] Add template for quarterly release issue --- .github/ISSUE_TEMPLATE/RELEASE.md | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/RELEASE.md diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md new file mode 100644 index 00000000000..db4c94a0975 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -0,0 +1,45 @@ +--- +name: Release +about: Schedule a release +--- + +## Main Release + +Released quarterly on January 2nd, April 1st, July 1st and October 15th. + +* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 +* [ ] Develop and prepare release in `main` branch. + * [ ] Add release notes for 11.2.1 https://github.com/python-pillow/Pillow/pull/8885 +* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch. +* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them. +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` +* [ ] Run pre-release check via `make release-test` in a freshly cloned repo. +* [ ] Create branch and tag for release e.g.: + ```bash + git branch [[MAJOR.MINOR.PATCH]] + git tag [[MAJOR.MINOR.PATCH]] + git push --tags + ``` +* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) has passed, including the "Upload release to PyPI" job. This will have been triggered by the new tag. +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases). +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: + ```bash + git push --all + ``` + +## Publicize Release + +* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321 + +## Documentation + +* [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes + +## Docker Images + +* [ ] Update Pillow in the Docker Images repository + ```bash + git clone https://github.com/python-pillow/docker-images + cd docker-images + ./update-pillow-tag.sh [[release tag]] + ``` From da9d5522f7c7cd96d99a25a580ac6488f63bedaf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 27 Apr 2025 21:35:08 +1000 Subject: [PATCH 1673/2374] Update dependency cibuildwheel to v2.23.3 (#8931) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index db5f89c9a04..0e314b8bf59 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.23.2 +cibuildwheel==2.23.3 From 8ab3bc469ec4ebcc7e5bc5a848fd59bd4a9a8221 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 08:21:48 -0400 Subject: [PATCH 1674/2374] Update .github/ISSUE_TEMPLATE/RELEASE.md Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md index db4c94a0975..c5bae8b2a68 100644 --- a/.github/ISSUE_TEMPLATE/RELEASE.md +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -9,7 +9,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 * [ ] Develop and prepare release in `main` branch. - * [ ] Add release notes for 11.2.1 https://github.com/python-pillow/Pillow/pull/8885 + * [ ] Add release notes e.g. https://github.com/python-pillow/Pillow/pull/8885 * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch. * [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them. * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` From 0205fb4fa2e9905dcec12715d60b5f4a75e30266 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 08:21:57 -0400 Subject: [PATCH 1675/2374] Update .github/ISSUE_TEMPLATE/RELEASE.md Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/RELEASE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md index c5bae8b2a68..72499e01dad 100644 --- a/.github/ISSUE_TEMPLATE/RELEASE.md +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -16,8 +16,8 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Create branch and tag for release e.g.: ```bash - git branch [[MAJOR.MINOR.PATCH]] - git tag [[MAJOR.MINOR.PATCH]] + git branch [[MAJOR.MINOR]].0 + git tag [[MAJOR.MINOR]].0 git push --tags ``` * [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) has passed, including the "Upload release to PyPI" job. This will have been triggered by the new tag. From 6f672191ad83b522c47e1facdeb12889f91a5824 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 27 Apr 2025 22:30:35 +1000 Subject: [PATCH 1676/2374] Branch uses .x --- .github/ISSUE_TEMPLATE/RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md index 72499e01dad..5dd070886d9 100644 --- a/.github/ISSUE_TEMPLATE/RELEASE.md +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -16,7 +16,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Create branch and tag for release e.g.: ```bash - git branch [[MAJOR.MINOR]].0 + git branch [[MAJOR.MINOR]].x git tag [[MAJOR.MINOR]].0 git push --tags ``` From 6881863eab8106a6a0e3cb788f11c7d0d71cf124 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 15:37:42 -0400 Subject: [PATCH 1677/2374] Update .github/ISSUE_TEMPLATE/RELEASE.md Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/RELEASE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md index 5dd070886d9..97701f30e66 100644 --- a/.github/ISSUE_TEMPLATE/RELEASE.md +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -1,6 +1,7 @@ --- -name: Release -about: Schedule a release +name: "Maintainers only: Release" +about: For maintainers to schedule a quarterly release +labels: Release --- ## Main Release From fcaffa22293c41851e9e28b3229b981398deacda Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 15:37:50 -0400 Subject: [PATCH 1678/2374] Update .github/ISSUE_TEMPLATE/RELEASE.md Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md index 97701f30e66..d7b246378b1 100644 --- a/.github/ISSUE_TEMPLATE/RELEASE.md +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -4,7 +4,7 @@ about: For maintainers to schedule a quarterly release labels: Release --- -## Main Release +## Main release Released quarterly on January 2nd, April 1st, July 1st and October 15th. From 1eba198b62d1444e72f598db805121022a19ab04 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 15:37:56 -0400 Subject: [PATCH 1679/2374] Update .github/ISSUE_TEMPLATE/RELEASE.md Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md index d7b246378b1..16bf30fdcf9 100644 --- a/.github/ISSUE_TEMPLATE/RELEASE.md +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -28,7 +28,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. git push --all ``` -## Publicize Release +## Publicize release * [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321 From e6ff42303b54d16e213b2152984a39f07742fcaf Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 15:38:02 -0400 Subject: [PATCH 1680/2374] Update .github/ISSUE_TEMPLATE/RELEASE.md Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md index 16bf30fdcf9..20d1ac79b2a 100644 --- a/.github/ISSUE_TEMPLATE/RELEASE.md +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -36,7 +36,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes -## Docker Images +## Docker images * [ ] Update Pillow in the Docker Images repository ```bash From e140027262b95d26080b5d58e095d2e294609e69 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 15:45:40 -0400 Subject: [PATCH 1681/2374] Move checklist to issue template --- RELEASING.md | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 932beb2c26e..b626d7d232f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -7,27 +7,8 @@ information about how the version numbers line up with releases. Released quarterly on January 2nd, April 1st, July 1st and October 15th. -* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 -* [ ] Develop and prepare release in `main` branch. -* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch. -* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them. -* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` -* [ ] Run pre-release check via `make release-test` in a freshly cloned repo. -* [ ] Create branch and tag for release e.g.: - ```bash - git branch 5.2.x - git tag 5.2.0 - git push --tags - ``` -* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) - has passed, including the "Upload release to PyPI" job. This will have been triggered - by the new tag. -* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases). -* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), - increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: - ```bash - git push --all - ``` +* [ ] Create a new issue and select the "Release" template. + ## Point Release Released as needed for security, installation or critical bug fixes. From f1d5cdaa07fbf4377d3cf4c68377e64998619b60 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Apr 2025 06:17:47 +1000 Subject: [PATCH 1682/2374] Use sentence case --- README.md | 4 ++-- RELEASING.md | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1cae558ada3..365d356a00c 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ This library provides extensive file format support, an efficient internal repre The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool. -## More Information +## More information - [Documentation](https://pillow.readthedocs.io/) - [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html) @@ -107,6 +107,6 @@ The core image library is designed for fast access to data stored in a few basic - [Changelog](https://github.com/python-pillow/Pillow/releases) - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) -## Report a Vulnerability +## Report a vulnerability To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security). diff --git a/RELEASING.md b/RELEASING.md index b626d7d232f..c160e96f513 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,15 +1,15 @@ -# Release Checklist +# Release checklist See https://pillow.readthedocs.io/en/stable/releasenotes/versioning.html for information about how the version numbers line up with releases. -## Main Release +## Main release Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Create a new issue and select the "Release" template. -## Point Release +## Point release Released as needed for security, installation or critical bug fixes. @@ -39,7 +39,7 @@ Released as needed for security, installation or critical bug fixes. git push ``` -## Embargoed Release +## Embargoed release Released as needed privately to individual vendors for critical security-related bug fixes. @@ -63,7 +63,7 @@ Released as needed privately to individual vendors for critical security-related git push origin 2.5.x ``` -## Publicize Release +## Publicize release * [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321 @@ -71,7 +71,7 @@ Released as needed privately to individual vendors for critical security-related * [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes -## Docker Images +## Docker images * [ ] Update Pillow in the Docker Images repository ```bash From dbe538a1307dc14c3ecb685819f28f22c02060ab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Apr 2025 06:19:18 +1000 Subject: [PATCH 1683/2374] Updated template name --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index c160e96f513..3c6188c8261 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -7,7 +7,7 @@ information about how the version numbers line up with releases. Released quarterly on January 2nd, April 1st, July 1st and October 15th. -* [ ] Create a new issue and select the "Release" template. +* [ ] Create a new issue and select the "Maintainers only: Release" template. ## Point release From 47bebfc801c3905979715d0d22a9560c395581e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Apr 2025 14:57:10 +1000 Subject: [PATCH 1684/2374] Allow loading state from Pillow < 11.2.1 --- Tests/test_pickle.py | 10 ++++++++++ src/PIL/ImageFile.py | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 1c48cb743e0..54cef00ad38 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -162,3 +162,13 @@ def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: # Assert helper_assert_pickled_font_images(font, unpickled_font) + + +def test_load_earlier_data() -> None: + im = pickle.loads( + b"\x80\x04\x95@\x00\x00\x00\x00\x00\x00\x00\x8c\x12PIL.PngImagePlugin" + b"\x94\x8c\x0cPngImageFile\x94\x93\x94)\x81\x94]\x94(}\x94\x8c\x01L\x94K\x01" + b"K\x01\x86\x94NC\x01\x00\x94eb." + ) + assert im.mode == "L" + assert im.size == (1, 1) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index bcb7d462eba..bf556a2c690 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -257,7 +257,8 @@ def __getstate__(self) -> list[Any]: def __setstate__(self, state: list[Any]) -> None: self.tile = [] - self.filename = state[5] + if len(state) > 5: + self.filename = state[5] super().__setstate__(state) def verify(self) -> None: From 07df26aa5d2cf0937737c787bc88ff05454e53d2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:37:45 +0300 Subject: [PATCH 1685/2374] Refactor docs `Makefile` (#8933) Co-authored-by: Andrew Murray --- docs/Makefile | 42 ++++++++++++++++++------------------------ docs/make.bat | 2 -- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 1e6c06ede80..8c10192940d 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,17 +3,23 @@ # You can set these variables from the command line. PYTHON = python3 -SPHINXOPTS = SPHINXBUILD = $(PYTHON) -m sphinx.cmd.build -PAPER = +SPHINXOPTS = --fail-on-warning BUILDDIR = _build +BUILDER = html +JOBS = auto +PAPER = # Internal variables. PAPEROPT_a4 = --define latex_paper_size=a4 PAPEROPT_letter = --define latex_paper_size=letter -ALLSPHINXOPTS = --doctree-dir $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +ALLSPHINXOPTS = --builder $(BUILDER) \ + --doctree-dir $(BUILDDIR)/doctrees \ + --jobs $(JOBS) \ + $(PAPEROPT_$(PAPER)) \ + $(SPHINXOPTS) \ + . $(BUILDDIR)/$(BUILDER) .PHONY: help help: @@ -36,31 +42,19 @@ install-sphinx: .PHONY: html html: $(MAKE) install-sphinx - $(SPHINXBUILD) --builder html --fail-on-warning --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + $(SPHINXBUILD) $(ALLSPHINXOPTS) .PHONY: dirhtml -dirhtml: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." +dirhtml: BUILDER = dirhtml +dirhtml: html .PHONY: singlehtml -singlehtml: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." +singlehtml: BUILDER = singlehtml +singlehtml: html .PHONY: linkcheck -linkcheck: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." +linkcheck: BUILDER = linkcheck +linkcheck: html .PHONY: htmlview htmlview: html diff --git a/docs/make.bat b/docs/make.bat index 4126f786b8d..9d15537fb9e 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -7,10 +7,8 @@ if "%SPHINXBUILD%" == "" ( ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help From 2245fd09de9c358bce2cb7f6f49b3f9710229489 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 30 Apr 2025 07:54:07 +1000 Subject: [PATCH 1686/2374] Updated Ghostscript to 10.5.1 --- .github/workflows/test-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index bf8ec2f2cdf..abbfd95c891 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -98,8 +98,8 @@ jobs: choco install nasm --no-progress echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - choco install ghostscript --version=10.5.0 --no-progress - echo "C:\Program Files\gs\gs10.05.0\bin" >> $env:GITHUB_PATH + choco install ghostscript --version=10.5.1 --no-progress + echo "C:\Program Files\gs\gs10.05.1\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images From 0e292a80c8bed0f98cd56141431632a9433c5274 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 May 2025 00:52:35 +1000 Subject: [PATCH 1687/2374] Restore original encoderinfo after saving --- Tests/test_file_mpo.py | 3 +++ src/PIL/Image.py | 8 +++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 73838ef4484..71642253720 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -315,6 +315,9 @@ def test_save_xmp() -> None: im2.encoderinfo = {"xmp": b"Second frame"} im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2]) + # Test that encoderinfo is unchanged + assert im2.encoderinfo == {"xmp": b"Second frame"} + assert im_reloaded.info["xmp"] == b"First frame" im_reloaded.seek(1) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ded40bc5ddc..02627c37af4 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2551,7 +2551,8 @@ def save( self.load() save_all = params.pop("save_all", None) - self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} + encoderinfo = getattr(self, "encoderinfo", {}) + self.encoderinfo = {**encoderinfo, **params} self.encoderconfig: tuple[Any, ...] = () if format.upper() not in SAVE: @@ -2589,10 +2590,7 @@ def save( pass raise finally: - try: - del self.encoderinfo - except AttributeError: - pass + self.encoderinfo = encoderinfo if open_fp: fp.close() From 4d56b90f38eda564ce8913bdc9b5222c3407652f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 May 2025 07:12:20 +1000 Subject: [PATCH 1688/2374] Updated docstring --- src/PIL/DdsImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 26307817c91..f9ade18f9a1 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -1,5 +1,5 @@ """ -A Pillow loader for .dds files (S3TC-compressed aka DXTC) +A Pillow plugin for .dds files (S3TC-compressed aka DXTC) Jerome Leclanche Documentation: From 349cc44fd47427a7cefbf5444dcd4010c8723360 Mon Sep 17 00:00:00 2001 From: Stefan <96178532+stefan6419846@users.noreply.github.com> Date: Wed, 7 May 2025 17:21:22 +0200 Subject: [PATCH 1689/2374] Add Apache-2.0 notice to IcoImagePlugin Closes #8946. --- src/PIL/IcoImagePlugin.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 55c57f203ab..8ddb94b3710 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -17,6 +17,20 @@ # . # https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki # +# Copyright 2008 Bryan Davis +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # Icon format references: # * https://en.wikipedia.org/wiki/ICO_(file_format) # * https://msdn.microsoft.com/en-us/library/ms997538.aspx From d02f7868732cdacc227dd794f6ff84bd6f1858c9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 19:16:40 +1000 Subject: [PATCH 1690/2374] [pre-commit.ci] pre-commit autoupdate (#8944) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/zizmor.yml | 7 +++++++ .pre-commit-config.yaml | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .github/zizmor.yml diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 00000000000..5bdc48c301b --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,7 @@ +# Configuration for the zizmor static analysis tool, run via pre-commit in CI +# https://woodruffw.github.io/zizmor/configuration/ +rules: + unpinned-uses: + config: + policies: + "*": ref-pin diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 140ce33bead..e15e6f6390a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.4 + rev: v0.11.8 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.0 + rev: v20.1.3 hooks: - id: clang-format types: [c] @@ -51,14 +51,14 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.32.1 + rev: 0.33.0 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.5.2 + rev: v1.6.0 hooks: - id: zizmor From c7193f74fc5ce1a0fe1742a0845165024be45ef5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 May 2025 20:10:34 +1000 Subject: [PATCH 1691/2374] Updated error message --- Tests/test_image_resample.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index ce6209c0da4..73b25ed51b2 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -462,7 +462,7 @@ def test_wrong_arguments(self, resample: Image.Resampling) -> None: im.resize((32, 32), resample, (20, 20, 20, 100)) im.resize((32, 32), resample, (20, 20, 100, 20)) - with pytest.raises(TypeError, match="must be sequence of length 4"): + with pytest.raises(TypeError, match="must be (sequence|tuple) of length 4"): im.resize((32, 32), resample, (im.width, im.height)) # type: ignore[arg-type] with pytest.raises(ValueError, match="can't be negative"): From 71a916ad53502ed8cb8ea71c40081c169c3eae0f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 May 2025 22:10:22 +1000 Subject: [PATCH 1692/2374] Do not install PyQt6 on Python 3.14 --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index bf8ec2f2cdf..12f06ee0391 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -84,7 +84,7 @@ jobs: python3 -m pip install --upgrade pip - name: Install CPython dependencies - if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'" + if: "!contains(matrix.python-version, 'pypy') && !contains(matrix.python-version, '3.14') && matrix.architecture != 'x86'" run: | python3 -m pip install PyQt6 From 215069af5ddec6f4d3b92b8bc7554a10e2efb669 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 May 2025 22:13:13 +1000 Subject: [PATCH 1693/2374] Added support for Python 3.14 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5ecd6b8160a..5d41e27d981 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ def get_version() -> str: ZLIB_ROOT = None FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ -if sys.platform == "win32" and sys.version_info >= (3, 14): +if sys.platform == "win32" and sys.version_info >= (3, 15): import atexit atexit.register( From 78887f6114e68d4208a6d5e8f3d5134a6da6831a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 May 2025 23:52:18 +1000 Subject: [PATCH 1694/2374] Corrected comment --- Tests/test_pyarrow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index e7fce1e3399..c5872231b3b 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -162,7 +162,7 @@ class DataShape(NamedTuple): UINT_ARR = DataShape( dtype=fl_uint8_4_type, - elt=[1, 2, 3, 4], # array of 4 uint 8 per pixel + elt=[1, 2, 3, 4], # array of 4 uint8 per pixel elts_per_pixel=1, # only one array per pixel ) From 74ab5ac4cda564714545eee52ab789d4bddf1516 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Sun, 11 May 2025 23:46:21 +0200 Subject: [PATCH 1695/2374] Fix memory leak in arrow export using array structure --- src/libImaging/Arrow.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index 33ff2ce779a..36f4554fc75 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -127,9 +127,7 @@ static void release_const_array(struct ArrowArray *array) { Imaging im = (Imaging)array->private_data; - if (array->n_children == 0) { - ImagingDelete(im); - } + ImagingDelete(im); // Free the buffers and the buffers array if (array->buffers) { From c64a7b50983d64b15bc8551315e28f4ac0cd8e84 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 May 2025 07:41:00 +1000 Subject: [PATCH 1696/2374] Updated harfbuzz to 11.2.1 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index a4592871f69..a6b52064ceb 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -38,7 +38,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.3 -HARFBUZZ_VERSION=11.1.0 +HARFBUZZ_VERSION=11.2.1 LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 17fc37572e8..fbe47929194 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ def cmd_msbuild( "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "11.1.0", + "HARFBUZZ": "11.2.1", "JPEGTURBO": "3.1.0", "LCMS2": "2.17", "LIBAVIF": "1.2.1", From 4984c45da2f6b854cb49dc81fc56372f335d43a0 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 10:27:38 +0200 Subject: [PATCH 1697/2374] valgrind memory leak check --- Makefile | 6 ++++++ Tests/oss-fuzz/python.supp | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Makefile b/Makefile index 53164b08a90..fd124d12427 100644 --- a/Makefile +++ b/Makefile @@ -99,6 +99,12 @@ valgrind: --log-file=/tmp/valgrind-output \ python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output +.PHONY: valgrind-leak +valgrind-leak: + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ + --log-file=/tmp/valgrind-output \ + python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output Tests/ + .PHONY: readme readme: python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp index 36385d67266..1ea2a8eb5a6 100644 --- a/Tests/oss-fuzz/python.supp +++ b/Tests/oss-fuzz/python.supp @@ -14,3 +14,23 @@ fun:_TIFFReadEncodedTileAndAllocBuffer ... } + +{ + + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:_PyMem_RawMalloc + fun:PyObject_Malloc + ... +} + +{ + + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:_PyMem_RawRealloc + fun:PyMem_Realloc + ... +} From fdfba982c8d514240435f3ecef540939fb97f120 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 10:28:09 +0200 Subject: [PATCH 1698/2374] fix memory leak in arrow schema --- src/libImaging/Arrow.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index 36f4554fc75..7d330612367 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -37,6 +37,10 @@ ReleaseExportedSchema(struct ArrowSchema *array) { child->release = NULL; } // UNDONE -- should I be releasing the children? + free(array->children[i]); + } + if (array->children) { + free(array->children); } // Release dictionary @@ -117,6 +121,7 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) { retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel"); if (retval != 0) { free(schema->children[0]); + free(schema->children); schema->release(schema); return retval; } From 84b88a9fbc9c4cfd2bfb7d7a8d18225ee43efedb Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 10:58:12 +0200 Subject: [PATCH 1699/2374] Suppress all python level leaks for now --- Tests/oss-fuzz/python.supp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp index 1ea2a8eb5a6..4803497adeb 100644 --- a/Tests/oss-fuzz/python.supp +++ b/Tests/oss-fuzz/python.supp @@ -18,7 +18,7 @@ { Memcheck:Leak - match-leak-kinds: possible + match-leak-kinds: all fun:malloc fun:_PyMem_RawMalloc fun:PyObject_Malloc @@ -28,7 +28,7 @@ { Memcheck:Leak - match-leak-kinds: possible + match-leak-kinds: all fun:malloc fun:_PyMem_RawRealloc fun:PyMem_Realloc From eaab43540344e26889116262651001f4e42b1630 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 10:58:37 +0200 Subject: [PATCH 1700/2374] Fix leak in webp_encode * Free the output buffer on webp encode error --- src/_webp.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index 3aa4c408b71..a7809c40e59 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -641,6 +641,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { ImagingSectionLeave(&cookie); WebPPictureFree(&pic); + + output = writer.mem; + ret_size = writer.size; + if (!ok) { int error_code = (&pic)->error_code; char message[50] = ""; @@ -652,10 +656,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { ); } PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); + free(output); return NULL; } - output = writer.mem; - ret_size = writer.size; { /* I want to truncate the *_size items that get passed into WebP From a9bcd7db884d89bbfe1966c0611f7f3dda1f8f08 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 19:50:55 +0200 Subject: [PATCH 1701/2374] Fix leak of destination image in ImagingUnsharpMask when an error occurs --- src/_imaging.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_imaging.c b/src/_imaging.c index 72f12214390..79e0a2b2321 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2226,6 +2226,7 @@ _unsharp_mask(ImagingObject *self, PyObject *args) { } if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) { + ImagingDelete(imOut); return NULL; } From e2e40c54568236d2504921eb0b335cdab734a7d5 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 22:33:27 +0200 Subject: [PATCH 1702/2374] Fix memory leak in TiffEncode * If setimage errors out, the tiff client state was not freed. --- src/encode.c | 2 ++ src/libImaging/TiffDecode.c | 42 ++++++++++++++++++------------------- src/libImaging/TiffDecode.h | 2 ++ 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/encode.c b/src/encode.c index 7c365a74f7d..e56494036ff 100644 --- a/src/encode.c +++ b/src/encode.c @@ -703,6 +703,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { return NULL; } + encoder->cleanup = ImagingLibTiffEncodeCleanup; + num_core_tags = sizeof(core_tags) / sizeof(int); for (pos = 0; pos < tags_size; pos++) { item = PyList_GetItemRef(tags, pos); diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 9a2db95b400..173eca160d2 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -929,6 +929,27 @@ ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...) { return status; } +int +ImagingLibTiffEncodeCleanup(ImagingCodecState state) { + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + TIFF *tiff = clientstate->tiff; + + if (!tiff) { + return 0; + } + // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup + if (clientstate->fp) { + // Python will manage the closing of the file rather than libtiff + // So only call TIFFCleanup + TIFFCleanup(tiff); + } else { + // When tif_closeproc refers to our custom _tiffCloseProc though, + // that is fine, as it does not close the file + TIFFClose(tiff); + } + return 0; +} + int ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) { /* One shot encoder. Encode everything to the tiff in the clientstate. @@ -1010,16 +1031,6 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Encode Error, row %d\n", state->y)); state->errcode = IMAGING_CODEC_BROKEN; - // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup - if (clientstate->fp) { - // Python will manage the closing of the file rather than libtiff - // So only call TIFFCleanup - TIFFCleanup(tiff); - } else { - // When tif_closeproc refers to our custom _tiffCloseProc though, - // that is fine, as it does not close the file - TIFFClose(tiff); - } if (!clientstate->fp) { free(clientstate->data); } @@ -1036,22 +1047,11 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Error flushing the tiff")); // likely reason is memory. state->errcode = IMAGING_CODEC_MEMORY; - if (clientstate->fp) { - TIFFCleanup(tiff); - } else { - TIFFClose(tiff); - } if (!clientstate->fp) { free(clientstate->data); } return -1; } - TRACE(("Closing \n")); - if (clientstate->fp) { - TIFFCleanup(tiff); - } else { - TIFFClose(tiff); - } // reset the clientstate metadata to use it to read out the buffer. clientstate->loc = 0; clientstate->size = clientstate->eof; // redundant? diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index 22361210dcb..77808b543fc 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -40,6 +40,8 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset); extern int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); extern int +ImagingLibTiffEncodeCleanup(ImagingCodecState state); +extern int ImagingLibTiffMergeFieldInfo( ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length ); From f792e0b1ef4f25e0df33e8e971056142f9f5248d Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 22:48:36 +0200 Subject: [PATCH 1703/2374] Fix memory leak * Return after setting the error for advanced features without libraqm. Not returning here leads to an alloc that's never freed. --- src/_imagingft.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index 62dab73e5c0..ca8e556f0c4 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -425,6 +425,7 @@ text_layout_fallback( "setting text direction, language or font features is not supported " "without libraqm" ); + return 0; } if (PyUnicode_Check(string)) { From 789631c60c3760beeb623cd1728a737502fd9ca3 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 23:31:09 +0200 Subject: [PATCH 1704/2374] Fix memory leak when JpegEncode returns an error. --- src/libImaging/JpegEncode.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 3c11eac2206..79a38e12fb3 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -131,6 +131,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { break; default: state->errcode = IMAGING_CODEC_CONFIG; + jpeg_destroy_compress(&context->cinfo); return -1; } @@ -161,6 +162,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { /* Would subsample the green and blue channels, which doesn't make sense */ state->errcode = IMAGING_CODEC_CONFIG; + jpeg_destroy_compress(&context->cinfo); return -1; } jpeg_set_colorspace(&context->cinfo, JCS_RGB); From 7aa6a61d430c585cd10c91c5a73ce13f9f851b9e Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 23:50:52 +0200 Subject: [PATCH 1705/2374] Wrap Makefile --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fd124d12427..15f03ba4527 100644 --- a/Makefile +++ b/Makefile @@ -101,7 +101,8 @@ valgrind: .PHONY: valgrind-leak valgrind-leak: - PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ + --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ --log-file=/tmp/valgrind-output \ python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output Tests/ From efa2288643a3d2840b573d14b1aec41f6fd2b80c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 May 2025 08:38:33 +1000 Subject: [PATCH 1706/2374] Updated libavif to 1.3.0 --- Tests/test_file_avif.py | 2 +- depends/install_libavif.sh | 2 +- winbuild/build_prepare.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index bd87947c014..b2e586637fa 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -233,7 +233,7 @@ def test_background_from_gif(self, tmp_path: Path) -> None: with Image.open(out_gif) as reread: reread_value = reread.convert("RGB").getpixel((1, 1)) difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)]) - assert difference <= 3 + assert difference <= 6 def test_save_single_frame(self, tmp_path: Path) -> None: temp_file = tmp_path / "temp.avif" diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh index fc10d3e545c..26af8a36ce7 100755 --- a/depends/install_libavif.sh +++ b/depends/install_libavif.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -eo pipefail -version=1.2.1 +version=1.3.0 ./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 17fc37572e8..9fee5bd9066 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ def cmd_msbuild( "HARFBUZZ": "11.1.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.17", - "LIBAVIF": "1.2.1", + "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.3.4", "LIBPNG": "1.6.47", "LIBWEBP": "1.5.0", @@ -399,7 +399,6 @@ def cmd_msbuild( "-DAVIF_CODEC_DAV1D=LOCAL", "-DAVIF_CODEC_RAV1E=LOCAL", "-DAVIF_CODEC_SVT=LOCAL", - "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", ), cmd_xcopy("include", "{inc_dir}"), ], From fb126af7a6a12e0870e56187257f75f35fe8558b Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 21:10:48 +0200 Subject: [PATCH 1707/2374] Adding pytest-valgrind install --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 15f03ba4527..bdddecda57c 100644 --- a/Makefile +++ b/Makefile @@ -101,6 +101,7 @@ valgrind: .PHONY: valgrind-leak valgrind-leak: + python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ --log-file=/tmp/valgrind-output \ From d5449d576013566100d8a0d41bbc1a756df86da5 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 21:11:31 +0200 Subject: [PATCH 1708/2374] Guess so. --- src/libImaging/Arrow.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index 7d330612367..0b8c89a0773 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -36,7 +36,6 @@ ReleaseExportedSchema(struct ArrowSchema *array) { child->release(child); child->release = NULL; } - // UNDONE -- should I be releasing the children? free(array->children[i]); } if (array->children) { From 218f055865a4f0abd05ac221c48cf86127907ca9 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 21:59:02 +0200 Subject: [PATCH 1709/2374] Add github workflow/test-script --- .github/workflows/test-valgrind-memory.yml | 60 ++++++++++++++++++++++ depends/docker-test-valgrind-memory.sh | 11 ++++ 2 files changed, 71 insertions(+) create mode 100644 .github/workflows/test-valgrind-memory.yml create mode 100644 depends/docker-test-valgrind-memory.sh diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml new file mode 100644 index 00000000000..e6a5f6e779f --- /dev/null +++ b/.github/workflows/test-valgrind-memory.yml @@ -0,0 +1,60 @@ +name: Test Valgrind Memory Leaks + +# like the Docker tests, but running valgrind only on *.c/*.h changes. + +# this is very expensive. Only run on the pull request. +on: + # push: + # branches: + # - "**" + # paths: + # - ".github/workflows/test-valgrind.yml" + # - "**.c" + # - "**.h" + pull_request: + paths: + - ".github/workflows/test-valgrind.yml" + - "**.c" + - "**.h" + - "depends/docker-test-valgrind-memory.sh" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + docker: [ + ubuntu-22.04-jammy-amd64-valgrind, + ] + dockerTag: [main] + + name: ${{ matrix.docker }} + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Docker pull + run: | + docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + + - name: Build and Run Valgrind + run: | + # The Pillow user in the docker container is UID 1001 + sudo chown -R 1001 $GITHUB_WORKSPACE + docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} /Pillow/depends/docker-test-valgrind-memory.sh + sudo chown -R runner $GITHUB_WORKSPACE diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh new file mode 100644 index 00000000000..4fd6652d871 --- /dev/null +++ b/depends/docker-test-valgrind-memory.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +## Run this as the test script in the docker valgrind image. +## Note -- can be included directly into the docker image, +## but requires the currnet python.supp. + +source /vpy3/bin/activate +cd /Pillow +make clean +make install +make valgrind-memory From a6b8b3af7709081d8c53818e68b8bc15e9a48f34 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 22:04:14 +0200 Subject: [PATCH 1710/2374] executable --- depends/docker-test-valgrind-memory.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 depends/docker-test-valgrind-memory.sh diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh old mode 100644 new mode 100755 From 2d506f6f5a478b4a798b3ce71f31b5e5f6f6b60f Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 22:06:35 +0200 Subject: [PATCH 1711/2374] correct target --- depends/docker-test-valgrind-memory.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh index 4fd6652d871..29fc6f2300d 100755 --- a/depends/docker-test-valgrind-memory.sh +++ b/depends/docker-test-valgrind-memory.sh @@ -8,4 +8,4 @@ source /vpy3/bin/activate cd /Pillow make clean make install -make valgrind-memory +make valgrind-leak From f1957b49b2d01a9d063aed4000f985b220e30fa0 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 16 May 2025 12:08:45 +0200 Subject: [PATCH 1712/2374] Xfail timouts in Valgrind tests * ensure that the env variable is set in the makefile --- Makefile | 4 ++-- Tests/test_file_jpeg.py | 5 +++++ Tests/test_imagefontpil.py | 6 ++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index bdddecda57c..82c2c085fd9 100644 --- a/Makefile +++ b/Makefile @@ -95,14 +95,14 @@ test: .PHONY: valgrind valgrind: python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind - PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ + PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ --log-file=/tmp/valgrind-output \ python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output .PHONY: valgrind-leak valgrind-leak: python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind - PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ + PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ --log-file=/tmp/valgrind-output \ python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output Tests/ diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 79f0ec1a809..fb9f26fc7fc 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1034,6 +1034,11 @@ def test_save_xmp(self, tmp_path: Path) -> None: im.save(f, xmp=b"1" * 65505) @pytest.mark.timeout(timeout=1) + @pytest.mark.xfail( + "PILLOW_VALGRIND_TEST" in os.environ, + reason="Valgrind is slower", + raises=TimeoutError + ) def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # Even though this decoder never says that it is finished # the image should still end when there is no new data diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 695aecbded2..adce4a75ca5 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -2,6 +2,7 @@ import struct from io import BytesIO +import os import pytest @@ -73,6 +74,11 @@ def test_decompression_bomb() -> None: @pytest.mark.timeout(4) +@pytest.mark.xfail( + "PILLOW_VALGRIND_TEST" in os.environ, + reason="Valgrind is slower", + raises=TimeoutError +) def test_oom() -> None: glyph = struct.pack( ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 From ff50e30d3e9f1425ca6af95ac044d365c63719d1 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 16 May 2025 12:47:22 +0200 Subject: [PATCH 1713/2374] Fix memory leak in text_layout_raqm on 0 length string --- src/_imagingft.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index ca8e556f0c4..0d70544a545 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -275,6 +275,7 @@ text_layout_raqm( if (!text || !size) { /* return 0 and clean up, no glyphs==no size, and raqm fails with empty strings */ + PyMem_Free(text); goto failed; } set_text = raqm_set_text(rq, text, size); From 20b49a332bd0f0f39660fbb3587cfc4b6d539f0c Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Sat, 17 May 2025 10:45:43 +0200 Subject: [PATCH 1714/2374] Remove timeout as the specific reason, pytest-timeout doesn't raise a timeout error. --- Tests/test_file_jpeg.py | 3 +-- Tests/test_imagefontpil.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index fb9f26fc7fc..d923020c8c7 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1036,8 +1036,7 @@ def test_save_xmp(self, tmp_path: Path) -> None: @pytest.mark.timeout(timeout=1) @pytest.mark.xfail( "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower", - raises=TimeoutError + reason="Valgrind is slower" ) def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # Even though this decoder never says that it is finished diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index adce4a75ca5..bd9bafb5535 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -76,8 +76,7 @@ def test_decompression_bomb() -> None: @pytest.mark.timeout(4) @pytest.mark.xfail( "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower", - raises=TimeoutError + reason="Valgrind is slower" ) def test_oom() -> None: glyph = struct.pack( From c35082b619899d5351ba249e8ea23a4412d0c728 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 08:47:59 +0000 Subject: [PATCH 1715/2374] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_jpeg.py | 3 +-- Tests/test_imagefontpil.py | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index d923020c8c7..7c33c751720 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1035,8 +1035,7 @@ def test_save_xmp(self, tmp_path: Path) -> None: @pytest.mark.timeout(timeout=1) @pytest.mark.xfail( - "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower" + "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # Even though this decoder never says that it is finished diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index bd9bafb5535..e5b770745c9 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -1,8 +1,8 @@ from __future__ import annotations +import os import struct from io import BytesIO -import os import pytest @@ -74,10 +74,7 @@ def test_decompression_bomb() -> None: @pytest.mark.timeout(4) -@pytest.mark.xfail( - "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower" -) +@pytest.mark.xfail("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") def test_oom() -> None: glyph = struct.pack( ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 From a666057989efc669909e85754943f2734c5f2055 Mon Sep 17 00:00:00 2001 From: Stefan <96178532+stefan6419846@users.noreply.github.com> Date: Wed, 21 May 2025 15:40:16 +0200 Subject: [PATCH 1716/2374] HTTP -> HTTPS Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/IcoImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 8ddb94b3710..151eef2c9b9 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -23,7 +23,7 @@ # not use this file except in compliance with the License. You may obtain # a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, From 45d1c4162b9666281857f3b5d38fdefdbf9b1979 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 May 2025 15:55:43 +1000 Subject: [PATCH 1717/2374] Do not build against libavif < 1 --- setup.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index 5d41e27d981..ab36c6b1783 100644 --- a/setup.py +++ b/setup.py @@ -224,13 +224,14 @@ def _add_directory( path.insert(where, subdir) -def _find_include_file(self: pil_build_ext, include: str) -> int: +def _find_include_file(self: pil_build_ext, include: str) -> str | None: for directory in self.compiler.include_dirs: _dbg("Checking for include file %s in %s", (include, directory)) - if os.path.isfile(os.path.join(directory, include)): + path = os.path.join(directory, include) + if os.path.isfile(path): _dbg("Found %s", include) - return 1 - return 0 + return path + return None def _find_library_file(self: pil_build_ext, library: str) -> str | None: @@ -852,9 +853,13 @@ def build_extensions(self) -> None: if feature.want("avif"): _dbg("Looking for avif") - if _find_include_file(self, "avif/avif.h"): - if _find_library_file(self, "avif"): - feature.set("avif", "avif") + if avif_h := _find_include_file(self, "avif/avif.h"): + with open(avif_h, "rb") as fp: + major_version = int( + fp.read().split(b"#define AVIF_VERSION_MAJOR ")[1].split()[0] + ) + if major_version >= 1 and _find_library_file(self, "avif"): + feature.set("avif", "avif") for f in feature: if not feature.get(f) and feature.require(f): From 7824d2f8c61648c6ea0185b50e55df1a71213168 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 May 2025 08:48:38 +1000 Subject: [PATCH 1718/2374] Update rust when building libavif --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9fee5bd9066..6cdcf6f0db3 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -389,6 +389,7 @@ def cmd_msbuild( "filename": f"libavif-{V['LIBAVIF']}.zip", "license": "LICENSE", "build": [ + "rustup update", f"{sys.executable} -m pip install meson", *cmds_cmake( "avif_static", From 2603a249df9223133b74c671acfcdc6a51567843 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 23 May 2025 10:57:03 +0100 Subject: [PATCH 1719/2374] Update depends/docker-test-valgrind-memory.sh Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- depends/docker-test-valgrind-memory.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh index 29fc6f2300d..5f7805421f4 100755 --- a/depends/docker-test-valgrind-memory.sh +++ b/depends/docker-test-valgrind-memory.sh @@ -2,7 +2,7 @@ ## Run this as the test script in the docker valgrind image. ## Note -- can be included directly into the docker image, -## but requires the currnet python.supp. +## but requires the current python.supp. source /vpy3/bin/activate cd /Pillow From 9526d949b07bbddfc7e515810dc23738b778bee4 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 23 May 2025 10:58:28 +0100 Subject: [PATCH 1720/2374] Update Tests/test_pyarrow.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_pyarrow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index e7fce1e3399..c5872231b3b 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -162,7 +162,7 @@ class DataShape(NamedTuple): UINT_ARR = DataShape( dtype=fl_uint8_4_type, - elt=[1, 2, 3, 4], # array of 4 uint 8 per pixel + elt=[1, 2, 3, 4], # array of 4 uint8 per pixel elts_per_pixel=1, # only one array per pixel ) From 6807bd3d70cc5873b3cad29d598e08a34fdc1fa0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 May 2025 00:03:08 +1000 Subject: [PATCH 1721/2374] Added type hints --- .ci/requirements-mypy.txt | 1 + Tests/test_pyarrow.py | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 2e361047891..86ac2e0b26b 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -4,6 +4,7 @@ IceSpringPySideStubs-PySide6 ipython numpy packaging +pyarrow-stubs pytest sphinx types-atheris diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index c5872231b3b..2029f96f568 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -13,7 +13,11 @@ is_big_endian, ) -pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") +TYPE_CHECKING = False +if TYPE_CHECKING: + import pyarrow +else: + pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") TEST_IMAGE_SIZE = (10, 10) @@ -94,14 +98,14 @@ def _test_img_equals_int32_pyarray( ("HSV", fl_uint8_4_type, [0, 1, 2]), ), ) -def test_to_array(mode: str, dtype: Any, mask: list[int] | None) -> None: +def test_to_array(mode: str, dtype: pyarrow.DataType, mask: list[int] | None) -> None: img = hopper(mode) # Resize to non-square img = img.crop((3, 0, 124, 127)) assert img.size == (121, 127) - arr = pyarrow.array(img) + arr = pyarrow.array(img) # type: ignore[call-overload] _test_img_equals_pyarray(img, arr, mask) assert arr.type == dtype @@ -118,8 +122,8 @@ def test_lifetime() -> None: img = hopper("L") - arr_1 = pyarrow.array(img) - arr_2 = pyarrow.array(img) + arr_1 = pyarrow.array(img) # type: ignore[call-overload] + arr_2 = pyarrow.array(img) # type: ignore[call-overload] del img @@ -136,8 +140,8 @@ def test_lifetime2() -> None: img = hopper("L") - arr_1 = pyarrow.array(img) - arr_2 = pyarrow.array(img) + arr_1 = pyarrow.array(img) # type: ignore[call-overload] + arr_2 = pyarrow.array(img) # type: ignore[call-overload] assert arr_1.sum().as_py() > 0 del arr_1 @@ -152,7 +156,7 @@ def test_lifetime2() -> None: class DataShape(NamedTuple): - dtype: Any + dtype: pyarrow.DataType # Strictly speaking, elt should be a pixel or pixel component, so # list[uint8][4], float, int, uint32, uint8, etc. But more # correctly, it should be exactly the dtype from the line above. From 60a1a20536fe18cfe936e90140ed56c3eb31bd81 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 23 May 2025 15:32:46 +0200 Subject: [PATCH 1722/2374] add timeouts to two more tests --- Tests/test_file_tiff.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 502d9df9a33..050bfe57809 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -990,6 +990,10 @@ def test_string_dimension(self) -> None: @pytest.mark.timeout(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") + @pytest.mark.xfail( + "PILLOW_VALGRIND_TEST" in os.environ, + reason="Valgrind is slower" + ) def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) @@ -1002,6 +1006,10 @@ def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: ], ) @pytest.mark.timeout(2) + @pytest.mark.xfail( + "PILLOW_VALGRIND_TEST" in os.environ, + reason="Valgrind is slower" + ) def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): From c63db77db3850c51df38af8f4a96f5c13f286b42 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 13:37:02 +0000 Subject: [PATCH 1723/2374] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_tiff.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 050bfe57809..b6985b83b36 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -991,8 +991,7 @@ def test_string_dimension(self) -> None: @pytest.mark.timeout(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") @pytest.mark.xfail( - "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower" + "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: @@ -1007,8 +1006,7 @@ def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: ) @pytest.mark.timeout(2) @pytest.mark.xfail( - "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower" + "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): From 4d0678ca33b65af2686fe93be5b77c2b28027959 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 23 May 2025 16:35:57 +0200 Subject: [PATCH 1724/2374] Add parallel test target, using pytest-xdist --- Makefile | 8 +++++++- pyproject.toml | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5a815245402..a56fe8fec3c 100644 --- a/Makefile +++ b/Makefile @@ -95,7 +95,13 @@ sdist: .PHONY: test test: python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest - python3 -m pytest -qq + python3 -m pytest -qq Tests/ + +.PHONY: test-p +test-p: + python3 -c "import xdist" > /dev/null 2>&1 || python3 -m pip install pytest-xdist + python3 -m pytest -qq -n auto Tests/ + .PHONY: valgrind valgrind: diff --git a/pyproject.toml b/pyproject.toml index a3ff9723b4e..683ab24ef07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ optional-dependencies.tests = [ "pytest", "pytest-cov", "pytest-timeout", + "pytest-xdist", "trove-classifiers>=2024.10.12", ] From e018dc99faf8d7196ec2a2e7d2760cede851fbd9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 May 2025 08:51:51 +1000 Subject: [PATCH 1725/2374] Updated libpng to 1.6.48 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index a6b52064ceb..1583435c13f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -39,7 +39,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=11.2.1 -LIBPNG_VERSION=1.6.47 +LIBPNG_VERSION=1.6.48 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d23f0eb5b59..6e176e29cf3 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -118,7 +118,7 @@ def cmd_msbuild( "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.3.4", - "LIBPNG": "1.6.47", + "LIBPNG": "1.6.48", "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.7.0", From 4eb89f8e5bcd19ad64ed2328c9566061a7116cc2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 May 2025 20:36:19 +1000 Subject: [PATCH 1726/2374] Reduced number of bytes read for header --- src/PIL/WmfImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index f709d026bfd..d569cb4b819 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -81,7 +81,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): def _open(self) -> None: # check placable header - s = self.fp.read(80) + s = self.fp.read(44) if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"): # placeable windows metafile From 57b77bde96484d4a1d6f92adec9a2c2b86485f55 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 May 2025 11:55:18 +1000 Subject: [PATCH 1727/2374] Removed CMAKE_POLICY_VERSION_MINIMUM=3.5 --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index d065e7ab5c5..acb84f046d1 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -66,7 +66,7 @@ if [[ $(uname) != CYGWIN* ]]; then pushd depends && ./install_raqm.sh && popd # libavif - pushd depends && CMAKE_POLICY_VERSION_MINIMUM=3.5 ./install_libavif.sh && popd + pushd depends && ./install_libavif.sh && popd # extra test images pushd depends && ./install_extra_test_images.sh && popd From 041acf13440f9307dcc129eff21bcd065362ae91 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 25 May 2025 15:00:47 +1000 Subject: [PATCH 1728/2374] Clear core image if memory mapping was used for last load --- Tests/test_tiff_crashes.py | 14 ++++++++++++++ src/PIL/TiffImagePlugin.py | 5 +++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index 073e5415c41..976f6238465 100644 --- a/Tests/test_tiff_crashes.py +++ b/Tests/test_tiff_crashes.py @@ -52,3 +52,17 @@ def test_tiff_crashes(test_file: str) -> None: pytest.skip("test image not found") except OSError: pass + + +def test_tiff_mmap() -> None: + try: + with Image.open("Tests/images/crash_mmap.tif") as im: + im.seek(1) + im.load() + + im.seek(0) + im.load() + except FileNotFoundError: + if on_ci(): + raise + pytest.skip("test image not found") diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 88af9162e0a..5cbac0c2679 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1217,9 +1217,10 @@ def seek(self, frame: int) -> None: return self._seek(frame) if self._im is not None and ( - self.im.size != self._tile_size or self.im.mode != self.mode + self.im.size != self._tile_size + or self.im.mode != self.mode + or self.readonly ): - # The core image will no longer be used self._im = None def _seek(self, frame: int) -> None: From eff667a8614ba3b567684597795924387c8cbaaa Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 23 May 2025 10:22:59 +0100 Subject: [PATCH 1729/2374] Mark the image read-only in the C layer if it's created from a read only buffer --- src/map.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/map.c b/src/map.c index c66702981d3..9a3144ab904 100644 --- a/src/map.c +++ b/src/map.c @@ -137,6 +137,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { } } + im->read_only = view.readonly; im->destroy = mapping_destroy_buffer; Py_INCREF(target); From bcc6e42bf82dfc962d49ed1876a946bd7be16b4f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 27 May 2025 21:08:58 +1000 Subject: [PATCH 1730/2374] Fixed saving MPO with more than one appended image --- Tests/test_file_mpo.py | 22 ++++++++++++---------- src/PIL/MpoImagePlugin.py | 13 ++++++++++--- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 73838ef4484..adfa61962b7 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -293,16 +293,18 @@ def test_save_all() -> None: assert_image_similar(im, im_reloaded, 30) im = Image.new("RGB", (1, 1)) - im2 = Image.new("RGB", (1, 1), "#f00") - im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) - - assert_image_equal(im, im_reloaded) - assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile) - assert im_reloaded.mpinfo is not None - assert im_reloaded.mpinfo[45056] == b"0100" - - im_reloaded.seek(1) - assert_image_similar(im2, im_reloaded, 1) + for colors in (("#f00",), ("#f00", "#0f0")): + append_images = (Image.new("RGB", (1, 1), color) for color in colors) + im_reloaded = roundtrip(im, save_all=True, append_images=append_images) + + assert_image_equal(im, im_reloaded) + assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile) + assert im_reloaded.mpinfo is not None + assert im_reloaded.mpinfo[45056] == b"0100" + + for im_expected in append_images: + im_reloaded.seek(im_reloaded.tell() + 1) + assert_image_similar(im_reloaded, im_expected, 1) # Test that a single frame image will not be saved as an MPO jpg = roundtrip(im, save_all=True) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index f7393eac059..f96f658fcdb 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -19,7 +19,6 @@ # from __future__ import annotations -import itertools import os import struct from typing import IO, Any, cast @@ -47,12 +46,20 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: mpf_offset = 28 offsets: list[int] = [] - for imSequence in itertools.chain([im], append_images): + total = 0 + imSequences = [im] + list(append_images) + for imSequence in imSequences: + total += getattr(imSequence, "n_frames", 1) + for imSequence in imSequences: for im_frame in ImageSequence.Iterator(imSequence): if not offsets: # APP2 marker + ifd_length = 66 + 16 * total im_frame.encoderinfo["extra"] = ( - b"\xff\xe2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 + b"\xff\xe2" + + struct.pack(">H", 6 + ifd_length) + + b"MPF\0" + + b" " * ifd_length ) exif = im_frame.encoderinfo.get("exif") if isinstance(exif, Image.Exif): From 5a04b9581b16a7f1e1109f1e31a206a6550f314c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 28 May 2025 08:20:35 +1000 Subject: [PATCH 1731/2374] Run slow tests on valgrind, but without timeout (#8975) --- Tests/helper.py | 6 ++++++ Tests/test_file_eps.py | 3 ++- Tests/test_file_fli.py | 9 +++++++-- Tests/test_file_jpeg.py | 3 ++- Tests/test_file_pdf.py | 10 +++++++--- Tests/test_file_tiff.py | 5 +++-- Tests/test_image.py | 6 ++---- Tests/test_imagefontpil.py | 4 ++-- 8 files changed, 31 insertions(+), 15 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 909fff879c8..ec61cd263e9 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -161,6 +161,12 @@ def assert_tuple_approx_equal( pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets)) +def timeout_unless_slower_valgrind(timeout: float) -> pytest.MarkDecorator: + if "PILLOW_VALGRIND_TEST" in os.environ: + return pytest.mark.pil_noop_mark() + return pytest.mark.timeout(timeout) + + def skip_unless_feature(feature: str) -> pytest.MarkDecorator: reason = f"{feature} not available" return pytest.mark.skipif(not features.check(feature), reason=reason) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index b484a8cfa0a..d94de728709 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -15,6 +15,7 @@ is_win32, mark_if_feature_version, skip_unless_feature, + timeout_unless_slower_valgrind, ) HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() @@ -398,7 +399,7 @@ def test_emptyline() -> None: assert image.format == "EPS" -@pytest.mark.timeout(timeout=5) +@timeout_unless_slower_valgrind(5) @pytest.mark.parametrize( "test_file", ["Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 81df1ab0b80..0fadd01d0c9 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -7,7 +7,12 @@ from PIL import FliImagePlugin, Image, ImageFile -from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + is_pypy, + timeout_unless_slower_valgrind, +) # created as an export of a palette image from Gimp2.6 # save as...-> hopper.fli, default options. @@ -189,7 +194,7 @@ def test_seek() -> None: "Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli", ], ) -@pytest.mark.timeout(timeout=3) +@timeout_unless_slower_valgrind(3) def test_timeouts(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 79f0ec1a809..b9eec591dcf 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -32,6 +32,7 @@ is_win32, mark_if_feature_version, skip_unless_feature, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -1033,7 +1034,7 @@ def test_save_xmp(self, tmp_path: Path) -> None: with pytest.raises(ValueError): im.save(f, xmp=b"1" * 65505) - @pytest.mark.timeout(timeout=1) + @timeout_unless_slower_valgrind(1) def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # Even though this decoder never says that it is finished # the image should still end when there is no new data diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index bde1e3ab847..a2218673b44 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -13,7 +13,12 @@ from PIL import Image, PdfParser, features -from .helper import hopper, mark_if_feature_version, skip_unless_feature +from .helper import ( + hopper, + mark_if_feature_version, + skip_unless_feature, + timeout_unless_slower_valgrind, +) def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str: @@ -339,8 +344,7 @@ def test_pdf_append_to_bytesio() -> None: assert len(f.getvalue()) > initial_size -@pytest.mark.timeout(1) -@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") +@timeout_unless_slower_valgrind(1) @pytest.mark.parametrize("newline", (b"\r", b"\n")) def test_redos(newline: bytes) -> None: malicious = b" trailer<<>>" + newline * 3456 diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 502d9df9a33..d0d394aa9cd 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -26,6 +26,7 @@ hopper, is_pypy, is_win32, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -988,7 +989,7 @@ def test_string_dimension(self) -> None: with pytest.raises(OSError): im.load() - @pytest.mark.timeout(6) + @timeout_unless_slower_valgrind(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: @@ -1001,7 +1002,7 @@ def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: "Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif", ], ) - @pytest.mark.timeout(2) + @timeout_unless_slower_valgrind(2) def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): diff --git a/Tests/test_image.py b/Tests/test_image.py index 7e6118d5280..14a0671277d 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -34,6 +34,7 @@ is_win32, mark_if_feature_version, skip_unless_feature, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -572,10 +573,7 @@ def test_check_size(self) -> None: i = Image.new("RGB", [1, 1]) assert isinstance(i.size, tuple) - @pytest.mark.timeout(0.75) - @pytest.mark.skipif( - "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" - ) + @timeout_unless_slower_valgrind(0.75) @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) def test_empty_image(self, size: tuple[int, int]) -> None: Image.new("RGB", size) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 695aecbded2..3eb98d3797d 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -7,7 +7,7 @@ from PIL import Image, ImageDraw, ImageFont, _util, features -from .helper import assert_image_equal_tofile +from .helper import assert_image_equal_tofile, timeout_unless_slower_valgrind fonts = [ImageFont.load_default_imagefont()] if not features.check_module("freetype2"): @@ -72,7 +72,7 @@ def test_decompression_bomb() -> None: font.getmask("A" * 1_000_000) -@pytest.mark.timeout(4) +@timeout_unless_slower_valgrind(4) def test_oom() -> None: glyph = struct.pack( ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 From 5000c83bcc1514d371be53b52b50aaf7237506d5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 May 2025 23:50:18 +1000 Subject: [PATCH 1732/2374] Use multi-phase initialization --- src/_avif.c | 24 ++++++++++-------------- src/_imaging.c | 25 ++++++++++--------------- src/_imagingcms.c | 26 +++++++++++--------------- src/_imagingft.c | 24 ++++++++++-------------- src/_imagingmath.c | 24 ++++++++++-------------- src/_imagingmorph.c | 19 +++++++++---------- src/_imagingtk.c | 22 ++++++++++------------ src/_webp.c | 24 ++++++++++-------------- 8 files changed, 80 insertions(+), 108 deletions(-) diff --git a/src/_avif.c b/src/_avif.c index 7e7bee7031b..3585297fe87 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -881,26 +881,22 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__avif(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_avif", - .m_size = -1, .m_methods = avifMethods, + .m_slots = slots }; - m = PyModule_Create(&module_def); - if (setup_module(m) < 0) { - Py_DECREF(m); - return NULL; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imaging.c b/src/_imaging.c index 72f12214390..0c93a96bc81 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4460,27 +4460,22 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imaging(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_imaging", - .m_size = -1, .m_methods = functions, + .m_slots = slots }; - m = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - Py_DECREF(m); - return NULL; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index f93c1613b47..e2f29d1b708 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1463,28 +1463,24 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imagingcms(void) { - PyObject *m; + PyDateTime_IMPORT; static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_imagingcms", - .m_size = -1, .m_methods = pyCMSdll_methods, + .m_slots = slots }; - m = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - return NULL; - } - - PyDateTime_IMPORT; - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingft.c b/src/_imagingft.c index 62dab73e5c0..c3e6e2f391a 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1599,26 +1599,22 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imagingft(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_imagingft", - .m_size = -1, .m_methods = _functions, + .m_slots = slots }; - m = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - return NULL; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingmath.c b/src/_imagingmath.c index 4b9bf08ba00..48c3959003e 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -302,26 +302,22 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imagingmath(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_imagingmath", - .m_size = -1, .m_methods = _functions, + .m_slots = slots }; - m = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - return NULL; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index a2088829424..5995f9d429e 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -246,23 +246,22 @@ static PyMethodDef functions[] = { {NULL, NULL, 0, NULL} }; +static PyModuleDef_Slot slots[] = { +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imagingmorph(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_imagingmorph", .m_doc = "A module for doing image morphology", - .m_size = -1, .m_methods = functions, + .m_slots = slots }; - m = PyModule_Create(&module_def); - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingtk.c b/src/_imagingtk.c index 4e06fe9b848..68d7bf4cd11 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -46,24 +46,22 @@ static PyMethodDef functions[] = { {NULL, NULL} /* sentinel */ }; +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, load_tkinter_funcs}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imagingtk(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_imagingtk", - .m_size = -1, .m_methods = functions, + .m_slots = slots }; - PyObject *m; - m = PyModule_Create(&module_def); - if (load_tkinter_funcs() != 0) { - Py_DECREF(m); - return NULL; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_webp.c b/src/_webp.c index 3aa4c408b71..0dff9f6dd51 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -777,26 +777,22 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__webp(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_webp", - .m_size = -1, .m_methods = webpMethods, + .m_slots = slots }; - m = PyModule_Create(&module_def); - if (setup_module(m) < 0) { - Py_DECREF(m); - return NULL; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } From 2ee2a1496d9d4b2cc1f8455342d0e2f5da8f542c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 May 2025 22:06:27 +1000 Subject: [PATCH 1733/2374] Simplified code --- src/PIL/ImageGrab.py | 5 +---- src/PIL/JpegImagePlugin.py | 9 +++------ src/libImaging/Arrow.c | 6 +++--- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index c29350b7a81..d11609483d0 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -134,10 +134,7 @@ def grabclipboard() -> Image.Image | list[str] | None: import struct o = struct.unpack_from("I", data)[0] - if data[16] != 0: - files = data[o:].decode("utf-16le").split("\0") - else: - files = data[o:].decode("mbcs").split("\0") + files = data[o:].decode("mbcs" if data[16] == 0 else "utf-16le").split("\0") return files[: files.index("")] if isinstance(data, bytes): data = io.BytesIO(data) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 96952884118..defe9f773f9 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -762,8 +762,7 @@ def validate_qtables( extra = info.get("extra", b"") MAX_BYTES_IN_MARKER = 65533 - xmp = info.get("xmp") - if xmp: + if xmp := info.get("xmp"): overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len if len(xmp) > max_data_bytes_in_marker: @@ -772,8 +771,7 @@ def validate_qtables( size = o16(2 + overhead_len + len(xmp)) extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp - icc_profile = info.get("icc_profile") - if icc_profile: + if icc_profile := info.get("icc_profile"): overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len markers = [] @@ -831,7 +829,6 @@ def validate_qtables( # in a shot. Guessing on the size, at im.size bytes. (raw pixel size is # channels*size, this is a value that's been used in a django patch. # https://github.com/matthewwithanm/django-imagekit/issues/50 - bufsize = 0 if optimize or progressive: # CMYK can be bigger if im.mode == "CMYK": @@ -848,7 +845,7 @@ def validate_qtables( else: # The EXIF info needs to be written as one block, + APP1, + one spare byte. # Ensure that our buffer is big enough. Same with the icc_profile block. - bufsize = max(bufsize, len(exif) + 5, len(extra) + 1) + bufsize = max(len(exif) + 5, len(extra) + 1) ImageFile._save( im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index 33ff2ce779a..3d34076dc63 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -98,7 +98,7 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) { } /* for now, single block images */ - if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + if (im->blocks_count > 1) { return IMAGING_ARROW_MEMORY_LAYOUT; } @@ -157,7 +157,7 @@ export_single_channel_array(Imaging im, struct ArrowArray *array) { int length = im->xsize * im->ysize; /* for now, single block images */ - if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + if (im->blocks_count > 1) { return IMAGING_ARROW_MEMORY_LAYOUT; } @@ -200,7 +200,7 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { int length = im->xsize * im->ysize; /* for now, single block images */ - if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + if (im->blocks_count > 1) { return IMAGING_ARROW_MEMORY_LAYOUT; } From fcac6e78966f7cac4813731dc2303f80f844b320 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 May 2025 18:27:17 +1000 Subject: [PATCH 1734/2374] Removed hasAlpha argument --- src/libImaging/Draw.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index d5aff8709f5..0462933790a 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -439,9 +439,7 @@ draw_horizontal_lines( * Filled polygon draw function using scan line algorithm. */ static inline int -polygon_generic( - Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, int hasAlpha -) { +polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline) { Edge **edge_table; float *xx; int edge_count = 0; @@ -461,6 +459,7 @@ polygon_generic( return -1; } + int hasAlpha = hline == hline32rgba; for (i = 0; i < n; i++) { if (ymin > e[i].ymin) { ymin = e[i].ymin; @@ -592,17 +591,17 @@ polygon_generic( static inline int polygon8(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline8, 0); + return polygon_generic(im, n, e, ink, eofill, hline8); } static inline int polygon32(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline32, 0); + return polygon_generic(im, n, e, ink, eofill, hline32); } static inline int polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline32rgba, 1); + return polygon_generic(im, n, e, ink, eofill, hline32rgba); } static inline void From 62da23bf83a568079cd514cbac6a99bf37009820 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 May 2025 18:22:49 +1000 Subject: [PATCH 1735/2374] Removed polygon from DRAW struct --- src/libImaging/Draw.c | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 0462933790a..4c08e985504 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -589,21 +589,6 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h return 0; } -static inline int -polygon8(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline8); -} - -static inline int -polygon32(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline32); -} - -static inline int -polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline32rgba); -} - static inline void add_edge(Edge *e, int x0, int y0, int x1, int y1) { /* printf("edge %d %d %d %d\n", x0, y0, x1, y1); */ @@ -640,12 +625,11 @@ typedef struct { void (*point)(Imaging im, int x, int y, int ink); void (*hline)(Imaging im, int x0, int y0, int x1, int ink); void (*line)(Imaging im, int x0, int y0, int x1, int y1, int ink); - int (*polygon)(Imaging im, int n, Edge *e, int ink, int eofill); } DRAW; -DRAW draw8 = {point8, hline8, line8, polygon8}; -DRAW draw32 = {point32, hline32, line32, polygon32}; -DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba, polygon32rgba}; +DRAW draw8 = {point8, hline8, line8}; +DRAW draw32 = {point32, hline32, line32}; +DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba}; /* -------------------------------------------------------------------- */ /* Interface */ @@ -730,7 +714,7 @@ ImagingDrawWideLine( add_edge(e + 2, vertices[2][0], vertices[2][1], vertices[3][0], vertices[3][1]); add_edge(e + 3, vertices[3][0], vertices[3][1], vertices[0][0], vertices[0][1]); - draw->polygon(im, 4, e, ink, 0); + polygon_generic(im, 4, e, ink, 0, draw->hline); } return 0; } @@ -838,7 +822,7 @@ ImagingDrawPolygon( if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) { add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]); } - draw->polygon(im, n, e, ink, 0); + polygon_generic(im, n, e, ink, 0, draw->hline); free(e); } else { @@ -1988,7 +1972,7 @@ ImagingDrawOutline( DRAWINIT(); - draw->polygon(im, outline->count, outline->edges, ink, 0); + polygon_generic(im, outline->count, outline->edges, ink, 0, draw->hline); return 0; } From 6a60b2e6dd0909f627d093cbc431891a79d2b987 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 30 May 2025 10:27:11 +0100 Subject: [PATCH 1736/2374] Remove Tests/ path arg, this is already configured --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a56fe8fec3c..1f9d2ce1330 100644 --- a/Makefile +++ b/Makefile @@ -95,12 +95,12 @@ sdist: .PHONY: test test: python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest - python3 -m pytest -qq Tests/ + python3 -m pytest -qq .PHONY: test-p test-p: python3 -c "import xdist" > /dev/null 2>&1 || python3 -m pip install pytest-xdist - python3 -m pytest -qq -n auto Tests/ + python3 -m pytest -qq -n auto .PHONY: valgrind From 98cf15e9e487cbc53b101498d43cc0cc141ee7e7 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 30 May 2025 10:35:13 +0100 Subject: [PATCH 1737/2374] Update depends/docker-test-valgrind-memory.sh Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- depends/docker-test-valgrind-memory.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh index 5f7805421f4..f0d1d851dd1 100755 --- a/depends/docker-test-valgrind-memory.sh +++ b/depends/docker-test-valgrind-memory.sh @@ -1,7 +1,7 @@ #!/bin/bash -## Run this as the test script in the docker valgrind image. -## Note -- can be included directly into the docker image, +## Run this as the test script in the Docker valgrind image. +## Note -- can be included directly into the Docker image, ## but requires the current python.supp. source /vpy3/bin/activate From 399b6c1045ff2387e7db8206e72baec33f996030 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 30 May 2025 10:40:07 +0100 Subject: [PATCH 1738/2374] Update Makefile Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4f63cfe020f..27d70dcb720 100644 --- a/Makefile +++ b/Makefile @@ -110,7 +110,7 @@ valgrind-leak: PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ --log-file=/tmp/valgrind-output \ - python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output Tests/ + python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output .PHONY: readme readme: From 506691729a2f9d33228f8693cdbe90418e1b321a Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 30 May 2025 10:40:35 +0100 Subject: [PATCH 1739/2374] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_pyarrow.py | 4 ++-- src/libImaging/Storage.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index 2029f96f568..8dad94fe035 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -29,7 +29,7 @@ def _test_img_equals_pyarray( px = img.load() assert px is not None if elts_per_pixel > 1 and mask is None: - # have to do element wise comparison when we're comparing + # have to do element-wise comparison when we're comparing # flattened r,g,b,a to a pixel. mask = list(range(elts_per_pixel)) for x in range(0, img.size[0], int(img.size[0] / 10)): @@ -56,7 +56,7 @@ def _test_img_equals_int32_pyarray( px = img.load() assert px is not None if mask is None: - # have to do element wise comparison when we're comparing + # have to do element-wise comparison when we're comparing # flattened rgba in an uint32 to a pixel. mask = list(range(elts_per_pixel)) for x in range(0, img.size[0], int(img.size[0] / 10)): diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 1a9171a0cbb..6f0a1bfa3a1 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -757,7 +757,7 @@ ImagingNewArrow( if (strcmp(schema->format, "C") == 0 // uint8 && im->pixelsize == 4 // storage as 32 bpc && schema->n_children == 0 // make sure schema is well formed. - && strcmp(im->arrow_band_format, "C") == 0 // Expected Format + && strcmp(im->arrow_band_format, "C") == 0 // expected format && 4 * pixels == external_array->length) { // expected length // single flat array, interleaved storage. if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) { From 3944db288a5b54ea6171fd1334e517fbcc3c9136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=93=E9=BC=A0?= Date: Sat, 31 May 2025 09:10:45 +0800 Subject: [PATCH 1740/2374] Update MinGW package names (#8987) --- docs/installation/building-from-source.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index c72568b208e..8988a92ce36 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -194,9 +194,9 @@ Many of Pillow's features require external libraries: pacman -S \ mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-python3 \ - mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools + mingw-w64-x86_64-python \ + mingw-w64-x86_64-python-pip \ + mingw-w64-x86_64-python-setuptools Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: From bc4138f1692d718ec9fe7b3b7449dc20d0e2d85e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 May 2025 11:48:49 +1000 Subject: [PATCH 1741/2374] ubuntu-latest now uses Ubuntu 24.04 --- docs/installation/platform-support.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 93486d034c8..1071380fdf3 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -42,11 +42,13 @@ These platforms are built and tested for every change. | macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | | | PyPy3 | | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, 3.13, PyPy3 | | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, arm64v8, | -| | | ppc64le, s390x | +| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, 3.13, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.12 | arm64v8, ppc64le, | +| | | s390x | +----------------------------------+----------------------------+---------------------+ | Windows Server 2019 | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ From 9327e425ba77523ec9d98eb9558806ecf29b9365 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 May 2025 12:02:16 +1000 Subject: [PATCH 1742/2374] Stop testing deprecated Windows Server 2019 --- .github/workflows/test-windows.yml | 5 ++--- docs/installation/platform-support.rst | 6 +++--- winbuild/README.md | 3 +-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index bfa4c7cd3d6..6b76351b001 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -31,16 +31,15 @@ env: jobs: build: - runs-on: ${{ matrix.os }} + runs-on: windows-latest strategy: fail-fast: false matrix: python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"] architecture: ["x64"] - os: ["windows-latest"] include: # Test the oldest Python on 32-bit - - { python-version: "3.9", architecture: "x86", os: "windows-2019" } + - { python-version: "3.9", architecture: "x86" } timeout-minutes: 45 diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 93486d034c8..f262d861c07 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -48,9 +48,9 @@ These platforms are built and tested for every change. | Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, arm64v8, | | | | ppc64le, s390x | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2019 | 3.9 | x86 | -+----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 | +| Windows Server 2022 | 3.9 | x86 | +| +----------------------------+---------------------+ +| | 3.10, 3.11, 3.12, 3.13, | x86-64 | | | PyPy3 | | | +----------------------------+---------------------+ | | 3.12 (MinGW) | x86-64 | diff --git a/winbuild/README.md b/winbuild/README.md index c474f12ceee..0d3ec8d8aa4 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -11,8 +11,7 @@ For more extensive info, see the [Windows build instructions](build.rst). * Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires NASM for libjpeg-turbo, a required dependency when using this script. * Requires CMake 3.15 or newer (available as Visual Studio component). -* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise and Windows Server - 2019 with Visual Studio 2019 Enterprise (GitHub Actions). +* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). Here's an example script to build on Windows: From 2059e060051acd2024360834da435a266d9dc665 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 30 May 2025 09:56:47 +0100 Subject: [PATCH 1743/2374] Add parallel compile from pybind11 --- pyproject.toml | 1 + setup.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 683ab24ef07..ae4b70990bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [build-system] build-backend = "backend" requires = [ + "pybind11", "setuptools>=77", ] backend-path = [ diff --git a/setup.py b/setup.py index ab36c6b1783..ec6b47b1c8c 100644 --- a/setup.py +++ b/setup.py @@ -18,9 +18,12 @@ from collections.abc import Iterator from typing import Any +from pybind11.setup_helpers import ParallelCompile from setuptools import Extension, setup from setuptools.command.build_ext import build_ext +ParallelCompile("MAX_CONCURRENCY", default=0).install() + def get_version() -> str: version_file = "src/PIL/_version.py" @@ -1048,12 +1051,12 @@ def debug_build() -> bool: Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), ] - # parse configuration from _custom_build/backend.py while sys.argv[-1].startswith("--pillow-configuration="): _, key, value = sys.argv.pop().split("=", 2) configuration.setdefault(key, []).append(value) + try: setup( cmdclass={"build_ext": pil_build_ext}, From b931402046f840bedc09b3c2b0c4039ac28531dc Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Sat, 31 May 2025 15:14:17 +0200 Subject: [PATCH 1744/2374] add pybind11 elsewhere so mypy can find it --- .ci/requirements-mypy.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 86ac2e0b26b..645605aa67a 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -5,6 +5,7 @@ ipython numpy packaging pyarrow-stubs +pybind11 pytest sphinx types-atheris From 892fd2c2affa4980121a059bc2b7875834571804 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 1 Jun 2025 15:41:48 +1000 Subject: [PATCH 1745/2374] Removed unreachable code (#8918) --- src/PIL/MpegImagePlugin.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index 5aa00d05ba6..47ebe9d62c4 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -33,11 +33,7 @@ def next(self) -> int: def peek(self, bits: int) -> int: while self.bits < bits: - c = self.next() - if c < 0: - self.bits = 0 - continue - self.bitbuffer = (self.bitbuffer << 8) + c + self.bitbuffer = (self.bitbuffer << 8) + self.next() self.bits += 8 return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 From 95603e9717c81d3492933c3a8d094bfbb7e90340 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 2 Jun 2025 20:14:11 +1000 Subject: [PATCH 1746/2374] Use ImageFile.MAXBLOCK in tobytes() (#8906) --- src/PIL/Image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index aaa3332ee2e..ed2f728aab5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -802,7 +802,9 @@ def tobytes(self, encoder_name: str = "raw", *args: Any) -> bytes: e = _getencoder(self.mode, encoder_name, encoder_args) e.setimage(self.im) - bufsize = max(65536, self.size[0] * 4) # see RawEncode.c + from . import ImageFile + + bufsize = max(ImageFile.MAXBLOCK, self.size[0] * 4) # see RawEncode.c output = [] while True: From 070e1eba626736a5cfa4a90a8a97dfbbf6278b91 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:08:24 +1000 Subject: [PATCH 1747/2374] [pre-commit.ci] pre-commit autoupdate (#8993) --- .pre-commit-config.yaml | 8 ++++---- src/_imaging.c | 9 +++++---- src/display.c | 8 ++++---- src/libImaging/Fill.c | 5 +++-- src/libImaging/Filter.c | 6 ++++-- src/libImaging/Jpeg2KEncode.c | 8 ++++---- src/libImaging/Point.c | 5 +++-- src/libImaging/Resample.c | 6 ++++-- src/libImaging/Storage.c | 5 +++-- src/libImaging/TiffDecode.c | 3 ++- 10 files changed, 36 insertions(+), 27 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e15e6f6390a..a1a054e008f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.8 + rev: v0.11.12 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.3 + rev: v20.1.5 hooks: - id: clang-format types: [c] @@ -58,7 +58,7 @@ repos: - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.6.0 + rev: v1.9.0 hooks: - id: zizmor @@ -68,7 +68,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.5.1 + rev: v2.6.0 hooks: - id: pyproject-fmt diff --git a/src/_imaging.c b/src/_imaging.c index 79e0a2b2321..9213ba13dff 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -308,9 +308,9 @@ _new_arrow(PyObject *self, PyObject *args) { } // ImagingBorrowArrow is responsible for retaining the array_capsule - ret = - PyImagingNew(ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule) - ); + ret = PyImagingNew( + ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule) + ); if (!ret) { return ImagingError_ValueError("Invalid Arrow array mode or size mismatch"); } @@ -1665,7 +1665,8 @@ _putdata(ImagingObject *self, PyObject *args) { int bigendian = 0; if (image->type == IMAGING_TYPE_SPECIAL) { // I;16* - if (strcmp(image->mode, "I;16B") == 0 + if ( + strcmp(image->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(image->mode, "I;16N") == 0 #endif diff --git a/src/display.c b/src/display.c index 11742a8952f..3215f6691ee 100644 --- a/src/display.c +++ b/src/display.c @@ -327,11 +327,11 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { // added in Windows 10 (1607) // loaded dynamically to avoid link errors user32 = LoadLibraryA("User32.dll"); - SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext - )GetProcAddress(user32, "SetThreadDpiAwarenessContext"); + SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext) + GetProcAddress(user32, "SetThreadDpiAwarenessContext"); if (SetThreadDpiAwarenessContext_function != NULL) { - GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext - )GetProcAddress(user32, "GetWindowDpiAwarenessContext"); + GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext) + GetProcAddress(user32, "GetWindowDpiAwarenessContext"); if (screens == -1 && GetWindowDpiAwarenessContext_function != NULL) { dpiAwareness = GetWindowDpiAwarenessContext_function(wnd); } diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index 8fb481e7e4d..28f42737053 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -118,8 +118,9 @@ ImagingFillRadialGradient(const char *mode) { for (y = 0; y < 256; y++) { for (x = 0; x < 256; x++) { - d = (int - )sqrt((double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0); + d = (int)sqrt( + (double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0 + ); if (d >= 255) { d = 255; } diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 7b7b2e4292d..c46dd3cd1cd 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -155,7 +155,8 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { } else { int bigendian = 0; if (im->type == IMAGING_TYPE_SPECIAL) { - if (strcmp(im->mode, "I;16B") == 0 + if ( + strcmp(im->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(im->mode, "I;16N") == 0 #endif @@ -308,7 +309,8 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { } else { int bigendian = 0; if (im->type == IMAGING_TYPE_SPECIAL) { - if (strcmp(im->mode, "I;16B") == 0 + if ( + strcmp(im->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(im->mode, "I;16N") == 0 #endif diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 34d1a22949c..61e095ad67d 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -207,8 +207,8 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { if (params->cp_cinema == OPJ_CINEMA4K_24) { float max_rate = - ((float)(components * im->xsize * im->ysize * 8) / (CINEMA_24_CS_LENGTH * 8) - ); + ((float)(components * im->xsize * im->ysize * 8) / + (CINEMA_24_CS_LENGTH * 8)); params->POC[0].tile = 1; params->POC[0].resno0 = 0; @@ -243,8 +243,8 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { params->max_comp_size = COMP_24_CS_MAX_LENGTH; } else { float max_rate = - ((float)(components * im->xsize * im->ysize * 8) / (CINEMA_48_CS_LENGTH * 8) - ); + ((float)(components * im->xsize * im->ysize * 8) / + (CINEMA_48_CS_LENGTH * 8)); for (n = 0; n < params->tcp_numlayers; ++n) { rate = 0; diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index 6a4060b4bc1..b11ea62ed85 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -197,8 +197,9 @@ ImagingPoint(Imaging imIn, const char *mode, const void *table) { return imOut; mode_mismatch: - return (Imaging - )ImagingError_ValueError("point operation not supported for this mode"); + return (Imaging)ImagingError_ValueError( + "point operation not supported for this mode" + ); } Imaging diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index f5e386dc2ba..b114e002330 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -470,7 +470,8 @@ ImagingResampleHorizontal_16bpc( double *k; int bigendian = 0; - if (strcmp(imIn->mode, "I;16N") == 0 + if ( + strcmp(imIn->mode, "I;16N") == 0 #ifdef WORDS_BIGENDIAN || strcmp(imIn->mode, "I;16B") == 0 #endif @@ -509,7 +510,8 @@ ImagingResampleVertical_16bpc( double *k; int bigendian = 0; - if (strcmp(imIn->mode, "I;16N") == 0 + if ( + strcmp(imIn->mode, "I;16N") == 0 #ifdef WORDS_BIGENDIAN || strcmp(imIn->mode, "I;16B") == 0 #endif diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 6f0a1bfa3a1..11d6c06cc63 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -602,8 +602,9 @@ ImagingBorrowArrow( } if (!borrowed_buffer) { - return (Imaging - )ImagingError_ValueError("Arrow Array, exactly 2 buffers required"); + return (Imaging)ImagingError_ValueError( + "Arrow Array, exactly 2 buffers required" + ); } for (y = i = 0; y < im->ysize; y++) { diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 173eca160d2..2e83fb847c9 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -557,7 +557,8 @@ _decodeStrip( (tdata_t)state->buffer, strip_size ) == -1) { - TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0)) + TRACE( + ("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0)) ); state->errcode = IMAGING_CODEC_BROKEN; return -1; From fa7413904b4eee630401c31994eeb5bcda2441a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Jun 2025 14:13:22 +1000 Subject: [PATCH 1748/2374] Updated ruff ID --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a1a054e008f..1b8fa7199dd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.12 hooks: - - id: ruff + - id: ruff-check args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror From eb0256acc082e362b4172f3256a551a412ef4b09 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Jun 2025 22:44:26 +1000 Subject: [PATCH 1749/2374] Fixed test --- Tests/test_deprecate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 82ff14181a3..88479ff0d1d 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -47,7 +47,6 @@ def test_unknown_version() -> None: ], ) def test_old_version(deprecated: str, plural: bool, expected: str) -> None: - expected = r"" with pytest.raises(RuntimeError, match=expected): _deprecate.deprecate(deprecated, 1, plural=plural) From cb077a16c80e9d23bb3976182acae7fc090aa5dc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Jun 2025 20:07:13 +1000 Subject: [PATCH 1750/2374] Handle UNDEFINED XMP data --- Tests/test_file_tiff.py | 24 ++++++++++++++++++++++++ src/PIL/TiffImagePlugin.py | 5 ++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index d0d394aa9cd..73046eb5fd4 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -14,6 +14,7 @@ ImageFile, JpegImagePlugin, TiffImagePlugin, + TiffTags, UnidentifiedImageError, ) from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION @@ -900,6 +901,29 @@ def test_getxmp(self) -> None: assert description[0]["format"] == "image/tiff" assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] + def test_getxmp_undefined(self, tmp_path: Path) -> None: + tmpfile = tmp_path / "temp.tif" + im = Image.new("L", (1, 1)) + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd.tagtype[700] = TiffTags.UNDEFINED + with Image.open("Tests/images/lab.tif") as im_xmp: + ifd[700] = im_xmp.info["xmp"] + im.save(tmpfile, tiffinfo=ifd) + + with Image.open(tmpfile) as im_reloaded: + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + assert im_reloaded.getxmp() == {} + else: + assert "xmp" in im_reloaded.info + xmp = im_reloaded.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description[0]["format"] == "image/tiff" + def test_get_photoshop_blocks(self) -> None: with Image.open("Tests/images/lab.tif") as im: assert isinstance(im, TiffImagePlugin.TiffImageFile) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 88af9162e0a..22c5208e29d 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1259,7 +1259,10 @@ def _seek(self, frame: int) -> None: self.fp.seek(self._frame_pos[frame]) self.tag_v2.load(self.fp) if XMP in self.tag_v2: - self.info["xmp"] = self.tag_v2[XMP] + xmp = self.tag_v2[XMP] + if isinstance(xmp, tuple) and len(xmp) == 1: + xmp = xmp[0] + self.info["xmp"] = xmp elif "xmp" in self.info: del self.info["xmp"] self._reload_exif() From f03c23683ed83a9d8f73e73073ac28f1ab2b74ea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Jun 2025 20:08:58 +1000 Subject: [PATCH 1751/2374] Trim whitespace from end when parsing XMP data --- Tests/test_image.py | 2 +- src/PIL/Image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 14a0671277d..ac358f5bf47 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -989,7 +989,7 @@ def test_getxmp_padded(self) -> None: im = Image.new("RGB", (1, 1)) im.info["xmp"] = ( b'\n' - b'\n\x00\x00' + b'\n\x00\x00 ' ) if ElementTree is None: with pytest.warns( diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ed2f728aab5..e03e9cc8adc 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1511,7 +1511,7 @@ def get_value(element: Element) -> str | dict[str, Any] | None: return {} if "xmp" not in self.info: return {} - root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00")) + root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00 ")) return {get_name(root.tag): get_value(root)} def getexif(self) -> Exif: From 9d5ea827e4ac401b85ec0ed61b7d2e97a101b05a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 5 Jun 2025 18:16:05 +1000 Subject: [PATCH 1752/2374] Call startswith once with a tuple --- setup.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index ab36c6b1783..3716a7b9f66 100644 --- a/setup.py +++ b/setup.py @@ -163,7 +163,7 @@ def _find_library_dirs_ldconfig() -> list[str]: args: list[str] env: dict[str, str] expr: str - if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): + if sys.platform.startswith(("linux", "gnu")): if struct.calcsize("l") == 4: machine = os.uname()[4] + "-32" else: @@ -623,11 +623,7 @@ def build_extensions(self) -> None: for extension in self.extensions: extension.extra_compile_args = ["-Wno-nullability-completeness"] - elif ( - sys.platform.startswith("linux") - or sys.platform.startswith("gnu") - or sys.platform.startswith("freebsd") - ): + elif sys.platform.startswith(("linux", "gnu", "freebsd")): for dirname in _find_library_dirs_ldconfig(): _add_directory(library_dirs, dirname) if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"): From f3b05d6fab2a8fb0db033fc507d0d6f19ed2330e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Jun 2025 11:07:21 +1000 Subject: [PATCH 1753/2374] Update dependency mypy to v1.16.0 (#8991) Co-authored-by: Andrew Murray --- .ci/requirements-mypy.txt | 2 +- Tests/test_file_jpeg.py | 10 ++++++---- Tests/test_image.py | 1 + src/PIL/GifImagePlugin.py | 9 ++++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 86ac2e0b26b..a9c18ae2bfd 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.15.0 +mypy==1.16.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index b9eec591dcf..2827937cf57 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -145,14 +145,16 @@ def test_cmyk(self) -> None: assert k > 0.9 # roundtrip, and check again im = self.roundtrip(im) - c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) + cmyk = im.getpixel((0, 0)) + assert isinstance(cmyk, tuple) + c, m, y, k = (x / 255.0 for x in cmyk) assert c == 0.0 assert m > 0.8 assert y > 0.8 assert k == 0.0 - c, m, y, k = ( - x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) - ) + cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1)) + assert isinstance(cmyk, tuple) + k = cmyk[3] / 255.0 assert k > 0.9 def test_rgb(self) -> None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 14a0671277d..4cc8416037e 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -671,6 +671,7 @@ def test_remap_palette(self) -> None: im_remapped = im.remap_palette(list(range(256))) assert_image_equal(im, im_remapped) assert im.palette is not None + assert im_remapped.palette is not None assert im.palette.palette == im_remapped.palette.palette # Test illegal image mode diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 4392c4cb909..c98e02f69e0 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,7 +31,7 @@ import subprocess from enum import IntEnum from functools import cached_property -from typing import IO, Any, Literal, NamedTuple, Union +from typing import IO, Any, Literal, NamedTuple, Union, cast from . import ( Image, @@ -350,12 +350,15 @@ def _rgb(color: int) -> tuple[int, int, int]: if self._frame_palette: if color * 3 + 3 > len(self._frame_palette.palette): color = 0 - return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) + return cast( + tuple[int, int, int], + tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]), + ) else: return (color, color, color) self.dispose = None - self.dispose_extent = frame_dispose_extent + self.dispose_extent: tuple[int, int, int, int] | None = frame_dispose_extent if self.dispose_extent and self.disposal_method >= 2: try: if self.disposal_method == 2: From 0d1edba311ffdc9683c6541a5133c60d425debe8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Apr 2025 14:16:49 +1000 Subject: [PATCH 1754/2374] Assert tile args is tuple --- Tests/test_file_gif.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 20d58a9dda4..2712e683ccd 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1422,7 +1422,9 @@ def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None: def test_lzw_bits() -> None: # see https://github.com/python-pillow/Pillow/issues/2811 with Image.open("Tests/images/issue_2811.gif") as im: - assert im.tile[0][3][0] == 11 # LZW bits + args = im.tile[0][3] + assert isinstance(args, tuple) + assert args[0] == 11 # LZW bits # codec error prepatch im.load() From 33460d2f82ee148eff2451cfe7e8bc4b6f33b66a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Apr 2025 14:22:02 +1000 Subject: [PATCH 1755/2374] Assert _getmp() does not return None --- Tests/test_file_mpo.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 73838ef4484..462c955355f 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -156,6 +156,7 @@ def test_reload_exif_after_seek() -> None: def test_mp(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() + assert mpinfo is not None assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 @@ -165,6 +166,7 @@ def test_mp_offset() -> None: # in APP2 data, in contrast to normal 8 with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: mpinfo = im._getmp() + assert mpinfo is not None assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 @@ -181,6 +183,7 @@ def test_mp_no_data() -> None: def test_mp_attribute(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() + assert mpinfo is not None for frame_number, mpentry in enumerate(mpinfo[0xB002]): mpattr = mpentry["Attribute"] if frame_number: From cba096b4a99f92eb865c7fb127b9783dbd38643e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Jun 2025 11:13:12 +1000 Subject: [PATCH 1756/2374] Assert pixel data is tuple --- Tests/test_file_gif.py | 8 ++++++-- Tests/test_file_jpeg.py | 10 ++++++---- Tests/test_file_tga.py | 8 ++++++-- Tests/test_file_webp.py | 2 ++ 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 2712e683ccd..f46a28971dc 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -540,7 +540,9 @@ def test_dispose_background_transparency() -> None: img.seek(2) px = img.load() assert px is not None - assert px[35, 30][3] == 0 + value = px[35, 30] + assert isinstance(value, tuple) + assert value[3] == 0 @pytest.mark.parametrize( @@ -1479,7 +1481,9 @@ def test_saving_rgba(tmp_path: Path) -> None: with Image.open(out) as reloaded: reloaded_rgba = reloaded.convert("RGBA") - assert reloaded_rgba.load()[0, 0][3] == 0 + value = reloaded_rgba.load()[0, 0] + assert isinstance(value, tuple) + assert value[3] == 0 @pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False})) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 2827937cf57..50ee046114c 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -133,15 +133,17 @@ def test_cmyk(self) -> None: f = "Tests/images/pil_sample_cmyk.jpg" with Image.open(f) as im: # the source image has red pixels in the upper left corner. - c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) + cmyk = im.getpixel((0, 0)) + assert isinstance(cmyk, tuple) + c, m, y, k = (x / 255.0 for x in cmyk) assert c == 0.0 assert m > 0.8 assert y > 0.8 assert k == 0.0 # the opposite corner is black - c, m, y, k = ( - x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) - ) + cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1)) + assert isinstance(cmyk, tuple) + k = cmyk[3] / 255.0 assert k > 0.9 # roundtrip, and check again im = self.roundtrip(im) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 8b6ed3ed226..d3cceb37f25 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -220,12 +220,16 @@ def test_horizontal_orientations() -> None: with Image.open("Tests/images/rgb32rle_top_right.tga") as im: px = im.load() assert px is not None - assert px[90, 90][:3] == (0, 0, 0) + value = px[90, 90] + assert isinstance(value, tuple) + assert value[:3] == (0, 0, 0) with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im: px = im.load() assert px is not None - assert px[90, 90][:3] == (0, 255, 0) + value = px[90, 90] + assert isinstance(value, tuple) + assert value[:3] == (0, 255, 0) def test_save_rle(tmp_path: Path) -> None: diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index f61e2c82ee5..4ea7629d1d5 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -219,6 +219,7 @@ def test_background_from_gif(self, tmp_path: Path) -> None: # Save P mode GIF with background with Image.open("Tests/images/chi.gif") as im: original_value = im.convert("RGB").getpixel((1, 1)) + assert isinstance(original_value, tuple) # Save as WEBP im.save(out_webp, save_all=True) @@ -230,6 +231,7 @@ def test_background_from_gif(self, tmp_path: Path) -> None: with Image.open(out_gif) as reread: reread_value = reread.convert("RGB").getpixel((1, 1)) + assert isinstance(reread_value, tuple) difference = sum(abs(original_value[i] - reread_value[i]) for i in range(3)) assert difference < 5 From a3da70e76e14da1bc0e5b1c2331d746e4a095a6b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Apr 2025 14:23:58 +1000 Subject: [PATCH 1757/2374] Assert load() does not return None --- Tests/test_file_gif.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index f46a28971dc..e418af45ce7 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1481,7 +1481,9 @@ def test_saving_rgba(tmp_path: Path) -> None: with Image.open(out) as reloaded: reloaded_rgba = reloaded.convert("RGBA") - value = reloaded_rgba.load()[0, 0] + px = reloaded_rgba.load() + assert px is not None + value = px[0, 0] assert isinstance(value, tuple) assert value[3] == 0 From 89c38258dc33fd410858c6b760d533789578e15e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Apr 2025 14:24:20 +1000 Subject: [PATCH 1758/2374] Assert getcolors() does not return None --- Tests/test_file_avif.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index b2e586637fa..6d0cc74f9f9 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -254,7 +254,9 @@ def test_load_transparent_rgb(self) -> None: assert_image(im, "RGBA", (64, 64)) # image has 876 transparent pixels - assert im.getchannel("A").getcolors()[0] == (876, 0) + colors = im.getchannel("A").getcolors() + assert colors is not None + assert colors[0] == (876, 0) def test_save_transparent(self, tmp_path: Path) -> None: im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) From 04c984f2f202639479a161dc44ba378b9ebee931 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Jun 2025 11:29:11 +1000 Subject: [PATCH 1759/2374] Removed duplicate code --- Tests/test_file_jpeg.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 50ee046114c..00f2c004d46 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -130,9 +130,7 @@ def test_comment_write(self) -> None: def test_cmyk(self) -> None: # Test CMYK handling. Thanks to Tim and Charlie for test data, # Michael for getting me to look one more time. - f = "Tests/images/pil_sample_cmyk.jpg" - with Image.open(f) as im: - # the source image has red pixels in the upper left corner. + def check(im: ImageFile.ImageFile) -> None: cmyk = im.getpixel((0, 0)) assert isinstance(cmyk, tuple) c, m, y, k = (x / 255.0 for x in cmyk) @@ -145,19 +143,13 @@ def test_cmyk(self) -> None: assert isinstance(cmyk, tuple) k = cmyk[3] / 255.0 assert k > 0.9 + + with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: + # the source image has red pixels in the upper left corner. + check(im) + # roundtrip, and check again - im = self.roundtrip(im) - cmyk = im.getpixel((0, 0)) - assert isinstance(cmyk, tuple) - c, m, y, k = (x / 255.0 for x in cmyk) - assert c == 0.0 - assert m > 0.8 - assert y > 0.8 - assert k == 0.0 - cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1)) - assert isinstance(cmyk, tuple) - k = cmyk[3] / 255.0 - assert k > 0.9 + check(self.roundtrip(im)) def test_rgb(self) -> None: def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]: From 0bb99e5561e1e2b87f94d6ae30f07eba1744421c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Jun 2025 15:08:16 +1000 Subject: [PATCH 1760/2374] Use save parameters as encoderinfo defaults --- Tests/test_file_mpo.py | 20 +++++++++++++++----- Tests/test_file_tiff.py | 13 +++++++++---- src/PIL/Image.py | 9 ++++++++- src/PIL/MpoImagePlugin.py | 1 + src/PIL/TiffImagePlugin.py | 3 +-- 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 73838ef4484..c192f017f70 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -312,10 +312,20 @@ def test_save_all() -> None: def test_save_xmp() -> None: im = Image.new("RGB", (1, 1)) im2 = Image.new("RGB", (1, 1), "#f00") - im2.encoderinfo = {"xmp": b"Second frame"} - im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2]) - assert im_reloaded.info["xmp"] == b"First frame" + def roundtrip_xmp(): + im_reloaded = roundtrip(im, xmp=b"Default", save_all=True, append_images=[im2]) + xmp = [im_reloaded.info["xmp"]] + im_reloaded.seek(1) + return xmp + [im_reloaded.info["xmp"]] - im_reloaded.seek(1) - assert im_reloaded.info["xmp"] == b"Second frame" + # Use the save parameters for all frames by default + assert roundtrip_xmp() == [b"Default", b"Default"] + + # Specify a value for the first frame + im.encoderinfo = {"xmp": b"First frame"} + assert roundtrip_xmp() == [b"First frame", b"Default"] + + # Specify value for the second frame + im2.encoderinfo = {"xmp": b"Second frame"} + assert roundtrip_xmp() == [b"Default", b"Second frame"] diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index d0d394aa9cd..d192e9685dd 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -695,16 +695,21 @@ def test_rowsperstrip(self, tmp_path: Path) -> None: assert im.tag_v2[278] == 256 im = hopper() + im.encoderinfo = {"tiffinfo": {278: 100}} im2 = Image.new("L", (128, 128)) - im2.encoderinfo = {"tiffinfo": {278: 256}} - im.save(outfile, save_all=True, append_images=[im2]) + im3 = im2.copy() + im3.encoderinfo = {"tiffinfo": {278: 300}} + im.save(outfile, save_all=True, tiffinfo={278: 200}, append_images=[im2, im3]) with Image.open(outfile) as im: assert isinstance(im, TiffImagePlugin.TiffImageFile) - assert im.tag_v2[278] == 128 + assert im.tag_v2[278] == 100 im.seek(1) - assert im.tag_v2[278] == 256 + assert im.tag_v2[278] == 200 + + im.seek(2) + assert im.tag_v2[278] == 300 def test_strip_raw(self) -> None: infile = "Tests/images/tiff_strip_raw.tif" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ed2f728aab5..7a8d937936a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2555,7 +2555,8 @@ def save( self.load() save_all = params.pop("save_all", None) - self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} + self._default_encoderinfo = params + self._attach_default_encoderinfo(self) self.encoderconfig: tuple[Any, ...] = () if format.upper() not in SAVE: @@ -2600,6 +2601,12 @@ def save( if open_fp: fp.close() + def _attach_default_encoderinfo(self, im: Image) -> Any: + self.encoderinfo = { + **im._default_encoderinfo, + **getattr(self, "encoderinfo", {}), + } + def seek(self, frame: int) -> None: """ Seeks to the given frame in this sequence file. If you seek diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index f7393eac059..f1f44c0ff0e 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -64,6 +64,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: JpegImagePlugin._save(im_frame, fp, filename) offsets.append(fp.tell()) else: + im_frame._attach_default_encoderinfo(im) im_frame.save(fp, "JPEG") offsets.append(fp.tell() - offsets[-1]) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 88af9162e0a..e1b10fea501 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2310,8 +2310,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: try: with AppendingTiffWriter(fp) as tf: for ims in [im] + append_images: - if not hasattr(ims, "encoderinfo"): - ims.encoderinfo = {} + ims._attach_default_encoderinfo(im) if not hasattr(ims, "encoderconfig"): ims.encoderconfig = () nfr = getattr(ims, "n_frames", 1) From ef1f90fe1c92ec4d038ddf4d03638f467ba94181 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 9 Jun 2025 09:06:08 +1000 Subject: [PATCH 1761/2374] Check for equality rather than inequality Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/ImageGrab.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index d11609483d0..1eb4507344c 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -134,7 +134,10 @@ def grabclipboard() -> Image.Image | list[str] | None: import struct o = struct.unpack_from("I", data)[0] - files = data[o:].decode("mbcs" if data[16] == 0 else "utf-16le").split("\0") + if data[16] == 0: + files = data[o:].decode("mbcs").split("\0") + else: + files = data[o:].decode("utf-16le").split("\0") return files[: files.index("")] if isinstance(data, bytes): data = io.BytesIO(data) From 313969cf0bcf6b6185d486830478d2864eb56fe1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 9 Jun 2025 12:21:49 +1000 Subject: [PATCH 1762/2374] Removed unnecessary seek --- src/PIL/PcxImagePlugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 299405ae0a5..47b6e80e2df 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -66,6 +66,8 @@ def _open(self) -> None: raise SyntaxError(msg) logger.debug("BBox: %s %s %s %s", *bbox) + offset = self.fp.tell() + # format version = s[1] bits = s[3] @@ -102,7 +104,6 @@ def _open(self) -> None: break if mode == "P": self.palette = ImagePalette.raw("RGB", s[1:]) - self.fp.seek(128) elif version == 5 and bits == 8 and planes == 3: mode = "RGB" @@ -128,9 +129,7 @@ def _open(self) -> None: bbox = (0, 0) + self.size logger.debug("size: %sx%s", *self.size) - self.tile = [ - ImageFile._Tile("pcx", bbox, self.fp.tell(), (rawmode, planes * stride)) - ] + self.tile = [ImageFile._Tile("pcx", bbox, offset, (rawmode, planes * stride))] # -------------------------------------------------------------------- From 7341e70f6be9c3e910c81f563bb7900167873c02 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 9 Jun 2025 12:20:52 +1000 Subject: [PATCH 1763/2374] Reduced number of bytes read for header --- src/PIL/PcxImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 47b6e80e2df..458d586c463 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -54,7 +54,7 @@ def _open(self) -> None: # header assert self.fp is not None - s = self.fp.read(128) + s = self.fp.read(68) if not _accept(s): msg = "not a PCX file" raise SyntaxError(msg) @@ -66,7 +66,7 @@ def _open(self) -> None: raise SyntaxError(msg) logger.debug("BBox: %s %s %s %s", *bbox) - offset = self.fp.tell() + offset = self.fp.tell() + 60 # format version = s[1] From 7b163cc35d3ef9bb0204613add92f05eda65ab63 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:46:12 +1000 Subject: [PATCH 1764/2374] Use mask in C when drawing wide polygon lines (#8984) --- src/PIL/ImageDraw.py | 16 +----- src/_imaging.c | 20 +++++-- src/libImaging/Draw.c | 113 ++++++++++++++++++++++++++++----------- src/libImaging/Imaging.h | 19 ++++++- 4 files changed, 116 insertions(+), 52 deletions(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e6c7b029830..98ae67539ab 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -365,22 +365,10 @@ def polygon( # use the fill as a mask mask = Image.new("1", self.im.size) mask_ink = self._getink(1)[0] - - fill_im = mask.copy() - draw = Draw(fill_im) + draw = Draw(mask) draw.draw.draw_polygon(xy, mask_ink, 1) - ink_im = mask.copy() - draw = Draw(ink_im) - width = width * 2 - 1 - draw.draw.draw_polygon(xy, mask_ink, 0, width) - - mask.paste(ink_im, mask=fill_im) - - im = Image.new(self.mode, self.im.size) - draw = Draw(im) - draw.draw.draw_polygon(xy, ink, 0, width) - self.im.paste(im.im, (0, 0) + im.size, mask.im) + self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im) def regular_polygon( self, diff --git a/src/_imaging.c b/src/_imaging.c index 9213ba13dff..2a7bc8d3f5e 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3220,7 +3220,8 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) { (int)p[3], &ink, width, - self->blend + self->blend, + NULL ) < 0) { free(xy); return NULL; @@ -3358,7 +3359,10 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { int ink; int fill = 0; int width = 0; - if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { + ImagingObject *maskp = NULL; + if (!PyArg_ParseTuple( + args, "Oi|iiO!", &data, &ink, &fill, &width, &Imaging_Type, &maskp + )) { return NULL; } @@ -3388,8 +3392,16 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { free(xy); - if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) < - 0) { + if (ImagingDrawPolygon( + self->image->image, + n, + ixy, + &ink, + fill, + width, + self->blend, + maskp ? maskp->image : NULL + ) < 0) { free(ixy); return NULL; } diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 4c08e985504..70f267ae4a9 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -63,7 +63,7 @@ typedef struct { } Edge; /* Type used in "polygon*" functions */ -typedef void (*hline_handler)(Imaging, int, int, int, int); +typedef void (*hline_handler)(Imaging, int, int, int, int, Imaging); static inline void point8(Imaging im, int x, int y, int ink) { @@ -103,7 +103,7 @@ point32rgba(Imaging im, int x, int y, int ink) { } static inline void -hline8(Imaging im, int x0, int y0, int x1, int ink) { +hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { int pixelwidth; if (y0 >= 0 && y0 < im->ysize) { @@ -119,15 +119,30 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) { } if (x0 <= x1) { pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1; - memset( - im->image8[y0] + x0 * pixelwidth, (UINT8)ink, (x1 - x0 + 1) * pixelwidth - ); + if (mask == NULL) { + memset( + im->image8[y0] + x0 * pixelwidth, + (UINT8)ink, + (x1 - x0 + 1) * pixelwidth + ); + } else { + UINT8 *p = im->image8[y0]; + while (x0 <= x1) { + if (mask->image8[y0][x0]) { + p[x0 * pixelwidth] = ink; + if (pixelwidth == 2) { + p[x0 * pixelwidth + 1] = ink; + } + } + x0++; + } + } } } } static inline void -hline32(Imaging im, int x0, int y0, int x1, int ink) { +hline32(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { INT32 *p; if (y0 >= 0 && y0 < im->ysize) { @@ -143,13 +158,16 @@ hline32(Imaging im, int x0, int y0, int x1, int ink) { } p = im->image32[y0]; while (x0 <= x1) { - p[x0++] = ink; + if (mask == NULL || mask->image8[y0][x0]) { + p[x0] = ink; + } + x0++; } } } static inline void -hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { +hline32rgba(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { unsigned int tmp; if (y0 >= 0 && y0 < im->ysize) { @@ -167,9 +185,11 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { UINT8 *out = (UINT8 *)im->image[y0] + x0 * 4; UINT8 *in = (UINT8 *)&ink; while (x0 <= x1) { - out[0] = BLEND(in[3], out[0], in[0], tmp); - out[1] = BLEND(in[3], out[1], in[1], tmp); - out[2] = BLEND(in[3], out[2], in[2], tmp); + if (mask == NULL || mask->image8[y0][x0]) { + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); + } x0++; out += 4; } @@ -407,7 +427,14 @@ x_cmp(const void *x0, const void *x1) { static void draw_horizontal_lines( - Imaging im, int n, Edge *e, int ink, int *x_pos, int y, hline_handler hline + Imaging im, + int n, + Edge *e, + int ink, + int *x_pos, + int y, + hline_handler hline, + Imaging mask ) { int i; for (i = 0; i < n; i++) { @@ -429,7 +456,7 @@ draw_horizontal_lines( } } - (*hline)(im, xmin, e[i].ymin, xmax, ink); + (*hline)(im, xmin, e[i].ymin, xmax, ink, mask); *x_pos = xmax + 1; } } @@ -439,7 +466,9 @@ draw_horizontal_lines( * Filled polygon draw function using scan line algorithm. */ static inline int -polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline) { +polygon_generic( + Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, Imaging mask +) { Edge **edge_table; float *xx; int edge_count = 0; @@ -469,7 +498,7 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h } if (e[i].ymin == e[i].ymax) { if (hasAlpha != 1) { - (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink); + (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink, mask); } continue; } @@ -557,7 +586,7 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h // Line would be before the current position continue; } - draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline, mask); if (x_end < x_pos) { // Line would be before the current position continue; @@ -573,13 +602,13 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h continue; } } - (*hline)(im, x_start, ymin, x_end, ink); + (*hline)(im, x_start, ymin, x_end, ink, mask); x_pos = x_end + 1; } - draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline, mask); } else { for (i = 1; i < j; i += 2) { - (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink); + (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink, mask); } } } @@ -623,7 +652,7 @@ add_edge(Edge *e, int x0, int y0, int x1, int y1) { typedef struct { void (*point)(Imaging im, int x, int y, int ink); - void (*hline)(Imaging im, int x0, int y0, int x1, int ink); + void (*hline)(Imaging im, int x0, int y0, int x1, int ink, Imaging mask); void (*line)(Imaging im, int x0, int y0, int x1, int y1, int ink); } DRAW; @@ -674,7 +703,15 @@ ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink_, in int ImagingDrawWideLine( - Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int width, int op + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink_, + int width, + int op, + Imaging mask ) { DRAW *draw; INT32 ink; @@ -714,7 +751,7 @@ ImagingDrawWideLine( add_edge(e + 2, vertices[2][0], vertices[2][1], vertices[3][0], vertices[3][1]); add_edge(e + 3, vertices[3][0], vertices[3][1], vertices[0][0], vertices[0][1]); - polygon_generic(im, 4, e, ink, 0, draw->hline); + polygon_generic(im, 4, e, ink, 0, draw->hline, mask); } return 0; } @@ -757,7 +794,7 @@ ImagingDrawRectangle( } for (y = y0; y <= y1; y++) { - draw->hline(im, x0, y, x1, ink); + draw->hline(im, x0, y, x1, ink, NULL); } } else { @@ -766,8 +803,8 @@ ImagingDrawRectangle( width = 1; } for (i = 0; i < width; i++) { - draw->hline(im, x0, y0 + i, x1, ink); - draw->hline(im, x0, y1 - i, x1, ink); + draw->hline(im, x0, y0 + i, x1, ink, NULL); + draw->hline(im, x0, y1 - i, x1, ink, NULL); draw->line(im, x1 - i, y0 + width, x1 - i, y1 - width + 1, ink); draw->line(im, x0 + i, y0 + width, x0 + i, y1 - width + 1, ink); } @@ -778,7 +815,14 @@ ImagingDrawRectangle( int ImagingDrawPolygon( - Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op + Imaging im, + int count, + int *xy, + const void *ink_, + int fill, + int width, + int op, + Imaging mask ) { int i, n, x0, y0, x1, y1; DRAW *draw; @@ -822,7 +866,7 @@ ImagingDrawPolygon( if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) { add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]); } - polygon_generic(im, n, e, ink, 0, draw->hline); + polygon_generic(im, n, e, ink, 0, draw->hline, mask); free(e); } else { @@ -844,11 +888,12 @@ ImagingDrawPolygon( xy[i * 2 + 3], ink_, width, - op + op, + mask ); } ImagingDrawWideLine( - im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op + im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op, mask ); } } @@ -1519,7 +1564,9 @@ ellipseNew( ellipse_init(&st, a, b, width); int32_t X0, Y, X1; while (ellipse_next(&st, &X0, &Y, &X1) != -1) { - draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); + draw->hline( + im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink, NULL + ); } return 0; } @@ -1554,7 +1601,9 @@ clipEllipseNew( int32_t X0, Y, X1; int next_code; while ((next_code = clip_ellipse_next(&st, &X0, &Y, &X1)) >= 0) { - draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); + draw->hline( + im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink, NULL + ); } clip_ellipse_free(&st); return next_code == -1 ? 0 : -1; @@ -1972,7 +2021,7 @@ ImagingDrawOutline( DRAWINIT(); - polygon_generic(im, outline->count, outline->edges, ink, 0, draw->hline); + polygon_generic(im, outline->count, outline->edges, ink, 0, draw->hline, NULL); return 0; } diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 234f9943c5a..39ecdbff63d 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -510,7 +510,15 @@ extern int ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink, int op); extern int ImagingDrawWideLine( - Imaging im, int x0, int y0, int x1, int y1, const void *ink, int width, int op + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink, + int width, + int op, + Imaging mask ); extern int ImagingDrawPieslice( @@ -530,7 +538,14 @@ extern int ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op); extern int ImagingDrawPolygon( - Imaging im, int points, int *xy, const void *ink, int fill, int width, int op + Imaging im, + int points, + int *xy, + const void *ink, + int fill, + int width, + int op, + Imaging mask ); extern int ImagingDrawRectangle( From 6bd55684e0d172749a74dd2098ebc18ce5f34fdd Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:00:08 +1000 Subject: [PATCH 1765/2374] Only accept missing tkinter when building wheels on Windows (#8981) --- Tests/check_wheel.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 8ba40ba3fbe..9602410da52 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -11,13 +11,14 @@ def test_wheel_modules() -> None: expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} - # tkinter is not available in cibuildwheel installed CPython on Windows - try: - import tkinter + if sys.platform == "win32": + # tkinter is not available in cibuildwheel installed CPython on Windows + try: + import tkinter - assert tkinter - except ImportError: - expected_modules.remove("tkinter") + assert tkinter + except ImportError: + expected_modules.remove("tkinter") assert set(features.get_supported_modules()) == expected_modules From e65e5bea45e92a118590c69c022d6e6741e3b101 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Jun 2025 20:30:18 +1000 Subject: [PATCH 1766/2374] Start decoding with a zero-initialized array of previously seen pixels --- Tests/images/op_index.qoi | Bin 0 -> 15 bytes Tests/test_file_qoi.py | 6 ++++++ src/PIL/QoiImagePlugin.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 Tests/images/op_index.qoi diff --git a/Tests/images/op_index.qoi b/Tests/images/op_index.qoi new file mode 100644 index 0000000000000000000000000000000000000000..e626aafe6a433487cbacb4bdbcbbe5e66e8d07db GIT binary patch literal 15 TcmXTS&rD-rU| None: with pytest.raises(SyntaxError): QoiImagePlugin.QoiImageFile(invalid_file) + + +def test_op_index() -> None: + # QOI_OP_INDEX as the first chunk + with Image.open("Tests/images/op_index.qoi") as im: + assert im.getpixel((0, 0)) == (0, 0, 0, 0) diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index df552243e37..75070abd740 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -51,7 +51,7 @@ def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int assert self.fd is not None self._previously_seen_pixels = {} - self._add_to_previous_pixels(bytearray((0, 0, 0, 255))) + self._previous_pixel = bytearray((0, 0, 0, 255)) data = bytearray() bands = Image.getmodebands(self.mode) From 646885e546ecd02a8162d91b51d32eed9da67b7a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:06:28 +1000 Subject: [PATCH 1767/2374] Parse XMP tag bytes without decoding to string (#8960) Co-authored-by: Andrew Murray --- Tests/test_image.py | 5 +++++ src/PIL/Image.py | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 4cc8416037e..512a52433bc 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -974,6 +974,11 @@ def test_exif_hide_offsets(self) -> None: assert tag not in exif.get_ifd(0x8769) assert exif.get_ifd(0xA005) + def test_exif_from_xmp_bytes(self) -> None: + im = Image.new("RGB", (1, 1)) + im.info["xmp"] = b'\xff tiff:Orientation="2"' + assert im.getexif()[274] == 2 + def test_empty_xmp(self) -> None: with Image.open("Tests/images/hopper.gif") as im: if ElementTree is None: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ed2f728aab5..216022565b2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1542,10 +1542,11 @@ def getexif(self) -> Exif: # XMP tags if ExifTags.Base.Orientation not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") + pattern: str | bytes = r'tiff:Orientation(="|>)([0-9])' if not xmp_tags and (xmp_tags := self.info.get("xmp")): - xmp_tags = xmp_tags.decode("utf-8") + pattern = rb'tiff:Orientation(="|>)([0-9])' if xmp_tags: - match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) + match = re.search(pattern, xmp_tags) if match: self._exif[ExifTags.Base.Orientation] = int(match[2]) From 36cea1953231d71f1184ef1396c1f01ff11c939a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:08:29 +1000 Subject: [PATCH 1768/2374] Do not decode bytes in PPM error message (#8958) --- Tests/test_file_ppm.py | 7 ++++--- src/PIL/PpmImagePlugin.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 41e2b5416c3..c7d1f4df4d6 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -288,12 +288,13 @@ def test_non_integer_token(tmp_path: Path) -> None: pass -def test_header_token_too_long(tmp_path: Path) -> None: +@pytest.mark.parametrize("data", (b"P3\x0cAAAAAAAAAA\xee", b"P6\n 01234567890")) +def test_header_token_too_long(tmp_path: Path, data: bytes) -> None: path = tmp_path / "temp.ppm" with open(path, "wb") as f: - f.write(b"P6\n 01234567890") + f.write(data) - with pytest.raises(ValueError, match="Token too long in file header: 01234567890"): + with pytest.raises(ValueError, match="Token too long in file header: "): with Image.open(path): pass diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 03afa2d2ef2..db34d107a4f 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -94,8 +94,8 @@ def _read_token(self) -> bytes: msg = "Reached EOF while reading header" raise ValueError(msg) elif len(token) > 10: - msg = f"Token too long in file header: {token.decode()}" - raise ValueError(msg) + msg_too_long = b"Token too long in file header: %s" % token + raise ValueError(msg_too_long) return token def _open(self) -> None: From d7a45cc250f8ae35ee8095753eff0cad1c9f8216 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:57:37 +1000 Subject: [PATCH 1769/2374] ImageFont does not handle multiline text (#9000) --- docs/reference/ImageFont.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 8b2f923234b..aac55fe6b05 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -18,6 +18,9 @@ OpenType fonts (as well as other font formats supported by the FreeType library). For earlier versions, TrueType support is only available as part of the imToolkit package. +When measuring text sizes, this module will not break at newline characters. For +multiline text, see the :py:mod:`~PIL.ImageDraw` module. + .. warning:: To protect against potential DOS attacks when using arbitrary strings as text input, Pillow will raise a :py:exc:`ValueError` if the number of characters From 056dc89a3c85cbd6d6c960cbfc5aaa52f996bd3d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 22:12:40 +1000 Subject: [PATCH 1770/2374] Correct drawing I;16 horizontal lines (#8985) --- Tests/images/imagedraw_rectangle_I.tiff | Bin 20122 -> 20122 bytes Tests/test_imagedraw.py | 3 ++- src/libImaging/Draw.c | 34 +++++++++++++++--------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Tests/images/imagedraw_rectangle_I.tiff b/Tests/images/imagedraw_rectangle_I.tiff index 9b9eda883a371d9cc88b4677b09d2e351c42e609..f0cb534b63e47c940ecb6c3323cb9de3dce573d4 100644 GIT binary patch literal 20122 zcmeI&F%AJy7=_V)j0hbKjY4fF8mq7hdz`h*7CbV=ls6F~a){*Rweqr`|14bRhJA-KYQ None: draw = ImageDraw.Draw(im) # Act - draw.rectangle(bbox, outline=0xFFFF) + draw.rectangle(bbox, outline=0xCDEF) # Assert + assert im.getpixel((X0, Y0)) == 0xCDEF assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 70f267ae4a9..27cac687e5d 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -104,8 +104,6 @@ point32rgba(Imaging im, int x, int y, int ink) { static inline void hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { - int pixelwidth; - if (y0 >= 0 && y0 < im->ysize) { if (x0 < 0) { x0 = 0; @@ -118,20 +116,30 @@ hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { x1 = im->xsize - 1; } if (x0 <= x1) { - pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1; - if (mask == NULL) { - memset( - im->image8[y0] + x0 * pixelwidth, - (UINT8)ink, - (x1 - x0 + 1) * pixelwidth - ); + int bigendian = -1; + if (strncmp(im->mode, "I;16", 4) == 0) { + bigendian = + ( +#ifdef WORDS_BIGENDIAN + strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16L") == 0 +#else + strcmp(im->mode, "I;16B") == 0 +#endif + ) + ? 1 + : 0; + } + if (mask == NULL && bigendian == -1) { + memset(im->image8[y0] + x0, (UINT8)ink, (x1 - x0 + 1)); } else { UINT8 *p = im->image8[y0]; while (x0 <= x1) { - if (mask->image8[y0][x0]) { - p[x0 * pixelwidth] = ink; - if (pixelwidth == 2) { - p[x0 * pixelwidth + 1] = ink; + if (mask == NULL || mask->image8[y0][x0]) { + if (bigendian == -1) { + p[x0] = ink; + } else { + p[x0 * 2 + (bigendian ? 1 : 0)] = ink; + p[x0 * 2 + (bigendian ? 0 : 1)] = ink >> 8; } } x0++; From 3eb893f0c16c958962758c03a597454aacac8f84 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 11 Jun 2025 20:56:28 +1000 Subject: [PATCH 1771/2374] Updated libjpeg-turbo to 3.1.1 (#9009) --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 1583435c13f..b46811f5a01 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -40,7 +40,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=11.2.1 LIBPNG_VERSION=1.6.48 -JPEGTURBO_VERSION=3.1.0 +JPEGTURBO_VERSION=3.1.1 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 TIFF_VERSION=4.7.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 6e176e29cf3..0cc383733b7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -114,7 +114,7 @@ def cmd_msbuild( "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", "HARFBUZZ": "11.2.1", - "JPEGTURBO": "3.1.0", + "JPEGTURBO": "3.1.1", "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.3.4", From 8ccdc399df1254c89bdb4e8fda6d6daf98943ab6 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 11 Jun 2025 23:19:09 +1000 Subject: [PATCH 1772/2374] Remove padding between interleaved PCX palette data (#9005) --- Tests/images/p_4_planes.pcx | Bin 0 -> 136 bytes Tests/test_file_pcx.py | 5 +++++ src/libImaging/PcxDecode.c | 22 ++++++++++++++++------ 3 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 Tests/images/p_4_planes.pcx diff --git a/Tests/images/p_4_planes.pcx b/Tests/images/p_4_planes.pcx new file mode 100644 index 0000000000000000000000000000000000000000..8c5743a98554c89fa46188308332461a4aa87f91 GIT binary patch literal 136 ocmd;LWn^T4f)s`n7?XkFKZ1#u#lpnE2!?o7;goD(XaLIr0O6ei;s5{u literal 0 HcmV?d00001 diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 5d7fd1c1bb4..2e999eff6e1 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -37,6 +37,11 @@ def test_sanity(tmp_path: Path) -> None: im.save(f) +def test_p_4_planes() -> None: + with Image.open("Tests/images/p_4_planes.pcx") as im: + assert im.getpixel((0, 0)) == 3 + + def test_bad_image_size() -> None: with open("Tests/images/pil184.pcx", "rb") as fp: data = fp.read() diff --git a/src/libImaging/PcxDecode.c b/src/libImaging/PcxDecode.c index 942c8dc224d..a65952fb1da 100644 --- a/src/libImaging/PcxDecode.c +++ b/src/libImaging/PcxDecode.c @@ -60,15 +60,25 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt } if (state->x >= state->bytes) { - if (state->bytes % state->xsize && state->bytes > state->xsize) { - int bands = state->bytes / state->xsize; - int stride = state->bytes / bands; + int bands; + int xsize = 0; + int stride = 0; + if (state->bits == 2 || state->bits == 4) { + xsize = (state->xsize + 7) / 8; + bands = state->bits; + stride = state->bytes / state->bits; + } else { + xsize = state->xsize; + bands = state->bytes / state->xsize; + if (bands != 0) { + stride = state->bytes / bands; + } + } + if (stride > xsize) { int i; for (i = 1; i < bands; i++) { // note -- skipping first band memmove( - &state->buffer[i * state->xsize], - &state->buffer[i * stride], - state->xsize + &state->buffer[i * xsize], &state->buffer[i * stride], xsize ); } } From b65a7acf259682c9d02d7ab6bef57bd4ca596c74 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:20:34 +0000 Subject: [PATCH 1773/2374] Update dependency cibuildwheel to v3 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 0e314b8bf59..520b6e32084 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.23.3 +cibuildwheel==3.0.0 From d2295c0843e828816af892da53b03d1698a3a616 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Jun 2025 18:53:35 +1000 Subject: [PATCH 1774/2374] Do not activate virtualenv --- .github/workflows/wheels-test.ps1 | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 index a1edc14ef25..256e84edf08 100644 --- a/.github/workflows/wheels-test.ps1 +++ b/.github/workflows/wheels-test.ps1 @@ -9,17 +9,16 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") { C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null } $env:path += ";$pillow\winbuild\build\bin\" -& "$venv\Scripts\activate.ps1" & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f if ("$venv" -like "*\cibw-run-*-win_amd64\*") { - & python -m pip install numpy + & $venv\Scripts\python.exe -m pip install numpy } cd $pillow -& python -VV +& $venv\Scripts\python.exe -VV if (!$?) { exit $LASTEXITCODE } -& python selftest.py +& $venv\Scripts\python.exe selftest.py if (!$?) { exit $LASTEXITCODE } -& python -m pytest -vx Tests\check_wheel.py +& $venv\Scripts\python.exe -m pytest -vx Tests\check_wheel.py if (!$?) { exit $LASTEXITCODE } -& python -m pytest -vx Tests +& $venv\Scripts\python.exe -m pytest -vx Tests if (!$?) { exit $LASTEXITCODE } From b9aac77003cda8c4af13fcf7f4fd712506374951 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Jun 2025 22:48:27 +1000 Subject: [PATCH 1775/2374] Test Python 3.14t --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 006d574f3fb..b4b5162281c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,6 +43,7 @@ jobs: python-version: [ "pypy3.11", "pypy3.10", + "3.14t", "3.14", "3.13t", "3.13", @@ -55,6 +56,7 @@ jobs: - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - { python-version: "3.10", PYTHONOPTIMIZE: 2 } # Free-threaded + - { python-version: "3.14t", disable-gil: true } - { python-version: "3.13t", disable-gil: true } # M1 only available for 3.10+ - { os: "macos-13", python-version: "3.9" } From 9bffc015e6d97151cf49e71eed31deb20548652f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Jun 2025 23:52:51 +1000 Subject: [PATCH 1776/2374] Use pypy.exe if it exists --- .github/workflows/wheels-test.ps1 | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 index 256e84edf08..54e7fbbfc30 100644 --- a/.github/workflows/wheels-test.ps1 +++ b/.github/workflows/wheels-test.ps1 @@ -9,16 +9,21 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") { C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null } $env:path += ";$pillow\winbuild\build\bin\" +if (Test-Path $venv\Scripts\pypy.exe) { + $python = "pypy.exe" +} else { + $python = "python.exe" +} & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f if ("$venv" -like "*\cibw-run-*-win_amd64\*") { - & $venv\Scripts\python.exe -m pip install numpy + & $venv\Scripts\$python -m pip install numpy } cd $pillow -& $venv\Scripts\python.exe -VV +& $venv\Scripts\$python -VV if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\python.exe selftest.py +& $venv\Scripts\$python selftest.py if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\python.exe -m pytest -vx Tests\check_wheel.py +& $venv\Scripts\$python -m pytest -vx Tests\check_wheel.py if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\python.exe -m pytest -vx Tests +& $venv\Scripts\$python -m pytest -vx Tests if (!$?) { exit $LASTEXITCODE } From 4a1eea84669dec76e573db2fc25e9b0ec3ea58e3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 9 Jun 2025 13:15:49 +0300 Subject: [PATCH 1777/2374] Add Python 3.14 beta wheels --- docs/releasenotes/11.3.0.rst | 56 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + tox.ini | 2 +- 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 docs/releasenotes/11.3.0.rst diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst new file mode 100644 index 00000000000..b0595def9b8 --- /dev/null +++ b/docs/releasenotes/11.3.0.rst @@ -0,0 +1,56 @@ +11.3.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards incompatible changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API changes +=========== + +TODO +^^^^ + +TODO + +API additions +============= + +TODO +^^^^ + +TODO + +Other changes +============= + +Python 3.14 beta +^^^^^^^^^^^^^^^^ + +To help other projects prepare for Python 3.14, wheels are now built for the +3.14 beta as a preview. This is not official support for Python 3.14, but rather +an opportunity for you to test how Pillow works with the beta and report any +problems. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 5d7b21d593d..a85f1e0752e 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 11.3.0 11.2.1 11.1.0 11.0.0 diff --git a/tox.ini b/tox.ini index 4065245ee11..967d4b53768 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ requires = tox>=4.2 env_list = lint - py{py3, 313, 312, 311, 310, 39} + py{py3, 314, 313, 312, 311, 310, 39} [testenv] deps = From aca0e57126ce5938dfd3cd57eed1a668cac5abc7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 12 Jun 2025 19:22:35 +0300 Subject: [PATCH 1778/2374] Add 3.14 to CI targets --- docs/installation/platform-support.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 57a2298f8c0..a56f9431652 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -40,12 +40,12 @@ These platforms are built and tested for every change. | macOS 13 Ventura | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | -| | PyPy3 | | +| | 3.14, PyPy3 | | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, 3.13, PyPy3 | | +| | 3.12, 3.13, 3.14, PyPy3 | | | +----------------------------+---------------------+ | | 3.12 | arm64v8, ppc64le, | | | | s390x | @@ -53,7 +53,7 @@ These platforms are built and tested for every change. | Windows Server 2022 | 3.9 | x86 | | +----------------------------+---------------------+ | | 3.10, 3.11, 3.12, 3.13, | x86-64 | -| | PyPy3 | | +| | 3.14, PyPy3 | | | +----------------------------+---------------------+ | | 3.12 (MinGW) | x86-64 | | +----------------------------+---------------------+ From 3841db0252cc2dfd485eae96363dc91cffd65c0c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 13 Jun 2025 00:08:52 +0300 Subject: [PATCH 1779/2374] Fix: Invalid skip selector: 'pp39-*' --- .github/workflows/wheels.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 33e1976f096..72516651f42 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -110,7 +110,6 @@ jobs: CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} - CIBW_SKIP: pp39-* MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - uses: actions/upload-artifact@v4 @@ -188,7 +187,6 @@ jobs: CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy - CIBW_SKIP: pp39-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm -v {project}:C:\pillow From a3d91cb0ce30bc5c8418956ab9dd32d1b7a13f00 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 14 Jun 2025 05:21:31 +0300 Subject: [PATCH 1780/2374] CI: Require Python >= 3.13.5 on Windows (#9017) --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 6b76351b001..6d8acc44f25 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"] architecture: ["x64"] include: # Test the oldest Python on 32-bit From a219e96fd3e0d4be53b2dad9dfa08bed993c6f80 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jun 2025 21:03:08 +1000 Subject: [PATCH 1781/2374] Fixed warning --- Tests/test_file_ppm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index c7d1f4df4d6..68f2f946855 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -294,9 +294,10 @@ def test_header_token_too_long(tmp_path: Path, data: bytes) -> None: with open(path, "wb") as f: f.write(data) - with pytest.raises(ValueError, match="Token too long in file header: "): + with pytest.raises(ValueError) as e: with Image.open(path): pass + assert "Token too long in file header: " in repr(e) def test_truncated_file(tmp_path: Path) -> None: From 4ba97d13276f7406c6355e9f81df3255ad392f92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jun 2025 19:36:31 +1000 Subject: [PATCH 1782/2374] Removed entries for non-existent modes --- src/PIL/TiffImagePlugin.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 946fbd531c5..4c0ed01efd1 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1680,7 +1680,6 @@ def _setup(self) -> None: "PA": ("PA", II, 3, 1, (8, 8), 2), "I": ("I;32S", II, 1, 2, (32,), None), "I;16": ("I;16", II, 1, 1, (16,), None), - "I;16S": ("I;16S", II, 1, 2, (16,), None), "F": ("F;32F", II, 1, 3, (32,), None), "RGB": ("RGB", II, 2, 1, (8, 8, 8), None), "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0), @@ -1688,10 +1687,7 @@ def _setup(self) -> None: "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), - "I;32BS": ("I;32BS", MM, 1, 2, (32,), None), "I;16B": ("I;16B", MM, 1, 1, (16,), None), - "I;16BS": ("I;16BS", MM, 1, 2, (16,), None), - "F;32BF": ("F;32BF", MM, 1, 3, (32,), None), } From 925fe519043a5056384b2eb4caa4cab792c92780 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jun 2025 19:41:20 +1000 Subject: [PATCH 1783/2374] Support saving I;16L images --- Tests/test_file_tiff.py | 23 ++++------------------- src/PIL/TiffImagePlugin.py | 3 ++- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 73046eb5fd4..e92b97c8a5e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -49,25 +49,10 @@ def test_sanity(self, tmp_path: Path) -> None: assert im.size == (128, 128) assert im.format == "TIFF" - hopper("1").save(filename) - with Image.open(filename): - pass - - hopper("L").save(filename) - with Image.open(filename): - pass - - hopper("P").save(filename) - with Image.open(filename): - pass - - hopper("RGB").save(filename) - with Image.open(filename): - pass - - hopper("I").save(filename) - with Image.open(filename): - pass + for mode in ("1", "L", "P", "RGB", "I", "I;16", "I;16L"): + hopper(mode).save(filename) + with Image.open(filename): + pass @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(self) -> None: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 4c0ed01efd1..146b01e5f08 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1680,6 +1680,7 @@ def _setup(self) -> None: "PA": ("PA", II, 3, 1, (8, 8), 2), "I": ("I;32S", II, 1, 2, (32,), None), "I;16": ("I;16", II, 1, 1, (16,), None), + "I;16L": ("I;16L", II, 1, 1, (16,), None), "F": ("F;32F", II, 1, 3, (32,), None), "RGB": ("RGB", II, 2, 1, (8, 8, 8), None), "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0), @@ -1963,7 +1964,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # we're storing image byte order. So, if the rawmode # contains I;16, we need to convert from native to image # byte order. - if im.mode in ("I;16B", "I;16"): + if im.mode in ("I;16", "I;16B", "I;16L"): rawmode = "I;16N" # Pass tags as sorted list so that the tags are set in a fixed order. From 5aa09cd1078440578b6cd040f86977358c7983b9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jun 2025 14:56:18 +1000 Subject: [PATCH 1784/2374] Updated libpng to 1.6.49 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index b46811f5a01..996d32bc253 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -39,7 +39,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=11.2.1 -LIBPNG_VERSION=1.6.48 +LIBPNG_VERSION=1.6.49 JPEGTURBO_VERSION=3.1.1 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0cc383733b7..098716b6075 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -118,7 +118,7 @@ def cmd_msbuild( "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.3.4", - "LIBPNG": "1.6.48", + "LIBPNG": "1.6.49", "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.7.0", From e6af31e709eb61c553d13fb8648772379211f250 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jun 2025 16:09:11 +1000 Subject: [PATCH 1785/2374] Deprecate fromarray mode argument --- Tests/test_image_array.py | 6 ++++-- docs/deprecations.rst | 8 ++++++++ docs/releasenotes/11.3.0.rst | 7 ++++--- src/PIL/Image.py | 3 ++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index eb2309e0ffe..2c71dceb841 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -101,7 +101,8 @@ def __init__(self, arr_params: dict[str, Any]) -> None: with pytest.raises(ValueError): wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) - Image.fromarray(wrapped, "L") + with pytest.warns(DeprecationWarning): + Image.fromarray(wrapped, "L") def test_fromarray_palette() -> None: @@ -110,7 +111,8 @@ def test_fromarray_palette() -> None: a = numpy.array(i) # Act - out = Image.fromarray(a, "P") + with pytest.warns(DeprecationWarning): + out = Image.fromarray(a, "P") # Assert that the Python and C palettes match assert out.palette is not None diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 0490ba439fd..a5d89408b96 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -193,6 +193,14 @@ Image.Image.get_child_images() method uses an image's file pointer, and so child images could only be retrieved from an :py:class:`PIL.ImageFile.ImageFile` instance. +Image.fromarray mode parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.3.0 + +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. + Removed features ---------------- diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index b0595def9b8..f0fa8c85801 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -23,10 +23,11 @@ TODO Deprecations ============ -TODO -^^^^ +Image.fromarray mode parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +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. API changes =========== diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7e9540e48ce..0be9e8ce491 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3272,7 +3272,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: :param obj: Object with array interface :param mode: Optional mode to use when reading ``obj``. Will be determined from - type if ``None``. + type if ``None``. Deprecated. This will not be used to convert the data after reading, but will be used to change how the data is read:: @@ -3307,6 +3307,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" raise TypeError(msg) from e else: + deprecate("'mode' parameter", 13) rawmode = mode if mode in ["1", "L", "I", "P", "F"]: ndmax = 2 From 27ce12bb7a4b5e7d8f662d7eb3a6e39fe636b7c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jun 2025 16:44:42 +1000 Subject: [PATCH 1786/2374] Added release notes for #8969 --- docs/releasenotes/11.3.0.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index b0595def9b8..bf9506a3bd6 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -47,6 +47,13 @@ TODO Other changes ============= +Do not build against libavif < 1 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow only supports libavif 1.0.0 or later. In order to prevent errors when building +from source, if a user happens to have an earlier libavif on their system, Pillow will +now ignore it. + Python 3.14 beta ^^^^^^^^^^^^^^^^ From 3ac1edf6dafddd26ecfae1dbf041e678d4eda97c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jun 2025 17:13:02 +1000 Subject: [PATCH 1787/2374] Added release notes for #8912 --- docs/releasenotes/11.3.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index bf9506a3bd6..ea22a9c8e31 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -47,6 +47,12 @@ TODO Other changes ============= +Support using grim with ImageGrab on Linux +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.ImageGrab.grab` is now able to use ``gnome-screenshot``, ``grim`` or +``spectacle`` on Linux in order to take a snapshot of the screen. + Do not build against libavif < 1 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 59667bbec54c7e37887ba1dfd0a3389c2d5bc413 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jun 2025 18:39:30 +1000 Subject: [PATCH 1788/2374] Use *_tofile helpers --- Tests/test_file_blp.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 9f50df22d86..f64a9d420c4 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -7,9 +7,8 @@ from PIL import BlpImagePlugin, Image from .helper import ( - assert_image_equal, assert_image_equal_tofile, - assert_image_similar, + assert_image_similar_tofile, hopper, ) @@ -52,15 +51,13 @@ def test_save(tmp_path: Path) -> None: im = hopper("P") im.save(f, blp_version=version) - with Image.open(f) as reloaded: - assert_image_equal(im.convert("RGB"), reloaded) + assert_image_equal_tofile(im.convert("RGB"), f) with Image.open("Tests/images/transparent.png") as im: f = tmp_path / "temp.blp" im.convert("P").save(f, blp_version=version) - with Image.open(f) as reloaded: - assert_image_similar(im, reloaded, 8) + assert_image_similar_tofile(im, f, 8) im = hopper() with pytest.raises(ValueError): From ce8083e0d871899294142e09c88d249409334aaf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jun 2025 18:40:03 +1000 Subject: [PATCH 1789/2374] Match error message --- Tests/test_file_blp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index f64a9d420c4..5f6b263a1e3 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -60,7 +60,7 @@ def test_save(tmp_path: Path) -> None: assert_image_similar_tofile(im, f, 8) im = hopper() - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Unsupported BLP image mode"): im.save(f) From cb433ad00ad4ad6eeaed8e70c191ea94e8087298 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Jun 2025 08:15:08 +1000 Subject: [PATCH 1790/2374] Replaced ImagingError_Clear with PyErr_Clear --- src/_imaging.c | 5 ----- src/libImaging/Imaging.h | 2 -- src/libImaging/Storage.c | 2 +- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 2a7bc8d3f5e..2e8c8b0adb9 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -369,11 +369,6 @@ ImagingError_ValueError(const char *message) { return NULL; } -void -ImagingError_Clear(void) { - PyErr_Clear(); -} - /* -------------------------------------------------------------------- */ /* HELPERS */ /* -------------------------------------------------------------------- */ diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 39ecdbff63d..29e21c55123 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -280,8 +280,6 @@ extern void * ImagingError_Mismatch(void); /* maps to ValueError by default */ extern void * ImagingError_ValueError(const char *message); -extern void -ImagingError_Clear(void); /* Transform callbacks */ /* ------------------- */ diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 11d6c06cc63..6fe26e1bd1a 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -645,7 +645,7 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { return im; } - ImagingError_Clear(); + PyErr_Clear(); // Try to allocate the image once more with smallest possible block size MUTEX_LOCK(&ImagingDefaultArena.mutex); From 8309962926f8e4f77c9899c4c5b763e9f5966311 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Jun 2025 08:19:27 +1000 Subject: [PATCH 1791/2374] Replaced ImagingError_OSError with PyErr_SetString --- src/_imaging.c | 6 ------ src/libImaging/File.c | 2 +- src/libImaging/Imaging.h | 2 -- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 2e8c8b0adb9..6241dc3ca08 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -338,12 +338,6 @@ static const char *no_palette = "image has no palette"; static const char *readonly = "image is readonly"; /* static const char* no_content = "image has no content"; */ -void * -ImagingError_OSError(void) { - PyErr_SetString(PyExc_OSError, "error when accessing file"); - return NULL; -} - void * ImagingError_MemoryError(void) { return PyErr_NoMemory(); diff --git a/src/libImaging/File.c b/src/libImaging/File.c index 76d0abccc4f..901fe83ad27 100644 --- a/src/libImaging/File.c +++ b/src/libImaging/File.c @@ -54,7 +54,7 @@ ImagingSavePPM(Imaging im, const char *outfile) { fp = fopen(outfile, "wb"); if (!fp) { - (void)ImagingError_OSError(); + PyErr_SetString(PyExc_OSError, "error when accessing file"); return 0; } diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 29e21c55123..bfe67d46213 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -270,8 +270,6 @@ ImagingSectionLeave(ImagingSectionCookie *cookie); /* Exceptions */ /* ---------- */ -extern void * -ImagingError_OSError(void); extern void * ImagingError_MemoryError(void); extern void * From c19afb94301de3980ea0a6fd7c26aa9ab3c05065 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:05:34 +1000 Subject: [PATCH 1792/2374] Use names Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/releasenotes/11.3.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index ea22a9c8e31..45dff04decb 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -50,8 +50,8 @@ Other changes Support using grim with ImageGrab on Linux ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:py:meth:`~PIL.ImageGrab.grab` is now able to use ``gnome-screenshot``, ``grim`` or -``spectacle`` on Linux in order to take a snapshot of the screen. +:py:meth:`~PIL.ImageGrab.grab` is now able to use GNOME Screenshot, grim or +Spectacle on Linux in order to take a snapshot of the screen. Do not build against libavif < 1 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 7b5e11deb7441cab16df247f1f0890cd0d303372 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Jun 2025 20:06:53 +1000 Subject: [PATCH 1793/2374] Updated heading --- docs/releasenotes/11.3.0.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index 45dff04decb..ba091fa2c4f 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -47,11 +47,11 @@ TODO Other changes ============= -Support using grim with ImageGrab on Linux -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Support using more screenshot utilities with ImageGrab on Linux +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:py:meth:`~PIL.ImageGrab.grab` is now able to use GNOME Screenshot, grim or -Spectacle on Linux in order to take a snapshot of the screen. +:py:meth:`~PIL.ImageGrab.grab` is now able to use GNOME Screenshot, grim or Spectacle +on Linux in order to take a snapshot of the screen. Do not build against libavif < 1 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From d23d56e195d34734b42452995682e4a9649e5332 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 17 Jun 2025 23:10:15 +1000 Subject: [PATCH 1794/2374] Deprecate saving I mode images as PNG --- Tests/test_file_png.py | 14 ++++++++++++-- docs/deprecations.rst | 14 ++++++++++++++ docs/releasenotes/11.3.0.rst | 13 ++++++++++--- src/PIL/PngImagePlugin.py | 3 +++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0f0886ab8dc..15f67385a2d 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -100,11 +100,11 @@ def test_sanity(self, tmp_path: Path) -> None: assert im.format == "PNG" assert im.get_format_mimetype() == "image/png" - for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]: + for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]: im = hopper(mode) im.save(test_file) with Image.open(test_file) as reloaded: - if mode in ("I", "I;16B"): + if mode == "I;16B": reloaded = reloaded.convert(mode) assert_image_equal(reloaded, im) @@ -801,6 +801,16 @@ def test_truncated_end_chunk(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/truncated_end_chunk.png") as im: assert_image_equal_tofile(im, "Tests/images/hopper.png") + def test_deprecation(self, tmp_path: Path) -> None: + test_file = tmp_path / "out.png" + + im = hopper("I") + with pytest.warns(DeprecationWarning): + im.save(test_file) + + with Image.open(test_file) as reloaded: + assert_image_equal(im, reloaded.convert("I")) + @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @skip_unless_feature("zlib") diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 0490ba439fd..a36eb4aa74b 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -193,6 +193,20 @@ Image.Image.get_child_images() method uses an image's file pointer, and so child images could only be retrieved from an :py:class:`PIL.ImageFile.ImageFile` instance. +Saving I mode images as PNG +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.3.0 + +In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain +at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly +changing the data, this is now deprecated. Instead, the image can be converted to +another mode before saving:: + + from PIL import Image + im = Image.new("I", (1, 1)) + im.convert("I;16").save("out.png") + Removed features ---------------- diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index ba091fa2c4f..5dd151bf38a 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -23,10 +23,17 @@ TODO Deprecations ============ -TODO -^^^^ +Saving I mode images as PNG +^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain +at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly +changing the data, this is now deprecated. Instead, the image can be converted to +another mode before saving:: + + from PIL import Image + im = Image.new("I", (1, 1)) + im.convert("I;16").save("out.png") API changes =========== diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index f3815a12205..7999381a6b9 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -48,6 +48,7 @@ from ._binary import o8 from ._binary import o16be as o16 from ._binary import o32be as o32 +from ._deprecate import deprecate from ._util import DeferredError TYPE_CHECKING = False @@ -1368,6 +1369,8 @@ def _save( except KeyError as e: msg = f"cannot write mode {mode} as PNG" raise OSError(msg) from e + if outmode == "I": + deprecate("Saving I mode images as PNG", 13) # # write minimal PNG file From a4e8d675b4e0eeba79d4579bca8621bea7ee68fe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Jun 2025 21:59:31 +1000 Subject: [PATCH 1795/2374] Only check DHT marker for libjpeg-turbo --- Tests/test_file_jpeg.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 2827937cf57..614044d97eb 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1067,10 +1067,16 @@ def test_separate_tables(self) -> None: for marker in b"\xff\xd8", b"\xff\xd9": assert marker in data[1] assert marker in data[2] - # DHT, DQT - for marker in b"\xff\xc4", b"\xff\xdb": + + # DQT + markers = [b"\xff\xdb"] + if features.check_feature("libjpeg_turbo"): + # DHT + markers.append(b"\xff\xc4") + for marker in markers: assert marker in data[1] assert marker not in data[2] + # SOF0, SOS, APP0 (JFIF header) for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0": assert marker not in data[1] From 79e0b0b6ada86f16bf7a8cf1c878756225d85d44 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Jun 2025 22:19:20 +1000 Subject: [PATCH 1796/2374] Allow for custom stacklevel in deprecations --- src/PIL/PngImagePlugin.py | 2 +- src/PIL/_deprecate.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 7999381a6b9..1b9a89aef0d 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1370,7 +1370,7 @@ def _save( msg = f"cannot write mode {mode} as PNG" raise OSError(msg) from e if outmode == "I": - deprecate("Saving I mode images as PNG", 13) + deprecate("Saving I mode images as PNG", 13, stacklevel=4) # # write minimal PNG file diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 9f9d8bbc9cc..170d4449049 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -12,6 +12,7 @@ def deprecate( *, action: str | None = None, plural: bool = False, + stacklevel: int = 3, ) -> None: """ Deprecations helper. @@ -67,5 +68,5 @@ def deprecate( warnings.warn( f"{deprecated} {is_} deprecated and will be removed in {removed}{action}", DeprecationWarning, - stacklevel=3, + stacklevel=stacklevel, ) From 92de1db067112d5b15c034036b2216009822a3b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:12:40 +1000 Subject: [PATCH 1797/2374] Update dependency mypy to v1.16.1 (#9026) --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index a9c18ae2bfd..44b5badabf1 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.16.0 +mypy==1.16.1 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From ef0bab0c6579fd00987740a6a327603ff3886a38 Mon Sep 17 00:00:00 2001 From: thisismypassport <109758321+thisismypassport@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:16:26 +0300 Subject: [PATCH 1798/2374] Support writing QOI images (#9007) Co-authored-by: Andrew Murray --- Tests/test_file_qoi.py | 23 +++++- docs/handbook/image-file-formats.rst | 29 +++++-- docs/releasenotes/11.3.0.rst | 7 ++ src/PIL/QoiImagePlugin.py | 119 +++++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py index 7ce1da2099d..b9becb24f77 100644 --- a/Tests/test_file_qoi.py +++ b/Tests/test_file_qoi.py @@ -1,10 +1,12 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, QoiImagePlugin -from .helper import assert_image_equal_tofile +from .helper import assert_image_equal_tofile, hopper def test_sanity() -> None: @@ -34,3 +36,22 @@ def test_op_index() -> None: # QOI_OP_INDEX as the first chunk with Image.open("Tests/images/op_index.qoi") as im: assert im.getpixel((0, 0)) == (0, 0, 0, 0) + + +def test_save(tmp_path: Path) -> None: + f = tmp_path / "temp.qoi" + + im = hopper() + im.save(f, colorspace="sRGB") + + assert_image_equal_tofile(im, f) + + for path in ("Tests/images/default_font.png", "Tests/images/pil123rgba.png"): + with Image.open(path) as im: + im.save(f) + + assert_image_equal_tofile(im, f) + + im = hopper("P") + with pytest.raises(ValueError, match="Unsupported QOI image mode"): + im.save(f) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 5ca549c3787..a15e845745f 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1082,6 +1082,26 @@ Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well. +QOI +^^^ + +.. versionadded:: 9.5.0 + +Pillow reads and writes images in Quite OK Image format using a Python codec. If you +wish to write code specifically for this format, :pypi:`qoi` is an alternative library +that uses C to decode the image and interfaces with NumPy. + +.. _qoi-saving: + +Saving +~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**colorspace** + If set to "sRGB", the colorspace will be written as sRGB with linear alpha, instead + of all channels being linear. + SGI ^^^ @@ -1578,15 +1598,6 @@ PSD Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. -QOI -^^^ - -.. versionadded:: 9.5.0 - -Pillow reads images in Quite OK Image format using a Python decoder. If you wish to -write code specifically for this format, :pypi:`qoi` is an alternative library that -uses C to decode the image and interfaces with NumPy. - SUN ^^^ diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index ba091fa2c4f..6bd4f7481c4 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -47,6 +47,13 @@ TODO Other changes ============= +Added QOI saving +^^^^^^^^^^^^^^^^ + +Support has been added for saving QOI images. ``colorspace`` can be used to specify the +colorspace as sRGB with linear alpha, e.g. ``im.save("out.qoi", colorspace="sRGB")``. +By default, all channels will be linear. + Support using more screenshot utilities with ImageGrab on Linux ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 75070abd740..dba5d809fef 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -8,9 +8,12 @@ from __future__ import annotations import os +from typing import IO from . import Image, ImageFile from ._binary import i32be as i32 +from ._binary import o8 +from ._binary import o32be as o32 def _accept(prefix: bytes) -> bool: @@ -110,6 +113,122 @@ def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int return -1, 0 +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode == "RGB": + channels = 3 + elif im.mode == "RGBA": + channels = 4 + else: + msg = "Unsupported QOI image mode" + raise ValueError(msg) + + colorspace = 0 if im.encoderinfo.get("colorspace") == "sRGB" else 1 + + fp.write(b"qoif") + fp.write(o32(im.size[0])) + fp.write(o32(im.size[1])) + fp.write(o8(channels)) + fp.write(o8(colorspace)) + + ImageFile._save(im, fp, [ImageFile._Tile("qoi", (0, 0) + im.size)]) + + +class QoiEncoder(ImageFile.PyEncoder): + _pushes_fd = True + _previous_pixel: tuple[int, int, int, int] | None = None + _previously_seen_pixels: dict[int, tuple[int, int, int, int]] = {} + _run = 0 + + def _write_run(self) -> bytes: + data = o8(0b11000000 | (self._run - 1)) # QOI_OP_RUN + self._run = 0 + return data + + def _delta(self, left: int, right: int) -> int: + result = (left - right) & 255 + if result >= 128: + result -= 256 + return result + + def encode(self, bufsize: int) -> tuple[int, int, bytes]: + assert self.im is not None + + self._previously_seen_pixels = {0: (0, 0, 0, 0)} + self._previous_pixel = (0, 0, 0, 255) + + data = bytearray() + w, h = self.im.size + bands = Image.getmodebands(self.mode) + + for y in range(h): + for x in range(w): + pixel = self.im.getpixel((x, y)) + if bands == 3: + pixel = (*pixel, 255) + + if pixel == self._previous_pixel: + self._run += 1 + if self._run == 62: + data += self._write_run() + else: + if self._run: + data += self._write_run() + + r, g, b, a = pixel + hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 + if self._previously_seen_pixels.get(hash_value) == pixel: + data += o8(hash_value) # QOI_OP_INDEX + elif self._previous_pixel: + self._previously_seen_pixels[hash_value] = pixel + + prev_r, prev_g, prev_b, prev_a = self._previous_pixel + if prev_a == a: + delta_r = self._delta(r, prev_r) + delta_g = self._delta(g, prev_g) + delta_b = self._delta(b, prev_b) + + if ( + -2 <= delta_r < 2 + and -2 <= delta_g < 2 + and -2 <= delta_b < 2 + ): + data += o8( + 0b01000000 + | (delta_r + 2) << 4 + | (delta_g + 2) << 2 + | (delta_b + 2) + ) # QOI_OP_DIFF + else: + delta_gr = self._delta(delta_r, delta_g) + delta_gb = self._delta(delta_b, delta_g) + if ( + -8 <= delta_gr < 8 + and -32 <= delta_g < 32 + and -8 <= delta_gb < 8 + ): + data += o8( + 0b10000000 | (delta_g + 32) + ) # QOI_OP_LUMA + data += o8((delta_gr + 8) << 4 | (delta_gb + 8)) + else: + data += o8(0b11111110) # QOI_OP_RGB + data += bytes(pixel[:3]) + else: + data += o8(0b11111111) # QOI_OP_RGBA + data += bytes(pixel) + + self._previous_pixel = pixel + + if self._run: + data += self._write_run() + data += bytes((0, 0, 0, 0, 0, 0, 0, 1)) # padding + + return len(data), 0, data + + Image.register_open(QoiImageFile.format, QoiImageFile, _accept) Image.register_decoder("qoi", QoiDecoder) Image.register_extension(QoiImageFile.format, ".qoi") + +Image.register_save(QoiImageFile.format, _save) +Image.register_encoder("qoi", QoiEncoder) From 2316c930f9b2985d27894f043f5f2e4787543dca Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 Jun 2025 22:46:09 +1000 Subject: [PATCH 1799/2374] Removed default argument --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index ec6b47b1c8c..23381119219 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ from setuptools import Extension, setup from setuptools.command.build_ext import build_ext -ParallelCompile("MAX_CONCURRENCY", default=0).install() +ParallelCompile("MAX_CONCURRENCY").install() def get_version() -> str: @@ -1051,12 +1051,12 @@ def debug_build() -> bool: Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), ] + # parse configuration from _custom_build/backend.py while sys.argv[-1].startswith("--pillow-configuration="): _, key, value = sys.argv.pop().split("=", 2) configuration.setdefault(key, []).append(value) - try: setup( cmdclass={"build_ext": pil_build_ext}, From f937dd27cd670971b93f4bdf17abccc0338e6b4c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 20 Jun 2025 23:44:30 +1000 Subject: [PATCH 1800/2374] Do not call sys.executable in PyInstaller application --- src/PIL/ImageShow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index dd240fb5500..7705608e3ec 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -175,7 +175,9 @@ def show_file(self, path: str, **options: Any) -> int: if not os.path.exists(path): raise FileNotFoundError subprocess.call(["open", "-a", "Preview.app", path]) - executable = sys.executable or shutil.which("python3") + + pyinstaller = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS") + executable = (not pyinstaller and sys.executable) or shutil.which("python3") if executable: subprocess.Popen( [ From 216dc4ca60947b4076defa2f0565f8b74a7864e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Jun 2025 19:12:23 +1000 Subject: [PATCH 1801/2374] Added Python 3.14 macOS x86-64 wheels --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 72516651f42..229e23ef04e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -58,7 +58,7 @@ jobs: - name: "macOS 10.13 x86_64" os: macos-13 cibw_arch: x86_64 - build: "cp3{12,13}*" + build: "cp3{12,13,14}*" macosx_deployment_target: "10.13" - name: "macOS 10.15 x86_64" os: macos-13 From ae025183148a900511e7f8866ca5c28f3efc7b5f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 22 Jun 2025 22:08:51 +1000 Subject: [PATCH 1802/2374] Use same AVIF URL when fetching dependency (#8871) --- winbuild/build_prepare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 098716b6075..4baa5ccefb8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -385,8 +385,8 @@ def cmd_msbuild( "bins": [r"*.dll"], }, "libavif": { - "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip", - "filename": f"libavif-{V['LIBAVIF']}.zip", + "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.tar.gz", + "filename": f"libavif-{V['LIBAVIF']}.tar.gz", "license": "LICENSE", "build": [ "rustup update", From 2954964cd2b70c463d75bd7f828c7bb7f3802bf2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 23 Jun 2025 07:05:43 +1000 Subject: [PATCH 1803/2374] Removed ImageCmsProfile._set method (#9032) Co-authored-by: Luke Granger-Brown --- src/PIL/ImageCms.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index fdfbee78974..a1584f111d4 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -248,6 +248,9 @@ def __init__(self, profile: str | SupportsRead[bytes] | core.CmsProfile) -> None low-level profile object """ + self.filename = None + self.product_name = None # profile.product_name + self.product_info = None # profile.product_info if isinstance(profile, str): if sys.platform == "win32": @@ -256,23 +259,18 @@ def __init__(self, profile: str | SupportsRead[bytes] | core.CmsProfile) -> None profile_bytes_path.decode("ascii") except UnicodeDecodeError: with open(profile, "rb") as f: - self._set(core.profile_frombytes(f.read())) + self.profile = core.profile_frombytes(f.read()) return - self._set(core.profile_open(profile), profile) + self.filename = profile + self.profile = core.profile_open(profile) elif hasattr(profile, "read"): - self._set(core.profile_frombytes(profile.read())) + self.profile = core.profile_frombytes(profile.read()) elif isinstance(profile, core.CmsProfile): - self._set(profile) + self.profile = profile else: msg = "Invalid type for Profile" # type: ignore[unreachable] raise TypeError(msg) - def _set(self, profile: core.CmsProfile, filename: str | None = None) -> None: - self.profile = profile - self.filename = filename - self.product_name = None # profile.product_name - self.product_info = None # profile.product_info - def tobytes(self) -> bytes: """ Returns the profile in a format suitable for embedding in From 1557585411f1f891180775dab40cfeec236d69bf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Jun 2025 20:29:38 +1000 Subject: [PATCH 1804/2374] Use percent formatting --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3716a7b9f66..256c78fc99c 100644 --- a/setup.py +++ b/setup.py @@ -509,11 +509,11 @@ def build_extensions(self) -> None: if root is None and pkg_config: if isinstance(lib_name, str): - _dbg(f"Looking for `{lib_name}` using pkg-config.") + _dbg("Looking for `%s` using pkg-config.", lib_name) root = pkg_config(lib_name) else: for lib_name2 in lib_name: - _dbg(f"Looking for `{lib_name2}` using pkg-config.") + _dbg("Looking for `%s` using pkg-config.", lib_name2) root = pkg_config(lib_name2) if root: break From 18f8af78d3f56639f91f4b788ab9d89ae573b72c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Jun 2025 20:35:09 +1000 Subject: [PATCH 1805/2374] Pass strings or tuples of strings to _dbg --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 256c78fc99c..f05379d6d8b 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,6 @@ import sys import warnings from collections.abc import Iterator -from typing import Any from setuptools import Extension, setup from setuptools.command.build_ext import build_ext @@ -148,7 +147,7 @@ class RequiredDependencyException(Exception): PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version -def _dbg(s: str, tp: Any = None) -> None: +def _dbg(s: str, tp: str | tuple[str, ...] | None = None) -> None: if DEBUG: if tp: print(s % tp) @@ -732,7 +731,7 @@ def build_extensions(self) -> None: best_path = os.path.join(directory, name) _dbg( "Best openjpeg version %s so far in %s", - (best_version, best_path), + (str(best_version), best_path), ) if best_version and _find_library_file(self, "openjp2"): From acd8b0c2acfa1c69f91a14d5881a7f9bf436900f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 25 Jun 2025 09:09:31 +1000 Subject: [PATCH 1806/2374] Fix libtiff cleanup (#9002) --- src/libImaging/TiffDecode.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 2e83fb847c9..e289ce4056e 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -1032,7 +1032,10 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Encode Error, row %d\n", state->y)); state->errcode = IMAGING_CODEC_BROKEN; - if (!clientstate->fp) { + if (clientstate->fp) { + TIFFCleanup(tiff); + clientstate->tiff = NULL; + } else { free(clientstate->data); } return -1; From e1ee8afc7d1d0a1b3b91ce4caa018e4bb0a96e71 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Jun 2025 18:58:29 +1000 Subject: [PATCH 1807/2374] Search for libtiff library file first on Windows and macOS --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f05379d6d8b..354e09f85f2 100644 --- a/setup.py +++ b/setup.py @@ -753,12 +753,12 @@ def build_extensions(self) -> None: if feature.want("tiff"): _dbg("Looking for tiff") if _find_include_file(self, "tiff.h"): - if _find_library_file(self, "tiff"): - feature.set("tiff", "tiff") if sys.platform in ["win32", "darwin"] and _find_library_file( self, "libtiff" ): feature.set("tiff", "libtiff") + elif _find_library_file(self, "tiff"): + feature.set("tiff", "tiff") if feature.want("freetype"): _dbg("Looking for freetype") From ecd264fffc680ab05da6a71ff4466c774185ab90 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Jun 2025 22:22:46 +1000 Subject: [PATCH 1808/2374] Use "parallel" config setting and 4 as defaults --- setup.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 1134879beec..6c2180ebd15 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,15 @@ from setuptools import Extension, setup from setuptools.command.build_ext import build_ext -ParallelCompile("MAX_CONCURRENCY").install() +configuration: dict[str, list[str]] = {} + +# parse configuration from _custom_build/backend.py +while sys.argv[-1].startswith("--pillow-configuration="): + _, key, value = sys.argv.pop().split("=", 2) + configuration.setdefault(key, []).append(value) + +default = int(configuration.get("parallel", ["4"])[-1]) +ParallelCompile("MAX_CONCURRENCY", default).install() def get_version() -> str: @@ -30,9 +38,6 @@ def get_version() -> str: return f.read().split('"')[1] -configuration: dict[str, list[str]] = {} - - PILLOW_VERSION = get_version() AVIF_ROOT = None FREETYPE_ROOT = None @@ -1047,11 +1052,6 @@ def debug_build() -> bool: ] -# parse configuration from _custom_build/backend.py -while sys.argv[-1].startswith("--pillow-configuration="): - _, key, value = sys.argv.pop().split("=", 2) - configuration.setdefault(key, []).append(value) - try: setup( cmdclass={"build_ext": pil_build_ext}, From 23ed906b622e25466981fa7c2b80f2a1da612661 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Jun 2025 22:00:36 +1000 Subject: [PATCH 1809/2374] Removed default limit of 4 --- docs/installation/building-from-source.rst | 5 ++--- setup.py | 6 ++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 8988a92ce36..4c114a5e27f 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -276,10 +276,9 @@ Build options * Config setting: ``-C parallel=n``. Can also be given with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use - multiprocessing to build the extension. Setting ``-C parallel=n`` + multiprocessing to build the extensions. Setting ``-C parallel=n`` sets the number of CPUs to use to ``n``, or can disable parallel building by - using a setting of 1. By default, it uses 4 CPUs, or if 4 are not - available, as many as are present. + using a setting of 1. By default, it uses as many CPUs as are present. * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, diff --git a/setup.py b/setup.py index 6c2180ebd15..aee1b04eb54 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ _, key, value = sys.argv.pop().split("=", 2) configuration.setdefault(key, []).append(value) -default = int(configuration.get("parallel", ["4"])[-1]) +default = int(configuration.get("parallel", ["0"])[-1]) ParallelCompile("MAX_CONCURRENCY", default).install() @@ -394,9 +394,7 @@ def finalize_options(self) -> None: cpu_count = os.cpu_count() if cpu_count is not None: try: - self.parallel = int( - os.environ.get("MAX_CONCURRENCY", min(4, cpu_count)) - ) + self.parallel = int(os.environ.get("MAX_CONCURRENCY", cpu_count)) except TypeError: pass for x in self.feature: From 3d261a210192509f2c7980f87a922b7b3f3404dc Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Thu, 26 Jun 2025 02:21:44 -0400 Subject: [PATCH 1810/2374] Add AVIF to wheels using only aomenc and dav1d AVIF codecs for reduced size (#8858) Co-authored-by: Andrew Murray Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/workflows/wheels-dependencies.sh | 55 ++++ .github/workflows/wheels.yml | 2 +- Tests/check_wheel.py | 6 +- docs/releasenotes/11.3.0.rst | 6 + wheels/dependency_licenses/AOM.txt | 26 ++ wheels/dependency_licenses/DAV1D.txt | 23 ++ wheels/dependency_licenses/LIBAVIF.txt | 387 +++++++++++++++++++++++ wheels/dependency_licenses/LIBYUV.txt | 29 ++ winbuild/build_prepare.py | 15 +- 9 files changed, 542 insertions(+), 7 deletions(-) create mode 100644 wheels/dependency_licenses/AOM.txt create mode 100644 wheels/dependency_licenses/DAV1D.txt create mode 100644 wheels/dependency_licenses/LIBAVIF.txt create mode 100644 wheels/dependency_licenses/LIBYUV.txt diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 996d32bc253..5384a74c0cd 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -51,6 +51,7 @@ LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 +LIBAVIF_VERSION=1.3.0 function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi @@ -98,6 +99,59 @@ function build_harfbuzz { touch harfbuzz-stamp } +function build_libavif { + if [ -e libavif-stamp ]; then return; fi + + python3 -m pip install meson ninja + + if [[ "$PLAT" == "x86_64" ]] || [ -n "$SANITIZER" ]; then + build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03 + fi + + local build_type=MinSizeRel + local lto=ON + + local libavif_cmake_flags + + if [ -n "$IS_MACOS" ]; then + lto=OFF + libavif_cmake_flags=( + -DCMAKE_C_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \ + -DCMAKE_CXX_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \ + -DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,-S,-x,-dead_strip_dylibs" \ + ) + else + if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "x86_64" ]]; then + build_type=Release + fi + libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now") + fi + + local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz) + # CONFIG_AV1_HIGHBITDEPTH=0 is a flag for libaom (included as a subproject + # of libavif) that disables support for encoding high bit depth images. + (cd $out_dir \ + && cmake \ + -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \ + -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \ + -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \ + -DBUILD_SHARED_LIBS=ON \ + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + -DAVIF_CODEC_AOM=LOCAL \ + -DCONFIG_AV1_HIGHBITDEPTH=0 \ + -DAVIF_CODEC_AOM_DECODE=OFF \ + -DAVIF_CODEC_DAV1D=LOCAL \ + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \ + -DCMAKE_C_VISIBILITY_PRESET=hidden \ + -DCMAKE_CXX_VISIBILITY_PRESET=hidden \ + -DCMAKE_BUILD_TYPE=$build_type \ + "${libavif_cmake_flags[@]}" \ + . \ + && make install) + touch libavif-stamp +} + function build { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then @@ -132,6 +186,7 @@ function build { build_tiff fi + build_libavif build_libpng build_lcms2 build_openjpeg diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 229e23ef04e..16c350a1484 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -159,7 +159,7 @@ jobs: # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images - & python.exe winbuild\build_prepare.py -v --no-imagequant --no-avif --architecture=${{ matrix.cibw_arch }} + & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} shell: pwsh - name: Build wheels diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 9602410da52..a78fb09b041 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -9,7 +9,7 @@ def test_wheel_modules() -> None: - expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} + expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"} if sys.platform == "win32": # tkinter is not available in cibuildwheel installed CPython on Windows @@ -20,6 +20,10 @@ def test_wheel_modules() -> None: except ImportError: expected_modules.remove("tkinter") + # libavif is not available on Windows for ARM64 architectures + if platform.machine() == "ARM64": + expected_modules.remove("avif") + assert set(features.get_supported_modules()) == expected_modules diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index 82e4d44b692..654a7e6b6f1 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -80,6 +80,12 @@ Pillow only supports libavif 1.0.0 or later. In order to prevent errors when bui from source, if a user happens to have an earlier libavif on their system, Pillow will now ignore it. +AVIF support in wheels +^^^^^^^^^^^^^^^^^^^^^^ + +Support for reading and writing AVIF images is now included in Pillow's wheels, except +for Windows ARM64. libaom is available as an encoder and dav1d as a decoder. + Python 3.14 beta ^^^^^^^^^^^^^^^^ diff --git a/wheels/dependency_licenses/AOM.txt b/wheels/dependency_licenses/AOM.txt new file mode 100644 index 00000000000..3a2e46c264b --- /dev/null +++ b/wheels/dependency_licenses/AOM.txt @@ -0,0 +1,26 @@ +Copyright (c) 2016, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. 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. + +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. diff --git a/wheels/dependency_licenses/DAV1D.txt b/wheels/dependency_licenses/DAV1D.txt new file mode 100644 index 00000000000..875b138ecf6 --- /dev/null +++ b/wheels/dependency_licenses/DAV1D.txt @@ -0,0 +1,23 @@ +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. 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. + +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 OWNER 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. diff --git a/wheels/dependency_licenses/LIBAVIF.txt b/wheels/dependency_licenses/LIBAVIF.txt new file mode 100644 index 00000000000..350eb9d15ce --- /dev/null +++ b/wheels/dependency_licenses/LIBAVIF.txt @@ -0,0 +1,387 @@ +Copyright 2019 Joe Drago. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. 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. + +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. + +------------------------------------------------------------------------------ + +Files: src/obu.c + +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. 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. + +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 OWNER 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. + +------------------------------------------------------------------------------ + +Files: third_party/iccjpeg/* + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +The Unix configuration script "configure" was produced with GNU Autoconf. +It is copyright by the Free Software Foundation but is freely distributable. +The same holds for its supporting scripts (config.guess, config.sub, +ltmain.sh). Another support script, install-sh, is copyright by X Consortium +but is also freely distributable. + +The IJG distribution formerly included code to read and write GIF files. +To avoid entanglement with the Unisys LZW patent, GIF reading support has +been removed altogether, and the GIF writer has been simplified to produce +"uncompressed GIFs". This technique does not use the LZW algorithm; the +resulting GIF files are larger than usual, but are readable by all standard +GIF decoders. + +We are required to state that + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +------------------------------------------------------------------------------ + +Files: contrib/gdk-pixbuf/* + +Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. 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. + +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. + +------------------------------------------------------------------------------ + +Files: android_jni/gradlew* + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ + +Files: third_party/libyuv/* + +Copyright 2011 The LibYuv Project Authors. 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 of Google 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. diff --git a/wheels/dependency_licenses/LIBYUV.txt b/wheels/dependency_licenses/LIBYUV.txt new file mode 100644 index 00000000000..c911747a6b5 --- /dev/null +++ b/wheels/dependency_licenses/LIBYUV.txt @@ -0,0 +1,29 @@ +Copyright 2011 The LibYuv Project Authors. 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 of Google 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. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 4baa5ccefb8..187d07b20c2 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -57,7 +57,10 @@ def cmd_nmake( def cmds_cmake( - target: str | tuple[str, ...] | list[str], *params: str, build_dir: str = "." + target: str | tuple[str, ...] | list[str], + *params: str, + build_dir: str = ".", + build_type: str = "Release", ) -> list[str]: if not isinstance(target, str): target = " ".join(target) @@ -66,7 +69,7 @@ def cmds_cmake( " ".join( [ "{cmake}", - "-DCMAKE_BUILD_TYPE=Release", + f"-DCMAKE_BUILD_TYPE={build_type}", "-DCMAKE_VERBOSE_MAKEFILE=ON", "-DCMAKE_RULE_MESSAGES:BOOL=OFF", # for NMake "-DCMAKE_C_COMPILER=cl.exe", # for Ninja @@ -397,9 +400,11 @@ def cmd_msbuild( "-DAVIF_LIBSHARPYUV=LOCAL", "-DAVIF_LIBYUV=LOCAL", "-DAVIF_CODEC_AOM=LOCAL", + "-DCONFIG_AV1_HIGHBITDEPTH=0", + "-DAVIF_CODEC_AOM_DECODE=OFF", "-DAVIF_CODEC_DAV1D=LOCAL", - "-DAVIF_CODEC_RAV1E=LOCAL", - "-DAVIF_CODEC_SVT=LOCAL", + "-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON", + build_type="MinSizeRel", ), cmd_xcopy("include", "{inc_dir}"), ], @@ -755,7 +760,7 @@ def main() -> None: disabled += ["libimagequant"] if args.no_fribidi: disabled += ["fribidi"] - if args.no_avif or args.architecture != "AMD64": + if args.no_avif or args.architecture == "ARM64": disabled += ["libavif"] prefs = { From b9afe18646c5dc28efed1a74579129f8c64976e2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 25 Jun 2025 15:00:19 +0300 Subject: [PATCH 1811/2374] Bump pre-commit hooks --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a1a054e008f..c6485829984 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.12 + rev: v0.12.0 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.8.3 + rev: 1.8.5 hooks: - id: bandit args: [--severity-level=high] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.5 + rev: v20.1.6 hooks: - id: clang-format types: [c] @@ -51,7 +51,7 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.0 + rev: 0.33.1 hooks: - id: check-github-workflows - id: check-readthedocs From 234875bf9001037f7510769f83e281bfe6012441 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 25 Jun 2025 15:01:14 +0300 Subject: [PATCH 1812/2374] Update Ruff hook from legacy --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c6485829984..6abb732bbaa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.12.0 hooks: - - id: ruff + - id: ruff-check args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror From d1894dcd46c9096a91dd6ab39fe1df918ba18d6c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 25 Jun 2025 16:47:06 +0300 Subject: [PATCH 1813/2374] Add match parameter to pytest.warns() --- Tests/helper.py | 2 +- Tests/test_core_resources.py | 2 +- Tests/test_features.py | 13 ++++++------- Tests/test_file_apng.py | 8 ++++---- Tests/test_file_avif.py | 4 ++-- Tests/test_file_gif.py | 6 ++++-- Tests/test_file_icns.py | 4 +++- Tests/test_file_ico.py | 2 +- Tests/test_file_iptc.py | 6 +++--- Tests/test_file_jpeg.py | 13 +++++++------ Tests/test_file_png.py | 2 +- Tests/test_file_tga.py | 4 +++- Tests/test_file_tiff.py | 4 ++-- Tests/test_file_tiff_metadata.py | 4 ++-- Tests/test_file_webp.py | 4 ++-- Tests/test_image.py | 12 ++++++------ Tests/test_image_access.py | 2 +- Tests/test_image_array.py | 6 +++--- Tests/test_image_convert.py | 5 ++++- Tests/test_image_getim.py | 4 ++-- Tests/test_image_putdata.py | 2 +- Tests/test_imagecms.py | 14 +++++++------- Tests/test_imagedraw.py | 2 +- Tests/test_imagefile.py | 2 +- Tests/test_imagefont.py | 12 ++++++------ Tests/test_imagemath_lambda_eval.py | 2 +- Tests/test_imagemath_unsafe_eval.py | 4 ++-- Tests/test_lib_pack.py | 4 +++- 28 files changed, 80 insertions(+), 69 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index ec61cd263e9..e71b4665b28 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -272,7 +272,7 @@ def _cached_hopper(mode: str) -> Image.Image: else: im = hopper() if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="BGR;"): im = im.convert(mode) else: try: diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 2c1de8bc3d2..2a22f805d8d 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -188,5 +188,5 @@ def test_units(self) -> None: ), ) def test_warnings(self, var: dict[str, str]) -> None: - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match=list(var)[0]): Image._apply_env_variables(var) diff --git a/Tests/test_features.py b/Tests/test_features.py index f8f7f6eec59..d06fb4d841c 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -19,7 +19,7 @@ def test_check() -> None: assert features.check_codec(codec) == features.check(codec) for feature in features.features: if "webp" in feature: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="webp"): assert features.check_feature(feature) == features.check(feature) else: assert features.check_feature(feature) == features.check(feature) @@ -49,24 +49,24 @@ def test(name: str, function: Callable[[str], str | None]) -> None: test(codec, features.version_codec) for feature in features.features: if "webp" in feature: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="webp"): test(feature, features.version_feature) else: test(feature, features.version_feature) def test_webp_transparency() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="transp_webp"): assert (features.check("transp_webp") or False) == features.check_module("webp") def test_webp_mux() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="webp_mux"): assert (features.check("webp_mux") or False) == features.check_module("webp") def test_webp_anim() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="webp_anim"): assert (features.check("webp_anim") or False) == features.check_module("webp") @@ -95,10 +95,9 @@ def test_check_codecs(feature: str) -> None: def test_check_warns_on_nonexistent() -> None: - with pytest.warns(UserWarning) as cm: + with pytest.warns(UserWarning, match="Unknown feature 'typo'."): has_feature = features.check("typo") assert has_feature is False - assert str(cm[-1].message) == "Unknown feature 'typo'." def test_supported_modules() -> None: diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index a5734c202a7..66410b3dad8 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -303,7 +303,7 @@ def test_apng_chunk_errors() -> None: assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: im.load() assert isinstance(im, PngImagePlugin.PngImageFile) @@ -330,14 +330,14 @@ def test_apng_chunk_errors() -> None: def test_apng_syntax_errors() -> None: - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with pytest.raises(OSError): im.load() - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated @@ -354,7 +354,7 @@ def test_apng_syntax_errors() -> None: im.seek(im.n_frames - 1) im.load() - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index b2e586637fa..e42e1029161 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -77,8 +77,8 @@ class TestUnsupportedAvif: def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) - with pytest.warns(UserWarning): - with pytest.raises(UnidentifiedImageError): + with pytest.raises(UnidentifiedImageError): + with pytest.warns(UserWarning, match="AVIF support not installed"): with Image.open(TEST_AVIF_FILE): pass diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 20d58a9dda4..29bc55ee55d 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1229,7 +1229,9 @@ def test_removed_transparency(tmp_path: Path) -> None: im.putpixel((x, 0), (x, 0, 0)) im.info["transparency"] = (255, 255, 255) - with pytest.warns(UserWarning): + with pytest.warns( + UserWarning, match="Couldn't allocate palette entry for transparency" + ): im.save(out) with Image.open(out) as reloaded: @@ -1251,7 +1253,7 @@ def test_rgb_transparency(tmp_path: Path) -> None: im = Image.new("RGB", (1, 1)) im.info["transparency"] = b"" ims = [Image.new("RGB", (1, 1))] - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="should be converted to RGBA images"): im.save(out, save_all=True, append_images=ims) with Image.open(out) as reloaded: diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 2dabfd2f30a..8ff59161ff3 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -95,7 +95,9 @@ def test_sizes() -> None: for w, h, r in im.info["sizes"]: wr = w * r hr = h * r - with pytest.warns(DeprecationWarning): + with pytest.warns( + DeprecationWarning, match=r"Setting size to \(width, height, scale\)" + ): im.size = (w, h, r) im.load() assert im.mode == "RGBA" diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 5d2ace35e28..99c312ead92 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -233,7 +233,7 @@ def test_save_append_images(tmp_path: Path) -> None: def test_unexpected_size() -> None: # This image has been manually hexedited to state that it is 16x32 # while the image within is still 16x16 - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Image was not the expected size"): with Image.open("Tests/images/hopper_unexpected.ico") as im: assert im.size == (16, 16) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index c6c0c1aab9d..ffd3aad3e30 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -99,7 +99,7 @@ def test_i() -> None: c = b"a" # Act - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="IptcImagePlugin.i"): ret = IptcImagePlugin.i(c) # Assert @@ -114,7 +114,7 @@ def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "stdout", mystdout) # Act - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="IptcImagePlugin.dump"): IptcImagePlugin.dump(c) # Assert @@ -122,5 +122,5 @@ def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: def test_pad_deprecation() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="IptcImagePlugin.PAD"): assert IptcImagePlugin.PAD == b"\0\0\0\0" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 614044d97eb..0ba08aaf954 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -752,10 +752,11 @@ def test_bad_mpo_header(self) -> None: # Act # Shouldn't raise error - fn = "Tests/images/sugarshack_bad_mpo_header.jpg" - with pytest.warns(UserWarning, Image.open, fn) as im: - # Assert - assert im.format == "JPEG" + with pytest.warns(UserWarning, match="malformed MPO file"): + im = Image.open("Tests/images/sugarshack_bad_mpo_header.jpg") + + # Assert + assert im.format == "JPEG" @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) def test_save_correct_modes(self, mode: str) -> None: @@ -1103,9 +1104,9 @@ def test_repr_jpeg_error_returns_none(self) -> None: def test_deprecation(self) -> None: with Image.open(TEST_FILE) as im: assert isinstance(im, JpegImagePlugin.JpegImageFile) - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="huffman_ac"): assert im.huffman_ac == {} - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="huffman_dc"): assert im.huffman_dc == {} diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 15f67385a2d..0a51fd49338 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -805,7 +805,7 @@ def test_deprecation(self, tmp_path: Path) -> None: test_file = tmp_path / "out.png" im = hopper("I") - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="Saving I mode images as PNG"): im.save(test_file) with Image.open(test_file) as reloaded: diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 8b6ed3ed226..27ff4f1a4b8 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -190,7 +190,9 @@ def test_save_id_section(tmp_path: Path) -> None: # Save with custom id section greater than 255 characters id_section = b"Test content" * 25 - with pytest.warns(UserWarning): + with pytest.warns( + UserWarning, match="id_section has been trimmed to 255 characters" + ): im.save(out, id_section=id_section) with Image.open(out) as test_im: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index e92b97c8a5e..046a9f1f16e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -221,7 +221,7 @@ def test_bad_exif(self) -> None: assert isinstance(im, JpegImagePlugin.JpegImageFile) # Should not raise struct.error. - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Corrupt EXIF data"): im._getexif() def test_save_rgba(self, tmp_path: Path) -> None: @@ -1014,7 +1014,7 @@ def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: @timeout_unless_slower_valgrind(2) def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Corrupt EXIF data"): with Image.open(test_file): pass diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 88486834593..36ad8cee93b 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -300,7 +300,7 @@ def test_empty_metadata() -> None: head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) # Should not raise struct.error. - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Corrupt EXIF data"): info.load(f) @@ -481,7 +481,7 @@ def test_too_many_entries() -> None: ifd.tagtype[277] = TiffTags.SHORT # Should not raise ValueError. - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Metadata Warning"): assert ifd[277] == 4 diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index f61e2c82ee5..916ea56fcb6 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -33,8 +33,8 @@ def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(WebPImagePlugin, "SUPPORTED", False) file_path = "Tests/images/hopper.webp" - with pytest.warns(UserWarning): - with pytest.raises(OSError): + with pytest.raises(OSError): + with pytest.warns(UserWarning, match="WEBP support not installed"): with Image.open(file_path): pass diff --git a/Tests/test_image.py b/Tests/test_image.py index b018b4309e9..069083b1963 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -53,7 +53,7 @@ # Deprecation helper def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="BGR;"): return Image.new(mode, size) else: return Image.new(mode, size) @@ -141,8 +141,8 @@ def test_open_verbose_failure(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(Image, "WARN_POSSIBLE_FORMATS", True) im = io.BytesIO(b"") - with pytest.warns(UserWarning): - with pytest.raises(UnidentifiedImageError): + with pytest.raises(UnidentifiedImageError): + with pytest.warns(UserWarning, match="opening failed"): with Image.open(im): pass @@ -1008,7 +1008,7 @@ def test_getxmp_padded(self) -> None: def test_get_child_images(self) -> None: im = Image.new("RGB", (1, 1)) - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="Image.Image.get_child_images"): assert im.get_child_images() == [] @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) @@ -1139,7 +1139,7 @@ def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None: assert im.fp is None def test_deprecation(self) -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="Image.isImageType"): assert not Image.isImageType(None) @@ -1150,7 +1150,7 @@ def test_roundtrip_bytes_constructor(self, mode: str) -> None: source_bytes = im.tobytes() if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match=mode): reloaded = Image.frombytes(mode, im.size, source_bytes) else: reloaded = Image.frombytes(mode, im.size, source_bytes) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 14a5e2e7bfe..66412a03582 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -193,7 +193,7 @@ def test_basic(self, mode: str) -> None: @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) def test_deprecated(self, mode: str) -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="BGR;"): self.check(mode) def test_list(self) -> None: diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 2c71dceb841..c27ce13d5be 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -47,7 +47,7 @@ def test_with_dtype(dtype: npt.DTypeLike) -> None: with pytest.raises(OSError): numpy.array(im_truncated) else: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="__array_interface__"): numpy.array(im_truncated) @@ -101,7 +101,7 @@ def __init__(self, arr_params: dict[str, Any]) -> None: with pytest.raises(ValueError): wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="'mode' parameter"): Image.fromarray(wrapped, "L") @@ -111,7 +111,7 @@ def test_fromarray_palette() -> None: a = numpy.array(i) # Act - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="'mode' parameter"): out = Image.fromarray(a, "P") # Assert that the Python and C palettes match diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 7d4f78c2371..33f8444378d 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -203,7 +203,10 @@ def test_trns_RGB(tmp_path: Path) -> None: assert "transparency" not in im_rgba.info assert im_rgba.getpixel((0, 0)) == (0, 0, 0, 0) - im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE) + with pytest.warns( + UserWarning, match="Couldn't allocate palette entry for transparency" + ): + im_p = im.convert("P", palette=Image.Palette.ADAPTIVE) assert "transparency" not in im_p.info im_p.save(f) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index fa58492fc76..7b5f7a5890f 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -11,9 +11,9 @@ def test_sanity() -> None: type_repr = repr(type(im.getim())) assert "PyCapsule" in type_repr - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="id property"): assert isinstance(im.im.id, int) - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="unsafe_ptrs property"): ptrs = dict(im.im.unsafe_ptrs) assert ptrs.keys() == {"image8", "image32", "image"} diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 27cb7c59d89..34c1763b886 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -81,7 +81,7 @@ def test_mode_F() -> None: @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) def test_mode_BGR(mode: str) -> None: data = [(16, 32, 49), (32, 32, 98)] - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match=mode): im = Image.new(mode, (1, 2)) im.putdata(data) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index f062651f0c5..b6db0ab5c1b 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -54,7 +54,7 @@ def skip_missing() -> None: def test_sanity() -> None: # basic smoke test. # this mostly follows the cms_test outline. - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="PIL.ImageCms.versions"): v = ImageCms.versions() # should return four strings assert v[0] == "1.0.0 pil" assert list(map(type, v)) == [str, str, str, str] @@ -679,7 +679,7 @@ def test_auxiliary_channels_isolated() -> None: def test_long_modes() -> None: p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc") - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ABCDEFGHI"): ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI") @@ -703,15 +703,15 @@ def test_cmyk_lab() -> None: def test_deprecation() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageCms.DESCRIPTION"): assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageCms.VERSION"): assert ImageCms.VERSION == "1.0.0 pil" - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageCms.FLAGS"): assert isinstance(ImageCms.FLAGS, dict) profile = ImageCmsProfile(ImageCms.createProfile("sRGB")) - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="RGBA;16B"): ImageCms.ImageCmsTransform(profile, profile, "RGBA;16B", "RGB") - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="RGBA;16B"): ImageCms.ImageCmsTransform(profile, profile, "RGB", "RGBA;16B") diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 37669a2e55b..881f9c85dd2 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1735,5 +1735,5 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None: def test_getdraw() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="'hints' parameter"): ImageDraw.getdraw(None, []) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 7622eea99dd..a9444c26d5b 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -152,7 +152,7 @@ def read(self, size: int | None = None) -> bytes: assert reads.count(im.decodermaxblock) == 1 def test_raise_oserror(self) -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="raise_oserror"): with pytest.raises(OSError): ImageFile.raise_oserror(1) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 69533c2f840..aa8bbb3394d 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1175,15 +1175,15 @@ def test_oom(test_file: str) -> None: def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) - with pytest.warns(UserWarning) as record: + with pytest.warns( + UserWarning, + match="Raqm layout was requested, but Raqm is not available. " + "Falling back to basic layout.", + ): font = ImageFont.truetype( FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM ) assert font.layout_engine == ImageFont.Layout.BASIC - assert str(record[-1].message) == ( - "Raqm layout was requested, but Raqm is not available. " - "Falling back to basic layout." - ) @pytest.mark.parametrize("size", [-1, 0]) @@ -1202,5 +1202,5 @@ def fake_version_module(module: str) -> str: monkeypatch.setattr(features, "version_module", fake_version_module) # Act / Assert - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="FreeType 2.9.0"): ImageFont.truetype(FONT_PATH, FONT_SIZE) diff --git a/Tests/test_imagemath_lambda_eval.py b/Tests/test_imagemath_lambda_eval.py index 360325780bb..eec76118af0 100644 --- a/Tests/test_imagemath_lambda_eval.py +++ b/Tests/test_imagemath_lambda_eval.py @@ -56,7 +56,7 @@ def test_sanity() -> None: def test_options_deprecated() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageMath.lambda_eval options"): assert ImageMath.lambda_eval(lambda args: 1, images) == 1 diff --git a/Tests/test_imagemath_unsafe_eval.py b/Tests/test_imagemath_unsafe_eval.py index b7ac8469180..60ad6aafa49 100644 --- a/Tests/test_imagemath_unsafe_eval.py +++ b/Tests/test_imagemath_unsafe_eval.py @@ -36,12 +36,12 @@ def test_sanity() -> None: def test_eval_deprecated() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageMath.eval"): assert ImageMath.eval("1") == 1 def test_options_deprecated() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageMath.unsafe_eval options"): assert ImageMath.unsafe_eval("1", images) == 1 diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index b4a300d0c59..2d6af70eb78 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -362,13 +362,15 @@ def test_RGB(self) -> None: ) def test_BGR(self) -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="BGR;15"): self.assert_unpack( "BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8) ) + with pytest.warns(DeprecationWarning, match="BGR;16"): self.assert_unpack( "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) ) + with pytest.warns(DeprecationWarning, match="BGR;24"): self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) def test_RGBA(self) -> None: From a61a23d7ae054581db05d7eb94817685bc3ab3e4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Jun 2025 13:00:48 +1000 Subject: [PATCH 1814/2374] Fixed PT031 --- Tests/test_file_apng.py | 45 ++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 66410b3dad8..12204b5b78c 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -303,11 +303,11 @@ def test_apng_chunk_errors() -> None: assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated - with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 - with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: - im.load() - assert isinstance(im, PngImagePlugin.PngImageFile) - assert not im.is_animated + with pytest.warns(UserWarning, match="Invalid APNG"): + im = Image.open("Tests/images/apng/chunk_multi_actl.png") + assert isinstance(im, PngImagePlugin.PngImageFile) + assert not im.is_animated + im.close() with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: assert isinstance(im, PngImagePlugin.PngImageFile) @@ -330,18 +330,20 @@ def test_apng_chunk_errors() -> None: def test_apng_syntax_errors() -> None: - with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 - with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: - assert isinstance(im, PngImagePlugin.PngImageFile) - assert not im.is_animated - with pytest.raises(OSError): - im.load() + with pytest.warns(UserWarning, match="Invalid APNG"): + im = Image.open("Tests/images/apng/syntax_num_frames_zero.png") + assert isinstance(im, PngImagePlugin.PngImageFile) + assert not im.is_animated + with pytest.raises(OSError): + im.load() + im.close() - with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 - with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: - assert isinstance(im, PngImagePlugin.PngImageFile) - assert not im.is_animated - im.load() + with pytest.warns(UserWarning, match="Invalid APNG"): + im = Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") + assert isinstance(im, PngImagePlugin.PngImageFile) + assert not im.is_animated + im.load() + im.close() # we can handle this case gracefully with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: @@ -354,11 +356,12 @@ def test_apng_syntax_errors() -> None: im.seek(im.n_frames - 1) im.load() - with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 - with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: - assert isinstance(im, PngImagePlugin.PngImageFile) - assert not im.is_animated - im.load() + with pytest.warns(UserWarning, match="Invalid APNG"): + im = Image.open("Tests/images/apng/syntax_num_frames_invalid.png") + assert isinstance(im, PngImagePlugin.PngImageFile) + assert not im.is_animated + im.load() + im.close() @pytest.mark.parametrize( From e783aff6885c8b5d92f819bd9a7bc1566f1b5318 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 27 Jun 2025 22:32:30 +1000 Subject: [PATCH 1815/2374] Improve SgiImagePlugin test coverage (#8896) --- Tests/test_file_sgi.py | 18 ++++++++++++++++++ src/PIL/SgiImagePlugin.py | 30 +++++++----------------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index da0965fa14f..abf424dbf11 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,5 +1,6 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path import pytest @@ -71,6 +72,15 @@ def test_invalid_file() -> None: SgiImagePlugin.SgiImageFile(invalid_file) +def test_unsupported_image_mode() -> None: + with open("Tests/images/hopper.rgb", "rb") as fp: + data = fp.read() + data = data[:3] + b"\x03" + data[4:] + with pytest.raises(ValueError, match="Unsupported SGI image mode"): + with Image.open(BytesIO(data)): + pass + + def roundtrip(img: Image.Image, tmp_path: Path) -> None: out = tmp_path / "temp.sgi" img.save(out, format="sgi") @@ -109,3 +119,11 @@ def test_unsupported_mode(tmp_path: Path) -> None: with pytest.raises(ValueError): im.save(out, format="sgi") + + +def test_unsupported_number_of_bytes_per_pixel(tmp_path: Path) -> None: + im = hopper() + out = tmp_path / "temp.sgi" + + with pytest.raises(ValueError, match="Unsupported number of bytes per pixel"): + im.save(out, bpc=3) diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 44254b7a4b4..853022150ae 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -82,17 +82,10 @@ def _open(self) -> None: # zsize : channels count zsize = i16(s, 10) - # layout - layout = bpc, dimension, zsize - # determine mode from bits/zsize - rawmode = "" try: - rawmode = MODES[layout] + rawmode = MODES[(bpc, dimension, zsize)] except KeyError: - pass - - if rawmode == "": msg = "Unsupported SGI image mode" raise ValueError(msg) @@ -156,24 +149,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Run-Length Encoding Compression - Unsupported at this time rle = 0 - # Number of dimensions (x,y,z) - dim = 3 # X Dimension = width / Y Dimension = height x, y = im.size - if im.mode == "L" and y == 1: - dim = 1 - elif im.mode == "L": - dim = 2 # Z Dimension: Number of channels z = len(im.mode) - - if dim in {1, 2}: - z = 1 - - # assert we've got the right number of bands. - if len(im.getbands()) != z: - msg = f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}" - raise ValueError(msg) + # Number of dimensions (x,y,z) + if im.mode == "L": + dimension = 1 if y == 1 else 2 + else: + dimension = 3 # Minimum Byte value pinmin = 0 @@ -188,7 +172,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(struct.pack(">h", magic_number)) fp.write(o8(rle)) fp.write(o8(bpc)) - fp.write(struct.pack(">H", dim)) + fp.write(struct.pack(">H", dimension)) fp.write(struct.pack(">H", x)) fp.write(struct.pack(">H", y)) fp.write(struct.pack(">H", z)) From 958c449b988e49aa88a412990bebdb799a97a39f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:17:20 +0300 Subject: [PATCH 1816/2374] Close image after assert Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_jpeg.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index c0e6b467ec6..6dab418bfda 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -752,6 +752,8 @@ def test_bad_mpo_header(self) -> None: # Assert assert im.format == "JPEG" + im.close() + @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) def test_save_correct_modes(self, mode: str) -> None: out = BytesIO() From ef98b3510e3e4f14b547762764813d7e5ca3c5a4 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 28 Jun 2025 00:29:58 +1000 Subject: [PATCH 1817/2374] Fix buffer overflow when saving compressed DDS images (#9041) Co-authored-by: Eric Soroos --- Tests/test_file_dds.py | 17 +++++++++++++++++ src/libImaging/BcnEncode.c | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 3388fce164e..5c7a943b1b7 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -511,3 +511,20 @@ def test_save_dx10_bc5(tmp_path: Path) -> None: im = hopper("L") with pytest.raises(OSError, match="only RGB mode can be written as BC5"): im.save(out, pixel_format="BC5") + + +@pytest.mark.parametrize( + "pixel_format, mode", + ( + ("DXT1", "RGBA"), + ("DXT3", "RGBA"), + ("DXT5", "RGBA"), + ("BC2", "RGBA"), + ("BC3", "RGBA"), + ("BC5", "RGB"), + ), +) +def test_save_large_file(tmp_path: Path, pixel_format: str, mode: str) -> None: + im = hopper(mode).resize((440, 440)) + # should not error in valgrind + im.save(tmp_path / "img.dds", pixel_format=pixel_format) diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c index 2bad73b9261..7a5072ddee6 100644 --- a/src/libImaging/BcnEncode.c +++ b/src/libImaging/BcnEncode.c @@ -258,6 +258,10 @@ ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { UINT8 *dst = buf; for (;;) { + // Loop writes a max of 16 bytes per iteration + if (dst + 16 >= bytes + buf) { + break; + } if (n == 5) { encode_bc3_alpha(im, state, dst, 0); dst += 8; From d07aa6fd17d356b8f09f89a5c485fc8b1532635f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 28 Jun 2025 00:30:22 +1000 Subject: [PATCH 1818/2374] Added release notes for #9041 (#9042) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/releasenotes/11.3.0.rst | 38 +++++++++++------------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index 654a7e6b6f1..2d35d8228fc 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -4,21 +4,21 @@ Security ======== -TODO -^^^^ +:cve:`2025-48379`: Write buffer overflow on BCn encoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +There is a heap buffer overflow when writing a sufficiently large (>64k encoded with +default settings) image in the DDS format due to writing into a buffer without checking +for available space. -:cve:`YYYY-XXXXX`: TODO -^^^^^^^^^^^^^^^^^^^^^^^ +This only affects users who save untrusted data as a compressed DDS image. -TODO +* Unclear how large the potential write could be. It is likely limited by process + segfault, so it's not necessarily deterministic. It may be practically unbounded. +* Unclear if there's a restriction on the bytes that could be emitted. It's likely that + the only restriction is that the bytes would be emitted in chunks of 8 or 16. -Backwards incompatible changes -============================== - -TODO -^^^^ +This was introduced in Pillow 11.2.0 when the feature was added. Deprecations ============ @@ -41,22 +41,6 @@ another mode before saving:: im = Image.new("I", (1, 1)) im.convert("I;16").save("out.png") -API changes -=========== - -TODO -^^^^ - -TODO - -API additions -============= - -TODO -^^^^ - -TODO - Other changes ============= From 41129ce1cb88ae6f7733b40f3f6a9f1a5662d2af Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 28 Jun 2025 01:20:02 +1000 Subject: [PATCH 1819/2374] Use list Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_file_mpo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index adfa61962b7..840202214a3 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -294,7 +294,7 @@ def test_save_all() -> None: im = Image.new("RGB", (1, 1)) for colors in (("#f00",), ("#f00", "#0f0")): - append_images = (Image.new("RGB", (1, 1), color) for color in colors) + append_images = [Image.new("RGB", (1, 1), color) for color in colors] im_reloaded = roundtrip(im, save_all=True, append_images=append_images) assert_image_equal(im, im_reloaded) From 69c0c422c86b1b1a8a7f5fca5e2f464cadfcf7f2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 28 Jun 2025 10:29:01 +1000 Subject: [PATCH 1820/2374] Increase pytest verbosity (#9040) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .ci/test.cmd | 2 +- .ci/test.sh | 2 +- .github/workflows/wheels-test.ps1 | 4 ++-- .github/workflows/wheels-test.sh | 4 ++-- winbuild/README.md | 2 +- winbuild/build.rst | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.ci/test.cmd b/.ci/test.cmd index aafc9290c80..acfac3d1acd 100644 --- a/.ci/test.cmd +++ b/.ci/test.cmd @@ -1,3 +1,3 @@ python.exe -c "from PIL import Image" IF ERRORLEVEL 1 EXIT /B -python.exe -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests +python.exe -bb -m pytest -vv -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests diff --git a/.ci/test.sh b/.ci/test.sh index 3f0ddc350a9..87a605d84be 100755 --- a/.ci/test.sh +++ b/.ci/test.sh @@ -4,4 +4,4 @@ set -e python3 -c "from PIL import Image" -python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE +python3 -bb -m pytest -vv -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 index 54e7fbbfc30..9f5561c46d4 100644 --- a/.github/workflows/wheels-test.ps1 +++ b/.github/workflows/wheels-test.ps1 @@ -23,7 +23,7 @@ cd $pillow if (!$?) { exit $LASTEXITCODE } & $venv\Scripts\$python selftest.py if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\$python -m pytest -vx Tests\check_wheel.py +& $venv\Scripts\$python -m pytest -vv -x Tests\check_wheel.py if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\$python -m pytest -vx Tests +& $venv\Scripts\$python -m pytest -vv -x Tests if (!$?) { exit $LASTEXITCODE } diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index ce83a4278cd..94dbb46791e 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -35,5 +35,5 @@ fi # Runs tests python3 selftest.py -python3 -m pytest Tests/check_wheel.py -python3 -m pytest +python3 -m pytest -vv -x Tests/check_wheel.py +python3 -m pytest -vv -x diff --git a/winbuild/README.md b/winbuild/README.md index 0d3ec8d8aa4..62345af60bc 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -24,6 +24,6 @@ cd .. %PYTHON%\python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . path C:\Pillow\winbuild\build\bin;%PATH% %PYTHON%\python.exe selftest.py -%PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests +%PYTHON%\python.exe -m pytest -vv -x --cov PIL --cov Tests --cov-report term --cov-report xml Tests %PYTHON%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . ``` diff --git a/winbuild/build.rst b/winbuild/build.rst index 3c20c7d179f..aa4677ad595 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -124,5 +124,5 @@ Here's an example script to build on Windows:: %PYTHON%\python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . path C:\Pillow\winbuild\build\bin;%PATH% %PYTHON%\python.exe selftest.py - %PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests + %PYTHON%\python.exe -m pytest -vv -x --cov PIL --cov Tests --cov-report term --cov-report xml Tests %PYTHON%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . From 5732a86cc6f8aad4a28a92b7c7c78748000c8d1d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 28 Jun 2025 10:52:25 +1000 Subject: [PATCH 1821/2374] Use snake case Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/MpoImagePlugin.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index f96f658fcdb..784b6f2089b 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -46,12 +46,10 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: mpf_offset = 28 offsets: list[int] = [] - total = 0 - imSequences = [im] + list(append_images) - for imSequence in imSequences: - total += getattr(imSequence, "n_frames", 1) - for imSequence in imSequences: - for im_frame in ImageSequence.Iterator(imSequence): + im_sequences = [im, *append_images] + total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences) + for im_sequence in im_sequences: + for im_frame in ImageSequence.Iterator(im_sequence): if not offsets: # APP2 marker ifd_length = 66 + 16 * total From ed82f4d235eb6a739699e8485748204818393442 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jun 2025 10:57:23 +1000 Subject: [PATCH 1822/2374] Use unpacking --- src/PIL/ImageMath.py | 2 +- src/PIL/McIdasImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 484797f912c..c33809ced89 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -308,7 +308,7 @@ def unsafe_eval( # build execution namespace args: dict[str, Any] = ops.copy() - for k in list(options.keys()) + list(kw.keys()): + for k in [*options, *kw]: if "__" in k or hasattr(builtins, k): msg = f"'{k}' not allowed" raise ValueError(msg) diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index b4460a9a51f..4c34dd7e542 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -44,7 +44,7 @@ def _open(self) -> None: raise SyntaxError(msg) self.area_descriptor_raw = s - self.area_descriptor = w = [0] + list(struct.unpack("!64i", s)) + self.area_descriptor = w = [0, *struct.unpack("!64i", s)] # get mode if w[11] == 1: From 4ac24035320cb213d1f0175f9b0e53fcab6d3445 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jun 2025 15:48:44 +1000 Subject: [PATCH 1823/2374] Read 16-bit images into I;16B mode to allow for memory mapping --- .../cmx3g8_wv_1998.260_0745_mcidas.tiff | Bin 2880134 -> 1440122 bytes Tests/test_file_mcidas.py | 2 +- src/PIL/McIdasImagePlugin.py | 4 +--- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff index b72509cc4b3c57c901fb02c24c2b201e94d99fa3..2ebb287e26d6901319b9a05900212b9a65536ce7 100644 GIT binary patch literal 1440122 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

_J#pltRA!g1U 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 z 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

+`seb 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%0A@LyZ-@ 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 z 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

hlB=^ERxexAz?HReN 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
(dINmri3yrq(IA0HJC#h4K-zTb1 zwp~vH>tao;hx_jt@+^pLIi;=MGq%BVqqMKzjPqkW>$;=1gLzhPkxAw<$^C$9{f2h^ zq!%()$`$hrq%<%Onb3nnjxXk|P9OapTgPXApkH6mrk~?wLJy4X90M-3vgX$8r*25= zoHY6`X;ZGpH*M;ZPTRe4k4A8~@BSXS{4N0 zjJd%0mpR3@y!tmM`=en0207jsr(i$nw7-#Yl*S!FL%LTo3)XW{S8JOL+Oqz@#V*DW zEtR-g0h>lnuxc8c)6c1B~GsDxLl%zH_)v_v*5D z?qA)@Y^QA4^Ukeu7vD2dZMJ*h9jf`iAy}zhfM0oQ%0>U&wrFZtByv$#=1a8PBS$rTyg49@O|+XZEDA zH=c#Wy}GbGBkW;|J$9dMZ*&Knc5$E8FUa$e@`hg9%Rna$YRo}QVI*;?6 z518METd+T%`nS-j7yB>Y{5QTAdi)!}@!tTRe*@X{lk2~KJdDn~4~zac4z{HK!`v`G zu+jZn8sFkRVZ2ZJsvC?=7VYFlTltCftMZdy=Nn%#X;%*Y#`iuMg-tuTw8d|Nvfx1T z8yZ}{wed}F6%K9b@m;X?)m75=Nv;y}Pwj#4OOAQ<=A5KWU)#!`Hg{|U4JoI*W4g~e zo_WjX9{8-E&;#4*8aB~qJ*d;B-;4TcGu}5kw!<@8Rxps2)GN;~Xdl0zJvkA_`IDOC zF6&aSk7FD^VDgS>uy`+B-cK8v?<(I}(Bb;G7wj(#^wJ84ztv<09r+3Eski;2?Z!8~ zf&7Nw{Pfe`7$?Y_GQZR-)m?Lrb@zBTZ|Y%fUJg3zXB~G)`&mcF4`R)zo3#`Ba)O-i z=ub?~nCJR(z8CL{6LjAl=x&_Pg|EK{%uj5qI|bvVkJ7$A<|(sK`)Plru^n$5<7Mnk zJ4wAhpU|HAh0lSU+qDl^1=pK+#}jq>4QPSw7j2t$if!!|GLF*z3fsibyv)-zWxj8& z8PBnk7zgsXq;#)jyJz5DHh&wy^>>250igRk0A{dDJ9Q~Lw&QP*!12kT{WpB_H=j=! zFZC0j2Cl>PG_ZDq^=EyvCM{~IP3))7q`n>8_W{<}v%doGjqMia#BtXc%(ub0#rq)U za{}`-&&0kqeH`QX0q&K1X8y*`95c_Fqj|db<|>{Cb50S4^lFyvY zvqmO#IPpC5nU$#fg#O}K*+KSuu{Gvg@l4C-jn5jNN#=IJJde4WH*0hK)}%$PtXZda zGU0$V9dNBJ=fktP;Rb`>vMZ>+J3GHKXW;KnzuU-&-<}tlbbe$0iS%7Dejpnd+j&0C zt?d)~C)$l;JXwWHTg)xn$^`=&I6h-3GhW&{I_qP-terLNanE+t)*8!z3;O8qnlh$R zj3>q%5m#T^wVyiu4mif~l}+DuEVzF_9~scVv0}Uq&f)xxTwI^)@f>;ocDa9tGq>Un zde@uJ2k)A9inA%+D|OmEpNkmAvo?#^*2j4^xIS|*Cv&T))Ar*Nc@69P;e2oONqc-8 zbHR1x{4?fv&Bi~0wQx?^LH4Wc*j}oA$5_`O*;neMu+*AARm)K4wef^c9e!&38=J+0d&HaV}&LtN-PtM;WAM-Lt zr3}dYjO83++gOf2fquq(;~HZ}Jf)};=dz~O+uG+I*nU~)2DlF=ax_NHpEC0>KQW(% z-;VhfV;wJzSYi&wz2H2_`H;saoq1>OwVl4k8v^NfAAhQ`ggHuKz|N3Q0ZsQaW(yYhs;evlv43~OJoFRO5Ai|4|4$)WuX=daY< zKD9Ugl}le-t7|@BJvQXtDc1+8z4{z{Jqr^ycn7_Y3ohidWAgm?M*4h`#xT|Z<2#r6 znZqUT#J;&?9+^vv@y2*D=HJrrHMaAALR*q{^GO!%0oGb($n4)(i$2M#GsXeqJJ$yD zT3@*6zfs2w{C*>SZ^X-e%Kbd-t>n8=sm*b&uXCmc&!x-r$$QYyHEeavV2tdCOaGIS^L+?HGKIZ*Y=Ka+6 zqD^`kV_VeADQ)#)+zkVK#!PT;UVJY7>=QEIO;y_7(A-O~-QlpuzJJ917We!`y*>wO zEa#L37tG5XrRHe=fa|e-*3!En)^^j+x>|=BJ_lRg8}E)>@SM7Lvta+A*OvB;&V65S z3-)C^H2Pv(V-0YAabD+e%!L)$7yF*U`JnD%6Md3FTUyA}U;2`<4&#%?Z{kGVjMxKz z*J^&Q-Mp12ux+AkP;!GXrVd&vC4_TNPcqZ8{33%#5V^naK)n8=rn?xpcv?*Au)_?`!#u?a=-c>o0uX7>=oo zGH2>j_a~YDDNo6##XVDQo@+9LjyyaMSKu=~QLkP98e5Mzm9^y9NEF#;9#=`a9ltQiq@4D*j&i zzcnB#x0C*lwV&ISp_w|3*|@3;$#{cw-|A8l`UY+H*XTJmDVTyOn1U&o(qrtDq7?L6`<$#>{V+m85<>h;=G*x+KuT2^Cjm0MEX-^%phKDr{2&H^;&ZWzgwKg2%Mkubk5_! zwMg{2KCV-9omjUHuA^(*z_so0bM`s^Bc228#@Wvthg`X~t<+*&7p&8I<$OTDaRaQs zNBwWDo3WIeVdqwTmFAaRw56}6C`_TJmz<%zl_x^g@lX%&2r*(3? z-0&%lOVXb4i@wQ0Z*d=5qqSPIbIJ90&s-1pz_oJSTtC;*^>nR0Hy1pYo>9;2gbg?F z8*>%%8+P)YxdXp7WfZCYVob&U=IL-^499b9>qz#OTC6MMGv*SjO`jYvzc`lcqTB9Z zoY?0@d+lfZ(8gFk&pvl~emAx;M7kW- zzi0B0Z*v|O`6puy#&rB1wOCWurA?nP4UD(8VPA6eg6(9Zi*pd;FLe5w zxr2-R4eOK@9AqW!$`0l-_kaeq9`)Mpw5@%`Je*6?&=YMdP}lAngjvsATD!PIT@z~*0_G5<8$4`IOFYGb7;@F5qs&rt@ys$-0O$Cet6&WEmGOE z{XS{nw~DOb#@}-T={JqvHi>QR8+!CJm*X_X^~`K)u`cVYHEPc~jjNnEYx*zPFW1UB zSF|7eFKN#)&DFr%iEZsWxFNN*2W`m?T`4!{WuA`vqW>lPX0Ac52G%&C7knObKV4t< zRD5Um*x$w4i~C&Bb^>GVBl{N|BN^z)iaJtW+SaM8Xmbv`;M|}P-3A4p%r zXS~?=rDGGvsHDA0{ToKmkcsR4qR-e}u&v$L8K`HR<1IK~?UkI9a{ofcwK8|}CEAVI z!9_~$t9zMfGuFO6xYTd$hqLMV|2gxXRqQ|U<=wi`lg9g4d@ujTdi?g0PkpqTf5tc& zx3J57Nc$%_h?N#(-cIfl=i9Y#o-NiuIbg#LX}dw)}^w7uxpwgdIrC-p;|`|DokzAJO@C-$VHuNd3B6Y{xO)TqsT z#_u3?bB>2Pj{U5$fzR1y-lDEeU(J2VZ@vZRA-{>W>mOhpoiWTcOU{MOyh_Iyj)UJA z#}@Y>v3Pw8j(D_?` z!U@zXX>XmrFoKPg`-T1w!oRSv%PF)kzXML}@+W`B{ZGoxxN^TR_^nS~zqS<|q;!7s zlUdm4(){lLgMpL<7dpTHiN6J&FEoBnlNk);{=&unH`w2}D%Hn-1N;4{Z^op&^bzmC zy#G>a`;Cj=JO6<1fep8iZKaH$Art$Gd87yCUO}y)VH0f&Zjk+yY11zQ8u%XQf$s)c z!9a@d4&Na%|ALHFUdTK(mw8$t{}%gA`gfXtd)@du%)iGP_&3^w{sn!;{DyfF_n`A|Uhb27 zJ6!CdU#aaApW@6RCumWFby!bg`y0kwsB_e2PRDGJ&$LopOVO7M^iM48(#co3 z*gj$GfL5@r{T8X7`P9?5Lf1D7w)G9j`s-ZOJu99W&yLUU<{bGfHt>1Ba zUUMY2t;HDUX`dPWFVb~%T{qOd_**^qETwtmZC88dUhK8pAfL_mIjqlQ)oXm}>CblN zex>7%pdpj2Q#qjro;7j3l7XJMmKz#)mImwX{@${;o|PGRmc%oZy1or9_V3+0_d0Q$ zWX9MVc?0#zq+Lnh{Su#k`!w+EI4{r94xD2P4s#XfY~2-YNygiE7cTmXsfc4ZGC+!yG`YhH+ zI$ZYG@8%Z!JY%18uP^(NXw!ed_}>`#zp?SjeXt&}HaTGJ&SAp^=jdDyxE8rao?-WN zK|VV^M{+)p_8Q~;Fkr(CI(OqIR=Gb<(bI09%u(~H4`|@n9WFJCwM({@`i(naAN%gY z4PBY~KyP3l`}PO(I?f2xE0=bX_JM7)-jZvcQd=uIvafQ%fCk1TwzaQ9wl6m8wtnZb zV{Z2k(l{B=3bthj7b)(MxM!(rAK=*5kaTOIt_Id;jQxDK%KpH`mha|oa+UXY#@O$- zzWOFOzMQB-yZMrXo}_L3GTx@Yn7f&ayw20PFXs$O=dVp^dwii`&oiRzAI^^U{lV_@ z?Q`xk@3Sv6Xh`n{?}#4Kw&?qWK4Tkq&f-p+@s9Gn(`akUdoHCm86VJoq27t(%e|KO zoa4)aV=dIWoeT4Htz6T_dT-Y}_SL<0FO=?w_Lm!d7E)dRj5ynAJJ<%fcA%cL7$f_c zYe0idT@$QNsm(DmU*-4qENZ!&$AJ`e&*;zlCeEYhQk${0Zu6P{FqYr%8!o*6{-7<|bb|yf6w2J-O(WPrnP!e=yDm{^mEr_RH^s`bMykFX{VN zrFm|l`y1bb{@1pMkps=&{t6RacHyEY`MaRM4W19=Z-x1Lp}!e!Xz`m}{f>v)>3DE639=r(j$E4koe{avsjF!{s}92g7gX;PBo^CVJ9+pA^#m%8a?ZyH+re zFU@xwZ2rEIY*(B18FZu^{ytlUfu3kT1>4^+E`0<2Z^=G~KGwFung=YnL9Rot&tdIy zja~0#qq`sOk9+3+-GXiH4UAb4tCS9x97+1^lYLLbG{>S2>$Kh$I0xsF^U-gtIqmD% zna`ZJFgSM={l>NrXs;M!erxVg<7P}5g@#^nEt6T!q;s=v?1l^Gb_`MPpj}zfmVMH` zh&f=*8=R-lrq5~P9V@#$v-*v-k9~Wf?p*a5bBv1|iyWPIb&GF|&inpAi*JOV?*w#h z75yXFNV(8?$FC2hT`9(j{RWtOg5Me=SblTBg&zJ!tk@?>Tjns{ehs$YA>cemte0!( z8ss`?H)cMuE#h4(V+XhouA}Q&*E#o8`%fHw9Milnj+1QkWJKN8pYzQ*&6vM-F=mzd z<=pZ-{OqrL99X}sz#1>L?lKq8OP<5Qd2HZ$zVW>JoOAGn|W5$sDDBO_gofq z@GK-d?weDdKc7L#XY!SvDLEnakMhiRp8dle;XQFVhn~fs^N93W?ZMP;Ed8y`nv<+s zd)A$`4{B}?=8$Ut$xB_{A>Lm;^FAX7)PL>*?+NO#)*1Jg^{ZQ(&yYC3iZ)~HSLddF zur2(K@A_V__N?Ew_P=EiYpw|$%xx{!kXTDE*p?kKeLuBP-=)rJtz|#FH(!44lCZmP z3+y8;a+pVKr+uT}K%f0Gx4O3BI)T#o2KLwQxD6(Ex9=m{Z$Ibayqu$J;`%h!%Jq^B z7hLxPo~g>DE&Gwx)}oL7leDGG_)YA6sL!^~=KmBmuX-KVI%d$2lC`fmhaKtKi0dRH zXh_dno;BCkb#|}ZC-*M*$vx?Tx_0-`7-Qvvd+dHq@_2ULuUoL4Hpg`@twAZh=-ICQ z(|<~wHZiVB^%k;c)a06?14$j3nZMfh%EI8n~-f*ElC-wO`@of2gz09~L zHqv|X6M1i@T*S!@ne9Pa+Jm{pu{v0f^^L&V6Xznf^&4v+`|jY)U)#nQYrj=6mpNz9 zkdp0WA{rE~3NuGvp} zrS^ICQF5!NU;Tpb(aMbPgnaJ~-o2Gg`x_UZeDiCsF;yS<96M`V)b`UZ`7-{K+!1G< zN~0}@+~#x6jxkw7&yV}D!SiF=H5PRlfpN)n57|$jRi9hg@!V=}_>EB}+C2mIY2b4& zC!T+8>K(ivyeBGoXS~uF@0sB}R^C;Y_mJ()JLnc{ClkG*{lZT!@_DD_J=PfKWnhyP zT%`3o7w733xQ4EEW6j-D*FUi>ExNv>p^NQRux*Xj zm$gmCbIh!%QOAt4=e~4wS?D{?-1OYpCU!d~=4oEXbi50l@ALzWe_I&k-zWUH26=6^ zrGfqIZ>|wJZR^Vz^<<-8So|A|oDZ~4Jj{np`v&75ad^kZ|U57m%B2tEfWqj??f11NPm?XyXljp?UmJ^ z`lX+Ao@t+RQP(!0f%RFZSo`ul0C6S3bya5U?P)- zE{k_r{{{V(jJ^DgmTdIOpe@y=uC2lG-^v5q+Be)ly^{7%(t51#fOEUfiS@|!t7}^I z&Dy#b?#T)+(*1JpPM|Iw^cy>%70i`rH^z9+f#+ev4Q&7Th&j}0z3zkc=NzoxS{=t+ zatrpC8DzW48n3SJKx2Gs*det|FlNCB8qyl1gSA?hj0fkCY;>8>9<=GR{|P?j)282X zt;3pDaFNc%n&rSdC)>u@Z^s;*N26_j`#Ar}GuwQ3!|^@tcLogK@4lyl?srR|uFsf^ zHBNg{+xFYgeBVE4*WbZDEAoy!yjV}yS=0|`*MEt#{{_c(jT7tnjT!SCu@=rJ=ccqR12(b7o4dncyh;7et36n| z_8X`tX-lbnK+e@UHqKG!Y8gqx?|NEQh#gEejtAjOsP$uxtjU>7H6Hr^|D>j z_RX)a`i-+~{|P5huV~M9N6$PdwMoV()21|T!T19Wzjv3F@!8aWz&p-( zQP-y5z6}=7ZF0&zk$T7GJ+k92ne;J-bzHFaoQv%N`eZ-j+HVEckh*rUZGOk{+4mXn zy(PYvu5IkzlPzlci4FZC#hR_(`MoUcVoYM&xD6Mq#r3lO#CC(MHKjEt9X+Wwa}M(| zj`a=jZuhSA&hx%gIxp?|a~{?>!M>utqrd$c{Hzsn1llI+tIQp+EQ9KC56`TEX@zT=X7ivhJz->NAj1J=@kfpcQP(4zgXPz6+V_ z&F9=bV-;(&M(b|DjyY#r-+H*T?VGs{x-`cBjS=@^1j^qE_72E~ZX1&hgosYFQn8bK*3~&y~qOIMziMlrR4d%UnfpL{VyMZ}sUUQ4% zXs=A#$ws%om`m1!wg$%9uLtU~LBAL)>p@!!jJKb)4RDP%Oz;j~-os_zbFW|N{-jO4 zlJ=CmII>_sd(fs& z)EhXaIDUsy&YHS5{bIb>SF)|tKcR#BbmJ^5Wfa>EoB8c~!TOz-b5T~bB^^EE)w8WF z(KdsFtZSvNO@9xm?Lv=ttMAwI3ypX2CwxyQ8(p$pW%{bT#3tGsSci4(s7>1h{r3y{ zl8K%)^cUxtV+_V{OzN!x(-0r%d0caIx;>+^fSeRhxa&4^JK?Zz8pALErW zLz>4s!MkCERM*z=i7|2lZOwS@pTqm-@-C7Q@1pIyD458kqf53odLze*d#rO;%^>eB zYpQJ8a=|*R$+3)jsON?w7(7-;II(FHE zOO3MJ5A4=xAM>2Zr+%?%A3;N!&vKN%m;-ib5b9M+fl9YbuV&9my+-GO@2(8cyH z*w)_3a~VT2r#9%7c1*wLJO9qGKl~en{}!Qr zf-%Rx3FtGTR;B&*=f7KIAN$FL5x)&yWbgmQM1MKhr2XUn%fR-Dja}|v(97?FufFiT z9Bh^OwA0u4jqW9VudH!{e!sEtC&s0I(ItNuT=Bc0zYSKjcTj5Y{0=z5-vpcgO(0Uc zd9AaqVUM{wjxomjJ7MQ{w;3FyLo7}+q z=LZfp*TQwW;QG4GuK5M`pt52=57NDQ_f31nm_)ldCmhCcEKzS^-$-LdjI%=8S6#n- zHnm#2^O70!(B30w<~6t2mIcl=>0wL1()bA%95?5>nU74crV%w|esh`EItDbbPV37$ zm6Cf?_o>>nkJv}`+TS>BEl9s(T0@tDe57yA#m z{GKQK7c$29iaF$fwT*b@tXDnjHDlt#pN8gl>tABL%tA+(v~Bdnyz1Zb55J4QjIDhYF1n;` zpi2YCm~i|S2fxWL^K`BbI}Cq?Nq(F>Zj*zGuO+lXsN&l)C3c`+^Iemqs74Uk81nu6={!%8Ica zUtOF2QTjVhKI1=Uw>m5JYpw0I>r21=%yYpp>-gG@^VxR(%1%2;`@rrU zAiI$5iB0sG$NuJ3+PCRroqeZ^x_6Cs`zcf3=;pBhpuQ7z*V=OqoB6oD3odw0=7Y8u zW5s?08kno1P5%Y^9~e9fJD$DHbLjKfypIar8yjw*-rz*M{p3b2^VPiOG}b=zL0iQ* zyGs8~b%wch|n6O&T%czU<&4CEI_J#yVb(f6&FctS{N< z)@{>&R^bzuzf~9FtPOqzol;6tA}rl?so>? z8-Aa(FX%JI*aeOL8%Es8)^Nf5yWR(Rew>TgmJJP7nV0?Kf^(Jht7}i+p*`vUVqSUe zw$t9w?VmXZd0U{qgNqd7WCaIl4w;3t={L4R=6Iz(V-D0DshdlmG4|as!1bN6_pbMX z0WI(hxaXhHCpFe~#&+~1ZGR(o$!RUgLcicSwPtH?F%Rc+p~oDZt8;e#7d$H~I7soi z5TB7s+EZ4a`u;#ZkM$W{JexQ8^#0Lrd<)bQZ8?r(cChB0gEHr!-&p%DxPfE(9>}q^ zr(ZdOhLmi7lg2oXwK)C&*LDYWovkhFGkym4PhHu;xXd-lmFLv6Ds?X%%W)QcTFIrJ z?Ts$xHMRv4xj~z#k3ij=lYSQ#b3Kr2r=Dor;Ct5hcCyebm-dVAXl=^Mr2V%XVn1R3 z4mqZ}w!g)Asr}Pd?JMT&8cc9qR$-uv>*zkT!2M94#n!R?>^IMDKFgCmcRw!ZS5^jX z`*iS`udK+k%iW+Y$yoa)+6H*1c-I`>K|S!@CfZW3vKPBCE$%<>Fmril9Wb{wRa(@l zbo>PuoZEJe%-wbTblw_!kmNqN7h=2mdhF5pl550%+Q+^!!G0_D(S36-a_y~AeYrpG zDX}?b$~TX>oX>vFfajs2-#EuNhxIME1=}qcVV|7c^My}7ZPLNq<`nZaa88$6uJdGH zI{W8-x`&>fhFx+kT%Tq<9O}2fdCa$hi#&|C`HuJB9ghFr!0&f6P#1ld7-Q`t)@ncd z7{3E``-u95(f^Buo>aSf+Lfo!|G^xCiG11EK4F~p#&2B7K>xY-$=4y1Pxg^{5_TU_m$XA z{goY`T>tGj@ZX9@!M47P%^ca*muS;}z`C-|jh?E3#z%rn9I z7Fg@`+v&l1B?i8_1~~ugw-~r4?!9ZYSu44*X4+O@%!_A7n{q-UpLq}Yt;aDB zI0xr)gW>lXtdMCpMrNUfp1w;v`=`_<6CBTR9p5_YT&>SKv+i2E_9Xp_xs8JD>{DZ0 zGtZ#suYu2rXW28|VDl^_J{zA%|0|8vrj!mV?%MpVZpZJ0wg*1_#u@MU1AJ}=wfUS_ zuXAw@vca_(@G~#xVn6aUavuJ-NR`P%zuNIT#s{!Ci<7h)o1{(o#$~%o{9Z&vlIs za}dXMP9t!R9b1dKtS@WSmhr}k{aT=|?S@p(cG?Cu>u@gaoAXL+Pwd7R?^q{L@32bF z>aTIy?Vqs&T^g9*u|-{WLHnn=w8?#={zI%j>Zqv#X$1q0ec-B;^m ze?+ay9aNio1N+#wgKIIsJ?-E#=`-iE=kwTL`d$a$@3zN-acMI~I(Y96*xautzA3y{ z=LcSU-oJ}}-oX=E+~bqnj(5SkBkzbt{m$clLEkJi^iM3-;Ianh-Jq_`*bO&OpTE%X z$)T^9ql5VdvSxm-A8@WBdSf&O$@?e7mRat*9R`Za--EOn7EIKgvxz z<1@GY9M5sBW5hh2uWR61$O(LBR|f4}xadju`=H?WK;_U*8s7`*%1ZTD`zJkPtNawF zt;VFjp8DQ)$M4*7z75@V$u;We;@Yji_00Wsk1L(_a@}KZ-J6DfV^8!|25o(_#4q|t zFXSDNQeVcsGW!`jJRjUAd1gE}&2#h1eO2!)<134H|8Pe+PO{PEg0(q+=jl4RcCM%E zy93u;Y->+!YafAn$9Bje>e^PpSZ!i|nP9AATZ8LAV!d6f37dAqXI}f5=gnn~h2K1v z*bU~hUy|+IKXDBQ@SKVv3iSflfIzuZe% z=_+8K6gk-|OuHQKEd9CpoTjizS=>KFP zzj5#-8DqQu z`u7nW|Bd!x(0=(AbN%$>3v zuDg5DVt;clQ{J+_+UA!y?dCC``P+kITf?Fj$(m-=rQbgGyj_#Qjlm%ZTqS<#=kp2_rE zF4otaj?w%I1lDC;@sBf-+5{m?RlmbYfy1b+`|=T-2EGWIE$Ax-f>1I@@LKC z{QT@My6B8~sJFffWnWC;u-8qL<=F`xfkMkICiI)x2zRX2h zw6$L_-n>25!#w7{;95>NAINoH5wA=}*s8Q{$I051E85obv(~J$b6(8db(n>^E}a@WZVSRec&0NVu7u$~K9Ic~KpQvloXN<88a;=os^&1_(&(VhAJ_YL9F4*6^=6~7P zM0*R=wOz<{a;+q5HP*2mr-5>Vwgcv{f3nfF>03d@=}Ui$m`+S$ynP+}e84sQhVd)v zzT^<|i1q{7cao=~U4M)uhnb2Nu*W{kO7pe{SO$PuyXFZ%ul$4d@+ zlC?RnPw2Ow<4$m1(xSbircvrJ*0%i)@S%s+xm^m*p%rv{u2Yc z^;vgCTg5oZ90xsd?8I@Cv>9i=0gf@jHCvvEI46@k(L2ufzyj~Q2AlhD20!AMGdA#EuSKHp{bv;Tz^?<((S$8mgXvOeq0v+X=yoZp|w zK0Y704##%`w0LfMYabKBsY2RMJ%;y~{2V2_iH{_^@RG17Uq5}!3W(8}|;(dEM6u95Wt z?Z(u2+llu1ffhDpV%&xc-^Tk^gYhpJn|AY>e?kN2-N7~4&jMW{()cYandr&(x3J(M zW$-)MD&%i|o!`*DF~g^n{e|@ZNxuD!Pg~fV@dZS(!cr+%Nt z`(&H{$(($S0-rHyaCpWh+<4}O&t2esn@sd1Z7I{P?EfNui~3DEAiq<0{0;O;zA=+n zY2e>X{+*L_bpQ5A{99-g8oGZgiGMrwg6-s>|A~z4k;8vCT!H^~*aG!rp~^>i=X%LVyvaj%oKUHoEO-}!)c{l=NE8BhKj-by?Sap-v{$KhP7Ow zpI6V^i*aOt<2rr=Yue9RWzDXseo;40pL1>CIvmc>g7KiO$66UDwrl+M9LBTM<9W)n zs~pfC(w1j_@!W`QWA$&ifx14QwFcfJqCPlQh0!`~`C!nB>1O8P9sG*FBi*&vbt|AD3?+zsJvm zwx92y57IuBH?I0t`PSBMthwbB{I(ts+Ke%_gZ<4nz}h{>o?XvtVq5zR)b%;vQ_iVp z)#okG>)@<$2$LwPruJ$&dIx`K7-Cre9o3Wa=H%ov;T_RuHA;lI*aSA z)Gj^9w)I%oC5Nb+D{T{-Ykxo74fT0n)HdKU=frl_Y~2~JbZ-62-RYP~i}9VSYva6s zo&oxLR}I?QlZ!6KuLo^XW2&v@IK)}QhFh@R0%IyGa+_y>^;v6&pFLyFu7himYo*Nf zxUsMH@8n7@?H7FqvX)6r$wrs^3mG$sX<#mMI*#KHXy6=_wlAz0)BYDcD@mRieG?jF zjkdMlhaUYK=W@dZ&+G~gQf%uprpAr1SD*g*3k~04T*n?!W6s?s`bdZ$IoCz;nMy@j{ix%@ipg&ajI-feWV$GM-anyJ4+8cKJPRMl|td-CC z;raDBA8>ijebyWFxEF`-Cz$aK;&;g9Js}GQG??){vv`;IKH0qg6Te6N9uW0UZ0s`V zYYyK><}|nC{CxlTZUSptV4WMR-8pz?IVaflkNH#sGbai1`ceY3xPvcGb` zrLKx~PH1qbJ#ik=Kj1sb`O0|EcE4ag$GKoV&N1tKYqnO|#L9#goKl-(Ip%oKR_%?p z9KTmXAIQpn+ST{!pJKJ$AlJ<~i#1!n^GG(jT*&#lE~l)`W{q;M2m3B7xJb!%mD+ty zD?Vp(0-wLcXLG|17WbW;LfSgEjQb|dHG+nGu?B6%Y`B5B|AvfD`K^!s4FeiDZU@(D z1nPCY)U{Q#iG5`TCuG_)F6E-n0Qb>-b$`c$``+Slx7ducF?fT8@m_K7%12tOP0Bf@@_uvwjxf=Z?bLdOVmke}i;20I{7p!3h=CrmQ zsB3eaBx7yszu;LtA84F;nFZU%+i!z8MBOv4Pi!yh8=kv38}8qYeR56Y;1}oSe9UG4 zI%evdvHf?61DoH3r1gKIKK-32P}i1zrR}sW+H(Iz{2eNp=*dB^_`B9OhQDJy+-NJM z^IM?gZ-V)IptAY98Tk9zpV-96{R=YozbkVLf9p&9txirMe;=IuhW8uB+1I?zyF*&!(@x4KDchyI}tha>6Xs-|asAHusyY_Wh>+)_?pbH9u_H^4h&yyk{zd zc7qf5sdwyxQE2Edn|J+hO#GEjTMmDFRMM`Tg=(+yRbS*9aQ!#j{QJhgap3SbkNo`Y zgS`ALl;qz=DM$RRlWjH67uZj`Qk(hAd!bWzWybuK+KpL(dZKNBbvwo; ze+#Tdtk1ovxE7*K|BBd2UvpTa`K;62*4h|H)V1j|<`Q4Ae?{MjSo`$opE<36f%Dw3 z*vA=rJb1tEg74d;^PUyow=KRWF4Aua-_-|t`JPaJF(%1A*{(ACRh#hx9Mku`HCW36 zYqahG4Lq~rypoRoqVGItGtPAx(7<&Pb?q0juVXrP<zZZSW9AU8?3bjwl5fa0(ET@oLh&%TwM>>#Wk9&)!~du zo;7Xy2CRZ@X<&@>pyn`-47i->pYx3Dobl!}!Lv0A4c$BU^1Dxi?Qc6ULuOm4e}S<* z?rEiM?J_>luqFCepl;p{V&pZPfOzQ368P4G*6OfkuA6(~8j9;VgNCe}zu32oQLczrH|GV% zbf2u@=WOy0@ZN5mVbAsU%<~NW+#~Fr^Kt&JgX@uN;yO6bO^kE&*|VR|kU2zMn|IL9 zb4q;Xy5w_>E9TZ-(Nj%B>4Yn!m)f_)rQ&Ii)>$~U(9)7RXuAZ=CZv%R2$^;p+{2G;AG8+dLn zdnoRsXge_QJHG?=HHWB61AQ~dxT~FUtz&*6>reT{X}5lJZ5YsgLBDY~aNhQ}j*7OV z+8xgtHVn?#0ng-w4c-?o`ixsa_OYMX9$-$nk=uBE>DR7*LJQQj**{}bX8tPEw}`)y zqcd06q=9?X3+|QnQ7|^C_J-YfbL6wKI2&Tyy!wo@-v-Baon7NPr<|8_Q`cvV@%A%E zjx)&J3bq|j{{hFkF;bE9gka-q2u#R8O?+2W`>{Y1`N? zm^<^Q{z~I2_8s85bXc)&JDz8s>*aIKoju|39`Ie@_sxKx?{~l9eSi6l6D;KL-2t2T z$OQkc@EzwjzzBTLNVY%8h5vy4&9lMW7pC75)FIX*o%&k5W4x2xf9K^HP}&yt8&X}y z2ejY7HF`;(aRVCU`Z<2m(dDq771virth4V&*V;LVYyRRMIL~j~SO@*a9x$(C9Iy`S z+7H?)op!A)>oTXg#j)P;^nc^vt9+Yh+LeyGKcM~fU2KW-73U!5!TBY1-W$97=zhwA z3-0}d^M&ebJXhlL^(QiJ6MMnucEN$XOT1fBf2DC1`$`YYY3^_67vodUwl;IgDNA~JQFV+n|NkM;CV}3xdZjQAJxre{u^wh?H1D7JbTX7 zb*NlyvOs@>T#FfNqSPk(jWx#s*Zc+>DGg?~AZw>zY!-NgZ-}-`qyoKJ0g|wxf()boEr1lA?(EdTpFARKt%O+NCk?PX< z?QebIU{4yqd&vy8zk|UI2EU1YBK;{p#Vq>%PjLL*F6jIwCo|aoMhDIRj;}EM-40f8 zk<$P7KV`O4u812*F{iap*1@%EWnFfxjWso5FPL}2f%YHd_`t??|3JnllRx$Ol>dzS4r@-!nn? z?;HNkS;6pco}eK=q5qTplg-!~pY615{@#-5-(BGEfGbefCi?e-HhrQl8yaISaBdr1 z2iMCzx{!ORw7o%}sLKtESr5&34lLh`+_&3zl6P)Fo4!Ge{T<)+w=Qe!fx2sP0&A{% z*6$uSY8rvMwo6W9>~qOwKIMvh`UWw^B@10!3)C-k@|p8IW967i$NhwT#QxG@kuz%> z)bj~z{SEu%Tsm`^U`}hdZpXNR?FPo3=x5)|k$LnPV;=h~`$XN&&3SHc?{ohTy8Aw3 z?|=4{y*+-5u+QRtZ}1!}xX^jm9^b>fqX+EZB0r)3lRa(S?+jQWwcBSvgX#Mo9DhIq zYqGXVr)}-Y@|*?(DVMn>JLc{DrF}sAg+pJ*a%^i@a06?z#sk)C?Q#pwZw3b`&2JUh z!9~8L?~{CEZFjz}Bz^W9(7-V}Eb1B$+N@W9Vq5zN8gl1ca}8n-T<4#4;!NE*Z=O3D zup!T#>+HHFwq?=x6ZX52!(1nxMbEkCzVW>Io1DMLU(AS?7Kefh5O{bAI`$h z?{0h(@4`h-(mt_$BK^1Y8GKjE`h|n9lCdNDWNq`;CGB8}>8*h8z2J<99;u@kXuQ*NeTo)bIV` zv(}lL^LIVOIXhqN&UrHD>zu*oFYl;B|Dm5a`(1EtU1Mpicak>cDtxkc{O0<^#y+5d zb$-HnE_GSESfe!XyuN5pe~oe8J#emK+y2I{;39j}VqMm^gNsbkrX0|MoP%|n$9$Q4 zpgW#4blVq<&3;vC*T29%9Ts(qx;As@S4s=1ExG8D?S^h&nIF(^Y-fCHuqLJSLfTU5 zyU}+b&6C((1=~rBxVI+T=`(i01;=x}UH2Vaq-V|ZnCGybSMB-}+j1b|W;x@ZY{n+` z?FGkAw9j9t{ZhB@f(Fj-I9KN1!Tt7`^{$c)BhJA=HrFYzZM=TtjQ1J;L<|2#cH$HJ zn{UE}TzhMBkHs2AUHgi9?Pq^;?qDFLL9N5K_N$#fj+bMZ&%8O-W}I)hW-l2#qekzoKrcGJxsi%M7Z&1%%u4$(h@!gqN#x63t87UX^gQ0>_5R==3E8GdC`9Y``EXlElK;vCbivSeCzlV zml(AXUd69TF7kwjogv%BE>OQaFB_%0me*dkn``Xf$Qq}jzC@XB{yu^)ze<3 z_KZtif7;wr&xYsAvzgD0=hn6y==hZbe5Ufw>)bE0f@~k!K5zDKJnMYlIU75o&qcZpVt+Z1d0ac!wyw4MiFI`> z^Ea@sxt+iBlZoya#@J`m*F8-xZEfb12FAT~Y&q75F>PyKCEgqxZb)@)qU|uY=kD7X z)%VTsIUj+#IgIUKURg0W$8BJJ*6Z5jT5sC=X2dv;78q+EedcnU3;x@LzwP-OU$W8V z2EEhwL7y=LT1aCK%zylojg+^o?S4Re^39+2!EaZxe!<_jlFn~il}TIn{|_{OKPwFM zWT98I*LLa`d(!#MPG)eB6@RNs270pNH@sVeG{yQ|^wZZWn9i&aEZ3P{9c~2)B zy)tP3MvYa!U~b2#W7cth8#BjhTxPh$& z{_XV}8CPY-Ya5Z@dEQvJ$^L8@yx$g_km}kJ?UVP({=o2k1KsxwG~Zu=x}?3f4}Hw% zSnk(;_P{!|4RB9x%pvFGeB2-Xo&o2Qcph%)+d?;1yX>$vq&UU|<)D4RyyjB&2mQ%L zZ;(06^NEh#d90X=Qk$`-U|V`%pHJAoVvPMxpq_Me{WZSo`iwc?Tn0EVxsdaBEl%LR z4Y=Sw)^qadTrAE)=N&tP<9pY4^anP!)4(@&!T75Gb`3v z(RRR?`Jhdo7&qX4cqTm8`iwEhU|egkmYlQoB?DbHIKKs_U|V{~^fkUwX0VaTMgK(l zXY^?yZKqFp3u&L&8aTGKNH4l}F{Z{Db7O8f&$=(J&CeRSR&gJ6&XQ+p2kP2n#W`!l zihWZ5Bu~WKuhMCo*R$L_u%-7-ad(q3n_8Z1e`VR6~ufKc!%$+rG{eI3W`{jNQzpeew_Io>M zehUX_du93uaVxmUm-JQn#&o`$lk^!sf`)Yb3B9nW`!WaT;dvDG9n!Yv(r421x1BrZ zAlBCC+FAu z-1?n|^J#JCH0GJlo_aoiowx& zbU3W{2-Njii~TowT*npb?V77=`@|*QJ~_VinM*kXby?U4G;mCr&;x6c6}64f)1ETp z?VD_LF?W)-8sqpYxX4P@Y262$(+Hen<ZTXVbQR4V<@Y;QF|BuA6&ty|1ACg5Mq;F89FC-R6DZ9T#nV_BYQ3^Y*f5 zqwI@%Vl8sNpl?6}>-CHba2&@wjQgT}!i7(DIF{@1o`Jy{iEAbLGbU|mpRtzaS}>qN zuB&rTtU2{R$w@z{acSGw#c@87zKm0La1C6G4Gmnc6?CMyr|zfUF+cAu?)>R@Irsna z9RQba`3*B@$n)WEfbO?H{Co#Ie`gNbl^yFDA-E6aHo8oFp(U>|dw zf@3;%Wz+t|#V+~=G%%MrdvF;iNt^x?*vG!v-*zQ^8+&rm#dUKH%rGsofdj)MT>7T?U+KgKt$o|G;{6?=_+TJe<4)nNtl(sABOIx-xb`d|o zoH@oUW2=9&r|%LY=Bb$bfKo<~@jJ%JwdkyeYwupT$1`YQ)1GYf3-*y!NSpqOeat&J zPsg*$ec_$aLfZCO@yr_2iEZ>5fqfTt{l;Z~?;h`<9prtq%}cEFIMnC-UH1*XH^yI( z@0pBU^ts@>eE!10_ler46H}=^_46-~-y`-({grP%$4EMQ;{2Sa3~1n*y-aLLN0-AI zOSY@jzQFV6*>g`kD|tpdAJV`*OKfYuVEiiBt{7wNfL8GAn6HEPndeB<6WaqCcy799 z$FoM;{^osg>;p6A`?-hiXJycq7Hr~@8GC6hncp$Y*|2H1h6CoepSf00^J?>Ko#$*0zO}`3s&5AB zm9%&4H*#sSpZ#}8`)GH(2G-;}BTVVbUs60XAH?AaYelgAeam znT3v?T;I*V;JZ7?INPbG%zTb>z_ADHf^F^k#aQW(dCWb6hRkn<$+trf{M%*)1NlEe z<~!sQ$8yXaQe9j6GhR6g`S)1f+iQ>FnFcGsIqUemFPmF?-#$=RPZ( z4bI3c)OOSJ_w_8!_lc!vv+2@rLVi18TuV;ICpKr$=Z{=go;`A`M9JBC<6oI-gKhX# zu+z_c%*#5-X^D8i>zP{H5xlSPPGV>SeHGj7x8-9Zb>w&P`;cWFw-sk3t?1b9wr%7( zMHj3chG=4|CP%dy)JL`Qel*pe^Rx3jajp#K3wu{ToIGcv>>)Z3$NDb)AssqrGNIkD zD>m0UXFu|q`vmXR_*Jl(r!m-8j$xkt0C^ncO3viY8ej;{ChH5Wm2>pEpK;Gq9hUCZ?76=O zF4YJIwMwWT_3VXn?WNnR?AMU~GsIv1+aJ9`oFm93$V)ksJ8R%R&wg_444v(rd&<4& zwYR$NuuXxC3$9DwyL=8_HOm^_=YU$X2gp|H?pjaP8(NMJN!VhDmY`z`!5lZ(0UsDi zlO%qW`wQy?*4$)g9q*uj%g?;?U-ApVTqp6oLw3g9G3<|D7mM+li~4#GsDGD@81`pv zte<*P*Il3uEjjWtOSZ zSs5GI1#1~eldO2Zs6`Xhj#_eds5$$z6ZYZ8eW5;J3^4u(ViA+rLoiPWYHer(eK15z zw#HV7Rq1Lo4|9@B6Pz1Bp8|Vemru)h%y9%cP=lRN8)~&Kut7`gZ%K?Uh`9vuBj;jH za%BE4$OX``(Ld7{NgIy(jBQoU921gH?CeKQ(FE~&LS2SvVya$U@UF5F-dQ%!F2TFY z;$AOy`OR|l4B$Bsywl`6{giBiX9T|qOqYH`U$yaGRH3i7tL|~cID$Eu`}Llo91l5b z4XiK3D)qeU>b51mm0s$G{;7|-b}}TJII69o*3b*o-H@{x2Agai80-_2<5^ zcR(M0`vRNo%m>W9z5pL+NsWum{m3Okux=z^dxw3hWGuBi1?o$kD_iYI*e~>Z53%9* zKY_7up9RjvNzjdLNUlRe8=qdF4JWbh%?{&z!GIg7aa$L$svUqw3TW zdU8}>a2wggKka#z+0T<_hW+Ghz!0rKJA}{F)R>CxmiVrOu}!e|ud~E?O743cZO&yA zud}B!gN{Eji0L_){|L_I$aA?ae943PVGG)UE#u(7MOSTB)^Dn2J3*(0KppXk zoQD|XM(i$H#-*Qe?BNmKUrTH&!3RSATM(a|$dPzMbk>nMk^`~vW!y< zNP1Tc!MCAPBzlGZE*OWT-F{Pk$(H;sAz$FT)K}P-*fVL8a54{MWvN})$GF!wFusH3 zoFSWW-%(%J_rNK3q3QeBcNp@kto*hXlF;>y4QAm;zu~*x&5+G(=@3c(U2z59?%;^_ z_a98j8~X5T8c&X_m$f5b?aYZ>Ywq|nC-JvpgcyHlOmZYE{`U5jWDGu*)&I$6-#VA| zPkq$~zbTL}UC(Hq-@r55@I0>A{vA0LXBR>`&+pfBo@c%2x-SDdb{Gl%K#VDxa4x(L zm@XZ7PvG4FhG+@DZ7NgkufX3vz3ABCj!oNhTH5zXdVC#+UVUfIi;bA9@d#=`O+rws zBc^IN^n5${jN{n>TdaJBG~FAy=bJ4ZXg6{4`Gw>;#501i#Be;zrFmX+GPm+rS^vm9 zX*b!8`?vZ5KV#dF4cZ0k?}Fd1ON;~^LQt!_PWRlz#SedvBd)QXKKI+!n1gSG`WtTN zZ@2;<#vV~~racSvZP8T|>Tt3)NZO&k3cf=$L99C-v9ZyQ-%di(bj5`!j-VDp>?9<+ z_70Zt849jN?JaxGdqU*d;Y@HwI4ipl(z~9wJabRNvlnLKw)>Iz_kG{t!bbA(`C<|h|&BTuhmYb_9h_o17W??~9u?|#PJ z*!RdEc$cY6wZFrbpRs?JjhHJLk`>#lq@Nt8Xu^9$J-ja~>qw2MH??OUI76H*&R9#( zv4!wC!i z;731ahk1@-5})^P-tTz_2lOtmuLRxgA-xImB2UgHxiLTUvDd7-Qv=n5eWO;;3)Il; ztZS1rC+kfc{b!i+zv;4l1!In2&MnA!um;Yg*0i;koM+BAwSTQydl#Z!i`=u=14A2M z>d#(x)r)J{`~BJr_Pa2pSLov$FlHoM5*vOy{qZ9n&+`yY*4q;FB|_kbKjW5o)j@GtPtQDqd6gSykh8eo(D=Pr`tUs( z$Jh!s=<3JEW6+^r;Op^2I*`*AQ?$a#`bMb{Yuj0mqcsDyVeQyiKN7HYfzL|pPd~oI zTZNEL-CR5N<{eFW0rlM?1a)R#$(^;Zrk#YO@!Bm{smqgYKgL^sl8(Qxd7pf%&Et@D z>{Q#d)RDj%f%SI5`l&+`GtV>irPkgD@5Ru5K;-?^?Iqhu7&CL9*dy-^vArkE1+=k4 z7x*%!UGvxY%)=b}f;q9%KMIVs|H$~v!+b}O#}*-|2T+@L*av^c!4ORlZwg`q^Q?rq zn;>s;1@Z#7w{(2)HMH^Zy79gJmVDv^^pAwG#2}ubM(pVpJQJJXUhZdVm(KH&XIbO9 zse64d?=!8u(;S}bg6BI75qd@d&kF0{EMogrD(Ga^&;h zeRtdTCyB4w(tn1K{}bPdG0ME?O|VYZ3;hdRKk5ZTw1hKvL*2XZd1GIvU|)Ss*mH0j zNk8BVyXdwx*}&t7(G$*{btG-r$&lo{b;11@XyZf7Eh6C>Sr^!h`|xLMop0q@h=)JE zM^G1PMg6E_6llW`_)tH_5#!`q$e(#8^U*I~_VM0d@3Hq8ANCm{-L!@iS9IVGVb9jUEj){*y;m(dVw}}j~TxyrdUZ79oyaK{-!?i2<0>j_KqMN3KB^U`malld6Ocw+gR zn`Qe`{e>a@3H#iAwtte0x%+P$?N4m=gMC8JRI_-#mNxdzbNnZmJj;1T^XwN}gjn2% zdDiP*jO>Cwd~TX-#9?0Y8KNay?*liJ-!4h#eL)=k&iM-aTH-(WeWbrF8``VH!gnX+ zWWCC%ep6#^#hv`!y1+c1!}BnfH3w@}EkaPQ%2FFnLS47$JpaVxc_N1H`HTC$o(1Nl zjXw7y@n_u9_{ey~Wgc>1U+bQ%%n=)qFSb!=(ks-d3+e)_8Ad`qsLvEFK{s1Egka1O z)Qfqiau~`JnxOr%Q&0IZu8EoBmu%!hP9t+B*H=8^%J1;EXc7DkH=zwn5N~I^NF7_X zdmiShkUP*1;|=sNj=Xk4p5(m5DR?hf`xex}P#0LDYfq;b+F$mXJ#X6koo9wK!g-mz zpIkIOZwK!tNk|%OqtH@Ef@2Z4i&f4Y=e7%S;#`wsWX|N>Qg{CN;0wDD(ksLS;&Z;e zM&>6U=eU$#h|W7vGI^&+8t+I2+uzCRYklxem2629(!Zkf9+n(QaJx%qJn@ET7uYKF zllK&?nYA~;zOeV9IyFJvs4cbjUU05BUtQF*XB|8JTd-&B7h~DaDToE=pYVLd8am^V z*8tZ+OOCD$=z{BSh@`CZ5&Ffo@WV_%y&Jtk|%=yKS zbIAE4*RI@%^@^{#$dNrHNAilS6Zuu9+F+a8T{d&F*58pH^OdZzmf}BwPg5Qts4web zUDWY)%}_J0KdwX7qI2(&9D;il*Jq=T8YSr1@!@)Y)sb43Yktd4EQ23x@!Fr_OM6@p zE5y`3v0u>mZ6(;lEkbntcDbPspC%Yj9O5x|7vvqvAFOZbtf?jF*dPRcOE8`|#Os+C zb1`Qm&SxpNBbt6!O|eB3MpxXV+O1FdHbD%>p=~?5vCopP>C(xG++c_%s3o~m7uSaM zjI7br`i|DhnpuAn>^YJ)cKWGB1=|wz;|DWwJ90dJjNPK;+{{lN5C!&-oXH(n$G*T; zaev5OvE8!vz0>VG6sr};$=L38&6mD9mcE&E$tUXismBm4p=MK1Z`Zo1u3VoyM~8U5 zd+45q1fH$1MTlOmWyZ2Ub#K@o;sZJifzK38a3+`&h)3)pT1s?m5CR{3Pk}acfe+9o z{ua!61ZRacvv%r0JysHu@YHr+?H}@mCWuE|V4fwIleu9gO%iDDH_!)7oLm=csbHty zzEy77O%xp;$Lkso)N}~%bAA83^`(3K5RH31&t;yKJfnGE5^SNywq;1ZtO8SrRe9Zk0 ze4hHY><~%k-j?^BBiZs^#C-`u_p4^Lw(->m7%+0_!$Ix^az=)M+M75`3;WL!tV@PU*I7L-te72Iqr)xv_n!5C3=2 z-wTX`mC|k7n(Ul2&I9*W!#x%|^~kwX$B;v2!h~2k>EC ztce)JBz}nA)hF6-|1a2K7HHd-?`Zo0?DoO`i7xxn{N&*^B8S?xvu4%{=+t3n?)Zfu zHuHt(`kuE0-}S1*zSWOk6GvmFU_5gnY11D9e~)kaCOFG?!SUgHVAprG%FOR=mRs$c z_Fu}WFr=F${U&7dce?Yt)*|>`2P4^%k?;-g#(nrTK|JEVi#_CnZzRN9;)wR=zcSUn z*~Nzbj(gmHqWv^ijr-4j-u*9L&)0m;qK`K$*&)R18LsE}>zSYLyq))gg!?h?!9(B! zJiloJI<_ggU{0R(JwgA3{g6lR3&!>>ds9q6hnaLq``;2Dav>-3gIU17)Q^v25^wR& z!r%P(`)OfI$3E1L5A)&ASYi;HdboCeCRkEW>pP!EJTEFdpLnJ`J)d|ktb8W;*@J&5 z))d5N?w$J)xi0b~XILfYrv6H;0qZi1XB^|H!OWT*{oNN>m zjgD=Rx4752w5E4Z1J><2a6R2{uGm+ft)czy+WRA#&dba*!?_5-djn?$j^G_)zJWe` z!2TXn`-b|7-vo1$gU{SjZsgd6_tFv@{p3CcV+?KRDX}-jgP&5kgPj`RG}#yj=u6;F48{_}&^AMQ2==h)HwnL848Kv(TXJ%*@dd^)o;jFr zlDG0_?Je&;+<#VrjlO}8duNyR@NK2N30*%Z$o>F5cobpUj?5fj$j_@$(+nR z1o@Dc^E}eOLtQI6W2$`WuW@J2S#7s&pDtfm$`#1_3HOKkZy0YIej{m;Kurua>Vle4 zJL)+F`>;e8n{%3Hc;+*d=V<8tV~LT_#*Po?8yL5fkR%>)-$6fqFhpaF;xWh8y@dNo zWvY#xe*8|(%~(SW>svZ7&k;M%5NoFn)B|?nnthV?_1G%A;(+Trvc}Yzxv6yp8-&33 z3Hqm?hP7UybyeuY=LUP_T|aedg8gJ~m!QrSYz8}f!T#`WHN@8YPs?|p&a*S&*#JDV zhiC=b*bQxiEj~c}bsIcr%*YD8_0XsWltMjyD|)anUh-QX96 zr&#wG&NZ+X%)rF+atn(k9D3tReE*uUj?q+?n#PZ!L~{3Ah!NbsrH{#|*GCH6>~ zC z!TpJt{H|5o@c-3Zo zTyboFC(lEy8iRG4Eqx|U5{_yH)DC(Qs;Aj`cBpX^K0llZ&Jk_fUVEYaXoZny2HVzt z%>>>(I;1T_Kb zvy+g#p>HINBL=aEJH!&SCw=l`U)ZlGZ0Uf#34GWyVmCofE8`;XSd24UI%kEm#W||* z{yYWm_2Va5@{fzo@3dq}-nfr(j2~iae)1r{k$GBY4K3F~pYvtROt2a3#AV*m6=k^Dav>*jjNA)!)=oX>$B%I#h!4yKr|8%t z{dSH)Vxtd#=R1}2&6X|1(l^1bZ-G@3ck*u`e75ieV z9ZCPY<|rKL(Ed=&WJ}(JY!&y7HhTAsJBXD-j(Lg!h|Gx?(N+!rGWp2d2OAHluM(8f-`sqLNPnNuLK<%%)*fl*on_vfbOwP`joAgzQ}lvMH_#{;>Y_5EHOmr@1=l_ zop{6@CC*k3K+K+Us1~c#D^j1TV^eLn2(j{6GK066X>PH-65_^kCm>0bZ{203w zd-L~darAds{~a4i8+!%ct}%=Q^pRt`{OjMT{kLi)(1#E4LO2GpDHqsDNRp#rO>jh0 z{Ia`2_H7r~OBiAauDf%cq1U^O&H(3Oix8X<&P?ZhLd;}K-nj2ccKM%- zMeHUxvz%Y@ARqD~zjlGmEa}djvuc`jaC=J+!J5pG&b-X;HIeHUP47)+NHa#?j1^4lO13Pj1mM!Bk?vB-EBR}$Of*POF+(bLaWyj6+K}`(1F2oV@n9j$YRi*HbbiD@|?Ky7s2v z8jQRixh6-UNe6#p>VMN*e^qS3H=Fq72V2tEhO`Yi3$}-BKzk-l(wx6&z6acooXM69 zL0;s@H!;?7k|jy|UJGl&H_{(J#!we(v{WnVOP#q7HINN|>JT|Ey64^MgBp{##_@?W z75jA`)7~%65ob&145H|qzmqnuM|?RqoDXs%4!IJOyvd(=h9HkAnjlAV9T#55liXUP zalhM0+rBHi=KhW^e&kB-tc88x-afcjsRq=?b)h!sBWtwPSD9)zaWoFlLojCQ8sB;U z;Cgoq`q5AE;a&mNpYhn1@b9MAZh2JV$M1eMTd>GF-<{>9?a9&$^ zP0<8vS)z-``!)9^bZqpq4ss;kEb}wh6XX$s-%=}oPw|_ob;T=fbo!wuj00i}(S+B- znhbeVPHEfcmiXJoT83z>DQiTXT&r3y*NS?<79m#dExzd3=$oPyu#NQHnuB?Rd6f_A zW1UaXXO4WqZOf5k-L@aG-_g}qseaW?j!Td)v;ym>)IN0l4ejb%^-euh8*0W`TY__k ztY8D5(Z>1Y9+uBco|}g2+broOq*tn)SY}E$wlQvqt!Hw5J|FJ8Vu&d3fV>Z4qmO5J z%ejer3an$7YmGWYIWN>_mReKGn_B0)k6JSxdct|FEVbDKU%m zL()EiK0vR~U!l*w_NU)xV5n}?l09NC*`Fuqiv+z1_V7IH8}Cm|yx#BitOz|HO;?-e z2YpD>r5oF-JnAPV5dQ`{K8&$1J|Q?ixiE5ch0#qXNbEV*oUanI(CE2MA2X4yY{@Q3lhr@klu+tw75byHvHMW?+5 zK2PIpTk3}>*jDYVk+mJc*&QO1p7)$1+44TNC4YvQ;}}mISV@-z=40L_$b+1~xlifX z@p;ECG4S_Vi9=k*;Zq5LoThg!<@bj@Pw9Z0=dQKLYp2mh&+ezT_Q(S|36Ef&JJb5&Uz9KJ4G& zXZ>qTk40AdQTr3scgWYc21sf$lO}mX4S{+>PpI)0L!8_%@7uigudy$=m+aANpCqUD zmA!rKujCfD?~)%x;yEmt>m=y-H9>6dr+`lEkumUVnY-6OyTE@k@FC9agN1H$1a}`M|Ed!_5(Bb4pe1^s_ z-g8ySoJa7xa3otY1mDj5{fuvDKs(;_AN4hTM>DkRn5}UzQo3!4z0sGoKms=U++TfW z`XZ0kxC*_D_ZV#U!GEjm*p^Fku}-gNXid9(BRu6hA>Rwll-|lW!LPmp-rolKCI~aR zzX|eP&{(xD0FnFV4R`tSk# zVGGAzij97^Lq1?T>kC26s2?@u`LHfR?aA{gpGhsBDYOm!UEoWMoqIw2{lJv%i6*jvvXI(1`zyeI67!4|nk)UnpmZN^)-#Ku_vT{-mkUZFBbd7vHh=%k^{^(dt1DOXb?!|yp)RmRBt9cZ+ivNz zLcNcud&8cD;CefP>yLeB|9xgSE4(X&=z33B;v~F(m??dWcmTWmtvA_-wH22$=(D#p zcgyp~nI*LR)O~nx9NL= z{*yGlj{xr|`vdfwCLL-F_ifoD@w+_Iwp-#4Q#3(L^6Lp}9ijC@zH_qM^oJr0m=XMCrJ=a0YYnAKfzRtL&p5S`(^|y2lcEPtI*xz8t z2U>FUtp=v(1=^WVX#?8((Ym? zCUZ8y{FPPm>_x{1|DA*+xe?FvEX@%KzjM&BjW^I&v0szDg1xqx?}_SjkEwli>@2bH zp$}UZu0Qnx)(q63vHq;j*4{rcWQV6d+o&842sw~@48>t^j)$E8|dPTvttF^IDhVv~P}qcv~ctEod| zji?K?qfXRxCn5PA>_2jRlQc(hZ<9ZY1$)H44bjBrdBO8GLB|gGm?b?DbQq!u zVjuzA5|J<;^BUTfqc-_*R=RR#O{|f%QwM6YL=5MWB|anbbbic(jXr#vAU1P!;r*p{Fhmp7)OE!MyAaZW z7{oe)*i%HpyyU@}>-w=RL4O6?5w4NPSlW&c;9se>p+4h$w(^4cP3(`1570kFyTC>t z#D(qi@*(FX&b2n%r~&oZg0s7lkTl$Dxc3~vJ)e3SHj{!QTmw^qZz{fyUnjkvj=V;9HFp$9NGRiX#dcf#izNR^2632@_zC?*IGR1J@3<4+qQly@S9+j_tvTBE3^x2 z`vRNoAsuM*4oqC)w~L`R(C4|4H}oIixJR^{$8#c0NI$9tHKdk9&!3gg zAU}_28~X9v3FDxNRccCH7@{#p$z^9B{Wt6hafnCUEh0h3M!ud43APb$2-Z)1sFi7I zV~Yfz8~RrIHA%)!;eF=su2VF@-?L4ZUJ3q2Ti|cDPYl_Cz7WJ9Ch<3OP0?!tS=>pem52vhLh zU>ffXY9o1H;Tol|RtBY3_V+St2b3}Z)1+Suto!ecD40sYVu#yySQJl92B z@Xll0hu;uQ)HyttC37;@5<^fcYUaAKcR;`(ZW?>@7@cP!df@_hwXVMwnuwGDRbmRsLyfNwU?zsb?JrJE&N-%nwV;+qP;>5Op?wJL3;V_X`PySoBSCM%_mS7$ z>zr_YPM)F8b;dc7UsoI;-fL{0AI=qi#%GN)#kG$AD-Z5#c~-cGlH(Bb&3Or}YYEoZ z1Z#zvbjh2e+F(O|!akNyzIPw(DYhUdYQCtq*1|PGO%~S`*PX6WuHTi{apbj&eGA$K zTN7Nr)E>}}zz*oxy70Bl^=+`@L#)7G^YYt)-v%%RZS2mer*2<-LJa+uSP42d`WUwr zn>nB*%sUhEFt0i&$5t59vExsS5VPdUTwQZkuwlBcvhB2(uFeh^_K^}k}fJveuD=?kihxPcgJ15_}ow8aSR~ z61Qh;{8oXn#39}kO>jTsI_`pd827)a`xy7K;2M7eeeNI9H}}be_R4*`%XdhS)cZt8BF)#Lzs3_7ud5QhV0o^{^KDdgg#{(->eZxLtE{ z-!48Y=NUN{b%QRb<0??wC-8}caX<_>NmG30*doN8yK}Q_WewKp+ae0Iv4hvjnuz}n z<_B_tku=Hg;xWkm1^Bdr$AoN8Y}xUt;OF(!^`XNNE$a}HK2P@TeeYQ6`vl{N0mQ4&*9G-qeKTu79cUZcW=eM* z7*D({_;-Q8_dhrVz6b977Ra~1_3+(~Z+~V=f5%Z@(>Fddr0*@D0%40o&g- zR`4AVb`p|se-Au=Xf2}u`Gcza+gFuaemC`d+O7YL?>+WiyT+W#=eyK#U+JFv$Ik)WiEz!$wGU@@Y8!_>*A9m_e8R<_P{Dz3ki+Rwo(N{TY&oVc$f%%Ea9Fede zFa>)w#1@gDV{781AO6H>f_a#46vzczf5-uUpbtEjwM@~(%5yNZpX{yoedXVho8;Et zlPlO@q@=Hlqrctq_uF^SKjp``5X|RuL>}yG&wQ|x6SM^X8}}jc-Gz{Dh|3(Tc?)Xj z+PQwzhBTT_NLR<{JcPzpCN7wtqOwbK|fZq_r;hlr{ z_j%y#kOQDYPuxCgbB3H}WUj=7rua8gHnYnb$cfwx`Q5DSw>;TyS|iM4OF~>=o1zKE z5~p88=0t9lp>`|K7lIhXBR21Jt5DzD&}$$15TS8X5QCfm9k7Gj%mrOAH#rQ^5^6q0 z7t8ySdl%)}<~(vXeO=zyLeqJkd5v-naV>GJg}$3?F-1#`zNb`{+NMj7zbc;xiOt{x ze4jBxIWTJwB)<*fX<*RfWv zKkCA7mzBR+_}zj|UFkcehBIx{k^NyjY(aa-ZfI8GcuQX%}cx5nquzs!QK;e*ah1Cg7w}uxACuF#~)TokH?Q( zmI&6VHS_!GBu&4|eg?)dz9+;4;t$cp&iu$FvUXDvnmDR0^#yeI?ex9n$aS$!VlxMG zEfK=GkQbbkw&P=L`y}yQVu&WFAuvBsTh>;=X0V6W0(C7@?Iu_=c>UPu+agk8>+*rn zwTHwXPJ$157q9CxpLH$oNnE#vdlc*f?Jkb)qpQGu6|fzF557A=$3}l}k4x?`_}~jW zA-5;+>4KQ%T=Ng^^;+W=uX}ISg&I(ck+nnKY}rEeYv0(be+S}yf;pM{X2{kO@}Ien z?Da~TBx{}5n(_6TYYc2p;150V97FNz9Mskv)f$$d?k2QHX6K$xNuX`LNoW6ho{_CH zVmL3@SMD#qP{-PK#~2z5#Bv-YZ6GdVgZY&=>v8>d){!-{_9a44n`ZTOUV>Ri~#1Mq_()&tnx7t(vK1z+r*V`)9qZs&eb zOZI@hqPDI%I<|9x|4fJhk&HY?%u(0TG(V7sS<;PdPqO7$YQP$x7nrBQ+y=Y#Asw9S zlVmNG>f`Z__mw=Fi#(n4mJV)r>G<4Wx6jTou7T^bRFe?wF=xrpu3$fcvp4b{6_WLS z_H=J_Kfe1Kcl+Dd(qo#&F+cerZ%)Z;$QKxc9qjKml755#6Yh76u5r|gn!!k#B)Gk# zhp<0-#r@TWFHCU+bCc69(6$}jU}FqpdrE9uP)q83lBRbOm=A2(-ckKq#`q+Md;lLy zbc5|3TYcdECY}6ef{xAD&vKWs)rS4X{jJ3FTwU`(sE$u8*_)v5Lwn3M))Mp(*pX9k zeJ-x$l(bv+ZAtvVW3bIaZKFd7{EuMoz5mv+S8Tuimi&fzn!~oHxyYIPxu5y|Hl%ZJ zpGnJm_{5)>%8NRU3vBd}3-gEknPX*r7>^x4_JMuzKHd99-^{&Y9^%e~@geBE2q zwg|zyJe;KI_ko%DTVYE=q;&h8Z1N)>5+A--tvApY@|iit;~0yrg3WoZ%p2Pl#PK{w zI^Wfvn6f{yWruft?YH*N?|7r=jBy|O&6Mq?`gklcVZDLACZ^)`1bqwoR)P<?9fK75Ho?3oEJS>^07 z7xUIUj@r&8)ITyGpUIk6WGrmtjPDj9SPOCB20@UhamRMv%tAvP8gyUKAkagBnGis z#@&+4VQ7yx&__<>cK!{#&YA1pl2I6Xhgt#&^x*?dFgH2eblJQPOKcU7IkH!XJ97@^ z331PlkH?@-u@iJ`^sh1}c~G~JG)Z6&po_@8WFOf-&K2jY>AZ19htBHM+2uShF+>wP zzX_ha?zp!2*3P$+_*MOa?>bPwulSow|C{0`{yu}O-)pMfI{h$`Ci#i;i{^h|%2w%W zn`%D~W3g4APkMjji@ov`gLSda5Y&xYP7$K3&Pz~p?r%f;z@BiA;a;)WPxf2qA_Qmp zh_2rQFjJz_htCxFZ^8M)AHOXc@A?9rG4y#H{$^$j&LQ)2&W3W~T+MQ(xF2@GnethB zt%2`ug7s~!bBHF6)=v%Iotdo~0rmO{`ywMD?i43sPUa5bT2p7vdno6lYlG`$WQ}{) zYN$rN{H8&NBRhT6&$VOBtJc&zYZ#J%9foKLI<^qho;ZfKv8}Oh0(%#XV?2zaS6j%= zc=C)K-(;KIFSOpRJ>=eiq>X*D*YaTw=3@NJ@vM0X*8ha-;~L?E{}9Yi9)@2 z>=W`KCJ?`J)MoxEwrKio`2@c`pRg}ufjB)OHggR@4&>PcHKJbBZBTR7kUI7R9UHX= zbc6pEj9&@yhzraSf;pM{TwtRQpIz`6`oMl}BR!^R9CMRHWIp6XZl*~m=b4~mqkoHd zgJaTKCE&4z}ZS1pc#t z4WA)j?h!LV$F?(u$K!8^Rl$xQ^D?#-h(jN9vmUNP?nNP{)=v$9wrxjkGo=G<>Pn5H zz#deV+MlSt=-=^WKIVl{Xwt#CcIC(#mKaHsG}xBFZ|0gj_MOD9($$8@Sj6P~a7H*A zK%GZ{wr!DTfc?AolRc{7W4y;b_nEy0+Dp)nug9Zf1NwWxW2@YXHAEBWjM<7o-mHyt z!#bPRYgl__sU3;yMD0$3jvYVZQ6ustR|s)rV;d7uxvV9XLN zVUCLJPjYCCSs4czsnbWSDafCiu{PE-SyR?NvbNM0Xj5m_j^9p+{sjFHg80mHM9;as zo~C@BAkQc8#g23g)>7$e8|M53d5wfz4Y|*xNgCU>Y!JfZiS@+E99mg>NZ-j!JNV4?kC;joBV+}fcd(3sy($F zS#RpgK5~8e8uPxgw?oWwefDylVh@3Tg)!J+CsVSCdYxm#m$655fX|ZBZ#v_h@S8HN76L?b^(6Jz!q_V?Y7gNNlO`${AOAoz-J5QU!o`EN1h`kc6^8p z_yRV@*LjZSGrcS315>ujR@>O__Rq?Hcl^_QeCvaegk;xuzghkrp#M!^(|16=2k!hP zSicL_?|+}Z^F4j{!&bll@eQ!T_dw{s|A23T_cuVk{nhV(d;_dZwcmXU9Dgcaf)4E) z*!L^BGxpEO(3nsBOswztGS}@_ZRnNT_io#F4U7cqc+br7!SO&<4Ygo9n5E;RlV_)LL6 zFpnWl5KD2l)?Cj5bu!e8zL1ZhZRm%dXO;T1H|*0EUGEvd`-VBvP0M!@-b*5Ry+=sj zf_}zz5gC)Xqd@zn$>wuHK0q7$NEj1>*vxSR^UVTn>`;BG-@Yj#{u4TZrq2oza=sH1@R4Y4nb|G(G*R%o>fv?)1|XF>=XMI zc^w~}GtMg41Lv2s&H3j#+PbEIYwPG5TO#<5!#65%ew9u1B=oIiCwzOUw1+mfCyu`1 zJh5cI>BWX#34N2RY_*}>P!xtbI$$i z8c@F6n_u@{>Xr3*{Z7$uldj(*mEiY_px-qcSu^&ava|h)S*~Sj*%I^+Oa1H*;BTh% z5X|9ucE+Wz6=>5J0^fCwsdjYcWd3?y(6RN*lFn@@&mo%FIu~8AXTUmN zYn8-41wQx!<5m)qhWG}XeTH;sf;hzMf;p%wXP_nM)y6)t2m67pK45>$k$&U8p8mu$ z*yukAIyUATqNSuA2|5hXgwI`|Yiwu@tc^9YUSREY{ZqDzeW=q)A|*EZk75%0HHYRR z2XdumuUg8djwcT78|?87@M{V4lW%_mxse01ftkKibBI=;jh%k# zfd77gSoE)?`iA;|c~?R%m3y7@!@BD@$Fm%Yd*ivQ9orT&X_7$RH+4-ByIXabiKbA-e;aTvRCZW%UE>cnc8Q6>rL&gdUnD7y=tsFbFH;J50R2K*P*Xb zWY25TzB_%VZ;U-D%PI(@G*tXhZYxi8)yBsT8}`++}_{lV^W_yKK0dx|55&c_zP`@Vj! zz)Fr})9;^m_;-;devGN(>5F7^%_rR6vYRR0H0i`4C-Q8{otjXq5Uh2{M*c1HXHLd# z5sBxsT$&qbBhNSKzo{4i*Z06Fn*I&o z5W5i4yT0!&aYWO1JeXoX@XmhwhQ^xlyXF;39MS)-_+lsR5A_$8bQ9A1pYl(R2hQd0^+eEAOwS>-`RB(?3K@_+2qY z7yQOG*zgI#*p*mE0{y$hs&V+;+{D!HX#QTf^!GRZzRBMWxBhO(-wu!dHpkx&E7Ovi;4K<-Qk@aUE zTEhMx?bQ(M>%H&%eHKE0tA+gl{YPw#Cl2w53q9j6*(&%!Xbj_K&b@LzVghYL+j-o% z(8iBA#AV(seBP+R$eK_WbZRpn>cY60P$$R0pR+@~*@q)K&&-5pXw$Q_f~{g-OZw1z zLd^|79-i~7K)c#P?XOrlrllO0r2mPK-E_TU0Ph-r?mos2 z(F(L-6+${N_6Xi%Uhguz&m>#25^CSn{-OR>ATG>8m+raAXNV@KFLkbTwJS$^Vc3@; z*dz9eYd7*ta^5(9oMX;2=bG!TiJsqY_Wx0T!uJ%ut4tAplYhbAWSVT1t#4rFot^&q zhAum}-|Z)Peo=mfCH*Ugd`$JV?JLQclQ<{m`Xsm3QlTaz>ll(cA zx}m+2o)Q~5QunSn6=L4t2P6CNZFWaDQD8iEgE}{{X=BI7e%6Oq<^Qeowd`|1OA0?2$A{AP?u%r5nx!tRm4z z!dacy`KEn?UnCw|X!Nqj1|-%`JEU&}Gf#hlQ^QEos^BlFz3SKg=4+V%xD){74`jhPAKLl6&$ zU9laX3gcjkCOkj$^a8mI(ZnfxhyDcnQ>i}H-sN}iH*I{F>z)(;N<9}V&kX%XY>j6f ztc&$+5e3%-iJkuHgMEpU(59ang<=!G=Es~Z?bNPiJjR7|VEjypj;+q&xv;^=vG~Fi zO^^d?V;(@iai9AcM~o1}t+A(U^b>=%)pbp+%X>6b<5r-(L_^tEaCzB5KZ7? zKib$UTWyHMYau^47vx@T_Cxl>v5>ai(mp%qACk}#=Z%~X)P8i2tCD!IMF?^@f;E^S z9ca@x1-^_~WgK?;!DEq({SI4xQRsQzY@@wI%lp@sG&60tbpI_w{=PSIuSCZ-MJt?~ z&o=sDi+BJ#{Vid4fJ^& zF-H3F9%Fc~F;6!4RcXH`S^cbU%>le$S^tjo`&jjzC12~c|4Db8ujaq~h~I_lOFe-) zx~@%iWp6nLEg9uppmY6kE%wCMBV*$7ZRw93*@vt#hy~SW%O?__HMcEoN7}Z_98Eef zm*?|b$S%0g0QZ>+Hu~`+#tTn2>*^b`FqE|e=M>}ep9`YEeUQn>E|I9cCa7Pw(4)&EvwH` ze87g^6kCMwx4~1s3G#0R`7Q`UED;Ic1z{%sJ~;Gk4|e&+$G5)z`w!58{t7mH_zno? zf%bRJC;V+Nr2E?--vp27fBq{o+ep|Sun)37Q2RgW`@lo58jULwe+IS$@x=(~p0m(M?&AucST|nr)g}@-_Jw8nf==t8}K{2 ziNm)QvGg~(QJ{@|3jD?!w(P`P;t1xgxswn1&$0&YJKUcuGw(&LtFDJ}uoL=*z%K-` z7)vZ9^b6bHN&6G;9rn3nU}ww{L$o(Ie@p!EUm^;g=a%1@m)JAulAImR0{dY?IyQry zeT&>*_M5-gvj6oAu>bgYui0;Oz`k>QlOzUbhw&}heDg@ABy@4~_vwmlOZFkQ{+4WL z(?1lGIhd0?n$|FLjjXS-vyc8(I62NX{6bJOYRLYuH|)a}JR?V8NjJ7P+3>-@B_ z+hYCmD0p5q*cm@twi3FPivS&#AhkY-4K;_h$MMf=O1hW77p z9qP<+4V}F1xL!Epod3wP-z4Ga+sYF0t8x}YG{HBR*LN7c#r&Iejfc?pv&vH24C(J^ z>Z|NuezYxX-;<8t&6LgDeY72ClpI4ku$Eerx<2dnIr62R)HVdQo~pa=JKWE<_UU!+ z;5y`5)j8q(M1t<~)72N+bL2@s&R@&l47`g+f!`0tc6?^Sx#k=bgZYLa?i9?yT%0-1 zSyygD=Zt#=_cZQpEzcVF>y->i*3`voeL8zB&!6wV)WGMDGYGV??*eVxLi*wUO)S;z zaIQ7S6wJrG{H)!HDpXOz5 z>bA@Mmz)-P6=-8;@9_^o3^<9~owcM7I`eHoKIFz4SW{)H4b_H@?f>#}ZdbD0NS3v~ zo~bG!z^|z$Yjl7p5Cx(@6o>**=Io@MZfmrAWRmPPAA6a>UNAg7GUECoLpbk9_wO`* zpN;lATM`&I(edf`9DC>*u0yRUT1v*m5WABp35=N!s0SW9*1pi3z}z#bx^1WUu@Mtu zsrJpd=4DR$>Vmyk3H?XHdT#I$vl4V-nmBnrUGj!=gOd;sGue`^UF&-uD%vyLG`u)Yd5d@DJUhJ3GkO5eG5Y8!0d;anti!5qvvM2Ol`)u;Le&S;4!^t53& zM|!*BId?-2{wG7;G~khhE89f^}>$v>)pmLgxaUPi5v9AA5RKmwD>E=<8bJ z%wFh&9OE(jOg;Ldwiy~@$4~#nN6yK-z}PnCG1WH|v&0c~u3py1oXoecxv1y;?OIm| z&SxYopHC#VEr?mkk!-2~)PkL$w}iMFiyi+KtgkXOh9%Ag`*hnF6Z^!F{SzVo6ZB>1 zvkCfNxelat>_7|;%C>_dcbE_LaF9$CX;UpNo_{n+~c{Pj0h=YgGdEya@aG%vN81I`QfSLxXP zi{=*d!gfo?TQ1Et9>5Mqkc+Ii-jolvXuJayLxkP|b}hBn_l5#-A?P1D#1X_yu@m%4)AzU`)&tm|sIgD}(08_;OiAz{M>M@cTB)^}jT4(j_aQG3Vy7HfxcmiE$Z5_;k)Cp-w^i%xy(642zp>Ypoyct z=&k2I)b)4H_aZa~_6E?2nS$IcBI{9;+D9-KbJ7F-(CZegmor}?_+2KtIHKwImtkzC zbc1auF7!KXiPq#3J0;_eT``?l%`NEdJ(6LQ{AITWp6ES9GUgX-n-lMT;ItRmqva$ zgR5 z8-3NiW8X%e1HBT1d;;GMaks5$F2lUmtHiz&dNlOB#7S72_h?H8{>F^LlKvBFPJhY4 zZdQpo(v9tm(QUVE`d;@(aNb{)-_7}5{gvzKk|w0H=R-6xbuM3Lq_g7;VTnjPl5rDT zcLUH3q~hVPA`8tVcZG(p}hxDK}d0P|Mv827ib&mn0-I?oLf+Y;n67yWoI z=CvnFK0{o~zIWzz#k0rkH~W7Q&J}$L?D%1%^tj4U9FTLwR1FAz3kiO|tiq8FZ@+`4 zeg{>y#{8y(ku1q4;uqB|%%bCil{K0qwYv!Q!y4Ep{+9CHyX*V*;oqeA{*8@2)4GT! zZ?yl~gX?$A0Uoashs}QE6z2i#H(j<6OZA3m7ufI-L#&z7;XH8rcsIBO?*&)B3-tRy z-U(Kw#<0sf!g0ML?0Wy_cZEpCFvS*4@Bd&VOA_LrzvFj;y!$%|@BS*ceaQdxE)ZM% zs`2Jo?zs2zHG5ovC z8$&)5(l`I+o?t&>ss{h&10xCjUHbal#J|gH&d}V<4@8Y`vdF~^nz_$HgHX3&8>d>?C;8>$H+ZqO>lCJtP|aKR4XYZu(@sXNYn z=l=67tbAs8PIyOUnsolVtl&Mm-eEOy{`Nal5_%Gnf0U*7WM)W*mTbLyLr0n^9r^?3 z-_(~_FW6t@Q=KOpb5^#-mD)3QkE%v2W}rd;7KTItR{$GvZm}{CvioX%~z~ zo^9iK6FcL$1KdG=Q}CU;iHhbWB_J+Iv6?64LuIHSpVUBcTTbJ#PkdI!OhdzM5f%&_j zcY2~X2*KIF6peGr&rhC9V;g?Q;Oim;&#z(p#FCxd%^b|De(0C=%v>w`z?v}x~1v%udl<3eBVuVCOkyU969FvTpW@JzIja;9O3DGc%mwC+@hVc2w7tqvYTNGkD)BkC zN#EIXr{`P;>w%qT+a#CP3q2+42Yl=U>>m>BRYDHAtw7zCbV-PW&q>aKvx$Q53}=as ze~MEe4~EzYIy6yZpLFUO;r?h)G?tikb1`i*@^d`GZ1>=FCLnZQbpBSKODi``wrrUphnAH>-@F0Yx>^t`Dpmev~IuUyzZ*)pCmb5(A#8v?1}cNK8K94 zuiP*CH>?3XrUrOirG1uLaS)pOhJJ3AY;XiUdtK}Y`vU8Q`nRX+I}OYij_mN}?;m_m zIn=7u?{>FdowK%o&BjK=jJVU>RB*ddI;nZLv@yVV*Tt9?6McEf&TFW zI<{&j=di!p^DXLGGREEoF_on;*hUPoTTo*Ne#3MzlOqXD@6^oT{aRs5hiJdkCEr-` zogdJCQMm#+E9-_N^V8Q-&tGfhoY`x>Q}FK}eD4;E@8kOHVjo!_dE`^;B+O$SxeB(s zm-&n8Uqt>ld;E8M73@Gf`KQ$F(km-{#1IQp?1VbCcGZ`BNAO;7ik;N=f!2rK2f`}v z1v~Hl9+wKYxdJfR(MWsqX`M|97)wi(h}IOIB>RtUmNvxP2bi z80$xQ;CElQw9j#WMc%bNeyeH!Eo)D=?;h_DYr|KW8iQ?jEyknJ^qj%rI|APe#L{>5 zk)UI%bd4)h>?8Ss9?uG#SzrqBJ`Pz1Z?<-aa>C~K4OSB zjImelm>P?(a_24ift)U8fpG=jd7?rHA_H;)s@OrAO8Pr%2W}#THFx!Fk-Qa#wtw z*qR`hdP9U@ALa!%z)#!}bzbK7{MM&z{RQ%u2;uoy>&kVrcJy}5ODwh;cNB-6n#{?X z#szC--P~uxxW*Av!T*UV{}Fy}?q|oop=WA~uJ0DBz_=0`n`*ai-z}*P%mYm@H~m5X z0RAJ$G1RDFCx$xgkM|5c(mo|Cj%|wJTsUXWsf(feeaiDR^-P?6K0>lSM~n@2o+tj> zHr`{+d_TDUCcEH0mtp*b{Xdmk@6RfJuhwKgdiOTJq09e7$Xo#B=t< z9PTr}FGT12w?JQFh)6cSQAA77u@Q3wc{B4}!*=_&{5Lamh;=+R$JxK+AA&jQVdZnU zwFc9qf2~XF^XHmBYgqFT?1|T2_sjc+&2@;4d}cC;O%6C-56`$gN<*-v#^39<&RekL|>mExm%h33Aw1?=3dKzaPL3 zO*nVu{ocCI+;#2@pikMlIC-Bpf8zyrm3vn2BI7O&c4D~u%B5DQN7M5@AhE5SkNN9- zz0lo{yUmto*)l9@@C6+0|yTW>1e?8RoUQ@fP zHlU9rioT>*j>g2#WJ`kQ#7<1jsXD$Vdgk3e=T>`TKEYhD3&H;6`msCaO*X}tItRMv z?yR|t@d0_%-ASDrJATKml5f4sR>6J}a;dQ|yf)8;9Ab$hxC6d_=oS1xY-MO1;-1T6 ze8enK>od2B~< zn`13!sr5;Byd|;LtE_X~`cQur`^m35<5}p^L$DV^v;-X+pmTOzaF)m^umNN4z(~-E zL&6Y|5*^zSH-7(a&%@P~beAgg`p6Ib@56momJ8e@MVWCwgx96|07TkyMOB}cOHTTRT1t#Kuqav$LLobBkak|X&< z|3&>>5Vr(#(n}Nc+3At~*%QuLpEG=3@tr;Ny_)agO`tEn?{ikHTY1!q_Ipp%uiW!9 zp2;o}z3H7CjKpIke#dv|=bDG{DpWu5W=e;hcn;6GG*{?-px+H1>AVZ{d%!LI6Y=m~ zaLESsy&&%f`R@ip?*vD_8|?p-O+38^wPjREX3#|H?}k`;Yw4)Em$vc>E-t z;~L0bc(RB5P~*^Tw{OY@C#hq4p*M5)<{YGbHI`Z-M(%0X{vG~3gnyq%hNQuEF0kPb zvGsX8MH5G##ej|tKXEIiv;lTu0PC>Cf0ca((RM%)7|mau1OeY{33^ z!C9OOY?ZEYh^9Q|Sh;7+PcQVive%}u`y%cLa<-rk&u>Xx>Kn%RnqV#DQ)h?}o|Ac@ zU0@@Q9M8+#)HC;Z$Y+*xbEE^kO~GCEJzhn(&3=4*C+r2r-$CA;JCz4VG<|0q1;(b@ zt=m^+^-+`BJqgL$gY{9?>i&pzo+Y+VklV!0{bWy}3-&og(^+wLKI@_L zF=W4ST&>T1m6?4YPl4Wt*n&OkVu_}GV~^QK_IHb~&on;!bVf&KHgtZRC+9qM7f#*> zZ1^}^&Xsu1wR6sy!@c32hQ4bzeTNQx?_SB^w}aUHo)BFz#BId`I<_PD{_WhO9N>Gr z^`)_~4S!|w{ax@oVDMWYLC1!VccM)RexpVg1feJR_$YXB$ z>Uk%8#_Vw~Ea^vZCpd?Z=k2rO+@k$XmxNh3(%UbpAqi8E!`V`k`G)Qnp!b|_mbs>4 zJ)iFfccg+HRuY=0V!Pw2Y^4_S0CR%JmR&aPH20Fdhyr6{J9`GK2Z^1Sb%D)sO>q_W ze?5TRaWiv}H;zHpT3z`N1@3|2Oq!Va3~*oSz2)w5k1E(8QsO&;yU%mMGefLEJ7rp-l zT5U$r$m%afVJAUGxII;t^z++2nGue{BxQnH{5T1LMHB}w| z4S7!t*_Sv8^DrOmg!O~R`1&`v9<|2>wrUTJVWj;?x<0yD>3@>MU>`|HdJoX?!Fj-Q zu`aWvd)=QTaZvMYv-I8((-U<3_=Z@CKcoDaS*3lJpM0)|q`qm=nR~Dw+*y6L@VAh^ zRg=GojPoux-6P*^-*0R)?^meC6V$FO<`S&k>t^kSo=mmT^Ab<%o%$SybK$>zL*H{M zTjM_i-_;EH)NG>8&lvkoh_AZyyX-giA)Rx}8uO-;^X9wl{pw?CdUU!Drith_?O&so-2*LA@WDKjoI0P{;3N??MlT_U{ zBz9vv{m>`t>AItEa*WM(_GcFu+s2-bs3XpD`|F7}>% z;rAN(P4x`5_XYF95KEjw*E_j+fejo-4vZuuZ}v4OV`7}Mq@Tp~nYZa3;1HpAe_hPv zNZvHP=L6gCSo@JfY!P}7*u_eYBs9GjoFep2aEK-1-~T&ZwwWBsoA#^nlP&p#{Yd-c zH_i2dpT>QvVIQ@uTXxM2>jnEm{@Tl(Q{#yB_>){2XZaM1?}?|_YJ1A5wxhb0uFu6K zj+pwK+XBfLU!+`;*joCDsXAlZuoFuT^cSi>6a!n&_u%^pp8@)=Vi;qGoe7&r@s>{O$PwF~l=Zl)0$i zMTnvAf?J=b(4;Sc4`zZM0w3|j0s0d7(5cbI(&sb`KCcD;uCmHIJapd2VFz^kk$}y9 zgop%_#5A3Q>K0O{W)idI{?7?B3 zI-@PvPgBS2^%Cs+6GuKE#&qe_Xkuy(=31JQc`Mi={lpPJvKML{!JP8~8$KYe7ecz{ zK3SVNJb%-i>`C1h_vC(RPtj`|s zm@YjE)M1Rg!A?9ZaT0WF5Er&D*^l_t6S>Se62`scO|9u>7hA2(Tu&_7Lof$>&i*f* zLlZO4mow$=FTryFA^cqNobbHx-0(~Rx(WGs-iSE`#@LDDxjTPXt_Z<pU=LskK9{Cwxu=o)#{NOm z{)axNI1`>Z&X4o#I@fx>Q)fHmJNc|0#RKQtb=LUF;SLRbw_XAXM{Isa=zF(ej6DQ* zjk(AN^dqR{+@{aNYG(kV52_s%6#^0{DdZjNl=7<6KKAyUTl_tN zF3&KaKh+@r6C-O_v5k!N)zIoBa}5>4|9#@3NNS(1Q_ zT+Wu-OLH&}@|5|${F;Zk>O6OS>h%Y(1MwAVK}%_!F)-f~Q}!))jNoe9Y6PJij(j> zaK9>BW1d6kg;jKX#F5to^{CrLh?RZt49t8EcozIT@XV|WY{WVqe_-P~U;ZuZ`rPF6 z*6=;Ef_;f2$RT$N>c9|(nwn<|_QlY%Inr4RYubV}vR1RCgZF4^KaSvRhS&+`*TqVX zBsA_>;l2;}9Ba8sohRLK$S40NTUSm9`lkOTSWg#GV9nkGBx4|kSeO?_V?$kF4rALL zW4opOGv^6O=B5v53Fmd68L@^O=c3o#TBEBD_h5(++!yW*G{HSu3F9q@C5K$Y_{P3@ z^-4b#db??|!EN{YdhU;KN)m7a^F>bJj7vwB)TvYP>ODe9T4d z?;sB7i8-iugxAFS-f&Efx9yW8-qg5}b8SiQeb+gF$M`C?S84w!y*KH=yj`#_Jd;z; zUf1Ugf5-f{jL#lFhs0F6?h(xEPBqoz{3_VK=3y=|H81QJ+@s~l{+A@{>tgGm8i_MVR1@~*^eG5t8E?OVyTaw^$6dfBr zID)#ak8ZGiM_o_Xx`B1EMz0gw5$uuo4ZVVWi@N`e?-=3?KJ=Y%W=kBw@0TqifA=hY z>m*OVpQ>#(<-f3HZ@>Ik{NAg|JLVqWb(~Aw)Er;^vKH+d=h67g$?p$*Zt>lMbDZog z>(<(O>6!XKJ?oa(m~V;5_kZ<09q;N+zOO?9ww{C}xj>yJcFuRpDLY^%jvVr-HN_FU z6WpTfJ>a_D1^PYT)cd_ZgWn5|@=mbpT_EsI5VmOLy&&%aAueo3&Wu5tBfb6lJ3|uu zKJAwAo9c`Hg8hF~-u0gH@0g#CmwK(7YiwUHYkIO(|986cEstv7*jMEp^J(1vr85IP zPdrom0vkR+f5S0H&+1INq_OSDc4J?aJ#&|y^(MZaf1Z00>2v(i*z;Su&#j&z^f_Yq zOtFr1jB}deso6yc=5!CW5A1+#Cg|A6BYzecLnJHniATrom?<5OpdWgpS9)LKJb)d4 z2;#jC>U0r;x#+Pce4bXYGq!(f4A_R?`y>B0;(s4$8Uz1k5@PVqt^At`HvWC33Euft zu-QI&UzdJt=)G{HJ09Q4_ksL7`q96y1Med$*zkY7=THv495L0easNo$wsh9Ynzz;- zc|J#yePln)mhSWMo@2XlJT-g<-qWsm)(41d>VsZZ_TYI}-V4tB4PxlcH0k7bK`%aE zdY~6TH>c?M41V_&xi0EiU)d{bXHK|#z(*f9*tZ~_J3R&8w_yAbVo&B$Ba)?>1{*#z zrB{x|&;>b7&>Ns*!#7_b*L_#X8ny_&;}jTI@U07M5D9gFnoTe#J%w=3>}4063HOL| z3(ik>z3Z7+`7HXmN_=$t8^@KJg{Y}5aTiPcR@c#&>PH@*zk1`f;{qDQv0ZV5<@JINB&BVBy$WA`PsxC zh4zbmtii^Hl5+E$<|Eg5L@JHW&gq zH9opGfX?p+Vz^J-pIPnD>_|4(p9ZlcI&5+LbbHkY5CXVmY zfjUD_6A3FNHuuT=^vc@kw>^N}&^K%9*Sgpzo`n$XrC~e@jBCtPED&#>rE4IGX~O5T zbXL$sh_Amf<-9p}Z1{*f#3+AcFDoTB*L56Y?6r^i5{I2Q>T$lIv*mo*BlePgTMw}3 zhke&w;apmtVdy+L(|W#jeB?8ZK@LF;&X)T}ZNqp7e9Se8%bZ%{WbNLxuLav{>`(NO zbA;s0&h^ssQBOc`JqbznhQ0wEup45RID$G)wH)hwc75nx>9PvO5e(F*820IW#Y!l4EeKlP=M8e&F zW5@^Xg6FIAtm&C+g3n+64#Lsrv32AWJ0W)Db4vac)S~_t^t8kg^v)W*Cd;Aqtq&ae z_`Kqo1oX<*nDgS?IKz{0rkpM8qTAMFGtNO$18j4Q<+y6u{v=%kNloY?68diCuCYGD zx^9RC@{GsGzY24A!Jd2H8DqDv`iWT=HIG=w6E{Q@cHRl@2lpfd_mexsU4kySmk`pA zAf{)I@0DwD#|$>+V_sN=Bi+2&?QgmMEqOE3M~vfLuWJs7gnpn2);tC4fDk-0tgYo9 zAeX>@#K`qf{|M&vyx6)x-e6~5@&P?c{E<$bDYgjRrJEt!66XQz_{pV46V!BF#?@AR z#BUMGe}cMZNjKO+kV_3{1<$e6#|=Kml1Clt!4jvy^LjI7yK#(jsRN@B(r&HwxF)(KrV3v?+N|i1HQ?v7~uWk5UUV+7dXp*8#wg-ufqF4{#!xl`ChP< z_k4cecgw996Td1y8ImiB-(U-1C;H{boa%y@`aU<@nq93JCqV(WVd@O|VB z{KNuzOSm3APQhmdj0@XO#t}xBo{{Fv8CFy0~* zPmW=X9X|}g7`yQp*#tEg|CX+QQ=k01dP;QIqRBr*l=(;2rp^=0*ToV?@ZFfTE^!2V z#hz`!9*!h(Z`tDt`)xuxu>a(dPaSGfdx#L;XU5o>k63bUmTc5yjwv{A&VCmdpY#(; z4d$gTa|81*AM0h^>;ZTWu-Cp;_J$-c1b)L9`w`63viI6AJu`Mc^sy8V=;YAv6i0Y% zp}+OO-~7td_&XdQie+CuvE(PtIY`FFW8z$==1j!`aW$5h8h7O9yWvRqes~jnxA}nM zU8AXn=ML4Uzb4qr&VJ^;ha~62IfZCCL(ZLNtmiYvGsLr0pQ)uW@N8AcVT^BN&f)zR z?+O2*nqvQ5KGBl%H}z2H(jhLE-WOJGKV$N4rff%eKIW&7#(PWp5`1Pf^$V<%aToZO zAZ{w29MYHf+IO9qk$W&hx|lZ#oms1<*O4p5SL(A&%#Wv*oP0Z`^^d zJ97lT2ev>m<}UfJpfAzI&U-_Rty-_#DUFhPk_lgy(_hHHN$;x5=fct7h_26>Asrh&YLLrW)n}7)ge^E<&e{09t0WG{ zsbF)Q^N}m5wvY|TG1O{;-VD9=BqY6NBx`x1#*hQ9aZ7wPo_ag?n*BD|przmA+i$yL ziNAB~W3DCc{un<&Jh@$PFG6rnn&2LBpSV|?H}`FpdxsrAvE&TpQsWefPHmWC7w$FS zcMLvyTLt&uwU(KnV<(mz>bs6>VZ&z_ci}!mJ=Qgc)--a>tQ#HM5>x9WehKoa12d_0 ziMiv*xoOJXf;AlB^=+*SxWB+%zFD$W@Ha7)N3E5hySD8i8}kDCDlz1LM_0@f)CcN~ z2e89=fO^ck6V}3-{w(CXR+V)Q+p*CTy@g<3X2JV3GKN^Q()LLX#Y94_THoVa5>MVr zjwE#TYUulB=l)sVevf*ahOdSOvzm9qBF4l=+zVB=ka0u#0XxHsDT~r+aCiYdDA4CH!~Ff4ipsW|<@X z#^0ZA`*-7+b-N^Z{3g%C+)p<0tDW&p^`qZ@#&^s;_PVPa`VMJ!(QW%ATYjIlWWFvq z7w!P}Vd`!m8FOwvFXBQl269Z3K8Ynq`P{Kp@SXF$V-Dh&XC&kr+bppk(KQG2RIuSQ zj@hyUy@7jO(v59ZvPRZA#Sz}CTMq5z&i%(ne9gfR_^4wTW8Z?`8eJUG%I}{oJ=*UK zNwcJzlX{itz`Ly{rtCLQao8MJ{nR$LI}T}EmDPXiTRDKeCnx*I&U?9$EJ+C7#TBOX z3ce8JHR0OSUCEG)#Cwg+Ea`9reNh8;LS5<`#-_$#BW4MDqBoeL<-0=O5w?6cxFsXs z3oc0*ycbN+u^ln>j&LVU?*L(ZV96K1{!W*ih58P#{ia$ML*qB@SdYJ}NnEYzyxU%5 zcFC!_W3bt7{g$KDsQMjuk8A9$*Vy=%_E_{6?Ee%)ZjG~UJJM`@&d#Js0^==0pRprB zH~5HY;^?~r60pG*j&**O_=qESiC$ogy`>+$f}LDyP;-eRI-l&!J{!$%x>aL$=wK&8*$)XAcZE_J_UNg8lOzG6sB2oKhpCvyZ?$ z%*VXU?_Q80+Y~K(p$4^F(>ikwaSDvF6AR>a!F<%KFhBD!pL=MU-_Q$oV#ud|Y61F| zjT${`AQ@v{@-zPy^aJSb2|snKPIMho;jW~{M?Bf?uczoI?SvUJg2-1u!QHUV9Sm}*!Sj~)#QIi!=HSi{GZafc^~N<{({GJYU`#LcKrA`1GLIgegJkU3 ztvTG=*1Yt{+^fKN%6GDFdTrw5UUo@fPuZ)jJv-XFVBgutJj-#N(bPHh1f6p;oFDN! z&ylkz2cnNL-z6r z&bBi1u5cbczbZLP&W!Wh!tc|9-sWQ!zK>zTw!O^+VE76R2OYy-NF=Vu9W&GtZgl zDFn|L_rNsi6`rwAaF4nO!E<=>+2gt6-U2$$Bfg=S3U`q)5@zOyq;ajLI%Y`6-^DIW z=@tA(v9JVrJO{ut(S@I*p=Sw}z*ZraoX8r~ay@E72x1(EpZQrA`!mH6?C*F1-%>pF z_KyizY;>rigb=e6-%UU0T%ORt3P!V`|EzESSkk`5=O$3x=~;`F<9Dv(vDt_2F*ehrv$yPV7a@lBaclqCJI>;;r|7a1L+lacQU?jxjs3{m zwiQc{U999tnx=h0w=bl79%^+Ff_~`htG{v|xre@sE$^P!z}mfb9r+ z9oe({uJ$e8z1E|(0_Vrw-h#Vc!3OxD3368MQ%FKDFjwt`ei&D_#z5W@r@$C{B{X&p za$ML)T+O@nCI2a~9$;;-U#On}j%|vqwVrh5>mtNc->l>Aot$%lt-lbm8`q+4yTE2Y z(mBYUEJ-*CYy5=cET{5JmtF~tVQKCs@J(?{We&=T}eoS)4j9jGw{^_u3RK6CekUMfdpL(fC(7w|z#$m<2h__nCeZNL7G ztsI}r&KR1$hy4`itgO`~{|wH5lAr1kkN+tT+euiD^;_bb$(H;xguW*m=TozZP7gX8 z?gICNd&2#QJg>O=CmyhKmLt!bv*u1wV_kO-8#TfAj``LFa{{%jTVk8y6c|G<*iTsMc)Vo;?AVBhQ}VC_Hp6%d&SQwhZE@)4ym5@>?XP3U+_J{-9;}Yhv6)fyYQt`y^YLTb1>UUz@75|yV`Dpcjwv`@x>?2l?*@f%k+h-v#nM zkpE^7df`aF-wAHL1AHTXRgI)e!b*-LwBMeYvOn<_|3~fj+;y(4xqkxh(1=4;KIP7b zb=~o`)|)m2*V>?C0~}Z0Qy3O^`!wPeRi34E5I%_g>d?uZdW4)&sS!{n$J&lJOhl zP`iT7@l&}wVO{9hpr^!!kF~EN(L;aVjD1aZ@{i_e;^;RSzs*AMo9&4vdxd{b;rHy< zzkBj;pCYc`xWqfI%ZK0p4#K~M9R1trEHI`HF~pNgFI&(Xl5rFGkbn))u`kgRbnIJD zV~Hc0?yS!o9h@0MM2nN_~y9qEkm zg`ielpXc!0p?Qb)jeYdqv)?24nse(qv!-)jdaiih{H}!eAYEggMV^;W_ajsTxIe_5 ze7<;IskwNclJ8sUe?y199vHIm-``Df{-KzpOGdK)uDJ?~0Y7m^P=oqk?|A5eUUEHC zJp#P~dtg`t?8IYa6V4&O3u*%Et+X=6Pb@jqpw>>%v7M|(4RW0yX(yN1rM>1Hc_uh> z?nM3W&u;+6+!O8^_XM~z*oa3m#%~xK;<-m%`1wO`S!XHs2!3|}^C7X}i-g+LB5#&@ z+)JQ#$PVb(TFUCPAK%w!Lw@G4hu%N-rwjJWd&rq{j}Pzj@*Xzc_vv0#xF=5#Lp*nA z3u?ehjwJOBW3Yb|9XoMTY~k}=I%|5S_gOg7pWv>%!CveHy^eX-56`Lgjy+^Ad!8xh z)bs2(zZ-lZhy!$DICJjX$lsd$&g3_yVGO;%?^5Eoa$y#zcd|D1(eX3K$~kJSs#BMk zaq-hU=Z5m#m;0d)LtkA);{Ct|OB_Kiy;BR&E4Ei(Qw)%QL)|yJd?Dy%h?byZ1N+gj zH*s|SGvO{&LSvpm7=ruaza`wC%Hs1#3=x7m#@`p<&LOekKf-^H(3ij-VwM~}=eV;` zU~HE3BgmmP`P8Xk1N_866XcQKMI<~oJTL8fZtN#^U0}1{`P2Z$_{d?-ExLL+qUmq) z6zpf@{<6;};T+}zTehFT*;Y7jSP6H5JM$e=F>q3PY}sWGL2de_|50E~fc<937J@wh z?}x|uR_1lpuAKA@NfXj5uDP_|Lwi4U4qY7Kv%uGKe%DzKHR+8w&d%pYeR7U)Z|EMg z9~ENo4M8q7p$TdRy{OONzHoo^`48Nsm3N6Xu$G>EkiSJ_-6aXkVM01F#6nAn%`r)h7e8)IUK--3C^#ZxcUMjrV+`83BJH#Gm&bH+2ra|}y# z;pdlU7(LR*Gf7S;m;99+N%!z3nSYDA&ZYG}G4ehTPd(-d!Q3O~q7J$BneBRp&6E!3 z5AgOq?Y2|5CsB0U@SAEwxBW>v_LIExj%va#xL@QbEa`j?{*L|-~5{7CDJZHw%O zzUU9!tH+kaRBR`H3)VZtD%@v)k2vCM4tWMYFkZqnIFl)kX!=by#1_%icVS64N4jah zC^YeVD&+nXgx+OUc)#WGTRr=4S#xinYuQdLtdw|3(INyw)T^{Bg2GTyQSe#02w5%jzT@BhC38$s#V+%J-`-w#f`BRu)v5DvW; zG`u5>d`HNCFUY&WDRy};$bS>~6GHF)po@8N$`~EmZ|dU(d_SSqa?MY9Z}Zu{HE$*U zr8WIYYMnRTwY%o{BP_+6r#$=qsyvzp_{@e7Tc5pr{@!2*VxA!1*zX)_wTq+g7`EZx zr8aer==z+WVvDBF>2ZM#9}ov!kh`*DrSJTff(aX#8%|?^QFU zcYeQKEd92{hHr{fV2nKkaW|vbh$H?8=B_-HgUTOHqZN6v#ZTYB)F?gBe8%+UqwWZmqO_X^ns z`^g#kK5(X-Eoa{a&%xAtiNX5{y|-v$3&vgcEx4=m;jU1_bs}pu#gaqr;{8X~TzaR{ z6Le#nCH9k`V~-1LW=TJS-s$-;r{-o|^bPc8y7ZZxqE|b9d`=&}eJEq#YazZdC z^H;C|{+T$w$?pAP&%6f5GWQldi#%T=pNr6$bN<|gt~)Xl^e>xyn`b8P1@VV_B70NL z%Dkiarr!zsV({BR;HPE?a;Qx{k}=?`{;fHfhyIr4bRXQuo_BKad7(8$eonDY7`a~T z_>X9c1$1mnF(>z$^JdS62*F)|Cb%2ikq}$&_y+I!#L4&0ynF6NXACDHmYkhXr)SM0 zd9%__3@|=|Ug&8HdZgDSPBOIyU@hDS7-GF}Was|OWJ|Jl>`TA)jD3bJ*n92+cY$-M zccGpkXUh3DQO}b5!A?JkF)>pd!Cd5Zb=QW-Qy{nInoWbo`mFgxR2UfQ$N5N-si~q z4-qfms}RR?3{!BADo1yV=Q|Sa8*uk-?6bt*5)A&BBomu3>4r3ed^QSLeVy1>>1wWvSxjF`5Y%dd`_Tn= zhQ4Nk&RwEU{6~zOzs}{_)Ca~pB|0=gPR*k~_HBkDv$osXd`@LElSoe_*COX~!Q0#!H-}=56J2&rL{wf;i@Bf;!ZL zo_K6q$WE_@IDAJ?;|YAkbn&%L3^^LhLexk{by^!^a*IXw3#=~zq0dThCJo-P>)y0Hz3Z6!z2HD@^o z>-IfKa;ei(qQeoyBMtU0LU1NSv}Ee8brE9enFO9sBsTm<5KrE|peD87Sn?g=HOyS+ zmV5#~@yx@#Jz;+4XeIA{Zik-b%F@^z=}-83*^>|3Dh%mQIOfTR4ZnTp72A=KRNK1Z z@9~izybk0nZ0W#vY2bTurP{53Ctb6vZZN;jg}cFB;k=Mv=f^qfyhg6Uy;`oc<0pQgm*DqLznB`groO~}Ab9_jbV*njM`P3Up6U(9SQ0l2HP11Y_SN`Za;yDGCpPqM zY=~80Y-~TW-PkuWJ|wBx1U+^UVrvgag8m6|$fd><)B|c8+p4tBawvX@lc2*Wgml2R zMAtjS%H*A5{x^k(cZK=i4)Shr<$J-VcY-tjJHgPqyj}jA!D`1h#3~%=KcVYgoSD*p zigP|m=Kth>n`>Y12N^fLAA7Rf_sL&lhak3x7lQqu7W*Dz=yzI6&^?~|je7X4nk>oAZ&^V+(lPje*hq|$CmT}jv0!BNa^uyTUn3XExcwVYhHr=sP~b*GVEE)eVe&=X-0V19C3=RC}ZpSUH+bxqdB-QbQ)aT0XsVkw3=K##QJ z`;MuYBYa2EvGoFD?BF=$xNr>d_J^K3=s#pB&K&7Y-@5=E?6bt?n5noM`>MQSp2l0X zAoM*CJa%l$o(1%9YU1cU#7cTfY$KmLV!_X0 zmwp7bkSl%En#v1udDo%$AHBdk5p2-pH@4Z2eGBSP?+Evaj?HxG!1xGyrcZjLFF1l; zx%Yq$*d6l}w-p2I8;s;0FWFCmKE%xW!QAwt-d3&&onFn99trwayz;0)y{YGL3(gTn zK3l%uJ?|cOW9g1`eeXAnN8TImD0hUs5G(6&UtEJbcGsX5HhkQ-r8<$dsE2+8zA2Cf zdl#e3%NqD>VV^>HpOLJ&<=$~V);XUp*iX)wJz}qV?i+i{z2OdZ!MnH+ewR1(P7j*k zecuvYM6&;(*u*ycOPqvUpw1gxKEQ9CK8BztdfR#bhx#@258MOR1V^w=*1HAkKY~3m zj91#W?7lDDk*{Z8`_A6`oH&bBU>p~>ja+KfI@IuaZ=H9&H&gdzCyn2lVx&a34@oR^ zadf{%-ZN@erp90!^ASr9xvo=dE%^*_j@gO_kGpgrregc;b4}#9sJ`#?que@&b?oq# zKdw1HjYF~Q8GF|R=f?T^Oq@@~9Nj_gp$X}R&oh`8P2;1_ z1nzOAYizKe0^=L|tKBwawRepnE_j|s;^)m{OP;?e`UN&}wxISBCt;1i+P7eTmSEr5 z+acCPT+e77JC;1tif%uWJkEcLlW;GrKS}&G#^WLTjpMp%(~Iu{_rdF<5B#(AfF7dB z$6l~6-WzhapdK+@5D(~)P?s2L5D!xv)nkvFYH}8w9p?t<*zkp7n7d~#b*NbxS-;k$ z&nfEpGNvB$o~%g@>;mI1n3vcP)N5If-nc{b+C`*v?;-yZo{u``1I$%n{w-Lm@tCz5 ze8lzwV}qT!#|5^^xyHnYpntEKeMATBJ0Z@Hx5N?5?LLrOcr8`3mL<3|NBAE5&Q96z zMS(GPIE9*PedP1tG4bSe5ef5Kx5Ufg>oKNawT3%?F(OxZ5o?e4;y3S7|W4$mgINTKC1pH&vAE5UCStQWAAyMmQOZ(M|p2B zA9X{_-0PN-y}*7${1Qh{ALa}8S4kdFVli#%4)BFZ6uJHOqT0 z-f1zebdAlM{mD=4F165~XnObchTn|^|9{PdcV&PN+BZ1PvF@p>ANGbl8VNd_3u0j= zThcH;wbujm%zL~ec$YWC7Quh-C(yCs2XyR9^z?t(sjZ$`lhi$cEu9|l6Fc+$;Ly84 zIHH&Lga41Uv%8WW*P$!k?I1V-I0uHpP#6kBVJHm6XZOWfhH0s~dafV#0*F6?6s4+a zt#5{RgH7+-V5GFpcqO5CX%*~sjNX4?%Kr^_oo{j+b2LZA@4KJ`|!G4Ir zR{3k4nv3rDf=I@$*Q6Wzp9Qb+mZ8u8k>A~sOYoiGV9)1q&u22v<%*x9metpJhKmq8 zKO>OT^cbJ9?JtsA^hv*FN;ge@AkJ**%-O{fCu#Z|;Papod>-Uy1D_8bTk_cfI@zrjf_$E%Sool%Do2-k&Z_)hC%-FUi+tzQ>CJukA6j4~x zvEz3P{=wfk6Z9>xcd-gbx^tb6-}b4xPt+XrEkeE0FTKwM9orE20euyYba38Qj?Z{# zub?OB>>K`;`$s%F*zWPn`9t;T(S0%=g;nM!$8_lx?5@pR#JUpF zHI`tFQ*cH&D_i(1E}g?le>kJW1G&aFB(=b~mMd#B4ngcweyxSSj;)hVoh44fJ!iNF z!DC`-taFex#&&Gv@1)-MOZPsE7wmtNcOG%rLU=w)dT4^)L$LlPxPDwuuJ6#bo;q{f zOWafSzG|`q{!kpTtAK5%pEz>62*JG!+}BMU-p>>mW5>S*Ipm)Ny%!j}7v`+SC+hvceeQdKeI3_%!`8W1xOcQy%)?sQhrzob-cgChJ1g;H zm!EYL%e>6_lV9#V&$+mkc?L(GwXSo(nc(+8===T>UEk+m78sxO6ZiA`A#=&E9F3dm z5d-M>LohEr&9F;lZzfIB@ZX&UkBQldbE|ywjhuU>Jd*SV^h(e4zC_nr zSkn<+=g@k$;B0Wd2WLreo_oR>JECd-Lg(cOUlaD2>%ukQTJ*d=M-sTEhVc?zgdiRX z*vyiC1ZTo$jWboxoAoMZo=0pxgDcPCkc?!jE>NF&m=AiX7fCH+JL6ffAL$t6$o_15imSF+=6bT@ziF}od**!%>0DpVMH9XsBkxJ>XM@f5 zk@q|2tAgFEl8dbg?s1-9U2s01Sh9!USqt+4&Nz1b#8$B3Yl-g(#@KhE=6OzHh$YXg zyvKTxA-!TBHttQsc!?uC|4`4c9yqeGKkO5G#@?~-{leFy$~(qm$E>X9x{QfAf-}Hb z;7oLZyw3-570wKh3tKpk8pd_V4d%)|?)jZdolczgg6AF2L!Og7H=E?ovr^AP_LV(k zJ=CQh`^R4Yfm!N`XZNFO|bqDLu&-qijDQwIjBKR`lR0}PD;ihkT46jz1c&tK<#nO zK@L9bO?VBg85m>7Zy1APyJA?gVLVITQmlKh#8&O}F$MAs{73i4z?}n-{Kt0ns-!(IiHx>mwm+EP-`Sn zbZq!;j%?JU_P)SYss3tz(z|kv{rjq4$HXwUME5o2IzQ>QSvsFJvCbhv_#WWA17qy? zmpG!Axm~ZyQO;Y{m+XM8iKiZzW2!ze!~%Lt&^;!fUg(KDK=(MLvxb&y8CMLsFvU)Z z{t4pfg&vmheb)1HhjHzXc(~)8_er|uR82S!`1{q9e2Twq)V;Z582dY%LvJ zuGX6J1F?YKMF@IePQw^G5Hl~Z)f~rTukrLt-}?evwX+}IpCuj6Mbqynz}^KlUB|T| zefVJ&j&z{LOtxgS|1czBB}WqaKmTD$!Y(wuJE{zgVI`q=PH#Bo-^yCIYo0fze3hed z<2_k|4%nf_{-$@uKM}HfT_g7dX&e3<>Kx(Twt5QQh zcXv77@)PHHbdRYuG|$X_xxTB(CVsBB?8MBRgZk8-qUl}W5Gx72~eCKX%1Pn%f$Ugd9@qKTv5^DrO4P7JYI zgmO)n4jx-#Tfg||V~WVW(6OD&B_FT>e%J|lz`B-TtuxOGYX`<%oQxsPV8cfoOu@Lu z4#h-*Ucv71k>0XSAsqJ1J=hi3$9yU-=Xu&neUwUywAATLgSg>H%;;6{04F4bV1!F zcHZAz5~jcg_=zK)Jo1g_+Oh%T_6Mjp6W6AOp;nD?EV5q^NBmB>PrC5^$NsX`!#rLu~!+dGPmVaq?YZmozhN_=($sJm;hT z2C+*VLGSe}uvX4P)4E~hx%k-&okO1u&Ig=4C+yo1oDuf4o)h+aOvPw9Krm|1&&8=330@S__gHIzu!vdQDY?Na0L6YU#Mq% zurLwK1CNHmhR(`pyMN! zyd$Uwu7ArdpNV`9d+gYjJuTIvuJ=1~4&t1HWc*}nvbzR#tW(D|k&LS!-Qy$OP|y03 z-C)C4dthx(Y{h|nBk$K+;&<#*{K`BeoEP|x^&8~gF>m#l%e=+dUgNA+>9{KIKBoGA zg1Bnm8uPt$BunyJoKs~}J~f#q*jruW5I#3uIySD&$h{}_X3Az*C$*Y#$gj0#)^R>L z=;SOhGMD)bwXmPWxt7>h#t;XLZ;0Q5H5W)H_{)Vp+vc@?7@ckxfdLDzvNZW?&;4zZ%7QwSztmH^S<6H`lEmy{MNr;46 z1{?Yi^f5)hz=p47968i8jE!weHglxY-%djJ5O_RgziF}oxo^0BR}6S;iH)_<_a}z@ z#6c*>*mldIxSJ-M_b#+oL*G^T9m?-fey2|TM&)mrU4+>BtvKsRYdftNmN3x1UKLz{76@$HUH166P!+3}g%*VWjF}@{^ zAZLm#8ovklogmoro^Tz`1K1ry4*Aq%j+J}FzMwbRLcUSvB|cPxI!AELp`L}Uv9Y~n zT$T1A-|UXrS@Vu1&pyi?5%!1qLr#ZN72`<{P# z7T{lkSaPU=jI`l1j$w_#I(6MRH%r%&Ys%T;eD`wAx$caYID+d>TxDxq!PgRUjBS?0 zZNXWh51(E7GT7WBeOAbeggVq~f?f@Cqj$l5Y#8Gshg?8!0=rp7w+)HCf{lE7qUXaJ zbRAY+3nb&8GbY~An$>;V9Ya+zL}5ySaT;&P$N`pQBT)&Xb!IP z5uBSX_)O_yi6ff6x4{&<5c>W$9>8v&b)<8tLtX0kBqW)8x-r9_K>qAJ_HzxenT=5Q{y#V4s`V+Ee!X=WoEd=j`_rTnp2s^PX;q zNO0b`h5cC4#eQdtUn~c`mR)PeKwrp3=>cUg3-x+q+^` z_Ql-j*zglO5_IhJNZ(w4p0PjAS+2MG-|UBCCUGJJHCa3R;q`J(Sa(wn`#_!{wu>V> zKJp?ZHsahT^(*ua_zh#n^s6V&LtTSy78qBSVj<{7{j&yOZQ${Iuy5H$&cz&CH1!Ac z2}jW98~B0PkwnpL!w*N)T8wXqgOP+Jz0u>zep$1@*0O(M$y?%xI@i=X_6Imq)lYn- zYfN1j5B2dAzr}r48M{6)&T%_w+Vtb3lXYLs1FU7z~d_Dduoo`9aAwn>B6I_EOx(M-e|CalD z<~=`TBZeIEsda>VnlI-LKfW96Kn+993Un1;+S^JAzzf1)mwx zAyQ&Lf;`WUbRGPT!R~u>i5r~@34PiD;}KF@;h+Xob=4K^O=dfmR;9m=Cxt1k^Q+J z!lWXLp6N)E- z*i<_@b#5Euku1rI?cYjrBj54yo(oP&^e&>n82b(O>N_RIzL1Ui=&cFw(ainAhM%}8 zPU3fhySx)zyt@-e@IDWw*n;awqvtBq*q3csmZ%PdTQ#k>$(TmUeI@!9Y1m8 z9MSbY(D?me%lCtRM;Lm4WV&>iFC5vSzHhp9zkhm?ckFNDPxC$1f3j`8L#sT++_oD3 zq}O+N_G3e@zHfBrKgm(v1%A@o^`4N}U2^ebGmL}hH_!IMl-?e|P8>Od=e0PZ>zO*m zE;OEb7u8R^>C!9IG|W-4op|S0S?m0!lefjUeG+TPHC_5QIR2L3`9%6B|{S3Ua98iL>9{Ou;h)bIJ0pzj3V5bJ?Mo`28t`&SIH5_D{ppWptnA%XlZ zsKq={SXrMs^b7rkr5NI#z-RC`!Mw}~%)bS8;yf3UvGM#<_9Gg<3q;qxvcE^L*H7Rh z&imiXI;h8(d77X%)*h^1*8$M6;Twt}{s`)Y>aLu7Ykqh-ANC<ll&A%&7IWVY^##k3O>e7uxDI9_L2RJWJb6o2t(1&bC;NOCH@|JL~=zzUKe5DmX@$_+Wotz;YTf<0kxy7q{@1N0Es4P$&wdmFkwTqjt`k>na0uIo*}RKZ?IEZ zXH5SUY!zZ}x@;BvJ7G_jV842TUa@`3eiC%-A;_mrD=@}KT+8csUqi0(N{-|c_(O1= z$#)HG(8WrQByg5)W;tg*YvlEki=DWA@pN99ho0C2uN}$wT(qlq^30HqpPZRcW943Q zov6zgJ8|TYPw(!3BHxSGgQIeG6spSC+OIyLrvyj zZhGm0zD|OUt%B{1BOll86GMI=hIp8wUxdahd-FcHSM2x=V}q?HQxZY+ zFoyYneIXw;nTz#x!Jf1C<03TPd5wIn7-J`X=Cz@o>%YzYWXDFHedv+>Q4<(rpUEjY zb`#R?9DLZCV6F6f_dH8IbaG5&{!o5}n)ra&d4bK~JA!)>xIbaPQ2o{|n{ucH)CF|> zd}ejAUcmQPLOEu?^!sL~C)0{fJT+HxB;RQIPKgg`X8F#E{${VfPce>Tp3YqKr#Yl?3!x{;++%Hsm(k<9C$or+X=DI6OXIkzNHvo zA6PH!B=kGx#y(5?hhkTP4uMpyCXVI+baL_i_$9ko4;%ADw2FBpX;o$I>ztmQi@ z-(B^0n6AIo4F0C0zt{f!&6dC0iY6QOouEUz2<0EKRcojwbpbsDcIIh36U6d*i*Y zGBS?dH|td`=7@67`o8t`=h_2yGmCE9Z}O?mokLydB82zBd%-@jAApXX7$Dw|Z?M(; zrdYscR>?=V-Lm>!j~>{+A?|Ckw79~`Aa?^2Ajba*tu4^Z#wst-hWKJ z=ir?O??JF}-x-gOY(qIw`ryoP&4Ie(1$@Ld;dRghHs>A1xKB%LOAtdWIpl|+mnP_w z9^EJ7Bk&QkMbA9ur_N5$E7+L}hB$&=X0j!@eq1|WKO{C{h$V;I5KZ;mGdtn-Z3^Ow(zExNwvFTVSK0Dt8v7JnBZs5=C69L>di zJoAU<-t>@rL$6EycY*X;*rz4v`zV$ia)JI0V|-nN;+o=ra;c?#3&HtCGT!W`eBLW0 z_+ z&(!)`*&p_rJwFA;75lhGTraL0*ALbMM>f;+dk^LdTlNZZO*kL9L>EzDjGb6=wuttB zYGJE(`)q%bO*z!0HtS@)W|VakbAG{fLUH6$r|u(T?63>O{~KzJC*3u>>VIO%UpX3A zs?U1Qnjs0Ef8_d_Bygs>2*G{HJ;_<*zT{kTZ*oRgf{qQ}zQBf$m?1*&448s@8;$Vd=hkOp6 z&H{N)xvpVZYb~8A&e#+9jQw4)^8xHI$Wb4xmuGR)^Evbk2cF|R|5??c5-Z58{JtTIClg!*hdi&|Gtm{axtc(ds=5Gl#Zm7>ZA?Ry}CYE}k zAJelpe8k=H*hku_$DGWy1$_)bU#GwrMoNz@PsY~kW!=}RO5*P^HtG!#2|ZP8w{$G> z6E&W=CC&vlV;|!o*qhA;{ z8`$P`Al>_t4ktkmLC#Fjv5gn-J^9Hy>aFMckc_E&*IZX^*F+Ng#M7Lk)LGKuT=@Rv zUfnO?a~zTy)EXiZ^dpGfiNAAIiLZ{o)m;-gMSlSM8+SZ$TQL6+EAjWoDsNxU@0!T@ zB{k=h9;(NDP0%y@>g(O5V{`BHABr0fy`Qw$vJI-v?GXWqZSMZ~mR{{6g>2-Wc*3$J9K0OB~U8*Of5FeuLdqA2#P& zI_^z6ZsvQoruS?xQd(ymypt14%;ZP{@8zc03Gd~sBke<4#}4<{wk997nX4!0m7_6n zm7y_gK_5dbaYX089h%I1Kgc`6#(TJANItP-H=%b-|G#nVr@67&?|Hj=_$$u+t9+Vg zYJPa~eX=z9B+zeGOyVLeJ5jOvxi!J}dpq+!{mZSyp+o z_baaEJo#M5a%ish0{xk9VjPQ{59{-s$dS~#HUCK`)_Ims`OFRdfsn1@7;Jq0Ha;iI z_bzOFc0NHYpRrx={Vw>9r|+gq-&y%R-T3|f@b?PrH~gNzV~N?Ks~$D$_h7~$i0R6W zoQE3NhQJT~3mnUQ6p)SwnM>4SNW@R{J6!+5|x&O`+}WBcDEF*DCnT_-&Zfn;Ck>tv6} zrm?}dg>zQsl5emb!CcJgd0Wm+95txrdW>yr(wVn{Z6@|F#dNW?r@-E_*X(-;|IL%} z6z7Geyb7`8@{WSPh4J2k_ZN|p@e%ll;cuy-oSvXprp8Aw2R*_ngw{~OSL5(aaRh7k z`n$%CZOYj>KfSl?f2u#AS8(6>4>8JNF6sbtQP(gw*rqsw9)<{AOJ8rUudi{J9Y3+; z5Kk{=d+&wEAi(X zpK(u2+0P67cBnXx9O^R{bB?@kPwor-voBM9a&1=b1O3x;7c+ZjAIW3CXYM;XtgOcx z$z#pzAN4}8$A+;P(w8_F*okjGKY1^3Kk#nPFzyMqpT8yN-a?ign^Y>=_j$g_- zg8IzkKIoaf*n%}!{_K&~H8@9FE9>8a{ak{*<#U&P=JUH=SN5MVcEdRGI{R92{m^|K zNBM1tZaXpaqSkO6l02xkI!CpmyN)HfJBg;A3thUYcH+r{R-on-A(qx}829wmc-OGB z?`S?IH6}pBk%hq_=!FaRv-$0Xu$p%klXcI!D0R zdX-CdbEE_3&}XrZIis)(#L?T-8hHLrJ*TJsJI_P^zrp=K|HG2r^xt@9{j8h)4fRf5 zFY~X|v4vorQ*p#D?J2Og#y0%@g1jS`gId&F7e`}gT0;dJu|0_*sRu)}0&@ZL(pM9k zJrU?bU=R6k$T@0WV7EdIL7jhi+t8pyJIbzY6J5EJ@h0bE4Ev%jQ>VD zr^-98`q1r155ZosXDuZX``QB*q-3-}`eO!CaKnTtX z_fi*!XM;e;=5s_`&zwWf!#uZ*ID>D9eSytXAGRi@YJ}h%k8+-S)+Po#k0rK|Y)NAq zvI*j6(ub7jw8b`U=4w0Q!yhhdnabpvHFP9??`E z&}Z71a|`-$k5w+&;S_8O*~oQG>jUD^GG*O z_w0VShl%f^_PtBru0!sB3(wLg-s;Y)-x_!OJSN6-49&NaBYE@YtKU8OE?TKR^t`5cD~y_+*b`Ws8WCr-ZG<2@btz1^E6-Z{jOQ@Qh=Vz+V( z?*aWTFv`0)-oI7+9`1ez$9uR>bl%MsYAo;ZT+j89b>3Ot3s$@1oxjU_LFQo2C5~vk z2TZ1<*$?lJ{2s}2cbZxdX5n4(vBE{d7YyryTH8PY4ZBkMD<+R?`!x%2O_=c31+gyfBV zpXAJSo(G8!x`;0zj(Ed3#M0;06GQgKXPG|#W_}LxS%_}7bYOh=jwAT)2Z%bkooKNmh4_?!j&KS_aK4edpq`?-&ex$>?z}B?K6>MFEV9u$2SOfjh zFMWG0tf!?7KXFqWK`yk!W8@Yq=jQrztvLfd&xp?f(q{{OCSCFg`z>n>^@re0v0m0m z4_!opUa9saA9+(y1E>e9aNCZ2Kn%VR{Ao$rD1O?w%?MNE<*U4 zaoxC9!1b%x#`WxC>F-`63CXR$0V1&zL;Mg+Fvrt8uJ2f4x|rfzuuj$sJ0&*223xQ< zb$_Ts%}D5nzL=Bw=@oj)+RIdq>$`5xYp^7rus`w|(Gy_jT5?@|eUEI!F$aAP$)z#Z z9ni7i>w@0hH@%}{!$%Ej^#mPu+N;koNbKZNW2hF?dWRUD$EN#f3!d*h?|Js`3)}b{ zTq5{hFJ^wn=X-u@{|Cl5#D<{8&peuw`G5LQFH5}+F>^m^&-m&dd!L8)67T`Frs%9q zP_JdplY8L0RWAAPvp+x&NNmJ~;JR?0nmFbDn7TiX{$9L9Tx|V~c@!8E(=+}^lHXO2 zy3E6zTXWMpYhXP`us+thMc2Nqa_vIbZV0X+oWjpuU)R&um+vJ{*OY5zdeN&5dnBBR z8~c#N!jsLuDgVup4X}3+f_adPr#QlAZ#1n(c#MyjF320A3D!Rg*Z@B)5t&Es6ZnBx za_*Y7zI9@ZZI(}V+mKCjyuti$tl~p|<0-$DIq6|0UD9yYhTs`;@?3I%a#lIBoL|l{ z_pt9rBxCIOkMP;%e3Rd^7BzwA6mp4P2e7p50A*zHYl6M>zB0x~4mE1c{V---z&c>1#72B&WKUDIp{GPYsfXlKPfI=FgI<`@ zk7%k*eb|EedqPh)M>c4BrdYpY7=QBJ@xa*TT{;jhI74J*6 zIR_j5USN#9Lj1@cSqp1|B~F3y6J7QYJL`CUq~~GYC%Wu$f$fHvp|io6*?EqS?vo1V z8y_)G=jfh?7<2m=PjN(Dk9F+$pXjo?u4RKe6EdT{04sNJ(KE+1o{A9|zrp8eA|HhkdN zT4N~}MuLynDaa4OwfFVMhL1S%%#e9Zi(RE-ia<| z;Ye?K$6|)`Z`fBH`P8bltB$XNuNN4bld;rtWYdcWE38e27fqvjA>$Ded!Kf%A-gHJJ^d^3A#l3W8{mo7anY(v5l=NE{9E&@Hz z1LvYL80&df85$egkDiAW+i&T-Px8*Gb*nyePGp4`kGpj67-^ej^&R=)w;pPa_5M)X zHe%4@fu+wn82K64ukSL9&CJipmhni~vRk*rMqcoJQr|zjzMH}lC&3OLpuuXxVxK?_L^btcmYq{C7v3~CX zwWu=$`Q$pMDVA{;_#8)U6a4*-wHxOS%k|jh+WJ~OT`&5f7oe`|QODGJ zzTWtVC5N882G%h(9V@ z-bbv3brBn4WWAsDYhRJnc1>b>#ve&D(;kw{P5jJ#^f}{voxET8d-2NmeIe=hf&4A` z=--(D`Vja!e|IKMc|SF>H|AkZ`d}@rm-E8889rk873uOruEmg`ur zU)4{>5DPsC$tUnnaT3=WP3uZ*tJ3k7)rXF~3(g^D2%^j}9_o`bGmlt94eLGq$oUJ7 z4aK>SrdqyFKFL|`ry7U7(lx%pzZ2$rW5`!o8ds|CO>bG>^D%c%eC9Y~A-2vWXOrhg z7eDuCxevR}GG`jl&6IwUrg-pqrw(VHnuam-BqV{Hk@?i4Zitch{?WY^JX87X%KMf3 zZWKcHF7O}G6iYmLLr{Zf2hRZ5f_ru4{mOhj=RT6Gr9ywO3+|uwpj)C7M|>+#7iO8q zwvf&1r8oCSkJu|Kd+rZ)Tn{-#jiDEM`j)L&@Y^_o8V`kIWg0Ar47xY~;Yb2F5qkfi6M})jqlJ%!!T- zA91zz(fEcK$8GiC{#N!$uYlbU=YFU1D<|i`)ehy2uO3A$_bml{pq&%kw`#`ak{W-9OB z;u@B&jl|}Flr-}{k&+d}W)ZpOoV zx%!UIy5H|v^4_kp^F1E2={;T@qt8N@4%BA;NZc=D>~I9JJE1m=!jc~Oj*fS8e&52o zmI`*pmi95e@w+`s>Y`g#J2u;E?X9}J3j*E?EpZBrO|^fk`(0L*=>I>N`QGf7`^A38 z{hP6CBR}DHg5SIseCwlW4)&Yt&;{3Q=k-MTdM?>3k@4h^dxJeLoIg|xTERAC=($#@ z&%h_$v7P5(=34(&M&`Nx(6bln*!m|)&P|gIZhIXs`JbpURc~1nx%oUxCZA=AZB^RW z6?636rGl+eeYbwcExrfwJxt&8Lh$|5Fy{LoK0xmxgkzB7fhHU9n`MeF_`UMR((f7b z6l34u??ef@v2E+`O28N&wYoUMJ-Oel{=nl_^lBs55L2mka<*W9(21+_I@UH~YMlM# zId^9NtE?5hXHVpBQLjn0E%nm!J2lrCLf3pGT>m$Si5FbUa^@UO@@P)x?IIqa-zK~^ z)`ETncKQc$sjL-B2gdk^SV_;bJ@PEj!;z0z=aEAkF|~Kb^o?AC z^-OUDd(B<|dqLhEPmXP@n>hfzrSGm;>$2ZHVgJpM4qTUtuNM-37xYKJ%mZC8W}e78 z#B=?9cBx4&>msN2>Rj^i&#XyYg&g*oz4dkZ*>|md$=4N6?n$h>FXjh&ge8t}uS@+N zG4&kZg3k*+|E3uE`yiTfMgIcFBKZx`1oeJuYaZsOH}}ihx9Fv>o%`tC=nohVf!})@ zS&y2Ade~j_NGIMf2K>a5=YH5f_H}E2hxVNNhI@#6X_R{kTeY|1U-C`e7vytBM_wo9 zU_R!hmn~QiYh(X?4g5LHwOE4h8SEjFG5)T2?+21x?2ey`W$(ELpRQfKUcIUA3&gn= zdW~BenoJUPhy(I^5|ZGtCG$2xU+x)6 z&$Ult%#i-ZlJA~}vB6(srhKMne~gb{U0$c9*Xr?B4A*%jNAlmmxrF%v$5;}(Md*A3 z=lm0nBZpj|S9skLueuDm{*y3#u@!gQq1htol2k3|XTEaQ3R6FOii%9%z82Nl@ zEGfUWIVup%-aNOX6?Ojbe)XtjJalpF+Oq)W9&olSp$(d__kn9=BKwQ zj^GTA!j}F-jUf+Q!+Mu31agX2>N+1=weOs--aFVvLM$BBGt_1stckTPt&_D+>7%TV zI72RVT=$mygO9u$^2w=v|69H5vkuk^zxhAq-M*taT7GZk`|D2K5}WPVKH=Cm*_8hY z=Mh6+K)>!0`xe3Pg8Z#uy~BP=tOG4-!98{h0NR}j=Lf5-Fm=9ogtaB}gYJ^~}DVp$Fkc_c! zLF^Hp%MzQxw#Cppot1>%wOB_ozNvoeL$NpZk>8UkS*i9TU(@@cA$Aga7iDbwR#FEX z?>b1^Zs{28*8d{AYX242uX#&#Ott@2ca18KYD2ZX>AkEcqyu|11lNn}7lLbzgei__ zim6~Two}LYlO%o?y7WqD{4=+neer;w)0V{Wj5J+in1951magY{hUSPD@ZAv0=gf_3 zJjL6Gw7=S_9Y5o#Iht7dF4Gfqf49L$%q}o~qsa&4gKHss5|Wjz?^k@sYRS=eF7Ozi z{n+_V)L_=LoH~6+B5s52j+JVRqq++cw$@Pc*jtWIAD&q`G|-45bM4z-7ock+T=1f zeRy9vH$y~%j?H_Hz65s8BJUiU;BTD#O|*gyjvxkz-$`T*#@H)OV<5ka5d19__?znu z{v*h#b+*Qy2fynqbj{DYcAg_&4%v~M=Ujsu?BE!5 zVs^syvyN}77PXn@274F8oB}pt=`++X(D#omv zTd?t21?a?X^-15o^zJnwS%=xVCgxfRI^d(u(w=j6*zdufYHv^Obx8WmBiVm^O)-ED zD2+Bwl~H4nsr^P%F1hpJQwzL*(3QmFPxbIHbb0s%aO4j6GKfvuT&d(oKw#0 z(z$J7>I`$14dY!PW+dc*$4J|dp}J6Q#6T0&WghOIk@slJ`;_|&x(LC&*mV!`x%Bid z(C-DO-U<4>Ao1jpe}tct}!1C-%+zIHX5{e#E_( zjC+BxV_NPZ^G!itK(E%1?1q@0gyg@)HD;-OqHKp(I#ytm)VJO8g^dT4?k>A5H9 z*znz8uMi6%xTZH;W7DLYt?LDh@h?G*o=_9c1GT529$_c!o%L@dxlMQ;=Inxg4E;6X zo_)?d?pgBNAGMhS)&urc*%U|aOwjR1_Rktv%MJE*<#y!)wWv!S zVjvX%E!1|b^QqMY{ZSvu81R`T-MQ3xW6D<{u8z+&#zzc@dd!aa5M^a)?L&^7dD z0Q$&liofDo)i!hO%uNqR(AyTAKj19Fy1+&p@o%_}YgBpHdm7)*9Ac=^a&GDydZ2&S z!n#=JN}9&lLcS$>_Dn9dfVyT$f5ScDN3YU4_Fiy~?f1CGTA#{?^IzZ^x2!e!&TEEr zv!&kz-^0uI>f7(}D0Qs2GS96$26_AF)i(TFboEeiUzWFz9CE3}UWD+q>e8{Bma*jA z_0}~n^Ed{{_!CS1lb}O?AY^0y3bq2Q+zZjkqb z`+|3bFbYe$3B3b@UYOEvj%Q;P^}C#z?|8N(Fs_n!L%b`3 z^(B5^Rb|ijS+l&?vaQOxA?*?7E&X%e8ItgX2A}^2tX{L%n{%xdNZH!MHq^Z=L6KWorBh$3DgE z@~nOG+t$jQ?x)(Sj(>?rw!SYIZ0Pn`+HYT1+#5A#s1~&3x+C zoBdTfc4{vAbkE3L=BExZrjEDlrq)=h;XP#E-XP{EmiQa&hB((i2jW98KRvM?ugN`G z&Z|#yso`t6bUir>hOrqh*V;Xd(ue1}C4Ca-d8g*yIe$}qU~Fu&wB54Wr}BD%G5+zu zlFi_6qUN9Mk$zc26P%e4TYs;FkrEs91YZ+Vf7=YP@?Bk%1jhKtcivL&5zI5oHA2UZ zzl#vA&)-hB{+7D+K2k6X?Z)8uzQwy9+9P=^v5%hLLf3O96 zHFDop$|DJlYa!U<%GNmY`qb;eH358PNk0j??bLuKn2Wi4O2)IyF_g!c7*krwvz*U~DTY3~_|3p?g$lnL zOtp8}Ly*Vk+EEREzM&gz#Owqe8~&MuB&_^wp*OCJ*WvlV;c{I>agEG^||qQ=(gRG7-)j)Xt-XlZjnER`7 zrt^&ZoOAzi4-vz8KMA?c$Nq$4k@$xQK~3%<=HNc#p6i0=VH5sL;d2EYn_+zP8N=tz z&u0vuGs(=)9el(%m%7xSg1PQ{oBJK`8De{aj-8mDME1WVSqpo|y7re4OAfizfS!aT z^Nj4tz0^MF1wV1bJ9kMpN4mk*MF?`pZ3%0u>m!EPC61s^_gm$bo$Co)Tksg4{n(H2 znVC8}uwUSOeqzX9SsH(X?+44boLakOFZAlZExT;M+F*$xBJmzf?L`yprS}wll>H-z zm??WCU9!d<#W43yLcIZF>^Due8^_%Cr!lcR@m!We^TK-I$o2_8kE(S1xAh!1vJcD9 z*gey~u?@e$WbQb!v6y%5qXu8EDfC5~WCtZf$U^zLgwF=hu0*>X~YP(j8Z&&nEH|7!N@_xwQ`C%G5X%3*_7^+02pt zg#E~ogydFj>N6ibtlSUQI}-GsYbBN(Am6wqa%KH$v;D{itKhMH-$*|{XMU&LlK%~S ze>LCKaqP(Nzf02aJ-TB1EuUgW)q_2SPU2YpTn>Owki` zY=#)($#cCa{rmyqp(p6)7l?r_qQDq?1)ITs1i#J37i`6VV?Fk7z5b7CizQCd^uF&m zcrOS$iSjP+>Am2QzdyVW+_F{L^=^-Md2bN=&4T>aK-e${80tJ?pK?zt>oqt0>b#N4*OjCaYW z7BK%3<03Q$uBY`XM~QbFv7>OG1;;t2DfWhQHXrDE7JtiBtl83U?CTkK`|Frk#+J9v z*boCPIr=;?wpG7vj#-NTM3?`Kkk4#=A2CC^Y0@jUBW*u?cea?>K~iM|{n%IzHy?VktKSd&xClzrdN; z`upW5_-~;|{>B-?e-HKFKv(|#lU(LtF0Pwl{w?SQ(6JHcIjP@Oo4>R2K9Ki=fZkaXm@JutU%Kgc1DVBgrkov@!3+utOyL#%{n5%64^;yi$l zcxo>7KtJvg$#{q(diFO{GRBUd*d|yrYxnv+W>453_6@oS!Je|uBln#BWiQD)0_j|G z0Dq*v_DCNPDLq~q(<}QExd(opTk?$Gg3pg5`0Uvt^qT=j0s9ohEkRBQ@+;V>L+o9{ z^{~4hu_xHp1i4i_QKbS{WV;>nb(^bVuy0b zrKd2U1*g#4|2@V>%n%`{K|MeR?D&^Bf*NK@hkeoh(R#&5mgJ3n zmMwEEZC_c#_MLN1$$o+DH^^Zftm!6XtLyF>uZyFx!Pi9y=OM?nf5Z^`8^py`tJX!w zU$LJ#p$X1KWpYkLPg%#`l(1&Lwa*8Dk%Um`bgMofzkHw&MlgYxs#< z!gGNB7##^GPzq&wa>qPtUlW&pV!Z=!55CKKpq7Rl3G7 z#SugI^3MAi$rxHf4DlhT>AKkF#nG7DEqeA#&v58D*TR}$C#>}o#5F;U3U#{(!JPE` z)D!k0h#{UlI2YKuAddJZSQAhei4A{bEb(SZuN;k`u9dYgwvBZ!aT3l5XT@ykP3Q4j zh`%A1GyTSr?>Fi_V*So>{g(aKK4z}T>$GI;W=My)uzh7LWA>4~?IOeyO*V3gJEG^D zwGVW9!Dkp(w#J6FpxkLO6#F9b8zkf9cHp6iEU-iw)7`v#t>_c za-kP$J?9|z3yx{>Q;YmadXBN{a}q}_YQh%G0nBF@VR!E4&8RgS@x1^+j+6M=E(lGWc>3E{u6mx2ufuPS5&!dADb~-|u<+CLQ-A zyWRsBqGJPe_KSVn zg1xWTV&xcV>hq@S`E!JyWk{Z7*bL+9uQB8^AM?`-d&OGVLtw8$_9NV5SL}~X{+>9N ze0)Iel7HtOvtR5bd&j=El-Pdul0DVCSKi0|NR5wtL1}g>8)u!tc!K? zykCOP3qCit2>mXoV8eG52gLUT9a}58BlG_^Z|`pKy_sPz4E!-Y7HywH9(Mf1oj8x$#GbeZ?8H<-jlz7#H5^(G zYbqhVwifYRQy`Zbn;NQzY@!JKTlw5aT+6O&RO4E+e~hs+2eIUlPj4%rXLKa{utlqi zNi69n*?-fUa;P-}bb~KL{tD_J^>KhLgMS9-C-sPb8ar=Z>XEb5(}-T_y@j47_9?nx zAK6<#$7V|Sv7=+3Vh6bD z&tRVk#@w5OdsO%F(*4Rk%QOFTU&~(QCkD`$Y)#-Nj`)^fyhM*0#GP!}r~yN)18mTO z9FNU@OZ=XrO9y(WV(U3s>A zSlGc+edb&jiq><&8c!Qz@~KmW=kxqc^BdbvxfMh18{`z>HM4H0TCc&kV*SLnfZQjx zeDuybSl>L@8vCBelKWsCaIgA)%|6?oWRH6tIWPEm!iRo~d9w9X+j^Ca{#Zj3tP#$& zciBvl4oiDw*f(_S#LbY{s$g%~cO+x%#BIUcunw@bfVd}!A$A2%y8Wyfya%7;RBVR$ znqZ9I*XB(UcjEkPKd&t@PvpFBb=Rt@%f48mvwuyn-}7PL@t^qmU|)hcrg++q>}$H0 z;fy7QI#rCAGta|Z*qM(QB;zKC>p_tO?z>YbW(Hdl7oW)X>BE*| zh=V5RrHc}<&o}TBH^mm@x{hmMC&u|D#@K+deMoH7S;Fh;Y0alT<%9Fyy^V96^Qv;0 zEBCvj2W;+xxE^ykmofe(W?BntV(dEj4fdgR{@jOgk5)z7`N`wsi13$UKkro8Puu4g1JPcg!gr_eoMG zvotP$o@w%(Oxa)u_43~d-p&MV^O+#$fzX2~X|{C8|E(bU+qqzkv%-Fy5&FEZ=&TOf z7v#91c$0H&+wxbP`I-LVJP`j%6rFQGOHAocZ28{Ed1qX^oX@N{hskFw=zz~uo%j5f zt{j*l^B5bHIFsUR%4b%m&N$nMv+cCwgZ+U0S?1VmZ^u0Ur^jb6jOiiUf1{HNTl)?@ zC|rjFUw7_Vx0iw!?<#IJ;Ni6I_oo#&tmo*|x#622`7g;1)j~&n0IgXQ0XEoy$4%p6t; z<^sk`6#0p996si4qKLfysy*fM+A$_>YMvGIGB-W_*yU%%FAYKXyRJPt7LqZ`ljJsL39+0G)a80e1G0 zeJzqdW7)|!E#_g4DcCF4<25nfVIR_2JA1PP?|T0hZ2cBg{!Wn`L6d~7xEa3#Re4)A zfPF2p<6nv+zb6<|%lTb##3Nx|u&>BR{TbJXHH@HZ9Zhz6Sr^Eq_C8=O^i-s@*E z0eprSbfon>cE6TGy_+VT`)_^$_o6A%c?O2ynS}Wu@o|3xdJ{ZL`Po7T>?IbIYpC`8So`o8qPl3OQae(bcksZjThB<3( z#nkANUf&=tlVg{BC-!4UuYu>xV{E4#zwO9z&@?WA*SYjNvh~|C1#-miO4S}1Vz4{T zu}wC7#C1XKE$DqExVBv584}%KJK3_m!Mz0BSH`x}j(>f?_v=l5%C(<)YC!L-sR{PM z*JWsLGVB##YoY|`Taf2{>`}+H@aGujV$OYG$ObJ@q?@VQhL|C?$m?NjTTlCm{teX1 zaqFc<_Mww!+j))buZl4}#p4I(S-1U7=A0bYRS&W)>-J}fKaZ=%?B@_o6ya-Ni4E|N zU`vuqt(9=EpCmctQs<<~ZtNRr?j{{d;65#FCuT$}I(Elc_PC#i?&B70y+dZaOL(9B zyi+83_tbd*@D2jTMZPVnYEh5c%mw2ETYk93HQAvgY#R?bwi8z_V22TGN$`GiO}=3% zE<;WYsOS2~CW>HRfIWgLmiC;z2Xt(Je+u#pb;bcU;)r*S<95U{E+X6Mm7Zb!67Fs1 z-0pMxEW7GgK|dL6fIow6D3*1xFWi6pzHy(m*rO8bX_Bm)xF0{io%&mC@;7k&-B$Vg zE`C#q*@AfLlfT7?dPNdi*tR6S<#jf#{}WR&u)n0n_vbAhAB+o2w*L-A-^##ua%O4_ z*@j*M&*`2R+h^$*k9(>&rNe%pRqe4D!Ad$dd^wj|T`*@)6xo`<5Bq_tdWQODB^}#2 zcHEP!QP=Z&Z26RjeLYbAl>ayv^xt-tQ!#Lkw`|aKE|~H8pXEybyP;KQeb6sV*)m1v zfWUbmj9^Ow=YS)zq@NU>H<-@ZLog-(JDhd4>SdbFIADnNK;_&+^b596`Jm{m!?Za6 z7$F(sbL<(9?*@58Y*FKUXi1**@DXFMS^q{74=YJ@Q*>4ZKDV-*vYpuXNjh(H4)!I^ z$+BJgda@P8{+m3_WBb&6-#~mJHuk?t!ty;3_t}>8eSt6gi+sM9xv#6>o=0Nu@;l!( zM$|`Q!#@SFt_8wV)gzW7n*D)`){`FM|aF8>AX*IH@|bUTxlKX zdF;5B`pdund_O?8S+}ptU&P{@N$|Zg1HNI5ZI;-xp2yi<6vy{fW*y_~vtAXm^^L`M zRy)9EyJeAW>bnfOr~%)7L$n9{cgWQD-3dPO$X^L+GY8Co`Jn|vGGlv*xSMp$NV)6- zdomM@p$03(lS`c<=*@jr={LMC)@ZO5#h+kzOq0(3uxCBN7<-l9FgCU!hgbn~^)#pL zS)TnkhdkpLkEeWQOFyyC62JEly$gC6N555L*3<-NKF|+rx5Qt?=4_4gprA;C&xz3O zJLQmX3+hmB33|w2!&d|J5;0qn-hnlF9aHv9)0jHURhTdRJw!cX$zOqMVK0JwYTJ)2 zv7g@KrF#^(kGY?DSMVNzk>r-n-*o&P#d`|SjcvA*TgXvO=3-uY+3Jb@=&b~-F@v2L z_f9T!F-1!~)B6xRV#pys@7ap~Emrn$JX|;S6CW{6Fy5-eoXpYE`dM=o*k!C08G7^k4_BGjoHRtu`J#Y+i1WPi{nR~%U zEHyIO_BNMui0KK&_^2}uu;u#15pTNmlOo&6d9R&09zW%AFHr*tHQ-*Jg8QwArF$=f z4e-NEx@|=^=Wy>^mV!j8<~-vRS6@4A2;97|rNX;wDJMG3ez?B5A?Vu;;>{0z1keqzwE5i^AQ zIpvbi*vFWhCa7T;V_yg4bukl+Z&cagn;J9fQHS^@+{--Hc-G5f&q*C>@?QJ~Vl!_! zIqqA%t9}*qI$~`tN$XxS#MgL+ZqJQ-$dca9Ua|cxxi&g=s6S(kJtSl7 z_=#Hyu8XhHDd&Ta9LRO3YnpUmPD2k}6v2M6=TH;ushQFNKRM*PE_zROmSXIuubyzP z**4|R& zIe!EB^#I$+N_ESh@*n4Zwl(=qrfiw5@k!O+q&J4_u!5p^Oz5c)dmFJk7Y*7PwV{5J0k$*$3IbyDFrE}Tu9u!I6FWe*6u^Zd)|J;+hFN@$_ z_I+KYbI-Sc`yO9Y+!W+h<)2{3-!5Q>EzUf~<8RUL(-14c_(qfcZ}4w)macQobF(YPDbstmT4Vj+Cr(Wb+`Ysuwik-fTigaj+Dcx-8&Orx!O%$>GO%<|#kX3%z z`c@mFJy7_T3;b={`MwqWZ7~AtP2->MIeoA7#N-<=UskuG8czf_~{6s_=SP8|!4acSx!Cb%sDr&9V1pgxoUD(SH_IiPVJ(KW0b~2Kyv60%(?3-Q97C`4+=bV^_4fc& z?SUCd$2SAVGH#-X9_MYyC9rKljTQS^Vov5|ezT>w*o&?{xz4`UXU|2kxi8m7GG7bO zJ*FrAKL+{VxZHyNru3f8)$Vsp?sb03C|xiX(@7>N79%yI*7zOY2|{cl3#!*b;rw5A#!xxv4*bedvs# z{`pUa?BrKLJ?b)t?;p>DzX!ylBa8B>u|=-m|0qV-hW`m-9G~Tq{f)xkPy9`Fi97AC zTNS(2%MeYJV8?p$+RyO?}OEg53=1&_of`sDgfmXo;=&mf;-+op)JCbdPyYn(Uv7*#~(Jbmmdl%)v`(!z^cUABk z(?ki-vEe6{TGXUA^DrOt!VH@BX#`vHyca%=d%ADmbn4vv*q?kk-f@=n`h;V0{+sUj zTyLm;6GeEh*tZ(6p9UK-#BY)74UMsv1Y;N%wrt=Ux4!WaLw*a;VGI1^tuG)BYQkgW zI(MiL z=-BWX`#mpl;PI0rhFqu@?8}n46+Je3&_1)*fCBk&n)Q~+gs#X zVqXXO+T?5Tr287>I@E0kUc*g#on7lJ;^+C&Ub3g&-zL2zuy4Vh6K`tDL#H0S&?jq{ zUvS>TEuPbC|EA7c481`8683a!Ei+(E*kA?Ny8P@**Zs-;%HL*19_mNS)O zw)7kJTi#-xY_)2iAILe58H#;_xSW^unbu+*ziWRh&-s_;g!1QmoRekFzuCL$zcJ-E zTY9GH@6pVNF*39GyERdDz6U*|b;f2&zfpAVXPWd8Y)Q!Hft(4#N)(+>JmE8pshE>3 z8`z$8+dfHh;OERlXCzA$opGFW*URlah&CrAM%-=ZU0?))*R8}x3Z<)i}b&N{c>KehdxsM z>?^Xf-`rQ+W881P=eXYhAF>JV$sUxX+lIu>z0JK|cxLqcAsLT!&l5Yv7Sw(MzsWHp z`ubKb#b=yn+c5TyKiRTn@HbJ!(zg-cR`rFUZzyPdcP1F$7_vhN_@?cm3ckq( z-*5r?Z&}JQ)CB6o6wKSAhb{Suymn$o%?tT>b4?1FDy#(e8$u#JRckd=7la<0S~2s*X!Q6F-xC!M${c7UF--MxF9 zW3t|rZ*1SGK6Q-kjze})#LzlePfIXn-S}9iVO#}%m_d^)!h6I%0b}f2kVhTn-OQ{0 zxMp2&{kV=?+aY#>aS_C=bX}>FnHsZRVyMj=_?ed;IM3oNs|PL4w3d3D(c_37sYMO< z=N{>^sTX?UKIPu!eP9^(1Y=`6W8NcEY*FLg#5)SG;U|8lwbM^~A@}P($*-zo=)DO1 zBYez*A0PImIXoA8$X>9ITsy8K`?a-q>?d%Yv9-9yMX}`Ne!A*HjrvI2ET?Sl4OY)Dp#D*NFA(1Zb?AY*Ko5+u)1#TuFLuB_Vov7iU+_)bRGk|| ze@}hlmS75L)LA3 zlg>xZ3tcu-q(9-9o6I?^4MtEUPq@$68`F}G&)9#;rWjMC8$Tz=E~?1yz@g`!cL49D zB8fgllb!e0jCaD&d%^IIzz#(^V|+Vc4q(0$`;f$w2Tf3~iyAETK!5bI)zb}n?FZNl zKJ*<>i<+>K&bTGJd!QdPr9+8+=@}Rs{KQv5eb2%C^w9(KJYSB*elnsKb*VpJz)u`G z)M0MsErRO|{C)sBzA1J{#*V4-^LyjpoDy-=W)8EWKP0vpurKTp46zgWUhw^3Ke0I; zfB(Q)FOOX}OZ%y79Y6Cn!5Vt35y`lOkC>fs{wZ?|I(g87BAH!(|%|D^fUiReV`Y?UU2_$zcxv9_6a+F;;6d>??Jy8 zv-JD%$)97b=bYcUTxY*zjehY}la+VskwH9DnNgp(kqS_94$akMVU; z1$(<=uX)y?Xgo!e4?nC6*jvJ5*P{;C^5?qh+VZa9J!BZSU`oOk^LO*{I|Fc^;@vjGtfJDoc;E7`Av~-*kAU5 zJug9%WV}N%E(yl?*hliIy9GURKTY9#%(BJvIwWt{U*x~#-)xS>*JE#ynHqNFX{X<8 zpD6|(48hviFV_6?`>Ef}$=|&Eeazo>qHEk_=XaGD_6ymCZS2=n9cnT!^8xF)(PGb# zBeA8!`K`&g2TKw>M%s4TbByC|Hh=pzeLEZ5s0q&Z*nT8AGuV<(d=ry%Yt>#PvJV~K z_!8G~jrq{C?T$k__cqs?kN9;^{*;gBgJ%xor@XFuZ%p~0*z!Ts-=l$HvYpg) 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 diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index e11e6bb5284..7e236028d18 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -27,6 +27,6 @@ def test_valid_file() -> None: # Assert assert im.format == "MCIDAS" - assert im.mode == "I" + assert im.mode == "I;16B" assert im.size == (1800, 400) assert_image_equal_tofile(im, saved_file) diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index b4460a9a51f..a1214ad5047 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -50,9 +50,7 @@ def _open(self) -> None: if w[11] == 1: mode = rawmode = "L" elif w[11] == 2: - # FIXME: add memory map support - mode = "I" - rawmode = "I;16B" + mode = rawmode = "I;16B" elif w[11] == 4: # FIXME: add memory map support mode = "I" From d4162f85056223098fef0ba3f87e58519ba2955f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Jun 2025 18:27:49 +1000 Subject: [PATCH 1824/2374] Updated return type --- Tests/test_file_mpo.py | 2 +- src/PIL/Image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 682fc7361a5..6c9c541f126 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -316,7 +316,7 @@ def test_save_xmp() -> None: im = Image.new("RGB", (1, 1)) im2 = Image.new("RGB", (1, 1), "#f00") - def roundtrip_xmp(): + def roundtrip_xmp() -> list[Any]: im_reloaded = roundtrip(im, xmp=b"Default", save_all=True, append_images=[im2]) xmp = [im_reloaded.info["xmp"]] im_reloaded.seek(1) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7b1b575a0ff..9fc1c70671b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2600,7 +2600,7 @@ def save( if open_fp: fp.close() - def _attach_default_encoderinfo(self, im: Image) -> Any: + def _attach_default_encoderinfo(self, im: Image) -> dict[str, Any]: encoderinfo = getattr(self, "encoderinfo", {}) self.encoderinfo = {**im._default_encoderinfo, **encoderinfo} return encoderinfo From be2b4e78644fdc85e63f08a22514e4d32072439f Mon Sep 17 00:00:00 2001 From: Kylian Ronfleux--Corail <35237015+Kyliroco@users.noreply.github.com> Date: Mon, 30 Jun 2025 12:46:40 +0200 Subject: [PATCH 1825/2374] Fix qtables and quality scaling (#8879) Co-authored-by: Andrew Murray --- Tests/test_file_jpeg.py | 18 ++++++++++++++++++ docs/handbook/image-file-formats.rst | 7 +++++++ src/libImaging/JpegEncode.c | 15 +++++++++++---- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 6dab418bfda..5afae041287 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -611,6 +611,24 @@ def _n_qtables_helper(n: int, test_file: str) -> None: None ) ] + + for quality in range(101): + qtable_from_qtable_quality = self.roundtrip( + im, + qtables={0: standard_l_qtable, 1: standard_chrominance_qtable}, + quality=quality, + ).quantization + + qtable_from_quality = self.roundtrip(im, quality=quality).quantization + + if features.check_feature("libjpeg_turbo"): + assert qtable_from_qtable_quality == qtable_from_quality + else: + assert qtable_from_qtable_quality[0] == qtable_from_quality[0] + assert ( + qtable_from_qtable_quality[1][1:] == qtable_from_quality[1][1:] + ) + # list of qtable lists assert_image_similar( im, diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 83df2bd5c4f..03ee96c0f00 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -557,6 +557,8 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: hardly any gain in image quality. The value ``keep`` is only valid for JPEG files and will retain the original image quality level, subsampling, and qtables. + For more information on how qtables are modified based on the quality parameter, + see the qtables section. **optimize** If present and true, indicates that the encoder should make an extra pass @@ -622,6 +624,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: range(len(keys))) of lists of 64 integers. There must be between 2 and 4 tables. + If a quality parameter is provided, the qtables will be adjusted accordingly. + By default, the qtables are based on a standard JPEG table with a quality of 50. + The qtable values will be reduced if the quality is higher than 50 and increased + if the quality is lower than 50. + .. versionadded:: 2.5.0 **streamtype** diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 79a38e12fb3..972435ee117 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -175,18 +175,21 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { /* Use custom quantization tables */ if (context->qtables) { int i; - int quality = 100; + int quality = 50; int last_q = 0; + boolean force_baseline = FALSE; if (context->quality != -1) { quality = context->quality; + force_baseline = TRUE; } + int scale_factor = jpeg_quality_scaling(quality); for (i = 0; i < context->qtablesLen; i++) { jpeg_add_quant_table( &context->cinfo, i, &context->qtables[i * DCTSIZE2], - quality, - FALSE + scale_factor, + force_baseline ); context->cinfo.comp_info[i].quant_tbl_no = i; last_q = i; @@ -195,7 +198,11 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { // jpeg_set_defaults created two qtables internally, but we only // wanted one. jpeg_add_quant_table( - &context->cinfo, 1, &context->qtables[0], quality, FALSE + &context->cinfo, + 1, + &context->qtables[0], + scale_factor, + force_baseline ); } for (i = last_q; i < context->cinfo.num_components; i++) { From da10ed1cf3c4123a98a2f765d3beaf830d47d113 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 30 Jun 2025 21:46:07 +1000 Subject: [PATCH 1826/2374] Add support for iOS (#9030) Co-authored-by: Andrew Murray --- .github/workflows/wheels-dependencies.sh | 207 ++++++++++++++---- .github/workflows/wheels-test.ps1 | 5 +- .github/workflows/wheels-test.sh | 4 +- .github/workflows/wheels.yml | 23 +- .pre-commit-config.yaml | 6 +- MANIFEST.in | 2 + Tests/oss-fuzz/test_fuzzers.py | 5 +- Tests/test_main.py | 1 + Tests/test_pyroma.py | 4 +- {Tests => checks}/32bit_segfault_check.py | 0 {Tests => checks}/check_fli_oob.py | 0 {Tests => checks}/check_fli_overflow.py | 0 {Tests => checks}/check_icns_dos.py | 0 {Tests => checks}/check_imaging_leaks.py | 0 {Tests => checks}/check_j2k_dos.py | 0 {Tests => checks}/check_j2k_leaks.py | 0 {Tests => checks}/check_j2k_overflow.py | 0 {Tests => checks}/check_jp2_overflow.py | 0 {Tests => checks}/check_jpeg_leaks.py | 0 {Tests => checks}/check_large_memory.py | 0 {Tests => checks}/check_large_memory_numpy.py | 0 {Tests => checks}/check_libtiff_segfault.py | 0 {Tests => checks}/check_png_dos.py | 0 {Tests => checks}/check_release_notes.py | 0 {Tests => checks}/check_wheel.py | 12 +- docs/releasenotes/11.3.0.rst | 8 +- patches/README.md | 14 ++ patches/iOS/brotli-1.1.0.tar.gz.patch | 46 ++++ patches/iOS/libwebp-1.5.0.tar.gz.patch | 42 ++++ pyproject.toml | 46 +++- setup.py | 39 ++++ 31 files changed, 406 insertions(+), 58 deletions(-) rename {Tests => checks}/32bit_segfault_check.py (100%) rename {Tests => checks}/check_fli_oob.py (100%) rename {Tests => checks}/check_fli_overflow.py (100%) rename {Tests => checks}/check_icns_dos.py (100%) rename {Tests => checks}/check_imaging_leaks.py (100%) rename {Tests => checks}/check_j2k_dos.py (100%) rename {Tests => checks}/check_j2k_leaks.py (100%) rename {Tests => checks}/check_j2k_overflow.py (100%) rename {Tests => checks}/check_jp2_overflow.py (100%) rename {Tests => checks}/check_jpeg_leaks.py (100%) rename {Tests => checks}/check_large_memory.py (100%) rename {Tests => checks}/check_large_memory_numpy.py (100%) rename {Tests => checks}/check_libtiff_segfault.py (100%) rename {Tests => checks}/check_png_dos.py (100%) rename {Tests => checks}/check_release_notes.py (100%) rename {Tests => checks}/check_wheel.py (75%) create mode 100644 patches/README.md create mode 100644 patches/iOS/brotli-1.1.0.tar.gz.patch create mode 100644 patches/iOS/libwebp-1.5.0.tar.gz.patch diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 5384a74c0cd..d761d93b62c 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,42 +1,98 @@ #!/bin/bash -# Setup that needs to be done before multibuild utils are invoked -PROJECTDIR=$(pwd) -if [[ "$(uname -s)" == "Darwin" ]]; then - # Safety check - macOS builds require that CIBW_ARCHS is set, and that it - # only contains a single value (even though cibuildwheel allows multiple - # values in CIBW_ARCHS). +# Safety check - Pillow builds require that CIBW_ARCHS is set, and that it only +# contains a single value (even though cibuildwheel allows multiple values in +# CIBW_ARCHS). This check doesn't work on Linux because of how the CIBW_ARCHS +# variable is exposed. +function check_cibw_archs { if [[ -z "$CIBW_ARCHS" ]]; then - echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined." + echo "ERROR: Pillow builds require CIBW_ARCHS be defined." exit 1 fi if [[ "$CIBW_ARCHS" == *" "* ]]; then - echo "ERROR: Pillow macOS builds only support a single architecture in CIBW_ARCHS." + echo "ERROR: Pillow builds only support a single architecture in CIBW_ARCHS." exit 1 fi +} + +# Setup that needs to be done before multibuild utils are invoked. Process +# potential cross-build platforms before native platforms to ensure that we pick +# up the cross environment. +PROJECTDIR=$(pwd) +if [[ "$CIBW_PLATFORM" == "ios" ]]; then + check_cibw_archs + # On iOS, CIBW_ARCHS is actually a multi-arch - arm64_iphoneos, + # arm64_iphonesimulator or x86_64_iphonesimulator. Split into the CPU + # platform, and the iOS SDK. + PLAT=$(echo $CIBW_ARCHS | sed "s/\(.*\)_\(.*\)/\1/") + IOS_SDK=$(echo $CIBW_ARCHS | sed "s/\(.*\)_\(.*\)/\2/") + + # Build iOS builds in `build/iphoneos` or `build/iphonesimulator` + # (depending on the build target). Install them into `build/deps/iphoneos` + # or `build/deps/iphonesimulator` + WORKDIR=$(pwd)/build/$IOS_SDK + BUILD_PREFIX=$(pwd)/build/deps/$IOS_SDK + PATCH_DIR=$(pwd)/patches/iOS + + # GNU tooling insists on using aarch64 rather than arm64 + if [[ $PLAT == "arm64" ]]; then + GNU_ARCH=aarch64 + else + GNU_ARCH=x86_64 + fi + + IOS_SDK_PATH=$(xcrun --sdk $IOS_SDK --show-sdk-path) + CMAKE_SYSTEM_NAME=iOS + IOS_HOST_TRIPLE=$PLAT-apple-ios$IPHONEOS_DEPLOYMENT_TARGET + if [[ "$IOS_SDK" == "iphonesimulator" ]]; then + IOS_HOST_TRIPLE=$IOS_HOST_TRIPLE-simulator + fi + # GNU Autotools doesn't recognize the existence of arm64-apple-ios-simulator + # as a valid host. However, the only difference between arm64-apple-ios and + # arm64-apple-ios-simulator is the choice of sysroot, and that is + # coordinated by CC, CFLAGS etc. From the perspective of configure, the two + # platforms are identical, so we can use arm64-apple-ios consistently. + # This (mostly) avoids us needing to patch config.sub in dependency sources. + HOST_CONFIGURE_FLAGS="--disable-shared --enable-static --host=$GNU_ARCH-apple-ios --build=$GNU_ARCH-apple-darwin" + + # CMake has native support for iOS. However, most of that support is based + # on using the Xcode builder, which isn't very helpful for most of Pillow's + # dependencies. Therefore, we lean on the OSX configurations, plus CC, CFLAGS + # etc. to ensure the right sysroot is selected. + HOST_CMAKE_FLAGS="-DCMAKE_SYSTEM_NAME=$CMAKE_SYSTEM_NAME -DCMAKE_SYSTEM_PROCESSOR=$GNU_ARCH -DCMAKE_OSX_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET -DCMAKE_OSX_SYSROOT=$IOS_SDK_PATH -DBUILD_SHARED_LIBS=NO" + + # Meson needs to be pointed at a cross-platform configuration file + # This will be generated once CC etc. have been evaluated. + HOST_MESON_FLAGS="--cross-file $WORKDIR/meson-cross.txt -Dprefer_static=true -Ddefault_library=static" + +elif [[ "$(uname -s)" == "Darwin" ]]; then + check_cibw_archs # Build macOS dependencies in `build/darwin` # Install them into `build/deps/darwin` + PLAT=$CIBW_ARCHS WORKDIR=$(pwd)/build/darwin BUILD_PREFIX=$(pwd)/build/deps/darwin else # Build prefix will default to /usr/local + PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}" WORKDIR=$(pwd)/build MB_ML_LIBC=${AUDITWHEEL_POLICY::9} MB_ML_VER=${AUDITWHEEL_POLICY:9} fi -PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}" # Define custom utilities source wheels/multibuild/common_utils.sh source wheels/multibuild/library_builders.sh -if [ -z "$IS_MACOS" ]; then +if [[ -z "$IS_MACOS" ]]; then source wheels/multibuild/manylinux_utils.sh fi ARCHIVE_SDIR=pillow-depends-main -# Package versions for fresh source builds +# Package versions for fresh source builds. Version numbers with "Patched" +# annotations have a source code patch that is required for some platforms. If +# you change those versions, ensure the patch is also updated. FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=11.2.1 LIBPNG_VERSION=1.6.49 @@ -47,32 +103,58 @@ TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 ZLIB_VERSION=1.3.1 ZLIB_NG_VERSION=2.2.4 -LIBWEBP_VERSION=1.5.0 +LIBWEBP_VERSION=1.5.0 # Patched; next release won't need patching. See patch file. BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 -BROTLI_VERSION=1.1.0 +BROTLI_VERSION=1.1.0 # Patched; next release won't need patching. See patch file. LIBAVIF_VERSION=1.3.0 function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi - # This essentially duplicates the Homebrew recipe - CFLAGS="$CFLAGS -Wno-int-conversion" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ + # This essentially duplicates the Homebrew recipe. + # On iOS, we need a binary that can be executed on the build machine; but we + # can create a host-specific pc-path to store iOS .pc files. To ensure a + # macOS-compatible build, we temporarily clear environment flags that set + # iOS-specific values. + if [[ -n "$IOS_SDK" ]]; then + ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS + ORIGINAL_IPHONEOS_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET + unset HOST_CONFIGURE_FLAGS + unset IPHONEOS_DEPLOYMENT_TARGET + fi + + CFLAGS="$CFLAGS -Wno-int-conversion" CPPFLAGS="" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ --disable-debug --disable-host-tool --with-internal-glib \ --with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \ --with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include + + if [[ -n "$IOS_SDK" ]]; then + HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS + IPHONEOS_DEPLOYMENT_TARGET=$ORIGINAL_IPHONEOS_DEPLOYMENT_TARGET + fi; + export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config touch pkg-config-stamp } function build_zlib_ng { if [ -e zlib-stamp ]; then return; fi + # zlib-ng uses a "configure" script, but it's not a GNU autotools script, so + # it doesn't honor the usual flags. Temporarily disable any + # cross-compilation flags. + ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS + unset HOST_CONFIGURE_FLAGS + build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat - if [ -n "$IS_MACOS" ]; then + HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS + + if [[ -n "$IS_MACOS" ]] && [[ -z "$IOS_SDK" ]]; then # Ensure that on macOS, the library name is an absolute path, not an # @rpath, so that delocate picks up the right library (and doesn't need # DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an - # option to control the install_name. + # option to control the install_name. This isn't needed on iOS, as iOS + # only builds the static library. install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib fi touch zlib-stamp @@ -82,7 +164,7 @@ function build_brotli { if [ -e brotli-stamp ]; then return; fi local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ - && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ + && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \ && make install) touch brotli-stamp } @@ -93,7 +175,7 @@ function build_harfbuzz { local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ - && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=minsize -Dfreetype=enabled -Dglib=disabled -Dtests=disabled) + && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=minsize -Dfreetype=enabled -Dglib=disabled -Dtests=disabled $HOST_MESON_FLAGS) (cd $out_dir/build \ && meson install) touch harfbuzz-stamp @@ -164,19 +246,19 @@ function build { fi build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto - if [ -n "$IS_MACOS" ]; then + if [[ -n "$IS_MACOS" ]]; then build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else - sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc + sed "s/\${pc_sysrootdir\}//" $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc fi build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib build_libjpeg_turbo - if [ -n "$IS_MACOS" ]; then + if [[ -n "$IS_MACOS" ]]; then # Custom tiff build to include jpeg; by default, configure won't include - # headers/libs in the custom macOS prefix. Explicitly disable webp, + # headers/libs in the custom macOS/iOS prefix. Explicitly disable webp, # libdeflate and zstd, because on x86_64 macs, it will pick up the # Homebrew versions of those libraries from /usr/local. build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ @@ -186,7 +268,10 @@ function build { build_tiff fi - build_libavif + if [[ -z "$IOS_SDK" ]]; then + # Short term workaround; don't build libavif on iOS + build_libavif + fi build_libpng build_lcms2 build_openjpeg @@ -201,14 +286,44 @@ function build { build_brotli - if [ -n "$IS_MACOS" ]; then + if [[ -n "$IS_MACOS" ]]; then # Custom freetype build build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no else build_freetype fi - build_harfbuzz + if [[ -z "$IOS_SDK" ]]; then + # On iOS, there's no vendor-provided raqm, and we can't ship it due to + # licensing, so there's no point building harfbuzz. + build_harfbuzz + fi +} + +function create_meson_cross_config { + cat << EOF > $WORKDIR/meson-cross.txt +[binaries] +pkg-config = '$BUILD_PREFIX/bin/pkg-config' +cmake = '$(which cmake)' +c = '$CC' +cpp = '$CXX' +strip = '$STRIP' + +[built-in options] +c_args = '$CFLAGS -I$BUILD_PREFIX/include' +cpp_args = '$CXXFLAGS -I$BUILD_PREFIX/include' +c_link_args = '$CFLAGS -L$BUILD_PREFIX/lib' +cpp_link_args = '$CFLAGS -L$BUILD_PREFIX/lib' + +[host_machine] +system = 'darwin' +subsystem = 'ios' +kernel = 'xnu' +cpu_family = '$(uname -m)' +cpu = '$(uname -m)' +endian = 'little' + +EOF } # Perform all dependency builds in the build subfolder. @@ -227,24 +342,40 @@ if [[ ! -d $WORKDIR/pillow-depends-main ]]; then fi if [[ -n "$IS_MACOS" ]]; then - # Homebrew (or similar packaging environments) install can contain some of - # the libraries that we're going to build. However, they may be compiled - # with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use, - # and they may bring in other dependencies that we don't want. The same will - # be true of any other locations on the path. To avoid conflicts, strip the - # path down to the bare minimum (which, on macOS, won't include any - # development dependencies). - export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" - export CMAKE_PREFIX_PATH=$BUILD_PREFIX - # Ensure the basic structure of the build prefix directory exists. mkdir -p "$BUILD_PREFIX/bin" mkdir -p "$BUILD_PREFIX/lib" - # Ensure pkg-config is available + # Ensure pkg-config is available. This is done *before* setting CC, CFLAGS + # etc. to ensure that the build is *always* a macOS build, even when building + # for iOS. build_pkg_config - # Ensure cmake is available + + # Ensure cmake is available, and that the default prefix used by CMake is + # the build prefix python3 -m pip install cmake + export CMAKE_PREFIX_PATH=$BUILD_PREFIX + + if [[ -n "$IOS_SDK" ]]; then + export AR="$(xcrun --find --sdk $IOS_SDK ar)" + export CPP="$(xcrun --find --sdk $IOS_SDK clang) -E" + export CC=$(xcrun --find --sdk $IOS_SDK clang) + export CXX=$(xcrun --find --sdk $IOS_SDK clang++) + export LD=$(xcrun --find --sdk $IOS_SDK ld) + export STRIP=$(xcrun --find --sdk $IOS_SDK strip) + + CPPFLAGS="$CPPFLAGS --sysroot=$IOS_SDK_PATH" + CFLAGS="-target $IOS_HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET" + CXXFLAGS="-target $IOS_HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET" + + # Having IPHONEOS_DEPLOYMENT_TARGET in the environment causes problems + # with some cross-building toolchains, because it introduces implicit + # behavior into clang. + unset IPHONEOS_DEPLOYMENT_TARGET + + # Now that we know CC etc., we can create a meson cross-configuration file + create_meson_cross_config + fi fi wrap_wheel_builder build diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 index 9f5561c46d4..e6453d09118 100644 --- a/.github/workflows/wheels-test.ps1 +++ b/.github/workflows/wheels-test.ps1 @@ -15,15 +15,12 @@ if (Test-Path $venv\Scripts\pypy.exe) { $python = "python.exe" } & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f -if ("$venv" -like "*\cibw-run-*-win_amd64\*") { - & $venv\Scripts\$python -m pip install numpy -} cd $pillow & $venv\Scripts\$python -VV if (!$?) { exit $LASTEXITCODE } & $venv\Scripts\$python selftest.py if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\$python -m pytest -vv -x Tests\check_wheel.py +& $venv\Scripts\$python -m pytest -vv -x checks\check_wheel.py if (!$?) { exit $LASTEXITCODE } & $venv\Scripts\$python -m pytest -vv -x Tests if (!$?) { exit $LASTEXITCODE } diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 94dbb46791e..d73b6be5847 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -25,8 +25,6 @@ else yum install -y fribidi fi -python3 -m pip install numpy - if [ ! -d "test-images-main" ]; then curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip unzip pillow-test-images.zip @@ -35,5 +33,5 @@ fi # Runs tests python3 selftest.py -python3 -m pytest -vv -x Tests/check_wheel.py +python3 -m pytest -vv -x checks/check_wheel.py python3 -m pytest -vv -x diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 16c350a1484..52a3f2cdb2a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -51,40 +51,60 @@ jobs: matrix: include: - name: "macOS 10.10 x86_64" + platform: macos os: macos-13 cibw_arch: x86_64 build: "cp3{9,10,11}*" macosx_deployment_target: "10.10" - name: "macOS 10.13 x86_64" + platform: macos os: macos-13 cibw_arch: x86_64 build: "cp3{12,13,14}*" macosx_deployment_target: "10.13" - name: "macOS 10.15 x86_64" + platform: macos os: macos-13 cibw_arch: x86_64 build: "pp3*" macosx_deployment_target: "10.15" - name: "macOS arm64" + platform: macos os: macos-latest cibw_arch: arm64 macosx_deployment_target: "11.0" - name: "manylinux2014 and musllinux x86_64" + platform: linux os: ubuntu-latest cibw_arch: x86_64 - name: "manylinux_2_28 x86_64" + platform: linux os: ubuntu-latest cibw_arch: x86_64 build: "*manylinux*" manylinux: "manylinux_2_28" - name: "manylinux2014 and musllinux aarch64" + platform: linux os: ubuntu-24.04-arm cibw_arch: aarch64 - name: "manylinux_2_28 aarch64" + platform: linux os: ubuntu-24.04-arm cibw_arch: aarch64 build: "*manylinux*" manylinux: "manylinux_2_28" + - name: "iOS arm64 device" + platform: ios + os: macos-latest + cibw_arch: arm64_iphoneos + - name: "iOS arm64 simulator" + platform: ios + os: macos-latest + cibw_arch: arm64_iphonesimulator + - name: "iOS x86_64 simulator" + platform: ios + os: macos-13 + cibw_arch: x86_64_iphonesimulator steps: - uses: actions/checkout@v4 with: @@ -103,6 +123,7 @@ jobs: run: | python3 -m cibuildwheel --output-dir wheelhouse env: + CIBW_PLATFORM: ${{ matrix.platform }} CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy @@ -114,7 +135,7 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: dist-${{ matrix.os }}${{ matrix.macosx_deployment_target && format('-{0}', matrix.macosx_deployment_target) }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} + name: dist-${{ matrix.name }} path: ./wheelhouse/*.whl windows: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6abb732bbaa..d5fd964f128 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: rev: v1.5.5 hooks: - id: remove-tabs - exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) + exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$) - repo: https://github.com/pre-commit/mirrors-clang-format rev: v20.1.6 @@ -46,9 +46,9 @@ repos: - id: check-yaml args: [--allow-multiple-documents] - id: end-of-file-fixer - exclude: ^Tests/images/ + exclude: ^Tests/images/|\.patch$ - id: trailing-whitespace - exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ + exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$ - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.33.1 diff --git a/MANIFEST.in b/MANIFEST.in index 48085b82ed0..95a6b1b9297 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,6 +13,8 @@ include LICENSE include Makefile include tox.ini graft Tests +graft checks +graft patches graft src graft depends graft winbuild diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index e42ec90aa54..37d11e0ba4c 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -10,8 +10,9 @@ from PIL import Image, features from Tests.helper import skip_unless_feature -if sys.platform.startswith("win32"): - pytest.skip("Fuzzer is linux only", allow_module_level=True) +if sys.platform.startswith("win32") or sys.platform == "ios": + pytest.skip("Fuzzer doesn't run on Windows or iOS", allow_module_level=True) + libjpeg_turbo_version = features.version("libjpeg_turbo") if libjpeg_turbo_version is not None: version = packaging.version.parse(libjpeg_turbo_version) diff --git a/Tests/test_main.py b/Tests/test_main.py index 2582dbee3db..65e7a44d8d0 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -7,6 +7,7 @@ import pytest +@pytest.mark.skipif(sys.platform == "ios", reason="Processes not supported on iOS") @pytest.mark.parametrize( "args, report", ((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)), diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 8235daf3282..9669f485a6f 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,5 +1,7 @@ from __future__ import annotations +from importlib.metadata import metadata + import pytest from PIL import __version__ @@ -9,7 +11,7 @@ def test_pyroma() -> None: # Arrange - data = pyroma.projectdata.get_data(".") + data = pyroma.projectdata.map_metadata_keys(metadata("Pillow")) # Act rating = pyroma.ratings.rate(data) diff --git a/Tests/32bit_segfault_check.py b/checks/32bit_segfault_check.py similarity index 100% rename from Tests/32bit_segfault_check.py rename to checks/32bit_segfault_check.py diff --git a/Tests/check_fli_oob.py b/checks/check_fli_oob.py similarity index 100% rename from Tests/check_fli_oob.py rename to checks/check_fli_oob.py diff --git a/Tests/check_fli_overflow.py b/checks/check_fli_overflow.py similarity index 100% rename from Tests/check_fli_overflow.py rename to checks/check_fli_overflow.py diff --git a/Tests/check_icns_dos.py b/checks/check_icns_dos.py similarity index 100% rename from Tests/check_icns_dos.py rename to checks/check_icns_dos.py diff --git a/Tests/check_imaging_leaks.py b/checks/check_imaging_leaks.py similarity index 100% rename from Tests/check_imaging_leaks.py rename to checks/check_imaging_leaks.py diff --git a/Tests/check_j2k_dos.py b/checks/check_j2k_dos.py similarity index 100% rename from Tests/check_j2k_dos.py rename to checks/check_j2k_dos.py diff --git a/Tests/check_j2k_leaks.py b/checks/check_j2k_leaks.py similarity index 100% rename from Tests/check_j2k_leaks.py rename to checks/check_j2k_leaks.py diff --git a/Tests/check_j2k_overflow.py b/checks/check_j2k_overflow.py similarity index 100% rename from Tests/check_j2k_overflow.py rename to checks/check_j2k_overflow.py diff --git a/Tests/check_jp2_overflow.py b/checks/check_jp2_overflow.py similarity index 100% rename from Tests/check_jp2_overflow.py rename to checks/check_jp2_overflow.py diff --git a/Tests/check_jpeg_leaks.py b/checks/check_jpeg_leaks.py similarity index 100% rename from Tests/check_jpeg_leaks.py rename to checks/check_jpeg_leaks.py diff --git a/Tests/check_large_memory.py b/checks/check_large_memory.py similarity index 100% rename from Tests/check_large_memory.py rename to checks/check_large_memory.py diff --git a/Tests/check_large_memory_numpy.py b/checks/check_large_memory_numpy.py similarity index 100% rename from Tests/check_large_memory_numpy.py rename to checks/check_large_memory_numpy.py diff --git a/Tests/check_libtiff_segfault.py b/checks/check_libtiff_segfault.py similarity index 100% rename from Tests/check_libtiff_segfault.py rename to checks/check_libtiff_segfault.py diff --git a/Tests/check_png_dos.py b/checks/check_png_dos.py similarity index 100% rename from Tests/check_png_dos.py rename to checks/check_png_dos.py diff --git a/Tests/check_release_notes.py b/checks/check_release_notes.py similarity index 100% rename from Tests/check_release_notes.py rename to checks/check_release_notes.py diff --git a/Tests/check_wheel.py b/checks/check_wheel.py similarity index 75% rename from Tests/check_wheel.py rename to checks/check_wheel.py index a78fb09b041..c89d32ed7aa 100644 --- a/Tests/check_wheel.py +++ b/checks/check_wheel.py @@ -4,8 +4,7 @@ import sys from PIL import features - -from .helper import is_pypy +from Tests.helper import is_pypy def test_wheel_modules() -> None: @@ -24,6 +23,11 @@ def test_wheel_modules() -> None: if platform.machine() == "ARM64": expected_modules.remove("avif") + elif sys.platform == "ios": + # tkinter is not available on iOS + # libavif is not available on iOS (for now) + expected_modules -= {"tkinter", "avif"} + assert set(features.get_supported_modules()) == expected_modules @@ -50,5 +54,9 @@ def test_wheel_features() -> None: 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": + # 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. + expected_features -= {"raqm", "fribidi", "harfbuzz"} assert set(features.get_supported_features()) == expected_features diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index 2d35d8228fc..4af1f68ede0 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -68,7 +68,13 @@ AVIF support in wheels ^^^^^^^^^^^^^^^^^^^^^^ Support for reading and writing AVIF images is now included in Pillow's wheels, except -for Windows ARM64. libaom is available as an encoder and dav1d as a decoder. +for Windows ARM64 and iOS. libaom is available as an encoder and dav1d as a decoder. + +iOS +^^^ + +Pillow now provides wheels that can be used on iOS ARM64 devices, and on the iOS +simulator on ARM64 and x86_64. Currently, only Python 3.13 wheels are available. Python 3.14 beta ^^^^^^^^^^^^^^^^ diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 00000000000..ff4a8f0994f --- /dev/null +++ b/patches/README.md @@ -0,0 +1,14 @@ +Although we try to use official sources for dependencies, sometimes the official +sources don't support a platform (especially mobile platforms), or there's a bug +fix/feature that is required to support Pillow's usage. + +This folder contains patches that must be applied to official sources, organized +by the platforms that need those patches. + +Each patch is against the root of the unpacked official tarball, and is named by +appending `.patch` to the end of the tarball that is to be patched. This +includes the full version number; so if the version is bumped, the patch will +at a minimum require a filename change. + +Wherever possible, these patches should be contributed upstream, in the hope that +future Pillow versions won't need to maintain these patches. diff --git a/patches/iOS/brotli-1.1.0.tar.gz.patch b/patches/iOS/brotli-1.1.0.tar.gz.patch new file mode 100644 index 00000000000..f165a9ac12f --- /dev/null +++ b/patches/iOS/brotli-1.1.0.tar.gz.patch @@ -0,0 +1,46 @@ +# Brotli 1.1.0 doesn't have explicit support for iOS as a CMAKE_SYSTEM_NAME. +# That release was from 2023; there have been subsequent changes that allow +# Brotli to build on iOS without any patches, as long as -DBROTLI_BUILD_TOOLS=NO +# is specified on the command line. +# +diff -ru brotli-1.1.0-orig/CMakeLists.txt brotli-1.1.0/CMakeLists.txt +--- brotli-1.1.0-orig/CMakeLists.txt 2023-08-29 19:00:29 ++++ brotli-1.1.0/CMakeLists.txt 2024-11-07 10:46:26 +@@ -114,6 +114,8 @@ + add_definitions(-DOS_MACOSX) + set(CMAKE_MACOS_RPATH TRUE) + set(CMAKE_INSTALL_NAME_DIR "${CMAKE_INSTALL_PREFIX}/lib") ++elseif(${CMAKE_SYSTEM_NAME} MATCHES "iOS") ++ add_definitions(-DOS_IOS) + endif() + + if(BROTLI_EMSCRIPTEN) +@@ -174,10 +176,12 @@ + + # Installation + if(NOT BROTLI_BUNDLED_MODE) +- install( +- TARGETS brotli +- RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" +- ) ++ if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "iOS") ++ install( ++ TARGETS brotli ++ RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" ++ ) ++ endif() + + install( + TARGETS ${BROTLI_LIBRARIES_CORE} +diff -ru brotli-1.1.0-orig/c/common/platform.h brotli-1.1.0/c/common/platform.h +--- brotli-1.1.0-orig/c/common/platform.h 2023-08-29 19:00:29 ++++ brotli-1.1.0/c/common/platform.h 2024-11-07 10:47:28 +@@ -33,7 +33,7 @@ + #include + #elif defined(OS_FREEBSD) + #include +-#elif defined(OS_MACOSX) ++#elif defined(OS_MACOSX) || defined(OS_IOS) + #include + /* Let's try and follow the Linux convention */ + #define BROTLI_X_BYTE_ORDER BYTE_ORDER diff --git a/patches/iOS/libwebp-1.5.0.tar.gz.patch b/patches/iOS/libwebp-1.5.0.tar.gz.patch new file mode 100644 index 00000000000..fefb72b68d2 --- /dev/null +++ b/patches/iOS/libwebp-1.5.0.tar.gz.patch @@ -0,0 +1,42 @@ +# libwebp example binaries require dependencies that aren't available for iOS builds. +# There's also no easy way to invoke the build to *exclude* the example builds. +# Since we don't need the examples anyway, remove them from the Makefile. +# +# As a point of reference, libwebp provides an XCFramework build script that involves +# 7 separate invocations of make to avoid building the examples. Patching the Makefile +# to remove the examples is a simpler approach, and one that is more compatible with +# the existing multibuild infrastructure. +# +# In the next release, it should be possible to pass --disable-libwebpexamples +# instead of applying this patch. +# +diff -ur libwebp-1.5.0-orig/Makefile.am libwebp-1.5.0/Makefile.am +--- libwebp-1.5.0-orig/Makefile.am 2024-12-20 09:17:50 ++++ libwebp-1.5.0/Makefile.am 2025-01-09 11:24:17 +@@ -5,5 +5,3 @@ + if BUILD_EXTRAS + SUBDIRS += extras + endif +- +-SUBDIRS += examples +diff -ur libwebp-1.5.0-orig/Makefile.in libwebp-1.5.0/Makefile.in +--- libwebp-1.5.0-orig/Makefile.in 2024-12-20 09:52:53 ++++ libwebp-1.5.0/Makefile.in 2025-01-09 11:24:17 +@@ -156,7 +156,7 @@ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | $(am__uniquify_input)` +-DIST_SUBDIRS = sharpyuv src imageio man extras examples ++DIST_SUBDIRS = sharpyuv src imageio man extras + am__DIST_COMMON = $(srcdir)/Makefile.in \ + $(top_srcdir)/src/webp/config.h.in AUTHORS COPYING ChangeLog \ + NEWS README.md ar-lib compile config.guess config.sub \ +@@ -351,7 +351,7 @@ + top_srcdir = @top_srcdir@ + webp_libname_prefix = @webp_libname_prefix@ + ACLOCAL_AMFLAGS = -I m4 +-SUBDIRS = sharpyuv src imageio man $(am__append_1) examples ++SUBDIRS = sharpyuv src imageio man $(am__append_1) + EXTRA_DIST = COPYING autogen.sh + all: all-recursive + diff --git a/pyproject.toml b/pyproject.toml index 683ab24ef07..582d742b4d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,15 +103,55 @@ before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" -# Disable platform guessing on macOS -macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" +test-requires = [ + "numpy", +] +xbuild-tools = [ ] + +[tool.cibuildwheel.macos] +# Disable platform guessing on macOS to avoid picking up Homebrew etc. +config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" [tool.cibuildwheel.macos.environment] +# Isolate macOS build environment from Homebrew etc. PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +[tool.cibuildwheel.ios] +# Disable platform guessing on iOS, and disable raqm (since there won't be a +# vendor version, and we can't distribute it due to licensing) +config-settings = "raqm=disable imagequant=disable platform-guessing=disable" + +# iOS needs to be given a specific pytest invocation and list of test sources. +test-sources = [ + "checks", + "Tests", + "selftest.py", +] +test-command = [ + "python -m selftest", + "python -m pytest -vv -x -W always checks/check_wheel.py Tests", +] + +# There's no numpy wheel for iOS (yet...) +test-requires = [ ] + +[[tool.cibuildwheel.overrides]] +# iOS environment is isolated by cibuildwheel, but needs the dependencies +select = "*_iphoneos" +environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH" + +[[tool.cibuildwheel.overrides]] +# iOS simulator environment is isolated by cibuildwheel, but needs the dependencies +select = "*_iphonesimulator" +environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH" + +[[tool.cibuildwheel.overrides]] +select = "*-win32" +test-requires = [ ] + [tool.black] exclude = "wheels/multibuild" @@ -168,7 +208,7 @@ lint.isort.required-imports = [ max_supported_python = "3.13" [tool.pytest.ini_options] -addopts = "-ra --color=yes" +addopts = "-ra --color=auto" testpaths = [ "Tests", ] diff --git a/setup.py b/setup.py index 354e09f85f2..477d187a278 100644 --- a/setup.py +++ b/setup.py @@ -473,6 +473,19 @@ def get_macos_sdk_path(self) -> str | None: sdk_path = commandlinetools_sdk_path return sdk_path + def get_ios_sdk_path(self) -> str: + try: + sdk = sys.implementation._multiarch.split("-")[-1] + _dbg("Using %s SDK", sdk) + return ( + subprocess.check_output(["xcrun", "--show-sdk-path", "--sdk", sdk]) + .strip() + .decode("latin1") + ) + except Exception: + msg = "Unable to identify location of iOS SDK." + raise ValueError(msg) + def build_extensions(self) -> None: library_dirs: list[str] = [] include_dirs: list[str] = [] @@ -622,6 +635,18 @@ def build_extensions(self) -> None: for extension in self.extensions: extension.extra_compile_args = ["-Wno-nullability-completeness"] + + elif sys.platform == "ios": + # Add the iOS SDK path. + sdk_path = self.get_ios_sdk_path() + + # Add the iOS SDK path. + _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) + _add_directory(include_dirs, os.path.join(sdk_path, "usr", "include")) + + for extension in self.extensions: + extension.extra_compile_args = ["-Wno-nullability-completeness"] + elif sys.platform.startswith(("linux", "gnu", "freebsd")): for dirname in _find_library_dirs_ldconfig(): _add_directory(library_dirs, dirname) @@ -877,6 +902,9 @@ def build_extensions(self) -> None: # so we have to guess; by default it is defined in all Windows builds. # See #4237, #5243, #5359 for more information. defs.append(("USE_WIN32_FILEIO", None)) + elif sys.platform == "ios": + # Ensure transitive dependencies are linked. + libs.append("lzma") if feature.get("jpeg"): libs.append(feature.get("jpeg")) defs.append(("HAVE_LIBJPEG", None)) @@ -893,6 +921,9 @@ def build_extensions(self) -> None: defs.append(("HAVE_LIBIMAGEQUANT", None)) if feature.get("xcb"): libs.append(feature.get("xcb")) + if sys.platform == "ios": + # Ensure transitive dependencies are linked. + libs.append("Xau") defs.append(("HAVE_XCB", None)) if sys.platform == "win32": libs.extend(["kernel32", "user32", "gdi32"]) @@ -924,6 +955,11 @@ def build_extensions(self) -> None: libs.append(feature.get("fribidi")) else: # building FriBiDi shim from src/thirdparty srcs.append("src/thirdparty/fribidi-shim/fribidi.c") + + if sys.platform == "ios": + # Ensure transitive dependencies are linked. + libs.extend(["z", "bz2", "brotlicommon", "brotlidec", "png"]) + self._update_extension("PIL._imagingft", libs, defs, srcs) else: @@ -940,6 +976,9 @@ def build_extensions(self) -> None: webp = feature.get("webp") if isinstance(webp, str): libs = [webp, webp + "mux", webp + "demux"] + if sys.platform == "ios": + # Ensure transitive dependencies are linked. + libs.append("sharpyuv") self._update_extension("PIL._webp", libs) else: self._remove_extension("PIL._webp") From 49efe40f28ce0413ced31d8dcc0c707b0134d6bf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Jun 2025 22:19:14 +1000 Subject: [PATCH 1827/2374] Escape period --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d5fd964f128..c5276723a2b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: end-of-file-fixer exclude: ^Tests/images/|\.patch$ - id: trailing-whitespace - exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$ + exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$ - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.33.1 From 204d11d4da15879946c1120c43e6f75b2a338d5b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Jun 2025 22:29:24 +1000 Subject: [PATCH 1828/2374] Raise FileNotFoundError when opening an empty path --- Tests/test_image.py | 4 ++++ src/PIL/Image.py | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 069083b1963..6b8b6d42b07 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -160,6 +160,10 @@ def test_set_mode(self) -> None: with pytest.raises(AttributeError): im.mode = "P" # type: ignore[misc] + def test_empty_path(self) -> None: + with pytest.raises(FileNotFoundError): + Image.open("") + def test_invalid_image(self) -> None: im = io.BytesIO(b"") with pytest.raises(UnidentifiedImageError): diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9fc1c70671b..d209405c4c5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3510,8 +3510,6 @@ def open( filename: str | bytes = "" if is_path(fp): filename = os.fspath(fp) - - if filename: fp = builtins.open(filename, "rb") exclusive_fp = True else: From f2de251c769ed76acfe94b54cc87c2aee77bdadf Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:17:56 +1000 Subject: [PATCH 1829/2374] Updated check script paths (#9052) --- .coveragerc | 3 +-- Makefile | 2 +- checks/check_jpeg_leaks.py | 2 +- codecov.yml | 3 +-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.coveragerc b/.coveragerc index a94a2567854..1f474015e30 100644 --- a/.coveragerc +++ b/.coveragerc @@ -18,6 +18,5 @@ exclude_also = [run] omit = - Tests/32bit_segfault_check.py - Tests/check_*.py + checks/*.py Tests/createfontdatachunk.py diff --git a/Makefile b/Makefile index ce780acbc00..6e050c715d2 100644 --- a/Makefile +++ b/Makefile @@ -75,7 +75,7 @@ debug: .PHONY: release-test release-test: - python3 Tests/check_release_notes.py + python3 checks/check_release_notes.py python3 -m pip install -e .[tests] python3 selftest.py python3 -m pytest Tests diff --git a/checks/check_jpeg_leaks.py b/checks/check_jpeg_leaks.py index 5f290c6cd37..2f42ad734a8 100644 --- a/checks/check_jpeg_leaks.py +++ b/checks/check_jpeg_leaks.py @@ -13,7 +13,7 @@ When run on a system without the jpeg leak fixes, the valgrind runs look like this. -valgrind --tool=massif python test-installed.py -s -v Tests/check_jpeg_leaks.py +valgrind --tool=massif python test-installed.py -s -v checks/check_jpeg_leaks.py """ diff --git a/codecov.yml b/codecov.yml index 84920238ffc..c29b4bc9015 100644 --- a/codecov.yml +++ b/codecov.yml @@ -16,6 +16,5 @@ coverage: # Matches 'omit:' in .coveragerc ignore: - - "Tests/32bit_segfault_check.py" - - "Tests/check_*.py" + - "checks/*.py" - "Tests/createfontdatachunk.py" From 89f1f4626a2aaf5f3d5ca6437f41def2998fbe09 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 17:41:24 +1000 Subject: [PATCH 1830/2374] 11.3.0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index ac678c7d26e..74e63356c95 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "11.3.0.dev0" +__version__ = "11.3.0" From 37cd041e5e9041b0708551c02655328e8761226d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 19:25:23 +1000 Subject: [PATCH 1831/2374] 12.0.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 74e63356c95..6a3c01f2607 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "11.3.0" +__version__ = "12.0.0.dev0" From 0cd2d3b24b32d458ad47f885314e20faaab548a1 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Tue, 1 Jul 2025 09:10:20 -0400 Subject: [PATCH 1832/2374] Setup nit: "fork" should be lowercased --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 582d742b4d3..abab61e6b2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ backend-path = [ [project] name = "pillow" -description = "Python Imaging Library (Fork)" +description = "Python Imaging Library (fork)" readme = "README.md" keywords = [ "Imaging", From d4ef93150f055c342baccd01e079eb70a9e7f189 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Tue, 1 Jul 2025 09:25:32 -0400 Subject: [PATCH 1833/2374] Thanks, folks! As a general rule I think we should acknowledge when significant contribtions come from outside the core team. We know the core team does a lot of work (thank you!) but it's not always obvious when significant contributions come from outside the core team. In the old change log, we had ACKs via `[radarhere]` syntax which I miss. I don't expect we'll start using the old change log again but maybe we can make a note in the release notes to include such ACKs as needed and appropriate. --- docs/releasenotes/11.3.0.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index 4af1f68ede0..409d50295dd 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -69,12 +69,14 @@ AVIF support in wheels Support for reading and writing AVIF images is now included in Pillow's wheels, except for Windows ARM64 and iOS. libaom is available as an encoder and dav1d as a decoder. +(Thank you Frankie Dintino and Andrew Murray!) iOS ^^^ Pillow now provides wheels that can be used on iOS ARM64 devices, and on the iOS simulator on ARM64 and x86_64. Currently, only Python 3.13 wheels are available. +(Thank you Russell Keith-Magee and Andrew Murray!) Python 3.14 beta ^^^^^^^^^^^^^^^^ From 583f0a50d50107bf7857548efa5c990d1a7cb362 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 23:57:46 +1000 Subject: [PATCH 1834/2374] Removed BGR;15, BGR;16 and BGR;24 modes --- Tests/helper.py | 18 +++---- Tests/test_image.py | 32 +++-------- Tests/test_image_access.py | 16 +----- Tests/test_image_putdata.py | 10 ---- Tests/test_image_resize.py | 2 +- Tests/test_lib_pack.py | 12 ----- docs/deprecations.rst | 15 +++--- docs/reference/arrow_support.rst | 4 +- src/PIL/Image.py | 11 +--- src/PIL/ImageMode.py | 7 --- src/_imaging.c | 91 ++++++++------------------------ src/libImaging/Access.c | 38 ------------- src/libImaging/Convert.c | 35 ------------ src/libImaging/Pack.c | 9 ---- src/libImaging/Storage.c | 30 ----------- src/libImaging/Unpack.c | 10 ---- 16 files changed, 50 insertions(+), 290 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index e71b4665b28..34e4d6e75fb 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -271,17 +271,13 @@ def _cached_hopper(mode: str) -> Image.Image: im = hopper("L") else: im = hopper() - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning, match="BGR;"): - im = im.convert(mode) - else: - try: - im = im.convert(mode) - except ImportError: - if mode == "LAB": - im = Image.open("Tests/images/hopper.Lab.tif") - else: - raise + try: + im = im.convert(mode) + except ImportError: + if mode == "LAB": + im = Image.open("Tests/images/hopper.Lab.tif") + else: + raise return im diff --git a/Tests/test_image.py b/Tests/test_image.py index 6b8b6d42b07..1aa810e22ac 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -30,7 +30,6 @@ assert_image_similar_tofile, assert_not_all_same, hopper, - is_big_endian, is_win32, mark_if_feature_version, skip_unless_feature, @@ -50,19 +49,10 @@ PrettyPrinter = None -# Deprecation helper -def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning, match="BGR;"): - return Image.new(mode, size) - else: - return Image.new(mode, size) - - class TestImage: - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + @pytest.mark.parametrize("mode", Image.MODES) def test_image_modes_success(self, mode: str) -> None: - helper_image_new(mode, (1, 1)) + Image.new(mode, (1, 1)) @pytest.mark.parametrize("mode", ("", "bad", "very very long")) def test_image_modes_fail(self, mode: str) -> None: @@ -1148,33 +1138,27 @@ def test_deprecation(self) -> None: class TestImageBytes: - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + @pytest.mark.parametrize("mode", Image.MODES) def test_roundtrip_bytes_constructor(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning, match=mode): - reloaded = Image.frombytes(mode, im.size, source_bytes) - else: - reloaded = Image.frombytes(mode, im.size, source_bytes) + reloaded = Image.frombytes(mode, im.size, source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + @pytest.mark.parametrize("mode", Image.MODES) def test_roundtrip_bytes_method(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() - reloaded = helper_image_new(mode, im.size) + reloaded = Image.new(mode, im.size) reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + @pytest.mark.parametrize("mode", Image.MODES) def test_getdata_putdata(self, mode: str) -> None: - if is_big_endian() and mode == "BGR;15": - pytest.xfail("Known failure of BGR;15 on big-endian") im = hopper(mode) - reloaded = helper_image_new(mode, im.size) + reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 66412a03582..b3de5c13d3e 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -123,10 +123,6 @@ def color(mode: str) -> int | tuple[int, ...]: bands = Image.getmodebands(mode) if bands == 1: return 1 - if mode in ("BGR;15", "BGR;16"): - # These modes have less than 8 bits per band, - # so (1, 2, 3) cannot be roundtripped. - return (16, 32, 49) return tuple(range(1, bands + 1)) def check(self, mode: str, expected_color_int: int | None = None) -> None: @@ -191,11 +187,6 @@ def check(self, mode: str, expected_color_int: int | None = None) -> None: def test_basic(self, mode: str) -> None: self.check(mode) - @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) - def test_deprecated(self, mode: str) -> None: - with pytest.warns(DeprecationWarning, match="BGR;"): - self.check(mode) - def test_list(self) -> None: im = hopper() assert im.getpixel([0, 0]) == (20, 20, 70) @@ -218,7 +209,7 @@ def test_p_putpixel_rgb_rgba(self, mode: str, color: tuple[int, ...]) -> None: class TestImagePutPixelError: - IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] + IMAGE_MODES1 = ["LA", "RGB", "RGBA"] IMAGE_MODES2 = ["L", "I", "I;16"] INVALID_TYPES = ["foo", 1.0, None] @@ -234,11 +225,6 @@ def test_putpixel_type_error1(self, mode: str) -> None: ( ("L", (0, 2), "color must be int or single-element tuple"), ("LA", (0, 3), "color must be int, or tuple of one or two elements"), - ( - "BGR;15", - (0, 2), - "color must be int, or tuple of one or three elements", - ), ( "RGB", (0, 2, 5), diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 34c1763b886..bf8e89b53ce 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -78,16 +78,6 @@ def test_mode_F() -> None: assert list(im.getdata()) == target -@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) -def test_mode_BGR(mode: str) -> None: - data = [(16, 32, 49), (32, 32, 98)] - with pytest.warns(DeprecationWarning, match=mode): - im = Image.new(mode, (1, 2)) - im.putdata(data) - - assert list(im.getdata()) == data - - def test_array_B() -> None: # shouldn't segfault # see https://github.com/python-pillow/Pillow/issues/1008 diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index f700d20c0c9..270500a44b4 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -324,7 +324,7 @@ def test_default_filter_bicubic(self, mode: str) -> None: im = hopper(mode) assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) - @pytest.mark.parametrize("mode", ("1", "P", "BGR;15", "BGR;16")) + @pytest.mark.parametrize("mode", ("1", "P")) def test_default_filter_nearest(self, mode: str) -> None: im = hopper(mode) assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 2d6af70eb78..da6157d4e56 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -361,18 +361,6 @@ def test_RGB(self) -> None: "RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233) ) - def test_BGR(self) -> None: - with pytest.warns(DeprecationWarning, match="BGR;15"): - self.assert_unpack( - "BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8) - ) - with pytest.warns(DeprecationWarning, match="BGR;16"): - self.assert_unpack( - "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) - ) - with pytest.warns(DeprecationWarning, match="BGR;24"): - self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - def test_RGBA(self) -> None: self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) self.assert_unpack( diff --git a/docs/deprecations.rst b/docs/deprecations.rst index def98b80afb..b4eb1fa4c51 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -78,13 +78,6 @@ ImageMath eval() ``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or :py:meth:`~PIL.ImageMath.unsafe_eval` instead. -BGR;15, BGR 16 and BGR;24 -^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 - -The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated. - Non-image modes in ImageCms ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -221,6 +214,14 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +BGR;15, BGR 16 and BGR;24 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 + +The experimental BGR;15, BGR;16 and BGR;24 modes have been removed. + TiffImagePlugin IFD_LEGACY_API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/reference/arrow_support.rst b/docs/reference/arrow_support.rst index 063046d8c22..8e8b86c8e4d 100644 --- a/docs/reference/arrow_support.rst +++ b/docs/reference/arrow_support.rst @@ -21,9 +21,7 @@ with any Arrow provider or consumer in the Python ecosystem. Data formats ============ -Pillow currently supports exporting Arrow images in all modes -**except** for ``BGR;15``, ``BGR;16`` and ``BGR;24``. This is due to -line-length packing in these modes making for non-continuous memory. +Pillow currently supports exporting Arrow images in all modes. For single-band images, the exported array is width*height elements, with each pixel corresponding to the appropriate Arrow type. diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d209405c4c5..9df253498ea 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -980,9 +980,6 @@ def convert( :returns: An :py:class:`~PIL.Image.Image` object. """ - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) - self.load() has_transparency = "transparency" in self.info @@ -2229,8 +2226,6 @@ def resize( :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. If the image has mode "1" or "P", it is always set to - :py:data:`Resampling.NEAREST`. If the image mode is "BGR;15", - "BGR;16" or "BGR;24", then the default filter is :py:data:`Resampling.NEAREST`. Otherwise, the default filter is :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`. :param box: An optional 4-tuple of floats providing @@ -2253,8 +2248,7 @@ def resize( """ if resample is None: - bgr = self.mode.startswith("BGR;") - resample = Resampling.NEAREST if bgr else Resampling.BICUBIC + resample = Resampling.BICUBIC elif resample not in ( Resampling.NEAREST, Resampling.BILINEAR, @@ -3085,9 +3079,6 @@ def new( :returns: An :py:class:`~PIL.Image.Image` object. """ - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) - _check_size(size) if color is None: diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 92a08d2cbcb..b7c6c863659 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -18,8 +18,6 @@ from functools import lru_cache from typing import NamedTuple -from ._deprecate import deprecate - class ModeDescriptor(NamedTuple): """Wrapper for mode strings.""" @@ -57,16 +55,11 @@ def getmode(mode: str) -> ModeDescriptor: "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), # extra experimental modes "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), - "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"), - "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), - "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), "LA": ("L", "L", ("L", "A"), "|u1"), "La": ("L", "L", ("L", "a"), "|u1"), "PA": ("RGB", "L", ("P", "A"), "|u1"), } if mode in modes: - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) base_mode, base_type, bands, type_str = modes[mode] return ModeDescriptor(mode, bands, base_mode, base_type, type_str) diff --git a/src/_imaging.c b/src/_imaging.c index 6f13834a9d4..7cc1fb1a4b9 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -681,30 +681,6 @@ getink(PyObject *color, Imaging im, char *ink) { } else if (!PyArg_ParseTuple(color, "iiL", &b, &g, &r)) { return NULL; } - if (!strcmp(im->mode, "BGR;15")) { - UINT16 v = ((((UINT16)r) << 7) & 0x7c00) + - ((((UINT16)g) << 2) & 0x03e0) + - ((((UINT16)b) >> 3) & 0x001f); - - ink[0] = (UINT8)v; - ink[1] = (UINT8)(v >> 8); - ink[2] = ink[3] = 0; - return ink; - } else if (!strcmp(im->mode, "BGR;16")) { - UINT16 v = ((((UINT16)r) << 8) & 0xf800) + - ((((UINT16)g) << 3) & 0x07e0) + - ((((UINT16)b) >> 3) & 0x001f); - ink[0] = (UINT8)v; - ink[1] = (UINT8)(v >> 8); - ink[2] = ink[3] = 0; - return ink; - } else if (!strcmp(im->mode, "BGR;24")) { - ink[0] = (UINT8)b; - ink[1] = (UINT8)g; - ink[2] = (UINT8)r; - ink[3] = 0; - return ink; - } } } @@ -1650,54 +1626,33 @@ _putdata(ImagingObject *self, PyObject *args) { return NULL; } double value; - if (image->bands == 1) { - int bigendian = 0; - if (image->type == IMAGING_TYPE_SPECIAL) { - // I;16* - if ( - strcmp(image->mode, "I;16B") == 0 + int bigendian = 0; + if (image->type == IMAGING_TYPE_SPECIAL) { + // I;16* + if ( + strcmp(image->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN - || strcmp(image->mode, "I;16N") == 0 + || strcmp(image->mode, "I;16N") == 0 #endif - ) { - bigendian = 1; - } + ) { + bigendian = 1; } - for (i = x = y = 0; i < n; i++) { - set_value_to_item(seq, i); - if (scale != 1.0 || offset != 0.0) { - value = value * scale + offset; - } - if (image->type == IMAGING_TYPE_SPECIAL) { - image->image8[y][x * 2 + (bigendian ? 1 : 0)] = - CLIP8((int)value % 256); - image->image8[y][x * 2 + (bigendian ? 0 : 1)] = - CLIP8((int)value >> 8); - } else { - image->image8[y][x] = (UINT8)CLIP8(value); - } - if (++x >= (int)image->xsize) { - x = 0, y++; - } + } + for (i = x = y = 0; i < n; i++) { + set_value_to_item(seq, i); + if (scale != 1.0 || offset != 0.0) { + value = value * scale + offset; } - } else { - // BGR;* - int b; - for (i = x = y = 0; i < n; i++) { - char ink[4]; - - op = PySequence_Fast_GET_ITEM(seq, i); - if (!op || !getink(op, image, ink)) { - Py_DECREF(seq); - return NULL; - } - /* FIXME: what about scale and offset? */ - for (b = 0; b < image->pixelsize; b++) { - image->image8[y][x * image->pixelsize + b] = ink[b]; - } - if (++x >= (int)image->xsize) { - x = 0, y++; - } + if (image->type == IMAGING_TYPE_SPECIAL) { + image->image8[y][x * 2 + (bigendian ? 1 : 0)] = + CLIP8((int)value % 256); + image->image8[y][x * 2 + (bigendian ? 0 : 1)] = + CLIP8((int)value >> 8); + } else { + image->image8[y][x] = (UINT8)CLIP8(value); + } + if (++x >= (int)image->xsize) { + x = 0, y++; } } PyErr_Clear(); /* Avoid weird exceptions */ diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 1c193710531..3db52377e80 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -82,31 +82,6 @@ get_pixel_16B(Imaging im, int x, int y, void *color) { #endif } -static void -get_pixel_BGR15(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; - UINT16 pixel = in[0] + (in[1] << 8); - char *out = color; - out[0] = (pixel & 31) * 255 / 31; - out[1] = ((pixel >> 5) & 31) * 255 / 31; - out[2] = ((pixel >> 10) & 31) * 255 / 31; -} - -static void -get_pixel_BGR16(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; - UINT16 pixel = in[0] + (in[1] << 8); - char *out = color; - out[0] = (pixel & 31) * 255 / 31; - out[1] = ((pixel >> 5) & 63) * 255 / 63; - out[2] = ((pixel >> 11) & 31) * 255 / 31; -} - -static void -get_pixel_BGR24(Imaging im, int x, int y, void *color) { - memcpy(color, &im->image8[y][x * 3], sizeof(UINT8) * 3); -} - static void get_pixel_32(Imaging im, int x, int y, void *color) { memcpy(color, &im->image32[y][x], sizeof(INT32)); @@ -154,16 +129,6 @@ put_pixel_16B(Imaging im, int x, int y, const void *color) { out[1] = in[0]; } -static void -put_pixel_BGR1516(Imaging im, int x, int y, const void *color) { - memcpy(&im->image8[y][x * 2], color, 2); -} - -static void -put_pixel_BGR24(Imaging im, int x, int y, const void *color) { - memcpy(&im->image8[y][x * 3], color, 3); -} - static void put_pixel_32L(Imaging im, int x, int y, const void *color) { memcpy(&im->image8[y][x * 4], color, 4); @@ -212,9 +177,6 @@ ImagingAccessInit(void) { ADD("F", get_pixel_32, put_pixel_32); ADD("P", get_pixel_8, put_pixel_8); ADD("PA", get_pixel_32_2bands, put_pixel_32); - ADD("BGR;15", get_pixel_BGR15, put_pixel_BGR1516); - ADD("BGR;16", get_pixel_BGR16, put_pixel_BGR1516); - ADD("BGR;24", get_pixel_BGR24, put_pixel_BGR24); ADD("RGB", get_pixel_32, put_pixel_32); ADD("RGBA", get_pixel_32, put_pixel_32); ADD("RGBa", get_pixel_32, put_pixel_32); diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index c8f23426105..9a2c9ff1667 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -277,38 +277,6 @@ rgb2f(UINT8 *out_, const UINT8 *in, int xsize) { } } -static void -rgb2bgr15(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4, out_ += 2) { - UINT16 v = ((((UINT16)in[0]) << 7) & 0x7c00) + - ((((UINT16)in[1]) << 2) & 0x03e0) + - ((((UINT16)in[2]) >> 3) & 0x001f); - memcpy(out_, &v, sizeof(v)); - } -} - -static void -rgb2bgr16(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4, out_ += 2) { - UINT16 v = ((((UINT16)in[0]) << 8) & 0xf800) + - ((((UINT16)in[1]) << 3) & 0x07e0) + - ((((UINT16)in[2]) >> 3) & 0x001f); - memcpy(out_, &v, sizeof(v)); - } -} - -static void -rgb2bgr24(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - *out++ = in[2]; - *out++ = in[1]; - *out++ = in[0]; - } -} - static void rgb2hsv_row(UINT8 *out, const UINT8 *in) { // following colorsys.py float h, s, rc, gc, bc, cr; @@ -971,9 +939,6 @@ static struct { {"RGB", "I;16N", rgb2i16l}, #endif {"RGB", "F", rgb2f}, - {"RGB", "BGR;15", rgb2bgr15}, - {"RGB", "BGR;16", rgb2bgr16}, - {"RGB", "BGR;24", rgb2bgr24}, {"RGB", "RGBA", rgb2rgba}, {"RGB", "RGBa", rgb2rgba}, {"RGB", "RGBX", rgb2rgba}, diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index c29473d90db..7f8a50d198e 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -471,12 +471,6 @@ copy2(UINT8 *out, const UINT8 *in, int pixels) { memcpy(out, in, pixels * 2); } -static void -copy3(UINT8 *out, const UINT8 *in, int pixels) { - /* BGR;24, etc */ - memcpy(out, in, pixels * 3); -} - static void copy4(UINT8 *out, const UINT8 *in, int pixels) { /* RGBA, CMYK quadruples */ @@ -657,9 +651,6 @@ static struct { {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. {"I;16L", "I;16N", 16, packI16N_I16}, {"I;16B", "I;16N", 16, packI16N_I16B}, - {"BGR;15", "BGR;15", 16, copy2}, - {"BGR;16", "BGR;16", 16, copy2}, - {"BGR;24", "BGR;24", 24, copy3}, {NULL} /* sentinel */ }; diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 6fe26e1bd1a..4640f078a62 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -151,36 +151,6 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { strcpy(im->band_names[2], "B"); strcpy(im->band_names[3], "X"); - } else if (strcmp(mode, "BGR;15") == 0) { - /* EXPERIMENTAL */ - /* 15-bit reversed true colour */ - im->bands = 3; - im->pixelsize = 2; - im->linesize = (xsize * 2 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - /* not allowing arrow due to line length packing */ - strcpy(im->arrow_band_format, ""); - - } else if (strcmp(mode, "BGR;16") == 0) { - /* EXPERIMENTAL */ - /* 16-bit reversed true colour */ - im->bands = 3; - im->pixelsize = 2; - im->linesize = (xsize * 2 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - /* not allowing arrow due to line length packing */ - strcpy(im->arrow_band_format, ""); - - } else if (strcmp(mode, "BGR;24") == 0) { - /* EXPERIMENTAL */ - /* 24-bit reversed true colour */ - im->bands = 3; - im->pixelsize = 3; - im->linesize = (xsize * 3 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - /* not allowing arrow due to line length packing */ - strcpy(im->arrow_band_format, ""); - } else if (strcmp(mode, "RGBX") == 0) { /* 32-bit true colour images with padding */ im->bands = im->pixelsize = 4; diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 9c3ee26655f..976baa72673 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1284,12 +1284,6 @@ copy2(UINT8 *out, const UINT8 *in, int pixels) { memcpy(out, in, pixels * 2); } -static void -copy3(UINT8 *out, const UINT8 *in, int pixels) { - /* BGR;24 */ - memcpy(out, in, pixels * 3); -} - static void copy4(UINT8 *out, const UINT8 *in, int pixels) { /* RGBA, CMYK quadruples */ @@ -1649,10 +1643,6 @@ static struct { {"RGB", "B;16B", 16, band216B}, {"RGB", "CMYK", 32, cmyk2rgb}, - {"BGR;15", "BGR;15", 16, copy2}, - {"BGR;16", "BGR;16", 16, copy2}, - {"BGR;24", "BGR;24", 24, copy3}, - /* true colour w. alpha */ {"RGBA", "LA", 16, unpackRGBALA}, {"RGBA", "LA;16B", 32, unpackRGBALA16B}, From 5d4a05465d064147951178e140520d704b1092f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 20:06:00 +1000 Subject: [PATCH 1835/2374] Removed Image isImageType() --- Tests/test_image.py | 4 ---- docs/deprecations.rst | 17 +++++++++-------- src/PIL/Image.py | 17 +---------------- 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 1aa810e22ac..83b027aa238 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1132,10 +1132,6 @@ def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None: assert len(caplog.records) == 0 assert im.fp is None - def test_deprecation(self) -> None: - with pytest.warns(DeprecationWarning, match="Image.isImageType"): - assert not Image.isImageType(None) - class TestImageBytes: @pytest.mark.parametrize("mode", Image.MODES) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index b4eb1fa4c51..5ecd7de4240 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -123,14 +123,6 @@ ICNS (width, height, scale) sizes Setting an ICNS image size to ``(width, height, scale)`` before loading has been deprecated. Instead, ``load(scale)`` can be used. -Image isImageType() -^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 11.0.0 - -``Image.isImageType(im)`` has been deprecated. Use ``isinstance(im, Image.Image)`` -instead. - ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -222,6 +214,15 @@ BGR;15, BGR 16 and BGR;24 The experimental BGR;15, BGR;16 and BGR;24 modes have been removed. +Image isImageType() +^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +``Image.isImageType(im)`` has been removed. Use ``isinstance(im, Image.Image)`` +instead. + TiffImagePlugin IFD_LEGACY_API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9df253498ea..59168f5e3d1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -115,21 +115,6 @@ class DecompressionBombError(Exception): raise -def isImageType(t: Any) -> TypeGuard[Image]: - """ - Checks if an object is an image object. - - .. warning:: - - This function is for internal use only. - - :param t: object to check if it's an image - :returns: True if the object is an image - """ - deprecate("Image.isImageType(im)", 12, "isinstance(im, Image.Image)") - return hasattr(t, "im") - - # # Constants @@ -219,7 +204,7 @@ class Quantize(IntEnum): from IPython.lib.pretty import PrettyPrinter from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin - from ._typing import CapsuleType, NumpyArray, StrOrBytesPath, TypeGuard + from ._typing import CapsuleType, NumpyArray, StrOrBytesPath ID: list[str] = [] OPEN: dict[ str, From 1800e580d2310e5b7bb8e958d96c11691f5ce9df Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 20:07:31 +1000 Subject: [PATCH 1836/2374] Removed ImageFile raise_oserror() --- Tests/test_imagefile.py | 5 ----- docs/deprecations.rst | 20 ++++++++++---------- src/PIL/ImageFile.py | 11 ----------- 3 files changed, 10 insertions(+), 26 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index a9444c26d5b..d4dfb1b6d59 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -151,11 +151,6 @@ def read(self, size: int | None = None) -> bytes: # Despite multiple tiles, assert only one tile caused a read of maxblock size assert reads.count(im.decodermaxblock) == 1 - def test_raise_oserror(self) -> None: - with pytest.warns(DeprecationWarning, match="raise_oserror"): - with pytest.raises(OSError): - ImageFile.raise_oserror(1) - def test_raise_typeerror(self) -> None: with pytest.raises(TypeError): parser = ImageFile.Parser() diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 5ecd7de4240..4a208f212db 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,16 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a :py:exc:`DeprecationWarning` is issued. -ImageFile.raise_oserror -~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 10.2.0 - -``ImageFile.raise_oserror()`` has been deprecated and will be removed in Pillow -12.0.0 (2025-10-15). The function is undocumented and is only useful for translating -error codes returned by a codec's ``decode()`` method, which ImageFile already does -automatically. - IptcImageFile helper functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -206,6 +196,16 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +ImageFile.raise_oserror +~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.2.0 +.. versionremoved:: 12.0.0 + +``ImageFile.raise_oserror()`` has been removed. The function was undocumented and was +only useful for translating error codes returned by a codec's ``decode()`` method, +which ImageFile already did automatically. + BGR;15, BGR 16 and BGR;24 ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index bf556a2c690..27b27127e79 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -37,7 +37,6 @@ from typing import IO, Any, NamedTuple, cast from . import ExifTags, Image -from ._deprecate import deprecate from ._util import DeferredError, is_path TYPE_CHECKING = False @@ -83,16 +82,6 @@ def _get_oserror(error: int, *, encoder: bool) -> OSError: return OSError(msg) -def raise_oserror(error: int) -> OSError: - deprecate( - "raise_oserror", - 12, - action="It is only useful for translating error codes returned by a codec's " - "decode() method, which ImageFile already does automatically.", - ) - raise _get_oserror(error, encoder=False) - - def _tilesort(t: _Tile) -> int: # sort on offset return t[2] From b72b8dd84d121c919c645963a8675b82c5585dfd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 20:04:08 +1000 Subject: [PATCH 1837/2374] Removed JpegImageFile.huffman_ac and JpegImageFile.huffman_dc --- Tests/test_file_jpeg.py | 8 -------- docs/deprecations.rst | 17 +++++++++-------- src/PIL/JpegImagePlugin.py | 7 ------- 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 5afae041287..08e8798079d 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1115,14 +1115,6 @@ def test_repr_jpeg_error_returns_none(self) -> None: assert im._repr_jpeg_() is None - def test_deprecation(self) -> None: - with Image.open(TEST_FILE) as im: - assert isinstance(im, JpegImagePlugin.JpegImageFile) - with pytest.warns(DeprecationWarning, match="huffman_ac"): - assert im.huffman_ac == {} - with pytest.warns(DeprecationWarning, match="huffman_dc"): - assert im.huffman_dc == {} - @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4a208f212db..8e065fa8711 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -122,14 +122,6 @@ The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword arguments can be used instead. -JpegImageFile.huffman_ac and JpegImageFile.huffman_dc -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 11.0.0 - -The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They -have been deprecated, and will be removed in Pillow 12 (2025-10-15). - Specific WebP feature checks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -223,6 +215,15 @@ Image isImageType() ``Image.isImageType(im)`` has been removed. Use ``isinstance(im, Image.Image)`` instead. +JpegImageFile.huffman_ac and JpegImageFile.huffman_dc +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They +have been deprecated, and will be removed in Pillow 12 (2025-10-15). + TiffImagePlugin IFD_LEGACY_API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index defe9f773f9..082f3551a17 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -49,7 +49,6 @@ from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 -from ._deprecate import deprecate from .JpegPresets import presets TYPE_CHECKING = False @@ -393,12 +392,6 @@ def _open(self) -> None: self._read_dpi_from_exif() - def __getattr__(self, name: str) -> Any: - if name in ("huffman_ac", "huffman_dc"): - deprecate(name, 12) - return getattr(self, "_" + name) - raise AttributeError(name) - def __getstate__(self) -> list[Any]: return super().__getstate__() + [self.layers, self.layer] From cce39084f5f0116e9c149b359223d72e3cbe9f24 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 20:09:06 +1000 Subject: [PATCH 1838/2374] Removed specific WebP feature checks --- Tests/test_features.py | 15 --------------- checks/check_wheel.py | 3 --- docs/deprecations.rst | 23 +++++++++++------------ docs/reference/features.rst | 3 --- src/PIL/features.py | 3 --- 5 files changed, 11 insertions(+), 36 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index d06fb4d841c..520c25b4645 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -55,21 +55,6 @@ def test(name: str, function: Callable[[str], str | None]) -> None: test(feature, features.version_feature) -def test_webp_transparency() -> None: - with pytest.warns(DeprecationWarning, match="transp_webp"): - assert (features.check("transp_webp") or False) == features.check_module("webp") - - -def test_webp_mux() -> None: - with pytest.warns(DeprecationWarning, match="webp_mux"): - assert (features.check("webp_mux") or False) == features.check_module("webp") - - -def test_webp_anim() -> None: - with pytest.warns(DeprecationWarning, match="webp_anim"): - assert (features.check("webp_anim") or False) == features.check_module("webp") - - @skip_unless_feature("libjpeg_turbo") def test_libjpeg_turbo_version() -> None: version = features.version("libjpeg_turbo") diff --git a/checks/check_wheel.py b/checks/check_wheel.py index c89d32ed7aa..3d806eb71e2 100644 --- a/checks/check_wheel.py +++ b/checks/check_wheel.py @@ -39,9 +39,6 @@ def test_wheel_codecs() -> None: def test_wheel_features() -> None: expected_features = { - "webp_anim", - "webp_mux", - "transp_webp", "raqm", "fribidi", "harfbuzz", diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 8e065fa8711..78c6f1092ef 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -122,16 +122,6 @@ The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword arguments can be used instead. -Specific WebP feature checks -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 11.0.0 - -``features.check("transp_webp")``, ``features.check("webp_mux")`` and -``features.check("webp_anim")`` are now deprecated. They will always return -``True`` if the WebP module is installed, until they are removed in Pillow -12.0.0 (2025-10-15). - Get internal pointers to objects ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -222,14 +212,23 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc .. versionremoved:: 12.0.0 The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They -have been deprecated, and will be removed in Pillow 12 (2025-10-15). +have been removed. + +Specific WebP feature checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +``features.check("transp_webp")``, ``features.check("webp_mux")`` and +``features.check("webp_anim")`` have been removed. TiffImagePlugin IFD_LEGACY_API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. versionremoved:: 11.0.0 -``TiffImagePlugin.IFD_LEGACY_API`` was removed, as it was an unused setting. +``TiffImagePlugin.IFD_LEGACY_API`` has been removed, as it was an unused setting. PSFile ~~~~~~ diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 381d7830aac..45067ba3587 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -60,9 +60,6 @@ Support for the following features can be checked: * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. -* ``transp_webp``: Deprecated. Always ``True`` if WebP module is installed. -* ``webp_mux``: Deprecated. Always ``True`` if WebP module is installed. -* ``webp_anim``: Deprecated. Always ``True`` if WebP module is installed. .. autofunction:: PIL.features.check_feature .. autofunction:: PIL.features.version_feature diff --git a/src/PIL/features.py b/src/PIL/features.py index 573f1d41256..984f7532c7c 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -121,9 +121,6 @@ def get_supported_codecs() -> list[str]: features: dict[str, tuple[str, str | bool, str | None]] = { - "webp_anim": ("PIL._webp", True, None), - "webp_mux": ("PIL._webp", True, None), - "transp_webp": ("PIL._webp", True, None), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), From 88018c1c2d42f4554da4733aeec3b06c3740dde8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 20:12:19 +1000 Subject: [PATCH 1839/2374] Removed id and unsafe_ptrs --- Tests/test_image_getim.py | 9 --------- docs/deprecations.rst | 21 +++++++++++---------- src/_imaging.c | 35 ----------------------------------- 3 files changed, 11 insertions(+), 54 deletions(-) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index 7b5f7a5890f..07612e5872e 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,7 +1,5 @@ from __future__ import annotations -import pytest - from .helper import hopper @@ -10,10 +8,3 @@ def test_sanity() -> None: type_repr = repr(type(im.getim())) assert "PyCapsule" in type_repr - - with pytest.warns(DeprecationWarning, match="id property"): - assert isinstance(im.im.id, int) - - with pytest.warns(DeprecationWarning, match="unsafe_ptrs property"): - ptrs = dict(im.im.unsafe_ptrs) - assert ptrs.keys() == {"image8", "image32", "image"} diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 78c6f1092ef..3225b6d5220 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -122,16 +122,6 @@ The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword arguments can be used instead. -Get internal pointers to objects -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 11.0.0 - -``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been -deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining -raw pointers to ``ImagingCore`` internals. To interact with C code, you can use -``Image.Image.getim()``, which returns a ``Capsule`` object. - ExifTags.IFD.Makernote ^^^^^^^^^^^^^^^^^^^^^^ @@ -223,6 +213,17 @@ Specific WebP feature checks ``features.check("transp_webp")``, ``features.check("webp_mux")`` and ``features.check("webp_anim")`` have been removed. +Get internal pointers to objects +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been +removed. They were used for obtaining raw pointers to ``ImagingCore`` internals. To +interact with C code, you can use ``Image.Image.getim()``, which returns a ``Capsule`` +object. + TiffImagePlugin IFD_LEGACY_API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_imaging.c b/src/_imaging.c index 7cc1fb1a4b9..8ba2a290856 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3724,18 +3724,6 @@ _getattr_bands(ImagingObject *self, void *closure) { return PyLong_FromLong(self->image->bands); } -static PyObject * -_getattr_id(ImagingObject *self, void *closure) { - if (PyErr_WarnEx( - PyExc_DeprecationWarning, - "id property is deprecated and will be removed in Pillow 12 (2025-10-15)", - 1 - ) < 0) { - return NULL; - } - return PyLong_FromSsize_t((Py_ssize_t)self->image); -} - static void _ptr_destructor(PyObject *capsule) { PyObject *self = (PyObject *)PyCapsule_GetContext(capsule); @@ -3750,27 +3738,6 @@ _getattr_ptr(ImagingObject *self, void *closure) { return capsule; } -static PyObject * -_getattr_unsafe_ptrs(ImagingObject *self, void *closure) { - if (PyErr_WarnEx( - PyExc_DeprecationWarning, - "unsafe_ptrs property is deprecated and will be removed in Pillow 12 " - "(2025-10-15)", - 1 - ) < 0) { - return NULL; - } - return Py_BuildValue( - "(sn)(sn)(sn)", - "image8", - self->image->image8, - "image32", - self->image->image32, - "image", - self->image->image - ); -} - static PyObject * _getattr_readonly(ImagingObject *self, void *closure) { return PyLong_FromLong(self->image->read_only); @@ -3780,9 +3747,7 @@ static struct PyGetSetDef getsetters[] = { {"mode", (getter)_getattr_mode}, {"size", (getter)_getattr_size}, {"bands", (getter)_getattr_bands}, - {"id", (getter)_getattr_id}, {"ptr", (getter)_getattr_ptr}, - {"unsafe_ptrs", (getter)_getattr_unsafe_ptrs}, {"readonly", (getter)_getattr_readonly}, {NULL} }; From a7e00fba8bfd6c254682ebe3c25faddb8459655c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 20:14:26 +1000 Subject: [PATCH 1840/2374] Removed ImageDraw.getdraw hints parameter --- Tests/test_imagedraw.py | 5 ----- docs/deprecations.rst | 8 ++++++++ src/PIL/ImageDraw.py | 8 +------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 881f9c85dd2..e1dcbc52c61 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1732,8 +1732,3 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None: draw.rectangle(xy) with pytest.raises(ValueError): draw.rounded_rectangle(xy) - - -def test_getdraw() -> None: - with pytest.warns(DeprecationWarning, match="'hints' parameter"): - ImageDraw.getdraw(None, []) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 3225b6d5220..82530f93cab 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -168,6 +168,14 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +ImageDraw.getdraw hints parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 + +The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been removed. + ImageFile.raise_oserror ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 6cf1ee62659..e95fa91f8b3 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -38,7 +38,6 @@ from typing import Any, AnyStr, Callable, Union, cast from . import Image, ImageColor -from ._deprecate import deprecate from ._typing import Coords # experimental access to the outline API @@ -1009,16 +1008,11 @@ def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: return ImageDraw(im, mode) -def getdraw( - im: Image.Image | None = None, hints: list[str] | None = None -) -> tuple[ImageDraw2.Draw | None, ModuleType]: +def getdraw(im: Image.Image | None = None) -> tuple[ImageDraw2.Draw | None, ModuleType]: """ :param im: The image to draw in. - :param hints: An optional list of hints. Deprecated. :returns: A (drawing context, drawing resource factory) tuple. """ - if hints is not None: - deprecate("'hints' parameter", 12) from . import ImageDraw2 draw = ImageDraw2.Draw(im) if im is not None else None From 9c9449af346e6bfcbf2fa6573b7ec96a14ddc8c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Jul 2025 00:00:16 +1000 Subject: [PATCH 1841/2374] Removed support for LibTIFF < 4 --- Tests/test_file_libtiff.py | 31 +++---------------------------- docs/deprecations.rst | 17 +++++++++-------- src/PIL/TiffImagePlugin.py | 7 ------- src/_imaging.c | 10 ---------- src/libImaging/TiffDecode.c | 10 +--------- 5 files changed, 13 insertions(+), 62 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 1ec39eba588..c245a5a9bcb 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -256,19 +256,7 @@ def test_additional_metadata( im.save(out, tiffinfo=new_ifd) - @pytest.mark.parametrize( - "libtiff", - ( - pytest.param( - True, - marks=pytest.mark.skipif( - not getattr(Image.core, "libtiff_support_custom_tags", False), - reason="Custom tags not supported by older libtiff", - ), - ), - False, - ), - ) + @pytest.mark.parametrize("libtiff", (True, False)) def test_custom_metadata( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool ) -> None: @@ -724,8 +712,7 @@ def test_exif_ifd(self) -> None: with Image.open(out) as reloaded: assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) - if Image.core.libtiff_support_custom_tags: - assert reloaded.tag_v2[34665] == 125456 + assert reloaded.tag_v2[34665] == 125456 def test_crashing_metadata( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path @@ -777,19 +764,7 @@ def test_read_icc(self, monkeypatch: pytest.MonkeyPatch) -> None: assert icc_libtiff is not None assert icc == icc_libtiff - @pytest.mark.parametrize( - "libtiff", - ( - pytest.param( - True, - marks=pytest.mark.skipif( - not getattr(Image.core, "libtiff_support_custom_tags", False), - reason="Custom tags not supported by older libtiff", - ), - ), - False, - ), - ) + @pytest.mark.parametrize("libtiff", (True, False)) def test_write_icc( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool ) -> None: diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 82530f93cab..5973038e373 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -77,14 +77,6 @@ The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pill image modes has been deprecated. Defaulting to "L" or "1" if the mode cannot be mapped is also deprecated. -Support for LibTIFF earlier than 4 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 - -Support for LibTIFF earlier than version 4 has been deprecated. -Upgrade to a newer version of LibTIFF instead. - ImageDraw.getdraw hints parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -194,6 +186,15 @@ BGR;15, BGR 16 and BGR;24 The experimental BGR;15, BGR;16 and BGR;24 modes have been removed. +Support for LibTIFF earlier than 4 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 + +Support for LibTIFF earlier than version 4 has been removed. +Upgrade to a newer version of LibTIFF instead. + Image isImageType() ^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index daf20f2e899..c1850f084c0 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -56,7 +56,6 @@ from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 -from ._deprecate import deprecate from ._typing import StrOrBytesPath from ._util import DeferredError, is_path from .TiffTags import TYPES @@ -284,9 +283,6 @@ b"II\x2b\x00", # BigTIFF with little-endian byte order ] -if not getattr(Image.core, "libtiff_support_custom_tags", True): - deprecate("Support for LibTIFF earlier than version 4", 12) - def _accept(prefix: bytes) -> bool: return prefix.startswith(tuple(PREFIXES)) @@ -1934,9 +1930,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Custom items are supported for int, float, unicode, string and byte # values. Other types and tuples require a tagtype. if tag not in TiffTags.LIBTIFF_CORE: - if not getattr(Image.core, "libtiff_support_custom_tags", False): - continue - if tag in TiffTags.TAGS_V2_GROUPS: types[tag] = TiffTags.LONG8 elif tag in ifd.tagtype: diff --git a/src/_imaging.c b/src/_imaging.c index 8ba2a290856..fbfc0e41ae2 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4352,16 +4352,6 @@ setup_module(PyObject *m) { PyObject *v = PyUnicode_FromString(ImagingTiffVersion()); PyDict_SetItemString(d, "libtiff_version", v ? v : Py_None); Py_XDECREF(v); - - // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 - PyObject *support_custom_tags; -#if TIFFLIB_VERSION >= 20111221 && TIFFLIB_VERSION != 20120218 && \ - TIFFLIB_VERSION != 20120922 - support_custom_tags = Py_True; -#else - support_custom_tags = Py_False; -#endif - PyDict_SetItemString(d, "libtiff_support_custom_tags", support_custom_tags); } #endif diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index e289ce4056e..71516fd1b56 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -884,7 +884,6 @@ ImagingLibTiffMergeFieldInfo( // Refer to libtiff docs (http://www.simplesystems.org/libtiff/addingtags.html) TIFFSTATE *clientstate = (TIFFSTATE *)state->context; uint32_t n; - int status = 0; // custom fields added with ImagingLibTiffMergeFieldInfo are only used for // decoding, ignore readcount; @@ -907,14 +906,7 @@ ImagingLibTiffMergeFieldInfo( n = sizeof(info) / sizeof(info[0]); - // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 -#if TIFFLIB_VERSION >= 20111221 && TIFFLIB_VERSION != 20120218 && \ - TIFFLIB_VERSION != 20120922 - status = TIFFMergeFieldInfo(clientstate->tiff, info, n); -#else - TIFFMergeFieldInfo(clientstate->tiff, info, n); -#endif - return status; + return TIFFMergeFieldInfo(clientstate->tiff, info, n); } int From 0a29d6392afeaf6c7e8354dbfa67a1f9268028df Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 20:29:45 +1000 Subject: [PATCH 1842/2374] Removed IptcImageFile helper functions --- Tests/test_file_iptc.py | 37 +--------------------- docs/deprecations.rst | 64 +++++++++++++++++++------------------- src/PIL/IptcImagePlugin.py | 24 -------------- 3 files changed, 33 insertions(+), 92 deletions(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 5dca3da2164..3c4c892c8a0 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,9 +1,6 @@ from __future__ import annotations -import sys -from io import BytesIO, StringIO - -import pytest +from io import BytesIO from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags @@ -101,35 +98,3 @@ def test_getiptcinfo_tiff_none() -> None: # Assert assert iptc is None - - -def test_i() -> None: - # Arrange - c = b"a" - - # Act - with pytest.warns(DeprecationWarning, match="IptcImagePlugin.i"): - ret = IptcImagePlugin.i(c) - - # Assert - assert ret == 97 - - -def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: - # Arrange - c = b"abc" - # Temporarily redirect stdout - mystdout = StringIO() - monkeypatch.setattr(sys, "stdout", mystdout) - - # Act - with pytest.warns(DeprecationWarning, match="IptcImagePlugin.dump"): - IptcImagePlugin.dump(c) - - # Assert - assert mystdout.getvalue() == "61 62 63 \n" - - -def test_pad_deprecation() -> None: - with pytest.warns(DeprecationWarning, match="IptcImagePlugin.PAD"): - assert IptcImagePlugin.PAD == b"\0\0\0\0" diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 5973038e373..06767c20b1b 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,17 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a :py:exc:`DeprecationWarning` is issued. -IptcImageFile helper functions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 10.2.0 - -The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant -``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow -12.0.0 (2025-10-15). These are undocumented helper functions intended -for internal use, so there is no replacement. They can each be replaced -by a single line of code using builtin functions in Python. - ImageCms constants and versions() function ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -160,14 +149,6 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. -ImageDraw.getdraw hints parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 -.. versionremoved:: 12.0.0 - -The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been removed. - ImageFile.raise_oserror ~~~~~~~~~~~~~~~~~~~~~~~ @@ -178,22 +159,16 @@ ImageFile.raise_oserror only useful for translating error codes returned by a codec's ``decode()`` method, which ImageFile already did automatically. -BGR;15, BGR 16 and BGR;24 -^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 -.. versionremoved:: 12.0.0 - -The experimental BGR;15, BGR;16 and BGR;24 modes have been removed. - -Support for LibTIFF earlier than 4 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +IptcImageFile helper functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 10.4.0 +.. deprecated:: 10.2.0 .. versionremoved:: 12.0.0 -Support for LibTIFF earlier than version 4 has been removed. -Upgrade to a newer version of LibTIFF instead. +The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant +``IptcImageFile.PAD`` have been removed. These were undocumented helper functions +intended for internal use, so there is no replacement. They can each be replaced by a +single line of code using builtin functions in Python. Image isImageType() ^^^^^^^^^^^^^^^^^^^ @@ -233,6 +208,31 @@ removed. They were used for obtaining raw pointers to ``ImagingCore`` internals. interact with C code, you can use ``Image.Image.getim()``, which returns a ``Capsule`` object. +BGR;15, BGR 16 and BGR;24 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 + +The experimental BGR;15, BGR;16 and BGR;24 modes have been removed. + +Support for LibTIFF earlier than 4 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 + +Support for LibTIFF earlier than version 4 has been removed. +Upgrade to a newer version of LibTIFF instead. + +ImageDraw.getdraw hints parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 + +The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been removed. + TiffImagePlugin IFD_LEGACY_API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index fc024d668f2..b1fbb1bf139 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -16,26 +16,16 @@ # from __future__ import annotations -from collections.abc import Sequence from io import BytesIO from typing import cast from . import Image, ImageFile from ._binary import i16be as i16 from ._binary import i32be as i32 -from ._deprecate import deprecate COMPRESSION = {1: "raw", 5: "jpeg"} -def __getattr__(name: str) -> bytes: - if name == "PAD": - deprecate("IptcImagePlugin.PAD", 12) - return b"\0\0\0\0" - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - # # Helpers @@ -48,20 +38,6 @@ def _i8(c: int | bytes) -> int: return c if isinstance(c, int) else c[0] -def i(c: bytes) -> int: - """.. deprecated:: 10.2.0""" - deprecate("IptcImagePlugin.i", 12) - return _i(c) - - -def dump(c: Sequence[int | bytes]) -> None: - """.. deprecated:: 10.2.0""" - deprecate("IptcImagePlugin.dump", 12) - for i in c: - print(f"{_i8(i):02x}", end=" ") - print() - - ## # Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields # from TIFF and JPEG files, use the getiptcinfo function. From 4301c1fde63e96a32d27f4b2884131506886a415 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 20:35:12 +1000 Subject: [PATCH 1843/2374] Removed ImageMath eval and options parameters --- Tests/test_imagemath_lambda_eval.py | 7 ---- Tests/test_imagemath_unsafe_eval.py | 10 ----- docs/deprecations.rst | 36 ++++++++-------- src/PIL/ImageMath.py | 64 ++--------------------------- 4 files changed, 22 insertions(+), 95 deletions(-) diff --git a/Tests/test_imagemath_lambda_eval.py b/Tests/test_imagemath_lambda_eval.py index eec76118af0..26c04b9a07b 100644 --- a/Tests/test_imagemath_lambda_eval.py +++ b/Tests/test_imagemath_lambda_eval.py @@ -2,8 +2,6 @@ from typing import Any -import pytest - from PIL import Image, ImageMath @@ -55,11 +53,6 @@ def test_sanity() -> None: ) -def test_options_deprecated() -> None: - with pytest.warns(DeprecationWarning, match="ImageMath.lambda_eval options"): - assert ImageMath.lambda_eval(lambda args: 1, images) == 1 - - def test_ops() -> None: assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, **images)) == "I -1" diff --git a/Tests/test_imagemath_unsafe_eval.py b/Tests/test_imagemath_unsafe_eval.py index 60ad6aafa49..5e141a55b4f 100644 --- a/Tests/test_imagemath_unsafe_eval.py +++ b/Tests/test_imagemath_unsafe_eval.py @@ -35,16 +35,6 @@ def test_sanity() -> None: assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", **images)) == "I 3" -def test_eval_deprecated() -> None: - with pytest.warns(DeprecationWarning, match="ImageMath.eval"): - assert ImageMath.eval("1") == 1 - - -def test_options_deprecated() -> None: - with pytest.warns(DeprecationWarning, match="ImageMath.unsafe_eval options"): - assert ImageMath.unsafe_eval("1", images) == 1 - - def test_ops() -> None: assert pixel(ImageMath.unsafe_eval("-A", **images)) == "I -1" assert pixel(ImageMath.unsafe_eval("+B", **images)) == "L 2" diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 06767c20b1b..9eb9650b2f0 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -49,14 +49,6 @@ Deprecated Use instead :py:data:`sys.version_info`, and ``PIL.__version__`` ============================================ ==================================================== -ImageMath eval() -^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.3.0 - -``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or -:py:meth:`~PIL.ImageMath.unsafe_eval` instead. - Non-image modes in ImageCms ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -94,15 +86,6 @@ ICNS (width, height, scale) sizes Setting an ICNS image size to ``(width, height, scale)`` before loading has been deprecated. Instead, ``load(scale)`` can be used. -ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 11.0.0 - -The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and -:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword -arguments can be used instead. - ExifTags.IFD.Makernote ^^^^^^^^^^^^^^^^^^^^^^ @@ -179,6 +162,16 @@ Image isImageType() ``Image.isImageType(im)`` has been removed. Use ``isinstance(im, Image.Image)`` instead. +ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and +:py:meth:`~PIL.ImageMath.unsafe_eval()` has been removed. One or more keyword +arguments can be used instead. + JpegImageFile.huffman_ac and JpegImageFile.huffman_dc ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -208,6 +201,15 @@ removed. They were used for obtaining raw pointers to ``ImagingCore`` internals. interact with C code, you can use ``Image.Image.getim()``, which returns a ``Capsule`` object. +ImageMath eval() +^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.3.0 +.. versionremoved:: 12.0.0 + +``ImageMath.eval()`` has been removed. Use :py:meth:`~PIL.ImageMath.lambda_eval` or +:py:meth:`~PIL.ImageMath.unsafe_eval` instead. + BGR;15, BGR 16 and BGR;24 ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index c33809ced89..d2504b1ae5a 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -21,7 +21,6 @@ from typing import Any, Callable from . import Image, _imagingmath -from ._deprecate import deprecate class _Operand: @@ -233,11 +232,7 @@ def imagemath_convert(self: _Operand, mode: str) -> _Operand: } -def lambda_eval( - expression: Callable[[dict[str, Any]], Any], - options: dict[str, Any] = {}, - **kw: Any, -) -> Any: +def lambda_eval(expression: Callable[[dict[str, Any]], Any], **kw: Any) -> Any: """ Returns the result of an image function. @@ -246,23 +241,13 @@ def lambda_eval( :py:func:`~PIL.Image.merge` function. :param expression: A function that receives a dictionary. - :param options: Values to add to the function's dictionary. Deprecated. - You can instead use one or more keyword arguments. :param **kw: Values to add to the function's dictionary. :return: The expression result. This is usually an image object, but can also be an integer, a floating point value, or a pixel tuple, depending on the expression. """ - if options: - deprecate( - "ImageMath.lambda_eval options", - 12, - "ImageMath.lambda_eval keyword arguments", - ) - args: dict[str, Any] = ops.copy() - args.update(options) args.update(kw) for k, v in args.items(): if isinstance(v, Image.Image): @@ -275,11 +260,7 @@ def lambda_eval( return out -def unsafe_eval( - expression: str, - options: dict[str, Any] = {}, - **kw: Any, -) -> Any: +def unsafe_eval(expression: str, **kw: Any) -> Any: """ Evaluates an image expression. This uses Python's ``eval()`` function to process the expression string, and carries the security risks of doing so. It is not @@ -291,29 +272,19 @@ def unsafe_eval( :py:func:`~PIL.Image.merge` function. :param expression: A string containing a Python-style expression. - :param options: Values to add to the evaluation context. Deprecated. - You can instead use one or more keyword arguments. :param **kw: Values to add to the evaluation context. :return: The evaluated expression. This is usually an image object, but can also be an integer, a floating point value, or a pixel tuple, depending on the expression. """ - if options: - deprecate( - "ImageMath.unsafe_eval options", - 12, - "ImageMath.unsafe_eval keyword arguments", - ) - # build execution namespace args: dict[str, Any] = ops.copy() - for k in [*options, *kw]: + for k in kw: if "__" in k or hasattr(builtins, k): msg = f"'{k}' not allowed" raise ValueError(msg) - args.update(options) args.update(kw) for k, v in args.items(): if isinstance(v, Image.Image): @@ -337,32 +308,3 @@ def scan(code: CodeType) -> None: return out.im except AttributeError: return out - - -def eval( - expression: str, - _dict: dict[str, Any] = {}, - **kw: Any, -) -> Any: - """ - Evaluates an image expression. - - Deprecated. Use lambda_eval() or unsafe_eval() instead. - - :param expression: A string containing a Python-style expression. - :param _dict: Values to add to the evaluation context. You - can either use a dictionary, or one or more keyword - arguments. - :return: The evaluated expression. This is usually an image object, but can - also be an integer, a floating point value, or a pixel tuple, - depending on the expression. - - .. deprecated:: 10.3.0 - """ - - deprecate( - "ImageMath.eval", - 12, - "ImageMath.lambda_eval or ImageMath.unsafe_eval", - ) - return unsafe_eval(expression, _dict, **kw) From b4bc43fed2215418d85c022e6fc4eff31bb33ca7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 21:43:42 +1000 Subject: [PATCH 1844/2374] Removed ImageCms constants and versions() --- Tests/test_imagecms.py | 11 --- docs/deprecations.rst | 143 ++++++++++++++++++------------------ docs/reference/ImageCms.rst | 1 - src/PIL/ImageCms.py | 27 ------- 4 files changed, 72 insertions(+), 110 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index b6db0ab5c1b..8d463d0eb9d 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -54,10 +54,6 @@ def skip_missing() -> None: def test_sanity() -> None: # basic smoke test. # this mostly follows the cms_test outline. - with pytest.warns(DeprecationWarning, match="PIL.ImageCms.versions"): - v = ImageCms.versions() # should return four strings - assert v[0] == "1.0.0 pil" - assert list(map(type, v)) == [str, str, str, str] # internal version number version = features.version_module("littlecms2") @@ -703,13 +699,6 @@ def test_cmyk_lab() -> None: def test_deprecation() -> None: - with pytest.warns(DeprecationWarning, match="ImageCms.DESCRIPTION"): - assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") - with pytest.warns(DeprecationWarning, match="ImageCms.VERSION"): - assert ImageCms.VERSION == "1.0.0 pil" - with pytest.warns(DeprecationWarning, match="ImageCms.FLAGS"): - assert isinstance(ImageCms.FLAGS, dict) - profile = ImageCmsProfile(ImageCms.createProfile("sRGB")) with pytest.warns(DeprecationWarning, match="RGBA;16B"): ImageCms.ImageCmsTransform(profile, profile, "RGBA;16B", "RGB") diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 9eb9650b2f0..183abea092b 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,43 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a :py:exc:`DeprecationWarning` is issued. -ImageCms constants and versions() function -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 10.3.0 - -A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. -This includes a table of flags based on LittleCMS version 1 which has been -replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. - -============================================ ==================================================== -Deprecated Use instead -============================================ ==================================================== -``ImageCms.DESCRIPTION`` No replacement -``ImageCms.VERSION`` ``PIL.__version__`` -``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` -``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` No replacement -``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` -``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` -``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` -``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` -``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` -``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` -``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` -``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` -``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` -``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` -``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` -``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` -``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` -``ImageCms.versions()`` :py:func:`PIL.features.version_module` with - ``feature="littlecms2"``, :py:data:`sys.version` or - :py:data:`sys.version_info`, and ``PIL.__version__`` -============================================ ==================================================== - Non-image modes in ImageCms ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -153,6 +116,78 @@ The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant intended for internal use, so there is no replacement. They can each be replaced by a single line of code using builtin functions in Python. +ImageCms constants and versions() function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.3.0 +.. versionremoved:: 12.0.0 + +A number of constants and a function in :py:mod:`.ImageCms` have been removed. This +includes a table of flags based on LittleCMS version 1 which has been replaced with a +new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== + +ImageMath eval() +^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.3.0 +.. versionremoved:: 12.0.0 + +``ImageMath.eval()`` has been removed. Use :py:meth:`~PIL.ImageMath.lambda_eval` or +:py:meth:`~PIL.ImageMath.unsafe_eval` instead. + +BGR;15, BGR 16 and BGR;24 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 + +The experimental BGR;15, BGR;16 and BGR;24 modes have been removed. + +Support for LibTIFF earlier than 4 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 + +Support for LibTIFF earlier than version 4 has been removed. +Upgrade to a newer version of LibTIFF instead. + +ImageDraw.getdraw hints parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 + +The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been removed. + Image isImageType() ^^^^^^^^^^^^^^^^^^^ @@ -201,40 +236,6 @@ removed. They were used for obtaining raw pointers to ``ImagingCore`` internals. interact with C code, you can use ``Image.Image.getim()``, which returns a ``Capsule`` object. -ImageMath eval() -^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.3.0 -.. versionremoved:: 12.0.0 - -``ImageMath.eval()`` has been removed. Use :py:meth:`~PIL.ImageMath.lambda_eval` or -:py:meth:`~PIL.ImageMath.unsafe_eval` instead. - -BGR;15, BGR 16 and BGR;24 -^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 -.. versionremoved:: 12.0.0 - -The experimental BGR;15, BGR;16 and BGR;24 modes have been removed. - -Support for LibTIFF earlier than 4 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 -.. versionremoved:: 12.0.0 - -Support for LibTIFF earlier than version 4 has been removed. -Upgrade to a newer version of LibTIFF instead. - -ImageDraw.getdraw hints parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 -.. versionremoved:: 12.0.0 - -The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been removed. - TiffImagePlugin IFD_LEGACY_API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 238390e75f7..4a21236775f 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -56,7 +56,6 @@ Functions .. autofunction:: get_display_profile .. autofunction:: isIntentSupported .. autofunction:: profileToProfile -.. autofunction:: versions CmsProfile ---------- diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index a1584f111d4..a90efaeb68b 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -108,20 +108,6 @@ _VERSION = "1.0.0 pil" -def __getattr__(name: str) -> Any: - if name == "DESCRIPTION": - deprecate("PIL.ImageCms.DESCRIPTION", 12) - return _DESCRIPTION - elif name == "VERSION": - deprecate("PIL.ImageCms.VERSION", 12) - return _VERSION - elif name == "FLAGS": - deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags") - return _FLAGS - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - # --------------------------------------------------------------------. @@ -1108,16 +1094,3 @@ def isIntentSupported( return -1 except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) from v - - -def versions() -> tuple[str, str | None, str, str]: - """ - (pyCMS) Fetches versions. - """ - - deprecate( - "PIL.ImageCms.versions()", - 12, - '(PIL.features.version("littlecms2"), sys.version, PIL.__version__)', - ) - return _VERSION, core.littlecms_version, sys.version.split()[0], __version__ From 9fbc255ce56c355573fc81dffc73741411fb2d5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 20:47:58 +1000 Subject: [PATCH 1845/2374] Removed non-image modes in ImageCms --- Tests/test_imagecms.py | 14 -------------- docs/deprecations.rst | 19 ++++++++++--------- src/PIL/ImageCms.py | 30 ++---------------------------- 3 files changed, 12 insertions(+), 51 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 8d463d0eb9d..55a4a87fb2f 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -673,12 +673,6 @@ def test_auxiliary_channels_isolated() -> None: assert_image_equal(test_image.convert(dst_format[2]), reference_image) -def test_long_modes() -> None: - p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc") - with pytest.warns(DeprecationWarning, match="ABCDEFGHI"): - ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI") - - @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) def test_rgb_lab(mode: str) -> None: im = Image.new(mode, (1, 1)) @@ -696,11 +690,3 @@ def test_cmyk_lab() -> None: im = Image.new("CMYK", (1, 1)) converted_im = im.convert("LAB") assert converted_im.getpixel((0, 0)) == (255, 128, 128) - - -def test_deprecation() -> None: - profile = ImageCmsProfile(ImageCms.createProfile("sRGB")) - with pytest.warns(DeprecationWarning, match="RGBA;16B"): - ImageCms.ImageCmsTransform(profile, profile, "RGBA;16B", "RGB") - with pytest.warns(DeprecationWarning, match="RGBA;16B"): - ImageCms.ImageCmsTransform(profile, profile, "RGB", "RGBA;16B") diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 183abea092b..772e88147e5 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,15 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a :py:exc:`DeprecationWarning` is issued. -Non-image modes in ImageCms -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 - -The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow -image modes has been deprecated. Defaulting to "L" or "1" if the mode cannot be mapped -is also deprecated. - ImageDraw.getdraw hints parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -171,6 +162,16 @@ BGR;15, BGR 16 and BGR;24 The experimental BGR;15, BGR;16 and BGR;24 modes have been removed. +Non-image modes in ImageCms +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 + +The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow +image modes has been removed. Defaulting to "L" or "1" if the mode cannot be mapped has +also been removed. + Support for LibTIFF earlier than 4 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index a90efaeb68b..d3555694a51 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -23,10 +23,9 @@ import sys from enum import IntEnum, IntFlag from functools import reduce -from typing import Any, Literal, SupportsFloat, SupportsInt, Union +from typing import Literal, SupportsFloat, SupportsInt, Union -from . import Image, __version__ -from ._deprecate import deprecate +from . import Image from ._typing import SupportsRead try: @@ -287,31 +286,6 @@ def __init__( proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC, flags: Flags = Flags.NONE, ): - supported_modes = ( - "RGB", - "RGBA", - "RGBX", - "CMYK", - "I;16", - "I;16L", - "I;16B", - "YCbCr", - "LAB", - "L", - "1", - ) - for mode in (input_mode, output_mode): - if mode not in supported_modes: - deprecate( - mode, - 12, - { - "L;16": "I;16 or I;16L", - "L:16B": "I;16B", - "YCCA": "YCbCr", - "YCC": "YCbCr", - }.get(mode), - ) if proof is None: self.transform = core.buildTransform( input.profile, output.profile, input_mode, output_mode, intent, flags From aaf217cea0888a8f07d3c7ccbbf200114c5c0012 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 21:15:08 +1000 Subject: [PATCH 1846/2374] Removed ICNS (width, height, scale) sizes --- Tests/test_file_icns.py | 12 +----------- docs/deprecations.rst | 16 ++++++++-------- src/PIL/IcnsImagePlugin.py | 32 +++++++++++--------------------- 3 files changed, 20 insertions(+), 40 deletions(-) diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 8ff59161ff3..b9b81850665 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -93,21 +93,11 @@ def test_sizes() -> None: with Image.open(TEST_FILE) as im: assert isinstance(im, IcnsImagePlugin.IcnsImageFile) for w, h, r in im.info["sizes"]: - wr = w * r - hr = h * r - with pytest.warns( - DeprecationWarning, match=r"Setting size to \(width, height, scale\)" - ): - im.size = (w, h, r) - im.load() - assert im.mode == "RGBA" - assert im.size == (wr, hr) - # Test using load() with scale im.size = (w, h) im.load(scale=r) assert im.mode == "RGBA" - assert im.size == (wr, hr) + assert im.size == (w * r, h * r) # Check that we cannot load an incorrect size with pytest.raises(ValueError): diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 772e88147e5..e2c74f2a21a 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -32,14 +32,6 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ -ICNS (width, height, scale) sizes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 11.0.0 - -Setting an ICNS image size to ``(width, height, scale)`` before loading has been -deprecated. Instead, ``load(scale)`` can be used. - ExifTags.IFD.Makernote ^^^^^^^^^^^^^^^^^^^^^^ @@ -189,6 +181,14 @@ ImageDraw.getdraw hints parameter The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been removed. +ICNS (width, height, scale) sizes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +Setting an ICNS image size to ``(width, height, scale)`` before loading has been +removed. Instead, ``load(scale)`` can be used. + Image isImageType() ^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 5a88429e5e4..197ea7a2bb2 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -25,7 +25,6 @@ from typing import IO from . import Image, ImageFile, PngImagePlugin, features -from ._deprecate import deprecate enable_jpeg2k = features.check_codec("jpg_2000") if enable_jpeg2k: @@ -275,34 +274,25 @@ def _open(self) -> None: self.best_size[1] * self.best_size[2], ) - @property # type: ignore[override] - def size(self) -> tuple[int, int] | tuple[int, int, int]: + @property + def size(self) -> tuple[int, int]: return self._size @size.setter - def size(self, value: tuple[int, int] | tuple[int, int, int]) -> None: - if len(value) == 3: - deprecate("Setting size to (width, height, scale)", 12, "load(scale)") - if value in self.info["sizes"]: - self._size = value # type: ignore[assignment] + def size(self, value: tuple[int, int]) -> None: + # Check that a matching size exists, + # or that there is a scale that would create a size that matches + for size in self.info["sizes"]: + simple_size = size[0] * size[2], size[1] * size[2] + scale = simple_size[0] // value[0] + if simple_size[1] / value[1] == scale: + self._size = value return - else: - # Check that a matching size exists, - # or that there is a scale that would create a size that matches - for size in self.info["sizes"]: - simple_size = size[0] * size[2], size[1] * size[2] - scale = simple_size[0] // value[0] - if simple_size[1] / value[1] == scale: - self._size = value - return msg = "This is not one of the allowed sizes of this image" raise ValueError(msg) def load(self, scale: int | None = None) -> Image.core.PixelAccess | None: - if scale is not None or len(self.size) == 3: - if scale is None and len(self.size) == 3: - scale = self.size[2] - assert scale is not None + if scale is not None: width, height = self.size[:2] self.size = width * scale, height * scale self.best_size = width, height, scale From 92bafe6b88d903d06f0eeba2f1dd26dd0eea52bb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 21:44:33 +1000 Subject: [PATCH 1847/2374] Removed support for FreeType <= 2.9.0 --- Tests/test_imagefont.py | 39 --------------------------------------- docs/deprecations.rst | 33 +++++++++++++++++---------------- src/PIL/ImageFont.py | 17 +---------------- 3 files changed, 18 insertions(+), 71 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 9334d30e4f6..4565d35bab7 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -11,7 +11,6 @@ from typing import Any, BinaryIO import pytest -from packaging.version import parse as parse_version from PIL import Image, ImageDraw, ImageFont, features from PIL._typing import StrOrBytesPath @@ -691,16 +690,6 @@ def test_complex_font_settings() -> None: def test_variation_get(font: ImageFont.FreeTypeFont) -> None: - version = features.version_module("freetype2") - assert version is not None - freetype = parse_version(version) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.get_variation_names() - with pytest.raises(NotImplementedError): - font.get_variation_axes() - return - with pytest.raises(OSError): font.get_variation_names() with pytest.raises(OSError): @@ -763,14 +752,6 @@ def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: - version = features.version_module("freetype2") - assert version is not None - freetype = parse_version(version) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.set_variation_by_name("Bold") - return - with pytest.raises(OSError): font.set_variation_by_name("Bold") @@ -790,14 +771,6 @@ def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None: - version = features.version_module("freetype2") - assert version is not None - freetype = parse_version(version) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.set_variation_by_axes([100]) - return - with pytest.raises(OSError): font.set_variation_by_axes([500, 50]) @@ -1209,15 +1182,3 @@ def test_invalid_truetype_sizes_raise_valueerror( ) -> None: with pytest.raises(ValueError): ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) - - -def test_freetype_deprecation(monkeypatch: pytest.MonkeyPatch) -> None: - # Arrange: mock features.version_module to return fake FreeType version - def fake_version_module(module: str) -> str: - return "2.9.0" - - monkeypatch.setattr(features, "version_module", fake_version_module) - - # Act / Assert - with pytest.warns(DeprecationWarning, match="FreeType 2.9.0"): - ImageFont.truetype(FONT_PATH, FONT_SIZE) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index e2c74f2a21a..2365545656f 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -19,19 +19,6 @@ ImageDraw.getdraw hints parameter The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. -FreeType 2.9.0 -^^^^^^^^^^^^^^ - -.. deprecated:: 11.0.0 - -Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 -(2025-10-15), when FreeType 2.9.1 will be the minimum supported. - -We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). - -.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ - ExifTags.IFD.Makernote ^^^^^^^^^^^^^^^^^^^^^^ @@ -79,7 +66,7 @@ Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. ImageFile.raise_oserror -~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 10.2.0 .. versionremoved:: 12.0.0 @@ -89,7 +76,7 @@ only useful for translating error codes returned by a codec's ``decode()`` metho which ImageFile already did automatically. IptcImageFile helper functions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 10.2.0 .. versionremoved:: 12.0.0 @@ -100,7 +87,7 @@ intended for internal use, so there is no replacement. They can each be replaced single line of code using builtin functions in Python. ImageCms constants and versions() function -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 10.3.0 .. versionremoved:: 12.0.0 @@ -181,6 +168,20 @@ ImageDraw.getdraw hints parameter The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been removed. +FreeType 2.9.0 +^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +Support for FreeType 2.9.0 has been removed. FreeType 2.9.1 is the minimum version +supported. + +We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + ICNS (width, height, scale) sizes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 329c463ff86..bf3f471f5e3 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -36,7 +36,7 @@ from types import ModuleType from typing import IO, Any, BinaryIO, TypedDict, cast -from . import Image, features +from . import Image from ._typing import StrOrBytesPath from ._util import DeferredError, is_path @@ -236,21 +236,6 @@ def __init__( self.index = index self.encoding = encoding - try: - from packaging.version import parse as parse_version - except ImportError: - pass - else: - if freetype_version := features.version_module("freetype2"): - if parse_version(freetype_version) < parse_version("2.9.1"): - warnings.warn( - "Support for FreeType 2.9.0 is deprecated and will be removed " - "in Pillow 12 (2025-10-15). Please upgrade to FreeType 2.9.1 " - "or newer, preferably FreeType 2.10.4 which fixes " - "CVE-2020-15999.", - DeprecationWarning, - ) - if layout_engine not in (Layout.BASIC, Layout.RAQM): layout_engine = Layout.BASIC if core.HAVE_RAQM: From 0e3aac1ed18e5fa55a9fa7ef1956eccbc4b32ed7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 21:19:30 +1000 Subject: [PATCH 1848/2374] Updated deprecation timeline --- Tests/test_deprecate.py | 20 ++++++++++---------- src/PIL/_deprecate.py | 2 -- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 88479ff0d1d..1e98ecfff37 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -9,9 +9,9 @@ "version, expected", [ ( - 12, - "Old thing is deprecated and will be removed in Pillow 12 " - r"\(2025-10-15\)\. Use new thing instead\.", + 13, + "Old thing is deprecated and will be removed in Pillow 13 " + r"\(2026-10-15\)\. Use new thing instead\.", ), ( None, @@ -53,18 +53,18 @@ def test_old_version(deprecated: str, plural: bool, expected: str) -> None: def test_plural() -> None: expected = ( - r"Old things are deprecated and will be removed in Pillow 12 \(2025-10-15\)\. " + r"Old things are deprecated and will be removed in Pillow 13 \(2026-10-15\)\. " r"Use new thing instead\." ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old things", 12, "new thing", plural=True) + _deprecate.deprecate("Old things", 13, "new thing", plural=True) def test_replacement_and_action() -> None: expected = "Use only one of 'replacement' and 'action'" with pytest.raises(ValueError, match=expected): _deprecate.deprecate( - "Old thing", 12, replacement="new thing", action="Upgrade to new thing" + "Old thing", 13, replacement="new thing", action="Upgrade to new thing" ) @@ -77,16 +77,16 @@ def test_replacement_and_action() -> None: ) def test_action(action: str) -> None: expected = ( - r"Old thing is deprecated and will be removed in Pillow 12 \(2025-10-15\)\. " + r"Old thing is deprecated and will be removed in Pillow 13 \(2026-10-15\)\. " r"Upgrade to new thing\." ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old thing", 12, action=action) + _deprecate.deprecate("Old thing", 13, action=action) def test_no_replacement_or_action() -> None: expected = ( - r"Old thing is deprecated and will be removed in Pillow 12 \(2025-10-15\)" + r"Old thing is deprecated and will be removed in Pillow 13 \(2026-10-15\)" ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old thing", 12) + _deprecate.deprecate("Old thing", 13) diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 170d4449049..616a9aace9f 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -46,8 +46,6 @@ def deprecate( elif when <= int(__version__.split(".")[0]): msg = f"{deprecated} {is_} deprecated and should be removed." raise RuntimeError(msg) - elif when == 12: - removed = "Pillow 12 (2025-10-15)" elif when == 13: removed = "Pillow 13 (2026-10-15)" else: From f2417d8b390b3205a01795491dd45ebacd88800b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 21:42:32 +1000 Subject: [PATCH 1849/2374] Added release notes --- docs/releasenotes/12.0.0.rst | 140 +++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + docs/releasenotes/template.rst | 2 + 3 files changed, 143 insertions(+) create mode 100644 docs/releasenotes/12.0.0.rst diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst new file mode 100644 index 00000000000..68b6644438f --- /dev/null +++ b/docs/releasenotes/12.0.0.rst @@ -0,0 +1,140 @@ +12.0.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards incompatible changes +============================== + +ImageFile.raise_oserror +^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageFile.raise_oserror()`` has been removed. The function was undocumented and was +only useful for translating error codes returned by a codec's ``decode()`` method, +which ImageFile already did automatically. + +IptcImageFile helper functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant +``IptcImageFile.PAD`` have been removed. These were undocumented helper functions +intended for internal use, so there is no replacement. They can each be replaced by a +single line of code using builtin functions in Python. + +ImageCms constants and versions() function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A number of constants and a function in :py:mod:`.ImageCms` have been removed. This +includes a table of flags based on LittleCMS version 1 which has been replaced with a +new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== + +ImageMath eval() +^^^^^^^^^^^^^^^^ + +``ImageMath.eval()`` has been removed. Use :py:meth:`~PIL.ImageMath.lambda_eval` or +:py:meth:`~PIL.ImageMath.unsafe_eval` instead. + +BGR;15, BGR 16 and BGR;24 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The experimental BGR;15, BGR;16 and BGR;24 modes have been removed. + +Non-image modes in ImageCms +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow +image modes has been removed. Defaulting to "L" or "1" if the mode cannot be mapped has +also been removed. + +Support for LibTIFF earlier than 4 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support for LibTIFF earlier than version 4 has been removed. +Upgrade to a newer version of LibTIFF instead. + +ImageDraw.getdraw hints parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been removed. + +FreeType 2.9.0 +^^^^^^^^^^^^^^ + +Support for FreeType 2.9.0 has been removed. FreeType 2.9.1 is the minimum version +supported. + +We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API changes +=========== + +TODO +^^^^ + +TODO + +API additions +============= + +TODO +^^^^ + +TODO + +Other changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index a85f1e0752e..f66240c8957 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 12.0.0 11.3.0 11.2.1 11.1.0 diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst index a453d2a436d..b603a9938c5 100644 --- a/docs/releasenotes/template.rst +++ b/docs/releasenotes/template.rst @@ -20,6 +20,8 @@ Backwards incompatible changes TODO ^^^^ +TODO + Deprecations ============ From 5554e778bba52f2414d0151642a2906aed254ad2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Jul 2025 13:18:48 +1000 Subject: [PATCH 1850/2374] Removed unnecessary checks --- src/libImaging/AlphaComposite.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libImaging/AlphaComposite.c b/src/libImaging/AlphaComposite.c index 6d728f9088b..e14af0dea1a 100644 --- a/src/libImaging/AlphaComposite.c +++ b/src/libImaging/AlphaComposite.c @@ -25,13 +25,11 @@ ImagingAlphaComposite(Imaging imDst, Imaging imSrc) { int x, y; /* Check arguments */ - if (!imDst || !imSrc || strcmp(imDst->mode, "RGBA") || - imDst->type != IMAGING_TYPE_UINT8 || imDst->bands != 4) { + if (!imDst || !imSrc || strcmp(imDst->mode, "RGBA")) { return ImagingError_ModeError(); } - if (strcmp(imDst->mode, imSrc->mode) || imDst->type != imSrc->type || - imDst->bands != imSrc->bands || imDst->xsize != imSrc->xsize || + if (strcmp(imDst->mode, imSrc->mode) || imDst->xsize != imSrc->xsize || imDst->ysize != imSrc->ysize) { return ImagingError_Mismatch(); } From 3152da47355ed3f9b342dc94de8a2214798842c4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Jul 2025 13:51:18 +1000 Subject: [PATCH 1851/2374] Allow alpha_composite to use LA images --- Tests/test_image.py | 31 +++++++++++++++++++++++++++++++ src/PIL/Image.py | 5 ++--- src/libImaging/AlphaComposite.c | 3 ++- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 6b8b6d42b07..e4c25693a85 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -398,6 +398,37 @@ def test_alpha_composite(self) -> None: assert img_colors is not None assert sorted(img_colors) == expected_colors + def test_alpha_composite_la(self) -> None: + # Arrange + expected_colors = sorted( + [ + (3300, (255, 255)), + (1156, (170, 192)), + (1122, (128, 255)), + (1089, (0, 0)), + (1122, (255, 128)), + (1122, (0, 128)), + (1089, (0, 255)), + ] + ) + + dst = Image.new("LA", size=(100, 100), color=(0, 255)) + draw = ImageDraw.Draw(dst) + draw.rectangle((0, 33, 100, 66), fill=(0, 128)) + draw.rectangle((0, 67, 100, 100), fill=(0, 0)) + src = Image.new("LA", size=(100, 100), color=(255, 255)) + draw = ImageDraw.Draw(src) + draw.rectangle((33, 0, 66, 100), fill=(255, 128)) + draw.rectangle((67, 0, 100, 100), fill=(255, 0)) + + # Act + img = Image.alpha_composite(dst, src) + + # Assert + img_colors = img.getcolors() + assert img_colors is not None + assert sorted(img_colors) == expected_colors + def test_alpha_inplace(self) -> None: src = Image.new("RGBA", (128, 128), "blue") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d209405c4c5..f4f1eea7914 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3588,9 +3588,8 @@ def alpha_composite(im1: Image, im2: Image) -> Image: """ Alpha composite im2 over im1. - :param im1: The first image. Must have mode RGBA. - :param im2: The second image. Must have mode RGBA, and the same size as - the first image. + :param im1: The first image. Must have mode RGBA or LA. + :param im2: The second image. Must have the same mode and size as the first image. :returns: An :py:class:`~PIL.Image.Image` object. """ diff --git a/src/libImaging/AlphaComposite.c b/src/libImaging/AlphaComposite.c index e14af0dea1a..44c45167946 100644 --- a/src/libImaging/AlphaComposite.c +++ b/src/libImaging/AlphaComposite.c @@ -25,7 +25,8 @@ ImagingAlphaComposite(Imaging imDst, Imaging imSrc) { int x, y; /* Check arguments */ - if (!imDst || !imSrc || strcmp(imDst->mode, "RGBA")) { + if (!imDst || !imSrc || + (strcmp(imDst->mode, "RGBA") && strcmp(imDst->mode, "LA"))) { return ImagingError_ModeError(); } From 1ee91f22ba4ecf8fabf7b2de4ac9b3eafe5c168e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Jul 2025 22:51:02 +1000 Subject: [PATCH 1852/2374] Updated macOS tested Pillow versions --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index a56f9431652..c2227f1d29f 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -75,7 +75,7 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+============================+==================+==============+ -| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.2.1 |arm | +| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.3.0 |arm | | +----------------------------+------------------+ | | | 3.8 | 10.4.0 | | +----------------------------------+----------------------------+------------------+--------------+ From a84458ffbd56d59d59e0f6d750ba771e71596b4c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Jul 2025 11:45:02 +1000 Subject: [PATCH 1853/2374] Revert "Work around pyroma test" This reverts commit d8a0cb5db104cc5d9acc6b4ba1ba871636132f51. --- Tests/test_pyroma.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 9669f485a6f..a161d3f05c4 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -25,11 +25,5 @@ def test_pyroma() -> None: ) else: - # Should have a perfect score, but pyroma does not support PEP 639 yet. - assert rating == ( - 9, - [ - "Your package does neither have a license field " - "nor any license classifiers." - ], - ) + # Should have a perfect score + assert rating == (10, []) From 756dd04705be059136a77ba473e1e708a52711fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 7 Jul 2025 19:09:39 +1000 Subject: [PATCH 1854/2374] Removed reference to libtiff 3.x --- docs/installation/building-from-source.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 8988a92ce36..45cf5295c50 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -44,7 +44,7 @@ Many of Pillow's features require external libraries: * **libtiff** provides compressed TIFF functionality - * Pillow has been tested with libtiff versions **3.x** and **4.0-4.7.0** + * Pillow has been tested with libtiff versions **4.0-4.7.0** * **libfreetype** provides type related services From 14b0cebfc1c1acb0de44520c63de7294be1d59a5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:16:48 +0000 Subject: [PATCH 1855/2374] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.0 → v0.12.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.0...v0.12.2) - [github.com/PyCQA/bandit: 1.8.5 → 1.8.6](https://github.com/PyCQA/bandit/compare/1.8.5...1.8.6) - [github.com/pre-commit/mirrors-clang-format: v20.1.6 → v20.1.7](https://github.com/pre-commit/mirrors-clang-format/compare/v20.1.6...v20.1.7) - [github.com/python-jsonschema/check-jsonschema: 0.33.1 → 0.33.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.33.1...0.33.2) - [github.com/woodruffw/zizmor-pre-commit: v1.9.0 → v1.11.0](https://github.com/woodruffw/zizmor-pre-commit/compare/v1.9.0...v1.11.0) --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d5fd964f128..75c7d36324e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.0 + rev: v0.12.2 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.8.5 + rev: 1.8.6 hooks: - id: bandit args: [--severity-level=high] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.6 + rev: v20.1.7 hooks: - id: clang-format types: [c] @@ -51,14 +51,14 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.1 + rev: 0.33.2 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.9.0 + rev: v1.11.0 hooks: - id: zizmor From 4cfef00574803a64fbab26d2400fe1f39521cbbc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Jul 2025 16:33:22 +1000 Subject: [PATCH 1856/2374] Added "Colors" to concepts --- docs/handbook/concepts.rst | 22 ++++++++++++++++++++++ docs/reference/ImageDraw.rst | 4 +--- docs/reference/PixelAccess.rst | 2 +- src/PIL/Image.py | 24 +++++++++++++----------- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index c9d3f5e91cb..46f612be3b2 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -101,6 +101,28 @@ Palette The palette mode (``P``) uses a color palette to define the actual color for each pixel. +.. _colors: + +Colors +------ + +To specify colors, you can use tuples with a value for each channel in the image, e.g. +``Image.new("RGB", (1, 1), (255, 0, 0))``. + +If an image has a single channel, you can use a single number instead, e.g. +``Image.new("L", (1, 1), 255)``. For "F" mode images, floating point values are also +accepted. In the case of "P" mode images, these will be indexes for the color palette. + +If a single value is used for an image with more than one channel, it will still be +parsed:: + + >>> from PIL import Image + >>> im = Image.new("RGBA", (1, 1), 0x04030201) + >>> im.getpixel((0, 0)) + (1, 2, 3, 4) + +Some methods accept other forms, such as color names. See :ref:`color-names`. + Info ---- diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 6e73233a1ce..4a2223a40c5 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -45,9 +45,7 @@ Colors ^^^^^^ To specify colors, you can use numbers or tuples just as you would use with -:py:meth:`PIL.Image.new` or :py:meth:`PIL.Image.Image.putpixel`. For “1”, -“L”, and “I” images, use integers. For “RGB” images, use a 3-tuple containing -integer values. For “F” images, use integer or floating point values. +:py:meth:`PIL.Image.new`. See :ref:`colors` for more information. For palette images (mode “P”), use integers as color indexes. In 1.1.4 and later, you can also use RGB 3-tuples or color names (see below). The drawing diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 9d7cf83b640..e4af94b9f18 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -59,7 +59,7 @@ Access using negative indexes is also possible. :: Modifies the pixel at x,y. The color is given as a single numerical value for single band images, and a tuple for - multi-band images. + multi-band images. See :ref:`colors` for more information. :param xy: The pixel coordinate, given as (x, y). :param color: The pixel value according to its mode, diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 59168f5e3d1..262b5478b62 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1730,9 +1730,10 @@ def paste( details). Instead of an image, the source can be a integer or tuple - containing pixel values. The method then fills the region - with the given color. When creating RGB images, you can - also use color strings as supported by the ImageColor module. + containing pixel values. The method then fills the region + with the given color. When creating RGB images, you can + also use color strings as supported by the ImageColor module. See + :ref:`colors` for more information. If a mask is given, this method updates only the regions indicated by the mask. You can use either "1", "L", "LA", "RGBA" @@ -1988,7 +1989,8 @@ def putdata( sequence ends. The scale and offset values are used to adjust the sequence values: **pixel = value*scale + offset**. - :param data: A flattened sequence object. + :param data: A flattened sequence object. See :ref:`colors` for more + information about values. :param scale: An optional scale value. The default is 1.0. :param offset: An optional offset value. The default is 0.0. """ @@ -2047,7 +2049,7 @@ def putpixel( Modifies the pixel at the given position. The color is given as a single numerical value for single-band images, and a tuple for multi-band images. In addition to this, RGB and RGBA tuples are - accepted for P and PA images. + accepted for P and PA images. See :ref:`colors` for more information. Note that this method is relatively slow. For more extensive changes, use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw` @@ -3055,12 +3057,12 @@ def new( :param mode: The mode to use for the new image. See: :ref:`concept-modes`. :param size: A 2-tuple, containing (width, height) in pixels. - :param color: What color to use for the image. Default is black. - If given, this should be a single integer or floating point value - for single-band modes, and a tuple for multi-band modes (one value - per band). When creating RGB or HSV images, you can also use color - strings as supported by the ImageColor module. If the color is - None, the image is not initialised. + :param color: What color to use for the image. Default is black. If given, + this should be a single integer or floating point value for single-band + modes, and a tuple for multi-band modes (one value per band). When + creating RGB or HSV images, you can also use color strings as supported + by the ImageColor module. See :ref:`colors` for more information. If the + color is None, the image is not initialised. :returns: An :py:class:`~PIL.Image.Image` object. """ From e88f3120291cc208d8d1b46e3766fdbc1cfada82 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Jul 2025 12:57:07 +1000 Subject: [PATCH 1857/2374] Fix unclosed file warning --- Tests/test_file_libtiff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index c245a5a9bcb..958e2749f61 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -873,8 +873,8 @@ def test_lzma(self, capfd: pytest.CaptureFixture[str]) -> None: assert im.mode == "RGB" assert im.size == (128, 128) assert im.format == "TIFF" - im2 = hopper() - assert_image_similar(im, im2, 5) + with hopper() as im2: + assert_image_similar(im, im2, 5) except OSError: captured = capfd.readouterr() if "LZMA compression support is not configured" in captured.err: From dc7d646db03bb34abd493a79ec2ceb78ec778265 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 22:52:23 +1000 Subject: [PATCH 1858/2374] Use correct bands for 2 band histograms --- Tests/test_image_histogram.py | 3 +++ src/libImaging/Histo.c | 14 +++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index dbd55d4c2d3..436eb78a26e 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -10,9 +10,12 @@ def histogram(mode: str) -> tuple[int, int, int]: assert histogram("1") == (256, 0, 10994) assert histogram("L") == (256, 0, 662) + assert histogram("LA") == (512, 0, 16384) + assert histogram("La") == (512, 0, 16384) assert histogram("I") == (256, 0, 662) assert histogram("F") == (256, 0, 662) assert histogram("P") == (256, 0, 1551) + assert histogram("PA") == (512, 0, 16384) assert histogram("RGB") == (768, 4, 675) assert histogram("RGBA") == (1024, 0, 16384) assert histogram("CMYK") == (1024, 0, 16384) diff --git a/src/libImaging/Histo.c b/src/libImaging/Histo.c index c5a547a647b..87c09d3d4ca 100644 --- a/src/libImaging/Histo.c +++ b/src/libImaging/Histo.c @@ -132,11 +132,15 @@ ImagingGetHistogram(Imaging im, Imaging imMask, void *minmax) { ImagingSectionEnter(&cookie); for (y = 0; y < im->ysize; y++) { UINT8 *in = (UINT8 *)im->image[y]; - for (x = 0; x < im->xsize; x++) { - h->histogram[(*in++)]++; - h->histogram[(*in++) + 256]++; - h->histogram[(*in++) + 512]++; - h->histogram[(*in++) + 768]++; + for (x = 0; x < im->xsize; x++, in += 4) { + h->histogram[*in]++; + if (im->bands == 2) { + h->histogram[*(in + 3) + 256]++; + } else { + h->histogram[*(in + 1) + 256]++; + h->histogram[*(in + 2) + 512]++; + h->histogram[*(in + 3) + 768]++; + } } } ImagingSectionLeave(&cookie); From 99737228c5a65d5291ecdf4d9718a34a815e8b32 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Jul 2025 06:53:22 +1000 Subject: [PATCH 1859/2374] Only deprecate fromarray mode for changing data types --- Tests/test_image_array.py | 16 +++++--- src/PIL/Image.py | 79 +++++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 46 deletions(-) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index ecbce3d6ffa..abb22f94967 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -101,9 +101,8 @@ def __init__(self, arr_params: dict[str, Any]) -> None: self.__array_interface__ = arr_params with pytest.raises(ValueError): - wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) - with pytest.warns(DeprecationWarning, match="'mode' parameter"): - Image.fromarray(wrapped, "L") + wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1), "typestr": "|u1"}) + Image.fromarray(wrapped, "L") def test_fromarray_palette() -> None: @@ -112,9 +111,16 @@ def test_fromarray_palette() -> None: a = numpy.array(i) # 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 out.palette is not None 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") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 59168f5e3d1..e512da9a192 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3251,19 +3251,9 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: transferred. This means that P and PA mode images will lose their palette. :param obj: Object with array interface - :param mode: Optional mode to use when reading ``obj``. Will be determined from - type if ``None``. Deprecated. - - This will not be used to convert the data after reading, but will be used to - 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) + :param mode: Optional mode to use when reading ``obj``. Since pixel values do not + 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. See: :ref:`concept-modes` for general information about modes. :returns: An image object. @@ -3274,21 +3264,28 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: shape = arr["shape"] ndim = len(shape) strides = arr.get("strides", None) - if mode is None: - try: - typekey = (1, 1) + shape[2:], arr["typestr"] - except KeyError as e: + try: + typekey = (1, 1) + shape[2:], arr["typestr"] + except KeyError as e: + if mode is not None: + typekey = None + color_modes: list[str] = [] + else: msg = "Cannot handle this data type" raise TypeError(msg) from e + if typekey is not None: try: - mode, rawmode = _fromarray_typemap[typekey] + typemode, rawmode, color_modes = _fromarray_typemap[typekey] except KeyError as e: typekey_shape, typestr = typekey msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" raise TypeError(msg) from e - else: - deprecate("'mode' parameter", 13) + if mode is not None: + if mode != typemode and mode not in color_modes: + deprecate("'mode' parameter for changing data types", 13) rawmode = mode + else: + mode = typemode if mode in ["1", "L", "I", "P", "F"]: ndmax = 2 elif mode == "RGB": @@ -3385,29 +3382,29 @@ def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile: _fromarray_typemap = { - # (shape, typestr) => mode, rawmode + # (shape, typestr) => mode, rawmode, color modes # first two members of shape are set to one - ((1, 1), "|b1"): ("1", "1;8"), - ((1, 1), "|u1"): ("L", "L"), - ((1, 1), "|i1"): ("I", "I;8"), - ((1, 1), "u2"): ("I", "I;16B"), - ((1, 1), "i2"): ("I", "I;16BS"), - ((1, 1), "u4"): ("I", "I;32B"), - ((1, 1), "i4"): ("I", "I;32BS"), - ((1, 1), "f4"): ("F", "F;32BF"), - ((1, 1), "f8"): ("F", "F;64BF"), - ((1, 1, 2), "|u1"): ("LA", "LA"), - ((1, 1, 3), "|u1"): ("RGB", "RGB"), - ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), + ((1, 1), "|b1"): ("1", "1;8", []), + ((1, 1), "|u1"): ("L", "L", ["P"]), + ((1, 1), "|i1"): ("I", "I;8", []), + ((1, 1), "u2"): ("I", "I;16B", []), + ((1, 1), "i2"): ("I", "I;16BS", []), + ((1, 1), "u4"): ("I", "I;32B", []), + ((1, 1), "i4"): ("I", "I;32BS", []), + ((1, 1), "f4"): ("F", "F;32BF", []), + ((1, 1), "f8"): ("F", "F;64BF", []), + ((1, 1, 2), "|u1"): ("LA", "LA", ["La", "PA"]), + ((1, 1, 3), "|u1"): ("RGB", "RGB", ["YCbCr", "LAB", "HSV"]), + ((1, 1, 4), "|u1"): ("RGBA", "RGBA", ["RGBa"]), # shortcuts: - ((1, 1), f"{_ENDIAN}i4"): ("I", "I"), - ((1, 1), f"{_ENDIAN}f4"): ("F", "F"), + ((1, 1), f"{_ENDIAN}i4"): ("I", "I", []), + ((1, 1), f"{_ENDIAN}f4"): ("F", "F", []), } From 06f5cd1ddecea64d44f417bba539dc0b30734ea4 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:31:03 +1000 Subject: [PATCH 1860/2374] Restored manylinux2014 wheels (#9059) --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 52a3f2cdb2a..5cc4f03552c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -77,22 +77,22 @@ jobs: platform: linux os: ubuntu-latest cibw_arch: x86_64 + manylinux: "manylinux2014" - name: "manylinux_2_28 x86_64" platform: linux os: ubuntu-latest cibw_arch: x86_64 build: "*manylinux*" - manylinux: "manylinux_2_28" - name: "manylinux2014 and musllinux aarch64" platform: linux os: ubuntu-24.04-arm cibw_arch: aarch64 + manylinux: "manylinux2014" - name: "manylinux_2_28 aarch64" platform: linux os: ubuntu-24.04-arm cibw_arch: aarch64 build: "*manylinux*" - manylinux: "manylinux_2_28" - name: "iOS arm64 device" platform: ios os: macos-latest From 2195faf0dc739f4d46f5d77a4a323b5358f079af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:44:13 +1000 Subject: [PATCH 1861/2374] Update dependency cibuildwheel to v3.0.1 (#9075) --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 520b6e32084..e1eb52eb8ae 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.0.0 +cibuildwheel==3.0.1 From c9cf688ee7ef50dc1bd4531f19514508ae68a8e5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Jul 2025 21:10:26 +1000 Subject: [PATCH 1862/2374] Removed ImageDraw.getdraw hints deprecation section --- docs/deprecations.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 2365545656f..4e65dc8078a 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,13 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a :py:exc:`DeprecationWarning` is issued. -ImageDraw.getdraw hints parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 - -The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. - ExifTags.IFD.Makernote ^^^^^^^^^^^^^^^^^^^^^^ @@ -186,6 +179,7 @@ ICNS (width, height, scale) sizes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 Setting an ICNS image size to ``(width, height, scale)`` before loading has been removed. Instead, ``load(scale)`` can be used. From cbd47d8609e3306cb4b20ba2b04b32c176c88e43 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Jul 2025 23:07:07 +1000 Subject: [PATCH 1863/2374] Removed handling of deprecated WebP features --- src/PIL/features.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index 984f7532c7c..ff32c251045 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -9,7 +9,6 @@ import PIL from . import Image -from ._deprecate import deprecate modules = { "pil": ("PIL._imaging", "PILLOW_VERSION"), @@ -120,7 +119,7 @@ def get_supported_codecs() -> list[str]: return [f for f in codecs if check_codec(f)] -features: dict[str, tuple[str, str | bool, str | None]] = { +features: dict[str, tuple[str, str, str | None]] = { "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), @@ -146,12 +145,8 @@ def check_feature(feature: str) -> bool | None: module, flag, ver = features[feature] - if isinstance(flag, bool): - deprecate(f'check_feature("{feature}")', 12) try: imported_module = __import__(module, fromlist=["PIL"]) - if isinstance(flag, bool): - return flag return getattr(imported_module, flag) except ModuleNotFoundError: return None @@ -181,17 +176,7 @@ def get_supported_features() -> list[str]: """ :returns: A list of all supported features. """ - supported_features = [] - for f, (module, flag, _) in features.items(): - if flag is True: - for feature, (feature_module, _) in modules.items(): - if feature_module == module: - if check_module(feature): - supported_features.append(f) - break - elif check_feature(f): - supported_features.append(f) - return supported_features + return [f for f in features if check_feature(f)] def check(feature: str) -> bool | None: From 31e6c716ac0141ca03aed750b8b326183a45b0fb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Jul 2025 22:26:25 +1000 Subject: [PATCH 1864/2374] Improved features test coverage --- Tests/test_features.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Tests/test_features.py b/Tests/test_features.py index 520c25b4645..ddca9934498 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -112,6 +112,25 @@ def test_unsupported_module() -> None: features.version_module(module) +def test_unsupported_feature() -> None: + # Arrange + feature = "unsupported_feature" + # Act / Assert + with pytest.raises(ValueError): + features.check_feature(feature) + with pytest.raises(ValueError): + features.version_feature(feature) + + +def test_unsupported_version() -> None: + assert features.version("unsupported_version") is None + + +def test_modulenotfound(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(features, "features", {"test": ("PIL._test", "", "")}) + assert features.check_feature("test") is None + + @pytest.mark.parametrize("supported_formats", (True, False)) def test_pilinfo(supported_formats: bool) -> None: buf = io.StringIO() From 2af930b2f72a86f30e79b5abc6f6791362411206 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 10 Jul 2025 12:07:38 +0800 Subject: [PATCH 1865/2374] Ensure dynamic libjpeg libraries are not linked. --- .github/workflows/wheels-dependencies.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index d761d93b62c..2c38dc609fb 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -60,7 +60,7 @@ if [[ "$CIBW_PLATFORM" == "ios" ]]; then # on using the Xcode builder, which isn't very helpful for most of Pillow's # dependencies. Therefore, we lean on the OSX configurations, plus CC, CFLAGS # etc. to ensure the right sysroot is selected. - HOST_CMAKE_FLAGS="-DCMAKE_SYSTEM_NAME=$CMAKE_SYSTEM_NAME -DCMAKE_SYSTEM_PROCESSOR=$GNU_ARCH -DCMAKE_OSX_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET -DCMAKE_OSX_SYSROOT=$IOS_SDK_PATH -DBUILD_SHARED_LIBS=NO" + HOST_CMAKE_FLAGS="-DCMAKE_SYSTEM_NAME=$CMAKE_SYSTEM_NAME -DCMAKE_SYSTEM_PROCESSOR=$GNU_ARCH -DCMAKE_OSX_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET -DCMAKE_OSX_SYSROOT=$IOS_SDK_PATH -DBUILD_SHARED_LIBS=NO -DENABLE_SHARED=NO" # Meson needs to be pointed at a cross-platform configuration file # This will be generated once CC etc. have been evaluated. @@ -380,6 +380,15 @@ fi wrap_wheel_builder build +# A safety catch for iOS. iOS can't use dynamic libraries, but clang will prefer +# to link dynamic libraries to static libraries. The only way to reliably +# prevent this is to not have dynamic libraries available in the first place. +# The build process *shouldn't* generate any dylibs... but just in case, purge +# any dylibs that *have* been installed into the build prefix directory. +if [[ -n "$IOS_SDK" ]]; then + find "$BUILD_PREFIX" -name "*.dylib" -exec rm -rf {} \; +fi + # Return to the project root to finish the build popd > /dev/null From 6c12d188db46ea8cfb19024bd55c352a2aaa3a03 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Jul 2025 22:33:31 +1000 Subject: [PATCH 1866/2374] Updated libwebp to 1.6.0 --- .github/workflows/wheels-dependencies.sh | 4 +-- depends/install_webp.sh | 2 +- patches/iOS/libwebp-1.5.0.tar.gz.patch | 42 ------------------------ winbuild/build_prepare.py | 2 +- 4 files changed, 4 insertions(+), 46 deletions(-) delete mode 100644 patches/iOS/libwebp-1.5.0.tar.gz.patch diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2c38dc609fb..6d52ca98908 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -103,7 +103,7 @@ TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 ZLIB_VERSION=1.3.1 ZLIB_NG_VERSION=2.2.4 -LIBWEBP_VERSION=1.5.0 # Patched; next release won't need patching. See patch file. +LIBWEBP_VERSION=1.6.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 # Patched; next release won't need patching. See patch file. @@ -282,7 +282,7 @@ function build { fi CFLAGS="$CFLAGS $webp_cflags" build_simple libwebp $LIBWEBP_VERSION \ https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \ - --enable-libwebpmux --enable-libwebpdemux + --enable-libwebpmux --enable-libwebpdemux --disable-libwebpexamples build_brotli diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 9d29777159e..d7f3cd2f5d0 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.5.0 +archive=libwebp-1.6.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/patches/iOS/libwebp-1.5.0.tar.gz.patch b/patches/iOS/libwebp-1.5.0.tar.gz.patch deleted file mode 100644 index fefb72b68d2..00000000000 --- a/patches/iOS/libwebp-1.5.0.tar.gz.patch +++ /dev/null @@ -1,42 +0,0 @@ -# libwebp example binaries require dependencies that aren't available for iOS builds. -# There's also no easy way to invoke the build to *exclude* the example builds. -# Since we don't need the examples anyway, remove them from the Makefile. -# -# As a point of reference, libwebp provides an XCFramework build script that involves -# 7 separate invocations of make to avoid building the examples. Patching the Makefile -# to remove the examples is a simpler approach, and one that is more compatible with -# the existing multibuild infrastructure. -# -# In the next release, it should be possible to pass --disable-libwebpexamples -# instead of applying this patch. -# -diff -ur libwebp-1.5.0-orig/Makefile.am libwebp-1.5.0/Makefile.am ---- libwebp-1.5.0-orig/Makefile.am 2024-12-20 09:17:50 -+++ libwebp-1.5.0/Makefile.am 2025-01-09 11:24:17 -@@ -5,5 +5,3 @@ - if BUILD_EXTRAS - SUBDIRS += extras - endif -- --SUBDIRS += examples -diff -ur libwebp-1.5.0-orig/Makefile.in libwebp-1.5.0/Makefile.in ---- libwebp-1.5.0-orig/Makefile.in 2024-12-20 09:52:53 -+++ libwebp-1.5.0/Makefile.in 2025-01-09 11:24:17 -@@ -156,7 +156,7 @@ - unique=`for i in $$list; do \ - if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ - done | $(am__uniquify_input)` --DIST_SUBDIRS = sharpyuv src imageio man extras examples -+DIST_SUBDIRS = sharpyuv src imageio man extras - am__DIST_COMMON = $(srcdir)/Makefile.in \ - $(top_srcdir)/src/webp/config.h.in AUTHORS COPYING ChangeLog \ - NEWS README.md ar-lib compile config.guess config.sub \ -@@ -351,7 +351,7 @@ - top_srcdir = @top_srcdir@ - webp_libname_prefix = @webp_libname_prefix@ - ACLOCAL_AMFLAGS = -I m4 --SUBDIRS = sharpyuv src imageio man $(am__append_1) examples -+SUBDIRS = sharpyuv src imageio man $(am__append_1) - EXTRA_DIST = COPYING autogen.sh - all: all-recursive - diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 187d07b20c2..6b2d41a7e65 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -122,7 +122,7 @@ def cmd_msbuild( "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.3.4", "LIBPNG": "1.6.49", - "LIBWEBP": "1.5.0", + "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.3", "TIFF": "4.7.0", "XZ": "5.8.1", From 8b695cc0d36363cd853bd7d0cec7be2e31004537 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Jul 2025 22:50:05 +1000 Subject: [PATCH 1867/2374] When deleting EXIF IFD tag, clear IFD data --- Tests/test_image.py | 11 +++++++++++ src/PIL/Image.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/Tests/test_image.py b/Tests/test_image.py index 83b027aa238..e6f21c9769e 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -922,6 +922,17 @@ def test_exif_ifd(self) -> None: reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) + def test_delete_ifd_tag(self) -> None: + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + exif.get_ifd(0x8769) + assert 0x8769 in exif + del exif[0x8769] + + reloaded_exif = Image.Exif() + reloaded_exif.load(exif.tobytes()) + assert 0x8769 not in reloaded_exif + def test_exif_load_from_fp(self) -> None: with Image.open("Tests/images/flower.jpg") as im: data = im.info["exif"] diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 262b5478b62..8901b30341a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -4215,6 +4215,8 @@ def __delitem__(self, tag: int) -> None: del self._info[tag] else: del self._data[tag] + if tag in self._ifds: + del self._ifds[tag] def __iter__(self) -> Iterator[int]: keys = set(self._data) From 50dde1c125f0f3c1714c64fa6a049b1123e0a0cd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Jul 2025 23:19:16 +1000 Subject: [PATCH 1868/2374] Remove unused _save_cjpeg --- Tests/helper.py | 10 ---------- Tests/test_file_jpeg.py | 9 --------- Tests/test_shell_injection.py | 7 +------ src/PIL/JpegImagePlugin.py | 10 ---------- winbuild/build_prepare.py | 5 ++--- 5 files changed, 3 insertions(+), 38 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 34e4d6e75fb..df99f5f5571 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -291,16 +291,6 @@ def djpeg_available() -> bool: return False -def cjpeg_available() -> bool: - if shutil.which("cjpeg"): - try: - subprocess.check_call(["cjpeg", "-version"]) - return True - except subprocess.CalledProcessError: # pragma: no cover - return False - return False - - def netpbm_available() -> bool: return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 08e8798079d..51d518ae5ed 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -26,7 +26,6 @@ assert_image_equal_tofile, assert_image_similar, assert_image_similar_tofile, - cjpeg_available, djpeg_available, hopper, is_win32, @@ -731,14 +730,6 @@ def test_load_djpeg(self) -> None: img.load_djpeg() assert_image_similar_tofile(img, TEST_FILE, 5) - @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") - def test_save_cjpeg(self, tmp_path: Path) -> None: - with Image.open(TEST_FILE) as img: - tempfile = str(tmp_path / "temp.jpg") - JpegImagePlugin._save_cjpeg(img, BytesIO(), tempfile) - # Default save quality is 75%, so a tiny bit of difference is alright - assert_image_similar_tofile(img, tempfile, 17) - def test_no_duplicate_0x1001_tag(self) -> None: # Arrange tag_ids = {v: k for k, v in ExifTags.TAGS.items()} diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 03e92b5b913..38d46f312ed 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -9,7 +9,7 @@ from PIL import GifImagePlugin, Image, JpegImagePlugin -from .helper import cjpeg_available, djpeg_available, is_win32, netpbm_available +from .helper import djpeg_available, is_win32, netpbm_available TEST_JPG = "Tests/images/hopper.jpg" TEST_GIF = "Tests/images/hopper.gif" @@ -42,11 +42,6 @@ def test_load_djpeg_filename(self, tmp_path: Path) -> None: assert isinstance(im, JpegImagePlugin.JpegImageFile) im.load_djpeg() - @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") - def test_save_cjpeg_filename(self, tmp_path: Path) -> None: - with Image.open(TEST_JPG) as im: - self.assert_save_filename_check(tmp_path, im, JpegImagePlugin._save_cjpeg) - @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 082f3551a17..efe8eff3b29 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -845,16 +845,6 @@ def validate_qtables( ) -def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - # ALTERNATIVE: handle JPEGs via the IJG command line utilities. - tempfile = im._dump() - subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) - try: - os.unlink(tempfile) - except OSError: - pass - - ## # Factory for making JPEG and MPO instances def jpeg_factory( diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 187d07b20c2..84d103c086d 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -149,18 +149,17 @@ def cmd_msbuild( }, "build": [ *cmds_cmake( - ("jpeg-static", "cjpeg-static", "djpeg-static"), + ("jpeg-static", "djpeg-static"), "-DENABLE_SHARED:BOOL=FALSE", "-DWITH_JPEG8:BOOL=TRUE", "-DWITH_CRT_DLL:BOOL=TRUE", ), cmd_copy("jpeg-static.lib", "libjpeg.lib"), - cmd_copy("cjpeg-static.exe", "cjpeg.exe"), cmd_copy("djpeg-static.exe", "djpeg.exe"), ], "headers": ["jconfig.h", r"src\j*.h"], "libs": ["libjpeg.lib"], - "bins": ["cjpeg.exe", "djpeg.exe"], + "bins": ["djpeg.exe"], }, "zlib": { "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.tar.gz", From d88986a184ceb32a7eb919e3b21f950c564da35f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:53:43 +1000 Subject: [PATCH 1869/2374] Link transitive dependencies Co-authored-by: Russell Keith-Magee --- .github/workflows/wheels-dependencies.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 6d52ca98908..4296ba29290 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -280,8 +280,11 @@ function build { if [[ -n "$IS_MACOS" ]]; then webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names" fi - CFLAGS="$CFLAGS $webp_cflags" build_simple libwebp $LIBWEBP_VERSION \ - https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \ + webp_ldflags="" + if [[ -n "$IOS_SDK" ]]; then + webp_ldflags="$webp_ldflags -llzma -lz" + fi + CFLAGS="$CFLAGS $webp_cflags" LDFLAGS="$LDFLAGS $webp_ldflags" build_simple libwebp $LIBWEBP_VERSION \ --enable-libwebpmux --enable-libwebpdemux --disable-libwebpexamples build_brotli From 722c130b316443be7cc561d716711d1d39d704f7 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 11 Jul 2025 13:12:38 +1000 Subject: [PATCH 1870/2374] Restored URL Co-authored-by: Russell Keith-Magee --- .github/workflows/wheels-dependencies.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4296ba29290..e83012fd62f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -285,6 +285,7 @@ function build { webp_ldflags="$webp_ldflags -llzma -lz" fi CFLAGS="$CFLAGS $webp_cflags" LDFLAGS="$LDFLAGS $webp_ldflags" build_simple libwebp $LIBWEBP_VERSION \ + https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \ --enable-libwebpmux --enable-libwebpdemux --disable-libwebpexamples build_brotli From 985544d55715f2a5dfc539fdd09fb9bb7738694c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Jul 2025 13:28:08 +1000 Subject: [PATCH 1871/2374] Do not disable libwebpexamples --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index e83012fd62f..6b5aedb697f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -286,7 +286,7 @@ function build { fi CFLAGS="$CFLAGS $webp_cflags" LDFLAGS="$LDFLAGS $webp_ldflags" build_simple libwebp $LIBWEBP_VERSION \ https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \ - --enable-libwebpmux --enable-libwebpdemux --disable-libwebpexamples + --enable-libwebpmux --enable-libwebpdemux build_brotli From 74e36e0ee5da824132595c2c13dc1fc8416c743f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Jul 2025 16:48:46 +1000 Subject: [PATCH 1872/2374] Added RGBX and CMYK as alternatives for RGBA array data --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e512da9a192..c98630cc24f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3401,7 +3401,7 @@ def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile: ((1, 1), ">f8"): ("F", "F;64BF", []), ((1, 1, 2), "|u1"): ("LA", "LA", ["La", "PA"]), ((1, 1, 3), "|u1"): ("RGB", "RGB", ["YCbCr", "LAB", "HSV"]), - ((1, 1, 4), "|u1"): ("RGBA", "RGBA", ["RGBa"]), + ((1, 1, 4), "|u1"): ("RGBA", "RGBA", ["RGBa", "RGBX", "CMYK"]), # shortcuts: ((1, 1), f"{_ENDIAN}i4"): ("I", "I", []), ((1, 1), f"{_ENDIAN}f4"): ("F", "F", []), From 561ae3760c8a240d825598c0bd3b0365991586cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Jul 2025 17:18:47 +1000 Subject: [PATCH 1873/2374] Set correct size for rotated images after opening --- Tests/test_file_pcd.py | 15 +++++++++++++++ src/PIL/PcdImagePlugin.py | 3 +-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 81a316fc14a..9bf1a75f0f6 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -1,10 +1,15 @@ from __future__ import annotations +from io import BytesIO + +import pytest + from PIL import Image def test_load_raw() -> None: with Image.open("Tests/images/hopper.pcd") as im: + assert im.size == (768, 512) im.load() # should not segfault. # Note that this image was created with a resized hopper @@ -15,3 +20,13 @@ def test_load_raw() -> None: # target = hopper().resize((768,512)) # assert_image_similar(im, target, 10) + + +@pytest.mark.parametrize("orientation", (1, 3)) +def test_rotated(orientation: int) -> None: + with open("Tests/images/hopper.pcd", "rb") as fp: + data = bytearray(fp.read()) + data[2048 + 1538] = orientation + f = BytesIO(data) + with Image.open(f) as im: + assert im.size == (512, 768) diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 3aa249988c8..ac53f616e7d 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -46,14 +46,13 @@ def _open(self) -> None: self.tile_post_rotate = -90 self._mode = "RGB" - self._size = 768, 512 # FIXME: not correct for rotated images! + self._size = (512, 768) if orientation in (1, 3) else (768, 512) self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)] def load_end(self) -> None: if self.tile_post_rotate: # Handle rotated PCDs self.im = self.im.rotate(self.tile_post_rotate) - self._size = self.im.size # From 7328cf2e5e9da9bbf2f2b20859c09707de8b1e4d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Jul 2025 17:19:56 +1000 Subject: [PATCH 1874/2374] Reduced number of bytes read --- src/PIL/PcdImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index ac53f616e7d..7f9ab525c68 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -32,7 +32,7 @@ def _open(self) -> None: assert self.fp is not None self.fp.seek(2048) - s = self.fp.read(2048) + s = self.fp.read(1539) if not s.startswith(b"PCD_"): msg = "not a PCD file" From a8bb7579dc3dd5c24dcecc17832dc1ea5b2249a8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Jul 2025 21:06:30 +1000 Subject: [PATCH 1875/2374] Improved ImageMath test coverage --- Tests/test_imagemath_lambda_eval.py | 32 ++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagemath_lambda_eval.py b/Tests/test_imagemath_lambda_eval.py index 26c04b9a07b..ce2a32ae80e 100644 --- a/Tests/test_imagemath_lambda_eval.py +++ b/Tests/test_imagemath_lambda_eval.py @@ -2,7 +2,9 @@ from typing import Any -from PIL import Image, ImageMath +import pytest + +from PIL import Image, ImageMath, _imagingmath def pixel(im: Image.Image | int) -> str | int: @@ -498,3 +500,31 @@ def test_logical_not_equal() -> None: ) == "I 1" ) + + +def test_reflected_operands() -> None: + assert pixel(ImageMath.lambda_eval(lambda args: 1 + args["A"], **images)) == "I 2" + assert pixel(ImageMath.lambda_eval(lambda args: 1 - args["A"], **images)) == "I 0" + assert pixel(ImageMath.lambda_eval(lambda args: 1 * args["A"], **images)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: 1 / args["A"], **images)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: 1 % args["A"], **images)) == "I 0" + assert pixel(ImageMath.lambda_eval(lambda args: 1 ** args["A"], **images)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: 1 & args["A"], **images)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: 1 | args["A"], **images)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: 1 ^ args["A"], **images)) == "I 0" + + +def test_unsupported_mode() -> None: + im = Image.new("RGB", (1, 1)) + with pytest.raises(ValueError, match="unsupported mode: RGB"): + ImageMath.lambda_eval(lambda args: args["im"] + 1, im=im) + + +def test_bad_operand_type(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delattr(_imagingmath, "abs_I") + with pytest.raises(TypeError, match="bad operand type for 'abs'"): + ImageMath.lambda_eval(lambda args: abs(args["I"]), I=I) + + monkeypatch.delattr(_imagingmath, "max_F") + with pytest.raises(TypeError, match="bad operand type for 'max'"): + ImageMath.lambda_eval(lambda args: args["max"](args["I"], args["F"]), I=I, F=F) From bc2519abf10e6a2a095be82d7a268870afaaba71 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Jul 2025 22:45:22 +1000 Subject: [PATCH 1876/2374] Removed helper method _i8, unused since dump() was removed --- src/PIL/IptcImagePlugin.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index b1fbb1bf139..e5a52aa8f54 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -34,10 +34,6 @@ def _i(c: bytes) -> int: 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 # from TIFF and JPEG files, use the getiptcinfo function. From 68ac3375c68a3798d9f964a2ec704c0465ea4566 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Jul 2025 12:47:54 +1000 Subject: [PATCH 1877/2374] Codec is always "iptc" --- src/PIL/IptcImagePlugin.py | 54 ++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index e5a52aa8f54..85a13fe2cba 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -124,35 +124,33 @@ def _open(self) -> None: ] def load(self) -> Image.core.PixelAccess | None: - if len(self.tile) != 1 or self.tile[0][0] != "iptc": - return ImageFile.ImageFile.load(self) - - offset, compression = self.tile[0][2:] - - self.fp.seek(offset) - - # Copy image data to temporary file - o = BytesIO() - if compression == "raw": - # To simplify access to the extracted file, - # prepend a PPM header - o.write(b"P5\n%d %d\n255\n" % self.size) - while True: - type, size = self.field() - if type != (8, 10): - break - while size > 0: - s = self.fp.read(min(size, 8192)) - if not s: + if self.tile: + offset, compression = self.tile[0][2:] + + self.fp.seek(offset) + + # Copy image data to temporary file + o = BytesIO() + if compression == "raw": + # To simplify access to the extracted file, + # prepend a PPM header + o.write(b"P5\n%d %d\n255\n" % self.size) + while True: + type, size = self.field() + if type != (8, 10): break - o.write(s) - size -= len(s) - - with Image.open(o) as _im: - _im.load() - self.im = _im.im - self.tile = [] - return Image.Image.load(self) + while size > 0: + s = self.fp.read(min(size, 8192)) + if not s: + break + o.write(s) + size -= len(s) + + with Image.open(o) as _im: + _im.load() + self.im = _im.im + self.tile = [] + return ImageFile.ImageFile.load(self) Image.register_open(IptcImageFile.format, IptcImageFile) From cfa51ad4ada953c1a32d4cf9e1504de1cfec40b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Jul 2025 15:09:07 +1000 Subject: [PATCH 1878/2374] Populate single band --- Tests/test_file_iptc.py | 69 +++++++++++++++++++++++++++++++++++--- src/PIL/IptcImagePlugin.py | 33 +++++++++++------- 2 files changed, 85 insertions(+), 17 deletions(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 3c4c892c8a0..5a8aaa3ef32 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -2,6 +2,8 @@ from io import BytesIO +import pytest + from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags from .helper import assert_image_equal, hopper @@ -9,21 +11,78 @@ 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: expected = Image.new("L", (1, 1)) - f = BytesIO( - 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" - ) + f = create_iptc_image() 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) with Image.open(f) as im: 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: # Arrange with hopper() as im: diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 85a13fe2cba..c28f4dcc797 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -96,16 +96,18 @@ def _open(self) -> None: # mode layers = self.info[(3, 60)][0] 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: self._mode = "L" - elif layers == 3 and component: - self._mode = "RGB"[id] - elif layers == 4 and component: - self._mode = "CMYK"[id] + band = None + else: + if layers == 3 and component: + 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 self._size = self.getint((3, 20)), self.getint((3, 30)) @@ -120,14 +122,16 @@ def _open(self) -> None: # tile if tag == (8, 10): 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: if self.tile: - offset, compression = self.tile[0][2:] + args = self.tile[0].args + assert isinstance(args, tuple) + compression, band = args - self.fp.seek(offset) + self.fp.seek(self.tile[0].offset) # Copy image data to temporary file o = BytesIO() @@ -147,7 +151,12 @@ def load(self) -> Image.core.PixelAccess | None: size -= len(s) with Image.open(o) as _im: - _im.load() + 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() self.im = _im.im self.tile = [] return ImageFile.ImageFile.load(self) From 6fdbf5433108454f8085b5d01c803367f97535a7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Jul 2025 19:50:19 +1000 Subject: [PATCH 1879/2374] Width and height are unsigned --- src/PIL/GbrImagePlugin.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index f319d7e846e..d69295363f3 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -54,7 +54,7 @@ def _open(self) -> None: width = i32(self.fp.read(4)) height = i32(self.fp.read(4)) color_depth = i32(self.fp.read(4)) - if width <= 0 or height <= 0: + if width == 0 or height == 0: msg = "not a GIMP brush" raise SyntaxError(msg) if color_depth not in (1, 4): @@ -71,7 +71,7 @@ def _open(self) -> None: raise SyntaxError(msg) self.info["spacing"] = i32(self.fp.read(4)) - comment = self.fp.read(comment_length)[:-1] + self.info["comment"] = self.fp.read(comment_length)[:-1] if color_depth == 1: self._mode = "L" @@ -80,8 +80,6 @@ def _open(self) -> None: self._size = width, height - self.info["comment"] = comment - # Image might not be small Image._decompression_bomb_check(self.size) From 4adff39bfd7a04c875b41bd879f513cde09c4604 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Jul 2025 19:55:58 +1000 Subject: [PATCH 1880/2374] Improved test coverage --- Tests/test_file_gbr.py | 49 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index 1b834cd3c73..b8851d82beb 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -1,8 +1,10 @@ from __future__ import annotations +from io import BytesIO + import pytest -from PIL import GbrImagePlugin, Image +from PIL import GbrImagePlugin, Image, _binary from .helper import assert_image_equal_tofile @@ -31,8 +33,49 @@ def test_multiple_load_operations() -> None: assert_image_equal_tofile(im, "Tests/images/gbr.png") +def create_gbr_image(info: dict[str, int] = {}, magic_number=b"") -> BytesIO: + return BytesIO( + b"".join( + _binary.o32be(i) + for i in [ + info.get("header_size", 20), + info.get("version", 1), + info.get("width", 1), + info.get("height", 1), + info.get("color_depth", 1), + ] + ) + + magic_number + ) + + def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" + 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) - with pytest.raises(SyntaxError): + invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError, match="Unsupported GIMP brush version"): GbrImagePlugin.GbrImageFile(invalid_file) + + +def test_unsupported_gimp_brush() -> None: + f = create_gbr_image({"color_depth": 2}) + with pytest.raises(SyntaxError, match="Unsupported GIMP brush color depth: 2"): + GbrImagePlugin.GbrImageFile(f) + + +def test_bad_magic_number() -> None: + f = create_gbr_image({"version": 2}, magic_number=b"badm") + with pytest.raises(SyntaxError, match="not a GIMP brush, bad magic number"): + GbrImagePlugin.GbrImageFile(f) + + +def test_L() -> None: + f = create_gbr_image() + with Image.open(f) as im: + assert im.mode == "L" From d85fa7a2471b16bc547a46542f18f218b19a1c6b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 13 Jul 2025 16:13:44 +1000 Subject: [PATCH 1881/2374] Improved WmfImagePlugin test coverage --- Tests/test_file_wmf.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index dcf5f000ffe..906080d15a5 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -44,6 +44,18 @@ def test_load_zero_inch() -> None: pass +def test_load_unsupported_wmf() -> None: + b = BytesIO(b"\xd7\xcd\xc6\x9a\x00\x00" + b"\x01" * 10) + with pytest.raises(SyntaxError, match="Unsupported WMF file format"): + WmfImagePlugin.WmfStubImageFile(b) + + +def test_load_unsupported() -> None: + b = BytesIO(b"\x01\x00\x00\x00") + with pytest.raises(SyntaxError, match="Unsupported file format"): + WmfImagePlugin.WmfStubImageFile(b) + + def test_render() -> None: with open("Tests/images/drawing.emf", "rb") as fp: data = fp.read() From 5ce88dbe53a3d46e20d464fbd6ef06031645bfda Mon Sep 17 00:00:00 2001 From: GUO YANKE Date: Mon, 7 Jul 2025 13:57:11 +0800 Subject: [PATCH 1882/2374] feat(ImageGrab): enhance grab function to support window-based screenshot capturing on macOS --- src/PIL/ImageGrab.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 1eb4507344c..ba2c9b1415c 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -43,7 +43,10 @@ def grab( fh, filepath = tempfile.mkstemp(".png") os.close(fh) args = ["screencapture"] - if bbox: + if window: + args += ["-l", str(window)] + # -R is not working with -l + if bbox and not window: left, top, right, bottom = bbox args += ["-R", f"{left},{top},{right-left},{bottom-top}"] subprocess.call(args + ["-x", filepath]) @@ -51,9 +54,16 @@ def grab( im.load() os.unlink(filepath) if bbox: - im_resized = im.resize((right - left, bottom - top)) - im.close() - return im_resized + # manual crop for windowed mode + if window: + left, top, right, bottom = bbox + im_cropped = im.crop((left, top, right, bottom)) + im.close() + return im_cropped + else: + im_resized = im.resize((right - left, bottom - top)) + im.close() + return im_resized return im elif sys.platform == "win32": if window is not None: From 1f7e9c3b51db100de0164eab45ca16ec4e24bd78 Mon Sep 17 00:00:00 2001 From: Yan-Ke Guo Date: Mon, 7 Jul 2025 16:29:38 +0800 Subject: [PATCH 1883/2374] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageGrab.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index ba2c9b1415c..c188745815d 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -56,8 +56,7 @@ def grab( if bbox: # manual crop for windowed mode if window: - left, top, right, bottom = bbox - im_cropped = im.crop((left, top, right, bottom)) + im_cropped = im.crop(bbox) im.close() return im_cropped else: From 7eaac3fcf0cd0fa54ba91784727f1d1a7654b31b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 7 Jul 2025 18:13:07 +1000 Subject: [PATCH 1884/2374] Updated documentation --- docs/reference/ImageGrab.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index f6a2ec5bc03..25afc99266c 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -42,9 +42,9 @@ or the clipboard to a PIL image memory. .. versionadded:: 7.1.0 :param window: - HWND, to capture a single window. Windows only. + Capture a single window. On Windows, this is a HWND. On macOS, it uses windowid. - .. versionadded:: 11.2.1 + .. versionadded:: 11.2.1 (Windows), 12.0.0 (macOS) :return: An image .. py:function:: grabclipboard() From 79914ec8a57e1beeb2a5c62809044c116828962b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Jul 2025 20:00:20 +1000 Subject: [PATCH 1885/2374] Check for scaling in macOS windows --- src/PIL/ImageGrab.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index c188745815d..b82a2ff3a47 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -45,8 +45,7 @@ def grab( args = ["screencapture"] if window: args += ["-l", str(window)] - # -R is not working with -l - if bbox and not window: + elif bbox: left, top, right, bottom = bbox args += ["-R", f"{left},{top},{right-left},{bottom-top}"] subprocess.call(args + ["-x", filepath]) @@ -54,9 +53,29 @@ def grab( im.load() os.unlink(filepath) if bbox: - # manual crop for windowed mode if window: - im_cropped = im.crop(bbox) + # Determine if the window was in retina mode or not + # by capturing it without the shadow, + # and checking how different the width is + fh, filepath = tempfile.mkstemp(".png") + os.close(fh) + subprocess.call( + ["screencapture", "-l", str(window), "-o", "-x", filepath] + ) + with Image.open(filepath) as im_no_shadow: + retina = im.width - im_no_shadow.width > 100 + os.unlink(filepath) + + # Since screencapture's -R does not work with -l, + # crop the image manually + if retina: + left, top, right, bottom = bbox + im_cropped = im.resize( + (right - left, bottom - top), + box=tuple(coord * 2 for coord in bbox), + ) + else: + im_cropped = im.crop(bbox) im.close() return im_cropped else: From 7516805121cc1da1d00cd6f0a22f64e0232c3541 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 14 Jul 2025 19:29:27 +1000 Subject: [PATCH 1886/2374] Improved DDS test coverage --- Tests/images/unimplemented_pixel_format.dds | Bin 0 -> 132 bytes Tests/test_file_dds.py | 19 +++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 Tests/images/unimplemented_pixel_format.dds diff --git a/Tests/images/unimplemented_pixel_format.dds b/Tests/images/unimplemented_pixel_format.dds new file mode 100644 index 0000000000000000000000000000000000000000..9092df8b1b5acda7e115b9ceaf6241d0f294dd1b GIT binary patch literal 132 rcmZ>930A0KU|?Vu;9_841TsJvNWhsOE|EY1sE!4nS^-SSSfCI9+g<`M literal 0 HcmV?d00001 diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 5c7a943b1b7..116dfa59cec 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -380,21 +380,28 @@ def test_palette() -> None: assert_image_equal_tofile(im, "Tests/images/transparent.gif") +def test_unsupported_header_size() -> None: + with pytest.raises(OSError, match="Unsupported header size 0"): + with Image.open(BytesIO(b"DDS " + b"\x00" * 4)): + pass + + def test_unsupported_bitcount() -> None: - with pytest.raises(OSError): + with pytest.raises(OSError, match="Unsupported bitcount 24 for 131072"): with Image.open("Tests/images/unsupported_bitcount.dds"): pass @pytest.mark.parametrize( - "test_file", + "test_file, message", ( - "Tests/images/unimplemented_dxgi_format.dds", - "Tests/images/unimplemented_pfflags.dds", + ("Tests/images/unimplemented_dxgi_format.dds", "Unimplemented DXGI format 93"), + ("Tests/images/unimplemented_pixel_format.dds", "Unimplemented pixel format 0"), + ("Tests/images/unimplemented_pfflags.dds", "Unknown pixel format flags 8"), ), ) -def test_not_implemented(test_file: str) -> None: - with pytest.raises(NotImplementedError): +def test_not_implemented(test_file: str, message: str) -> None: + with pytest.raises(NotImplementedError, match=message): with Image.open(test_file): pass From 638eb1b9992804ca21577b5933d476b7bdbeb5d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 13:23:40 +1000 Subject: [PATCH 1887/2374] Update dependency mypy to v1.17.0 (#9092) --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 44b5badabf1..e81f527b86f 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.16.1 +mypy==1.17.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 91bbeb5dcb47ce6d3b5b1c9969c982910ebee56b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Jul 2025 13:54:13 +1000 Subject: [PATCH 1888/2374] Revert iOS change until the test runs again --- Tests/test_pyroma.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index a161d3f05c4..c2f7fe22ecb 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,7 +1,5 @@ from __future__ import annotations -from importlib.metadata import metadata - import pytest from PIL import __version__ @@ -11,7 +9,7 @@ def test_pyroma() -> None: # Arrange - data = pyroma.projectdata.map_metadata_keys(metadata("Pillow")) + data = pyroma.projectdata.get_data(".") # Act rating = pyroma.ratings.rate(data) From a426eb55afcb9e8a069d6aba21405c0d89f69bec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Jul 2025 13:40:22 +1000 Subject: [PATCH 1889/2374] Remove file after test completion --- Tests/test_image_access.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index b3de5c13d3e..2609b1e342a 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -276,10 +276,11 @@ def test_embeddable(self) -> None: except Exception: pytest.skip("Compiler could not be initialized") - with open("embed_pil.c", "w", encoding="utf-8") as fh: - home = sys.prefix.replace("\\", "\\\\") - fh.write( - f""" + try: + with open("embed_pil.c", "w", encoding="utf-8") as fh: + home = sys.prefix.replace("\\", "\\\\") + fh.write( + f""" #include "Python.h" int main(int argc, char* argv[]) @@ -301,17 +302,19 @@ def test_embeddable(self) -> None: return 0; }} """ - ) + ) - objects = compiler.compile(["embed_pil.c"]) - compiler.link_executable(objects, "embed_pil") + objects = compiler.compile(["embed_pil.c"]) + compiler.link_executable(objects, "embed_pil") - env = os.environ.copy() - env["PATH"] = sys.prefix + ";" + env["PATH"] + env = os.environ.copy() + env["PATH"] = sys.prefix + ";" + env["PATH"] - # Do not display the Windows Error Reporting dialog - getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) + # Do not display the Windows Error Reporting dialog + getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) - process = subprocess.Popen(["embed_pil.exe"], env=env) - process.communicate() - assert process.returncode == 0 + process = subprocess.Popen(["embed_pil.exe"], env=env) + process.communicate() + assert process.returncode == 0 + finally: + os.remove("embed_pil.c") From a39d14648bdbd6638e7097167c4b8c2964ce3752 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Jul 2025 13:39:19 +1000 Subject: [PATCH 1890/2374] Updated manifest --- MANIFEST.in | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 95a6b1b9297..6623f227d60 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,6 +13,7 @@ include LICENSE include Makefile include tox.ini graft Tests +graft Tests/images graft checks graft patches graft src @@ -28,8 +29,19 @@ exclude .editorconfig exclude .readthedocs.yml exclude codecov.yml exclude renovate.json +exclude Tests/images/README.md +exclude Tests/images/crash*.tif +exclude Tests/images/string_dimension.tiff global-exclude .git* global-exclude *.pyc global-exclude *.so prune .ci prune wheels +prune winbuild/build +prune winbuild/depends +prune Tests/errors +prune Tests/images/jpeg2000 +prune Tests/images/msp +prune Tests/images/picins +prune Tests/images/sunraster +prune Tests/test-images From cd93629a5c23a20c9a7dc13d13236e164bf2909f Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 20 Apr 2024 16:40:43 -0500 Subject: [PATCH 1891/2374] use a struct for mode names instead of just a string --- setup.py | 1 + src/libImaging/Imaging.h | 23 ++++---- src/libImaging/Mode.c | 115 +++++++++++++++++++++++++++++++++++++++ src/libImaging/Mode.h | 60 ++++++++++++++++++++ 4 files changed, 186 insertions(+), 13 deletions(-) create mode 100644 src/libImaging/Mode.c create mode 100644 src/libImaging/Mode.h diff --git a/setup.py b/setup.py index df584f8df63..93b5bcc7812 100644 --- a/setup.py +++ b/setup.py @@ -103,6 +103,7 @@ def get_version() -> str: "JpegDecode", "JpegEncode", "Matrix", + "Mode", "ModeFilter", "Negative", "Offset", diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index bfe67d46213..1eaabd8e561 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -11,6 +11,7 @@ */ #include "ImPlatform.h" +#include "Mode.h" #if defined(__cplusplus) extern "C" { @@ -71,9 +72,6 @@ typedef struct ImagingPaletteInstance *ImagingPalette; #define IMAGING_TYPE_FLOAT32 2 #define IMAGING_TYPE_SPECIAL 3 /* check mode for details */ -#define IMAGING_MODE_LENGTH \ - 6 + 1 /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK", "YCbCr", "BGR;xy") */ - typedef struct { char *ptr; int size; @@ -81,12 +79,11 @@ typedef struct { struct ImagingMemoryInstance { /* Format */ - char mode[IMAGING_MODE_LENGTH]; /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK", - "YCbCr", "BGR;xy") */ - int type; /* Data type (IMAGING_TYPE_*) */ - int depth; /* Depth (ignored in this version) */ - int bands; /* Number of bands (1, 2, 3, or 4) */ - int xsize; /* Image dimension. */ + const Mode *mode; /* Image mode (IMAGING_MODE_*) */ + int type; /* Data type (IMAGING_TYPE_*) */ + int depth; /* Depth (ignored in this version) */ + int bands; /* Number of bands (1, 2, 3, or 4) */ + int xsize; /* Image dimension. */ int ysize; /* Colour palette (for "P" images only) */ @@ -140,15 +137,15 @@ struct ImagingMemoryInstance { #define IMAGING_PIXEL_FLOAT32(im, x, y) (((FLOAT32 *)(im)->image32[y])[x]) struct ImagingAccessInstance { - const char *mode; + const Mode *mode; void (*get_pixel)(Imaging im, int x, int y, void *pixel); void (*put_pixel)(Imaging im, int x, int y, const void *pixel); }; struct ImagingHistogramInstance { /* Format */ - char mode[IMAGING_MODE_LENGTH]; /* Band names (of corresponding source image) */ - int bands; /* Number of bands (1, 3, or 4) */ + const Mode *mode; /* Mode of corresponding source image */ + int bands; /* Number of bands (1, 3, or 4) */ /* Data */ long *histogram; /* Histogram (bands*256 longs) */ @@ -156,7 +153,7 @@ struct ImagingHistogramInstance { struct ImagingPaletteInstance { /* Format */ - char mode[IMAGING_MODE_LENGTH]; /* Band names */ + const Mode *mode; /* Data */ int size; diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c new file mode 100644 index 00000000000..5b9c9dae199 --- /dev/null +++ b/src/libImaging/Mode.c @@ -0,0 +1,115 @@ +#include "Mode.h" +#include + + +#define CREATE_MODE(TYPE, NAME, INIT) \ +const TYPE IMAGING_##NAME##_VAL = INIT;\ +const TYPE * const IMAGING_##NAME = &IMAGING_##NAME##_VAL; + + +CREATE_MODE(Mode, MODE_1, {"1"}) +CREATE_MODE(Mode, MODE_CMYK, {"CMYK"}) +CREATE_MODE(Mode, MODE_F, {"F"}) +CREATE_MODE(Mode, MODE_HSV, {"HSV"}) +CREATE_MODE(Mode, MODE_I, {"I"}) +CREATE_MODE(Mode, MODE_L, {"L"}) +CREATE_MODE(Mode, MODE_LA, {"LA"}) +CREATE_MODE(Mode, MODE_LAB, {"LAB"}) +CREATE_MODE(Mode, MODE_La, {"La"}) +CREATE_MODE(Mode, MODE_P, {"P"}) +CREATE_MODE(Mode, MODE_PA, {"PA"}) +CREATE_MODE(Mode, MODE_RGB, {"RGB"}) +CREATE_MODE(Mode, MODE_RGBA, {"RGBA"}) +CREATE_MODE(Mode, MODE_RGBX, {"RGBX"}) +CREATE_MODE(Mode, MODE_RGBa, {"RGBa"}) +CREATE_MODE(Mode, MODE_YCbCr, {"YCbCr"}) + +const Mode * const MODES[] = { + IMAGING_MODE_1, + IMAGING_MODE_CMYK, + IMAGING_MODE_F, + IMAGING_MODE_HSV, + IMAGING_MODE_I, + IMAGING_MODE_L, + IMAGING_MODE_LA, + IMAGING_MODE_LAB, + IMAGING_MODE_La, + IMAGING_MODE_P, + IMAGING_MODE_PA, + IMAGING_MODE_RGB, + IMAGING_MODE_RGBA, + IMAGING_MODE_RGBX, + IMAGING_MODE_RGBa, + IMAGING_MODE_YCbCr, + NULL +}; + +const Mode * findMode(const char * const name) { + int i = 0; + const Mode * mode; + while ((mode = MODES[i++]) != NULL) { + if (!strcmp(mode->name, name)) { + return mode; + } + } + return NULL; +} + + +// Alias all of the modes as rawmodes so that the addresses are the same. +#define ALIAS_MODE_AS_RAWMODE(NAME) const RawMode * const IMAGING_RAWMODE_##NAME = (const RawMode * const)IMAGING_MODE_##NAME; +ALIAS_MODE_AS_RAWMODE(1) +ALIAS_MODE_AS_RAWMODE(CMYK) +ALIAS_MODE_AS_RAWMODE(F) +ALIAS_MODE_AS_RAWMODE(HSV) +ALIAS_MODE_AS_RAWMODE(I) +ALIAS_MODE_AS_RAWMODE(L) +ALIAS_MODE_AS_RAWMODE(LA) +ALIAS_MODE_AS_RAWMODE(LAB) +ALIAS_MODE_AS_RAWMODE(La) +ALIAS_MODE_AS_RAWMODE(P) +ALIAS_MODE_AS_RAWMODE(PA) +ALIAS_MODE_AS_RAWMODE(RGB) +ALIAS_MODE_AS_RAWMODE(RGBA) +ALIAS_MODE_AS_RAWMODE(RGBX) +ALIAS_MODE_AS_RAWMODE(RGBa) +ALIAS_MODE_AS_RAWMODE(YCbCr) + +CREATE_MODE(RawMode, RAWMODE_BGR_15, {"BGR;15"}) +CREATE_MODE(RawMode, RAWMODE_BGR_16, {"BGR;16"}) + +const RawMode * const RAWMODES[] = { + IMAGING_RAWMODE_1, + IMAGING_RAWMODE_CMYK, + IMAGING_RAWMODE_F, + IMAGING_RAWMODE_HSV, + IMAGING_RAWMODE_I, + IMAGING_RAWMODE_L, + IMAGING_RAWMODE_LA, + IMAGING_RAWMODE_LAB, + IMAGING_RAWMODE_La, + IMAGING_RAWMODE_P, + IMAGING_RAWMODE_PA, + IMAGING_RAWMODE_RGB, + IMAGING_RAWMODE_RGBA, + IMAGING_RAWMODE_RGBX, + IMAGING_RAWMODE_RGBa, + IMAGING_RAWMODE_YCbCr, + + IMAGING_RAWMODE_BGR_15, + IMAGING_RAWMODE_BGR_16, + + NULL +}; + +const RawMode * findRawMode(const char * const name) { + int i = 0; + const RawMode * rawmode; + while ((rawmode = RAWMODES[i++]) != NULL) { + const RawMode * const rawmode = RAWMODES[i]; + if (!strcmp(rawmode->name, name)) { + return rawmode; + } + } + return NULL; +} diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h new file mode 100644 index 00000000000..2d4d27c3170 --- /dev/null +++ b/src/libImaging/Mode.h @@ -0,0 +1,60 @@ +#ifndef __MODE_H__ +#define __MODE_H__ + + +// Maximum length (including null terminator) for both mode and rawmode names. +#define IMAGING_MODE_LENGTH 6+1 + + +typedef struct { + const char * const name; +} Mode; + +extern const Mode * const IMAGING_MODE_1; +extern const Mode * const IMAGING_MODE_CMYK; +extern const Mode * const IMAGING_MODE_F; +extern const Mode * const IMAGING_MODE_HSV; +extern const Mode * const IMAGING_MODE_I; +extern const Mode * const IMAGING_MODE_L; +extern const Mode * const IMAGING_MODE_LA; +extern const Mode * const IMAGING_MODE_LAB; +extern const Mode * const IMAGING_MODE_La; +extern const Mode * const IMAGING_MODE_P; +extern const Mode * const IMAGING_MODE_PA; +extern const Mode * const IMAGING_MODE_RGB; +extern const Mode * const IMAGING_MODE_RGBA; +extern const Mode * const IMAGING_MODE_RGBX; +extern const Mode * const IMAGING_MODE_RGBa; +extern const Mode * const IMAGING_MODE_YCbCr; + +const Mode * findMode(const char * const name); + + +typedef struct { + const char * const name; +} RawMode; + +extern const RawMode * const IMAGING_RAWMODE_1; +extern const RawMode * const IMAGING_RAWMODE_CMYK; +extern const RawMode * const IMAGING_RAWMODE_F; +extern const RawMode * const IMAGING_RAWMODE_HSV; +extern const RawMode * const IMAGING_RAWMODE_I; +extern const RawMode * const IMAGING_RAWMODE_L; +extern const RawMode * const IMAGING_RAWMODE_LA; +extern const RawMode * const IMAGING_RAWMODE_LAB; +extern const RawMode * const IMAGING_RAWMODE_La; +extern const RawMode * const IMAGING_RAWMODE_P; +extern const RawMode * const IMAGING_RAWMODE_PA; +extern const RawMode * const IMAGING_RAWMODE_RGB; +extern const RawMode * const IMAGING_RAWMODE_RGBA; +extern const RawMode * const IMAGING_RAWMODE_RGBX; +extern const RawMode * const IMAGING_RAWMODE_RGBa; +extern const RawMode * const IMAGING_RAWMODE_YCbCr; + +extern const RawMode * const IMAGING_RAWMODE_BGR_15; +extern const RawMode * const IMAGING_RAWMODE_BGR_16; + +const RawMode * findRawMode(const char * const name); + + +#endif // __MODE_H__ From 63a45ad8d0d876cd2c32c43ff1641a93db7307b5 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 20 Apr 2024 20:11:17 -0500 Subject: [PATCH 1892/2374] add special modes --- src/libImaging/Mode.c | 35 +++++++++++++++++++++++++++++++++-- src/libImaging/Mode.h | 16 ++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c index 5b9c9dae199..2bd09bda56a 100644 --- a/src/libImaging/Mode.c +++ b/src/libImaging/Mode.c @@ -24,6 +24,15 @@ CREATE_MODE(Mode, MODE_RGBX, {"RGBX"}) CREATE_MODE(Mode, MODE_RGBa, {"RGBa"}) CREATE_MODE(Mode, MODE_YCbCr, {"YCbCr"}) +CREATE_MODE(Mode, MODE_BGR_15, {"BGR;15"}) +CREATE_MODE(Mode, MODE_BGR_16, {"BGR;16"}) +CREATE_MODE(Mode, MODE_BGR_24, {"BGR;24"}) + +CREATE_MODE(Mode, MODE_I_16, {"I;16"}) +CREATE_MODE(Mode, MODE_I_16L, {"I;16L"}) +CREATE_MODE(Mode, MODE_I_16B, {"I;16B"}) +CREATE_MODE(Mode, MODE_I_16N, {"I;16N"}) + const Mode * const MODES[] = { IMAGING_MODE_1, IMAGING_MODE_CMYK, @@ -41,6 +50,16 @@ const Mode * const MODES[] = { IMAGING_MODE_RGBX, IMAGING_MODE_RGBa, IMAGING_MODE_YCbCr, + + IMAGING_MODE_BGR_15, + IMAGING_MODE_BGR_16, + IMAGING_MODE_BGR_24, + + IMAGING_MODE_I_16, + IMAGING_MODE_I_16L, + IMAGING_MODE_I_16B, + IMAGING_MODE_I_16N, + NULL }; @@ -75,8 +94,14 @@ ALIAS_MODE_AS_RAWMODE(RGBX) ALIAS_MODE_AS_RAWMODE(RGBa) ALIAS_MODE_AS_RAWMODE(YCbCr) -CREATE_MODE(RawMode, RAWMODE_BGR_15, {"BGR;15"}) -CREATE_MODE(RawMode, RAWMODE_BGR_16, {"BGR;16"}) +ALIAS_MODE_AS_RAWMODE(BGR_15) +ALIAS_MODE_AS_RAWMODE(BGR_16) +ALIAS_MODE_AS_RAWMODE(BGR_24) + +ALIAS_MODE_AS_RAWMODE(I_16) +ALIAS_MODE_AS_RAWMODE(I_16L) +ALIAS_MODE_AS_RAWMODE(I_16B) +ALIAS_MODE_AS_RAWMODE(I_16N) const RawMode * const RAWMODES[] = { IMAGING_RAWMODE_1, @@ -98,6 +123,12 @@ const RawMode * const RAWMODES[] = { IMAGING_RAWMODE_BGR_15, IMAGING_RAWMODE_BGR_16, + IMAGING_RAWMODE_BGR_24, + + IMAGING_RAWMODE_I_16, + IMAGING_RAWMODE_I_16L, + IMAGING_RAWMODE_I_16B, + IMAGING_RAWMODE_I_16N, NULL }; diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index 2d4d27c3170..6491beb81b7 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -27,6 +27,15 @@ extern const Mode * const IMAGING_MODE_RGBX; extern const Mode * const IMAGING_MODE_RGBa; extern const Mode * const IMAGING_MODE_YCbCr; +extern const Mode * const IMAGING_MODE_BGR_15; +extern const Mode * const IMAGING_MODE_BGR_16; +extern const Mode * const IMAGING_MODE_BGR_24; + +extern const Mode * const IMAGING_MODE_I_16; +extern const Mode * const IMAGING_MODE_I_16L; +extern const Mode * const IMAGING_MODE_I_16B; +extern const Mode * const IMAGING_MODE_I_16N; + const Mode * findMode(const char * const name); @@ -53,6 +62,13 @@ extern const RawMode * const IMAGING_RAWMODE_YCbCr; extern const RawMode * const IMAGING_RAWMODE_BGR_15; extern const RawMode * const IMAGING_RAWMODE_BGR_16; +extern const RawMode * const IMAGING_RAWMODE_BGR_24; +extern const RawMode * const IMAGING_RAWMODE_BGR_32; + +extern const RawMode * const IMAGING_RAWMODE_I_16; +extern const RawMode * const IMAGING_RAWMODE_I_16L; +extern const RawMode * const IMAGING_RAWMODE_I_16B; +extern const RawMode * const IMAGING_RAWMODE_I_16N; const RawMode * findRawMode(const char * const name); From 12409e4574ef1eb2df52b286a46912268a0e1de5 Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 14:44:41 +0200 Subject: [PATCH 1893/2374] use mode structs in _imaging.c --- src/_imaging.c | 178 +++++++++++++++++++++++++++------------ src/libImaging/Imaging.h | 34 ++++---- 2 files changed, 139 insertions(+), 73 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index fbfc0e41ae2..d0648540aeb 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -368,7 +368,7 @@ ImagingError_ValueError(const char *message) { /* -------------------------------------------------------------------- */ static int -getbands(const char *mode) { +getbands(const Mode *mode) { Imaging im; int bands; @@ -662,7 +662,11 @@ getink(PyObject *color, Imaging im, char *ink) { memcpy(ink, &ftmp, sizeof(ftmp)); return ink; case IMAGING_TYPE_SPECIAL: - if (strncmp(im->mode, "I;16", 4) == 0) { + if (im->mode == IMAGING_MODE_I_16 + || im->mode == IMAGING_MODE_I_16L + || im->mode == IMAGING_MODE_I_16B + || im->mode == IMAGING_MODE_I_16N + ) { ink[0] = (UINT8)r; ink[1] = (UINT8)(r >> 8); ink[2] = ink[3] = 0; @@ -681,6 +685,30 @@ getink(PyObject *color, Imaging im, char *ink) { } else if (!PyArg_ParseTuple(color, "iiL", &b, &g, &r)) { return NULL; } + if (im->mode == IMAGING_MODE_BGR_15) { + UINT16 v = ((((UINT16)r) << 7) & 0x7c00) + + ((((UINT16)g) << 2) & 0x03e0) + + ((((UINT16)b) >> 3) & 0x001f); + + ink[0] = (UINT8)v; + ink[1] = (UINT8)(v >> 8); + ink[2] = ink[3] = 0; + return ink; + } else if (im->mode == IMAGING_MODE_BGR_16) { + UINT16 v = ((((UINT16)r) << 8) & 0xf800) + + ((((UINT16)g) << 3) & 0x07e0) + + ((((UINT16)b) >> 3) & 0x001f); + ink[0] = (UINT8)v; + ink[1] = (UINT8)(v >> 8); + ink[2] = ink[3] = 0; + return ink; + } else if (im->mode == IMAGING_MODE_BGR_24) { + ink[0] = (UINT8)b; + ink[1] = (UINT8)g; + ink[2] = (UINT8)r; + ink[3] = 0; + return ink; + } } } @@ -694,7 +722,7 @@ getink(PyObject *color, Imaging im, char *ink) { static PyObject * _fill(PyObject *self, PyObject *args) { - char *mode; + char *mode_name; int xsize, ysize; PyObject *color; char buffer[4]; @@ -703,10 +731,12 @@ _fill(PyObject *self, PyObject *args) { xsize = ysize = 256; color = NULL; - if (!PyArg_ParseTuple(args, "s|(ii)O", &mode, &xsize, &ysize, &color)) { + if (!PyArg_ParseTuple(args, "s|(ii)O", &mode_name, &xsize, &ysize, &color)) { return NULL; } + const Mode * const mode = findMode(mode_name); + im = ImagingNewDirty(mode, xsize, ysize); if (!im) { return NULL; @@ -727,47 +757,55 @@ _fill(PyObject *self, PyObject *args) { static PyObject * _new(PyObject *self, PyObject *args) { - char *mode; + char *mode_name; int xsize, ysize; - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { + if (!PyArg_ParseTuple(args, "s(ii)", &mode_name, &xsize, &ysize)) { return NULL; } + const Mode * const mode = findMode(mode_name); + return PyImagingNew(ImagingNew(mode, xsize, ysize)); } static PyObject * _new_block(PyObject *self, PyObject *args) { - char *mode; + char *mode_name; int xsize, ysize; - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { + if (!PyArg_ParseTuple(args, "s(ii)", &mode_name, &xsize, &ysize)) { return NULL; } + const Mode * const mode = findMode(mode_name); + return PyImagingNew(ImagingNewBlock(mode, xsize, ysize)); } static PyObject * _linear_gradient(PyObject *self, PyObject *args) { - char *mode; + char *mode_name; - if (!PyArg_ParseTuple(args, "s", &mode)) { + if (!PyArg_ParseTuple(args, "s", &mode_name)) { return NULL; } + const Mode * const mode = findMode(mode_name); + return PyImagingNew(ImagingFillLinearGradient(mode)); } static PyObject * _radial_gradient(PyObject *self, PyObject *args) { - char *mode; + char *mode_name; - if (!PyArg_ParseTuple(args, "s", &mode)) { + if (!PyArg_ParseTuple(args, "s", &mode_name)) { return NULL; } + const Mode * const mode = findMode(mode_name); + return PyImagingNew(ImagingFillRadialGradient(mode)); } @@ -907,7 +945,7 @@ _prepare_lut_table(PyObject *table, Py_ssize_t table_size) { static PyObject * _color_lut_3d(ImagingObject *self, PyObject *args) { - char *mode; + char *mode_name; int filter; int table_channels; int size1D, size2D, size3D; @@ -919,7 +957,7 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, "sii(iii)O:color_lut_3d", - &mode, + &mode_name, &filter, &table_channels, &size1D, @@ -930,6 +968,8 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { return NULL; } + const Mode * const mode = findMode(mode_name); + /* actually, it is trilinear */ if (filter != IMAGING_TRANSFORM_BILINEAR) { PyErr_SetString(PyExc_ValueError, "Only LINEAR filter is supported."); @@ -976,11 +1016,11 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { static PyObject * _convert(ImagingObject *self, PyObject *args) { - char *mode; + char *mode_name; int dither = 0; ImagingObject *paletteimage = NULL; - if (!PyArg_ParseTuple(args, "s|iO", &mode, &dither, &paletteimage)) { + if (!PyArg_ParseTuple(args, "s|iO", &mode_name, &dither, &paletteimage)) { return NULL; } if (paletteimage != NULL) { @@ -997,6 +1037,8 @@ _convert(ImagingObject *self, PyObject *args) { } } + const Mode * const mode = findMode(mode_name); + return PyImagingNew(ImagingConvert( self->image, mode, paletteimage ? paletteimage->image->palette : NULL, dither )); @@ -1021,14 +1063,14 @@ _convert2(ImagingObject *self, PyObject *args) { static PyObject * _convert_matrix(ImagingObject *self, PyObject *args) { - char *mode; + char *mode_name; float m[12]; - if (!PyArg_ParseTuple(args, "s(ffff)", &mode, m + 0, m + 1, m + 2, m + 3)) { + if (!PyArg_ParseTuple(args, "s(ffff)", &mode_name, m + 0, m + 1, m + 2, m + 3)) { PyErr_Clear(); if (!PyArg_ParseTuple( args, "s(ffffffffffff)", - &mode, + &mode_name, m + 0, m + 1, m + 2, @@ -1046,18 +1088,22 @@ _convert_matrix(ImagingObject *self, PyObject *args) { } } + const Mode * const mode = findMode(mode_name); + return PyImagingNew(ImagingConvertMatrix(self->image, mode, m)); } static PyObject * _convert_transparent(ImagingObject *self, PyObject *args) { - char *mode; + char *mode_name; int r, g, b; - if (PyArg_ParseTuple(args, "s(iii)", &mode, &r, &g, &b)) { + if (PyArg_ParseTuple(args, "s(iii)", &mode_name, &r, &g, &b)) { + const Mode * const mode = findMode(mode_name); return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, g, b)); } PyErr_Clear(); - if (PyArg_ParseTuple(args, "si", &mode, &r)) { + if (PyArg_ParseTuple(args, "si", &mode_name, &r)) { + const Mode * const mode = findMode(mode_name); return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, 0, 0)); } return NULL; @@ -1156,9 +1202,9 @@ _getpalette(ImagingObject *self, PyObject *args) { int bits; ImagingShuffler pack; - char *mode = "RGB"; - char *rawmode = "RGB"; - if (!PyArg_ParseTuple(args, "|ss", &mode, &rawmode)) { + char *mode_name = "RGB"; + char *rawmode_name = "RGB"; + if (!PyArg_ParseTuple(args, "|ss", &mode_name, &rawmode_name)) { return NULL; } @@ -1167,6 +1213,9 @@ _getpalette(ImagingObject *self, PyObject *args) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + pack = ImagingFindPacker(mode, rawmode, &bits); if (!pack) { PyErr_SetString(PyExc_ValueError, wrong_raw_mode); @@ -1193,7 +1242,7 @@ _getpalettemode(ImagingObject *self) { return NULL; } - return PyUnicode_FromString(self->image->palette->mode); + return PyUnicode_FromString(self->image->palette->mode->name); } static inline int @@ -1474,12 +1523,14 @@ _point(ImagingObject *self, PyObject *args) { Imaging im; PyObject *list; - char *mode; - if (!PyArg_ParseTuple(args, "Oz", &list, &mode)) { + char *mode_name; + if (!PyArg_ParseTuple(args, "Oz", &list, &mode_name)) { return NULL; } - if (mode && !strcmp(mode, "F")) { + const Mode * const mode = findMode(mode_name); + + if (mode == IMAGING_MODE_F) { FLOAT32 *data; /* map from 8-bit data to floating point */ @@ -1490,8 +1541,7 @@ _point(ImagingObject *self, PyObject *args) { } im = ImagingPoint(self->image, mode, (void *)data); free(data); - - } else if (!strcmp(self->image->mode, "I") && mode && !strcmp(mode, "L")) { + } else if (self->image->mode == IMAGING_MODE_I && mode == IMAGING_MODE_L) { UINT8 *data; /* map from 16-bit subset of 32-bit data to 8-bit */ @@ -1503,7 +1553,6 @@ _point(ImagingObject *self, PyObject *args) { } im = ImagingPoint(self->image, mode, (void *)data); free(data); - } else { INT32 *data; UINT8 lut[1024]; @@ -1524,7 +1573,7 @@ _point(ImagingObject *self, PyObject *args) { return NULL; } - if (mode && !strcmp(mode, "I")) { + if (mode == IMAGING_MODE_I) { im = ImagingPoint(self->image, mode, (void *)data); } else if (mode && bands > 1) { for (i = 0; i < 256; i++) { @@ -1629,10 +1678,9 @@ _putdata(ImagingObject *self, PyObject *args) { int bigendian = 0; if (image->type == IMAGING_TYPE_SPECIAL) { // I;16* - if ( - strcmp(image->mode, "I;16B") == 0 + if (image->mode == IMAGING_MODE_I_16B #ifdef WORDS_BIGENDIAN - || strcmp(image->mode, "I;16N") == 0 + || image->mode == IMAGING_MODE_I_16N #endif ) { bigendian = 1; @@ -1729,7 +1777,7 @@ _quantize(ImagingObject *self, PyObject *args) { if (!self->image->xsize || !self->image->ysize) { /* no content; return an empty image */ - return PyImagingNew(ImagingNew("P", self->image->xsize, self->image->ysize)); + return PyImagingNew(ImagingNew(IMAGING_MODE_P, self->image->xsize, self->image->ysize)); } return PyImagingNew(ImagingQuantize(self->image, colours, method, kmeans)); @@ -1740,21 +1788,33 @@ _putpalette(ImagingObject *self, PyObject *args) { ImagingShuffler unpack; int bits; - char *palette_mode, *rawmode; + char *palette_mode_name, *rawmode_name; UINT8 *palette; Py_ssize_t palettesize; if (!PyArg_ParseTuple( - args, "ssy#", &palette_mode, &rawmode, &palette, &palettesize + args, "ssy#", &palette_mode_name, &rawmode_name, &palette, &palettesize )) { return NULL; } - if (strcmp(self->image->mode, "L") && strcmp(self->image->mode, "LA") && - strcmp(self->image->mode, "P") && strcmp(self->image->mode, "PA")) { + if (self->image->mode != IMAGING_MODE_L && self->image->mode != IMAGING_MODE_LA && + self->image->mode != IMAGING_MODE_P && self->image->mode != IMAGING_MODE_PA) { PyErr_SetString(PyExc_ValueError, wrong_mode); return NULL; } + const Mode * const palette_mode = findMode(palette_mode_name); + if (palette_mode == NULL) { + PyErr_SetString(PyExc_ValueError, wrong_mode); + return NULL; + } + + const RawMode * const rawmode = findRawMode(rawmode_name); + if (rawmode == NULL) { + PyErr_SetString(PyExc_ValueError, wrong_raw_mode); + return NULL; + } + unpack = ImagingFindUnpacker(palette_mode, rawmode, &bits); if (!unpack) { PyErr_SetString(PyExc_ValueError, wrong_raw_mode); @@ -1768,7 +1828,7 @@ _putpalette(ImagingObject *self, PyObject *args) { ImagingPaletteDelete(self->image->palette); - strcpy(self->image->mode, strlen(self->image->mode) == 2 ? "PA" : "P"); + self->image->mode = strlen(self->image->mode->name) == 2 ? IMAGING_MODE_PA : IMAGING_MODE_P; self->image->palette = ImagingPaletteNew(palette_mode); @@ -1796,7 +1856,7 @@ _putpalettealpha(ImagingObject *self, PyObject *args) { return NULL; } - strcpy(self->image->palette->mode, "RGBA"); + self->image->palette->mode = IMAGING_MODE_RGBA; self->image->palette->palette[index * 4 + 3] = (UINT8)alpha; Py_RETURN_NONE; @@ -1821,7 +1881,7 @@ _putpalettealphas(ImagingObject *self, PyObject *args) { return NULL; } - strcpy(self->image->palette->mode, "RGBA"); + self->image->palette->mode = IMAGING_MODE_RGBA; for (i = 0; i < length; i++) { self->image->palette->palette[i * 4 + 3] = (UINT8)values[i]; } @@ -1989,8 +2049,10 @@ _reduce(ImagingObject *self, PyObject *args) { return PyImagingNew(imOut); } -#define IS_RGB(mode) \ - (!strcmp(mode, "RGB") || !strcmp(mode, "RGBA") || !strcmp(mode, "RGBX")) +static int +isRGB(const Mode * const mode) { + return mode == IMAGING_MODE_RGB || mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_RGBX; +} static PyObject * im_setmode(ImagingObject *self, PyObject *args) { @@ -1998,23 +2060,25 @@ im_setmode(ImagingObject *self, PyObject *args) { Imaging im; - char *mode; + char *mode_name; Py_ssize_t modelen; - if (!PyArg_ParseTuple(args, "s#:setmode", &mode, &modelen)) { + if (!PyArg_ParseTuple(args, "s#:setmode", &mode_name, &modelen)) { return NULL; } + const Mode * const mode = findMode(mode_name); + im = self->image; /* move all logic in here to the libImaging primitive */ - if (!strcmp(im->mode, mode)) { + if (im->mode == mode) { ; /* same mode; always succeeds */ - } else if (IS_RGB(im->mode) && IS_RGB(mode)) { + } else if (isRGB(im->mode) && isRGB(mode)) { /* color to color */ - strcpy(im->mode, mode); + im->mode = mode; im->bands = modelen; - if (!strcmp(mode, "RGBA")) { + if (mode == IMAGING_MODE_RGBA) { (void)ImagingFillBand(im, 3, 255); } } else { @@ -2294,7 +2358,7 @@ _getextrema(ImagingObject *self) { case IMAGING_TYPE_FLOAT32: return Py_BuildValue("dd", extrema.f[0], extrema.f[1]); case IMAGING_TYPE_SPECIAL: - if (strcmp(self->image->mode, "I;16") == 0) { + if (self->image->mode == IMAGING_MODE_I_16) { return Py_BuildValue("HH", extrema.s[0], extrema.s[1]); } } @@ -2383,7 +2447,7 @@ _putband(ImagingObject *self, PyObject *args) { static PyObject * _merge(PyObject *self, PyObject *args) { - char *mode; + char *mode_name; ImagingObject *band0 = NULL; ImagingObject *band1 = NULL; ImagingObject *band2 = NULL; @@ -2393,7 +2457,7 @@ _merge(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, "sO!|O!O!O!", - &mode, + &mode_name, &Imaging_Type, &band0, &Imaging_Type, @@ -2406,6 +2470,8 @@ _merge(PyObject *self, PyObject *args) { return NULL; } + const Mode * const mode = findMode(mode_name); + if (band0) { bands[0] = band0->image; } @@ -3711,7 +3777,7 @@ static struct PyMethodDef methods[] = { static PyObject * _getattr_mode(ImagingObject *self, void *closure) { - return PyUnicode_FromString(self->image->mode); + return PyUnicode_FromString(self->image->mode->name); } static PyObject * diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 1eaabd8e561..49f17f0daa0 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -193,16 +193,16 @@ extern void ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator); extern Imaging -ImagingNew(const char *mode, int xsize, int ysize); +ImagingNew(const Mode *mode, int xsize, int ysize); extern Imaging -ImagingNewDirty(const char *mode, int xsize, int ysize); +ImagingNewDirty(const Mode *mode, int xsize, int ysize); extern Imaging -ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn); +ImagingNew2Dirty(const Mode *mode, Imaging imOut, Imaging imIn); extern void ImagingDelete(Imaging im); extern Imaging -ImagingNewBlock(const char *mode, int xsize, int ysize); +ImagingNewBlock(const Mode *mode, int xsize, int ysize); extern Imaging ImagingNewArrow( @@ -214,9 +214,9 @@ ImagingNewArrow( ); extern Imaging -ImagingNewPrologue(const char *mode, int xsize, int ysize); +ImagingNewPrologue(const Mode *mode, int xsize, int ysize); extern Imaging -ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int structure_size); +ImagingNewPrologueSubtype(const Mode *mode, int xsize, int ysize, int structure_size); extern void ImagingCopyPalette(Imaging destination, Imaging source); @@ -233,7 +233,7 @@ _ImagingAccessDelete(Imaging im, ImagingAccess access); #define ImagingAccessDelete(im, access) /* nop, for now */ extern ImagingPalette -ImagingPaletteNew(const char *mode); +ImagingPaletteNew(const Mode *mode); extern ImagingPalette ImagingPaletteNewBrowser(void); extern ImagingPalette @@ -305,13 +305,13 @@ ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha); extern Imaging ImagingCopy(Imaging im); extern Imaging -ImagingConvert(Imaging im, const char *mode, ImagingPalette palette, int dither); +ImagingConvert(Imaging im, const Mode *mode, ImagingPalette palette, int dither); extern Imaging -ImagingConvertInPlace(Imaging im, const char *mode); +ImagingConvertInPlace(Imaging im, const Mode *mode); extern Imaging -ImagingConvertMatrix(Imaging im, const char *mode, float m[]); +ImagingConvertMatrix(Imaging im, const Mode *mode, float m[]); extern Imaging -ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b); +ImagingConvertTransparent(Imaging im, const Mode *mode, int r, int g, int b); extern Imaging ImagingCrop(Imaging im, int x0, int y0, int x1, int y1); extern Imaging @@ -325,9 +325,9 @@ ImagingFill2( extern Imaging ImagingFillBand(Imaging im, int band, int color); extern Imaging -ImagingFillLinearGradient(const char *mode); +ImagingFillLinearGradient(const Mode *mode); extern Imaging -ImagingFillRadialGradient(const char *mode); +ImagingFillRadialGradient(const Mode *mode); extern Imaging ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 offset); extern Imaging @@ -341,7 +341,7 @@ ImagingGaussianBlur( extern Imaging ImagingGetBand(Imaging im, int band); extern Imaging -ImagingMerge(const char *mode, Imaging bands[4]); +ImagingMerge(const Mode *mode, Imaging bands[4]); extern int ImagingSplit(Imaging im, Imaging bands[4]); extern int @@ -368,7 +368,7 @@ ImagingOffset(Imaging im, int xoffset, int yoffset); extern int ImagingPaste(Imaging into, Imaging im, Imaging mask, int x0, int y0, int x1, int y1); extern Imaging -ImagingPoint(Imaging im, const char *tablemode, const void *table); +ImagingPoint(Imaging im, const Mode *tablemode, const void *table); extern Imaging ImagingPointTransform(Imaging imIn, double scale, double offset); extern Imaging @@ -709,9 +709,9 @@ extern void ImagingConvertYCbCr2RGB(UINT8 *out, const UINT8 *in, int pixels); extern ImagingShuffler -ImagingFindUnpacker(const char *mode, const char *rawmode, int *bits_out); +ImagingFindUnpacker(const Mode *mode, const RawMode *rawmode, int *bits_out); extern ImagingShuffler -ImagingFindPacker(const char *mode, const char *rawmode, int *bits_out); +ImagingFindPacker(const Mode *mode, const RawMode *rawmode, int *bits_out); struct ImagingCodecStateInstance { int count; From a37f53c94974232c3d239ca9194d93296638d3ad Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 00:58:11 -0500 Subject: [PATCH 1894/2374] use mode structs in tkImaging.c --- src/Tk/tkImaging.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index a36c3e0bdc4..3e35f885f61 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -121,15 +121,18 @@ PyImagingPhotoPut( /* Mode */ - if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { + if (im->mode == IMAGING_MODE_1 || im->mode == IMAGING_MODE_L) { block.pixelSize = 1; block.offset[0] = block.offset[1] = block.offset[2] = block.offset[3] = 0; - } else if (strncmp(im->mode, "RGB", 3) == 0) { + } else if ( + im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA || + im->mode == IMAGING_MODE_RGBX || im->mode == IMAGING_MODE_RGBa + ) { block.pixelSize = 4; block.offset[0] = 0; block.offset[1] = 1; block.offset[2] = 2; - if (strcmp(im->mode, "RGBA") == 0) { + if (im->mode == IMAGING_MODE_RGBA) { block.offset[3] = 3; /* alpha (or reserved, under Tk 8.2) */ } else { block.offset[3] = 0; /* no alpha */ From a12dc30dc0f4a9b16c49fef2597a8d2f052983fe Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 14:46:29 +0200 Subject: [PATCH 1895/2374] use mode structs in encode.c and decode.c --- src/decode.c | 105 ++++++++++++++++++++++++++---------------- src/encode.c | 94 +++++++++++++++++++++++++------------ src/libImaging/Jpeg.h | 8 ++-- src/libImaging/Mode.h | 7 +++ 4 files changed, 140 insertions(+), 74 deletions(-) diff --git a/src/decode.c b/src/decode.c index 03db1ce3516..9f4de28a828 100644 --- a/src/decode.c +++ b/src/decode.c @@ -266,7 +266,7 @@ static PyTypeObject ImagingDecoderType = { /* -------------------------------------------------------------------- */ int -get_unpacker(ImagingDecoderObject *decoder, const char *mode, const char *rawmode) { +get_unpacker(ImagingDecoderObject *decoder, const Mode *mode, const RawMode *rawmode) { int bits; ImagingShuffler unpack; @@ -436,12 +436,14 @@ PyObject * PyImaging_HexDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { + char *mode_name, *rawmode_name; + if (!PyArg_ParseTuple(args, "ss", &mode_name, &rawmode_name)) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { return NULL; @@ -469,16 +471,19 @@ PyImaging_HexDecoderNew(PyObject *self, PyObject *args) { PyObject * PyImaging_LibTiffDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; char *compname; int fp; uint32_t ifdoffset; - if (!PyArg_ParseTuple(args, "sssiI", &mode, &rawmode, &compname, &fp, &ifdoffset)) { + if (!PyArg_ParseTuple(args, "sssiI", &mode_name, &rawmode_name, &compname, &fp, &ifdoffset)) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + TRACE(("new tiff decoder %s\n", compname)); decoder = PyImaging_DecoderNew(sizeof(TIFFSTATE)); @@ -511,12 +516,15 @@ PyObject * PyImaging_PackbitsDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { + char *mode_name; + char *rawmode_name; + if (!PyArg_ParseTuple(args, "ss", &mode_name, &rawmode_name)) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { return NULL; @@ -545,7 +553,7 @@ PyImaging_PcdDecoderNew(PyObject *self, PyObject *args) { } /* Unpack from PhotoYCC to RGB */ - if (get_unpacker(decoder, "RGB", "YCC;P") < 0) { + if (get_unpacker(decoder, IMAGING_MODE_RGB, IMAGING_RAWMODE_YCC_P) < 0) { return NULL; } @@ -562,13 +570,15 @@ PyObject * PyImaging_PcxDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; + char *mode_name, *rawmode_name; int stride; - if (!PyArg_ParseTuple(args, "ssi", &mode, &rawmode, &stride)) { + if (!PyArg_ParseTuple(args, "ssi", &mode_name, &rawmode_name, &stride)) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { return NULL; @@ -593,14 +603,16 @@ PyObject * PyImaging_RawDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; + char *mode_name, *rawmode_name; int stride = 0; int ystep = 1; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &stride, &ystep)) { + if (!PyArg_ParseTuple(args, "ss|ii", &mode_name, &rawmode_name, &stride, &ystep)) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + decoder = PyImaging_DecoderNew(sizeof(RAWSTATE)); if (decoder == NULL) { return NULL; @@ -627,14 +639,16 @@ PyObject * PyImaging_SgiRleDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; + char *mode_name, *rawmode_name; int ystep = 1; int bpc = 1; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &bpc)) { + if (!PyArg_ParseTuple(args, "ss|ii", &mode_name, &rawmode_name, &ystep, &bpc)) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + decoder = PyImaging_DecoderNew(sizeof(SGISTATE)); if (decoder == NULL) { return NULL; @@ -661,12 +675,14 @@ PyObject * PyImaging_SunRleDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { + char *mode_name, *rawmode_name; + if (!PyArg_ParseTuple(args, "ss", &mode_name, &rawmode_name)) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { return NULL; @@ -689,14 +705,16 @@ PyObject * PyImaging_TgaRleDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; + char *mode_name, *rawmode_name; int ystep = 1; int depth = 8; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &depth)) { + if (!PyArg_ParseTuple(args, "ss|ii", &mode_name, &rawmode_name, &ystep, &depth)) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { return NULL; @@ -727,7 +745,7 @@ PyImaging_XbmDecoderNew(PyObject *self, PyObject *args) { return NULL; } - if (get_unpacker(decoder, "1", "1;R") < 0) { + if (get_unpacker(decoder, IMAGING_MODE_1, IMAGING_RAWMODE_1_R) < 0) { return NULL; } @@ -748,13 +766,15 @@ PyObject * PyImaging_ZipDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; + char *mode_name, *rawmode_name; int interlaced = 0; - if (!PyArg_ParseTuple(args, "ss|i", &mode, &rawmode, &interlaced)) { + if (!PyArg_ParseTuple(args, "ss|i", &mode_name, &rawmode_name, &interlaced)) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + decoder = PyImaging_DecoderNew(sizeof(ZIPSTATE)); if (decoder == NULL) { return NULL; @@ -798,16 +818,19 @@ PyObject * PyImaging_JpegDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; /* what we want from the decoder */ - char *jpegmode; /* what's in the file */ + char *mode_name; + char *rawmode_name; /* what we want from the decoder */ + char *jpegmode; /* what's in the file */ int scale = 1; int draft = 0; - if (!PyArg_ParseTuple(args, "ssz|ii", &mode, &rawmode, &jpegmode, &scale, &draft)) { + if (!PyArg_ParseTuple(args, "ssz|ii", &mode_name, &rawmode_name, &jpegmode, &scale, &draft)) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * rawmode = findRawMode(rawmode_name); + if (!jpegmode) { jpegmode = ""; } @@ -820,8 +843,8 @@ PyImaging_JpegDecoderNew(PyObject *self, PyObject *args) { // libjpeg-turbo supports different output formats. // We are choosing Pillow's native format (3 color bytes + 1 padding) // to avoid extra conversion in Unpack.c. - if (ImagingJpegUseJCSExtensions() && strcmp(rawmode, "RGB") == 0) { - rawmode = "RGBX"; + if (ImagingJpegUseJCSExtensions() && rawmode == IMAGING_RAWMODE_RGB) { + rawmode = IMAGING_RAWMODE_RGBX; } if (get_unpacker(decoder, mode, rawmode) < 0) { @@ -831,11 +854,13 @@ PyImaging_JpegDecoderNew(PyObject *self, PyObject *args) { decoder->decode = ImagingJpegDecode; decoder->cleanup = ImagingJpegDecodeCleanup; - strncpy(((JPEGSTATE *)decoder->state.context)->rawmode, rawmode, 8); - strncpy(((JPEGSTATE *)decoder->state.context)->jpegmode, jpegmode, 8); + JPEGSTATE *jpeg_decoder_state_context = (JPEGSTATE *)decoder->state.context; + + jpeg_decoder_state_context->rawmode = rawmode; + strncpy(jpeg_decoder_state_context->jpegmode, jpegmode, 8); - ((JPEGSTATE *)decoder->state.context)->scale = scale; - ((JPEGSTATE *)decoder->state.context)->draft = draft; + jpeg_decoder_state_context->scale = scale; + jpeg_decoder_state_context->draft = draft; return (PyObject *)decoder; } diff --git a/src/encode.c b/src/encode.c index e56494036ff..311ffa4eed0 100644 --- a/src/encode.c +++ b/src/encode.c @@ -334,14 +334,19 @@ static PyTypeObject ImagingEncoderType = { /* -------------------------------------------------------------------- */ int -get_packer(ImagingEncoderObject *encoder, const char *mode, const char *rawmode) { +get_packer(ImagingEncoderObject *encoder, const Mode *mode, const RawMode *rawmode) { int bits; ImagingShuffler pack; pack = ImagingFindPacker(mode, rawmode, &bits); if (!pack) { Py_DECREF(encoder); - PyErr_Format(PyExc_ValueError, "No packer found from %s to %s", mode, rawmode); + PyErr_Format( + PyExc_ValueError, + "No packer found from %s to %s", + mode->name, + rawmode->name + ); return -1; } @@ -402,11 +407,11 @@ PyObject * PyImaging_GifEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; Py_ssize_t bits = 8; Py_ssize_t interlace = 0; - if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &bits, &interlace)) { + if (!PyArg_ParseTuple(args, "ss|nn", &mode_name, &rawmode_name, &bits, &interlace)) { return NULL; } @@ -415,6 +420,9 @@ PyImaging_GifEncoderNew(PyObject *self, PyObject *args) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; } @@ -435,11 +443,11 @@ PyObject * PyImaging_PcxEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; Py_ssize_t bits = 8; - if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &bits)) { + if (!PyArg_ParseTuple(args, "ss|n", &mode_name, &rawmode_name, &bits)) { return NULL; } @@ -448,6 +456,9 @@ PyImaging_PcxEncoderNew(PyObject *self, PyObject *args) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; } @@ -465,12 +476,12 @@ PyObject * PyImaging_RawEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; Py_ssize_t stride = 0; Py_ssize_t ystep = 1; - if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &stride, &ystep)) { + if (!PyArg_ParseTuple(args, "ss|nn", &mode_name, &rawmode_name, &stride, &ystep)) { return NULL; } @@ -479,6 +490,9 @@ PyImaging_RawEncoderNew(PyObject *self, PyObject *args) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; } @@ -499,11 +513,11 @@ PyObject * PyImaging_TgaRleEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; Py_ssize_t ystep = 1; - if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &ystep)) { + if (!PyArg_ParseTuple(args, "ss|n", &mode_name, &rawmode_name, &ystep)) { return NULL; } @@ -512,6 +526,9 @@ PyImaging_TgaRleEncoderNew(PyObject *self, PyObject *args) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; } @@ -536,7 +553,7 @@ PyImaging_XbmEncoderNew(PyObject *self, PyObject *args) { return NULL; } - if (get_packer(encoder, "1", "1;R") < 0) { + if (get_packer(encoder, IMAGING_MODE_1, IMAGING_RAWMODE_1_R) < 0) { return NULL; } @@ -557,8 +574,8 @@ PyObject * PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; Py_ssize_t optimize = 0; Py_ssize_t compress_level = -1; Py_ssize_t compress_type = -1; @@ -567,8 +584,8 @@ PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, "ss|nnny#", - &mode, - &rawmode, + &mode_name, + &rawmode_name, &optimize, &compress_level, &compress_type, @@ -597,6 +614,9 @@ PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + if (get_packer(encoder, mode, rawmode) < 0) { free(dictionary); return NULL; @@ -605,7 +625,7 @@ PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) { encoder->encode = ImagingZipEncode; encoder->cleanup = ImagingZipEncodeCleanup; - if (rawmode[0] == 'P') { + if (rawmode == IMAGING_RAWMODE_P || rawmode == IMAGING_RAWMODE_PA) { /* disable filtering */ ((ZIPSTATE *)encoder->state.context)->mode = ZIP_PNG_PALETTE; } @@ -634,8 +654,8 @@ PyObject * PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; char *compname; char *filename; Py_ssize_t fp; @@ -655,7 +675,15 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { PyObject *item; if (!PyArg_ParseTuple( - args, "sssnsOO", &mode, &rawmode, &compname, &fp, &filename, &tags, &types + args, + "sssnsOO", + &mode_name, + &rawmode_name, + &compname, + &fp, + &filename, + &tags, + &types )) { return NULL; } @@ -693,6 +721,9 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * const rawmode = findRawMode(rawmode_name); + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; } @@ -1076,8 +1107,8 @@ PyObject * PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; Py_ssize_t quality = 0; Py_ssize_t progressive = 0; Py_ssize_t smooth = 0; @@ -1101,8 +1132,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, "ss|nnnnpn(nn)nnnOz#y#y#", - &mode, - &rawmode, + &mode_name, + &rawmode_name, &quality, &progressive, &smooth, @@ -1130,11 +1161,14 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { return NULL; } + const Mode * const mode = findMode(mode_name); + const RawMode * rawmode = findRawMode(rawmode_name); + // libjpeg-turbo supports different output formats. // We are choosing Pillow's native format (3 color bytes + 1 padding) // to avoid extra conversion in Pack.c. - if (ImagingJpegUseJCSExtensions() && strcmp(rawmode, "RGB") == 0) { - rawmode = "RGBX"; + if (ImagingJpegUseJCSExtensions() && rawmode == IMAGING_RAWMODE_RGB) { + rawmode = IMAGING_RAWMODE_RGBX; } if (get_packer(encoder, mode, rawmode) < 0) { @@ -1192,7 +1226,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { encoder->encode = ImagingJpegEncode; JPEGENCODERSTATE *jpeg_encoder_state = (JPEGENCODERSTATE *)encoder->state.context; - strncpy(jpeg_encoder_state->rawmode, rawmode, 8); + jpeg_encoder_state->rawmode = rawmode; jpeg_encoder_state->keep_rgb = keep_rgb; jpeg_encoder_state->quality = quality; jpeg_encoder_state->qtables = qarrays; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 7cdba902281..35df91d7ffa 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -31,9 +31,9 @@ typedef struct { /* Jpeg file mode (empty if not known) */ char jpegmode[8 + 1]; - /* Converter output mode (input to the shuffler). If empty, - convert conversions are disabled */ - char rawmode[8 + 1]; + /* Converter output mode (input to the shuffler) */ + /* If NULL, convert conversions are disabled */ + const RawMode *rawmode; /* If set, trade quality for speed */ int draft; @@ -91,7 +91,7 @@ typedef struct { unsigned int restart_marker_rows; /* Converter input mode (input to the shuffler) */ - char rawmode[8 + 1]; + const RawMode *rawmode; /* Custom quantization tables () */ unsigned int *qtables; diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index 6491beb81b7..a9910366710 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -43,6 +43,7 @@ typedef struct { const char * const name; } RawMode; +// Non-rawmode aliases. extern const RawMode * const IMAGING_RAWMODE_1; extern const RawMode * const IMAGING_RAWMODE_CMYK; extern const RawMode * const IMAGING_RAWMODE_F; @@ -60,16 +61,22 @@ extern const RawMode * const IMAGING_RAWMODE_RGBX; extern const RawMode * const IMAGING_RAWMODE_RGBa; extern const RawMode * const IMAGING_RAWMODE_YCbCr; +// BGR modes. extern const RawMode * const IMAGING_RAWMODE_BGR_15; extern const RawMode * const IMAGING_RAWMODE_BGR_16; extern const RawMode * const IMAGING_RAWMODE_BGR_24; extern const RawMode * const IMAGING_RAWMODE_BGR_32; +// I;16 modes. extern const RawMode * const IMAGING_RAWMODE_I_16; extern const RawMode * const IMAGING_RAWMODE_I_16L; extern const RawMode * const IMAGING_RAWMODE_I_16B; extern const RawMode * const IMAGING_RAWMODE_I_16N; +// Rawmodes +extern const RawMode * const IMAGING_RAWMODE_1_R; +extern const RawMode * const IMAGING_RAWMODE_YCC_P; + const RawMode * findRawMode(const char * const name); From 0df2ed0640b4be12fb7066d39868e1c77adfa52e Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 14:53:22 +0200 Subject: [PATCH 1896/2374] use mode structs in Access.c --- src/libImaging/Access.c | 123 ++++++++++++++++++---------------------- src/libImaging/Mode.c | 8 +++ src/libImaging/Mode.h | 6 +- 3 files changed, 69 insertions(+), 68 deletions(-) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 3db52377e80..850399b1401 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -11,38 +11,9 @@ #include "Imaging.h" -/* use make_hash.py from the pillow-scripts repository to calculate these values */ -#define ACCESS_TABLE_SIZE 35 -#define ACCESS_TABLE_HASH 8940 +#define ACCESS_TABLE_SIZE 24 +static struct ImagingAccessInstance ACCESS_TABLE[ACCESS_TABLE_SIZE]; -static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE]; - -static inline UINT32 -hash(const char *mode) { - UINT32 i = ACCESS_TABLE_HASH; - while (*mode) { - i = ((i << 5) + i) ^ (UINT8)*mode++; - } - return i % ACCESS_TABLE_SIZE; -} - -static ImagingAccess -add_item(const char *mode) { - UINT32 i = hash(mode); - /* printf("hash %s => %d\n", mode, i); */ - if (access_table[i].mode && strcmp(access_table[i].mode, mode) != 0) { - fprintf( - stderr, - "AccessInit: hash collision: %d for both %s and %s\n", - i, - mode, - access_table[i].mode - ); - exit(1); - } - access_table[i].mode = mode; - return &access_table[i]; -} /* fetch individual pixel */ @@ -149,51 +120,69 @@ put_pixel_32(Imaging im, int x, int y, const void *color) { memcpy(&im->image32[y][x], color, sizeof(INT32)); } + +static void +set_access_table_item( + const int index, + const Mode * const mode, + void (*get_pixel)(Imaging im, int x, int y, void *pixel), + void (*put_pixel)(Imaging im, int x, int y, const void *pixel) +) { + ACCESS_TABLE[index].mode = mode; + ACCESS_TABLE[index].get_pixel = get_pixel; + ACCESS_TABLE[index].put_pixel = put_pixel; +} + void ImagingAccessInit(void) { -#define ADD(mode_, get_pixel_, put_pixel_) \ - { \ - ImagingAccess access = add_item(mode_); \ - access->get_pixel = get_pixel_; \ - access->put_pixel = put_pixel_; \ - } - - /* populate access table */ - ADD("1", get_pixel_8, put_pixel_8); - ADD("L", get_pixel_8, put_pixel_8); - ADD("LA", get_pixel_32_2bands, put_pixel_32); - ADD("La", get_pixel_32_2bands, put_pixel_32); - ADD("I", get_pixel_32, put_pixel_32); - ADD("I;16", get_pixel_16L, put_pixel_16L); - ADD("I;16L", get_pixel_16L, put_pixel_16L); - ADD("I;16B", get_pixel_16B, put_pixel_16B); + int i = 0; + set_access_table_item(i++, IMAGING_MODE_1, get_pixel_8, put_pixel_8); + set_access_table_item(i++, IMAGING_MODE_L, get_pixel_8, put_pixel_8); + set_access_table_item(i++, IMAGING_MODE_LA, get_pixel_32_2bands, put_pixel_32); + set_access_table_item(i++, IMAGING_MODE_La, get_pixel_32_2bands, put_pixel_32); + set_access_table_item(i++, IMAGING_MODE_I, get_pixel_32, put_pixel_32); + set_access_table_item(i++, IMAGING_MODE_I_16, get_pixel_16L, put_pixel_16L); + set_access_table_item(i++, IMAGING_MODE_I_16L, get_pixel_16L, put_pixel_16L); + set_access_table_item(i++, IMAGING_MODE_I_16B, get_pixel_16B, put_pixel_16B); #ifdef WORDS_BIGENDIAN - ADD("I;16N", get_pixel_16B, put_pixel_16B); + set_access_table_item(i++, IMAGING_MODE_I_16N, get_pixel_16B, put_pixel_16B); #else - ADD("I;16N", get_pixel_16L, put_pixel_16L); + set_access_table_item(i++, IMAGING_MODE_I_16N, get_pixel_16L, put_pixel_16L); #endif - ADD("I;32L", get_pixel_32L, put_pixel_32L); - ADD("I;32B", get_pixel_32B, put_pixel_32B); - ADD("F", get_pixel_32, put_pixel_32); - ADD("P", get_pixel_8, put_pixel_8); - ADD("PA", get_pixel_32_2bands, put_pixel_32); - ADD("RGB", get_pixel_32, put_pixel_32); - ADD("RGBA", get_pixel_32, put_pixel_32); - ADD("RGBa", get_pixel_32, put_pixel_32); - ADD("RGBX", get_pixel_32, put_pixel_32); - ADD("CMYK", get_pixel_32, put_pixel_32); - ADD("YCbCr", get_pixel_32, put_pixel_32); - ADD("LAB", get_pixel_32, put_pixel_32); - ADD("HSV", get_pixel_32, put_pixel_32); + set_access_table_item(i++, IMAGING_MODE_I_32L, get_pixel_32L, put_pixel_32L); + set_access_table_item(i++, IMAGING_MODE_I_32B, get_pixel_32B, put_pixel_32B); + set_access_table_item(i++, IMAGING_MODE_F, get_pixel_32, put_pixel_32); + set_access_table_item(i++, IMAGING_MODE_P, get_pixel_8, put_pixel_8); + set_access_table_item(i++, IMAGING_MODE_PA, get_pixel_32_2bands, put_pixel_32); + set_access_table_item(i++, IMAGING_MODE_RGB, get_pixel_32, put_pixel_32); + set_access_table_item(i++, IMAGING_MODE_RGBA, get_pixel_32, put_pixel_32); + set_access_table_item(i++, IMAGING_MODE_RGBa, get_pixel_32, put_pixel_32); + set_access_table_item(i++, IMAGING_MODE_RGBX, get_pixel_32, put_pixel_32); + set_access_table_item(i++, IMAGING_MODE_CMYK, get_pixel_32, put_pixel_32); + set_access_table_item(i++, IMAGING_MODE_YCbCr, get_pixel_32, put_pixel_32); + set_access_table_item(i++, IMAGING_MODE_LAB, get_pixel_32, put_pixel_32); + set_access_table_item(i++, IMAGING_MODE_HSV, get_pixel_32, put_pixel_32); + + + if (i != ACCESS_TABLE_SIZE) { + fprintf( + stderr, + "AccessInit: incorrect number of items added to ACCESS_TABLE; expected %i but got %i\n", + ACCESS_TABLE_SIZE, + i); + exit(1); + } } ImagingAccess -ImagingAccessNew(Imaging im) { - ImagingAccess access = &access_table[hash(im->mode)]; - if (im->mode[0] != access->mode[0] || strcmp(im->mode, access->mode) != 0) { - return NULL; +ImagingAccessNew(const Imaging im) { + int i; + for (i = 0; i < ACCESS_TABLE_SIZE; i++) { + if (im->mode == ACCESS_TABLE[i].mode) { + return &ACCESS_TABLE[i]; + } } - return access; + return NULL; } void diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c index 2bd09bda56a..85ba50e3f52 100644 --- a/src/libImaging/Mode.c +++ b/src/libImaging/Mode.c @@ -32,6 +32,8 @@ CREATE_MODE(Mode, MODE_I_16, {"I;16"}) CREATE_MODE(Mode, MODE_I_16L, {"I;16L"}) CREATE_MODE(Mode, MODE_I_16B, {"I;16B"}) CREATE_MODE(Mode, MODE_I_16N, {"I;16N"}) +CREATE_MODE(Mode, MODE_I_32L, {"I;32L"}) +CREATE_MODE(Mode, MODE_I_32B, {"I;32B"}) const Mode * const MODES[] = { IMAGING_MODE_1, @@ -59,6 +61,8 @@ const Mode * const MODES[] = { IMAGING_MODE_I_16L, IMAGING_MODE_I_16B, IMAGING_MODE_I_16N, + IMAGING_MODE_I_32L, + IMAGING_MODE_I_32B, NULL }; @@ -102,6 +106,8 @@ ALIAS_MODE_AS_RAWMODE(I_16) ALIAS_MODE_AS_RAWMODE(I_16L) ALIAS_MODE_AS_RAWMODE(I_16B) ALIAS_MODE_AS_RAWMODE(I_16N) +ALIAS_MODE_AS_RAWMODE(I_32L) +ALIAS_MODE_AS_RAWMODE(I_32B) const RawMode * const RAWMODES[] = { IMAGING_RAWMODE_1, @@ -129,6 +135,8 @@ const RawMode * const RAWMODES[] = { IMAGING_RAWMODE_I_16L, IMAGING_RAWMODE_I_16B, IMAGING_RAWMODE_I_16N, + IMAGING_RAWMODE_I_32L, + IMAGING_RAWMODE_I_32B, NULL }; diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index a9910366710..bd184808d23 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -35,6 +35,8 @@ extern const Mode * const IMAGING_MODE_I_16; extern const Mode * const IMAGING_MODE_I_16L; extern const Mode * const IMAGING_MODE_I_16B; extern const Mode * const IMAGING_MODE_I_16N; +extern const Mode * const IMAGING_MODE_I_32L; +extern const Mode * const IMAGING_MODE_I_32B; const Mode * findMode(const char * const name); @@ -67,11 +69,13 @@ extern const RawMode * const IMAGING_RAWMODE_BGR_16; extern const RawMode * const IMAGING_RAWMODE_BGR_24; extern const RawMode * const IMAGING_RAWMODE_BGR_32; -// I;16 modes. +// I;* modes. extern const RawMode * const IMAGING_RAWMODE_I_16; extern const RawMode * const IMAGING_RAWMODE_I_16L; extern const RawMode * const IMAGING_RAWMODE_I_16B; extern const RawMode * const IMAGING_RAWMODE_I_16N; +extern const RawMode * const IMAGING_RAWMODE_I_32L; +extern const RawMode * const IMAGING_RAWMODE_I_32B; // Rawmodes extern const RawMode * const IMAGING_RAWMODE_1_R; From 82182ba5489e8406c963a88a2187c622d4ee90f2 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 13:09:47 -0500 Subject: [PATCH 1897/2374] use mode structs in AlphaComposite.c --- src/libImaging/AlphaComposite.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/AlphaComposite.c b/src/libImaging/AlphaComposite.c index 6d728f9088b..8d6ee886210 100644 --- a/src/libImaging/AlphaComposite.c +++ b/src/libImaging/AlphaComposite.c @@ -25,12 +25,12 @@ ImagingAlphaComposite(Imaging imDst, Imaging imSrc) { int x, y; /* Check arguments */ - if (!imDst || !imSrc || strcmp(imDst->mode, "RGBA") || + if (!imDst || !imSrc || imDst->mode != IMAGING_MODE_RGBA || imDst->type != IMAGING_TYPE_UINT8 || imDst->bands != 4) { return ImagingError_ModeError(); } - if (strcmp(imDst->mode, imSrc->mode) || imDst->type != imSrc->type || + if (imDst->mode != imSrc->mode || imDst->type != imSrc->type || imDst->bands != imSrc->bands || imDst->xsize != imSrc->xsize || imDst->ysize != imSrc->ysize) { return ImagingError_Mismatch(); From d0541a73b94f1b8b5ba8a9e42718288e6f02feb1 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 13:10:12 -0500 Subject: [PATCH 1898/2374] use mode structs in Bands.c --- src/libImaging/Bands.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libImaging/Bands.c b/src/libImaging/Bands.c index e1b16b34ac0..501b4625fc5 100644 --- a/src/libImaging/Bands.c +++ b/src/libImaging/Bands.c @@ -41,7 +41,7 @@ ImagingGetBand(Imaging imIn, int band) { band = 3; } - imOut = ImagingNewDirty("L", imIn->xsize, imIn->ysize); + imOut = ImagingNewDirty(IMAGING_MODE_L, imIn->xsize, imIn->ysize); if (!imOut) { return NULL; } @@ -82,7 +82,7 @@ ImagingSplit(Imaging imIn, Imaging bands[4]) { } for (i = 0; i < imIn->bands; i++) { - bands[i] = ImagingNewDirty("L", imIn->xsize, imIn->ysize); + bands[i] = ImagingNewDirty(IMAGING_MODE_L, imIn->xsize, imIn->ysize); if (!bands[i]) { for (j = 0; j < i; ++j) { ImagingDelete(bands[j]); @@ -240,7 +240,7 @@ ImagingFillBand(Imaging imOut, int band, int color) { } Imaging -ImagingMerge(const char *mode, Imaging bands[4]) { +ImagingMerge(const Mode *mode, Imaging bands[4]) { int i, x, y; int bandsCount = 0; Imaging imOut; From 38c75b9449c1bd2fbf5911ac683798f9f3a45fa5 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 13:16:01 -0500 Subject: [PATCH 1899/2374] use mode structs in Blend.c --- src/libImaging/Blend.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/Blend.c b/src/libImaging/Blend.c index a53ae0fad53..df94920f62e 100644 --- a/src/libImaging/Blend.c +++ b/src/libImaging/Blend.c @@ -24,8 +24,8 @@ ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha) { /* Check arguments */ if (!imIn1 || !imIn2 || imIn1->type != IMAGING_TYPE_UINT8 || imIn1->palette || - strcmp(imIn1->mode, "1") == 0 || imIn2->palette || - strcmp(imIn2->mode, "1") == 0) { + imIn1->mode == IMAGING_MODE_1 || imIn2->palette || + imIn2->mode == IMAGING_MODE_1) { return ImagingError_ModeError(); } From 6f6e1f99fc6fb94743e0f9281565b330fbbfaad0 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 14:00:44 -0500 Subject: [PATCH 1900/2374] use mode structs in BoxBlur.c --- src/libImaging/BoxBlur.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index ed91541fed4..4fea4fe44b8 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -248,7 +248,7 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) return ImagingError_ValueError("radius must be >= 0"); } - if (strcmp(imIn->mode, imOut->mode) || imIn->type != imOut->type || + if (imIn->mode != imOut->mode || imIn->type != imOut->type || imIn->bands != imOut->bands || imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { return ImagingError_Mismatch(); @@ -258,10 +258,10 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) return ImagingError_ModeError(); } - if (!(strcmp(imIn->mode, "RGB") == 0 || strcmp(imIn->mode, "RGBA") == 0 || - strcmp(imIn->mode, "RGBa") == 0 || strcmp(imIn->mode, "RGBX") == 0 || - strcmp(imIn->mode, "CMYK") == 0 || strcmp(imIn->mode, "L") == 0 || - strcmp(imIn->mode, "LA") == 0 || strcmp(imIn->mode, "La") == 0)) { + if (imIn->mode != IMAGING_MODE_RGB && imIn->mode != IMAGING_MODE_RGBA && + imIn->mode != IMAGING_MODE_RGBa && imIn->mode != IMAGING_MODE_RGBX && + imIn->mode != IMAGING_MODE_CMYK && imIn->mode != IMAGING_MODE_L && + imIn->mode != IMAGING_MODE_LA && imIn->mode != IMAGING_MODE_La) { return ImagingError_ModeError(); } From ecf1fce82baf2a4ca2deb37b58419e06a927e1eb Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 14:05:13 -0500 Subject: [PATCH 1901/2374] use mode structs in Chops.c --- src/libImaging/Chops.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libImaging/Chops.c b/src/libImaging/Chops.c index f326d402f2c..66d0b4f9774 100644 --- a/src/libImaging/Chops.c +++ b/src/libImaging/Chops.c @@ -60,11 +60,11 @@ return imOut; static Imaging -create(Imaging im1, Imaging im2, char *mode) { +create(Imaging im1, Imaging im2, const Mode *mode) { int xsize, ysize; if (!im1 || !im2 || im1->type != IMAGING_TYPE_UINT8 || - (mode != NULL && (strcmp(im1->mode, "1") || strcmp(im2->mode, "1")))) { + (mode != NULL && (im1->mode != mode || im2->mode != mode))) { return (Imaging)ImagingError_ModeError(); } if (im1->type != im2->type || im1->bands != im2->bands) { @@ -114,17 +114,17 @@ ImagingChopSubtract(Imaging imIn1, Imaging imIn2, float scale, int offset) { Imaging ImagingChopAnd(Imaging imIn1, Imaging imIn2) { - CHOP2((in1[x] && in2[x]) ? 255 : 0, "1"); + CHOP2((in1[x] && in2[x]) ? 255 : 0, IMAGING_MODE_1); } Imaging ImagingChopOr(Imaging imIn1, Imaging imIn2) { - CHOP2((in1[x] || in2[x]) ? 255 : 0, "1"); + CHOP2((in1[x] || in2[x]) ? 255 : 0, IMAGING_MODE_1); } Imaging ImagingChopXor(Imaging imIn1, Imaging imIn2) { - CHOP2(((in1[x] != 0) ^ (in2[x] != 0)) ? 255 : 0, "1"); + CHOP2(((in1[x] != 0) ^ (in2[x] != 0)) ? 255 : 0, IMAGING_MODE_1); } Imaging From 9bf3495898169879e75a8783310d4798741d3729 Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:12:51 +0200 Subject: [PATCH 1902/2374] use mode structs in Convert.c --- src/_imaging.c | 1 + src/libImaging/Access.c | 3 + src/libImaging/Convert.c | 406 +++++++++++++++++++++------------------ src/libImaging/Imaging.h | 15 +- 4 files changed, 235 insertions(+), 190 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index d0648540aeb..271a16dbb65 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4322,6 +4322,7 @@ setup_module(PyObject *m) { } ImagingAccessInit(); + ImagingConvertInit(); #ifdef HAVE_LIBJPEG { diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 850399b1401..6c41fc0915c 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -187,3 +187,6 @@ ImagingAccessNew(const Imaging im) { void _ImagingAccessDelete(Imaging im, ImagingAccess access) {} + +void +ImagingAccessFree() {} diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 9a2c9ff1667..2bc054616aa 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -877,147 +877,12 @@ I16_RGB(UINT8 *out, const UINT8 *in, int xsize) { } } -static struct { - const char *from; - const char *to; - ImagingShuffler convert; -} converters[] = { - - {"1", "L", bit2l}, - {"1", "I", bit2i}, - {"1", "F", bit2f}, - {"1", "RGB", bit2rgb}, - {"1", "RGBA", bit2rgb}, - {"1", "RGBX", bit2rgb}, - {"1", "CMYK", bit2cmyk}, - {"1", "YCbCr", bit2ycbcr}, - {"1", "HSV", bit2hsv}, - - {"L", "1", l2bit}, - {"L", "LA", l2la}, - {"L", "I", l2i}, - {"L", "F", l2f}, - {"L", "RGB", l2rgb}, - {"L", "RGBA", l2rgb}, - {"L", "RGBX", l2rgb}, - {"L", "CMYK", l2cmyk}, - {"L", "YCbCr", l2ycbcr}, - {"L", "HSV", l2hsv}, - - {"LA", "L", la2l}, - {"LA", "La", lA2la}, - {"LA", "RGB", la2rgb}, - {"LA", "RGBA", la2rgb}, - {"LA", "RGBX", la2rgb}, - {"LA", "CMYK", la2cmyk}, - {"LA", "YCbCr", la2ycbcr}, - {"LA", "HSV", la2hsv}, - - {"La", "LA", la2lA}, - - {"I", "L", i2l}, - {"I", "F", i2f}, - {"I", "RGB", i2rgb}, - {"I", "RGBA", i2rgb}, - {"I", "RGBX", i2rgb}, - {"I", "HSV", i2hsv}, - - {"F", "L", f2l}, - {"F", "I", f2i}, - - {"RGB", "1", rgb2bit}, - {"RGB", "L", rgb2l}, - {"RGB", "LA", rgb2la}, - {"RGB", "La", rgb2la}, - {"RGB", "I", rgb2i}, - {"RGB", "I;16", rgb2i16l}, - {"RGB", "I;16L", rgb2i16l}, - {"RGB", "I;16B", rgb2i16b}, -#ifdef WORDS_BIGENDIAN - {"RGB", "I;16N", rgb2i16b}, -#else - {"RGB", "I;16N", rgb2i16l}, -#endif - {"RGB", "F", rgb2f}, - {"RGB", "RGBA", rgb2rgba}, - {"RGB", "RGBa", rgb2rgba}, - {"RGB", "RGBX", rgb2rgba}, - {"RGB", "CMYK", rgb2cmyk}, - {"RGB", "YCbCr", ImagingConvertRGB2YCbCr}, - {"RGB", "HSV", rgb2hsv}, - - {"RGBA", "1", rgb2bit}, - {"RGBA", "L", rgb2l}, - {"RGBA", "LA", rgba2la}, - {"RGBA", "I", rgb2i}, - {"RGBA", "F", rgb2f}, - {"RGBA", "RGB", rgba2rgb}, - {"RGBA", "RGBa", rgbA2rgba}, - {"RGBA", "RGBX", rgb2rgba}, - {"RGBA", "CMYK", rgb2cmyk}, - {"RGBA", "YCbCr", ImagingConvertRGB2YCbCr}, - {"RGBA", "HSV", rgb2hsv}, - - {"RGBa", "RGBA", rgba2rgbA}, - {"RGBa", "RGB", rgba2rgb_}, - - {"RGBX", "1", rgb2bit}, - {"RGBX", "L", rgb2l}, - {"RGBX", "LA", rgb2la}, - {"RGBX", "I", rgb2i}, - {"RGBX", "F", rgb2f}, - {"RGBX", "RGB", rgba2rgb}, - {"RGBX", "CMYK", rgb2cmyk}, - {"RGBX", "YCbCr", ImagingConvertRGB2YCbCr}, - {"RGBX", "HSV", rgb2hsv}, - - {"CMYK", "RGB", cmyk2rgb}, - {"CMYK", "RGBA", cmyk2rgb}, - {"CMYK", "RGBX", cmyk2rgb}, - {"CMYK", "HSV", cmyk2hsv}, - - {"YCbCr", "L", ycbcr2l}, - {"YCbCr", "LA", ycbcr2la}, - {"YCbCr", "RGB", ImagingConvertYCbCr2RGB}, - - {"HSV", "RGB", hsv2rgb}, - - {"I", "I;16", I_I16L}, - {"I;16", "I", I16L_I}, - {"I;16", "RGB", I16_RGB}, - {"L", "I;16", L_I16L}, - {"I;16", "L", I16L_L}, - - {"I", "I;16L", I_I16L}, - {"I;16L", "I", I16L_I}, - {"I", "I;16B", I_I16B}, - {"I;16B", "I", I16B_I}, - - {"L", "I;16L", L_I16L}, - {"I;16L", "L", I16L_L}, - {"L", "I;16B", L_I16B}, - {"I;16B", "L", I16B_L}, -#ifdef WORDS_BIGENDIAN - {"L", "I;16N", L_I16B}, - {"I;16N", "L", I16B_L}, -#else - {"L", "I;16N", L_I16L}, - {"I;16N", "L", I16L_L}, -#endif - - {"I;16", "F", I16L_F}, - {"I;16L", "F", I16L_F}, - {"I;16B", "F", I16B_F}, - - {NULL} -}; - -/* FIXME: translate indexed versions to pointer versions below this line */ - /* ------------------- */ /* Palette conversions */ /* ------------------- */ +/* FIXME: translate indexed versions to pointer versions below this line */ + static void p2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; @@ -1065,13 +930,13 @@ pa2p(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { static void p2pa(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; - int rgb = strcmp(palette->mode, "RGB"); + const int rgb = palette->mode == IMAGING_MODE_RGB; for (x = 0; x < xsize; x++, in++) { const UINT8 *rgba = &palette->palette[in[0] * 4]; *out++ = in[0]; *out++ = in[0]; *out++ = in[0]; - *out++ = rgb == 0 ? 255 : rgba[3]; + *out++ = rgb ? 255 : rgba[3]; } } @@ -1225,7 +1090,7 @@ pa2ycbcr(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { } static Imaging -frompalette(Imaging imOut, Imaging imIn, const char *mode) { +frompalette(Imaging imOut, Imaging imIn, const Mode *mode) { ImagingSectionCookie cookie; int alpha; int y; @@ -1237,31 +1102,31 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { return (Imaging)ImagingError_ValueError("no palette"); } - alpha = !strcmp(imIn->mode, "PA"); + alpha = imIn->mode == IMAGING_MODE_PA; - if (strcmp(mode, "1") == 0) { + if (mode == IMAGING_MODE_1) { convert = alpha ? pa2bit : p2bit; - } else if (strcmp(mode, "L") == 0) { + } else if (mode == IMAGING_MODE_L) { convert = alpha ? pa2l : p2l; - } else if (strcmp(mode, "LA") == 0) { + } else if (mode == IMAGING_MODE_LA) { convert = alpha ? pa2la : p2la; - } else if (strcmp(mode, "P") == 0) { + } else if (mode == IMAGING_MODE_P) { convert = pa2p; - } else if (strcmp(mode, "PA") == 0) { + } else if (mode == IMAGING_MODE_PA) { convert = p2pa; - } else if (strcmp(mode, "I") == 0) { + } else if (mode == IMAGING_MODE_I) { convert = alpha ? pa2i : p2i; - } else if (strcmp(mode, "F") == 0) { + } else if (mode == IMAGING_MODE_F) { convert = alpha ? pa2f : p2f; - } else if (strcmp(mode, "RGB") == 0) { + } else if (mode == IMAGING_MODE_RGB) { convert = alpha ? pa2rgb : p2rgb; - } else if (strcmp(mode, "RGBA") == 0 || strcmp(mode, "RGBX") == 0) { + } else if (mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_RGBX) { convert = alpha ? pa2rgba : p2rgba; - } else if (strcmp(mode, "CMYK") == 0) { + } else if (mode == IMAGING_MODE_CMYK) { convert = alpha ? pa2cmyk : p2cmyk; - } else if (strcmp(mode, "YCbCr") == 0) { + } else if (mode == IMAGING_MODE_YCbCr) { convert = alpha ? pa2ycbcr : p2ycbcr; - } else if (strcmp(mode, "HSV") == 0) { + } else if (mode == IMAGING_MODE_HSV) { convert = alpha ? pa2hsv : p2hsv; } else { return (Imaging)ImagingError_ValueError("conversion not supported"); @@ -1271,7 +1136,7 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { if (!imOut) { return NULL; } - if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) { + if (mode == IMAGING_MODE_P || mode == IMAGING_MODE_PA) { ImagingPaletteDelete(imOut->palette); imOut->palette = ImagingPaletteDuplicate(imIn->palette); } @@ -1295,24 +1160,26 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { #endif static Imaging topalette( - Imaging imOut, Imaging imIn, const char *mode, ImagingPalette inpalette, int dither + Imaging imOut, Imaging imIn, const Mode *mode, ImagingPalette inpalette, int dither ) { ImagingSectionCookie cookie; int alpha; int x, y; ImagingPalette palette = inpalette; - /* Map L or RGB/RGBX/RGBA to palette image */ - if (strcmp(imIn->mode, "L") != 0 && strncmp(imIn->mode, "RGB", 3) != 0) { + /* Map L or RGB/RGBX/RGBA/RGBa to palette image */ + if (imIn->mode != IMAGING_MODE_L && imIn->mode != IMAGING_MODE_RGB && + imIn->mode != IMAGING_MODE_RGBX && imIn->mode != IMAGING_MODE_RGBA && + imIn->mode != IMAGING_MODE_RGBa) { return (Imaging)ImagingError_ValueError("conversion not supported"); } - alpha = !strcmp(mode, "PA"); + alpha = mode == IMAGING_MODE_PA; if (palette == NULL) { /* FIXME: make user configurable */ if (imIn->bands == 1) { - palette = ImagingPaletteNew("RGB"); + palette = ImagingPaletteNew(IMAGING_MODE_RGB); palette->size = 256; int i; @@ -1499,11 +1366,11 @@ tobilevel(Imaging imOut, Imaging imIn) { int *errors; /* Map L or RGB to dithered 1 image */ - if (strcmp(imIn->mode, "L") != 0 && strcmp(imIn->mode, "RGB") != 0) { + if (imIn->mode != IMAGING_MODE_L && imIn->mode != IMAGING_MODE_RGB) { return (Imaging)ImagingError_ValueError("conversion not supported"); } - imOut = ImagingNew2Dirty("1", imOut, imIn); + imOut = ImagingNew2Dirty(IMAGING_MODE_1, imOut, imIn); if (!imOut) { return NULL; } @@ -1585,9 +1452,19 @@ tobilevel(Imaging imOut, Imaging imIn) { #pragma optimize("", on) #endif +/* ------------------- */ +/* Conversion handlers */ +/* ------------------- */ + +static struct Converter { + const Mode *from; + const Mode *to; + ImagingShuffler convert; +} *converters = NULL; + static Imaging convert( - Imaging imOut, Imaging imIn, const char *mode, ImagingPalette palette, int dither + Imaging imOut, Imaging imIn, const Mode *mode, ImagingPalette palette, int dither ) { ImagingSectionCookie cookie; ImagingShuffler convert; @@ -1605,22 +1482,22 @@ convert( mode = imIn->palette->mode; } else { /* Same mode? */ - if (!strcmp(imIn->mode, mode)) { + if (imIn->mode == mode) { return ImagingCopy2(imOut, imIn); } } /* test for special conversions */ - if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "PA") == 0) { + if (imIn->mode == IMAGING_MODE_P || imIn->mode == IMAGING_MODE_PA) { return frompalette(imOut, imIn, mode); } - if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) { + if (mode == IMAGING_MODE_P || mode == IMAGING_MODE_PA) { return topalette(imOut, imIn, mode, palette, dither); } - if (dither && strcmp(mode, "1") == 0) { + if (dither && mode == IMAGING_MODE_1) { return tobilevel(imOut, imIn); } @@ -1629,8 +1506,7 @@ convert( convert = NULL; for (y = 0; converters[y].from; y++) { - if (!strcmp(imIn->mode, converters[y].from) && - !strcmp(mode, converters[y].to)) { + if (imIn->mode == converters[y].from && mode == converters[y].to) { convert = converters[y].convert; break; } @@ -1642,7 +1518,7 @@ convert( #else static char buf[100]; snprintf( - buf, 100, "conversion from %.10s to %.10s not supported", imIn->mode, mode + buf, 100, "conversion from %.10s to %.10s not supported", imIn->mode->name, mode->name ); return (Imaging)ImagingError_ValueError(buf); #endif @@ -1663,7 +1539,7 @@ convert( } Imaging -ImagingConvert(Imaging imIn, const char *mode, ImagingPalette palette, int dither) { +ImagingConvert(Imaging imIn, const Mode *mode, ImagingPalette palette, int dither) { return convert(NULL, imIn, mode, palette, dither); } @@ -1673,7 +1549,7 @@ ImagingConvert2(Imaging imOut, Imaging imIn) { } Imaging -ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { +ImagingConvertTransparent(Imaging imIn, const Mode *mode, int r, int g, int b) { ImagingSectionCookie cookie; ImagingShuffler convert; Imaging imOut = NULL; @@ -1687,27 +1563,30 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { return (Imaging)ImagingError_ModeError(); } - if (strcmp(imIn->mode, "RGB") == 0 && - (strcmp(mode, "RGBA") == 0 || strcmp(mode, "RGBa") == 0)) { + if (imIn->mode == IMAGING_MODE_RGB && (mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_RGBa)) { convert = rgb2rgba; - if (strcmp(mode, "RGBa") == 0) { + if (mode == IMAGING_MODE_RGBa) { premultiplied = 1; } - } else if (strcmp(imIn->mode, "RGB") == 0 && - (strcmp(mode, "LA") == 0 || strcmp(mode, "La") == 0)) { + } else if (imIn->mode == IMAGING_MODE_RGB && (mode == IMAGING_MODE_LA || mode == IMAGING_MODE_La)) { convert = rgb2la; source_transparency = 1; - if (strcmp(mode, "La") == 0) { + if (mode == IMAGING_MODE_La) { premultiplied = 1; } - } else if ((strcmp(imIn->mode, "1") == 0 || strcmp(imIn->mode, "I") == 0 || - strcmp(imIn->mode, "I;16") == 0 || strcmp(imIn->mode, "L") == 0) && - (strcmp(mode, "RGBA") == 0 || strcmp(mode, "LA") == 0)) { - if (strcmp(imIn->mode, "1") == 0) { + } else if ((imIn->mode == IMAGING_MODE_1 || + imIn->mode == IMAGING_MODE_I || + imIn->mode == IMAGING_MODE_I_16 || + imIn->mode == IMAGING_MODE_L + ) && ( + mode == IMAGING_MODE_RGBA || + mode == IMAGING_MODE_LA + )) { + if (imIn->mode == IMAGING_MODE_1) { convert = bit2rgb; - } else if (strcmp(imIn->mode, "I") == 0) { + } else if (imIn->mode == IMAGING_MODE_I) { convert = i2rgb; - } else if (strcmp(imIn->mode, "I;16") == 0) { + } else if (imIn->mode == IMAGING_MODE_I_16) { convert = I16_RGB; } else { convert = l2rgb; @@ -1719,8 +1598,8 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { buf, 100, "conversion from %.10s to %.10s not supported in convert_transparent", - imIn->mode, - mode + imIn->mode->name, + mode->name ); return (Imaging)ImagingError_ValueError(buf); } @@ -1743,15 +1622,15 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { } Imaging -ImagingConvertInPlace(Imaging imIn, const char *mode) { +ImagingConvertInPlace(Imaging imIn, const Mode *mode) { ImagingSectionCookie cookie; ImagingShuffler convert; int y; /* limited support for inplace conversion */ - if (strcmp(imIn->mode, "L") == 0 && strcmp(mode, "1") == 0) { + if (imIn->mode == IMAGING_MODE_L && mode == IMAGING_MODE_1) { convert = l2bit; - } else if (strcmp(imIn->mode, "1") == 0 && strcmp(mode, "L") == 0) { + } else if (imIn->mode == IMAGING_MODE_1 && mode == IMAGING_MODE_L) { convert = bit2l; } else { return ImagingError_ModeError(); @@ -1765,3 +1644,154 @@ ImagingConvertInPlace(Imaging imIn, const char *mode) { return imIn; } + +/* ------------------ */ +/* Converter mappings */ +/* ------------------ */ + +void +ImagingConvertInit() { + const struct Converter temp[] = { + {IMAGING_MODE_1, IMAGING_MODE_L, bit2l}, + {IMAGING_MODE_1, IMAGING_MODE_I, bit2i}, + {IMAGING_MODE_1, IMAGING_MODE_F, bit2f}, + {IMAGING_MODE_1, IMAGING_MODE_RGB, bit2rgb}, + {IMAGING_MODE_1, IMAGING_MODE_RGBA, bit2rgb}, + {IMAGING_MODE_1, IMAGING_MODE_RGBX, bit2rgb}, + {IMAGING_MODE_1, IMAGING_MODE_CMYK, bit2cmyk}, + {IMAGING_MODE_1, IMAGING_MODE_YCbCr, bit2ycbcr}, + {IMAGING_MODE_1, IMAGING_MODE_HSV, bit2hsv}, + + {IMAGING_MODE_L, IMAGING_MODE_1, l2bit}, + {IMAGING_MODE_L, IMAGING_MODE_LA, l2la}, + {IMAGING_MODE_L, IMAGING_MODE_I, l2i}, + {IMAGING_MODE_L, IMAGING_MODE_F, l2f}, + {IMAGING_MODE_L, IMAGING_MODE_RGB, l2rgb}, + {IMAGING_MODE_L, IMAGING_MODE_RGBA, l2rgb}, + {IMAGING_MODE_L, IMAGING_MODE_RGBX, l2rgb}, + {IMAGING_MODE_L, IMAGING_MODE_CMYK, l2cmyk}, + {IMAGING_MODE_L, IMAGING_MODE_YCbCr, l2ycbcr}, + {IMAGING_MODE_L, IMAGING_MODE_HSV, l2hsv}, + + {IMAGING_MODE_LA, IMAGING_MODE_L, la2l}, + {IMAGING_MODE_LA, IMAGING_MODE_La, lA2la}, + {IMAGING_MODE_LA, IMAGING_MODE_RGB, la2rgb}, + {IMAGING_MODE_LA, IMAGING_MODE_RGBA, la2rgb}, + {IMAGING_MODE_LA, IMAGING_MODE_RGBX, la2rgb}, + {IMAGING_MODE_LA, IMAGING_MODE_CMYK, la2cmyk}, + {IMAGING_MODE_LA, IMAGING_MODE_YCbCr, la2ycbcr}, + {IMAGING_MODE_LA, IMAGING_MODE_HSV, la2hsv}, + + {IMAGING_MODE_La, IMAGING_MODE_LA, la2lA}, + + {IMAGING_MODE_I, IMAGING_MODE_L, i2l}, + {IMAGING_MODE_I, IMAGING_MODE_F, i2f}, + {IMAGING_MODE_I, IMAGING_MODE_RGB, i2rgb}, + {IMAGING_MODE_I, IMAGING_MODE_RGBA, i2rgb}, + {IMAGING_MODE_I, IMAGING_MODE_RGBX, i2rgb}, + {IMAGING_MODE_I, IMAGING_MODE_HSV, i2hsv}, + + {IMAGING_MODE_F, IMAGING_MODE_L, f2l}, + {IMAGING_MODE_F, IMAGING_MODE_I, f2i}, + + {IMAGING_MODE_RGB, IMAGING_MODE_1, rgb2bit}, + {IMAGING_MODE_RGB, IMAGING_MODE_L, rgb2l}, + {IMAGING_MODE_RGB, IMAGING_MODE_LA, rgb2la}, + {IMAGING_MODE_RGB, IMAGING_MODE_La, rgb2la}, + {IMAGING_MODE_RGB, IMAGING_MODE_I, rgb2i}, + {IMAGING_MODE_RGB, IMAGING_MODE_I_16, rgb2i16l}, + {IMAGING_MODE_RGB, IMAGING_MODE_I_16L, rgb2i16l}, + {IMAGING_MODE_RGB, IMAGING_MODE_I_16B, rgb2i16b}, +#ifdef WORDS_BIGENDIAN + {IMAGING_MODE_RGB, IMAGING_MODE_I_16N, rgb2i16b}, +#else + {IMAGING_MODE_RGB, IMAGING_MODE_I_16N, rgb2i16l}, +#endif + {IMAGING_MODE_RGB, IMAGING_MODE_F, rgb2f}, + {IMAGING_MODE_RGB, IMAGING_MODE_BGR_15, rgb2bgr15}, + {IMAGING_MODE_RGB, IMAGING_MODE_BGR_16, rgb2bgr16}, + {IMAGING_MODE_RGB, IMAGING_MODE_BGR_24, rgb2bgr24}, + {IMAGING_MODE_RGB, IMAGING_MODE_RGBA, rgb2rgba}, + {IMAGING_MODE_RGB, IMAGING_MODE_RGBa, rgb2rgba}, + {IMAGING_MODE_RGB, IMAGING_MODE_RGBX, rgb2rgba}, + {IMAGING_MODE_RGB, IMAGING_MODE_CMYK, rgb2cmyk}, + {IMAGING_MODE_RGB, IMAGING_MODE_YCbCr, ImagingConvertRGB2YCbCr}, + {IMAGING_MODE_RGB, IMAGING_MODE_HSV, rgb2hsv}, + + {IMAGING_MODE_RGBA, IMAGING_MODE_1, rgb2bit}, + {IMAGING_MODE_RGBA, IMAGING_MODE_L, rgb2l}, + {IMAGING_MODE_RGBA, IMAGING_MODE_LA, rgba2la}, + {IMAGING_MODE_RGBA, IMAGING_MODE_I, rgb2i}, + {IMAGING_MODE_RGBA, IMAGING_MODE_F, rgb2f}, + {IMAGING_MODE_RGBA, IMAGING_MODE_RGB, rgba2rgb}, + {IMAGING_MODE_RGBA, IMAGING_MODE_RGBa, rgbA2rgba}, + {IMAGING_MODE_RGBA, IMAGING_MODE_RGBX, rgb2rgba}, + {IMAGING_MODE_RGBA, IMAGING_MODE_CMYK, rgb2cmyk}, + {IMAGING_MODE_RGBA, IMAGING_MODE_YCbCr, ImagingConvertRGB2YCbCr}, + {IMAGING_MODE_RGBA, IMAGING_MODE_HSV, rgb2hsv}, + + {IMAGING_MODE_RGBa, IMAGING_MODE_RGBA, rgba2rgbA}, + {IMAGING_MODE_RGBa, IMAGING_MODE_RGB, rgba2rgb_}, + + {IMAGING_MODE_RGBX, IMAGING_MODE_1, rgb2bit}, + {IMAGING_MODE_RGBX, IMAGING_MODE_L, rgb2l}, + {IMAGING_MODE_RGBX, IMAGING_MODE_LA, rgb2la}, + {IMAGING_MODE_RGBX, IMAGING_MODE_I, rgb2i}, + {IMAGING_MODE_RGBX, IMAGING_MODE_F, rgb2f}, + {IMAGING_MODE_RGBX, IMAGING_MODE_RGB, rgba2rgb}, + {IMAGING_MODE_RGBX, IMAGING_MODE_CMYK, rgb2cmyk}, + {IMAGING_MODE_RGBX, IMAGING_MODE_YCbCr, ImagingConvertRGB2YCbCr}, + {IMAGING_MODE_RGBX, IMAGING_MODE_HSV, rgb2hsv}, + + {IMAGING_MODE_CMYK, IMAGING_MODE_RGB, cmyk2rgb}, + {IMAGING_MODE_CMYK, IMAGING_MODE_RGBA, cmyk2rgb}, + {IMAGING_MODE_CMYK, IMAGING_MODE_RGBX, cmyk2rgb}, + {IMAGING_MODE_CMYK, IMAGING_MODE_HSV, cmyk2hsv}, + + {IMAGING_MODE_YCbCr, IMAGING_MODE_L, ycbcr2l}, + {IMAGING_MODE_YCbCr, IMAGING_MODE_LA, ycbcr2la}, + {IMAGING_MODE_YCbCr, IMAGING_MODE_RGB, ImagingConvertYCbCr2RGB}, + + {IMAGING_MODE_HSV, IMAGING_MODE_RGB, hsv2rgb}, + + {IMAGING_MODE_I, IMAGING_MODE_I_16, I_I16L}, + {IMAGING_MODE_I_16, IMAGING_MODE_I, I16L_I}, + {IMAGING_MODE_I_16, IMAGING_MODE_RGB, I16_RGB}, + {IMAGING_MODE_L, IMAGING_MODE_I_16, L_I16L}, + {IMAGING_MODE_I_16, IMAGING_MODE_L, I16L_L}, + + {IMAGING_MODE_I, IMAGING_MODE_I_16L, I_I16L}, + {IMAGING_MODE_I_16L, IMAGING_MODE_I, I16L_I}, + {IMAGING_MODE_I, IMAGING_MODE_I_16B, I_I16B}, + {IMAGING_MODE_I_16B, IMAGING_MODE_I, I16B_I}, + + {IMAGING_MODE_L, IMAGING_MODE_I_16L, L_I16L}, + {IMAGING_MODE_I_16L, IMAGING_MODE_L, I16L_L}, + {IMAGING_MODE_L, IMAGING_MODE_I_16B, L_I16B}, + {IMAGING_MODE_I_16B, IMAGING_MODE_L, I16B_L}, +#ifdef WORDS_BIGENDIAN + {IMAGING_MODE_L, IMAGING_MODE_I_16N, L_I16B}, + {IMAGING_MODE_I_16N, IMAGING_MODE_L, I16B_L}, +#else + {IMAGING_MODE_L, IMAGING_MODE_I_16N, L_I16L}, + {IMAGING_MODE_I_16N, IMAGING_MODE_L, I16L_L}, +#endif + + {IMAGING_MODE_I_16, IMAGING_MODE_F, I16L_F}, + {IMAGING_MODE_I_16L, IMAGING_MODE_F, I16L_F}, + {IMAGING_MODE_I_16B, IMAGING_MODE_F, I16B_F}, + + {NULL} + }; + converters = malloc(sizeof(temp)); + if (converters == NULL) { + fprintf(stderr, "ConvertInit: failed to allocate memory for converter table\n"); + exit(1); + } + memcpy(converters, temp, sizeof(temp)); +} + +void +ImagingConvertFree() { + free(converters); +} diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 49f17f0daa0..c7e06931353 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -181,6 +181,19 @@ typedef struct ImagingMemoryArena { #endif } *ImagingMemoryArena; +/* Memory Management */ +/* ----------------- */ + +extern void +ImagingAccessInit(void); +extern void +ImagingAccessFree(void); + +extern void +ImagingConvertInit(void); +extern void +ImagingConvertFree(void); + /* Objects */ /* ------- */ @@ -224,8 +237,6 @@ ImagingCopyPalette(Imaging destination, Imaging source); extern void ImagingHistogramDelete(ImagingHistogram histogram); -extern void -ImagingAccessInit(void); extern ImagingAccess ImagingAccessNew(Imaging im); extern void From bcfe5f21729a895388ac1385120dd7a60a6876c4 Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:37:03 +0200 Subject: [PATCH 1903/2374] use mode structs in Draw.c --- src/libImaging/Draw.c | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 27cac687e5d..b946304735f 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -68,7 +68,7 @@ typedef void (*hline_handler)(Imaging, int, int, int, int, Imaging); static inline void point8(Imaging im, int x, int y, int ink) { if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { - if (strncmp(im->mode, "I;16", 4) == 0) { + if (strncmp(im->mode->name, "I;16", 4) == 0) { #ifdef WORDS_BIGENDIAN im->image8[y][x * 2] = (UINT8)(ink >> 8); im->image8[y][x * 2 + 1] = (UINT8)ink; @@ -117,13 +117,13 @@ hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { } if (x0 <= x1) { int bigendian = -1; - if (strncmp(im->mode, "I;16", 4) == 0) { + if (isModeI16(im->mode)) { bigendian = ( #ifdef WORDS_BIGENDIAN - strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16L") == 0 + im->mode == IMAGING_MODE_I_16 || im->mode == IMAGING_MODE_I_16L #else - strcmp(im->mode, "I;16B") == 0 + im->mode == IMAGING_MODE_I_16B #endif ) ? 1 @@ -672,17 +672,17 @@ DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba}; /* Interface */ /* -------------------------------------------------------------------- */ -#define DRAWINIT() \ - if (im->image8) { \ - draw = &draw8; \ - if (strncmp(im->mode, "I;16", 4) == 0) { \ - ink = INK16(ink_); \ - } else { \ - ink = INK8(ink_); \ - } \ - } else { \ - draw = (op) ? &draw32rgba : &draw32; \ - memcpy(&ink, ink_, sizeof(ink)); \ +#define DRAWINIT() \ + if (im->image8) { \ + draw = &draw8; \ + if (strncmp(im->mode->name, "I;16", 4) == 0) { \ + ink = INK16(ink_); \ + } else { \ + ink = INK8(ink_); \ + } \ + } else { \ + draw = (op) ? &draw32rgba : &draw32; \ + memcpy(&ink, ink_, sizeof(ink)); \ } int From b5c4b821bca4373cfb60da07d35bfd4f7f4ff4a5 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 19:22:21 -0500 Subject: [PATCH 1904/2374] use mode structs in Effects.c --- src/libImaging/Effects.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/Effects.c b/src/libImaging/Effects.c index 93e7af0bce9..c05c5764e44 100644 --- a/src/libImaging/Effects.c +++ b/src/libImaging/Effects.c @@ -36,7 +36,7 @@ ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality) { return (Imaging)ImagingError_ValueError(NULL); } - im = ImagingNewDirty("L", xsize, ysize); + im = ImagingNewDirty(IMAGING_MODE_L, xsize, ysize); if (!im) { return NULL; } @@ -80,7 +80,7 @@ ImagingEffectNoise(int xsize, int ysize, float sigma) { int nextok; double this, next; - imOut = ImagingNewDirty("L", xsize, ysize); + imOut = ImagingNewDirty(IMAGING_MODE_L, xsize, ysize); if (!imOut) { return NULL; } From 19c0d1da769f153fc91e5f3da2d23b4cc9cf4fc5 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 19:25:46 -0500 Subject: [PATCH 1905/2374] use mode structs in File.c --- src/libImaging/File.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libImaging/File.c b/src/libImaging/File.c index 901fe83ad27..435dbeca0d4 100644 --- a/src/libImaging/File.c +++ b/src/libImaging/File.c @@ -23,14 +23,13 @@ int ImagingSaveRaw(Imaging im, FILE *fp) { int x, y, i; - if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { + if (im->mode == IMAGING_MODE_1 || im->mode == IMAGING_MODE_L) { /* @PIL227: FIXME: for mode "1", map != 0 to 255 */ /* PGM "L" */ for (y = 0; y < im->ysize; y++) { fwrite(im->image[y], 1, im->xsize, fp); } - } else { /* PPM "RGB" or other internal format */ for (y = 0; y < im->ysize; y++) { @@ -58,10 +57,10 @@ ImagingSavePPM(Imaging im, const char *outfile) { return 0; } - if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { + if (im->mode == IMAGING_MODE_1 || im->mode == IMAGING_MODE_L) { /* Write "PGM" */ fprintf(fp, "P5\n%d %d\n255\n", im->xsize, im->ysize); - } else if (strcmp(im->mode, "RGB") == 0) { + } else if (im->mode == IMAGING_MODE_RGB) { /* Write "PPM" */ fprintf(fp, "P6\n%d %d\n255\n", im->xsize, im->ysize); } else { From 6202eefcff942fea2773c02b6605f27d5c4cf87e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 19:27:30 -0500 Subject: [PATCH 1906/2374] use mode structs in Fill.c --- src/libImaging/Fill.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index 28f42737053..854cdb9fe93 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -68,11 +68,14 @@ ImagingFill(Imaging im, const void *colour) { } Imaging -ImagingFillLinearGradient(const char *mode) { +ImagingFillLinearGradient(const Mode *mode) { Imaging im; int y; - if (strlen(mode) != 1) { + if (mode != IMAGING_MODE_1 && mode != IMAGING_MODE_F && + mode != IMAGING_MODE_I && mode != IMAGING_MODE_L && + mode != IMAGING_MODE_P + ) { return (Imaging)ImagingError_ModeError(); } @@ -102,12 +105,15 @@ ImagingFillLinearGradient(const char *mode) { } Imaging -ImagingFillRadialGradient(const char *mode) { +ImagingFillRadialGradient(const Mode *mode) { Imaging im; int x, y; int d; - if (strlen(mode) != 1) { + if (mode != IMAGING_MODE_1 && mode != IMAGING_MODE_F && + mode != IMAGING_MODE_I && mode != IMAGING_MODE_L && + mode != IMAGING_MODE_P + ) { return (Imaging)ImagingError_ModeError(); } From af22363327dd0d9f5684b12834f2558f8a3acdce Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:39:16 +0200 Subject: [PATCH 1907/2374] use mode structs in Filter.c --- src/libImaging/Filter.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index c46dd3cd1cd..48f21080906 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -155,10 +155,9 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { } else { int bigendian = 0; if (im->type == IMAGING_TYPE_SPECIAL) { - if ( - strcmp(im->mode, "I;16B") == 0 + if (im->mode == IMAGING_MODE_I_16B #ifdef WORDS_BIGENDIAN - || strcmp(im->mode, "I;16N") == 0 + || im->mode == IMAGING_MODE_I_16N #endif ) { bigendian = 1; @@ -309,10 +308,9 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { } else { int bigendian = 0; if (im->type == IMAGING_TYPE_SPECIAL) { - if ( - strcmp(im->mode, "I;16B") == 0 + if (im->mode == IMAGING_MODE_I_16B #ifdef WORDS_BIGENDIAN - || strcmp(im->mode, "I;16N") == 0 + || im->mode == IMAGING_MODE_I_16N #endif ) { bigendian = 1; From cfe9155a0b9b7bd4435d9abfbc61b127a4d415b5 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 12 Oct 2024 21:10:41 -0500 Subject: [PATCH 1908/2374] use mode structs in Geometry.c --- src/libImaging/Geometry.c | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index 1e2abd7e75c..c141ad2a1a4 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -19,7 +19,7 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y, xr; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { @@ -41,7 +41,7 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { + if (strncmp(imIn->mode->name, "I;16", 4) == 0) { FLIP_LEFT_RIGHT(UINT16, image8) } else { FLIP_LEFT_RIGHT(UINT8, image8) @@ -62,7 +62,7 @@ ImagingFlipTopBottom(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int y, yr; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { @@ -89,7 +89,7 @@ ImagingRotate90(Imaging imOut, Imaging imIn) { int x, y, xx, yy, xr, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { @@ -127,7 +127,7 @@ ImagingRotate90(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { + if (strncmp(imIn->mode->name, "I;16", 4) == 0) { ROTATE_90(UINT16, image8); } else { ROTATE_90(UINT8, image8); @@ -149,7 +149,7 @@ ImagingTranspose(Imaging imOut, Imaging imIn) { int x, y, xx, yy, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { @@ -186,7 +186,7 @@ ImagingTranspose(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { + if (strncmp(imIn->mode->name, "I;16", 4) == 0) { TRANSPOSE(UINT16, image8); } else { TRANSPOSE(UINT8, image8); @@ -208,7 +208,7 @@ ImagingTransverse(Imaging imOut, Imaging imIn) { int x, y, xr, yr, xx, yy, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { @@ -247,7 +247,7 @@ ImagingTransverse(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { + if (strncmp(imIn->mode->name, "I;16", 4) == 0) { TRANSVERSE(UINT16, image8); } else { TRANSVERSE(UINT8, image8); @@ -268,7 +268,7 @@ ImagingRotate180(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y, xr, yr; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { @@ -291,7 +291,7 @@ ImagingRotate180(Imaging imOut, Imaging imIn) { yr = imIn->ysize - 1; if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { + if (strncmp(imIn->mode->name, "I;16", 4) == 0) { ROTATE_180(UINT16, image8) } else { ROTATE_180(UINT8, image8) @@ -313,7 +313,7 @@ ImagingRotate270(Imaging imOut, Imaging imIn) { int x, y, xx, yy, yr, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { @@ -351,7 +351,7 @@ ImagingRotate270(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { + if (strncmp(imIn->mode->name, "I;16", 4) == 0) { ROTATE_270(UINT16, image8); } else { ROTATE_270(UINT8, image8); @@ -791,7 +791,7 @@ ImagingGenericTransform( char *out; double xx, yy; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } @@ -848,7 +848,7 @@ ImagingScaleAffine( int xmin, xmax; int *xintab; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } @@ -1035,7 +1035,7 @@ ImagingTransformAffine( double xx, yy; double xo, yo; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } From 2668338583ed765f5e2843650e9784c32652e271 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 19:35:06 -0500 Subject: [PATCH 1909/2374] use mode structs in GetBBox.c --- src/libImaging/GetBBox.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index d430893ddb2..3719a9f1576 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -89,10 +89,11 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { INT32 mask = 0xffffffff; if (im->bands == 3) { ((UINT8 *)&mask)[3] = 0; - } else if (alpha_only && - (strcmp(im->mode, "RGBa") == 0 || strcmp(im->mode, "RGBA") == 0 || - strcmp(im->mode, "La") == 0 || strcmp(im->mode, "LA") == 0 || - strcmp(im->mode, "PA") == 0)) { + } else if (alpha_only && ( + im->mode == IMAGING_MODE_RGBa || im->mode == IMAGING_MODE_RGBA || + im->mode == IMAGING_MODE_La || im->mode == IMAGING_MODE_LA || + im->mode == IMAGING_MODE_PA + )) { #ifdef WORDS_BIGENDIAN mask = 0x000000ff; #else @@ -208,7 +209,7 @@ ImagingGetExtrema(Imaging im, void *extrema) { memcpy(((char *)extrema) + sizeof(fmin), &fmax, sizeof(fmax)); break; case IMAGING_TYPE_SPECIAL: - if (strcmp(im->mode, "I;16") == 0) { + if (im->mode == IMAGING_MODE_I_16) { UINT16 v; UINT8 *pixel = *im->image8; #ifdef WORDS_BIGENDIAN From 27497700ee55e6c42cf4428f3a9dfb34ccdecbe2 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 19:38:07 -0500 Subject: [PATCH 1910/2374] use mode structs in Histo.c --- src/libImaging/Histo.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libImaging/Histo.c b/src/libImaging/Histo.c index c5a547a647b..cfbf8333d24 100644 --- a/src/libImaging/Histo.c +++ b/src/libImaging/Histo.c @@ -43,10 +43,10 @@ ImagingHistogramNew(Imaging im) { if (!h) { return (ImagingHistogram)ImagingError_MemoryError(); } - strncpy(h->mode, im->mode, IMAGING_MODE_LENGTH - 1); - h->mode[IMAGING_MODE_LENGTH - 1] = 0; + h->mode = im->mode; h->bands = im->bands; + h->histogram = calloc(im->pixelsize, 256 * sizeof(long)); if (!h->histogram) { free(h); @@ -73,7 +73,7 @@ ImagingGetHistogram(Imaging im, Imaging imMask, void *minmax) { if (im->xsize != imMask->xsize || im->ysize != imMask->ysize) { return ImagingError_Mismatch(); } - if (strcmp(imMask->mode, "1") != 0 && strcmp(imMask->mode, "L") != 0) { + if (imMask->mode != IMAGING_MODE_1 && imMask->mode != IMAGING_MODE_L) { return ImagingError_ValueError("bad transparency mask"); } } From 33272580d020ea7eed6f5b735fcceb5b36b7ec80 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 19:45:28 -0500 Subject: [PATCH 1911/2374] use mode structs in Jpeg2KDecode.c --- src/libImaging/Jpeg2KDecode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index cc6955ca53d..3cbe2965df9 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -771,7 +771,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { if (color_space == j2k_unpackers[n].color_space && image->numcomps == j2k_unpackers[n].components && (j2k_unpackers[n].subsampling || (subsampling == -1)) && - strcmp(im->mode, j2k_unpackers[n].mode) == 0) { + strcmp(im->mode->name, j2k_unpackers[n].mode) == 0) { unpack = j2k_unpackers[n].unpacker; break; } From 98a2c63326b3c64ea9aafd1f25ea33bfb37b935d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 19:52:05 -0500 Subject: [PATCH 1912/2374] use mode structs in Jpeg2KEncode.c --- src/libImaging/Jpeg2KEncode.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 61e095ad67d..67290f6748f 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -305,28 +305,28 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { #endif /* Setup an opj_image */ - if (strcmp(im->mode, "L") == 0) { + if (im->mode == IMAGING_MODE_L) { components = 1; color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_l; - } else if (strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16B") == 0) { + } else if (im->mode == IMAGING_MODE_I_16 || im->mode == IMAGING_MODE_I_16B) { components = 1; color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_i16; prec = 16; - } else if (strcmp(im->mode, "LA") == 0) { + } else if (im->mode == IMAGING_MODE_LA) { components = 2; color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_la; - } else if (strcmp(im->mode, "RGB") == 0) { + } else if (im->mode == IMAGING_MODE_RGB) { components = 3; color_space = OPJ_CLRSPC_SRGB; pack = j2k_pack_rgb; - } else if (strcmp(im->mode, "YCbCr") == 0) { + } else if (im->mode == IMAGING_MODE_YCbCr) { components = 3; color_space = OPJ_CLRSPC_SYCC; pack = j2k_pack_rgb; - } else if (strcmp(im->mode, "RGBA") == 0) { + } else if (im->mode == IMAGING_MODE_RGBA) { components = 4; color_space = OPJ_CLRSPC_SRGB; pack = j2k_pack_rgba; @@ -497,9 +497,9 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { goto quick_exit; } - if (strcmp(im->mode, "RGBA") == 0) { + if (im->mode == IMAGING_MODE_RGBA) { image->comps[3].alpha = 1; - } else if (strcmp(im->mode, "LA") == 0) { + } else if (im->mode == IMAGING_MODE_LA) { image->comps[1].alpha = 1; } From 30d4cd02296eec22d5acb19c74c02597f65991ab Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 19:58:58 -0500 Subject: [PATCH 1913/2374] use mode structs in JpegDecode.c --- src/libImaging/JpegDecode.c | 14 +++++++------- src/libImaging/Mode.h | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/libImaging/JpegDecode.c b/src/libImaging/JpegDecode.c index 2970f56d1af..36eb7835a7f 100644 --- a/src/libImaging/JpegDecode.c +++ b/src/libImaging/JpegDecode.c @@ -196,22 +196,22 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by /* rawmode indicates what we want from the decoder. if not set, conversions are disabled */ - if (strcmp(context->rawmode, "L") == 0) { + if (context->rawmode == IMAGING_RAWMODE_L) { context->cinfo.out_color_space = JCS_GRAYSCALE; - } else if (strcmp(context->rawmode, "RGB") == 0) { + } else if (context->rawmode == IMAGING_RAWMODE_RGB) { context->cinfo.out_color_space = JCS_RGB; } #ifdef JCS_EXTENSIONS - else if (strcmp(context->rawmode, "RGBX") == 0) { + else if (context->rawmode == IMAGING_RAWMODE_RGBX) { context->cinfo.out_color_space = JCS_EXT_RGBX; } #endif - else if (strcmp(context->rawmode, "CMYK") == 0 || - strcmp(context->rawmode, "CMYK;I") == 0) { + else if (context->rawmode == IMAGING_RAWMODE_CMYK || + context->rawmode == IMAGING_RAWMODE_CMYK_I) { context->cinfo.out_color_space = JCS_CMYK; - } else if (strcmp(context->rawmode, "YCbCr") == 0) { + } else if (context->rawmode == IMAGING_RAWMODE_YCbCr) { context->cinfo.out_color_space = JCS_YCbCr; - } else if (strcmp(context->rawmode, "YCbCrK") == 0) { + } else if (context->rawmode == IMAGING_RAWMODE_YCbCrK) { context->cinfo.out_color_space = JCS_YCCK; } else { /* Disable decoder conversions */ diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index bd184808d23..36deddd02f5 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -79,7 +79,9 @@ extern const RawMode * const IMAGING_RAWMODE_I_32B; // Rawmodes extern const RawMode * const IMAGING_RAWMODE_1_R; +extern const RawMode * const IMAGING_RAWMODE_CMYK_I; extern const RawMode * const IMAGING_RAWMODE_YCC_P; +extern const RawMode * const IMAGING_RAWMODE_YCbCrK; const RawMode * findRawMode(const char * const name); From 0abfdd25b1dddbc0ca4fddf816dff9cbba837d14 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 20:00:42 -0500 Subject: [PATCH 1914/2374] use mode structs in JpegEncode.c --- src/libImaging/JpegEncode.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 972435ee117..098e431fca0 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -114,7 +114,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { break; case 24: context->cinfo.input_components = 3; - if (strcmp(im->mode, "YCbCr") == 0) { + if (im->mode == IMAGING_MODE_YCbCr) { context->cinfo.in_color_space = JCS_YCbCr; } else { context->cinfo.in_color_space = JCS_RGB; @@ -124,7 +124,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { context->cinfo.input_components = 4; context->cinfo.in_color_space = JCS_CMYK; #ifdef JCS_EXTENSIONS - if (strcmp(context->rawmode, "RGBX") == 0) { + if (context->rawmode == IMAGING_RAWMODE_RGBX) { context->cinfo.in_color_space = JCS_EXT_RGBX; } #endif From 378c3bd23d88e713fe5148d1cec35cf7126f6530 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 20:03:46 -0500 Subject: [PATCH 1915/2374] use mode structs in Matrix.c --- src/libImaging/Matrix.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index ec7f4d93e06..fd558461100 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -18,7 +18,7 @@ #define CLIPF(v) ((v <= 0.0) ? 0 : (v >= 255.0F) ? 255 : (UINT8)v) Imaging -ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { +ImagingConvertMatrix(Imaging im, const Mode *mode, float m[]) { Imaging imOut; int x, y; ImagingSectionCookie cookie; @@ -28,8 +28,8 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { return (Imaging)ImagingError_ModeError(); } - if (strcmp(mode, "L") == 0) { - imOut = ImagingNewDirty("L", im->xsize, im->ysize); + if (mode == IMAGING_MODE_L) { + imOut = ImagingNewDirty(IMAGING_MODE_L, im->xsize, im->ysize); if (!imOut) { return NULL; } @@ -46,8 +46,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { } } ImagingSectionLeave(&cookie); - - } else if (strlen(mode) == 3) { + } else if (strlen(mode->name) == 3) { imOut = ImagingNewDirty(mode, im->xsize, im->ysize); if (!imOut) { return NULL; From 49062856196ce78bbf9269aa41f65126b04de706 Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:41:13 +0200 Subject: [PATCH 1916/2374] add function isModeI16() to check if a mode is an I;16 mode --- src/_imaging.c | 6 +----- src/libImaging/Draw.c | 24 ++++++++++++------------ src/libImaging/Geometry.c | 12 ++++++------ src/libImaging/Mode.c | 7 +++++++ src/libImaging/Mode.h | 2 ++ src/libImaging/Paste.c | 6 +++--- src/map.c | 2 +- 7 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 271a16dbb65..bd7cc2233f6 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -662,11 +662,7 @@ getink(PyObject *color, Imaging im, char *ink) { memcpy(ink, &ftmp, sizeof(ftmp)); return ink; case IMAGING_TYPE_SPECIAL: - if (im->mode == IMAGING_MODE_I_16 - || im->mode == IMAGING_MODE_I_16L - || im->mode == IMAGING_MODE_I_16B - || im->mode == IMAGING_MODE_I_16N - ) { + if (isModeI16(im->mode)) { ink[0] = (UINT8)r; ink[1] = (UINT8)(r >> 8); ink[2] = ink[3] = 0; diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index b946304735f..d2898043256 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -68,7 +68,7 @@ typedef void (*hline_handler)(Imaging, int, int, int, int, Imaging); static inline void point8(Imaging im, int x, int y, int ink) { if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { - if (strncmp(im->mode->name, "I;16", 4) == 0) { + if (isModeI16(im->mode)) { #ifdef WORDS_BIGENDIAN im->image8[y][x * 2] = (UINT8)(ink >> 8); im->image8[y][x * 2 + 1] = (UINT8)ink; @@ -672,17 +672,17 @@ DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba}; /* Interface */ /* -------------------------------------------------------------------- */ -#define DRAWINIT() \ - if (im->image8) { \ - draw = &draw8; \ - if (strncmp(im->mode->name, "I;16", 4) == 0) { \ - ink = INK16(ink_); \ - } else { \ - ink = INK8(ink_); \ - } \ - } else { \ - draw = (op) ? &draw32rgba : &draw32; \ - memcpy(&ink, ink_, sizeof(ink)); \ +#define DRAWINIT() \ + if (im->image8) { \ + draw = &draw8; \ + if (isModeI16(im->mode)) { \ + ink = INK16(ink_); \ + } else { \ + ink = INK8(ink_); \ + } \ + } else { \ + draw = (op) ? &draw32rgba : &draw32; \ + memcpy(&ink, ink_, sizeof(ink)); \ } int diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index c141ad2a1a4..80ecd7cb6b3 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -41,7 +41,7 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode->name, "I;16", 4) == 0) { + if (isModeI16(imIn->mode)) { FLIP_LEFT_RIGHT(UINT16, image8) } else { FLIP_LEFT_RIGHT(UINT8, image8) @@ -127,7 +127,7 @@ ImagingRotate90(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode->name, "I;16", 4) == 0) { + if (isModeI16(imIn->mode)) { ROTATE_90(UINT16, image8); } else { ROTATE_90(UINT8, image8); @@ -186,7 +186,7 @@ ImagingTranspose(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode->name, "I;16", 4) == 0) { + if (isModeI16(imIn->mode)) { TRANSPOSE(UINT16, image8); } else { TRANSPOSE(UINT8, image8); @@ -247,7 +247,7 @@ ImagingTransverse(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode->name, "I;16", 4) == 0) { + if (isModeI16(imIn->mode)) { TRANSVERSE(UINT16, image8); } else { TRANSVERSE(UINT8, image8); @@ -291,7 +291,7 @@ ImagingRotate180(Imaging imOut, Imaging imIn) { yr = imIn->ysize - 1; if (imIn->image8) { - if (strncmp(imIn->mode->name, "I;16", 4) == 0) { + if (isModeI16(imIn->mode)) { ROTATE_180(UINT16, image8) } else { ROTATE_180(UINT8, image8) @@ -351,7 +351,7 @@ ImagingRotate270(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode->name, "I;16", 4) == 0) { + if (isModeI16(imIn->mode)) { ROTATE_270(UINT16, image8); } else { ROTATE_270(UINT8, image8); diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c index 85ba50e3f52..a11b65905e8 100644 --- a/src/libImaging/Mode.c +++ b/src/libImaging/Mode.c @@ -152,3 +152,10 @@ const RawMode * findRawMode(const char * const name) { } return NULL; } + +int isModeI16(const Mode * const mode) { + return mode == IMAGING_MODE_I_16 + || mode == IMAGING_MODE_I_16L + || mode == IMAGING_MODE_I_16B + || mode == IMAGING_MODE_I_16N; +} diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index 36deddd02f5..d1035efe8df 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -86,4 +86,6 @@ extern const RawMode * const IMAGING_RAWMODE_YCbCrK; const RawMode * findRawMode(const char * const name); +int isModeI16(const Mode * const mode); + #endif // __MODE_H__ diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index 86085942a2e..5d745d0c13b 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -67,8 +67,8 @@ paste_mask_1( int x, y; if (imOut->image8) { - int in_i16 = strncmp(imIn->mode, "I;16", 4) == 0; - int out_i16 = strncmp(imOut->mode, "I;16", 4) == 0; + int in_i16 = isModeI16(imIn->mode); + int out_i16 = isModeI16(imOut->mode); for (y = 0; y < ysize; y++) { UINT8 *out = imOut->image8[y + dy] + dx; if (out_i16) { @@ -437,7 +437,7 @@ fill_mask_L( unsigned int tmp1; if (imOut->image8) { - int i16 = strncmp(imOut->mode, "I;16", 4) == 0; + int i16 = isModeI16(imOut->mode); for (y = 0; y < ysize; y++) { UINT8 *out = imOut->image8[y + dy] + dx; if (i16) { diff --git a/src/map.c b/src/map.c index 9a3144ab904..f4933e45e93 100644 --- a/src/map.c +++ b/src/map.c @@ -85,7 +85,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { if (stride <= 0) { if (!strcmp(mode, "L") || !strcmp(mode, "P")) { stride = xsize; - } else if (!strncmp(mode, "I;16", 4)) { + } else if (isModeI16(mode)) { stride = xsize * 2; } else { stride = xsize * 4; From e5bc5b4ffacb00c7793e65b381f7c5dbfabfb86c Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:47:07 +0200 Subject: [PATCH 1917/2374] use mode structs in Pack.c --- src/_imaging.c | 1 + src/libImaging/Imaging.h | 5 ++ src/libImaging/Mode.h | 41 ++++++++++ src/libImaging/Pack.c | 163 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 201 insertions(+), 9 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index bd7cc2233f6..22276590a89 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4319,6 +4319,7 @@ setup_module(PyObject *m) { ImagingAccessInit(); ImagingConvertInit(); + ImagingPackInit(); #ifdef HAVE_LIBJPEG { diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index c7e06931353..c3efb2cda43 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -194,6 +194,11 @@ ImagingConvertInit(void); extern void ImagingConvertFree(void); +extern void +ImagingPackInit(void); +extern void +ImagingPackFree(void); + /* Objects */ /* ------- */ diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index d1035efe8df..cedad1e0322 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -78,10 +78,51 @@ extern const RawMode * const IMAGING_RAWMODE_I_32L; extern const RawMode * const IMAGING_RAWMODE_I_32B; // Rawmodes +extern const RawMode * const IMAGING_RAWMODE_1_I; +extern const RawMode * const IMAGING_RAWMODE_1_IR; extern const RawMode * const IMAGING_RAWMODE_1_R; +extern const RawMode * const IMAGING_RAWMODE_A; +extern const RawMode * const IMAGING_RAWMODE_ABGR; +extern const RawMode * const IMAGING_RAWMODE_B; +extern const RawMode * const IMAGING_RAWMODE_BGR; +extern const RawMode * const IMAGING_RAWMODE_BGRA; +extern const RawMode * const IMAGING_RAWMODE_BGRX; +extern const RawMode * const IMAGING_RAWMODE_BGRa; +extern const RawMode * const IMAGING_RAWMODE_C; extern const RawMode * const IMAGING_RAWMODE_CMYK_I; +extern const RawMode * const IMAGING_RAWMODE_CMYK_L; +extern const RawMode * const IMAGING_RAWMODE_Cb; +extern const RawMode * const IMAGING_RAWMODE_Cr; +extern const RawMode * const IMAGING_RAWMODE_F_32F; +extern const RawMode * const IMAGING_RAWMODE_F_32NF; +extern const RawMode * const IMAGING_RAWMODE_G; +extern const RawMode * const IMAGING_RAWMODE_H; +extern const RawMode * const IMAGING_RAWMODE_I_32NS; +extern const RawMode * const IMAGING_RAWMODE_I_32S; +extern const RawMode * const IMAGING_RAWMODE_K; +extern const RawMode * const IMAGING_RAWMODE_LA_L; +extern const RawMode * const IMAGING_RAWMODE_L_16; +extern const RawMode * const IMAGING_RAWMODE_L_16B; +extern const RawMode * const IMAGING_RAWMODE_M; +extern const RawMode * const IMAGING_RAWMODE_PA_L; +extern const RawMode * const IMAGING_RAWMODE_P_1; +extern const RawMode * const IMAGING_RAWMODE_P_2; +extern const RawMode * const IMAGING_RAWMODE_P_4; +extern const RawMode * const IMAGING_RAWMODE_R; +extern const RawMode * const IMAGING_RAWMODE_RGBA_L; +extern const RawMode * const IMAGING_RAWMODE_RGBX_L; +extern const RawMode * const IMAGING_RAWMODE_RGB_L; +extern const RawMode * const IMAGING_RAWMODE_S; +extern const RawMode * const IMAGING_RAWMODE_V; +extern const RawMode * const IMAGING_RAWMODE_X; +extern const RawMode * const IMAGING_RAWMODE_XBGR; +extern const RawMode * const IMAGING_RAWMODE_XRGB; +extern const RawMode * const IMAGING_RAWMODE_Y; extern const RawMode * const IMAGING_RAWMODE_YCC_P; extern const RawMode * const IMAGING_RAWMODE_YCbCrK; +extern const RawMode * const IMAGING_RAWMODE_YCbCrX; +extern const RawMode * const IMAGING_RAWMODE_YCbCr_L; +extern const RawMode * const IMAGING_RAWMODE_aBGR; const RawMode * findRawMode(const char * const name); diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index 7f8a50d198e..da714daccd1 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -518,9 +518,9 @@ band3(UINT8 *out, const UINT8 *in, int pixels) { } } -static struct { - const char *mode; - const char *rawmode; +static struct Packer { + const Mode *mode; + const RawMode *rawmode; int bits; ImagingShuffler pack; } packers[] = { @@ -656,13 +656,10 @@ static struct { }; ImagingShuffler -ImagingFindPacker(const char *mode, const char *rawmode, int *bits_out) { +ImagingFindPacker(const Mode *mode, const RawMode *rawmode, int *bits_out) { int i; - - /* find a suitable pixel packer */ - for (i = 0; packers[i].rawmode; i++) { - if (strcmp(packers[i].mode, mode) == 0 && - strcmp(packers[i].rawmode, rawmode) == 0) { + for (i = 0; packers[i].mode; i++) { + if (packers[i].mode == mode && packers[i].rawmode == rawmode) { if (bits_out) { *bits_out = packers[i].bits; } @@ -671,3 +668,151 @@ ImagingFindPacker(const char *mode, const char *rawmode, int *bits_out) { } return NULL; } + +void +ImagingPackInit(void) { + const struct Packer temp[] = { + /* bilevel */ + {IMAGING_MODE_1, IMAGING_RAWMODE_1, 1, pack1}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_I, 1, pack1I}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_R, 1, pack1R}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_IR, 1, pack1IR}, + {IMAGING_MODE_1, IMAGING_RAWMODE_L, 8, pack1L}, + + /* grayscale */ + {IMAGING_MODE_L, IMAGING_RAWMODE_L, 8, copy1}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_16, 16, packL16}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_16B, 16, packL16B}, + + /* grayscale w. alpha */ + {IMAGING_MODE_LA, IMAGING_RAWMODE_LA, 16, packLA}, + {IMAGING_MODE_LA, IMAGING_RAWMODE_LA_L, 16, packLAL}, + + /* grayscale w. alpha premultiplied */ + {IMAGING_MODE_La, IMAGING_RAWMODE_La, 16, packLA}, + + /* palette */ + {IMAGING_MODE_P, IMAGING_RAWMODE_P_1, 1, pack1}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_2, 2, packP2}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_4, 4, packP4}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P, 8, copy1}, + + /* palette w. alpha */ + {IMAGING_MODE_PA, IMAGING_RAWMODE_PA, 16, packLA}, + {IMAGING_MODE_PA, IMAGING_RAWMODE_PA_L, 16, packLAL}, + + /* true colour */ + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB, 24, ImagingPackRGB}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX, 32, copy4}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBA, 32, copy4}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_XRGB, 32, ImagingPackXRGB}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR, 24, ImagingPackBGR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGRX, 32, ImagingPackBGRX}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_XBGR, 32, ImagingPackXBGR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_L, 24, packRGBL}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B, 8, band2}, + + /* true colour w. alpha */ + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA, 32, copy4}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_L, 32, packRGBXL}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGB, 24, ImagingPackRGB}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGR, 24, ImagingPackBGR}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA, 32, ImagingPackBGRA}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_ABGR, 32, ImagingPackABGR}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRa, 32, ImagingPackBGRa}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A, 8, band3}, + + /* true colour w. alpha premultiplied */ + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_RGBa, 32, copy4}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_BGRa, 32, ImagingPackBGRA}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_aBGR, 32, ImagingPackABGR}, + + /* true colour w. padding */ + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX, 32, copy4}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_L, 32, packRGBXL}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB, 24, ImagingPackRGB}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR, 24, ImagingPackBGR}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGRX, 32, ImagingPackBGRX}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_XBGR, 32, ImagingPackXBGR}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_X, 8, band3}, + + /* colour separation */ + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK, 32, copy4}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_I, 32, copy4I}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_L, 32, packRGBXL}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_C, 8, band0}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_M, 8, band1}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y, 8, band2}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_K, 8, band3}, + + /* video (YCbCr) */ + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr, 24, ImagingPackRGB}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr_L, 24, packRGBL}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrX, 32, copy4}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrK, 32, copy4}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_Y, 8, band0}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_Cb, 8, band1}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_Cr, 8, band2}, + + /* LAB Color */ + {IMAGING_MODE_LAB, IMAGING_RAWMODE_LAB, 24, ImagingPackLAB}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_L, 8, band0}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_A, 8, band1}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_B, 8, band2}, + + /* HSV */ + {IMAGING_MODE_HSV, IMAGING_RAWMODE_HSV, 24, ImagingPackRGB}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_H, 8, band0}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_S, 8, band1}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_V, 8, band2}, + + /* integer */ + {IMAGING_MODE_I, IMAGING_RAWMODE_I, 32, copy4}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16B, 16, packI16B}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32S, 32, packI32S}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32NS, 32, copy4}, + + /* floating point */ + {IMAGING_MODE_F, IMAGING_RAWMODE_F, 32, copy4}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32F, 32, packI32S}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32NF, 32, copy4}, + + /* storage modes */ + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16, 16, copy2}, +#ifdef WORDS_BIGENDIAN + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, packI16N_I16}, +#else + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, packI16N_I16B}, +#endif + {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16B, 16, copy2}, + {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16L, 16, copy2}, + {IMAGING_MODE_I_16N, IMAGING_RAWMODE_I_16N, 16, copy2}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, packI16N_I16}, // LibTiff native->image endian. + {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, packI16N_I16}, + {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, packI16N_I16B}, + {IMAGING_MODE_BGR_15, IMAGING_RAWMODE_BGR_15, 16, copy2}, + {IMAGING_MODE_BGR_16, IMAGING_RAWMODE_BGR_16, 16, copy2}, + {IMAGING_MODE_BGR_24, IMAGING_RAWMODE_BGR_24, 24, copy3}, + + {NULL} /* sentinel */ + }; + packers = malloc(sizeof(temp)); + if (packers == NULL) { + fprintf(stderr, "PackInit: failed to allocate memory for packers table\n"); + exit(1); + } + memcpy(packers, temp, sizeof(temp)); +} + +void +ImagingPackFree(void) { + free(packers); +} From af3c24e12b2982a4713f80931164422acef0433b Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 22:29:12 -0500 Subject: [PATCH 1918/2374] use mode structs in Palette.c --- src/libImaging/Mode.h | 4 ---- src/libImaging/Palette.c | 9 ++++----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index cedad1e0322..09810b584d6 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -2,10 +2,6 @@ #define __MODE_H__ -// Maximum length (including null terminator) for both mode and rawmode names. -#define IMAGING_MODE_LENGTH 6+1 - - typedef struct { const char * const name; } Mode; diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c index 78916bca52b..6b4fea6a5fd 100644 --- a/src/libImaging/Palette.c +++ b/src/libImaging/Palette.c @@ -21,13 +21,13 @@ #include ImagingPalette -ImagingPaletteNew(const char *mode) { +ImagingPaletteNew(const Mode *mode) { /* Create a palette object */ int i; ImagingPalette palette; - if (strcmp(mode, "RGB") && strcmp(mode, "RGBA")) { + if (mode != IMAGING_MODE_RGB && mode != IMAGING_MODE_RGBA) { return (ImagingPalette)ImagingError_ModeError(); } @@ -36,8 +36,7 @@ ImagingPaletteNew(const char *mode) { return (ImagingPalette)ImagingError_MemoryError(); } - strncpy(palette->mode, mode, IMAGING_MODE_LENGTH - 1); - palette->mode[IMAGING_MODE_LENGTH - 1] = 0; + palette->mode = mode; palette->size = 0; for (i = 0; i < 256; i++) { @@ -54,7 +53,7 @@ ImagingPaletteNewBrowser(void) { int i, r, g, b; ImagingPalette palette; - palette = ImagingPaletteNew("RGB"); + palette = ImagingPaletteNew(IMAGING_MODE_RGB); if (!palette) { return NULL; } From 2a9d712ceb71b7e14dc73782076280aae1dbc362 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 22:32:54 -0500 Subject: [PATCH 1919/2374] use mode structs in Paste.c --- src/libImaging/Paste.c | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index 5d745d0c13b..54dd270e6f0 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -307,31 +307,26 @@ ImagingPaste( ImagingSectionEnter(&cookie); paste(imOut, imIn, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "1") == 0) { + } else if (imMask->mode == IMAGING_MODE_1) { ImagingSectionEnter(&cookie); paste_mask_1(imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "L") == 0) { + } else if (imMask->mode == IMAGING_MODE_L) { ImagingSectionEnter(&cookie); paste_mask_L(imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "LA") == 0 || strcmp(imMask->mode, "RGBA") == 0) { + } else if (imMask->mode == IMAGING_MODE_LA || imMask->mode == IMAGING_MODE_RGBA) { ImagingSectionEnter(&cookie); paste_mask_RGBA( imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize ); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "RGBa") == 0) { + } else if (imMask->mode == IMAGING_MODE_RGBa) { ImagingSectionEnter(&cookie); paste_mask_RGBa( imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize ); ImagingSectionLeave(&cookie); - } else { (void)ImagingError_ValueError("bad transparency mask"); return -1; @@ -455,10 +450,11 @@ fill_mask_L( } } else { - int alpha_channel = - strcmp(imOut->mode, "RGBa") == 0 || strcmp(imOut->mode, "RGBA") == 0 || - strcmp(imOut->mode, "La") == 0 || strcmp(imOut->mode, "LA") == 0 || - strcmp(imOut->mode, "PA") == 0; + int alpha_channel = imOut->mode == IMAGING_MODE_RGBa || + imOut->mode == IMAGING_MODE_RGBA || + imOut->mode == IMAGING_MODE_La || + imOut->mode == IMAGING_MODE_LA || + imOut->mode == IMAGING_MODE_PA; for (y = 0; y < ysize; y++) { UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx * pixelsize; UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; @@ -617,27 +613,22 @@ ImagingFill2( ImagingSectionEnter(&cookie); fill(imOut, ink, dx0, dy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "1") == 0) { + } else if (imMask->mode == IMAGING_MODE_1) { ImagingSectionEnter(&cookie); fill_mask_1(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "L") == 0) { + } else if (imMask->mode == IMAGING_MODE_L) { ImagingSectionEnter(&cookie); fill_mask_L(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "RGBA") == 0) { + } else if (imMask->mode == IMAGING_MODE_RGBA) { ImagingSectionEnter(&cookie); fill_mask_RGBA(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "RGBa") == 0) { + } else if (imMask->mode == IMAGING_MODE_RGBa) { ImagingSectionEnter(&cookie); fill_mask_RGBa(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - } else { (void)ImagingError_ValueError("bad transparency mask"); return -1; From 7e48697f8269d1d9caefd0516951cd52b31a154d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 22:36:23 -0500 Subject: [PATCH 1920/2374] use mode structs in Point.c --- src/libImaging/Point.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index b11ea62ed85..e33f38508dc 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -128,7 +128,7 @@ im_point_32_8(Imaging imOut, Imaging imIn, im_point_context *context) { } Imaging -ImagingPoint(Imaging imIn, const char *mode, const void *table) { +ImagingPoint(Imaging imIn, const Mode *mode, const void *table) { /* lookup table transform */ ImagingSectionCookie cookie; @@ -145,10 +145,10 @@ ImagingPoint(Imaging imIn, const char *mode, const void *table) { } if (imIn->type != IMAGING_TYPE_UINT8) { - if (imIn->type != IMAGING_TYPE_INT32 || strcmp(mode, "L") != 0) { + if (imIn->type != IMAGING_TYPE_INT32 || mode != IMAGING_MODE_L) { goto mode_mismatch; } - } else if (!imIn->image8 && strcmp(imIn->mode, mode) != 0) { + } else if (!imIn->image8 && imIn->mode != mode) { goto mode_mismatch; } @@ -210,8 +210,8 @@ ImagingPointTransform(Imaging imIn, double scale, double offset) { Imaging imOut; int x, y; - if (!imIn || (strcmp(imIn->mode, "I") != 0 && strcmp(imIn->mode, "I;16") != 0 && - strcmp(imIn->mode, "F") != 0)) { + if (!imIn || (imIn->mode != IMAGING_MODE_I && + imIn->mode != IMAGING_MODE_I_16 && imIn->mode != IMAGING_MODE_F)) { return (Imaging)ImagingError_ModeError(); } @@ -245,7 +245,7 @@ ImagingPointTransform(Imaging imIn, double scale, double offset) { ImagingSectionLeave(&cookie); break; case IMAGING_TYPE_SPECIAL: - if (strcmp(imIn->mode, "I;16") == 0) { + if (imIn->mode == IMAGING_MODE_I_16) { ImagingSectionEnter(&cookie); for (y = 0; y < imIn->ysize; y++) { char *in = (char *)imIn->image[y]; From fb73d9003e62d2023176d39c84e0ff3f258debb8 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 22:38:11 -0500 Subject: [PATCH 1921/2374] use mode structs in Quant.c --- src/libImaging/Quant.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index a489a882db2..b1397c5f054 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -1684,13 +1684,13 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { return (Imaging)ImagingError_ValueError("bad number of colors"); } - if (strcmp(im->mode, "L") != 0 && strcmp(im->mode, "P") != 0 && - strcmp(im->mode, "RGB") != 0 && strcmp(im->mode, "RGBA") != 0) { + if (im->mode != IMAGING_MODE_L && im->mode != IMAGING_MODE_P && + im->mode != IMAGING_MODE_RGB && im->mode != IMAGING_MODE_RGBA) { return ImagingError_ModeError(); } /* only octree and imagequant supports RGBA */ - if (!strcmp(im->mode, "RGBA") && mode != 2 && mode != 3) { + if (im->mode == IMAGING_MODE_RGBA && mode != 2 && mode != 3) { return ImagingError_ModeError(); } @@ -1708,7 +1708,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { /* FIXME: maybe we could load the hash tables directly from the image data? */ - if (!strcmp(im->mode, "L")) { + if (im->mode == IMAGING_MODE_L) { /* grayscale */ /* FIXME: converting a "L" image to "P" with 256 colors @@ -1721,7 +1721,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { } } - } else if (!strcmp(im->mode, "P")) { + } else if (im->mode == IMAGING_MODE_P) { /* palette */ pp = im->palette->palette; @@ -1736,10 +1736,10 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { } } - } else if (!strcmp(im->mode, "RGB") || !strcmp(im->mode, "RGBA")) { + } else if (im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA) { /* true colour */ - withAlpha = !strcmp(im->mode, "RGBA"); + withAlpha = im->mode == IMAGING_MODE_RGBA; int transparency = 0; unsigned char r = 0, g = 0, b = 0; for (i = y = 0; y < im->ysize; y++) { @@ -1830,7 +1830,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { ImagingSectionLeave(&cookie); if (result > 0) { - imOut = ImagingNewDirty("P", im->xsize, im->ysize); + imOut = ImagingNewDirty(IMAGING_MODE_P, im->xsize, im->ysize); ImagingSectionEnter(&cookie); for (i = y = 0; y < im->ysize; y++) { @@ -1855,7 +1855,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { } if (withAlpha) { - strcpy(imOut->palette->mode, "RGBA"); + imOut->palette->mode = IMAGING_MODE_RGBA; } free(palette); From c80fba3045893918f1e51c4df3934e314b2c5f8d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 22:40:40 -0500 Subject: [PATCH 1922/2374] use mode structs in Reduce.c --- src/libImaging/Reduce.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Reduce.c b/src/libImaging/Reduce.c index 022daa0003f..a4e58ced81b 100644 --- a/src/libImaging/Reduce.c +++ b/src/libImaging/Reduce.c @@ -1452,7 +1452,7 @@ ImagingReduce(Imaging imIn, int xscale, int yscale, int box[4]) { ImagingSectionCookie cookie; Imaging imOut = NULL; - if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0) { + if (imIn->mode == IMAGING_MODE_P || imIn->mode == IMAGING_MODE_1) { return (Imaging)ImagingError_ModeError(); } From 858b0b38059a6162c3317c2a5081d4ee7b2ff9b1 Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:47:47 +0200 Subject: [PATCH 1923/2374] use mode structs in Resample.c --- src/libImaging/Resample.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index b114e002330..3ab43a8955a 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -470,10 +470,9 @@ ImagingResampleHorizontal_16bpc( double *k; int bigendian = 0; - if ( - strcmp(imIn->mode, "I;16N") == 0 + if (imIn->mode == IMAGING_MODE_I_16N #ifdef WORDS_BIGENDIAN - || strcmp(imIn->mode, "I;16B") == 0 + || imIn->mode == IMAGING_MODE_I_16B #endif ) { bigendian = 1; @@ -510,10 +509,9 @@ ImagingResampleVertical_16bpc( double *k; int bigendian = 0; - if ( - strcmp(imIn->mode, "I;16N") == 0 + if (imIn->mode == IMAGING_MODE_I_16N #ifdef WORDS_BIGENDIAN - || strcmp(imIn->mode, "I;16B") == 0 + || imIn->mode == IMAGING_MODE_I_16B #endif ) { bigendian = 1; @@ -648,12 +646,12 @@ ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) { ResampleFunction ResampleHorizontal; ResampleFunction ResampleVertical; - if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0) { + if (imIn->mode == IMAGING_MODE_P || imIn->mode == IMAGING_MODE_1) { return (Imaging)ImagingError_ModeError(); } if (imIn->type == IMAGING_TYPE_SPECIAL) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { + if (isModeI16(imIn->mode)) { ResampleHorizontal = ImagingResampleHorizontal_16bpc; ResampleVertical = ImagingResampleVertical_16bpc; } else { From e75a0a9c39ab16da44482be77c21b2f08fea9923 Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:54:11 +0200 Subject: [PATCH 1924/2374] use mode structs in Storage.c --- src/libImaging/Storage.c | 59 ++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 4640f078a62..38142b7c5e1 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -42,7 +42,7 @@ */ Imaging -ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { +ImagingNewPrologueSubtype(const Mode *mode, int xsize, int ysize, int size) { Imaging im; /* linesize overflow check, roughly the current largest space req'd */ @@ -62,37 +62,37 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->type = IMAGING_TYPE_UINT8; strcpy(im->arrow_band_format, "C"); - if (strcmp(mode, "1") == 0) { + if (mode == IMAGING_MODE_1) { /* 1-bit images */ im->bands = im->pixelsize = 1; im->linesize = xsize; strcpy(im->band_names[0], "1"); - } else if (strcmp(mode, "P") == 0) { + } else if (mode == IMAGING_MODE_P) { /* 8-bit palette mapped images */ im->bands = im->pixelsize = 1; im->linesize = xsize; - im->palette = ImagingPaletteNew("RGB"); + im->palette = ImagingPaletteNew(IMAGING_MODE_RGB); strcpy(im->band_names[0], "P"); - } else if (strcmp(mode, "PA") == 0) { + } else if (mode == IMAGING_MODE_PA) { /* 8-bit palette with alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ im->linesize = xsize * 4; - im->palette = ImagingPaletteNew("RGB"); + im->palette = ImagingPaletteNew(IMAGING_MODE_RGB); strcpy(im->band_names[0], "P"); strcpy(im->band_names[1], "X"); strcpy(im->band_names[2], "X"); strcpy(im->band_names[3], "A"); - } else if (strcmp(mode, "L") == 0) { + } else if (mode == IMAGING_MODE_L) { /* 8-bit grayscale (luminance) images */ im->bands = im->pixelsize = 1; im->linesize = xsize; strcpy(im->band_names[0], "L"); - } else if (strcmp(mode, "LA") == 0) { + } else if (mode == IMAGING_MODE_LA) { /* 8-bit grayscale (luminance) with alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ @@ -102,7 +102,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { strcpy(im->band_names[2], "X"); strcpy(im->band_names[3], "A"); - } else if (strcmp(mode, "La") == 0) { + } else if (mode == IMAGING_MODE_La) { /* 8-bit grayscale (luminance) with premultiplied alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ @@ -112,7 +112,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { strcpy(im->band_names[2], "X"); strcpy(im->band_names[3], "a"); - } else if (strcmp(mode, "F") == 0) { + } else if (mode == IMAGING_MODE_F) { /* 32-bit floating point images */ im->bands = 1; im->pixelsize = 4; @@ -121,7 +121,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { strcpy(im->arrow_band_format, "f"); strcpy(im->band_names[0], "F"); - } else if (strcmp(mode, "I") == 0) { + } else if (mode == IMAGING_MODE_I) { /* 32-bit integer images */ im->bands = 1; im->pixelsize = 4; @@ -130,8 +130,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { strcpy(im->arrow_band_format, "i"); strcpy(im->band_names[0], "I"); - } else if (strcmp(mode, "I;16") == 0 || strcmp(mode, "I;16L") == 0 || - strcmp(mode, "I;16B") == 0 || strcmp(mode, "I;16N") == 0) { + } else if (isModeI16(mode)) { /* EXPERIMENTAL */ /* 16-bit raw integer images */ im->bands = 1; @@ -141,7 +140,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { strcpy(im->arrow_band_format, "s"); strcpy(im->band_names[0], "I"); - } else if (strcmp(mode, "RGB") == 0) { + } else if (mode == IMAGING_MODE_RGB) { /* 24-bit true colour images */ im->bands = 3; im->pixelsize = 4; @@ -151,7 +150,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { strcpy(im->band_names[2], "B"); strcpy(im->band_names[3], "X"); - } else if (strcmp(mode, "RGBX") == 0) { + } else if (mode == IMAGING_MODE_RGBX) { /* 32-bit true colour images with padding */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; @@ -160,7 +159,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { strcpy(im->band_names[2], "B"); strcpy(im->band_names[3], "X"); - } else if (strcmp(mode, "RGBA") == 0) { + } else if (mode == IMAGING_MODE_RGBA) { /* 32-bit true colour images with alpha */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; @@ -169,7 +168,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { strcpy(im->band_names[2], "B"); strcpy(im->band_names[3], "A"); - } else if (strcmp(mode, "RGBa") == 0) { + } else if (mode == IMAGING_MODE_RGBa) { /* 32-bit true colour images with premultiplied alpha */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; @@ -178,7 +177,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { strcpy(im->band_names[2], "B"); strcpy(im->band_names[3], "a"); - } else if (strcmp(mode, "CMYK") == 0) { + } else if (mode == IMAGING_MODE_CMYK) { /* 32-bit colour separation */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; @@ -187,7 +186,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { strcpy(im->band_names[2], "Y"); strcpy(im->band_names[3], "K"); - } else if (strcmp(mode, "YCbCr") == 0) { + } else if (mode == IMAGING_MODE_YCbCr) { /* 24-bit video format */ im->bands = 3; im->pixelsize = 4; @@ -197,7 +196,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { strcpy(im->band_names[2], "Cr"); strcpy(im->band_names[3], "X"); - } else if (strcmp(mode, "LAB") == 0) { + } else if (mode == IMAGING_MODE_LAB) { /* 24-bit color, luminance, + 2 color channels */ /* L is uint8, a,b are int8 */ im->bands = 3; @@ -208,7 +207,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { strcpy(im->band_names[2], "b"); strcpy(im->band_names[3], "X"); - } else if (strcmp(mode, "HSV") == 0) { + } else if (mode == IMAGING_MODE_HSV) { /* 24-bit color, luminance, + 2 color channels */ /* L is uint8, a,b are int8 */ im->bands = 3; @@ -225,7 +224,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } /* Setup image descriptor */ - strcpy(im->mode, mode); + im->mode = mode; /* Pointer array (allocate at least one line, to avoid MemoryError exceptions on platforms where calloc(0, x) returns NULL) */ @@ -257,7 +256,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } Imaging -ImagingNewPrologue(const char *mode, int xsize, int ysize) { +ImagingNewPrologue(const Mode *mode, int xsize, int ysize) { return ImagingNewPrologueSubtype( mode, xsize, ysize, sizeof(struct ImagingMemoryInstance) ); @@ -594,7 +593,7 @@ ImagingBorrowArrow( */ Imaging -ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { +ImagingNewInternal(const Mode *mode, int xsize, int ysize, int dirty) { Imaging im; if (xsize < 0 || ysize < 0) { @@ -630,7 +629,7 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { } Imaging -ImagingNew(const char *mode, int xsize, int ysize) { +ImagingNew(const Mode *mode, int xsize, int ysize) { if (ImagingDefaultArena.use_block_allocator) { return ImagingNewBlock(mode, xsize, ysize); } @@ -638,7 +637,7 @@ ImagingNew(const char *mode, int xsize, int ysize) { } Imaging -ImagingNewDirty(const char *mode, int xsize, int ysize) { +ImagingNewDirty(const Mode *mode, int xsize, int ysize) { if (ImagingDefaultArena.use_block_allocator) { return ImagingNewBlock(mode, xsize, ysize); } @@ -646,7 +645,7 @@ ImagingNewDirty(const char *mode, int xsize, int ysize) { } Imaging -ImagingNewBlock(const char *mode, int xsize, int ysize) { +ImagingNewBlock(const Mode *mode, int xsize, int ysize) { Imaging im; if (xsize < 0 || ysize < 0) { @@ -668,7 +667,7 @@ ImagingNewBlock(const char *mode, int xsize, int ysize) { Imaging ImagingNewArrow( - const char *mode, + const Mode *mode, int xsize, int ysize, PyObject *schema_capsule, @@ -741,12 +740,12 @@ ImagingNewArrow( } Imaging -ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn) { +ImagingNew2Dirty(const Mode *mode, Imaging imOut, Imaging imIn) { /* allocate or validate output image */ if (imOut) { /* make sure images match */ - if (strcmp(imOut->mode, mode) != 0 || imOut->xsize != imIn->xsize || + if (imOut->mode != mode || imOut->xsize != imIn->xsize || imOut->ysize != imIn->ysize) { return ImagingError_Mismatch(); } From 141c95df9a56ed177b07fb82acfa6087d338d4be Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 21 Apr 2024 22:54:58 -0500 Subject: [PATCH 1925/2374] use mode structs in TiffDecode.c --- src/libImaging/Mode.h | 4 ++++ src/libImaging/TiffDecode.c | 20 ++++++++------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index 09810b584d6..f953d586f7c 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -79,11 +79,13 @@ extern const RawMode * const IMAGING_RAWMODE_1_IR; extern const RawMode * const IMAGING_RAWMODE_1_R; extern const RawMode * const IMAGING_RAWMODE_A; extern const RawMode * const IMAGING_RAWMODE_ABGR; +extern const RawMode * const IMAGING_RAWMODE_A_16N; extern const RawMode * const IMAGING_RAWMODE_B; extern const RawMode * const IMAGING_RAWMODE_BGR; extern const RawMode * const IMAGING_RAWMODE_BGRA; extern const RawMode * const IMAGING_RAWMODE_BGRX; extern const RawMode * const IMAGING_RAWMODE_BGRa; +extern const RawMode * const IMAGING_RAWMODE_B_16N; extern const RawMode * const IMAGING_RAWMODE_C; extern const RawMode * const IMAGING_RAWMODE_CMYK_I; extern const RawMode * const IMAGING_RAWMODE_CMYK_L; @@ -92,6 +94,7 @@ extern const RawMode * const IMAGING_RAWMODE_Cr; extern const RawMode * const IMAGING_RAWMODE_F_32F; extern const RawMode * const IMAGING_RAWMODE_F_32NF; extern const RawMode * const IMAGING_RAWMODE_G; +extern const RawMode * const IMAGING_RAWMODE_G_16N; extern const RawMode * const IMAGING_RAWMODE_H; extern const RawMode * const IMAGING_RAWMODE_I_32NS; extern const RawMode * const IMAGING_RAWMODE_I_32S; @@ -108,6 +111,7 @@ extern const RawMode * const IMAGING_RAWMODE_R; extern const RawMode * const IMAGING_RAWMODE_RGBA_L; extern const RawMode * const IMAGING_RAWMODE_RGBX_L; extern const RawMode * const IMAGING_RAWMODE_RGB_L; +extern const RawMode * const IMAGING_RAWMODE_R_16N; extern const RawMode * const IMAGING_RAWMODE_S; extern const RawMode * const IMAGING_RAWMODE_V; extern const RawMode * const IMAGING_RAWMODE_X; diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 71516fd1b56..40e8fba505f 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -246,14 +246,10 @@ _pickUnpackers( // We'll pick appropriate set of unpackers depending on planar_configuration // It does not matter if data is RGB(A), CMYK or LUV really, // we just copy it plane by plane - unpackers[0] = - ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "R;16N" : "R", NULL); - unpackers[1] = - ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "G;16N" : "G", NULL); - unpackers[2] = - ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "B;16N" : "B", NULL); - unpackers[3] = - ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "A;16N" : "A", NULL); + unpackers[0] = ImagingFindUnpacker(IMAGING_MODE_RGBA, bits_per_sample == 16 ? IMAGING_RAWMODE_R_16N : IMAGING_RAWMODE_R, NULL); + unpackers[1] = ImagingFindUnpacker(IMAGING_MODE_RGBA, bits_per_sample == 16 ? IMAGING_RAWMODE_G_16N : IMAGING_RAWMODE_G, NULL); + unpackers[2] = ImagingFindUnpacker(IMAGING_MODE_RGBA, bits_per_sample == 16 ? IMAGING_RAWMODE_B_16N : IMAGING_RAWMODE_B, NULL); + unpackers[3] = ImagingFindUnpacker(IMAGING_MODE_RGBA, bits_per_sample == 16 ? IMAGING_RAWMODE_A_16N : IMAGING_RAWMODE_A, NULL); return im->bands; } else { @@ -644,7 +640,7 @@ ImagingLibTiffDecode( ); TRACE( ("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", - im->mode, + im->mode->name, im->type, im->bands, im->xsize, @@ -755,7 +751,7 @@ ImagingLibTiffDecode( if (!state->errcode) { // Check if raw mode was RGBa and it was stored on separate planes // so we have to convert it to RGBA - if (planes > 3 && strcmp(im->mode, "RGBA") == 0) { + if (planes > 3 && im->mode == IMAGING_MODE_RGBA) { uint16_t extrasamples; uint16_t *sampleinfo; ImagingShuffler shuffle; @@ -767,7 +763,7 @@ ImagingLibTiffDecode( if (extrasamples >= 1 && (sampleinfo[0] == EXTRASAMPLE_UNSPECIFIED || sampleinfo[0] == EXTRASAMPLE_ASSOCALPHA)) { - shuffle = ImagingFindUnpacker("RGBA", "RGBa", NULL); + shuffle = ImagingFindUnpacker(IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa, NULL); for (y = state->yoff; y < state->ysize; y++) { UINT8 *ptr = (UINT8 *)im->image[y + state->yoff] + @@ -991,7 +987,7 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt ); TRACE( ("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", - im->mode, + im->mode->name, im->type, im->bands, im->xsize, From 39d434b39dc736287604673720532b89c70eb260 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Apr 2024 12:47:51 -0500 Subject: [PATCH 1926/2374] use (void) for empty function parameters --- src/libImaging/Access.c | 2 +- src/libImaging/Convert.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 6c41fc0915c..95586d7edb7 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -189,4 +189,4 @@ void _ImagingAccessDelete(Imaging im, ImagingAccess access) {} void -ImagingAccessFree() {} +ImagingAccessFree(void) {} diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 2bc054616aa..48259dcdce2 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1650,7 +1650,7 @@ ImagingConvertInPlace(Imaging imIn, const Mode *mode) { /* ------------------ */ void -ImagingConvertInit() { +ImagingConvertInit(void) { const struct Converter temp[] = { {IMAGING_MODE_1, IMAGING_MODE_L, bit2l}, {IMAGING_MODE_1, IMAGING_MODE_I, bit2i}, @@ -1792,6 +1792,6 @@ ImagingConvertInit() { } void -ImagingConvertFree() { +ImagingConvertFree(void) { free(converters); } From 31118b0019ddd553dfd0b841b6b42c4b33ab1ef9 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Apr 2024 12:48:26 -0500 Subject: [PATCH 1927/2374] set pointer to NULL after free --- src/libImaging/Convert.c | 1 + src/libImaging/Pack.c | 1 + 2 files changed, 2 insertions(+) diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 48259dcdce2..8f580c294b7 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1794,4 +1794,5 @@ ImagingConvertInit(void) { void ImagingConvertFree(void) { free(converters); + converters = NULL; } diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index da714daccd1..aaa074c9249 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -815,4 +815,5 @@ ImagingPackInit(void) { void ImagingPackFree(void) { free(packers); + packers = NULL; } From d11819ca6bdebc50c94d84aad17514cf4a42365f Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:55:44 +0200 Subject: [PATCH 1928/2374] use mode structs in Unpack.c --- src/_imaging.c | 1 + src/libImaging/Imaging.h | 5 + src/libImaging/Mode.h | 96 ++++++++++++ src/libImaging/Unpack.c | 325 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 421 insertions(+), 6 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 22276590a89..6c98c42eacf 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4320,6 +4320,7 @@ setup_module(PyObject *m) { ImagingAccessInit(); ImagingConvertInit(); ImagingPackInit(); + ImagingUnpackInit(); #ifdef HAVE_LIBJPEG { diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index c3efb2cda43..9f450dd3ad2 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -199,6 +199,11 @@ ImagingPackInit(void); extern void ImagingPackFree(void); +extern void +ImagingUnpackInit(void); +extern void +ImagingUnpackFree(void); + /* Objects */ /* ------- */ diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index f953d586f7c..663f2f4681f 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -74,43 +74,136 @@ extern const RawMode * const IMAGING_RAWMODE_I_32L; extern const RawMode * const IMAGING_RAWMODE_I_32B; // Rawmodes +extern const RawMode * const IMAGING_RAWMODE_1_8; extern const RawMode * const IMAGING_RAWMODE_1_I; extern const RawMode * const IMAGING_RAWMODE_1_IR; extern const RawMode * const IMAGING_RAWMODE_1_R; extern const RawMode * const IMAGING_RAWMODE_A; extern const RawMode * const IMAGING_RAWMODE_ABGR; +extern const RawMode * const IMAGING_RAWMODE_ARGB; +extern const RawMode * const IMAGING_RAWMODE_A_16B; +extern const RawMode * const IMAGING_RAWMODE_A_16L; extern const RawMode * const IMAGING_RAWMODE_A_16N; extern const RawMode * const IMAGING_RAWMODE_B; +extern const RawMode * const IMAGING_RAWMODE_BGAR; extern const RawMode * const IMAGING_RAWMODE_BGR; extern const RawMode * const IMAGING_RAWMODE_BGRA; +extern const RawMode * const IMAGING_RAWMODE_BGRA_15; +extern const RawMode * const IMAGING_RAWMODE_BGRA_15Z; +extern const RawMode * const IMAGING_RAWMODE_BGRA_16B; +extern const RawMode * const IMAGING_RAWMODE_BGRA_16L; extern const RawMode * const IMAGING_RAWMODE_BGRX; +extern const RawMode * const IMAGING_RAWMODE_BGR_5; extern const RawMode * const IMAGING_RAWMODE_BGRa; +extern const RawMode * const IMAGING_RAWMODE_BGXR; +extern const RawMode * const IMAGING_RAWMODE_B_16B; +extern const RawMode * const IMAGING_RAWMODE_B_16L; extern const RawMode * const IMAGING_RAWMODE_B_16N; extern const RawMode * const IMAGING_RAWMODE_C; +extern const RawMode * const IMAGING_RAWMODE_CMYKX; +extern const RawMode * const IMAGING_RAWMODE_CMYKXX; +extern const RawMode * const IMAGING_RAWMODE_CMYK_16B; +extern const RawMode * const IMAGING_RAWMODE_CMYK_16L; +extern const RawMode * const IMAGING_RAWMODE_CMYK_16N; extern const RawMode * const IMAGING_RAWMODE_CMYK_I; extern const RawMode * const IMAGING_RAWMODE_CMYK_L; +extern const RawMode * const IMAGING_RAWMODE_C_I; extern const RawMode * const IMAGING_RAWMODE_Cb; extern const RawMode * const IMAGING_RAWMODE_Cr; +extern const RawMode * const IMAGING_RAWMODE_F_16; +extern const RawMode * const IMAGING_RAWMODE_F_16B; +extern const RawMode * const IMAGING_RAWMODE_F_16BS; +extern const RawMode * const IMAGING_RAWMODE_F_16N; +extern const RawMode * const IMAGING_RAWMODE_F_16NS; +extern const RawMode * const IMAGING_RAWMODE_F_16S; +extern const RawMode * const IMAGING_RAWMODE_F_32; +extern const RawMode * const IMAGING_RAWMODE_F_32B; +extern const RawMode * const IMAGING_RAWMODE_F_32BF; +extern const RawMode * const IMAGING_RAWMODE_F_32BS; extern const RawMode * const IMAGING_RAWMODE_F_32F; +extern const RawMode * const IMAGING_RAWMODE_F_32N; extern const RawMode * const IMAGING_RAWMODE_F_32NF; +extern const RawMode * const IMAGING_RAWMODE_F_32NS; +extern const RawMode * const IMAGING_RAWMODE_F_32S; +extern const RawMode * const IMAGING_RAWMODE_F_64BF; +extern const RawMode * const IMAGING_RAWMODE_F_64F; +extern const RawMode * const IMAGING_RAWMODE_F_64NF; +extern const RawMode * const IMAGING_RAWMODE_F_8; +extern const RawMode * const IMAGING_RAWMODE_F_8S; extern const RawMode * const IMAGING_RAWMODE_G; +extern const RawMode * const IMAGING_RAWMODE_G_16B; +extern const RawMode * const IMAGING_RAWMODE_G_16L; extern const RawMode * const IMAGING_RAWMODE_G_16N; extern const RawMode * const IMAGING_RAWMODE_H; +extern const RawMode * const IMAGING_RAWMODE_I_12; +extern const RawMode * const IMAGING_RAWMODE_I_16BS; +extern const RawMode * const IMAGING_RAWMODE_I_16NS; +extern const RawMode * const IMAGING_RAWMODE_I_16R; +extern const RawMode * const IMAGING_RAWMODE_I_16S; +extern const RawMode * const IMAGING_RAWMODE_I_32; +extern const RawMode * const IMAGING_RAWMODE_I_32BS; +extern const RawMode * const IMAGING_RAWMODE_I_32N; extern const RawMode * const IMAGING_RAWMODE_I_32NS; extern const RawMode * const IMAGING_RAWMODE_I_32S; +extern const RawMode * const IMAGING_RAWMODE_I_8; +extern const RawMode * const IMAGING_RAWMODE_I_8S; extern const RawMode * const IMAGING_RAWMODE_K; +extern const RawMode * const IMAGING_RAWMODE_K_I; +extern const RawMode * const IMAGING_RAWMODE_LA_16B; extern const RawMode * const IMAGING_RAWMODE_LA_L; extern const RawMode * const IMAGING_RAWMODE_L_16; extern const RawMode * const IMAGING_RAWMODE_L_16B; +extern const RawMode * const IMAGING_RAWMODE_L_2; +extern const RawMode * const IMAGING_RAWMODE_L_2I; +extern const RawMode * const IMAGING_RAWMODE_L_2IR; +extern const RawMode * const IMAGING_RAWMODE_L_2R; +extern const RawMode * const IMAGING_RAWMODE_L_4; +extern const RawMode * const IMAGING_RAWMODE_L_4I; +extern const RawMode * const IMAGING_RAWMODE_L_4IR; +extern const RawMode * const IMAGING_RAWMODE_L_4R; +extern const RawMode * const IMAGING_RAWMODE_L_I; +extern const RawMode * const IMAGING_RAWMODE_L_R; extern const RawMode * const IMAGING_RAWMODE_M; +extern const RawMode * const IMAGING_RAWMODE_M_I; extern const RawMode * const IMAGING_RAWMODE_PA_L; +extern const RawMode * const IMAGING_RAWMODE_PX; extern const RawMode * const IMAGING_RAWMODE_P_1; extern const RawMode * const IMAGING_RAWMODE_P_2; +extern const RawMode * const IMAGING_RAWMODE_P_2L; extern const RawMode * const IMAGING_RAWMODE_P_4; +extern const RawMode * const IMAGING_RAWMODE_P_4L; +extern const RawMode * const IMAGING_RAWMODE_P_R; extern const RawMode * const IMAGING_RAWMODE_R; +extern const RawMode * const IMAGING_RAWMODE_RGBAX; +extern const RawMode * const IMAGING_RAWMODE_RGBAXX; +extern const RawMode * const IMAGING_RAWMODE_RGBA_15; +extern const RawMode * const IMAGING_RAWMODE_RGBA_16B; +extern const RawMode * const IMAGING_RAWMODE_RGBA_16L; +extern const RawMode * const IMAGING_RAWMODE_RGBA_16N; +extern const RawMode * const IMAGING_RAWMODE_RGBA_4B; +extern const RawMode * const IMAGING_RAWMODE_RGBA_I; extern const RawMode * const IMAGING_RAWMODE_RGBA_L; +extern const RawMode * const IMAGING_RAWMODE_RGBXX; +extern const RawMode * const IMAGING_RAWMODE_RGBXXX; +extern const RawMode * const IMAGING_RAWMODE_RGBX_16B; +extern const RawMode * const IMAGING_RAWMODE_RGBX_16L; +extern const RawMode * const IMAGING_RAWMODE_RGBX_16N; extern const RawMode * const IMAGING_RAWMODE_RGBX_L; +extern const RawMode * const IMAGING_RAWMODE_RGB_15; +extern const RawMode * const IMAGING_RAWMODE_RGB_16; +extern const RawMode * const IMAGING_RAWMODE_RGB_16B; +extern const RawMode * const IMAGING_RAWMODE_RGB_16L; +extern const RawMode * const IMAGING_RAWMODE_RGB_16N; +extern const RawMode * const IMAGING_RAWMODE_RGB_4B; extern const RawMode * const IMAGING_RAWMODE_RGB_L; +extern const RawMode * const IMAGING_RAWMODE_RGB_R; +extern const RawMode * const IMAGING_RAWMODE_RGBaX; +extern const RawMode * const IMAGING_RAWMODE_RGBaXX; +extern const RawMode * const IMAGING_RAWMODE_RGBa_16B; +extern const RawMode * const IMAGING_RAWMODE_RGBa_16L; +extern const RawMode * const IMAGING_RAWMODE_RGBa_16N; +extern const RawMode * const IMAGING_RAWMODE_R_16B; +extern const RawMode * const IMAGING_RAWMODE_R_16L; extern const RawMode * const IMAGING_RAWMODE_R_16N; extern const RawMode * const IMAGING_RAWMODE_S; extern const RawMode * const IMAGING_RAWMODE_V; @@ -118,11 +211,14 @@ extern const RawMode * const IMAGING_RAWMODE_X; extern const RawMode * const IMAGING_RAWMODE_XBGR; extern const RawMode * const IMAGING_RAWMODE_XRGB; extern const RawMode * const IMAGING_RAWMODE_Y; +extern const RawMode * const IMAGING_RAWMODE_YCCA_P; extern const RawMode * const IMAGING_RAWMODE_YCC_P; extern const RawMode * const IMAGING_RAWMODE_YCbCrK; extern const RawMode * const IMAGING_RAWMODE_YCbCrX; extern const RawMode * const IMAGING_RAWMODE_YCbCr_L; +extern const RawMode * const IMAGING_RAWMODE_Y_I; extern const RawMode * const IMAGING_RAWMODE_aBGR; +extern const RawMode * const IMAGING_RAWMODE_aRGB; const RawMode * findRawMode(const char * const name); diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 976baa72673..78760215189 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1541,9 +1541,9 @@ band316L(UINT8 *out, const UINT8 *in, int pixels) { } } -static struct { - const char *mode; - const char *rawmode; +static struct Unpacker { + const Mode *mode; + const RawMode *rawmode; int bits; ImagingShuffler unpack; } unpackers[] = { @@ -1846,13 +1846,12 @@ static struct { }; ImagingShuffler -ImagingFindUnpacker(const char *mode, const char *rawmode, int *bits_out) { +ImagingFindUnpacker(const Mode *mode, const RawMode *rawmode, int *bits_out) { int i; /* find a suitable pixel unpacker */ for (i = 0; unpackers[i].rawmode; i++) { - if (strcmp(unpackers[i].mode, mode) == 0 && - strcmp(unpackers[i].rawmode, rawmode) == 0) { + if (unpackers[i].mode == mode && unpackers[i].rawmode == rawmode) { if (bits_out) { *bits_out = unpackers[i].bits; } @@ -1864,3 +1863,317 @@ ImagingFindUnpacker(const char *mode, const char *rawmode, int *bits_out) { return NULL; } + +void +ImagingUnpackInit(void) { + const struct Unpacker temp[] = { + /* raw mode syntax is ";" where "bits" defaults + depending on mode (1 for "1", 8 for "P" and "L", etc), and + "flags" should be given in alphabetical order. if both bits + and flags have their default values, the ; should be left out */ + + /* flags: "I" inverted data; "R" reversed bit order; "B" big + endian byte order (default is little endian); "L" line + interleave, "S" signed, "F" floating point, "Z" inverted alpha */ + + /* exception: rawmodes "I" and "F" are always native endian byte order */ + + /* bilevel */ + {IMAGING_MODE_1, IMAGING_RAWMODE_1, 1, unpack1}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_I, 1, unpack1I}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_R, 1, unpack1R}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_IR, 1, unpack1IR}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_8, 8, unpack18}, + + /* grayscale */ + {IMAGING_MODE_L, IMAGING_RAWMODE_L_2, 2, unpackL2}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_2I, 2, unpackL2I}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_2R, 2, unpackL2R}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_2IR, 2, unpackL2IR}, + + {IMAGING_MODE_L, IMAGING_RAWMODE_L_4, 4, unpackL4}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_4I, 4, unpackL4I}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_4R, 4, unpackL4R}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_4IR, 4, unpackL4IR}, + + {IMAGING_MODE_L, IMAGING_RAWMODE_L, 8, copy1}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_I, 8, unpackLI}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_R, 8, unpackLR}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_16, 16, unpackL16}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_16B, 16, unpackL16B}, + + /* grayscale w. alpha */ + {IMAGING_MODE_LA, IMAGING_RAWMODE_LA, 16, unpackLA}, + {IMAGING_MODE_LA, IMAGING_RAWMODE_LA_L, 16, unpackLAL}, + + /* grayscale w. alpha premultiplied */ + {IMAGING_MODE_La, IMAGING_RAWMODE_La, 16, unpackLA}, + + /* palette */ + {IMAGING_MODE_P, IMAGING_RAWMODE_P_1, 1, unpackP1}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_2, 2, unpackP2}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_2L, 2, unpackP2L}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_4, 4, unpackP4}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_4L, 4, unpackP4L}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P, 8, copy1}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_R, 8, unpackLR}, + {IMAGING_MODE_P, IMAGING_RAWMODE_L, 8, copy1}, + {IMAGING_MODE_P, IMAGING_RAWMODE_PX, 16, unpackL16B}, + + /* palette w. alpha */ + {IMAGING_MODE_PA, IMAGING_RAWMODE_PA, 16, unpackLA}, + {IMAGING_MODE_PA, IMAGING_RAWMODE_PA_L, 16, unpackLAL}, + {IMAGING_MODE_PA, IMAGING_RAWMODE_LA, 16, unpackLA}, + + /* true colour */ + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB, 24, ImagingUnpackRGB}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_L, 24, unpackRGBL}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_R, 24, unpackRGBR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16L, 48, unpackRGB16L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16B, 48, unpackRGB16B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR, 24, ImagingUnpackBGR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_15, 16, ImagingUnpackRGB15}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR_15, 16, ImagingUnpackBGR15}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16, 16, ImagingUnpackRGB16}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR_16, 16, ImagingUnpackBGR16}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_16L, 64, unpackRGBA16L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_16B, 64, unpackRGBA16B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_4B, 16, ImagingUnpackRGB4B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR_5, 16, ImagingUnpackBGR15}, /* compat */ + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX, 32, copy4}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_L, 32, unpackRGBAL}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBXX, 40, copy4skip1}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBXXX, 48, copy4skip2}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBA_L, 32, unpackRGBAL}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBA_15, 16, ImagingUnpackRGBA15}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGRX, 32, ImagingUnpackBGRX}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGXR, 32, ImagingUnpackBGXR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_XRGB, 32, ImagingUnpackXRGB}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_XBGR, 32, ImagingUnpackXBGR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_YCC_P, 24, ImagingUnpackYCC}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16L, 16, band016L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16L, 16, band116L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16L, 16, band216L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16B, 16, band016B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16B, 16, band116B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16B, 16, band216B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_CMYK, 32, cmyk2rgb}, + + {IMAGING_MODE_BGR_15, IMAGING_RAWMODE_BGR_15, 16, copy2}, + {IMAGING_MODE_BGR_16, IMAGING_RAWMODE_BGR_16, 16, copy2}, + {IMAGING_MODE_BGR_24, IMAGING_RAWMODE_BGR_24, 24, copy3}, + + /* true colour w. alpha */ + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_LA, 16, unpackRGBALA}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_LA_16B, 32, unpackRGBALA16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA, 32, copy4}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBAX, 40, copy4skip1}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBAXX, 48, copy4skip2}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa, 32, unpackRGBa}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBaX, 40, unpackRGBaskip1}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBaXX, 48, unpackRGBaskip2}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16L, 64, unpackRGBa16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16B, 64, unpackRGBa16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRa, 32, unpackBGRa}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_I, 32, unpackRGBAI}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_L, 32, unpackRGBAL}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_15, 16, ImagingUnpackRGBA15}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_15, 16, ImagingUnpackBGRA15}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_15Z, 16, ImagingUnpackBGRA15Z}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_4B, 16, ImagingUnpackRGBA4B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16L, 64, unpackRGBA16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16B, 64, unpackRGBA16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA, 32, unpackBGRA}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_16L, 64, unpackBGRA16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_16B, 64, unpackBGRA16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGAR, 32, unpackBGAR}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_ARGB, 32, unpackARGB}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_ABGR, 32, unpackABGR}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_YCCA_P, 32, ImagingUnpackYCCA}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A, 8, band3}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16L, 16, band016L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16L, 16, band116L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16L, 16, band216L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16L, 16, band316L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16B, 16, band016B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16B, 16, band116B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16B, 16, band216B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16B, 16, band316B}, + +#ifdef WORDS_BIGENDIAN + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16N, 48, unpackRGB16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16N, 64, unpackRGBa16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16N, 64, unpackRGBA16B}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16N, 64, unpackRGBA16B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16N, 16, band016B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16N, 16, band116B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16N, 16, band216B}, + + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16N, 16, band016B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16N, 16, band116B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16N, 16, band216B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16N, 16, band316B}, +#else + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16N, 48, unpackRGB16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16N, 64, unpackRGBa16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16N, 64, unpackRGBA16L}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16N, 64, unpackRGBA16L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16N, 16, band016L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16N, 16, band116L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16N, 16, band216L}, + + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16N, 16, band016L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16N, 16, band116L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16N, 16, band216L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16N, 16, band316L}, +#endif + + /* true colour w. alpha premultiplied */ + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_RGBa, 32, copy4}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_BGRa, 32, unpackBGRA}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_aRGB, 32, unpackARGB}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_aBGR, 32, unpackABGR}, + + /* true colour w. padding */ + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB, 24, ImagingUnpackRGB}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_L, 24, unpackRGBL}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_16B, 48, unpackRGB16B}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR, 24, ImagingUnpackBGR}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_15, 16, ImagingUnpackRGB15}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR_15, 16, ImagingUnpackBGR15}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_4B, 16, ImagingUnpackRGB4B}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR_5, 16, ImagingUnpackBGR15}, /* compat */ + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX, 32, copy4}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBXX, 40, copy4skip1}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBXXX, 48, copy4skip2}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_L, 32, unpackRGBAL}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16L, 64, unpackRGBA16L}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16B, 64, unpackRGBA16B}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGRX, 32, ImagingUnpackBGRX}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_XRGB, 32, ImagingUnpackXRGB}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_XBGR, 32, ImagingUnpackXBGR}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_YCC_P, 24, ImagingUnpackYCC}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_X, 8, band3}, + + /* colour separation */ + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK, 32, copy4}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYKX, 40, copy4skip1}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYKXX, 48, copy4skip2}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_I, 32, unpackCMYKI}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_L, 32, unpackRGBAL}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16L, 64, unpackRGBA16L}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16B, 64, unpackRGBA16B}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_C, 8, band0}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_M, 8, band1}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y, 8, band2}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_K, 8, band3}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_C_I, 8, band0I}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_M_I, 8, band1I}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y_I, 8, band2I}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_K_I, 8, band3I}, + +#ifdef WORDS_BIGENDIAN + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16N, 64, unpackRGBA16B}, +#else + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16N, 64, unpackRGBA16L}, +#endif + + /* video (YCbCr) */ + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr, 24, ImagingUnpackRGB}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr_L, 24, unpackRGBL}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrX, 32, copy4}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrK, 32, copy4}, + + /* LAB Color */ + {IMAGING_MODE_LAB, IMAGING_RAWMODE_LAB, 24, ImagingUnpackLAB}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_L, 8, band0}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_A, 8, band1}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_B, 8, band2}, + + /* HSV Color */ + {IMAGING_MODE_HSV, IMAGING_RAWMODE_HSV, 24, ImagingUnpackRGB}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_H, 8, band0}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_S, 8, band1}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_V, 8, band2}, + + /* integer variations */ + {IMAGING_MODE_I, IMAGING_RAWMODE_I, 32, copy4}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_8, 8, unpackI8}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_8S, 8, unpackI8S}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16, 16, unpackI16}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16S, 16, unpackI16S}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16B, 16, unpackI16B}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16BS, 16, unpackI16BS}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16N, 16, unpackI16N}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16NS, 16, unpackI16NS}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32, 32, unpackI32}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32S, 32, unpackI32S}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32B, 32, unpackI32B}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32BS, 32, unpackI32BS}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32N, 32, unpackI32N}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32NS, 32, unpackI32NS}, + + /* floating point variations */ + {IMAGING_MODE_F, IMAGING_RAWMODE_F, 32, copy4}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_8, 8, unpackF8}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_8S, 8, unpackF8S}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16, 16, unpackF16}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16S, 16, unpackF16S}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16B, 16, unpackF16B}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16BS, 16, unpackF16BS}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16N, 16, unpackF16N}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16NS, 16, unpackF16NS}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32, 32, unpackF32}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32S, 32, unpackF32S}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32B, 32, unpackF32B}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32BS, 32, unpackF32BS}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32N, 32, unpackF32N}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32NS, 32, unpackF32NS}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32F, 32, unpackF32F}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32BF, 32, unpackF32BF}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32NF, 32, unpackF32NF}, +#ifdef FLOAT64 + {IMAGING_MODE_F, IMAGING_RAWMODE_F_64F, 64, unpackF64F}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_64BF, 64, unpackF64BF}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_64NF, 64, unpackF64NF}, +#endif + + /* storage modes */ + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16, 16, copy2}, + {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16B, 16, copy2}, + {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16L, 16, copy2}, + {IMAGING_MODE_I_16N, IMAGING_RAWMODE_I_16N, 16, copy2}, + + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, unpackI16B_I16}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16}, // LibTiff native->image endian. + {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16}, // LibTiff native->image endian. + {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16B}, + + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16R, 16, unpackI16R_I16}, + + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_12, 12, unpackI12_I16}, // 12 bit Tiffs stored in 16bits. + + {NULL} /* sentinel */ + }; + unpackers = malloc(sizeof(temp)); + if (unpackers == NULL) { + fprintf(stderr, "UnpackInit: failed to allocate memory for unpackers table\n"); + exit(1); + } + memcpy(unpackers, temp, sizeof(temp)); +} + +void +ImagingUnpackFree(void) { + free(unpackers); + unpackers = NULL; +} From feb7e6ef2d269bf481ee070ceebb0a00f7f9123d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Apr 2024 12:59:17 -0500 Subject: [PATCH 1929/2374] use mode structs in map.c --- src/map.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/map.c b/src/map.c index f4933e45e93..451cca58973 100644 --- a/src/map.c +++ b/src/map.c @@ -55,7 +55,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { PyObject *target; Py_buffer view; - char *mode; + char *mode_name; char *codec; Py_ssize_t offset; int xsize, ysize; @@ -70,7 +70,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { &ysize, &codec, &offset, - &mode, + &mode_name, &stride, &ystep )) { @@ -82,8 +82,10 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { return NULL; } + const Mode * const mode = findMode(mode_name); + if (stride <= 0) { - if (!strcmp(mode, "L") || !strcmp(mode, "P")) { + if (mode == IMAGING_MODE_L || mode == IMAGING_MODE_P) { stride = xsize; } else if (isModeI16(mode)) { stride = xsize * 2; From c9c50ac678ca88da18c8b2695380065fdd637533 Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 16:00:26 +0200 Subject: [PATCH 1930/2374] initialize accessors similar to converters/packers/unpackers --- src/libImaging/Access.c | 90 +++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 52 deletions(-) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 95586d7edb7..59a776fe04e 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -11,10 +11,6 @@ #include "Imaging.h" -#define ACCESS_TABLE_SIZE 24 -static struct ImagingAccessInstance ACCESS_TABLE[ACCESS_TABLE_SIZE]; - - /* fetch individual pixel */ static void @@ -120,66 +116,53 @@ put_pixel_32(Imaging im, int x, int y, const void *color) { memcpy(&im->image32[y][x], color, sizeof(INT32)); } - -static void -set_access_table_item( - const int index, - const Mode * const mode, - void (*get_pixel)(Imaging im, int x, int y, void *pixel), - void (*put_pixel)(Imaging im, int x, int y, const void *pixel) -) { - ACCESS_TABLE[index].mode = mode; - ACCESS_TABLE[index].get_pixel = get_pixel; - ACCESS_TABLE[index].put_pixel = put_pixel; -} +static struct ImagingAccessInstance *accessors = NULL; void ImagingAccessInit(void) { - int i = 0; - set_access_table_item(i++, IMAGING_MODE_1, get_pixel_8, put_pixel_8); - set_access_table_item(i++, IMAGING_MODE_L, get_pixel_8, put_pixel_8); - set_access_table_item(i++, IMAGING_MODE_LA, get_pixel_32_2bands, put_pixel_32); - set_access_table_item(i++, IMAGING_MODE_La, get_pixel_32_2bands, put_pixel_32); - set_access_table_item(i++, IMAGING_MODE_I, get_pixel_32, put_pixel_32); - set_access_table_item(i++, IMAGING_MODE_I_16, get_pixel_16L, put_pixel_16L); - set_access_table_item(i++, IMAGING_MODE_I_16L, get_pixel_16L, put_pixel_16L); - set_access_table_item(i++, IMAGING_MODE_I_16B, get_pixel_16B, put_pixel_16B); + const struct ImagingAccessInstance temp[] = { + {IMAGING_MODE_1, get_pixel_8, put_pixel_8}, + {IMAGING_MODE_L, get_pixel_8, put_pixel_8}, + {IMAGING_MODE_LA, get_pixel_32_2bands, put_pixel_32}, + {IMAGING_MODE_La, get_pixel_32_2bands, put_pixel_32}, + {IMAGING_MODE_I, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_I_16, get_pixel_16L, put_pixel_16L}, + {IMAGING_MODE_I_16L, get_pixel_16L, put_pixel_16L}, + {IMAGING_MODE_I_16B, get_pixel_16B, put_pixel_16B}, #ifdef WORDS_BIGENDIAN - set_access_table_item(i++, IMAGING_MODE_I_16N, get_pixel_16B, put_pixel_16B); + {IMAGING_MODE_I_16N, get_pixel_16B, put_pixel_16B}, #else - set_access_table_item(i++, IMAGING_MODE_I_16N, get_pixel_16L, put_pixel_16L); + {IMAGING_MODE_I_16N, get_pixel_16L, put_pixel_16L}, #endif - set_access_table_item(i++, IMAGING_MODE_I_32L, get_pixel_32L, put_pixel_32L); - set_access_table_item(i++, IMAGING_MODE_I_32B, get_pixel_32B, put_pixel_32B); - set_access_table_item(i++, IMAGING_MODE_F, get_pixel_32, put_pixel_32); - set_access_table_item(i++, IMAGING_MODE_P, get_pixel_8, put_pixel_8); - set_access_table_item(i++, IMAGING_MODE_PA, get_pixel_32_2bands, put_pixel_32); - set_access_table_item(i++, IMAGING_MODE_RGB, get_pixel_32, put_pixel_32); - set_access_table_item(i++, IMAGING_MODE_RGBA, get_pixel_32, put_pixel_32); - set_access_table_item(i++, IMAGING_MODE_RGBa, get_pixel_32, put_pixel_32); - set_access_table_item(i++, IMAGING_MODE_RGBX, get_pixel_32, put_pixel_32); - set_access_table_item(i++, IMAGING_MODE_CMYK, get_pixel_32, put_pixel_32); - set_access_table_item(i++, IMAGING_MODE_YCbCr, get_pixel_32, put_pixel_32); - set_access_table_item(i++, IMAGING_MODE_LAB, get_pixel_32, put_pixel_32); - set_access_table_item(i++, IMAGING_MODE_HSV, get_pixel_32, put_pixel_32); - - - if (i != ACCESS_TABLE_SIZE) { - fprintf( - stderr, - "AccessInit: incorrect number of items added to ACCESS_TABLE; expected %i but got %i\n", - ACCESS_TABLE_SIZE, - i); + {IMAGING_MODE_I_32L, get_pixel_32L, put_pixel_32L}, + {IMAGING_MODE_I_32B, get_pixel_32B, put_pixel_32B}, + {IMAGING_MODE_F, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_P, get_pixel_8, put_pixel_8}, + {IMAGING_MODE_PA, get_pixel_32_2bands, put_pixel_32}, + {IMAGING_MODE_RGB, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_RGBA, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_RGBa, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_RGBX, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_CMYK, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_YCbCr, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_LAB, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_HSV, get_pixel_32, put_pixel_32}, + {NULL} + }; + accessors = malloc(sizeof(temp)); + if (accessors == NULL) { + fprintf(stderr, "AccessInit: failed to allocate memory for accessors table\n"); exit(1); } + memcpy(accessors, temp, sizeof(temp)); } ImagingAccess ImagingAccessNew(const Imaging im) { int i; - for (i = 0; i < ACCESS_TABLE_SIZE; i++) { - if (im->mode == ACCESS_TABLE[i].mode) { - return &ACCESS_TABLE[i]; + for (i = 0; accessors[i].mode; i++) { + if (im->mode == accessors[i].mode) { + return &accessors[i]; } } return NULL; @@ -189,4 +172,7 @@ void _ImagingAccessDelete(Imaging im, ImagingAccess access) {} void -ImagingAccessFree(void) {} +ImagingAccessFree(void) { + free(accessors); + accessors = NULL; +} From cacb8b3ce70e73dd0b0f338410510cfbbd22709d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Apr 2024 14:01:53 -0500 Subject: [PATCH 1931/2374] define rawmodes --- src/libImaging/Mode.c | 292 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c index a11b65905e8..8d651b06dba 100644 --- a/src/libImaging/Mode.c +++ b/src/libImaging/Mode.c @@ -109,6 +109,152 @@ ALIAS_MODE_AS_RAWMODE(I_16N) ALIAS_MODE_AS_RAWMODE(I_32L) ALIAS_MODE_AS_RAWMODE(I_32B) +CREATE_MODE(RawMode, RAWMODE_1_8, {"1;8"}) +CREATE_MODE(RawMode, RAWMODE_1_I, {"1;I"}) +CREATE_MODE(RawMode, RAWMODE_1_IR, {"1;IR"}) +CREATE_MODE(RawMode, RAWMODE_1_R, {"1;R"}) +CREATE_MODE(RawMode, RAWMODE_A, {"A"}) +CREATE_MODE(RawMode, RAWMODE_ABGR, {"ABGR"}) +CREATE_MODE(RawMode, RAWMODE_ARGB, {"ARGB"}) +CREATE_MODE(RawMode, RAWMODE_A_16B, {"A;16B"}) +CREATE_MODE(RawMode, RAWMODE_A_16L, {"A;16L"}) +CREATE_MODE(RawMode, RAWMODE_A_16N, {"A;16N"}) +CREATE_MODE(RawMode, RAWMODE_B, {"B"}) +CREATE_MODE(RawMode, RAWMODE_BGAR, {"BGAR"}) +CREATE_MODE(RawMode, RAWMODE_BGR, {"BGR"}) +CREATE_MODE(RawMode, RAWMODE_BGRA, {"BGRA"}) +CREATE_MODE(RawMode, RAWMODE_BGRA_15, {"BGRA;15"}) +CREATE_MODE(RawMode, RAWMODE_BGRA_15Z, {"BGRA;15Z"}) +CREATE_MODE(RawMode, RAWMODE_BGRA_16B, {"BGRA;16B"}) +CREATE_MODE(RawMode, RAWMODE_BGRA_16L, {"BGRA;16L"}) +CREATE_MODE(RawMode, RAWMODE_BGRX, {"BGRX"}) +CREATE_MODE(RawMode, RAWMODE_BGR_5, {"BGR;5"}) +CREATE_MODE(RawMode, RAWMODE_BGRa, {"BGRa"}) +CREATE_MODE(RawMode, RAWMODE_BGXR, {"BGXR"}) +CREATE_MODE(RawMode, RAWMODE_B_16B, {"B;16B"}) +CREATE_MODE(RawMode, RAWMODE_B_16L, {"B;16L"}) +CREATE_MODE(RawMode, RAWMODE_B_16N, {"B;16N"}) +CREATE_MODE(RawMode, RAWMODE_C, {"C"}) +CREATE_MODE(RawMode, RAWMODE_CMYKX, {"CMYKX"}) +CREATE_MODE(RawMode, RAWMODE_CMYKXX, {"CMYKXX"}) +CREATE_MODE(RawMode, RAWMODE_CMYK_16B, {"CMYK;16B"}) +CREATE_MODE(RawMode, RAWMODE_CMYK_16L, {"CMYK;16L"}) +CREATE_MODE(RawMode, RAWMODE_CMYK_16N, {"CMYK;16N"}) +CREATE_MODE(RawMode, RAWMODE_CMYK_I, {"CMYK;I"}) +CREATE_MODE(RawMode, RAWMODE_CMYK_L, {"CMYK;L"}) +CREATE_MODE(RawMode, RAWMODE_C_I, {"C;I"}) +CREATE_MODE(RawMode, RAWMODE_Cb, {"Cb"}) +CREATE_MODE(RawMode, RAWMODE_Cr, {"Cr"}) +CREATE_MODE(RawMode, RAWMODE_F_16, {"F;16"}) +CREATE_MODE(RawMode, RAWMODE_F_16B, {"F;16B"}) +CREATE_MODE(RawMode, RAWMODE_F_16BS, {"F;16BS"}) +CREATE_MODE(RawMode, RAWMODE_F_16N, {"F;16N"}) +CREATE_MODE(RawMode, RAWMODE_F_16NS, {"F;16NS"}) +CREATE_MODE(RawMode, RAWMODE_F_16S, {"F;16S"}) +CREATE_MODE(RawMode, RAWMODE_F_32, {"F;32"}) +CREATE_MODE(RawMode, RAWMODE_F_32B, {"F;32B"}) +CREATE_MODE(RawMode, RAWMODE_F_32BF, {"F;32BF"}) +CREATE_MODE(RawMode, RAWMODE_F_32BS, {"F;32BS"}) +CREATE_MODE(RawMode, RAWMODE_F_32F, {"F;32F"}) +CREATE_MODE(RawMode, RAWMODE_F_32N, {"F;32N"}) +CREATE_MODE(RawMode, RAWMODE_F_32NF, {"F;32NF"}) +CREATE_MODE(RawMode, RAWMODE_F_32NS, {"F;32NS"}) +CREATE_MODE(RawMode, RAWMODE_F_32S, {"F;32S"}) +CREATE_MODE(RawMode, RAWMODE_F_64BF, {"F;64BF"}) +CREATE_MODE(RawMode, RAWMODE_F_64F, {"F;64F"}) +CREATE_MODE(RawMode, RAWMODE_F_64NF, {"F;64NF"}) +CREATE_MODE(RawMode, RAWMODE_F_8, {"F;8"}) +CREATE_MODE(RawMode, RAWMODE_F_8S, {"F;8S"}) +CREATE_MODE(RawMode, RAWMODE_G, {"G"}) +CREATE_MODE(RawMode, RAWMODE_G_16B, {"G;16B"}) +CREATE_MODE(RawMode, RAWMODE_G_16L, {"G;16L"}) +CREATE_MODE(RawMode, RAWMODE_G_16N, {"G;16N"}) +CREATE_MODE(RawMode, RAWMODE_H, {"H"}) +CREATE_MODE(RawMode, RAWMODE_I_12, {"I;12"}) +CREATE_MODE(RawMode, RAWMODE_I_16BS, {"I;16BS"}) +CREATE_MODE(RawMode, RAWMODE_I_16NS, {"I;16NS"}) +CREATE_MODE(RawMode, RAWMODE_I_16R, {"I;16R"}) +CREATE_MODE(RawMode, RAWMODE_I_16S, {"I;16S"}) +CREATE_MODE(RawMode, RAWMODE_I_32, {"I;32"}) +CREATE_MODE(RawMode, RAWMODE_I_32BS, {"I;32BS"}) +CREATE_MODE(RawMode, RAWMODE_I_32N, {"I;32N"}) +CREATE_MODE(RawMode, RAWMODE_I_32NS, {"I;32NS"}) +CREATE_MODE(RawMode, RAWMODE_I_32S, {"I;32S"}) +CREATE_MODE(RawMode, RAWMODE_I_8, {"I;8"}) +CREATE_MODE(RawMode, RAWMODE_I_8S, {"I;8S"}) +CREATE_MODE(RawMode, RAWMODE_K, {"K"}) +CREATE_MODE(RawMode, RAWMODE_K_I, {"K;I"}) +CREATE_MODE(RawMode, RAWMODE_LA_16B, {"LA;16B"}) +CREATE_MODE(RawMode, RAWMODE_LA_L, {"LA;L"}) +CREATE_MODE(RawMode, RAWMODE_L_16, {"L;16"}) +CREATE_MODE(RawMode, RAWMODE_L_16B, {"L;16B"}) +CREATE_MODE(RawMode, RAWMODE_L_2, {"L;2"}) +CREATE_MODE(RawMode, RAWMODE_L_2I, {"L;2I"}) +CREATE_MODE(RawMode, RAWMODE_L_2IR, {"L;2IR"}) +CREATE_MODE(RawMode, RAWMODE_L_2R, {"L;2R"}) +CREATE_MODE(RawMode, RAWMODE_L_4, {"L;4"}) +CREATE_MODE(RawMode, RAWMODE_L_4I, {"L;4I"}) +CREATE_MODE(RawMode, RAWMODE_L_4IR, {"L;4IR"}) +CREATE_MODE(RawMode, RAWMODE_L_4R, {"L;4R"}) +CREATE_MODE(RawMode, RAWMODE_L_I, {"L;I"}) +CREATE_MODE(RawMode, RAWMODE_L_R, {"L;R"}) +CREATE_MODE(RawMode, RAWMODE_M, {"M"}) +CREATE_MODE(RawMode, RAWMODE_M_I, {"M;I"}) +CREATE_MODE(RawMode, RAWMODE_PA_L, {"PA;L"}) +CREATE_MODE(RawMode, RAWMODE_PX, {"PX"}) +CREATE_MODE(RawMode, RAWMODE_P_1, {"P;1"}) +CREATE_MODE(RawMode, RAWMODE_P_2, {"P;2"}) +CREATE_MODE(RawMode, RAWMODE_P_2L, {"P;2L"}) +CREATE_MODE(RawMode, RAWMODE_P_4, {"P;4"}) +CREATE_MODE(RawMode, RAWMODE_P_4L, {"P;4L"}) +CREATE_MODE(RawMode, RAWMODE_P_R, {"P;R"}) +CREATE_MODE(RawMode, RAWMODE_R, {"R"}) +CREATE_MODE(RawMode, RAWMODE_RGBAX, {"RGBAX"}) +CREATE_MODE(RawMode, RAWMODE_RGBAXX, {"RGBAXX"}) +CREATE_MODE(RawMode, RAWMODE_RGBA_15, {"RGBA;15"}) +CREATE_MODE(RawMode, RAWMODE_RGBA_16B, {"RGBA;16B"}) +CREATE_MODE(RawMode, RAWMODE_RGBA_16L, {"RGBA;16L"}) +CREATE_MODE(RawMode, RAWMODE_RGBA_16N, {"RGBA;16N"}) +CREATE_MODE(RawMode, RAWMODE_RGBA_4B, {"RGBA;4B"}) +CREATE_MODE(RawMode, RAWMODE_RGBA_I, {"RGBA;I"}) +CREATE_MODE(RawMode, RAWMODE_RGBA_L, {"RGBA;L"}) +CREATE_MODE(RawMode, RAWMODE_RGBXX, {"RGBXX"}) +CREATE_MODE(RawMode, RAWMODE_RGBXXX, {"RGBXXX"}) +CREATE_MODE(RawMode, RAWMODE_RGBX_16B, {"RGBX;16B"}) +CREATE_MODE(RawMode, RAWMODE_RGBX_16L, {"RGBX;16L"}) +CREATE_MODE(RawMode, RAWMODE_RGBX_16N, {"RGBX;16N"}) +CREATE_MODE(RawMode, RAWMODE_RGBX_L, {"RGBX;L"}) +CREATE_MODE(RawMode, RAWMODE_RGB_15, {"RGB;15"}) +CREATE_MODE(RawMode, RAWMODE_RGB_16, {"RGB;16"}) +CREATE_MODE(RawMode, RAWMODE_RGB_16B, {"RGB;16B"}) +CREATE_MODE(RawMode, RAWMODE_RGB_16L, {"RGB;16L"}) +CREATE_MODE(RawMode, RAWMODE_RGB_16N, {"RGB;16N"}) +CREATE_MODE(RawMode, RAWMODE_RGB_4B, {"RGB;4B"}) +CREATE_MODE(RawMode, RAWMODE_RGB_L, {"RGB;L"}) +CREATE_MODE(RawMode, RAWMODE_RGB_R, {"RGB;R"}) +CREATE_MODE(RawMode, RAWMODE_RGBaX, {"RGBaX"}) +CREATE_MODE(RawMode, RAWMODE_RGBaXX, {"RGBaXX"}) +CREATE_MODE(RawMode, RAWMODE_RGBa_16B, {"RGBa;16B"}) +CREATE_MODE(RawMode, RAWMODE_RGBa_16L, {"RGBa;16L"}) +CREATE_MODE(RawMode, RAWMODE_RGBa_16N, {"RGBa;16N"}) +CREATE_MODE(RawMode, RAWMODE_R_16B, {"R;16B"}) +CREATE_MODE(RawMode, RAWMODE_R_16L, {"R;16L"}) +CREATE_MODE(RawMode, RAWMODE_R_16N, {"R;16N"}) +CREATE_MODE(RawMode, RAWMODE_S, {"S"}) +CREATE_MODE(RawMode, RAWMODE_V, {"V"}) +CREATE_MODE(RawMode, RAWMODE_X, {"X"}) +CREATE_MODE(RawMode, RAWMODE_XBGR, {"XBGR"}) +CREATE_MODE(RawMode, RAWMODE_XRGB, {"XRGB"}) +CREATE_MODE(RawMode, RAWMODE_Y, {"Y"}) +CREATE_MODE(RawMode, RAWMODE_YCCA_P, {"YCCA;P"}) +CREATE_MODE(RawMode, RAWMODE_YCC_P, {"YCC;P"}) +CREATE_MODE(RawMode, RAWMODE_YCbCrK, {"YCbCrK"}) +CREATE_MODE(RawMode, RAWMODE_YCbCrX, {"YCbCrX"}) +CREATE_MODE(RawMode, RAWMODE_YCbCr_L, {"YCbCr;L"}) +CREATE_MODE(RawMode, RAWMODE_Y_I, {"Y;I"}) +CREATE_MODE(RawMode, RAWMODE_aBGR, {"aBGR"}) +CREATE_MODE(RawMode, RAWMODE_aRGB, {"aRGB"}) + const RawMode * const RAWMODES[] = { IMAGING_RAWMODE_1, IMAGING_RAWMODE_CMYK, @@ -138,6 +284,152 @@ const RawMode * const RAWMODES[] = { IMAGING_RAWMODE_I_32L, IMAGING_RAWMODE_I_32B, + IMAGING_RAWMODE_1_8, + IMAGING_RAWMODE_1_I, + IMAGING_RAWMODE_1_IR, + IMAGING_RAWMODE_1_R, + IMAGING_RAWMODE_A, + IMAGING_RAWMODE_ABGR, + IMAGING_RAWMODE_ARGB, + IMAGING_RAWMODE_A_16B, + IMAGING_RAWMODE_A_16L, + IMAGING_RAWMODE_A_16N, + IMAGING_RAWMODE_B, + IMAGING_RAWMODE_BGAR, + IMAGING_RAWMODE_BGR, + IMAGING_RAWMODE_BGRA, + IMAGING_RAWMODE_BGRA_15, + IMAGING_RAWMODE_BGRA_15Z, + IMAGING_RAWMODE_BGRA_16B, + IMAGING_RAWMODE_BGRA_16L, + IMAGING_RAWMODE_BGRX, + IMAGING_RAWMODE_BGR_5, + IMAGING_RAWMODE_BGRa, + IMAGING_RAWMODE_BGXR, + IMAGING_RAWMODE_B_16B, + IMAGING_RAWMODE_B_16L, + IMAGING_RAWMODE_B_16N, + IMAGING_RAWMODE_C, + IMAGING_RAWMODE_CMYKX, + IMAGING_RAWMODE_CMYKXX, + IMAGING_RAWMODE_CMYK_16B, + IMAGING_RAWMODE_CMYK_16L, + IMAGING_RAWMODE_CMYK_16N, + IMAGING_RAWMODE_CMYK_I, + IMAGING_RAWMODE_CMYK_L, + IMAGING_RAWMODE_C_I, + IMAGING_RAWMODE_Cb, + IMAGING_RAWMODE_Cr, + IMAGING_RAWMODE_F_16, + IMAGING_RAWMODE_F_16B, + IMAGING_RAWMODE_F_16BS, + IMAGING_RAWMODE_F_16N, + IMAGING_RAWMODE_F_16NS, + IMAGING_RAWMODE_F_16S, + IMAGING_RAWMODE_F_32, + IMAGING_RAWMODE_F_32B, + IMAGING_RAWMODE_F_32BF, + IMAGING_RAWMODE_F_32BS, + IMAGING_RAWMODE_F_32F, + IMAGING_RAWMODE_F_32N, + IMAGING_RAWMODE_F_32NF, + IMAGING_RAWMODE_F_32NS, + IMAGING_RAWMODE_F_32S, + IMAGING_RAWMODE_F_64BF, + IMAGING_RAWMODE_F_64F, + IMAGING_RAWMODE_F_64NF, + IMAGING_RAWMODE_F_8, + IMAGING_RAWMODE_F_8S, + IMAGING_RAWMODE_G, + IMAGING_RAWMODE_G_16B, + IMAGING_RAWMODE_G_16L, + IMAGING_RAWMODE_G_16N, + IMAGING_RAWMODE_H, + IMAGING_RAWMODE_I_12, + IMAGING_RAWMODE_I_16BS, + IMAGING_RAWMODE_I_16NS, + IMAGING_RAWMODE_I_16R, + IMAGING_RAWMODE_I_16S, + IMAGING_RAWMODE_I_32, + IMAGING_RAWMODE_I_32BS, + IMAGING_RAWMODE_I_32N, + IMAGING_RAWMODE_I_32NS, + IMAGING_RAWMODE_I_32S, + IMAGING_RAWMODE_I_8, + IMAGING_RAWMODE_I_8S, + IMAGING_RAWMODE_K, + IMAGING_RAWMODE_K_I, + IMAGING_RAWMODE_LA_16B, + IMAGING_RAWMODE_LA_L, + IMAGING_RAWMODE_L_16, + IMAGING_RAWMODE_L_16B, + IMAGING_RAWMODE_L_2, + IMAGING_RAWMODE_L_2I, + IMAGING_RAWMODE_L_2IR, + IMAGING_RAWMODE_L_2R, + IMAGING_RAWMODE_L_4, + IMAGING_RAWMODE_L_4I, + IMAGING_RAWMODE_L_4IR, + IMAGING_RAWMODE_L_4R, + IMAGING_RAWMODE_L_I, + IMAGING_RAWMODE_L_R, + IMAGING_RAWMODE_M, + IMAGING_RAWMODE_M_I, + IMAGING_RAWMODE_PA_L, + IMAGING_RAWMODE_PX, + IMAGING_RAWMODE_P_1, + IMAGING_RAWMODE_P_2, + IMAGING_RAWMODE_P_2L, + IMAGING_RAWMODE_P_4, + IMAGING_RAWMODE_P_4L, + IMAGING_RAWMODE_P_R, + IMAGING_RAWMODE_R, + IMAGING_RAWMODE_RGBAX, + IMAGING_RAWMODE_RGBAXX, + IMAGING_RAWMODE_RGBA_15, + IMAGING_RAWMODE_RGBA_16B, + IMAGING_RAWMODE_RGBA_16L, + IMAGING_RAWMODE_RGBA_16N, + IMAGING_RAWMODE_RGBA_4B, + IMAGING_RAWMODE_RGBA_I, + IMAGING_RAWMODE_RGBA_L, + IMAGING_RAWMODE_RGBXX, + IMAGING_RAWMODE_RGBXXX, + IMAGING_RAWMODE_RGBX_16B, + IMAGING_RAWMODE_RGBX_16L, + IMAGING_RAWMODE_RGBX_16N, + IMAGING_RAWMODE_RGBX_L, + IMAGING_RAWMODE_RGB_15, + IMAGING_RAWMODE_RGB_16, + IMAGING_RAWMODE_RGB_16B, + IMAGING_RAWMODE_RGB_16L, + IMAGING_RAWMODE_RGB_16N, + IMAGING_RAWMODE_RGB_4B, + IMAGING_RAWMODE_RGB_L, + IMAGING_RAWMODE_RGB_R, + IMAGING_RAWMODE_RGBaX, + IMAGING_RAWMODE_RGBaXX, + IMAGING_RAWMODE_RGBa_16B, + IMAGING_RAWMODE_RGBa_16L, + IMAGING_RAWMODE_RGBa_16N, + IMAGING_RAWMODE_R_16B, + IMAGING_RAWMODE_R_16L, + IMAGING_RAWMODE_R_16N, + IMAGING_RAWMODE_S, + IMAGING_RAWMODE_V, + IMAGING_RAWMODE_X, + IMAGING_RAWMODE_XBGR, + IMAGING_RAWMODE_XRGB, + IMAGING_RAWMODE_Y, + IMAGING_RAWMODE_YCCA_P, + IMAGING_RAWMODE_YCC_P, + IMAGING_RAWMODE_YCbCrK, + IMAGING_RAWMODE_YCbCrX, + IMAGING_RAWMODE_YCbCr_L, + IMAGING_RAWMODE_Y_I, + IMAGING_RAWMODE_aBGR, + IMAGING_RAWMODE_aRGB, + NULL }; From 20a5aeac84ea29a41699337f3e16acd1a667e85f Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Apr 2024 15:25:40 -0500 Subject: [PATCH 1932/2374] fix findRawMode() and change findMode() to match --- src/libImaging/Mode.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c index 8d651b06dba..04e843d3277 100644 --- a/src/libImaging/Mode.c +++ b/src/libImaging/Mode.c @@ -68,10 +68,9 @@ const Mode * const MODES[] = { }; const Mode * findMode(const char * const name) { - int i = 0; const Mode * mode; - while ((mode = MODES[i++]) != NULL) { - if (!strcmp(mode->name, name)) { + for (int i = 0; (mode = MODES[i]); i++) { + if (strcmp(mode->name, name) == 0) { return mode; } } @@ -434,11 +433,9 @@ const RawMode * const RAWMODES[] = { }; const RawMode * findRawMode(const char * const name) { - int i = 0; const RawMode * rawmode; - while ((rawmode = RAWMODES[i++]) != NULL) { - const RawMode * const rawmode = RAWMODES[i]; - if (!strcmp(rawmode->name, name)) { + for (int i = 0; (rawmode = RAWMODES[i]); i++) { + if (strcmp(rawmode->name, name) == 0) { return rawmode; } } From 579c55ea86680f7bc22a0a9852146eeccafe4189 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Apr 2024 17:16:56 -0500 Subject: [PATCH 1933/2374] check for null input in findMode() and findRawMode() --- src/libImaging/Mode.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c index 04e843d3277..01b2b55015a 100644 --- a/src/libImaging/Mode.c +++ b/src/libImaging/Mode.c @@ -68,6 +68,9 @@ const Mode * const MODES[] = { }; const Mode * findMode(const char * const name) { + if (name == NULL) { + return NULL; + } const Mode * mode; for (int i = 0; (mode = MODES[i]); i++) { if (strcmp(mode->name, name) == 0) { @@ -433,6 +436,9 @@ const RawMode * const RAWMODES[] = { }; const RawMode * findRawMode(const char * const name) { + if (name == NULL) { + return NULL; + } const RawMode * rawmode; for (int i = 0; (rawmode = RAWMODES[i]); i++) { if (strcmp(rawmode->name, name) == 0) { From 422eb1ebc4384c44eef5e1c3f084abbeaf6cb010 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Apr 2024 19:47:58 -0500 Subject: [PATCH 1934/2374] replace some string function usage with imaging mode checks --- src/_imaging.c | 8 +++++++- src/libImaging/Matrix.c | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 6c98c42eacf..f2d39614029 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1824,7 +1824,13 @@ _putpalette(ImagingObject *self, PyObject *args) { ImagingPaletteDelete(self->image->palette); - self->image->mode = strlen(self->image->mode->name) == 2 ? IMAGING_MODE_PA : IMAGING_MODE_P; + if (self->image->mode == IMAGING_MODE_LA) { + self->image->mode = IMAGING_MODE_PA; + } else if (self->image->mode == IMAGING_MODE_L) { + self->image->mode = IMAGING_MODE_P; + } else { + // The image already has a palette mode so we don't need to change it. + } self->image->palette = ImagingPaletteNew(palette_mode); diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index fd558461100..f848b870d05 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -46,7 +46,11 @@ ImagingConvertMatrix(Imaging im, const Mode *mode, float m[]) { } } ImagingSectionLeave(&cookie); - } else if (strlen(mode->name) == 3) { + } else if ( + mode == IMAGING_MODE_HSV || + mode == IMAGING_MODE_LAB || + mode == IMAGING_MODE_RGB + ) { imOut = ImagingNewDirty(mode, im->xsize, im->ysize); if (!imOut) { return NULL; From 16fc61ee657f4a5df67993a6845940cd1f35612d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Apr 2024 20:16:28 -0500 Subject: [PATCH 1935/2374] use RawMode struct for jpegmode --- src/decode.c | 11 ++++------- src/libImaging/Jpeg.h | 4 ++-- src/libImaging/JpegDecode.c | 10 +++++----- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/decode.c b/src/decode.c index 9f4de28a828..e48dbbc3da8 100644 --- a/src/decode.c +++ b/src/decode.c @@ -820,20 +820,17 @@ PyImaging_JpegDecoderNew(PyObject *self, PyObject *args) { char *mode_name; char *rawmode_name; /* what we want from the decoder */ - char *jpegmode; /* what's in the file */ + char *jpegmode_name; /* what's in the file */ int scale = 1; int draft = 0; - if (!PyArg_ParseTuple(args, "ssz|ii", &mode_name, &rawmode_name, &jpegmode, &scale, &draft)) { + if (!PyArg_ParseTuple(args, "ssz|ii", &mode_name, &rawmode_name, &jpegmode_name, &scale, &draft)) { return NULL; } const Mode * const mode = findMode(mode_name); const RawMode * rawmode = findRawMode(rawmode_name); - - if (!jpegmode) { - jpegmode = ""; - } + const RawMode * const jpegmode = findRawMode(jpegmode_name); decoder = PyImaging_DecoderNew(sizeof(JPEGSTATE)); if (decoder == NULL) { @@ -857,7 +854,7 @@ PyImaging_JpegDecoderNew(PyObject *self, PyObject *args) { JPEGSTATE *jpeg_decoder_state_context = (JPEGSTATE *)decoder->state.context; jpeg_decoder_state_context->rawmode = rawmode; - strncpy(jpeg_decoder_state_context->jpegmode, jpegmode, 8); + jpeg_decoder_state_context->jpegmode = jpegmode; jpeg_decoder_state_context->scale = scale; jpeg_decoder_state_context->draft = draft; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 35df91d7ffa..48c6c618411 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -28,8 +28,8 @@ typedef struct { typedef struct { /* CONFIGURATION */ - /* Jpeg file mode (empty if not known) */ - char jpegmode[8 + 1]; + /* Jpeg file mode (NULL if not known) */ + const RawMode *jpegmode; /* Converter output mode (input to the shuffler) */ /* If NULL, convert conversions are disabled */ diff --git a/src/libImaging/JpegDecode.c b/src/libImaging/JpegDecode.c index 36eb7835a7f..49d4fcb2f8a 100644 --- a/src/libImaging/JpegDecode.c +++ b/src/libImaging/JpegDecode.c @@ -182,15 +182,15 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by /* jpegmode indicates what's in the file; if not set, we'll trust the decoder */ - if (strcmp(context->jpegmode, "L") == 0) { + if (context->jpegmode == IMAGING_RAWMODE_L) { context->cinfo.jpeg_color_space = JCS_GRAYSCALE; - } else if (strcmp(context->jpegmode, "RGB") == 0) { + } else if (context->jpegmode == IMAGING_RAWMODE_RGB) { context->cinfo.jpeg_color_space = JCS_RGB; - } else if (strcmp(context->jpegmode, "CMYK") == 0) { + } else if (context->jpegmode == IMAGING_RAWMODE_CMYK) { context->cinfo.jpeg_color_space = JCS_CMYK; - } else if (strcmp(context->jpegmode, "YCbCr") == 0) { + } else if (context->jpegmode == IMAGING_RAWMODE_YCbCr) { context->cinfo.jpeg_color_space = JCS_YCbCr; - } else if (strcmp(context->jpegmode, "YCbCrK") == 0) { + } else if (context->jpegmode == IMAGING_RAWMODE_YCbCrK) { context->cinfo.jpeg_color_space = JCS_YCCK; } From 4b07ed52fd22e5a3407f98fc999d0a8d6ef4546e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Apr 2024 20:43:49 -0500 Subject: [PATCH 1936/2374] use Mode struct for windows display code --- src/display.c | 16 +++++++--------- src/libImaging/Dib.c | 34 ++++++++++++++++------------------ src/libImaging/ImDib.h | 6 +++--- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/display.c b/src/display.c index 3215f6691ee..0650b886640 100644 --- a/src/display.c +++ b/src/display.c @@ -47,7 +47,7 @@ typedef struct { static PyTypeObject ImagingDisplayType; static ImagingDisplayObject * -_new(const char *mode, int xsize, int ysize) { +_new(const Mode * const mode, int xsize, int ysize) { ImagingDisplayObject *display; if (PyType_Ready(&ImagingDisplayType) < 0) { @@ -235,7 +235,7 @@ static struct PyMethodDef methods[] = { static PyObject * _getattr_mode(ImagingDisplayObject *self, void *closure) { - return Py_BuildValue("s", self->dib->mode); + return Py_BuildValue("s", self->dib->mode->name); } static PyObject * @@ -258,13 +258,14 @@ static PyTypeObject ImagingDisplayType = { PyObject * PyImaging_DisplayWin32(PyObject *self, PyObject *args) { ImagingDisplayObject *display; - char *mode; + char *mode_name; int xsize, ysize; - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { + if (!PyArg_ParseTuple(args, "s(ii)", &mode_name, &xsize, &ysize)) { return NULL; } + const Mode * const mode = findMode(mode_name); display = _new(mode, xsize, ysize); if (display == NULL) { return NULL; @@ -275,12 +276,9 @@ PyImaging_DisplayWin32(PyObject *self, PyObject *args) { PyObject * PyImaging_DisplayModeWin32(PyObject *self, PyObject *args) { - char *mode; int size[2]; - - mode = ImagingGetModeDIB(size); - - return Py_BuildValue("s(ii)", mode, size[0], size[1]); + const Mode * const mode = ImagingGetModeDIB(size); + return Py_BuildValue("s(ii)", mode->name, size[0], size[1]); } /* -------------------------------------------------------------------- */ diff --git a/src/libImaging/Dib.c b/src/libImaging/Dib.c index c69e9e552ae..154c610ec22 100644 --- a/src/libImaging/Dib.c +++ b/src/libImaging/Dib.c @@ -25,20 +25,17 @@ #include "ImDib.h" -char * +const Mode * ImagingGetModeDIB(int size_out[2]) { /* Get device characteristics */ - HDC dc; - char *mode; + const HDC dc = CreateCompatibleDC(NULL); - dc = CreateCompatibleDC(NULL); - - mode = "P"; + const Mode *mode = IMAGING_MODE_P; if (!(GetDeviceCaps(dc, RASTERCAPS) & RC_PALETTE)) { - mode = "RGB"; + mode = IMAGING_MODE_RGB; if (GetDeviceCaps(dc, BITSPIXEL) == 1) { - mode = "1"; + mode = IMAGING_MODE_1; } } @@ -53,7 +50,7 @@ ImagingGetModeDIB(int size_out[2]) { } ImagingDIB -ImagingNewDIB(const char *mode, int xsize, int ysize) { +ImagingNewDIB(const Mode * const mode, int xsize, int ysize) { /* Create a Windows bitmap */ ImagingDIB dib; @@ -61,10 +58,12 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) { int i; /* Check mode */ - if (strcmp(mode, "1") != 0 && strcmp(mode, "L") != 0 && strcmp(mode, "RGB") != 0) { + if (mode != IMAGING_MODE_1 && mode != IMAGING_MODE_L && mode != IMAGING_MODE_RGB) { return (ImagingDIB)ImagingError_ModeError(); } + const int pixelsize = mode == IMAGING_MODE_RGB ? 3 : 1; + /* Create DIB context and info header */ /* malloc check ok, small constant allocation */ dib = (ImagingDIB)malloc(sizeof(*dib)); @@ -83,7 +82,7 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) { dib->info->bmiHeader.biWidth = xsize; dib->info->bmiHeader.biHeight = ysize; dib->info->bmiHeader.biPlanes = 1; - dib->info->bmiHeader.biBitCount = strlen(mode) * 8; + dib->info->bmiHeader.biBitCount = pixelsize * 8; dib->info->bmiHeader.biCompression = BI_RGB; /* Create DIB */ @@ -103,12 +102,12 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) { return (ImagingDIB)ImagingError_MemoryError(); } - strcpy(dib->mode, mode); + dib->mode = mode; dib->xsize = xsize; dib->ysize = ysize; - dib->pixelsize = strlen(mode); - dib->linesize = (xsize * dib->pixelsize + 3) & -4; + dib->pixelsize = pixelsize; + dib->linesize = (xsize * pixelsize + 3) & -4; if (dib->pixelsize == 1) { dib->pack = dib->unpack = (ImagingShuffler)memcpy; @@ -132,7 +131,7 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) { } /* Create an associated palette (for 8-bit displays only) */ - if (strcmp(ImagingGetModeDIB(NULL), "P") == 0) { + if (ImagingGetModeDIB(NULL) == IMAGING_MODE_P) { char palbuf[sizeof(LOGPALETTE) + 256 * sizeof(PALETTEENTRY)]; LPLOGPALETTE pal = (LPLOGPALETTE)palbuf; int i, r, g, b; @@ -142,7 +141,7 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) { pal->palNumEntries = 256; GetSystemPaletteEntries(dib->dc, 0, 256, pal->palPalEntry); - if (strcmp(mode, "L") == 0) { + if (mode == IMAGING_MODE_L) { /* Grayscale DIB. Fill all 236 slots with a grayscale ramp * (this is usually overkill on Windows since VGA only offers * 6 bits grayscale resolution). Ignore the slots already @@ -156,8 +155,7 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) { } dib->palette = CreatePalette(pal); - - } else if (strcmp(mode, "RGB") == 0) { + } else if (mode == IMAGING_MODE_RGB) { #ifdef CUBE216 /* Colour DIB. Create a 6x6x6 colour cube (216 entries) and diff --git a/src/libImaging/ImDib.h b/src/libImaging/ImDib.h index 91ff3f322ff..6d8f420cb5c 100644 --- a/src/libImaging/ImDib.h +++ b/src/libImaging/ImDib.h @@ -27,7 +27,7 @@ struct ImagingDIBInstance { UINT8 *bits; HPALETTE palette; /* Used by cut and paste */ - char mode[4]; + const Mode *mode; int xsize, ysize; int pixelsize; int linesize; @@ -37,11 +37,11 @@ struct ImagingDIBInstance { typedef struct ImagingDIBInstance *ImagingDIB; -extern char * +extern const Mode * ImagingGetModeDIB(int size_out[2]); extern ImagingDIB -ImagingNewDIB(const char *mode, int xsize, int ysize); +ImagingNewDIB(const Mode * const mode, int xsize, int ysize); extern void ImagingDeleteDIB(ImagingDIB im); From 9527ce7f8c925e30e8b5535e8b115366743495e4 Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 16:54:32 +0200 Subject: [PATCH 1937/2374] change mode structs to enums Structs have better type safety, but they make allocation more difficult, especially when we have multiple Python modules trying to share the same code. --- src/_imaging.c | 51 +-- src/decode.c | 44 +- src/display.c | 10 +- src/encode.c | 34 +- src/libImaging/Access.c | 71 ++- src/libImaging/Bands.c | 2 +- src/libImaging/Chops.c | 58 +-- src/libImaging/Convert.c | 325 +++++++------- src/libImaging/Dib.c | 6 +- src/libImaging/Fill.c | 4 +- src/libImaging/ImDib.h | 6 +- src/libImaging/Imaging.h | 77 ++-- src/libImaging/Jpeg.h | 10 +- src/libImaging/Jpeg2KDecode.c | 2 +- src/libImaging/JpegDecode.c | 10 +- src/libImaging/Matrix.c | 2 +- src/libImaging/Mode.c | 649 ++++++++++----------------- src/libImaging/Mode.h | 444 ++++++++++--------- src/libImaging/Pack.c | 340 ++++---------- src/libImaging/Palette.c | 2 +- src/libImaging/Point.c | 4 +- src/libImaging/Storage.c | 16 +- src/libImaging/TiffDecode.c | 4 +- src/libImaging/Unpack.c | 804 ++++++++++------------------------ src/map.c | 2 +- 25 files changed, 1127 insertions(+), 1850 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index f2d39614029..a940bb97448 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -368,7 +368,7 @@ ImagingError_ValueError(const char *message) { /* -------------------------------------------------------------------- */ static int -getbands(const Mode *mode) { +getbands(const ModeID mode) { Imaging im; int bands; @@ -731,7 +731,7 @@ _fill(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); im = ImagingNewDirty(mode, xsize, ysize); if (!im) { @@ -760,7 +760,7 @@ _new(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); return PyImagingNew(ImagingNew(mode, xsize, ysize)); } @@ -774,7 +774,7 @@ _new_block(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); return PyImagingNew(ImagingNewBlock(mode, xsize, ysize)); } @@ -787,7 +787,7 @@ _linear_gradient(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); return PyImagingNew(ImagingFillLinearGradient(mode)); } @@ -800,7 +800,7 @@ _radial_gradient(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); return PyImagingNew(ImagingFillRadialGradient(mode)); } @@ -964,7 +964,7 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); /* actually, it is trilinear */ if (filter != IMAGING_TRANSFORM_BILINEAR) { @@ -1033,7 +1033,7 @@ _convert(ImagingObject *self, PyObject *args) { } } - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); return PyImagingNew(ImagingConvert( self->image, mode, paletteimage ? paletteimage->image->palette : NULL, dither @@ -1084,7 +1084,7 @@ _convert_matrix(ImagingObject *self, PyObject *args) { } } - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); return PyImagingNew(ImagingConvertMatrix(self->image, mode, m)); } @@ -1094,12 +1094,12 @@ _convert_transparent(ImagingObject *self, PyObject *args) { char *mode_name; int r, g, b; if (PyArg_ParseTuple(args, "s(iii)", &mode_name, &r, &g, &b)) { - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, g, b)); } PyErr_Clear(); if (PyArg_ParseTuple(args, "si", &mode_name, &r)) { - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, 0, 0)); } return NULL; @@ -1209,8 +1209,8 @@ _getpalette(ImagingObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); pack = ImagingFindPacker(mode, rawmode, &bits); if (!pack) { @@ -1238,7 +1238,7 @@ _getpalettemode(ImagingObject *self) { return NULL; } - return PyUnicode_FromString(self->image->palette->mode->name); + return PyUnicode_FromString(getModeData(self->image->palette->mode)->name); } static inline int @@ -1524,7 +1524,7 @@ _point(ImagingObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); if (mode == IMAGING_MODE_F) { FLOAT32 *data; @@ -1799,14 +1799,14 @@ _putpalette(ImagingObject *self, PyObject *args) { return NULL; } - const Mode * const palette_mode = findMode(palette_mode_name); - if (palette_mode == NULL) { + const ModeID palette_mode = findModeID(palette_mode_name); + if (palette_mode == IMAGING_MODE_UNKNOWN) { PyErr_SetString(PyExc_ValueError, wrong_mode); return NULL; } - const RawMode * const rawmode = findRawMode(rawmode_name); - if (rawmode == NULL) { + const RawModeID rawmode = findRawModeID(rawmode_name); + if (rawmode == IMAGING_RAWMODE_UNKNOWN) { PyErr_SetString(PyExc_ValueError, wrong_raw_mode); return NULL; } @@ -2052,7 +2052,7 @@ _reduce(ImagingObject *self, PyObject *args) { } static int -isRGB(const Mode * const mode) { +isRGB(const ModeID mode) { return mode == IMAGING_MODE_RGB || mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_RGBX; } @@ -2068,7 +2068,7 @@ im_setmode(ImagingObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); im = self->image; @@ -2472,7 +2472,7 @@ _merge(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); if (band0) { bands[0] = band0->image; @@ -3779,7 +3779,7 @@ static struct PyMethodDef methods[] = { static PyObject * _getattr_mode(ImagingObject *self, void *closure) { - return PyUnicode_FromString(self->image->mode->name); + return PyUnicode_FromString(getModeData(self->image->mode)->name); } static PyObject * @@ -4323,11 +4323,6 @@ setup_module(PyObject *m) { return -1; } - ImagingAccessInit(); - ImagingConvertInit(); - ImagingPackInit(); - ImagingUnpackInit(); - #ifdef HAVE_LIBJPEG { extern const char *ImagingJpegVersion(void); diff --git a/src/decode.c b/src/decode.c index e48dbbc3da8..41b2f6f3167 100644 --- a/src/decode.c +++ b/src/decode.c @@ -266,7 +266,7 @@ static PyTypeObject ImagingDecoderType = { /* -------------------------------------------------------------------- */ int -get_unpacker(ImagingDecoderObject *decoder, const Mode *mode, const RawMode *rawmode) { +get_unpacker(ImagingDecoderObject *decoder, const ModeID mode, const RawModeID rawmode) { int bits; ImagingShuffler unpack; @@ -441,8 +441,8 @@ PyImaging_HexDecoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { @@ -481,8 +481,8 @@ PyImaging_LibTiffDecoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); TRACE(("new tiff decoder %s\n", compname)); @@ -522,8 +522,8 @@ PyImaging_PackbitsDecoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { @@ -576,8 +576,8 @@ PyImaging_PcxDecoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { @@ -610,8 +610,8 @@ PyImaging_RawDecoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); decoder = PyImaging_DecoderNew(sizeof(RAWSTATE)); if (decoder == NULL) { @@ -646,8 +646,8 @@ PyImaging_SgiRleDecoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); decoder = PyImaging_DecoderNew(sizeof(SGISTATE)); if (decoder == NULL) { @@ -680,8 +680,8 @@ PyImaging_SunRleDecoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { @@ -712,8 +712,8 @@ PyImaging_TgaRleDecoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { @@ -772,8 +772,8 @@ PyImaging_ZipDecoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); decoder = PyImaging_DecoderNew(sizeof(ZIPSTATE)); if (decoder == NULL) { @@ -828,9 +828,9 @@ PyImaging_JpegDecoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * rawmode = findRawMode(rawmode_name); - const RawMode * const jpegmode = findRawMode(jpegmode_name); + const ModeID mode = findModeID(mode_name); + RawModeID rawmode = findRawModeID(rawmode_name); + const RawModeID jpegmode = findRawModeID(jpegmode_name); decoder = PyImaging_DecoderNew(sizeof(JPEGSTATE)); if (decoder == NULL) { diff --git a/src/display.c b/src/display.c index 0650b886640..5b5853a3cb8 100644 --- a/src/display.c +++ b/src/display.c @@ -47,7 +47,7 @@ typedef struct { static PyTypeObject ImagingDisplayType; static ImagingDisplayObject * -_new(const Mode * const mode, int xsize, int ysize) { +_new(const ModeID mode, int xsize, int ysize) { ImagingDisplayObject *display; if (PyType_Ready(&ImagingDisplayType) < 0) { @@ -235,7 +235,7 @@ static struct PyMethodDef methods[] = { static PyObject * _getattr_mode(ImagingDisplayObject *self, void *closure) { - return Py_BuildValue("s", self->dib->mode->name); + return Py_BuildValue("s", getModeData(self->dib->mode)->name); } static PyObject * @@ -265,7 +265,7 @@ PyImaging_DisplayWin32(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); display = _new(mode, xsize, ysize); if (display == NULL) { return NULL; @@ -277,8 +277,8 @@ PyImaging_DisplayWin32(PyObject *self, PyObject *args) { PyObject * PyImaging_DisplayModeWin32(PyObject *self, PyObject *args) { int size[2]; - const Mode * const mode = ImagingGetModeDIB(size); - return Py_BuildValue("s(ii)", mode->name, size[0], size[1]); + const ModeID mode = ImagingGetModeDIB(size); + return Py_BuildValue("s(ii)", getModeData(mode)->name, size[0], size[1]); } /* -------------------------------------------------------------------- */ diff --git a/src/encode.c b/src/encode.c index 311ffa4eed0..3a6b6d6d0a2 100644 --- a/src/encode.c +++ b/src/encode.c @@ -334,7 +334,7 @@ static PyTypeObject ImagingEncoderType = { /* -------------------------------------------------------------------- */ int -get_packer(ImagingEncoderObject *encoder, const Mode *mode, const RawMode *rawmode) { +get_packer(ImagingEncoderObject *encoder, const ModeID mode, const RawModeID rawmode) { int bits; ImagingShuffler pack; @@ -344,8 +344,8 @@ get_packer(ImagingEncoderObject *encoder, const Mode *mode, const RawMode *rawmo PyErr_Format( PyExc_ValueError, "No packer found from %s to %s", - mode->name, - rawmode->name + getModeData(mode)->name, + getRawModeData(rawmode)->name ); return -1; } @@ -420,8 +420,8 @@ PyImaging_GifEncoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); if (get_packer(encoder, mode, rawmode) < 0) { return NULL; @@ -456,8 +456,8 @@ PyImaging_PcxEncoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); if (get_packer(encoder, mode, rawmode) < 0) { return NULL; @@ -490,8 +490,8 @@ PyImaging_RawEncoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); if (get_packer(encoder, mode, rawmode) < 0) { return NULL; @@ -526,8 +526,8 @@ PyImaging_TgaRleEncoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); if (get_packer(encoder, mode, rawmode) < 0) { return NULL; @@ -614,8 +614,8 @@ PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); if (get_packer(encoder, mode, rawmode) < 0) { free(dictionary); @@ -721,8 +721,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * const rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); if (get_packer(encoder, mode, rawmode) < 0) { return NULL; @@ -1161,8 +1161,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); - const RawMode * rawmode = findRawMode(rawmode_name); + const ModeID mode = findModeID(mode_name); + RawModeID rawmode = findRawModeID(rawmode_name); // libjpeg-turbo supports different output formats. // We are choosing Pillow's native format (3 color bytes + 1 padding) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 59a776fe04e..6360e914761 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -116,51 +116,38 @@ put_pixel_32(Imaging im, int x, int y, const void *color) { memcpy(&im->image32[y][x], color, sizeof(INT32)); } -static struct ImagingAccessInstance *accessors = NULL; - -void -ImagingAccessInit(void) { - const struct ImagingAccessInstance temp[] = { - {IMAGING_MODE_1, get_pixel_8, put_pixel_8}, - {IMAGING_MODE_L, get_pixel_8, put_pixel_8}, - {IMAGING_MODE_LA, get_pixel_32_2bands, put_pixel_32}, - {IMAGING_MODE_La, get_pixel_32_2bands, put_pixel_32}, - {IMAGING_MODE_I, get_pixel_32, put_pixel_32}, - {IMAGING_MODE_I_16, get_pixel_16L, put_pixel_16L}, - {IMAGING_MODE_I_16L, get_pixel_16L, put_pixel_16L}, - {IMAGING_MODE_I_16B, get_pixel_16B, put_pixel_16B}, +static struct ImagingAccessInstance accessors[] = { + {IMAGING_MODE_1, get_pixel_8, put_pixel_8}, + {IMAGING_MODE_L, get_pixel_8, put_pixel_8}, + {IMAGING_MODE_LA, get_pixel_32_2bands, put_pixel_32}, + {IMAGING_MODE_La, get_pixel_32_2bands, put_pixel_32}, + {IMAGING_MODE_I, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_I_16, get_pixel_16L, put_pixel_16L}, + {IMAGING_MODE_I_16L, get_pixel_16L, put_pixel_16L}, + {IMAGING_MODE_I_16B, get_pixel_16B, put_pixel_16B}, #ifdef WORDS_BIGENDIAN - {IMAGING_MODE_I_16N, get_pixel_16B, put_pixel_16B}, + {IMAGING_MODE_I_16N, get_pixel_16B, put_pixel_16B}, #else - {IMAGING_MODE_I_16N, get_pixel_16L, put_pixel_16L}, + {IMAGING_MODE_I_16N, get_pixel_16L, put_pixel_16L}, #endif - {IMAGING_MODE_I_32L, get_pixel_32L, put_pixel_32L}, - {IMAGING_MODE_I_32B, get_pixel_32B, put_pixel_32B}, - {IMAGING_MODE_F, get_pixel_32, put_pixel_32}, - {IMAGING_MODE_P, get_pixel_8, put_pixel_8}, - {IMAGING_MODE_PA, get_pixel_32_2bands, put_pixel_32}, - {IMAGING_MODE_RGB, get_pixel_32, put_pixel_32}, - {IMAGING_MODE_RGBA, get_pixel_32, put_pixel_32}, - {IMAGING_MODE_RGBa, get_pixel_32, put_pixel_32}, - {IMAGING_MODE_RGBX, get_pixel_32, put_pixel_32}, - {IMAGING_MODE_CMYK, get_pixel_32, put_pixel_32}, - {IMAGING_MODE_YCbCr, get_pixel_32, put_pixel_32}, - {IMAGING_MODE_LAB, get_pixel_32, put_pixel_32}, - {IMAGING_MODE_HSV, get_pixel_32, put_pixel_32}, - {NULL} - }; - accessors = malloc(sizeof(temp)); - if (accessors == NULL) { - fprintf(stderr, "AccessInit: failed to allocate memory for accessors table\n"); - exit(1); - } - memcpy(accessors, temp, sizeof(temp)); -} + {IMAGING_MODE_I_32L, get_pixel_32L, put_pixel_32L}, + {IMAGING_MODE_I_32B, get_pixel_32B, put_pixel_32B}, + {IMAGING_MODE_F, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_P, get_pixel_8, put_pixel_8}, + {IMAGING_MODE_PA, get_pixel_32_2bands, put_pixel_32}, + {IMAGING_MODE_RGB, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_RGBA, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_RGBa, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_RGBX, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_CMYK, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_YCbCr, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_LAB, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_HSV, get_pixel_32, put_pixel_32}, +}; ImagingAccess ImagingAccessNew(const Imaging im) { - int i; - for (i = 0; accessors[i].mode; i++) { + for (size_t i = 0; i < sizeof(accessors) / sizeof(*accessors); i++) { if (im->mode == accessors[i].mode) { return &accessors[i]; } @@ -170,9 +157,3 @@ ImagingAccessNew(const Imaging im) { void _ImagingAccessDelete(Imaging im, ImagingAccess access) {} - -void -ImagingAccessFree(void) { - free(accessors); - accessors = NULL; -} diff --git a/src/libImaging/Bands.c b/src/libImaging/Bands.c index 501b4625fc5..d1b0ebc4ed8 100644 --- a/src/libImaging/Bands.c +++ b/src/libImaging/Bands.c @@ -240,7 +240,7 @@ ImagingFillBand(Imaging imOut, int band, int color) { } Imaging -ImagingMerge(const Mode *mode, Imaging bands[4]) { +ImagingMerge(const ModeID mode, Imaging bands[4]) { int i, x, y; int bandsCount = 0; Imaging imOut; diff --git a/src/libImaging/Chops.c b/src/libImaging/Chops.c index 66d0b4f9774..331f2dfe64c 100644 --- a/src/libImaging/Chops.c +++ b/src/libImaging/Chops.c @@ -18,28 +18,28 @@ #include "Imaging.h" -#define CHOP(operation) \ - int x, y; \ - Imaging imOut; \ - imOut = create(imIn1, imIn2, NULL); \ - if (!imOut) { \ - return NULL; \ - } \ - for (y = 0; y < imOut->ysize; y++) { \ - UINT8 *out = (UINT8 *)imOut->image[y]; \ - UINT8 *in1 = (UINT8 *)imIn1->image[y]; \ - UINT8 *in2 = (UINT8 *)imIn2->image[y]; \ - for (x = 0; x < imOut->linesize; x++) { \ - int temp = operation; \ - if (temp <= 0) { \ - out[x] = 0; \ - } else if (temp >= 255) { \ - out[x] = 255; \ - } else { \ - out[x] = temp; \ - } \ - } \ - } \ +#define CHOP(operation) \ + int x, y; \ + Imaging imOut; \ + imOut = create(imIn1, imIn2, IMAGING_MODE_UNKNOWN); \ + if (!imOut) { \ + return NULL; \ + } \ + for (y = 0; y < imOut->ysize; y++) { \ + UINT8 *out = (UINT8 *)imOut->image[y]; \ + UINT8 *in1 = (UINT8 *)imIn1->image[y]; \ + UINT8 *in2 = (UINT8 *)imIn2->image[y]; \ + for (x = 0; x < imOut->linesize; x++) { \ + int temp = operation; \ + if (temp <= 0) { \ + out[x] = 0; \ + } else if (temp >= 255) { \ + out[x] = 255; \ + } else { \ + out[x] = temp; \ + } \ + } \ + } \ return imOut; #define CHOP2(operation, mode) \ @@ -60,11 +60,11 @@ return imOut; static Imaging -create(Imaging im1, Imaging im2, const Mode *mode) { +create(Imaging im1, Imaging im2, const ModeID mode) { int xsize, ysize; if (!im1 || !im2 || im1->type != IMAGING_TYPE_UINT8 || - (mode != NULL && (im1->mode != mode || im2->mode != mode))) { + (mode != IMAGING_MODE_UNKNOWN && (im1->mode != mode || im2->mode != mode))) { return (Imaging)ImagingError_ModeError(); } if (im1->type != im2->type || im1->bands != im2->bands) { @@ -129,12 +129,12 @@ ImagingChopXor(Imaging imIn1, Imaging imIn2) { Imaging ImagingChopAddModulo(Imaging imIn1, Imaging imIn2) { - CHOP2(in1[x] + in2[x], NULL); + CHOP2(in1[x] + in2[x], IMAGING_MODE_UNKNOWN); } Imaging ImagingChopSubtractModulo(Imaging imIn1, Imaging imIn2) { - CHOP2(in1[x] - in2[x], NULL); + CHOP2(in1[x] - in2[x], IMAGING_MODE_UNKNOWN); } Imaging @@ -142,7 +142,7 @@ ImagingChopSoftLight(Imaging imIn1, Imaging imIn2) { CHOP2( (((255 - in1[x]) * (in1[x] * in2[x])) / 65536) + (in1[x] * (255 - ((255 - in1[x]) * (255 - in2[x]) / 255))) / 255, - NULL + IMAGING_MODE_UNKNOWN ); } @@ -151,7 +151,7 @@ ImagingChopHardLight(Imaging imIn1, Imaging imIn2) { CHOP2( (in2[x] < 128) ? ((in1[x] * in2[x]) / 127) : 255 - (((255 - in2[x]) * (255 - in1[x])) / 127), - NULL + IMAGING_MODE_UNKNOWN ); } @@ -160,6 +160,6 @@ ImagingOverlay(Imaging imIn1, Imaging imIn2) { CHOP2( (in1[x] < 128) ? ((in1[x] * in2[x]) / 127) : 255 - (((255 - in1[x]) * (255 - in2[x])) / 127), - NULL + IMAGING_MODE_UNKNOWN ); } diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 8f580c294b7..862f228e5a5 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1090,7 +1090,7 @@ pa2ycbcr(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { } static Imaging -frompalette(Imaging imOut, Imaging imIn, const Mode *mode) { +frompalette(Imaging imOut, Imaging imIn, const ModeID mode) { ImagingSectionCookie cookie; int alpha; int y; @@ -1160,7 +1160,7 @@ frompalette(Imaging imOut, Imaging imIn, const Mode *mode) { #endif static Imaging topalette( - Imaging imOut, Imaging imIn, const Mode *mode, ImagingPalette inpalette, int dither + Imaging imOut, Imaging imIn, const ModeID mode, ImagingPalette inpalette, int dither ) { ImagingSectionCookie cookie; int alpha; @@ -1456,25 +1456,151 @@ tobilevel(Imaging imOut, Imaging imIn) { /* Conversion handlers */ /* ------------------- */ -static struct Converter { - const Mode *from; - const Mode *to; +static struct { + const ModeID from; + const ModeID to; ImagingShuffler convert; -} *converters = NULL; +} converters[] = { + {IMAGING_MODE_1, IMAGING_MODE_L, bit2l}, + {IMAGING_MODE_1, IMAGING_MODE_I, bit2i}, + {IMAGING_MODE_1, IMAGING_MODE_F, bit2f}, + {IMAGING_MODE_1, IMAGING_MODE_RGB, bit2rgb}, + {IMAGING_MODE_1, IMAGING_MODE_RGBA, bit2rgb}, + {IMAGING_MODE_1, IMAGING_MODE_RGBX, bit2rgb}, + {IMAGING_MODE_1, IMAGING_MODE_CMYK, bit2cmyk}, + {IMAGING_MODE_1, IMAGING_MODE_YCbCr, bit2ycbcr}, + {IMAGING_MODE_1, IMAGING_MODE_HSV, bit2hsv}, + + {IMAGING_MODE_L, IMAGING_MODE_1, l2bit}, + {IMAGING_MODE_L, IMAGING_MODE_LA, l2la}, + {IMAGING_MODE_L, IMAGING_MODE_I, l2i}, + {IMAGING_MODE_L, IMAGING_MODE_F, l2f}, + {IMAGING_MODE_L, IMAGING_MODE_RGB, l2rgb}, + {IMAGING_MODE_L, IMAGING_MODE_RGBA, l2rgb}, + {IMAGING_MODE_L, IMAGING_MODE_RGBX, l2rgb}, + {IMAGING_MODE_L, IMAGING_MODE_CMYK, l2cmyk}, + {IMAGING_MODE_L, IMAGING_MODE_YCbCr, l2ycbcr}, + {IMAGING_MODE_L, IMAGING_MODE_HSV, l2hsv}, + + {IMAGING_MODE_LA, IMAGING_MODE_L, la2l}, + {IMAGING_MODE_LA, IMAGING_MODE_La, lA2la}, + {IMAGING_MODE_LA, IMAGING_MODE_RGB, la2rgb}, + {IMAGING_MODE_LA, IMAGING_MODE_RGBA, la2rgb}, + {IMAGING_MODE_LA, IMAGING_MODE_RGBX, la2rgb}, + {IMAGING_MODE_LA, IMAGING_MODE_CMYK, la2cmyk}, + {IMAGING_MODE_LA, IMAGING_MODE_YCbCr, la2ycbcr}, + {IMAGING_MODE_LA, IMAGING_MODE_HSV, la2hsv}, + + {IMAGING_MODE_La, IMAGING_MODE_LA, la2lA}, + + {IMAGING_MODE_I, IMAGING_MODE_L, i2l}, + {IMAGING_MODE_I, IMAGING_MODE_F, i2f}, + {IMAGING_MODE_I, IMAGING_MODE_RGB, i2rgb}, + {IMAGING_MODE_I, IMAGING_MODE_RGBA, i2rgb}, + {IMAGING_MODE_I, IMAGING_MODE_RGBX, i2rgb}, + {IMAGING_MODE_I, IMAGING_MODE_HSV, i2hsv}, + + {IMAGING_MODE_F, IMAGING_MODE_L, f2l}, + {IMAGING_MODE_F, IMAGING_MODE_I, f2i}, + + {IMAGING_MODE_RGB, IMAGING_MODE_1, rgb2bit}, + {IMAGING_MODE_RGB, IMAGING_MODE_L, rgb2l}, + {IMAGING_MODE_RGB, IMAGING_MODE_LA, rgb2la}, + {IMAGING_MODE_RGB, IMAGING_MODE_La, rgb2la}, + {IMAGING_MODE_RGB, IMAGING_MODE_I, rgb2i}, + {IMAGING_MODE_RGB, IMAGING_MODE_I_16, rgb2i16l}, + {IMAGING_MODE_RGB, IMAGING_MODE_I_16L, rgb2i16l}, + {IMAGING_MODE_RGB, IMAGING_MODE_I_16B, rgb2i16b}, +#ifdef WORDS_BIGENDIAN + {IMAGING_MODE_RGB, IMAGING_MODE_I_16N, rgb2i16b}, +#else + {IMAGING_MODE_RGB, IMAGING_MODE_I_16N, rgb2i16l}, +#endif + {IMAGING_MODE_RGB, IMAGING_MODE_F, rgb2f}, + {IMAGING_MODE_RGB, IMAGING_MODE_BGR_15, rgb2bgr15}, + {IMAGING_MODE_RGB, IMAGING_MODE_BGR_16, rgb2bgr16}, + {IMAGING_MODE_RGB, IMAGING_MODE_BGR_24, rgb2bgr24}, + {IMAGING_MODE_RGB, IMAGING_MODE_RGBA, rgb2rgba}, + {IMAGING_MODE_RGB, IMAGING_MODE_RGBa, rgb2rgba}, + {IMAGING_MODE_RGB, IMAGING_MODE_RGBX, rgb2rgba}, + {IMAGING_MODE_RGB, IMAGING_MODE_CMYK, rgb2cmyk}, + {IMAGING_MODE_RGB, IMAGING_MODE_YCbCr, ImagingConvertRGB2YCbCr}, + {IMAGING_MODE_RGB, IMAGING_MODE_HSV, rgb2hsv}, + + {IMAGING_MODE_RGBA, IMAGING_MODE_1, rgb2bit}, + {IMAGING_MODE_RGBA, IMAGING_MODE_L, rgb2l}, + {IMAGING_MODE_RGBA, IMAGING_MODE_LA, rgba2la}, + {IMAGING_MODE_RGBA, IMAGING_MODE_I, rgb2i}, + {IMAGING_MODE_RGBA, IMAGING_MODE_F, rgb2f}, + {IMAGING_MODE_RGBA, IMAGING_MODE_RGB, rgba2rgb}, + {IMAGING_MODE_RGBA, IMAGING_MODE_RGBa, rgbA2rgba}, + {IMAGING_MODE_RGBA, IMAGING_MODE_RGBX, rgb2rgba}, + {IMAGING_MODE_RGBA, IMAGING_MODE_CMYK, rgb2cmyk}, + {IMAGING_MODE_RGBA, IMAGING_MODE_YCbCr, ImagingConvertRGB2YCbCr}, + {IMAGING_MODE_RGBA, IMAGING_MODE_HSV, rgb2hsv}, + + {IMAGING_MODE_RGBa, IMAGING_MODE_RGBA, rgba2rgbA}, + {IMAGING_MODE_RGBa, IMAGING_MODE_RGB, rgba2rgb_}, + + {IMAGING_MODE_RGBX, IMAGING_MODE_1, rgb2bit}, + {IMAGING_MODE_RGBX, IMAGING_MODE_L, rgb2l}, + {IMAGING_MODE_RGBX, IMAGING_MODE_LA, rgb2la}, + {IMAGING_MODE_RGBX, IMAGING_MODE_I, rgb2i}, + {IMAGING_MODE_RGBX, IMAGING_MODE_F, rgb2f}, + {IMAGING_MODE_RGBX, IMAGING_MODE_RGB, rgba2rgb}, + {IMAGING_MODE_RGBX, IMAGING_MODE_CMYK, rgb2cmyk}, + {IMAGING_MODE_RGBX, IMAGING_MODE_YCbCr, ImagingConvertRGB2YCbCr}, + {IMAGING_MODE_RGBX, IMAGING_MODE_HSV, rgb2hsv}, + + {IMAGING_MODE_CMYK, IMAGING_MODE_RGB, cmyk2rgb}, + {IMAGING_MODE_CMYK, IMAGING_MODE_RGBA, cmyk2rgb}, + {IMAGING_MODE_CMYK, IMAGING_MODE_RGBX, cmyk2rgb}, + {IMAGING_MODE_CMYK, IMAGING_MODE_HSV, cmyk2hsv}, + + {IMAGING_MODE_YCbCr, IMAGING_MODE_L, ycbcr2l}, + {IMAGING_MODE_YCbCr, IMAGING_MODE_LA, ycbcr2la}, + {IMAGING_MODE_YCbCr, IMAGING_MODE_RGB, ImagingConvertYCbCr2RGB}, + + {IMAGING_MODE_HSV, IMAGING_MODE_RGB, hsv2rgb}, + + {IMAGING_MODE_I, IMAGING_MODE_I_16, I_I16L}, + {IMAGING_MODE_I_16, IMAGING_MODE_I, I16L_I}, + {IMAGING_MODE_I_16, IMAGING_MODE_RGB, I16_RGB}, + {IMAGING_MODE_L, IMAGING_MODE_I_16, L_I16L}, + {IMAGING_MODE_I_16, IMAGING_MODE_L, I16L_L}, + + {IMAGING_MODE_I, IMAGING_MODE_I_16L, I_I16L}, + {IMAGING_MODE_I_16L, IMAGING_MODE_I, I16L_I}, + {IMAGING_MODE_I, IMAGING_MODE_I_16B, I_I16B}, + {IMAGING_MODE_I_16B, IMAGING_MODE_I, I16B_I}, + + {IMAGING_MODE_L, IMAGING_MODE_I_16L, L_I16L}, + {IMAGING_MODE_I_16L, IMAGING_MODE_L, I16L_L}, + {IMAGING_MODE_L, IMAGING_MODE_I_16B, L_I16B}, + {IMAGING_MODE_I_16B, IMAGING_MODE_L, I16B_L}, +#ifdef WORDS_BIGENDIAN + {IMAGING_MODE_L, IMAGING_MODE_I_16N, L_I16B}, + {IMAGING_MODE_I_16N, IMAGING_MODE_L, I16B_L}, +#else + {IMAGING_MODE_L, IMAGING_MODE_I_16N, L_I16L}, + {IMAGING_MODE_I_16N, IMAGING_MODE_L, I16L_L}, +#endif + + {IMAGING_MODE_I_16, IMAGING_MODE_F, I16L_F}, + {IMAGING_MODE_I_16L, IMAGING_MODE_F, I16L_F}, + {IMAGING_MODE_I_16B, IMAGING_MODE_F, I16B_F} +}; static Imaging -convert( - Imaging imOut, Imaging imIn, const Mode *mode, ImagingPalette palette, int dither -) { +convert(Imaging imOut, Imaging imIn, ModeID mode, ImagingPalette palette, int dither) { ImagingSectionCookie cookie; ImagingShuffler convert; - int y; if (!imIn) { return (Imaging)ImagingError_ModeError(); } - if (!mode) { + if (mode == IMAGING_MODE_UNKNOWN) { /* Map palette image to full depth */ if (!imIn->palette) { return (Imaging)ImagingError_ModeError(); @@ -1504,10 +1630,9 @@ convert( /* standard conversion machinery */ convert = NULL; - - for (y = 0; converters[y].from; y++) { - if (imIn->mode == converters[y].from && mode == converters[y].to) { - convert = converters[y].convert; + for (size_t i = 0; i < sizeof(converters) / sizeof(*converters); i++) { + if (imIn->mode == converters[i].from && mode == converters[i].to) { + convert = converters[i].convert; break; } } @@ -1518,7 +1643,11 @@ convert( #else static char buf[100]; snprintf( - buf, 100, "conversion from %.10s to %.10s not supported", imIn->mode->name, mode->name + buf, + 100, + "conversion from %.10s to %.10s not supported", + getModeData(imIn->mode)->name, + getModeData(mode)->name ); return (Imaging)ImagingError_ValueError(buf); #endif @@ -1530,7 +1659,7 @@ convert( } ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { + for (int y = 0; y < imIn->ysize; y++) { (*convert)((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); } ImagingSectionLeave(&cookie); @@ -1539,7 +1668,7 @@ convert( } Imaging -ImagingConvert(Imaging imIn, const Mode *mode, ImagingPalette palette, int dither) { +ImagingConvert(Imaging imIn, const ModeID mode, ImagingPalette palette, int dither) { return convert(NULL, imIn, mode, palette, dither); } @@ -1549,7 +1678,7 @@ ImagingConvert2(Imaging imOut, Imaging imIn) { } Imaging -ImagingConvertTransparent(Imaging imIn, const Mode *mode, int r, int g, int b) { +ImagingConvertTransparent(Imaging imIn, const ModeID mode, int r, int g, int b) { ImagingSectionCookie cookie; ImagingShuffler convert; Imaging imOut = NULL; @@ -1598,8 +1727,8 @@ ImagingConvertTransparent(Imaging imIn, const Mode *mode, int r, int g, int b) { buf, 100, "conversion from %.10s to %.10s not supported in convert_transparent", - imIn->mode->name, - mode->name + getModeData(imIn->mode)->name, + getModeData(mode)->name ); return (Imaging)ImagingError_ValueError(buf); } @@ -1622,7 +1751,7 @@ ImagingConvertTransparent(Imaging imIn, const Mode *mode, int r, int g, int b) { } Imaging -ImagingConvertInPlace(Imaging imIn, const Mode *mode) { +ImagingConvertInPlace(Imaging imIn, const ModeID mode) { ImagingSectionCookie cookie; ImagingShuffler convert; int y; @@ -1644,155 +1773,3 @@ ImagingConvertInPlace(Imaging imIn, const Mode *mode) { return imIn; } - -/* ------------------ */ -/* Converter mappings */ -/* ------------------ */ - -void -ImagingConvertInit(void) { - const struct Converter temp[] = { - {IMAGING_MODE_1, IMAGING_MODE_L, bit2l}, - {IMAGING_MODE_1, IMAGING_MODE_I, bit2i}, - {IMAGING_MODE_1, IMAGING_MODE_F, bit2f}, - {IMAGING_MODE_1, IMAGING_MODE_RGB, bit2rgb}, - {IMAGING_MODE_1, IMAGING_MODE_RGBA, bit2rgb}, - {IMAGING_MODE_1, IMAGING_MODE_RGBX, bit2rgb}, - {IMAGING_MODE_1, IMAGING_MODE_CMYK, bit2cmyk}, - {IMAGING_MODE_1, IMAGING_MODE_YCbCr, bit2ycbcr}, - {IMAGING_MODE_1, IMAGING_MODE_HSV, bit2hsv}, - - {IMAGING_MODE_L, IMAGING_MODE_1, l2bit}, - {IMAGING_MODE_L, IMAGING_MODE_LA, l2la}, - {IMAGING_MODE_L, IMAGING_MODE_I, l2i}, - {IMAGING_MODE_L, IMAGING_MODE_F, l2f}, - {IMAGING_MODE_L, IMAGING_MODE_RGB, l2rgb}, - {IMAGING_MODE_L, IMAGING_MODE_RGBA, l2rgb}, - {IMAGING_MODE_L, IMAGING_MODE_RGBX, l2rgb}, - {IMAGING_MODE_L, IMAGING_MODE_CMYK, l2cmyk}, - {IMAGING_MODE_L, IMAGING_MODE_YCbCr, l2ycbcr}, - {IMAGING_MODE_L, IMAGING_MODE_HSV, l2hsv}, - - {IMAGING_MODE_LA, IMAGING_MODE_L, la2l}, - {IMAGING_MODE_LA, IMAGING_MODE_La, lA2la}, - {IMAGING_MODE_LA, IMAGING_MODE_RGB, la2rgb}, - {IMAGING_MODE_LA, IMAGING_MODE_RGBA, la2rgb}, - {IMAGING_MODE_LA, IMAGING_MODE_RGBX, la2rgb}, - {IMAGING_MODE_LA, IMAGING_MODE_CMYK, la2cmyk}, - {IMAGING_MODE_LA, IMAGING_MODE_YCbCr, la2ycbcr}, - {IMAGING_MODE_LA, IMAGING_MODE_HSV, la2hsv}, - - {IMAGING_MODE_La, IMAGING_MODE_LA, la2lA}, - - {IMAGING_MODE_I, IMAGING_MODE_L, i2l}, - {IMAGING_MODE_I, IMAGING_MODE_F, i2f}, - {IMAGING_MODE_I, IMAGING_MODE_RGB, i2rgb}, - {IMAGING_MODE_I, IMAGING_MODE_RGBA, i2rgb}, - {IMAGING_MODE_I, IMAGING_MODE_RGBX, i2rgb}, - {IMAGING_MODE_I, IMAGING_MODE_HSV, i2hsv}, - - {IMAGING_MODE_F, IMAGING_MODE_L, f2l}, - {IMAGING_MODE_F, IMAGING_MODE_I, f2i}, - - {IMAGING_MODE_RGB, IMAGING_MODE_1, rgb2bit}, - {IMAGING_MODE_RGB, IMAGING_MODE_L, rgb2l}, - {IMAGING_MODE_RGB, IMAGING_MODE_LA, rgb2la}, - {IMAGING_MODE_RGB, IMAGING_MODE_La, rgb2la}, - {IMAGING_MODE_RGB, IMAGING_MODE_I, rgb2i}, - {IMAGING_MODE_RGB, IMAGING_MODE_I_16, rgb2i16l}, - {IMAGING_MODE_RGB, IMAGING_MODE_I_16L, rgb2i16l}, - {IMAGING_MODE_RGB, IMAGING_MODE_I_16B, rgb2i16b}, -#ifdef WORDS_BIGENDIAN - {IMAGING_MODE_RGB, IMAGING_MODE_I_16N, rgb2i16b}, -#else - {IMAGING_MODE_RGB, IMAGING_MODE_I_16N, rgb2i16l}, -#endif - {IMAGING_MODE_RGB, IMAGING_MODE_F, rgb2f}, - {IMAGING_MODE_RGB, IMAGING_MODE_BGR_15, rgb2bgr15}, - {IMAGING_MODE_RGB, IMAGING_MODE_BGR_16, rgb2bgr16}, - {IMAGING_MODE_RGB, IMAGING_MODE_BGR_24, rgb2bgr24}, - {IMAGING_MODE_RGB, IMAGING_MODE_RGBA, rgb2rgba}, - {IMAGING_MODE_RGB, IMAGING_MODE_RGBa, rgb2rgba}, - {IMAGING_MODE_RGB, IMAGING_MODE_RGBX, rgb2rgba}, - {IMAGING_MODE_RGB, IMAGING_MODE_CMYK, rgb2cmyk}, - {IMAGING_MODE_RGB, IMAGING_MODE_YCbCr, ImagingConvertRGB2YCbCr}, - {IMAGING_MODE_RGB, IMAGING_MODE_HSV, rgb2hsv}, - - {IMAGING_MODE_RGBA, IMAGING_MODE_1, rgb2bit}, - {IMAGING_MODE_RGBA, IMAGING_MODE_L, rgb2l}, - {IMAGING_MODE_RGBA, IMAGING_MODE_LA, rgba2la}, - {IMAGING_MODE_RGBA, IMAGING_MODE_I, rgb2i}, - {IMAGING_MODE_RGBA, IMAGING_MODE_F, rgb2f}, - {IMAGING_MODE_RGBA, IMAGING_MODE_RGB, rgba2rgb}, - {IMAGING_MODE_RGBA, IMAGING_MODE_RGBa, rgbA2rgba}, - {IMAGING_MODE_RGBA, IMAGING_MODE_RGBX, rgb2rgba}, - {IMAGING_MODE_RGBA, IMAGING_MODE_CMYK, rgb2cmyk}, - {IMAGING_MODE_RGBA, IMAGING_MODE_YCbCr, ImagingConvertRGB2YCbCr}, - {IMAGING_MODE_RGBA, IMAGING_MODE_HSV, rgb2hsv}, - - {IMAGING_MODE_RGBa, IMAGING_MODE_RGBA, rgba2rgbA}, - {IMAGING_MODE_RGBa, IMAGING_MODE_RGB, rgba2rgb_}, - - {IMAGING_MODE_RGBX, IMAGING_MODE_1, rgb2bit}, - {IMAGING_MODE_RGBX, IMAGING_MODE_L, rgb2l}, - {IMAGING_MODE_RGBX, IMAGING_MODE_LA, rgb2la}, - {IMAGING_MODE_RGBX, IMAGING_MODE_I, rgb2i}, - {IMAGING_MODE_RGBX, IMAGING_MODE_F, rgb2f}, - {IMAGING_MODE_RGBX, IMAGING_MODE_RGB, rgba2rgb}, - {IMAGING_MODE_RGBX, IMAGING_MODE_CMYK, rgb2cmyk}, - {IMAGING_MODE_RGBX, IMAGING_MODE_YCbCr, ImagingConvertRGB2YCbCr}, - {IMAGING_MODE_RGBX, IMAGING_MODE_HSV, rgb2hsv}, - - {IMAGING_MODE_CMYK, IMAGING_MODE_RGB, cmyk2rgb}, - {IMAGING_MODE_CMYK, IMAGING_MODE_RGBA, cmyk2rgb}, - {IMAGING_MODE_CMYK, IMAGING_MODE_RGBX, cmyk2rgb}, - {IMAGING_MODE_CMYK, IMAGING_MODE_HSV, cmyk2hsv}, - - {IMAGING_MODE_YCbCr, IMAGING_MODE_L, ycbcr2l}, - {IMAGING_MODE_YCbCr, IMAGING_MODE_LA, ycbcr2la}, - {IMAGING_MODE_YCbCr, IMAGING_MODE_RGB, ImagingConvertYCbCr2RGB}, - - {IMAGING_MODE_HSV, IMAGING_MODE_RGB, hsv2rgb}, - - {IMAGING_MODE_I, IMAGING_MODE_I_16, I_I16L}, - {IMAGING_MODE_I_16, IMAGING_MODE_I, I16L_I}, - {IMAGING_MODE_I_16, IMAGING_MODE_RGB, I16_RGB}, - {IMAGING_MODE_L, IMAGING_MODE_I_16, L_I16L}, - {IMAGING_MODE_I_16, IMAGING_MODE_L, I16L_L}, - - {IMAGING_MODE_I, IMAGING_MODE_I_16L, I_I16L}, - {IMAGING_MODE_I_16L, IMAGING_MODE_I, I16L_I}, - {IMAGING_MODE_I, IMAGING_MODE_I_16B, I_I16B}, - {IMAGING_MODE_I_16B, IMAGING_MODE_I, I16B_I}, - - {IMAGING_MODE_L, IMAGING_MODE_I_16L, L_I16L}, - {IMAGING_MODE_I_16L, IMAGING_MODE_L, I16L_L}, - {IMAGING_MODE_L, IMAGING_MODE_I_16B, L_I16B}, - {IMAGING_MODE_I_16B, IMAGING_MODE_L, I16B_L}, -#ifdef WORDS_BIGENDIAN - {IMAGING_MODE_L, IMAGING_MODE_I_16N, L_I16B}, - {IMAGING_MODE_I_16N, IMAGING_MODE_L, I16B_L}, -#else - {IMAGING_MODE_L, IMAGING_MODE_I_16N, L_I16L}, - {IMAGING_MODE_I_16N, IMAGING_MODE_L, I16L_L}, -#endif - - {IMAGING_MODE_I_16, IMAGING_MODE_F, I16L_F}, - {IMAGING_MODE_I_16L, IMAGING_MODE_F, I16L_F}, - {IMAGING_MODE_I_16B, IMAGING_MODE_F, I16B_F}, - - {NULL} - }; - converters = malloc(sizeof(temp)); - if (converters == NULL) { - fprintf(stderr, "ConvertInit: failed to allocate memory for converter table\n"); - exit(1); - } - memcpy(converters, temp, sizeof(temp)); -} - -void -ImagingConvertFree(void) { - free(converters); - converters = NULL; -} diff --git a/src/libImaging/Dib.c b/src/libImaging/Dib.c index 154c610ec22..2afe71d4ac9 100644 --- a/src/libImaging/Dib.c +++ b/src/libImaging/Dib.c @@ -25,13 +25,13 @@ #include "ImDib.h" -const Mode * +ModeID ImagingGetModeDIB(int size_out[2]) { /* Get device characteristics */ const HDC dc = CreateCompatibleDC(NULL); - const Mode *mode = IMAGING_MODE_P; + ModeID mode = IMAGING_MODE_P; if (!(GetDeviceCaps(dc, RASTERCAPS) & RC_PALETTE)) { mode = IMAGING_MODE_RGB; if (GetDeviceCaps(dc, BITSPIXEL) == 1) { @@ -50,7 +50,7 @@ ImagingGetModeDIB(int size_out[2]) { } ImagingDIB -ImagingNewDIB(const Mode * const mode, int xsize, int ysize) { +ImagingNewDIB(const ModeID mode, int xsize, int ysize) { /* Create a Windows bitmap */ ImagingDIB dib; diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index 854cdb9fe93..0224d1ba95e 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -68,7 +68,7 @@ ImagingFill(Imaging im, const void *colour) { } Imaging -ImagingFillLinearGradient(const Mode *mode) { +ImagingFillLinearGradient(const ModeID mode) { Imaging im; int y; @@ -105,7 +105,7 @@ ImagingFillLinearGradient(const Mode *mode) { } Imaging -ImagingFillRadialGradient(const Mode *mode) { +ImagingFillRadialGradient(const ModeID mode) { Imaging im; int x, y; int d; diff --git a/src/libImaging/ImDib.h b/src/libImaging/ImDib.h index 6d8f420cb5c..65f090f928a 100644 --- a/src/libImaging/ImDib.h +++ b/src/libImaging/ImDib.h @@ -27,7 +27,7 @@ struct ImagingDIBInstance { UINT8 *bits; HPALETTE palette; /* Used by cut and paste */ - const Mode *mode; + ModeID mode; int xsize, ysize; int pixelsize; int linesize; @@ -37,11 +37,11 @@ struct ImagingDIBInstance { typedef struct ImagingDIBInstance *ImagingDIB; -extern const Mode * +extern ModeID ImagingGetModeDIB(int size_out[2]); extern ImagingDIB -ImagingNewDIB(const Mode * const mode, int xsize, int ysize); +ImagingNewDIB(ModeID mode, int xsize, int ysize); extern void ImagingDeleteDIB(ImagingDIB im); diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 9f450dd3ad2..290a76c8e7d 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -79,11 +79,11 @@ typedef struct { struct ImagingMemoryInstance { /* Format */ - const Mode *mode; /* Image mode (IMAGING_MODE_*) */ - int type; /* Data type (IMAGING_TYPE_*) */ - int depth; /* Depth (ignored in this version) */ - int bands; /* Number of bands (1, 2, 3, or 4) */ - int xsize; /* Image dimension. */ + ModeID mode; /* Image mode (IMAGING_MODE_*) */ + int type; /* Data type (IMAGING_TYPE_*) */ + int depth; /* Depth (ignored in this version) */ + int bands; /* Number of bands (1, 2, 3, or 4) */ + int xsize; /* Image dimension. */ int ysize; /* Colour palette (for "P" images only) */ @@ -137,15 +137,15 @@ struct ImagingMemoryInstance { #define IMAGING_PIXEL_FLOAT32(im, x, y) (((FLOAT32 *)(im)->image32[y])[x]) struct ImagingAccessInstance { - const Mode *mode; + ModeID mode; void (*get_pixel)(Imaging im, int x, int y, void *pixel); void (*put_pixel)(Imaging im, int x, int y, const void *pixel); }; struct ImagingHistogramInstance { /* Format */ - const Mode *mode; /* Mode of corresponding source image */ - int bands; /* Number of bands (1, 3, or 4) */ + ModeID mode; /* Mode ID of corresponding source image */ + int bands; /* Number of bands (1, 3, or 4) */ /* Data */ long *histogram; /* Histogram (bands*256 longs) */ @@ -153,7 +153,7 @@ struct ImagingHistogramInstance { struct ImagingPaletteInstance { /* Format */ - const Mode *mode; + ModeID mode; /* Data */ int size; @@ -181,29 +181,6 @@ typedef struct ImagingMemoryArena { #endif } *ImagingMemoryArena; -/* Memory Management */ -/* ----------------- */ - -extern void -ImagingAccessInit(void); -extern void -ImagingAccessFree(void); - -extern void -ImagingConvertInit(void); -extern void -ImagingConvertFree(void); - -extern void -ImagingPackInit(void); -extern void -ImagingPackFree(void); - -extern void -ImagingUnpackInit(void); -extern void -ImagingUnpackFree(void); - /* Objects */ /* ------- */ @@ -216,20 +193,20 @@ extern void ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator); extern Imaging -ImagingNew(const Mode *mode, int xsize, int ysize); +ImagingNew(ModeID mode, int xsize, int ysize); extern Imaging -ImagingNewDirty(const Mode *mode, int xsize, int ysize); +ImagingNewDirty(ModeID mode, int xsize, int ysize); extern Imaging -ImagingNew2Dirty(const Mode *mode, Imaging imOut, Imaging imIn); +ImagingNew2Dirty(ModeID mode, Imaging imOut, Imaging imIn); extern void ImagingDelete(Imaging im); extern Imaging -ImagingNewBlock(const Mode *mode, int xsize, int ysize); +ImagingNewBlock(ModeID mode, int xsize, int ysize); extern Imaging ImagingNewArrow( - const char *mode, + const ModeID mode, int xsize, int ysize, PyObject *schema_capsule, @@ -237,9 +214,9 @@ ImagingNewArrow( ); extern Imaging -ImagingNewPrologue(const Mode *mode, int xsize, int ysize); +ImagingNewPrologue(ModeID mode, int xsize, int ysize); extern Imaging -ImagingNewPrologueSubtype(const Mode *mode, int xsize, int ysize, int structure_size); +ImagingNewPrologueSubtype(ModeID mode, int xsize, int ysize, int structure_size); extern void ImagingCopyPalette(Imaging destination, Imaging source); @@ -254,7 +231,7 @@ _ImagingAccessDelete(Imaging im, ImagingAccess access); #define ImagingAccessDelete(im, access) /* nop, for now */ extern ImagingPalette -ImagingPaletteNew(const Mode *mode); +ImagingPaletteNew(ModeID mode); extern ImagingPalette ImagingPaletteNewBrowser(void); extern ImagingPalette @@ -326,13 +303,13 @@ ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha); extern Imaging ImagingCopy(Imaging im); extern Imaging -ImagingConvert(Imaging im, const Mode *mode, ImagingPalette palette, int dither); +ImagingConvert(Imaging im, ModeID mode, ImagingPalette palette, int dither); extern Imaging -ImagingConvertInPlace(Imaging im, const Mode *mode); +ImagingConvertInPlace(Imaging im, ModeID mode); extern Imaging -ImagingConvertMatrix(Imaging im, const Mode *mode, float m[]); +ImagingConvertMatrix(Imaging im, ModeID mode, float m[]); extern Imaging -ImagingConvertTransparent(Imaging im, const Mode *mode, int r, int g, int b); +ImagingConvertTransparent(Imaging im, ModeID mode, int r, int g, int b); extern Imaging ImagingCrop(Imaging im, int x0, int y0, int x1, int y1); extern Imaging @@ -346,9 +323,9 @@ ImagingFill2( extern Imaging ImagingFillBand(Imaging im, int band, int color); extern Imaging -ImagingFillLinearGradient(const Mode *mode); +ImagingFillLinearGradient(ModeID mode); extern Imaging -ImagingFillRadialGradient(const Mode *mode); +ImagingFillRadialGradient(ModeID mode); extern Imaging ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 offset); extern Imaging @@ -362,7 +339,7 @@ ImagingGaussianBlur( extern Imaging ImagingGetBand(Imaging im, int band); extern Imaging -ImagingMerge(const Mode *mode, Imaging bands[4]); +ImagingMerge(ModeID mode, Imaging bands[4]); extern int ImagingSplit(Imaging im, Imaging bands[4]); extern int @@ -389,7 +366,7 @@ ImagingOffset(Imaging im, int xoffset, int yoffset); extern int ImagingPaste(Imaging into, Imaging im, Imaging mask, int x0, int y0, int x1, int y1); extern Imaging -ImagingPoint(Imaging im, const Mode *tablemode, const void *table); +ImagingPoint(Imaging im, ModeID tablemode, const void *table); extern Imaging ImagingPointTransform(Imaging imIn, double scale, double offset); extern Imaging @@ -730,9 +707,9 @@ extern void ImagingConvertYCbCr2RGB(UINT8 *out, const UINT8 *in, int pixels); extern ImagingShuffler -ImagingFindUnpacker(const Mode *mode, const RawMode *rawmode, int *bits_out); +ImagingFindUnpacker(ModeID mode, RawModeID rawmode, int *bits_out); extern ImagingShuffler -ImagingFindPacker(const Mode *mode, const RawMode *rawmode, int *bits_out); +ImagingFindPacker(ModeID mode, RawModeID rawmode, int *bits_out); struct ImagingCodecStateInstance { int count; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 48c6c618411..e07904fc70c 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -28,12 +28,12 @@ typedef struct { typedef struct { /* CONFIGURATION */ - /* Jpeg file mode (NULL if not known) */ - const RawMode *jpegmode; + /* Jpeg file mode */ + RawModeID jpegmode; /* Converter output mode (input to the shuffler) */ - /* If NULL, convert conversions are disabled */ - const RawMode *rawmode; + /* If not a valid mode, convert conversions are disabled */ + RawModeID rawmode; /* If set, trade quality for speed */ int draft; @@ -91,7 +91,7 @@ typedef struct { unsigned int restart_marker_rows; /* Converter input mode (input to the shuffler) */ - const RawMode *rawmode; + RawModeID rawmode; /* Custom quantization tables () */ unsigned int *qtables; diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index 3cbe2965df9..67f705ddd59 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -771,7 +771,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { if (color_space == j2k_unpackers[n].color_space && image->numcomps == j2k_unpackers[n].components && (j2k_unpackers[n].subsampling || (subsampling == -1)) && - strcmp(im->mode->name, j2k_unpackers[n].mode) == 0) { + strcmp(getModeData(im->mode)->name, j2k_unpackers[n].mode) == 0) { unpack = j2k_unpackers[n].unpacker; break; } diff --git a/src/libImaging/JpegDecode.c b/src/libImaging/JpegDecode.c index 49d4fcb2f8a..ae3274456d3 100644 --- a/src/libImaging/JpegDecode.c +++ b/src/libImaging/JpegDecode.c @@ -180,8 +180,8 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by /* Decoder settings */ - /* jpegmode indicates what's in the file; if not set, we'll - trust the decoder */ + /* jpegmode indicates what's in the file. */ + /* If not valid, we'll trust the decoder. */ if (context->jpegmode == IMAGING_RAWMODE_L) { context->cinfo.jpeg_color_space = JCS_GRAYSCALE; } else if (context->jpegmode == IMAGING_RAWMODE_RGB) { @@ -194,8 +194,8 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by context->cinfo.jpeg_color_space = JCS_YCCK; } - /* rawmode indicates what we want from the decoder. if not - set, conversions are disabled */ + /* rawmode indicates what we want from the decoder. */ + /* If not valid, conversions are disabled. */ if (context->rawmode == IMAGING_RAWMODE_L) { context->cinfo.out_color_space = JCS_GRAYSCALE; } else if (context->rawmode == IMAGING_RAWMODE_RGB) { @@ -214,7 +214,7 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by } else if (context->rawmode == IMAGING_RAWMODE_YCbCrK) { context->cinfo.out_color_space = JCS_YCCK; } else { - /* Disable decoder conversions */ + /* Disable decoder conversions. */ context->cinfo.jpeg_color_space = JCS_UNKNOWN; context->cinfo.out_color_space = JCS_UNKNOWN; } diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index f848b870d05..6bc9fbc1de2 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -18,7 +18,7 @@ #define CLIPF(v) ((v <= 0.0) ? 0 : (v >= 255.0F) ? 255 : (UINT8)v) Imaging -ImagingConvertMatrix(Imaging im, const Mode *mode, float m[]) { +ImagingConvertMatrix(Imaging im, const ModeID mode, float m[]) { Imaging imOut; int x, y; ImagingSectionCookie cookie; diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c index 01b2b55015a..659e7aada49 100644 --- a/src/libImaging/Mode.c +++ b/src/libImaging/Mode.c @@ -2,453 +2,258 @@ #include -#define CREATE_MODE(TYPE, NAME, INIT) \ -const TYPE IMAGING_##NAME##_VAL = INIT;\ -const TYPE * const IMAGING_##NAME = &IMAGING_##NAME##_VAL; +const ModeData MODES[] = { + [IMAGING_MODE_UNKNOWN] = {""}, + [IMAGING_MODE_1] = {"1"}, + [IMAGING_MODE_CMYK] = {"CMYK"}, + [IMAGING_MODE_F] = {"F"}, + [IMAGING_MODE_HSV] = {"HSV"}, + [IMAGING_MODE_I] = {"I"}, + [IMAGING_MODE_L] = {"L"}, + [IMAGING_MODE_LA] = {"LA"}, + [IMAGING_MODE_LAB] = {"LAB"}, + [IMAGING_MODE_La] = {"La"}, + [IMAGING_MODE_P] = {"P"}, + [IMAGING_MODE_PA] = {"PA"}, + [IMAGING_MODE_RGB] = {"RGB"}, + [IMAGING_MODE_RGBA] = {"RGBA"}, + [IMAGING_MODE_RGBX] = {"RGBX"}, + [IMAGING_MODE_RGBa] = {"RGBa"}, + [IMAGING_MODE_YCbCr] = {"YCbCr"}, -CREATE_MODE(Mode, MODE_1, {"1"}) -CREATE_MODE(Mode, MODE_CMYK, {"CMYK"}) -CREATE_MODE(Mode, MODE_F, {"F"}) -CREATE_MODE(Mode, MODE_HSV, {"HSV"}) -CREATE_MODE(Mode, MODE_I, {"I"}) -CREATE_MODE(Mode, MODE_L, {"L"}) -CREATE_MODE(Mode, MODE_LA, {"LA"}) -CREATE_MODE(Mode, MODE_LAB, {"LAB"}) -CREATE_MODE(Mode, MODE_La, {"La"}) -CREATE_MODE(Mode, MODE_P, {"P"}) -CREATE_MODE(Mode, MODE_PA, {"PA"}) -CREATE_MODE(Mode, MODE_RGB, {"RGB"}) -CREATE_MODE(Mode, MODE_RGBA, {"RGBA"}) -CREATE_MODE(Mode, MODE_RGBX, {"RGBX"}) -CREATE_MODE(Mode, MODE_RGBa, {"RGBa"}) -CREATE_MODE(Mode, MODE_YCbCr, {"YCbCr"}) + [IMAGING_MODE_BGR_15] = {"BGR;15"}, + [IMAGING_MODE_BGR_16] = {"BGR;16"}, + [IMAGING_MODE_BGR_24] = {"BGR;24"}, -CREATE_MODE(Mode, MODE_BGR_15, {"BGR;15"}) -CREATE_MODE(Mode, MODE_BGR_16, {"BGR;16"}) -CREATE_MODE(Mode, MODE_BGR_24, {"BGR;24"}) - -CREATE_MODE(Mode, MODE_I_16, {"I;16"}) -CREATE_MODE(Mode, MODE_I_16L, {"I;16L"}) -CREATE_MODE(Mode, MODE_I_16B, {"I;16B"}) -CREATE_MODE(Mode, MODE_I_16N, {"I;16N"}) -CREATE_MODE(Mode, MODE_I_32L, {"I;32L"}) -CREATE_MODE(Mode, MODE_I_32B, {"I;32B"}) - -const Mode * const MODES[] = { - IMAGING_MODE_1, - IMAGING_MODE_CMYK, - IMAGING_MODE_F, - IMAGING_MODE_HSV, - IMAGING_MODE_I, - IMAGING_MODE_L, - IMAGING_MODE_LA, - IMAGING_MODE_LAB, - IMAGING_MODE_La, - IMAGING_MODE_P, - IMAGING_MODE_PA, - IMAGING_MODE_RGB, - IMAGING_MODE_RGBA, - IMAGING_MODE_RGBX, - IMAGING_MODE_RGBa, - IMAGING_MODE_YCbCr, - - IMAGING_MODE_BGR_15, - IMAGING_MODE_BGR_16, - IMAGING_MODE_BGR_24, - - IMAGING_MODE_I_16, - IMAGING_MODE_I_16L, - IMAGING_MODE_I_16B, - IMAGING_MODE_I_16N, - IMAGING_MODE_I_32L, - IMAGING_MODE_I_32B, - - NULL + [IMAGING_MODE_I_16] = {"I;16"}, + [IMAGING_MODE_I_16L] = {"I;16L"}, + [IMAGING_MODE_I_16B] = {"I;16B"}, + [IMAGING_MODE_I_16N] = {"I;16N"}, + [IMAGING_MODE_I_32L] = {"I;32L"}, + [IMAGING_MODE_I_32B] = {"I;32B"}, }; -const Mode * findMode(const char * const name) { +const ModeID findModeID(const char * const name) { if (name == NULL) { - return NULL; + return IMAGING_MODE_UNKNOWN; } - const Mode * mode; - for (int i = 0; (mode = MODES[i]); i++) { - if (strcmp(mode->name, name) == 0) { - return mode; + for (size_t i = 0; i < sizeof(MODES) / sizeof(*MODES); i++) { + if (strcmp(MODES[i].name, name) == 0) { + return (ModeID)i; } } - return NULL; + return IMAGING_MODE_UNKNOWN; } +const ModeData * const getModeData(const ModeID id) { + if (id < 0 || id > sizeof(MODES) / sizeof(*MODES)) { + return &MODES[IMAGING_MODE_UNKNOWN]; + } + return &MODES[id]; +} -// Alias all of the modes as rawmodes so that the addresses are the same. -#define ALIAS_MODE_AS_RAWMODE(NAME) const RawMode * const IMAGING_RAWMODE_##NAME = (const RawMode * const)IMAGING_MODE_##NAME; -ALIAS_MODE_AS_RAWMODE(1) -ALIAS_MODE_AS_RAWMODE(CMYK) -ALIAS_MODE_AS_RAWMODE(F) -ALIAS_MODE_AS_RAWMODE(HSV) -ALIAS_MODE_AS_RAWMODE(I) -ALIAS_MODE_AS_RAWMODE(L) -ALIAS_MODE_AS_RAWMODE(LA) -ALIAS_MODE_AS_RAWMODE(LAB) -ALIAS_MODE_AS_RAWMODE(La) -ALIAS_MODE_AS_RAWMODE(P) -ALIAS_MODE_AS_RAWMODE(PA) -ALIAS_MODE_AS_RAWMODE(RGB) -ALIAS_MODE_AS_RAWMODE(RGBA) -ALIAS_MODE_AS_RAWMODE(RGBX) -ALIAS_MODE_AS_RAWMODE(RGBa) -ALIAS_MODE_AS_RAWMODE(YCbCr) - -ALIAS_MODE_AS_RAWMODE(BGR_15) -ALIAS_MODE_AS_RAWMODE(BGR_16) -ALIAS_MODE_AS_RAWMODE(BGR_24) - -ALIAS_MODE_AS_RAWMODE(I_16) -ALIAS_MODE_AS_RAWMODE(I_16L) -ALIAS_MODE_AS_RAWMODE(I_16B) -ALIAS_MODE_AS_RAWMODE(I_16N) -ALIAS_MODE_AS_RAWMODE(I_32L) -ALIAS_MODE_AS_RAWMODE(I_32B) - -CREATE_MODE(RawMode, RAWMODE_1_8, {"1;8"}) -CREATE_MODE(RawMode, RAWMODE_1_I, {"1;I"}) -CREATE_MODE(RawMode, RAWMODE_1_IR, {"1;IR"}) -CREATE_MODE(RawMode, RAWMODE_1_R, {"1;R"}) -CREATE_MODE(RawMode, RAWMODE_A, {"A"}) -CREATE_MODE(RawMode, RAWMODE_ABGR, {"ABGR"}) -CREATE_MODE(RawMode, RAWMODE_ARGB, {"ARGB"}) -CREATE_MODE(RawMode, RAWMODE_A_16B, {"A;16B"}) -CREATE_MODE(RawMode, RAWMODE_A_16L, {"A;16L"}) -CREATE_MODE(RawMode, RAWMODE_A_16N, {"A;16N"}) -CREATE_MODE(RawMode, RAWMODE_B, {"B"}) -CREATE_MODE(RawMode, RAWMODE_BGAR, {"BGAR"}) -CREATE_MODE(RawMode, RAWMODE_BGR, {"BGR"}) -CREATE_MODE(RawMode, RAWMODE_BGRA, {"BGRA"}) -CREATE_MODE(RawMode, RAWMODE_BGRA_15, {"BGRA;15"}) -CREATE_MODE(RawMode, RAWMODE_BGRA_15Z, {"BGRA;15Z"}) -CREATE_MODE(RawMode, RAWMODE_BGRA_16B, {"BGRA;16B"}) -CREATE_MODE(RawMode, RAWMODE_BGRA_16L, {"BGRA;16L"}) -CREATE_MODE(RawMode, RAWMODE_BGRX, {"BGRX"}) -CREATE_MODE(RawMode, RAWMODE_BGR_5, {"BGR;5"}) -CREATE_MODE(RawMode, RAWMODE_BGRa, {"BGRa"}) -CREATE_MODE(RawMode, RAWMODE_BGXR, {"BGXR"}) -CREATE_MODE(RawMode, RAWMODE_B_16B, {"B;16B"}) -CREATE_MODE(RawMode, RAWMODE_B_16L, {"B;16L"}) -CREATE_MODE(RawMode, RAWMODE_B_16N, {"B;16N"}) -CREATE_MODE(RawMode, RAWMODE_C, {"C"}) -CREATE_MODE(RawMode, RAWMODE_CMYKX, {"CMYKX"}) -CREATE_MODE(RawMode, RAWMODE_CMYKXX, {"CMYKXX"}) -CREATE_MODE(RawMode, RAWMODE_CMYK_16B, {"CMYK;16B"}) -CREATE_MODE(RawMode, RAWMODE_CMYK_16L, {"CMYK;16L"}) -CREATE_MODE(RawMode, RAWMODE_CMYK_16N, {"CMYK;16N"}) -CREATE_MODE(RawMode, RAWMODE_CMYK_I, {"CMYK;I"}) -CREATE_MODE(RawMode, RAWMODE_CMYK_L, {"CMYK;L"}) -CREATE_MODE(RawMode, RAWMODE_C_I, {"C;I"}) -CREATE_MODE(RawMode, RAWMODE_Cb, {"Cb"}) -CREATE_MODE(RawMode, RAWMODE_Cr, {"Cr"}) -CREATE_MODE(RawMode, RAWMODE_F_16, {"F;16"}) -CREATE_MODE(RawMode, RAWMODE_F_16B, {"F;16B"}) -CREATE_MODE(RawMode, RAWMODE_F_16BS, {"F;16BS"}) -CREATE_MODE(RawMode, RAWMODE_F_16N, {"F;16N"}) -CREATE_MODE(RawMode, RAWMODE_F_16NS, {"F;16NS"}) -CREATE_MODE(RawMode, RAWMODE_F_16S, {"F;16S"}) -CREATE_MODE(RawMode, RAWMODE_F_32, {"F;32"}) -CREATE_MODE(RawMode, RAWMODE_F_32B, {"F;32B"}) -CREATE_MODE(RawMode, RAWMODE_F_32BF, {"F;32BF"}) -CREATE_MODE(RawMode, RAWMODE_F_32BS, {"F;32BS"}) -CREATE_MODE(RawMode, RAWMODE_F_32F, {"F;32F"}) -CREATE_MODE(RawMode, RAWMODE_F_32N, {"F;32N"}) -CREATE_MODE(RawMode, RAWMODE_F_32NF, {"F;32NF"}) -CREATE_MODE(RawMode, RAWMODE_F_32NS, {"F;32NS"}) -CREATE_MODE(RawMode, RAWMODE_F_32S, {"F;32S"}) -CREATE_MODE(RawMode, RAWMODE_F_64BF, {"F;64BF"}) -CREATE_MODE(RawMode, RAWMODE_F_64F, {"F;64F"}) -CREATE_MODE(RawMode, RAWMODE_F_64NF, {"F;64NF"}) -CREATE_MODE(RawMode, RAWMODE_F_8, {"F;8"}) -CREATE_MODE(RawMode, RAWMODE_F_8S, {"F;8S"}) -CREATE_MODE(RawMode, RAWMODE_G, {"G"}) -CREATE_MODE(RawMode, RAWMODE_G_16B, {"G;16B"}) -CREATE_MODE(RawMode, RAWMODE_G_16L, {"G;16L"}) -CREATE_MODE(RawMode, RAWMODE_G_16N, {"G;16N"}) -CREATE_MODE(RawMode, RAWMODE_H, {"H"}) -CREATE_MODE(RawMode, RAWMODE_I_12, {"I;12"}) -CREATE_MODE(RawMode, RAWMODE_I_16BS, {"I;16BS"}) -CREATE_MODE(RawMode, RAWMODE_I_16NS, {"I;16NS"}) -CREATE_MODE(RawMode, RAWMODE_I_16R, {"I;16R"}) -CREATE_MODE(RawMode, RAWMODE_I_16S, {"I;16S"}) -CREATE_MODE(RawMode, RAWMODE_I_32, {"I;32"}) -CREATE_MODE(RawMode, RAWMODE_I_32BS, {"I;32BS"}) -CREATE_MODE(RawMode, RAWMODE_I_32N, {"I;32N"}) -CREATE_MODE(RawMode, RAWMODE_I_32NS, {"I;32NS"}) -CREATE_MODE(RawMode, RAWMODE_I_32S, {"I;32S"}) -CREATE_MODE(RawMode, RAWMODE_I_8, {"I;8"}) -CREATE_MODE(RawMode, RAWMODE_I_8S, {"I;8S"}) -CREATE_MODE(RawMode, RAWMODE_K, {"K"}) -CREATE_MODE(RawMode, RAWMODE_K_I, {"K;I"}) -CREATE_MODE(RawMode, RAWMODE_LA_16B, {"LA;16B"}) -CREATE_MODE(RawMode, RAWMODE_LA_L, {"LA;L"}) -CREATE_MODE(RawMode, RAWMODE_L_16, {"L;16"}) -CREATE_MODE(RawMode, RAWMODE_L_16B, {"L;16B"}) -CREATE_MODE(RawMode, RAWMODE_L_2, {"L;2"}) -CREATE_MODE(RawMode, RAWMODE_L_2I, {"L;2I"}) -CREATE_MODE(RawMode, RAWMODE_L_2IR, {"L;2IR"}) -CREATE_MODE(RawMode, RAWMODE_L_2R, {"L;2R"}) -CREATE_MODE(RawMode, RAWMODE_L_4, {"L;4"}) -CREATE_MODE(RawMode, RAWMODE_L_4I, {"L;4I"}) -CREATE_MODE(RawMode, RAWMODE_L_4IR, {"L;4IR"}) -CREATE_MODE(RawMode, RAWMODE_L_4R, {"L;4R"}) -CREATE_MODE(RawMode, RAWMODE_L_I, {"L;I"}) -CREATE_MODE(RawMode, RAWMODE_L_R, {"L;R"}) -CREATE_MODE(RawMode, RAWMODE_M, {"M"}) -CREATE_MODE(RawMode, RAWMODE_M_I, {"M;I"}) -CREATE_MODE(RawMode, RAWMODE_PA_L, {"PA;L"}) -CREATE_MODE(RawMode, RAWMODE_PX, {"PX"}) -CREATE_MODE(RawMode, RAWMODE_P_1, {"P;1"}) -CREATE_MODE(RawMode, RAWMODE_P_2, {"P;2"}) -CREATE_MODE(RawMode, RAWMODE_P_2L, {"P;2L"}) -CREATE_MODE(RawMode, RAWMODE_P_4, {"P;4"}) -CREATE_MODE(RawMode, RAWMODE_P_4L, {"P;4L"}) -CREATE_MODE(RawMode, RAWMODE_P_R, {"P;R"}) -CREATE_MODE(RawMode, RAWMODE_R, {"R"}) -CREATE_MODE(RawMode, RAWMODE_RGBAX, {"RGBAX"}) -CREATE_MODE(RawMode, RAWMODE_RGBAXX, {"RGBAXX"}) -CREATE_MODE(RawMode, RAWMODE_RGBA_15, {"RGBA;15"}) -CREATE_MODE(RawMode, RAWMODE_RGBA_16B, {"RGBA;16B"}) -CREATE_MODE(RawMode, RAWMODE_RGBA_16L, {"RGBA;16L"}) -CREATE_MODE(RawMode, RAWMODE_RGBA_16N, {"RGBA;16N"}) -CREATE_MODE(RawMode, RAWMODE_RGBA_4B, {"RGBA;4B"}) -CREATE_MODE(RawMode, RAWMODE_RGBA_I, {"RGBA;I"}) -CREATE_MODE(RawMode, RAWMODE_RGBA_L, {"RGBA;L"}) -CREATE_MODE(RawMode, RAWMODE_RGBXX, {"RGBXX"}) -CREATE_MODE(RawMode, RAWMODE_RGBXXX, {"RGBXXX"}) -CREATE_MODE(RawMode, RAWMODE_RGBX_16B, {"RGBX;16B"}) -CREATE_MODE(RawMode, RAWMODE_RGBX_16L, {"RGBX;16L"}) -CREATE_MODE(RawMode, RAWMODE_RGBX_16N, {"RGBX;16N"}) -CREATE_MODE(RawMode, RAWMODE_RGBX_L, {"RGBX;L"}) -CREATE_MODE(RawMode, RAWMODE_RGB_15, {"RGB;15"}) -CREATE_MODE(RawMode, RAWMODE_RGB_16, {"RGB;16"}) -CREATE_MODE(RawMode, RAWMODE_RGB_16B, {"RGB;16B"}) -CREATE_MODE(RawMode, RAWMODE_RGB_16L, {"RGB;16L"}) -CREATE_MODE(RawMode, RAWMODE_RGB_16N, {"RGB;16N"}) -CREATE_MODE(RawMode, RAWMODE_RGB_4B, {"RGB;4B"}) -CREATE_MODE(RawMode, RAWMODE_RGB_L, {"RGB;L"}) -CREATE_MODE(RawMode, RAWMODE_RGB_R, {"RGB;R"}) -CREATE_MODE(RawMode, RAWMODE_RGBaX, {"RGBaX"}) -CREATE_MODE(RawMode, RAWMODE_RGBaXX, {"RGBaXX"}) -CREATE_MODE(RawMode, RAWMODE_RGBa_16B, {"RGBa;16B"}) -CREATE_MODE(RawMode, RAWMODE_RGBa_16L, {"RGBa;16L"}) -CREATE_MODE(RawMode, RAWMODE_RGBa_16N, {"RGBa;16N"}) -CREATE_MODE(RawMode, RAWMODE_R_16B, {"R;16B"}) -CREATE_MODE(RawMode, RAWMODE_R_16L, {"R;16L"}) -CREATE_MODE(RawMode, RAWMODE_R_16N, {"R;16N"}) -CREATE_MODE(RawMode, RAWMODE_S, {"S"}) -CREATE_MODE(RawMode, RAWMODE_V, {"V"}) -CREATE_MODE(RawMode, RAWMODE_X, {"X"}) -CREATE_MODE(RawMode, RAWMODE_XBGR, {"XBGR"}) -CREATE_MODE(RawMode, RAWMODE_XRGB, {"XRGB"}) -CREATE_MODE(RawMode, RAWMODE_Y, {"Y"}) -CREATE_MODE(RawMode, RAWMODE_YCCA_P, {"YCCA;P"}) -CREATE_MODE(RawMode, RAWMODE_YCC_P, {"YCC;P"}) -CREATE_MODE(RawMode, RAWMODE_YCbCrK, {"YCbCrK"}) -CREATE_MODE(RawMode, RAWMODE_YCbCrX, {"YCbCrX"}) -CREATE_MODE(RawMode, RAWMODE_YCbCr_L, {"YCbCr;L"}) -CREATE_MODE(RawMode, RAWMODE_Y_I, {"Y;I"}) -CREATE_MODE(RawMode, RAWMODE_aBGR, {"aBGR"}) -CREATE_MODE(RawMode, RAWMODE_aRGB, {"aRGB"}) -const RawMode * const RAWMODES[] = { - IMAGING_RAWMODE_1, - IMAGING_RAWMODE_CMYK, - IMAGING_RAWMODE_F, - IMAGING_RAWMODE_HSV, - IMAGING_RAWMODE_I, - IMAGING_RAWMODE_L, - IMAGING_RAWMODE_LA, - IMAGING_RAWMODE_LAB, - IMAGING_RAWMODE_La, - IMAGING_RAWMODE_P, - IMAGING_RAWMODE_PA, - IMAGING_RAWMODE_RGB, - IMAGING_RAWMODE_RGBA, - IMAGING_RAWMODE_RGBX, - IMAGING_RAWMODE_RGBa, - IMAGING_RAWMODE_YCbCr, +const RawModeData RAWMODES[] = { + [IMAGING_RAWMODE_UNKNOWN] = {""}, - IMAGING_RAWMODE_BGR_15, - IMAGING_RAWMODE_BGR_16, - IMAGING_RAWMODE_BGR_24, + [IMAGING_RAWMODE_1] = {"1"}, + [IMAGING_RAWMODE_CMYK] = {"CMYK"}, + [IMAGING_RAWMODE_F] = {"F"}, + [IMAGING_RAWMODE_HSV] = {"HSV"}, + [IMAGING_RAWMODE_I] = {"I"}, + [IMAGING_RAWMODE_L] = {"L"}, + [IMAGING_RAWMODE_LA] = {"LA"}, + [IMAGING_RAWMODE_LAB] = {"LAB"}, + [IMAGING_RAWMODE_La] = {"La"}, + [IMAGING_RAWMODE_P] = {"P"}, + [IMAGING_RAWMODE_PA] = {"PA"}, + [IMAGING_RAWMODE_RGB] = {"RGB"}, + [IMAGING_RAWMODE_RGBA] = {"RGBA"}, + [IMAGING_RAWMODE_RGBX] = {"RGBX"}, + [IMAGING_RAWMODE_RGBa] = {"RGBa"}, + [IMAGING_RAWMODE_YCbCr] = {"YCbCr"}, - IMAGING_RAWMODE_I_16, - IMAGING_RAWMODE_I_16L, - IMAGING_RAWMODE_I_16B, - IMAGING_RAWMODE_I_16N, - IMAGING_RAWMODE_I_32L, - IMAGING_RAWMODE_I_32B, + [IMAGING_RAWMODE_BGR_15] = {"BGR;15"}, + [IMAGING_RAWMODE_BGR_16] = {"BGR;16"}, + [IMAGING_RAWMODE_BGR_24] = {"BGR;24"}, + [IMAGING_RAWMODE_BGR_32] = {"BGR;32"}, - IMAGING_RAWMODE_1_8, - IMAGING_RAWMODE_1_I, - IMAGING_RAWMODE_1_IR, - IMAGING_RAWMODE_1_R, - IMAGING_RAWMODE_A, - IMAGING_RAWMODE_ABGR, - IMAGING_RAWMODE_ARGB, - IMAGING_RAWMODE_A_16B, - IMAGING_RAWMODE_A_16L, - IMAGING_RAWMODE_A_16N, - IMAGING_RAWMODE_B, - IMAGING_RAWMODE_BGAR, - IMAGING_RAWMODE_BGR, - IMAGING_RAWMODE_BGRA, - IMAGING_RAWMODE_BGRA_15, - IMAGING_RAWMODE_BGRA_15Z, - IMAGING_RAWMODE_BGRA_16B, - IMAGING_RAWMODE_BGRA_16L, - IMAGING_RAWMODE_BGRX, - IMAGING_RAWMODE_BGR_5, - IMAGING_RAWMODE_BGRa, - IMAGING_RAWMODE_BGXR, - IMAGING_RAWMODE_B_16B, - IMAGING_RAWMODE_B_16L, - IMAGING_RAWMODE_B_16N, - IMAGING_RAWMODE_C, - IMAGING_RAWMODE_CMYKX, - IMAGING_RAWMODE_CMYKXX, - IMAGING_RAWMODE_CMYK_16B, - IMAGING_RAWMODE_CMYK_16L, - IMAGING_RAWMODE_CMYK_16N, - IMAGING_RAWMODE_CMYK_I, - IMAGING_RAWMODE_CMYK_L, - IMAGING_RAWMODE_C_I, - IMAGING_RAWMODE_Cb, - IMAGING_RAWMODE_Cr, - IMAGING_RAWMODE_F_16, - IMAGING_RAWMODE_F_16B, - IMAGING_RAWMODE_F_16BS, - IMAGING_RAWMODE_F_16N, - IMAGING_RAWMODE_F_16NS, - IMAGING_RAWMODE_F_16S, - IMAGING_RAWMODE_F_32, - IMAGING_RAWMODE_F_32B, - IMAGING_RAWMODE_F_32BF, - IMAGING_RAWMODE_F_32BS, - IMAGING_RAWMODE_F_32F, - IMAGING_RAWMODE_F_32N, - IMAGING_RAWMODE_F_32NF, - IMAGING_RAWMODE_F_32NS, - IMAGING_RAWMODE_F_32S, - IMAGING_RAWMODE_F_64BF, - IMAGING_RAWMODE_F_64F, - IMAGING_RAWMODE_F_64NF, - IMAGING_RAWMODE_F_8, - IMAGING_RAWMODE_F_8S, - IMAGING_RAWMODE_G, - IMAGING_RAWMODE_G_16B, - IMAGING_RAWMODE_G_16L, - IMAGING_RAWMODE_G_16N, - IMAGING_RAWMODE_H, - IMAGING_RAWMODE_I_12, - IMAGING_RAWMODE_I_16BS, - IMAGING_RAWMODE_I_16NS, - IMAGING_RAWMODE_I_16R, - IMAGING_RAWMODE_I_16S, - IMAGING_RAWMODE_I_32, - IMAGING_RAWMODE_I_32BS, - IMAGING_RAWMODE_I_32N, - IMAGING_RAWMODE_I_32NS, - IMAGING_RAWMODE_I_32S, - IMAGING_RAWMODE_I_8, - IMAGING_RAWMODE_I_8S, - IMAGING_RAWMODE_K, - IMAGING_RAWMODE_K_I, - IMAGING_RAWMODE_LA_16B, - IMAGING_RAWMODE_LA_L, - IMAGING_RAWMODE_L_16, - IMAGING_RAWMODE_L_16B, - IMAGING_RAWMODE_L_2, - IMAGING_RAWMODE_L_2I, - IMAGING_RAWMODE_L_2IR, - IMAGING_RAWMODE_L_2R, - IMAGING_RAWMODE_L_4, - IMAGING_RAWMODE_L_4I, - IMAGING_RAWMODE_L_4IR, - IMAGING_RAWMODE_L_4R, - IMAGING_RAWMODE_L_I, - IMAGING_RAWMODE_L_R, - IMAGING_RAWMODE_M, - IMAGING_RAWMODE_M_I, - IMAGING_RAWMODE_PA_L, - IMAGING_RAWMODE_PX, - IMAGING_RAWMODE_P_1, - IMAGING_RAWMODE_P_2, - IMAGING_RAWMODE_P_2L, - IMAGING_RAWMODE_P_4, - IMAGING_RAWMODE_P_4L, - IMAGING_RAWMODE_P_R, - IMAGING_RAWMODE_R, - IMAGING_RAWMODE_RGBAX, - IMAGING_RAWMODE_RGBAXX, - IMAGING_RAWMODE_RGBA_15, - IMAGING_RAWMODE_RGBA_16B, - IMAGING_RAWMODE_RGBA_16L, - IMAGING_RAWMODE_RGBA_16N, - IMAGING_RAWMODE_RGBA_4B, - IMAGING_RAWMODE_RGBA_I, - IMAGING_RAWMODE_RGBA_L, - IMAGING_RAWMODE_RGBXX, - IMAGING_RAWMODE_RGBXXX, - IMAGING_RAWMODE_RGBX_16B, - IMAGING_RAWMODE_RGBX_16L, - IMAGING_RAWMODE_RGBX_16N, - IMAGING_RAWMODE_RGBX_L, - IMAGING_RAWMODE_RGB_15, - IMAGING_RAWMODE_RGB_16, - IMAGING_RAWMODE_RGB_16B, - IMAGING_RAWMODE_RGB_16L, - IMAGING_RAWMODE_RGB_16N, - IMAGING_RAWMODE_RGB_4B, - IMAGING_RAWMODE_RGB_L, - IMAGING_RAWMODE_RGB_R, - IMAGING_RAWMODE_RGBaX, - IMAGING_RAWMODE_RGBaXX, - IMAGING_RAWMODE_RGBa_16B, - IMAGING_RAWMODE_RGBa_16L, - IMAGING_RAWMODE_RGBa_16N, - IMAGING_RAWMODE_R_16B, - IMAGING_RAWMODE_R_16L, - IMAGING_RAWMODE_R_16N, - IMAGING_RAWMODE_S, - IMAGING_RAWMODE_V, - IMAGING_RAWMODE_X, - IMAGING_RAWMODE_XBGR, - IMAGING_RAWMODE_XRGB, - IMAGING_RAWMODE_Y, - IMAGING_RAWMODE_YCCA_P, - IMAGING_RAWMODE_YCC_P, - IMAGING_RAWMODE_YCbCrK, - IMAGING_RAWMODE_YCbCrX, - IMAGING_RAWMODE_YCbCr_L, - IMAGING_RAWMODE_Y_I, - IMAGING_RAWMODE_aBGR, - IMAGING_RAWMODE_aRGB, + [IMAGING_RAWMODE_I_16] = {"I;16"}, + [IMAGING_RAWMODE_I_16L] = {"I;16L"}, + [IMAGING_RAWMODE_I_16B] = {"I;16B"}, + [IMAGING_RAWMODE_I_16N] = {"I;16N"}, + [IMAGING_RAWMODE_I_32L] = {"I;32L"}, + [IMAGING_RAWMODE_I_32B] = {"I;32B"}, - NULL + [IMAGING_RAWMODE_1_8] = {"1;8"}, + [IMAGING_RAWMODE_1_I] = {"1;I"}, + [IMAGING_RAWMODE_1_IR] = {"1;IR"}, + [IMAGING_RAWMODE_1_R] = {"1;R"}, + [IMAGING_RAWMODE_A] = {"A"}, + [IMAGING_RAWMODE_ABGR] = {"ABGR"}, + [IMAGING_RAWMODE_ARGB] = {"ARGB"}, + [IMAGING_RAWMODE_A_16B] = {"A;16B"}, + [IMAGING_RAWMODE_A_16L] = {"A;16L"}, + [IMAGING_RAWMODE_A_16N] = {"A;16N"}, + [IMAGING_RAWMODE_B] = {"B"}, + [IMAGING_RAWMODE_BGAR] = {"BGAR"}, + [IMAGING_RAWMODE_BGR] = {"BGR"}, + [IMAGING_RAWMODE_BGRA] = {"BGRA"}, + [IMAGING_RAWMODE_BGRA_15] = {"BGRA;15"}, + [IMAGING_RAWMODE_BGRA_15Z] = {"BGRA;15Z"}, + [IMAGING_RAWMODE_BGRA_16B] = {"BGRA;16B"}, + [IMAGING_RAWMODE_BGRA_16L] = {"BGRA;16L"}, + [IMAGING_RAWMODE_BGRX] = {"BGRX"}, + [IMAGING_RAWMODE_BGR_5] = {"BGR;5"}, + [IMAGING_RAWMODE_BGRa] = {"BGRa"}, + [IMAGING_RAWMODE_BGXR] = {"BGXR"}, + [IMAGING_RAWMODE_B_16B] = {"B;16B"}, + [IMAGING_RAWMODE_B_16L] = {"B;16L"}, + [IMAGING_RAWMODE_B_16N] = {"B;16N"}, + [IMAGING_RAWMODE_C] = {"C"}, + [IMAGING_RAWMODE_CMYKX] = {"CMYKX"}, + [IMAGING_RAWMODE_CMYKXX] = {"CMYKXX"}, + [IMAGING_RAWMODE_CMYK_16B] = {"CMYK;16B"}, + [IMAGING_RAWMODE_CMYK_16L] = {"CMYK;16L"}, + [IMAGING_RAWMODE_CMYK_16N] = {"CMYK;16N"}, + [IMAGING_RAWMODE_CMYK_I] = {"CMYK;I"}, + [IMAGING_RAWMODE_CMYK_L] = {"CMYK;L"}, + [IMAGING_RAWMODE_C_I] = {"C;I"}, + [IMAGING_RAWMODE_Cb] = {"Cb"}, + [IMAGING_RAWMODE_Cr] = {"Cr"}, + [IMAGING_RAWMODE_F_16] = {"F;16"}, + [IMAGING_RAWMODE_F_16B] = {"F;16B"}, + [IMAGING_RAWMODE_F_16BS] = {"F;16BS"}, + [IMAGING_RAWMODE_F_16N] = {"F;16N"}, + [IMAGING_RAWMODE_F_16NS] = {"F;16NS"}, + [IMAGING_RAWMODE_F_16S] = {"F;16S"}, + [IMAGING_RAWMODE_F_32] = {"F;32"}, + [IMAGING_RAWMODE_F_32B] = {"F;32B"}, + [IMAGING_RAWMODE_F_32BF] = {"F;32BF"}, + [IMAGING_RAWMODE_F_32BS] = {"F;32BS"}, + [IMAGING_RAWMODE_F_32F] = {"F;32F"}, + [IMAGING_RAWMODE_F_32N] = {"F;32N"}, + [IMAGING_RAWMODE_F_32NF] = {"F;32NF"}, + [IMAGING_RAWMODE_F_32NS] = {"F;32NS"}, + [IMAGING_RAWMODE_F_32S] = {"F;32S"}, + [IMAGING_RAWMODE_F_64BF] = {"F;64BF"}, + [IMAGING_RAWMODE_F_64F] = {"F;64F"}, + [IMAGING_RAWMODE_F_64NF] = {"F;64NF"}, + [IMAGING_RAWMODE_F_8] = {"F;8"}, + [IMAGING_RAWMODE_F_8S] = {"F;8S"}, + [IMAGING_RAWMODE_G] = {"G"}, + [IMAGING_RAWMODE_G_16B] = {"G;16B"}, + [IMAGING_RAWMODE_G_16L] = {"G;16L"}, + [IMAGING_RAWMODE_G_16N] = {"G;16N"}, + [IMAGING_RAWMODE_H] = {"H"}, + [IMAGING_RAWMODE_I_12] = {"I;12"}, + [IMAGING_RAWMODE_I_16BS] = {"I;16BS"}, + [IMAGING_RAWMODE_I_16NS] = {"I;16NS"}, + [IMAGING_RAWMODE_I_16R] = {"I;16R"}, + [IMAGING_RAWMODE_I_16S] = {"I;16S"}, + [IMAGING_RAWMODE_I_32] = {"I;32"}, + [IMAGING_RAWMODE_I_32BS] = {"I;32BS"}, + [IMAGING_RAWMODE_I_32N] = {"I;32N"}, + [IMAGING_RAWMODE_I_32NS] = {"I;32NS"}, + [IMAGING_RAWMODE_I_32S] = {"I;32S"}, + [IMAGING_RAWMODE_I_8] = {"I;8"}, + [IMAGING_RAWMODE_I_8S] = {"I;8S"}, + [IMAGING_RAWMODE_K] = {"K"}, + [IMAGING_RAWMODE_K_I] = {"K;I"}, + [IMAGING_RAWMODE_LA_16B] = {"LA;16B"}, + [IMAGING_RAWMODE_LA_L] = {"LA;L"}, + [IMAGING_RAWMODE_L_16] = {"L;16"}, + [IMAGING_RAWMODE_L_16B] = {"L;16B"}, + [IMAGING_RAWMODE_L_2] = {"L;2"}, + [IMAGING_RAWMODE_L_2I] = {"L;2I"}, + [IMAGING_RAWMODE_L_2IR] = {"L;2IR"}, + [IMAGING_RAWMODE_L_2R] = {"L;2R"}, + [IMAGING_RAWMODE_L_4] = {"L;4"}, + [IMAGING_RAWMODE_L_4I] = {"L;4I"}, + [IMAGING_RAWMODE_L_4IR] = {"L;4IR"}, + [IMAGING_RAWMODE_L_4R] = {"L;4R"}, + [IMAGING_RAWMODE_L_I] = {"L;I"}, + [IMAGING_RAWMODE_L_R] = {"L;R"}, + [IMAGING_RAWMODE_M] = {"M"}, + [IMAGING_RAWMODE_M_I] = {"M;I"}, + [IMAGING_RAWMODE_PA_L] = {"PA;L"}, + [IMAGING_RAWMODE_PX] = {"PX"}, + [IMAGING_RAWMODE_P_1] = {"P;1"}, + [IMAGING_RAWMODE_P_2] = {"P;2"}, + [IMAGING_RAWMODE_P_2L] = {"P;2L"}, + [IMAGING_RAWMODE_P_4] = {"P;4"}, + [IMAGING_RAWMODE_P_4L] = {"P;4L"}, + [IMAGING_RAWMODE_P_R] = {"P;R"}, + [IMAGING_RAWMODE_R] = {"R"}, + [IMAGING_RAWMODE_RGBAX] = {"RGBAX"}, + [IMAGING_RAWMODE_RGBAXX] = {"RGBAXX"}, + [IMAGING_RAWMODE_RGBA_15] = {"RGBA;15"}, + [IMAGING_RAWMODE_RGBA_16B] = {"RGBA;16B"}, + [IMAGING_RAWMODE_RGBA_16L] = {"RGBA;16L"}, + [IMAGING_RAWMODE_RGBA_16N] = {"RGBA;16N"}, + [IMAGING_RAWMODE_RGBA_4B] = {"RGBA;4B"}, + [IMAGING_RAWMODE_RGBA_I] = {"RGBA;I"}, + [IMAGING_RAWMODE_RGBA_L] = {"RGBA;L"}, + [IMAGING_RAWMODE_RGBXX] = {"RGBXX"}, + [IMAGING_RAWMODE_RGBXXX] = {"RGBXXX"}, + [IMAGING_RAWMODE_RGBX_16B] = {"RGBX;16B"}, + [IMAGING_RAWMODE_RGBX_16L] = {"RGBX;16L"}, + [IMAGING_RAWMODE_RGBX_16N] = {"RGBX;16N"}, + [IMAGING_RAWMODE_RGBX_L] = {"RGBX;L"}, + [IMAGING_RAWMODE_RGB_15] = {"RGB;15"}, + [IMAGING_RAWMODE_RGB_16] = {"RGB;16"}, + [IMAGING_RAWMODE_RGB_16B] = {"RGB;16B"}, + [IMAGING_RAWMODE_RGB_16L] = {"RGB;16L"}, + [IMAGING_RAWMODE_RGB_16N] = {"RGB;16N"}, + [IMAGING_RAWMODE_RGB_4B] = {"RGB;4B"}, + [IMAGING_RAWMODE_RGB_L] = {"RGB;L"}, + [IMAGING_RAWMODE_RGB_R] = {"RGB;R"}, + [IMAGING_RAWMODE_RGBaX] = {"RGBaX"}, + [IMAGING_RAWMODE_RGBaXX] = {"RGBaXX"}, + [IMAGING_RAWMODE_RGBa_16B] = {"RGBa;16B"}, + [IMAGING_RAWMODE_RGBa_16L] = {"RGBa;16L"}, + [IMAGING_RAWMODE_RGBa_16N] = {"RGBa;16N"}, + [IMAGING_RAWMODE_R_16B] = {"R;16B"}, + [IMAGING_RAWMODE_R_16L] = {"R;16L"}, + [IMAGING_RAWMODE_R_16N] = {"R;16N"}, + [IMAGING_RAWMODE_S] = {"S"}, + [IMAGING_RAWMODE_V] = {"V"}, + [IMAGING_RAWMODE_X] = {"X"}, + [IMAGING_RAWMODE_XBGR] = {"XBGR"}, + [IMAGING_RAWMODE_XRGB] = {"XRGB"}, + [IMAGING_RAWMODE_Y] = {"Y"}, + [IMAGING_RAWMODE_YCCA_P] = {"YCCA;P"}, + [IMAGING_RAWMODE_YCC_P] = {"YCC;P"}, + [IMAGING_RAWMODE_YCbCrK] = {"YCbCrK"}, + [IMAGING_RAWMODE_YCbCrX] = {"YCbCrX"}, + [IMAGING_RAWMODE_YCbCr_L] = {"YCbCr;L"}, + [IMAGING_RAWMODE_Y_I] = {"Y;I"}, + [IMAGING_RAWMODE_aBGR] = {"aBGR"}, + [IMAGING_RAWMODE_aRGB] = {"aRGB"}, }; -const RawMode * findRawMode(const char * const name) { +const RawModeID findRawModeID(const char * const name) { if (name == NULL) { - return NULL; + return IMAGING_RAWMODE_UNKNOWN; } - const RawMode * rawmode; - for (int i = 0; (rawmode = RAWMODES[i]); i++) { - if (strcmp(rawmode->name, name) == 0) { - return rawmode; + for (size_t i = 0; i < sizeof(RAWMODES) / sizeof(*RAWMODES); i++) { + if (strcmp(RAWMODES[i].name, name) == 0) { + return (RawModeID)i; } } - return NULL; + return IMAGING_RAWMODE_UNKNOWN; } -int isModeI16(const Mode * const mode) { +const RawModeData * const getRawModeData(const RawModeID id) { + if (id < 0 || id > sizeof(RAWMODES) / sizeof(*RAWMODES)) { + return &RAWMODES[IMAGING_RAWMODE_UNKNOWN]; + } + return &RAWMODES[id]; +} + + +int isModeI16(const ModeID mode) { return mode == IMAGING_MODE_I_16 || mode == IMAGING_MODE_I_16L || mode == IMAGING_MODE_I_16B diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index 663f2f4681f..e21ad941a26 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -2,227 +2,237 @@ #define __MODE_H__ +typedef enum { + IMAGING_MODE_UNKNOWN, + + IMAGING_MODE_1, + IMAGING_MODE_CMYK, + IMAGING_MODE_F, + IMAGING_MODE_HSV, + IMAGING_MODE_I, + IMAGING_MODE_L, + IMAGING_MODE_LA, + IMAGING_MODE_LAB, + IMAGING_MODE_La, + IMAGING_MODE_P, + IMAGING_MODE_PA, + IMAGING_MODE_RGB, + IMAGING_MODE_RGBA, + IMAGING_MODE_RGBX, + IMAGING_MODE_RGBa, + IMAGING_MODE_YCbCr, + + IMAGING_MODE_BGR_15, + IMAGING_MODE_BGR_16, + IMAGING_MODE_BGR_24, + + IMAGING_MODE_I_16, + IMAGING_MODE_I_16L, + IMAGING_MODE_I_16B, + IMAGING_MODE_I_16N, + IMAGING_MODE_I_32L, + IMAGING_MODE_I_32B, +} ModeID; + typedef struct { const char * const name; -} Mode; - -extern const Mode * const IMAGING_MODE_1; -extern const Mode * const IMAGING_MODE_CMYK; -extern const Mode * const IMAGING_MODE_F; -extern const Mode * const IMAGING_MODE_HSV; -extern const Mode * const IMAGING_MODE_I; -extern const Mode * const IMAGING_MODE_L; -extern const Mode * const IMAGING_MODE_LA; -extern const Mode * const IMAGING_MODE_LAB; -extern const Mode * const IMAGING_MODE_La; -extern const Mode * const IMAGING_MODE_P; -extern const Mode * const IMAGING_MODE_PA; -extern const Mode * const IMAGING_MODE_RGB; -extern const Mode * const IMAGING_MODE_RGBA; -extern const Mode * const IMAGING_MODE_RGBX; -extern const Mode * const IMAGING_MODE_RGBa; -extern const Mode * const IMAGING_MODE_YCbCr; - -extern const Mode * const IMAGING_MODE_BGR_15; -extern const Mode * const IMAGING_MODE_BGR_16; -extern const Mode * const IMAGING_MODE_BGR_24; - -extern const Mode * const IMAGING_MODE_I_16; -extern const Mode * const IMAGING_MODE_I_16L; -extern const Mode * const IMAGING_MODE_I_16B; -extern const Mode * const IMAGING_MODE_I_16N; -extern const Mode * const IMAGING_MODE_I_32L; -extern const Mode * const IMAGING_MODE_I_32B; - -const Mode * findMode(const char * const name); - +} ModeData; + +const ModeID findModeID(const char * const name); +const ModeData * const getModeData(const ModeID id); + + +typedef enum { + IMAGING_RAWMODE_UNKNOWN, + + // Non-rawmode aliases. + IMAGING_RAWMODE_1, + IMAGING_RAWMODE_CMYK, + IMAGING_RAWMODE_F, + IMAGING_RAWMODE_HSV, + IMAGING_RAWMODE_I, + IMAGING_RAWMODE_L, + IMAGING_RAWMODE_LA, + IMAGING_RAWMODE_LAB, + IMAGING_RAWMODE_La, + IMAGING_RAWMODE_P, + IMAGING_RAWMODE_PA, + IMAGING_RAWMODE_RGB, + IMAGING_RAWMODE_RGBA, + IMAGING_RAWMODE_RGBX, + IMAGING_RAWMODE_RGBa, + IMAGING_RAWMODE_YCbCr, + + // BGR modes. + IMAGING_RAWMODE_BGR_15, + IMAGING_RAWMODE_BGR_16, + IMAGING_RAWMODE_BGR_24, + IMAGING_RAWMODE_BGR_32, + + // I;* modes. + IMAGING_RAWMODE_I_16, + IMAGING_RAWMODE_I_16L, + IMAGING_RAWMODE_I_16B, + IMAGING_RAWMODE_I_16N, + IMAGING_RAWMODE_I_32L, + IMAGING_RAWMODE_I_32B, + + // Rawmodes + IMAGING_RAWMODE_1_8, + IMAGING_RAWMODE_1_I, + IMAGING_RAWMODE_1_IR, + IMAGING_RAWMODE_1_R, + IMAGING_RAWMODE_A, + IMAGING_RAWMODE_ABGR, + IMAGING_RAWMODE_ARGB, + IMAGING_RAWMODE_A_16B, + IMAGING_RAWMODE_A_16L, + IMAGING_RAWMODE_A_16N, + IMAGING_RAWMODE_B, + IMAGING_RAWMODE_BGAR, + IMAGING_RAWMODE_BGR, + IMAGING_RAWMODE_BGRA, + IMAGING_RAWMODE_BGRA_15, + IMAGING_RAWMODE_BGRA_15Z, + IMAGING_RAWMODE_BGRA_16B, + IMAGING_RAWMODE_BGRA_16L, + IMAGING_RAWMODE_BGRX, + IMAGING_RAWMODE_BGR_5, + IMAGING_RAWMODE_BGRa, + IMAGING_RAWMODE_BGXR, + IMAGING_RAWMODE_B_16B, + IMAGING_RAWMODE_B_16L, + IMAGING_RAWMODE_B_16N, + IMAGING_RAWMODE_C, + IMAGING_RAWMODE_CMYKX, + IMAGING_RAWMODE_CMYKXX, + IMAGING_RAWMODE_CMYK_16B, + IMAGING_RAWMODE_CMYK_16L, + IMAGING_RAWMODE_CMYK_16N, + IMAGING_RAWMODE_CMYK_I, + IMAGING_RAWMODE_CMYK_L, + IMAGING_RAWMODE_C_I, + IMAGING_RAWMODE_Cb, + IMAGING_RAWMODE_Cr, + IMAGING_RAWMODE_F_16, + IMAGING_RAWMODE_F_16B, + IMAGING_RAWMODE_F_16BS, + IMAGING_RAWMODE_F_16N, + IMAGING_RAWMODE_F_16NS, + IMAGING_RAWMODE_F_16S, + IMAGING_RAWMODE_F_32, + IMAGING_RAWMODE_F_32B, + IMAGING_RAWMODE_F_32BF, + IMAGING_RAWMODE_F_32BS, + IMAGING_RAWMODE_F_32F, + IMAGING_RAWMODE_F_32N, + IMAGING_RAWMODE_F_32NF, + IMAGING_RAWMODE_F_32NS, + IMAGING_RAWMODE_F_32S, + IMAGING_RAWMODE_F_64BF, + IMAGING_RAWMODE_F_64F, + IMAGING_RAWMODE_F_64NF, + IMAGING_RAWMODE_F_8, + IMAGING_RAWMODE_F_8S, + IMAGING_RAWMODE_G, + IMAGING_RAWMODE_G_16B, + IMAGING_RAWMODE_G_16L, + IMAGING_RAWMODE_G_16N, + IMAGING_RAWMODE_H, + IMAGING_RAWMODE_I_12, + IMAGING_RAWMODE_I_16BS, + IMAGING_RAWMODE_I_16NS, + IMAGING_RAWMODE_I_16R, + IMAGING_RAWMODE_I_16S, + IMAGING_RAWMODE_I_32, + IMAGING_RAWMODE_I_32BS, + IMAGING_RAWMODE_I_32N, + IMAGING_RAWMODE_I_32NS, + IMAGING_RAWMODE_I_32S, + IMAGING_RAWMODE_I_8, + IMAGING_RAWMODE_I_8S, + IMAGING_RAWMODE_K, + IMAGING_RAWMODE_K_I, + IMAGING_RAWMODE_LA_16B, + IMAGING_RAWMODE_LA_L, + IMAGING_RAWMODE_L_16, + IMAGING_RAWMODE_L_16B, + IMAGING_RAWMODE_L_2, + IMAGING_RAWMODE_L_2I, + IMAGING_RAWMODE_L_2IR, + IMAGING_RAWMODE_L_2R, + IMAGING_RAWMODE_L_4, + IMAGING_RAWMODE_L_4I, + IMAGING_RAWMODE_L_4IR, + IMAGING_RAWMODE_L_4R, + IMAGING_RAWMODE_L_I, + IMAGING_RAWMODE_L_R, + IMAGING_RAWMODE_M, + IMAGING_RAWMODE_M_I, + IMAGING_RAWMODE_PA_L, + IMAGING_RAWMODE_PX, + IMAGING_RAWMODE_P_1, + IMAGING_RAWMODE_P_2, + IMAGING_RAWMODE_P_2L, + IMAGING_RAWMODE_P_4, + IMAGING_RAWMODE_P_4L, + IMAGING_RAWMODE_P_R, + IMAGING_RAWMODE_R, + IMAGING_RAWMODE_RGBAX, + IMAGING_RAWMODE_RGBAXX, + IMAGING_RAWMODE_RGBA_15, + IMAGING_RAWMODE_RGBA_16B, + IMAGING_RAWMODE_RGBA_16L, + IMAGING_RAWMODE_RGBA_16N, + IMAGING_RAWMODE_RGBA_4B, + IMAGING_RAWMODE_RGBA_I, + IMAGING_RAWMODE_RGBA_L, + IMAGING_RAWMODE_RGBXX, + IMAGING_RAWMODE_RGBXXX, + IMAGING_RAWMODE_RGBX_16B, + IMAGING_RAWMODE_RGBX_16L, + IMAGING_RAWMODE_RGBX_16N, + IMAGING_RAWMODE_RGBX_L, + IMAGING_RAWMODE_RGB_15, + IMAGING_RAWMODE_RGB_16, + IMAGING_RAWMODE_RGB_16B, + IMAGING_RAWMODE_RGB_16L, + IMAGING_RAWMODE_RGB_16N, + IMAGING_RAWMODE_RGB_4B, + IMAGING_RAWMODE_RGB_L, + IMAGING_RAWMODE_RGB_R, + IMAGING_RAWMODE_RGBaX, + IMAGING_RAWMODE_RGBaXX, + IMAGING_RAWMODE_RGBa_16B, + IMAGING_RAWMODE_RGBa_16L, + IMAGING_RAWMODE_RGBa_16N, + IMAGING_RAWMODE_R_16B, + IMAGING_RAWMODE_R_16L, + IMAGING_RAWMODE_R_16N, + IMAGING_RAWMODE_S, + IMAGING_RAWMODE_V, + IMAGING_RAWMODE_X, + IMAGING_RAWMODE_XBGR, + IMAGING_RAWMODE_XRGB, + IMAGING_RAWMODE_Y, + IMAGING_RAWMODE_YCCA_P, + IMAGING_RAWMODE_YCC_P, + IMAGING_RAWMODE_YCbCrK, + IMAGING_RAWMODE_YCbCrX, + IMAGING_RAWMODE_YCbCr_L, + IMAGING_RAWMODE_Y_I, + IMAGING_RAWMODE_aBGR, + IMAGING_RAWMODE_aRGB, +} RawModeID; typedef struct { const char * const name; -} RawMode; - -// Non-rawmode aliases. -extern const RawMode * const IMAGING_RAWMODE_1; -extern const RawMode * const IMAGING_RAWMODE_CMYK; -extern const RawMode * const IMAGING_RAWMODE_F; -extern const RawMode * const IMAGING_RAWMODE_HSV; -extern const RawMode * const IMAGING_RAWMODE_I; -extern const RawMode * const IMAGING_RAWMODE_L; -extern const RawMode * const IMAGING_RAWMODE_LA; -extern const RawMode * const IMAGING_RAWMODE_LAB; -extern const RawMode * const IMAGING_RAWMODE_La; -extern const RawMode * const IMAGING_RAWMODE_P; -extern const RawMode * const IMAGING_RAWMODE_PA; -extern const RawMode * const IMAGING_RAWMODE_RGB; -extern const RawMode * const IMAGING_RAWMODE_RGBA; -extern const RawMode * const IMAGING_RAWMODE_RGBX; -extern const RawMode * const IMAGING_RAWMODE_RGBa; -extern const RawMode * const IMAGING_RAWMODE_YCbCr; - -// BGR modes. -extern const RawMode * const IMAGING_RAWMODE_BGR_15; -extern const RawMode * const IMAGING_RAWMODE_BGR_16; -extern const RawMode * const IMAGING_RAWMODE_BGR_24; -extern const RawMode * const IMAGING_RAWMODE_BGR_32; - -// I;* modes. -extern const RawMode * const IMAGING_RAWMODE_I_16; -extern const RawMode * const IMAGING_RAWMODE_I_16L; -extern const RawMode * const IMAGING_RAWMODE_I_16B; -extern const RawMode * const IMAGING_RAWMODE_I_16N; -extern const RawMode * const IMAGING_RAWMODE_I_32L; -extern const RawMode * const IMAGING_RAWMODE_I_32B; - -// Rawmodes -extern const RawMode * const IMAGING_RAWMODE_1_8; -extern const RawMode * const IMAGING_RAWMODE_1_I; -extern const RawMode * const IMAGING_RAWMODE_1_IR; -extern const RawMode * const IMAGING_RAWMODE_1_R; -extern const RawMode * const IMAGING_RAWMODE_A; -extern const RawMode * const IMAGING_RAWMODE_ABGR; -extern const RawMode * const IMAGING_RAWMODE_ARGB; -extern const RawMode * const IMAGING_RAWMODE_A_16B; -extern const RawMode * const IMAGING_RAWMODE_A_16L; -extern const RawMode * const IMAGING_RAWMODE_A_16N; -extern const RawMode * const IMAGING_RAWMODE_B; -extern const RawMode * const IMAGING_RAWMODE_BGAR; -extern const RawMode * const IMAGING_RAWMODE_BGR; -extern const RawMode * const IMAGING_RAWMODE_BGRA; -extern const RawMode * const IMAGING_RAWMODE_BGRA_15; -extern const RawMode * const IMAGING_RAWMODE_BGRA_15Z; -extern const RawMode * const IMAGING_RAWMODE_BGRA_16B; -extern const RawMode * const IMAGING_RAWMODE_BGRA_16L; -extern const RawMode * const IMAGING_RAWMODE_BGRX; -extern const RawMode * const IMAGING_RAWMODE_BGR_5; -extern const RawMode * const IMAGING_RAWMODE_BGRa; -extern const RawMode * const IMAGING_RAWMODE_BGXR; -extern const RawMode * const IMAGING_RAWMODE_B_16B; -extern const RawMode * const IMAGING_RAWMODE_B_16L; -extern const RawMode * const IMAGING_RAWMODE_B_16N; -extern const RawMode * const IMAGING_RAWMODE_C; -extern const RawMode * const IMAGING_RAWMODE_CMYKX; -extern const RawMode * const IMAGING_RAWMODE_CMYKXX; -extern const RawMode * const IMAGING_RAWMODE_CMYK_16B; -extern const RawMode * const IMAGING_RAWMODE_CMYK_16L; -extern const RawMode * const IMAGING_RAWMODE_CMYK_16N; -extern const RawMode * const IMAGING_RAWMODE_CMYK_I; -extern const RawMode * const IMAGING_RAWMODE_CMYK_L; -extern const RawMode * const IMAGING_RAWMODE_C_I; -extern const RawMode * const IMAGING_RAWMODE_Cb; -extern const RawMode * const IMAGING_RAWMODE_Cr; -extern const RawMode * const IMAGING_RAWMODE_F_16; -extern const RawMode * const IMAGING_RAWMODE_F_16B; -extern const RawMode * const IMAGING_RAWMODE_F_16BS; -extern const RawMode * const IMAGING_RAWMODE_F_16N; -extern const RawMode * const IMAGING_RAWMODE_F_16NS; -extern const RawMode * const IMAGING_RAWMODE_F_16S; -extern const RawMode * const IMAGING_RAWMODE_F_32; -extern const RawMode * const IMAGING_RAWMODE_F_32B; -extern const RawMode * const IMAGING_RAWMODE_F_32BF; -extern const RawMode * const IMAGING_RAWMODE_F_32BS; -extern const RawMode * const IMAGING_RAWMODE_F_32F; -extern const RawMode * const IMAGING_RAWMODE_F_32N; -extern const RawMode * const IMAGING_RAWMODE_F_32NF; -extern const RawMode * const IMAGING_RAWMODE_F_32NS; -extern const RawMode * const IMAGING_RAWMODE_F_32S; -extern const RawMode * const IMAGING_RAWMODE_F_64BF; -extern const RawMode * const IMAGING_RAWMODE_F_64F; -extern const RawMode * const IMAGING_RAWMODE_F_64NF; -extern const RawMode * const IMAGING_RAWMODE_F_8; -extern const RawMode * const IMAGING_RAWMODE_F_8S; -extern const RawMode * const IMAGING_RAWMODE_G; -extern const RawMode * const IMAGING_RAWMODE_G_16B; -extern const RawMode * const IMAGING_RAWMODE_G_16L; -extern const RawMode * const IMAGING_RAWMODE_G_16N; -extern const RawMode * const IMAGING_RAWMODE_H; -extern const RawMode * const IMAGING_RAWMODE_I_12; -extern const RawMode * const IMAGING_RAWMODE_I_16BS; -extern const RawMode * const IMAGING_RAWMODE_I_16NS; -extern const RawMode * const IMAGING_RAWMODE_I_16R; -extern const RawMode * const IMAGING_RAWMODE_I_16S; -extern const RawMode * const IMAGING_RAWMODE_I_32; -extern const RawMode * const IMAGING_RAWMODE_I_32BS; -extern const RawMode * const IMAGING_RAWMODE_I_32N; -extern const RawMode * const IMAGING_RAWMODE_I_32NS; -extern const RawMode * const IMAGING_RAWMODE_I_32S; -extern const RawMode * const IMAGING_RAWMODE_I_8; -extern const RawMode * const IMAGING_RAWMODE_I_8S; -extern const RawMode * const IMAGING_RAWMODE_K; -extern const RawMode * const IMAGING_RAWMODE_K_I; -extern const RawMode * const IMAGING_RAWMODE_LA_16B; -extern const RawMode * const IMAGING_RAWMODE_LA_L; -extern const RawMode * const IMAGING_RAWMODE_L_16; -extern const RawMode * const IMAGING_RAWMODE_L_16B; -extern const RawMode * const IMAGING_RAWMODE_L_2; -extern const RawMode * const IMAGING_RAWMODE_L_2I; -extern const RawMode * const IMAGING_RAWMODE_L_2IR; -extern const RawMode * const IMAGING_RAWMODE_L_2R; -extern const RawMode * const IMAGING_RAWMODE_L_4; -extern const RawMode * const IMAGING_RAWMODE_L_4I; -extern const RawMode * const IMAGING_RAWMODE_L_4IR; -extern const RawMode * const IMAGING_RAWMODE_L_4R; -extern const RawMode * const IMAGING_RAWMODE_L_I; -extern const RawMode * const IMAGING_RAWMODE_L_R; -extern const RawMode * const IMAGING_RAWMODE_M; -extern const RawMode * const IMAGING_RAWMODE_M_I; -extern const RawMode * const IMAGING_RAWMODE_PA_L; -extern const RawMode * const IMAGING_RAWMODE_PX; -extern const RawMode * const IMAGING_RAWMODE_P_1; -extern const RawMode * const IMAGING_RAWMODE_P_2; -extern const RawMode * const IMAGING_RAWMODE_P_2L; -extern const RawMode * const IMAGING_RAWMODE_P_4; -extern const RawMode * const IMAGING_RAWMODE_P_4L; -extern const RawMode * const IMAGING_RAWMODE_P_R; -extern const RawMode * const IMAGING_RAWMODE_R; -extern const RawMode * const IMAGING_RAWMODE_RGBAX; -extern const RawMode * const IMAGING_RAWMODE_RGBAXX; -extern const RawMode * const IMAGING_RAWMODE_RGBA_15; -extern const RawMode * const IMAGING_RAWMODE_RGBA_16B; -extern const RawMode * const IMAGING_RAWMODE_RGBA_16L; -extern const RawMode * const IMAGING_RAWMODE_RGBA_16N; -extern const RawMode * const IMAGING_RAWMODE_RGBA_4B; -extern const RawMode * const IMAGING_RAWMODE_RGBA_I; -extern const RawMode * const IMAGING_RAWMODE_RGBA_L; -extern const RawMode * const IMAGING_RAWMODE_RGBXX; -extern const RawMode * const IMAGING_RAWMODE_RGBXXX; -extern const RawMode * const IMAGING_RAWMODE_RGBX_16B; -extern const RawMode * const IMAGING_RAWMODE_RGBX_16L; -extern const RawMode * const IMAGING_RAWMODE_RGBX_16N; -extern const RawMode * const IMAGING_RAWMODE_RGBX_L; -extern const RawMode * const IMAGING_RAWMODE_RGB_15; -extern const RawMode * const IMAGING_RAWMODE_RGB_16; -extern const RawMode * const IMAGING_RAWMODE_RGB_16B; -extern const RawMode * const IMAGING_RAWMODE_RGB_16L; -extern const RawMode * const IMAGING_RAWMODE_RGB_16N; -extern const RawMode * const IMAGING_RAWMODE_RGB_4B; -extern const RawMode * const IMAGING_RAWMODE_RGB_L; -extern const RawMode * const IMAGING_RAWMODE_RGB_R; -extern const RawMode * const IMAGING_RAWMODE_RGBaX; -extern const RawMode * const IMAGING_RAWMODE_RGBaXX; -extern const RawMode * const IMAGING_RAWMODE_RGBa_16B; -extern const RawMode * const IMAGING_RAWMODE_RGBa_16L; -extern const RawMode * const IMAGING_RAWMODE_RGBa_16N; -extern const RawMode * const IMAGING_RAWMODE_R_16B; -extern const RawMode * const IMAGING_RAWMODE_R_16L; -extern const RawMode * const IMAGING_RAWMODE_R_16N; -extern const RawMode * const IMAGING_RAWMODE_S; -extern const RawMode * const IMAGING_RAWMODE_V; -extern const RawMode * const IMAGING_RAWMODE_X; -extern const RawMode * const IMAGING_RAWMODE_XBGR; -extern const RawMode * const IMAGING_RAWMODE_XRGB; -extern const RawMode * const IMAGING_RAWMODE_Y; -extern const RawMode * const IMAGING_RAWMODE_YCCA_P; -extern const RawMode * const IMAGING_RAWMODE_YCC_P; -extern const RawMode * const IMAGING_RAWMODE_YCbCrK; -extern const RawMode * const IMAGING_RAWMODE_YCbCrX; -extern const RawMode * const IMAGING_RAWMODE_YCbCr_L; -extern const RawMode * const IMAGING_RAWMODE_Y_I; -extern const RawMode * const IMAGING_RAWMODE_aBGR; -extern const RawMode * const IMAGING_RAWMODE_aRGB; - -const RawMode * findRawMode(const char * const name); - - -int isModeI16(const Mode * const mode); +} RawModeData; + +const RawModeID findRawModeID(const char * const name); +const RawModeData * const getRawModeData(const RawModeID id); + + +int isModeI16(const ModeID mode); #endif // __MODE_H__ diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index aaa074c9249..63bbc8acb35 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -518,147 +518,146 @@ band3(UINT8 *out, const UINT8 *in, int pixels) { } } -static struct Packer { - const Mode *mode; - const RawMode *rawmode; +static struct { + const ModeID mode; + const RawModeID rawmode; int bits; ImagingShuffler pack; } packers[] = { /* bilevel */ - {"1", "1", 1, pack1}, - {"1", "1;I", 1, pack1I}, - {"1", "1;R", 1, pack1R}, - {"1", "1;IR", 1, pack1IR}, - {"1", "L", 8, pack1L}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1, 1, pack1}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_I, 1, pack1I}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_R, 1, pack1R}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_IR, 1, pack1IR}, + {IMAGING_MODE_1, IMAGING_RAWMODE_L, 8, pack1L}, /* grayscale */ - {"L", "L", 8, copy1}, - {"L", "L;16", 16, packL16}, - {"L", "L;16B", 16, packL16B}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L, 8, copy1}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_16, 16, packL16}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_16B, 16, packL16B}, /* grayscale w. alpha */ - {"LA", "LA", 16, packLA}, - {"LA", "LA;L", 16, packLAL}, + {IMAGING_MODE_LA, IMAGING_RAWMODE_LA, 16, packLA}, + {IMAGING_MODE_LA, IMAGING_RAWMODE_LA_L, 16, packLAL}, /* grayscale w. alpha premultiplied */ - {"La", "La", 16, packLA}, + {IMAGING_MODE_La, IMAGING_RAWMODE_La, 16, packLA}, /* palette */ - {"P", "P;1", 1, pack1}, - {"P", "P;2", 2, packP2}, - {"P", "P;4", 4, packP4}, - {"P", "P", 8, copy1}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_1, 1, pack1}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_2, 2, packP2}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_4, 4, packP4}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P, 8, copy1}, /* palette w. alpha */ - {"PA", "PA", 16, packLA}, - {"PA", "PA;L", 16, packLAL}, + {IMAGING_MODE_PA, IMAGING_RAWMODE_PA, 16, packLA}, + {IMAGING_MODE_PA, IMAGING_RAWMODE_PA_L, 16, packLAL}, /* true colour */ - {"RGB", "RGB", 24, ImagingPackRGB}, - {"RGB", "RGBX", 32, copy4}, - {"RGB", "RGBA", 32, copy4}, - {"RGB", "XRGB", 32, ImagingPackXRGB}, - {"RGB", "BGR", 24, ImagingPackBGR}, - {"RGB", "BGRX", 32, ImagingPackBGRX}, - {"RGB", "XBGR", 32, ImagingPackXBGR}, - {"RGB", "RGB;L", 24, packRGBL}, - {"RGB", "R", 8, band0}, - {"RGB", "G", 8, band1}, - {"RGB", "B", 8, band2}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB, 24, ImagingPackRGB}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX, 32, copy4}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBA, 32, copy4}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_XRGB, 32, ImagingPackXRGB}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR, 24, ImagingPackBGR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGRX, 32, ImagingPackBGRX}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_XBGR, 32, ImagingPackXBGR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_L, 24, packRGBL}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B, 8, band2}, /* true colour w. alpha */ - {"RGBA", "RGBA", 32, copy4}, - {"RGBA", "RGBA;L", 32, packRGBXL}, - {"RGBA", "RGB", 24, ImagingPackRGB}, - {"RGBA", "BGR", 24, ImagingPackBGR}, - {"RGBA", "BGRA", 32, ImagingPackBGRA}, - {"RGBA", "ABGR", 32, ImagingPackABGR}, - {"RGBA", "BGRa", 32, ImagingPackBGRa}, - {"RGBA", "R", 8, band0}, - {"RGBA", "G", 8, band1}, - {"RGBA", "B", 8, band2}, - {"RGBA", "A", 8, band3}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA, 32, copy4}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_L, 32, packRGBXL}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGB, 24, ImagingPackRGB}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGR, 24, ImagingPackBGR}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA, 32, ImagingPackBGRA}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_ABGR, 32, ImagingPackABGR}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRa, 32, ImagingPackBGRa}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A, 8, band3}, /* true colour w. alpha premultiplied */ - {"RGBa", "RGBa", 32, copy4}, - {"RGBa", "BGRa", 32, ImagingPackBGRA}, - {"RGBa", "aBGR", 32, ImagingPackABGR}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_RGBa, 32, copy4}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_BGRa, 32, ImagingPackBGRA}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_aBGR, 32, ImagingPackABGR}, /* true colour w. padding */ - {"RGBX", "RGBX", 32, copy4}, - {"RGBX", "RGBX;L", 32, packRGBXL}, - {"RGBX", "RGB", 24, ImagingPackRGB}, - {"RGBX", "BGR", 24, ImagingPackBGR}, - {"RGBX", "BGRX", 32, ImagingPackBGRX}, - {"RGBX", "XBGR", 32, ImagingPackXBGR}, - {"RGBX", "R", 8, band0}, - {"RGBX", "G", 8, band1}, - {"RGBX", "B", 8, band2}, - {"RGBX", "X", 8, band3}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX, 32, copy4}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_L, 32, packRGBXL}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB, 24, ImagingPackRGB}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR, 24, ImagingPackBGR}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGRX, 32, ImagingPackBGRX}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_XBGR, 32, ImagingPackXBGR}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_X, 8, band3}, /* colour separation */ - {"CMYK", "CMYK", 32, copy4}, - {"CMYK", "CMYK;I", 32, copy4I}, - {"CMYK", "CMYK;L", 32, packRGBXL}, - {"CMYK", "C", 8, band0}, - {"CMYK", "M", 8, band1}, - {"CMYK", "Y", 8, band2}, - {"CMYK", "K", 8, band3}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK, 32, copy4}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_I, 32, copy4I}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_L, 32, packRGBXL}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_C, 8, band0}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_M, 8, band1}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y, 8, band2}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_K, 8, band3}, /* video (YCbCr) */ - {"YCbCr", "YCbCr", 24, ImagingPackRGB}, - {"YCbCr", "YCbCr;L", 24, packRGBL}, - {"YCbCr", "YCbCrX", 32, copy4}, - {"YCbCr", "YCbCrK", 32, copy4}, - {"YCbCr", "Y", 8, band0}, - {"YCbCr", "Cb", 8, band1}, - {"YCbCr", "Cr", 8, band2}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr, 24, ImagingPackRGB}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr_L, 24, packRGBL}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrX, 32, copy4}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrK, 32, copy4}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_Y, 8, band0}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_Cb, 8, band1}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_Cr, 8, band2}, /* LAB Color */ - {"LAB", "LAB", 24, ImagingPackLAB}, - {"LAB", "L", 8, band0}, - {"LAB", "A", 8, band1}, - {"LAB", "B", 8, band2}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_LAB, 24, ImagingPackLAB}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_L, 8, band0}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_A, 8, band1}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_B, 8, band2}, /* HSV */ - {"HSV", "HSV", 24, ImagingPackRGB}, - {"HSV", "H", 8, band0}, - {"HSV", "S", 8, band1}, - {"HSV", "V", 8, band2}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_HSV, 24, ImagingPackRGB}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_H, 8, band0}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_S, 8, band1}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_V, 8, band2}, /* integer */ - {"I", "I", 32, copy4}, - {"I", "I;16B", 16, packI16B}, - {"I", "I;32S", 32, packI32S}, - {"I", "I;32NS", 32, copy4}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I, 32, copy4}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16B, 16, packI16B}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32S, 32, packI32S}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32NS, 32, copy4}, /* floating point */ - {"F", "F", 32, copy4}, - {"F", "F;32F", 32, packI32S}, - {"F", "F;32NF", 32, copy4}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F, 32, copy4}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32F, 32, packI32S}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32NF, 32, copy4}, /* storage modes */ - {"I;16", "I;16", 16, copy2}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16, 16, copy2}, #ifdef WORDS_BIGENDIAN - {"I;16", "I;16B", 16, packI16N_I16}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, packI16N_I16}, #else - {"I;16", "I;16B", 16, packI16N_I16B}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, packI16N_I16B}, #endif - {"I;16B", "I;16B", 16, copy2}, - {"I;16L", "I;16L", 16, copy2}, - {"I;16N", "I;16N", 16, copy2}, - {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. - {"I;16L", "I;16N", 16, packI16N_I16}, - {"I;16B", "I;16N", 16, packI16N_I16B}, + {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16B, 16, copy2}, + {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16L, 16, copy2}, + {IMAGING_MODE_I_16N, IMAGING_RAWMODE_I_16N, 16, copy2}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, packI16N_I16}, // LibTiff native->image endian. + {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, packI16N_I16}, + {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, packI16N_I16B}, {NULL} /* sentinel */ }; ImagingShuffler -ImagingFindPacker(const Mode *mode, const RawMode *rawmode, int *bits_out) { - int i; - for (i = 0; packers[i].mode; i++) { +ImagingFindPacker(const ModeID mode, const RawModeID rawmode, int *bits_out) { + for (size_t i = 0; i < sizeof(packers) / sizeof(*packers); i++) { if (packers[i].mode == mode && packers[i].rawmode == rawmode) { if (bits_out) { *bits_out = packers[i].bits; @@ -668,152 +667,3 @@ ImagingFindPacker(const Mode *mode, const RawMode *rawmode, int *bits_out) { } return NULL; } - -void -ImagingPackInit(void) { - const struct Packer temp[] = { - /* bilevel */ - {IMAGING_MODE_1, IMAGING_RAWMODE_1, 1, pack1}, - {IMAGING_MODE_1, IMAGING_RAWMODE_1_I, 1, pack1I}, - {IMAGING_MODE_1, IMAGING_RAWMODE_1_R, 1, pack1R}, - {IMAGING_MODE_1, IMAGING_RAWMODE_1_IR, 1, pack1IR}, - {IMAGING_MODE_1, IMAGING_RAWMODE_L, 8, pack1L}, - - /* grayscale */ - {IMAGING_MODE_L, IMAGING_RAWMODE_L, 8, copy1}, - {IMAGING_MODE_L, IMAGING_RAWMODE_L_16, 16, packL16}, - {IMAGING_MODE_L, IMAGING_RAWMODE_L_16B, 16, packL16B}, - - /* grayscale w. alpha */ - {IMAGING_MODE_LA, IMAGING_RAWMODE_LA, 16, packLA}, - {IMAGING_MODE_LA, IMAGING_RAWMODE_LA_L, 16, packLAL}, - - /* grayscale w. alpha premultiplied */ - {IMAGING_MODE_La, IMAGING_RAWMODE_La, 16, packLA}, - - /* palette */ - {IMAGING_MODE_P, IMAGING_RAWMODE_P_1, 1, pack1}, - {IMAGING_MODE_P, IMAGING_RAWMODE_P_2, 2, packP2}, - {IMAGING_MODE_P, IMAGING_RAWMODE_P_4, 4, packP4}, - {IMAGING_MODE_P, IMAGING_RAWMODE_P, 8, copy1}, - - /* palette w. alpha */ - {IMAGING_MODE_PA, IMAGING_RAWMODE_PA, 16, packLA}, - {IMAGING_MODE_PA, IMAGING_RAWMODE_PA_L, 16, packLAL}, - - /* true colour */ - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB, 24, ImagingPackRGB}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX, 32, copy4}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBA, 32, copy4}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_XRGB, 32, ImagingPackXRGB}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR, 24, ImagingPackBGR}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGRX, 32, ImagingPackBGRX}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_XBGR, 32, ImagingPackXBGR}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_L, 24, packRGBL}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_R, 8, band0}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_G, 8, band1}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_B, 8, band2}, - - /* true colour w. alpha */ - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA, 32, copy4}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_L, 32, packRGBXL}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGB, 24, ImagingPackRGB}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGR, 24, ImagingPackBGR}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA, 32, ImagingPackBGRA}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_ABGR, 32, ImagingPackABGR}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRa, 32, ImagingPackBGRa}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R, 8, band0}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G, 8, band1}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B, 8, band2}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A, 8, band3}, - - /* true colour w. alpha premultiplied */ - {IMAGING_MODE_RGBa, IMAGING_RAWMODE_RGBa, 32, copy4}, - {IMAGING_MODE_RGBa, IMAGING_RAWMODE_BGRa, 32, ImagingPackBGRA}, - {IMAGING_MODE_RGBa, IMAGING_RAWMODE_aBGR, 32, ImagingPackABGR}, - - /* true colour w. padding */ - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX, 32, copy4}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_L, 32, packRGBXL}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB, 24, ImagingPackRGB}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR, 24, ImagingPackBGR}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGRX, 32, ImagingPackBGRX}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_XBGR, 32, ImagingPackXBGR}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_R, 8, band0}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_G, 8, band1}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_B, 8, band2}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_X, 8, band3}, - - /* colour separation */ - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK, 32, copy4}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_I, 32, copy4I}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_L, 32, packRGBXL}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_C, 8, band0}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_M, 8, band1}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y, 8, band2}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_K, 8, band3}, - - /* video (YCbCr) */ - {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr, 24, ImagingPackRGB}, - {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr_L, 24, packRGBL}, - {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrX, 32, copy4}, - {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrK, 32, copy4}, - {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_Y, 8, band0}, - {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_Cb, 8, band1}, - {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_Cr, 8, band2}, - - /* LAB Color */ - {IMAGING_MODE_LAB, IMAGING_RAWMODE_LAB, 24, ImagingPackLAB}, - {IMAGING_MODE_LAB, IMAGING_RAWMODE_L, 8, band0}, - {IMAGING_MODE_LAB, IMAGING_RAWMODE_A, 8, band1}, - {IMAGING_MODE_LAB, IMAGING_RAWMODE_B, 8, band2}, - - /* HSV */ - {IMAGING_MODE_HSV, IMAGING_RAWMODE_HSV, 24, ImagingPackRGB}, - {IMAGING_MODE_HSV, IMAGING_RAWMODE_H, 8, band0}, - {IMAGING_MODE_HSV, IMAGING_RAWMODE_S, 8, band1}, - {IMAGING_MODE_HSV, IMAGING_RAWMODE_V, 8, band2}, - - /* integer */ - {IMAGING_MODE_I, IMAGING_RAWMODE_I, 32, copy4}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_16B, 16, packI16B}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_32S, 32, packI32S}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_32NS, 32, copy4}, - - /* floating point */ - {IMAGING_MODE_F, IMAGING_RAWMODE_F, 32, copy4}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_32F, 32, packI32S}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_32NF, 32, copy4}, - - /* storage modes */ - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16, 16, copy2}, -#ifdef WORDS_BIGENDIAN - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, packI16N_I16}, -#else - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, packI16N_I16B}, -#endif - {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16B, 16, copy2}, - {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16L, 16, copy2}, - {IMAGING_MODE_I_16N, IMAGING_RAWMODE_I_16N, 16, copy2}, - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, packI16N_I16}, // LibTiff native->image endian. - {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, packI16N_I16}, - {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, packI16N_I16B}, - {IMAGING_MODE_BGR_15, IMAGING_RAWMODE_BGR_15, 16, copy2}, - {IMAGING_MODE_BGR_16, IMAGING_RAWMODE_BGR_16, 16, copy2}, - {IMAGING_MODE_BGR_24, IMAGING_RAWMODE_BGR_24, 24, copy3}, - - {NULL} /* sentinel */ - }; - packers = malloc(sizeof(temp)); - if (packers == NULL) { - fprintf(stderr, "PackInit: failed to allocate memory for packers table\n"); - exit(1); - } - memcpy(packers, temp, sizeof(temp)); -} - -void -ImagingPackFree(void) { - free(packers); - packers = NULL; -} diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c index 6b4fea6a5fd..2bbdb69ef65 100644 --- a/src/libImaging/Palette.c +++ b/src/libImaging/Palette.c @@ -21,7 +21,7 @@ #include ImagingPalette -ImagingPaletteNew(const Mode *mode) { +ImagingPaletteNew(const ModeID mode) { /* Create a palette object */ int i; diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index e33f38508dc..fa0b1027cd0 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -128,7 +128,7 @@ im_point_32_8(Imaging imOut, Imaging imIn, im_point_context *context) { } Imaging -ImagingPoint(Imaging imIn, const Mode *mode, const void *table) { +ImagingPoint(Imaging imIn, ModeID mode, const void *table) { /* lookup table transform */ ImagingSectionCookie cookie; @@ -140,7 +140,7 @@ ImagingPoint(Imaging imIn, const Mode *mode, const void *table) { return (Imaging)ImagingError_ModeError(); } - if (!mode) { + if (mode == IMAGING_MODE_UNKNOWN) { mode = imIn->mode; } diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 38142b7c5e1..c09062c92e5 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -42,7 +42,7 @@ */ Imaging -ImagingNewPrologueSubtype(const Mode *mode, int xsize, int ysize, int size) { +ImagingNewPrologueSubtype(const ModeID mode, int xsize, int ysize, int size) { Imaging im; /* linesize overflow check, roughly the current largest space req'd */ @@ -256,7 +256,7 @@ ImagingNewPrologueSubtype(const Mode *mode, int xsize, int ysize, int size) { } Imaging -ImagingNewPrologue(const Mode *mode, int xsize, int ysize) { +ImagingNewPrologue(const ModeID mode, int xsize, int ysize) { return ImagingNewPrologueSubtype( mode, xsize, ysize, sizeof(struct ImagingMemoryInstance) ); @@ -593,7 +593,7 @@ ImagingBorrowArrow( */ Imaging -ImagingNewInternal(const Mode *mode, int xsize, int ysize, int dirty) { +ImagingNewInternal(const ModeID mode, int xsize, int ysize, int dirty) { Imaging im; if (xsize < 0 || ysize < 0) { @@ -629,7 +629,7 @@ ImagingNewInternal(const Mode *mode, int xsize, int ysize, int dirty) { } Imaging -ImagingNew(const Mode *mode, int xsize, int ysize) { +ImagingNew(const ModeID mode, int xsize, int ysize) { if (ImagingDefaultArena.use_block_allocator) { return ImagingNewBlock(mode, xsize, ysize); } @@ -637,7 +637,7 @@ ImagingNew(const Mode *mode, int xsize, int ysize) { } Imaging -ImagingNewDirty(const Mode *mode, int xsize, int ysize) { +ImagingNewDirty(const ModeID mode, int xsize, int ysize) { if (ImagingDefaultArena.use_block_allocator) { return ImagingNewBlock(mode, xsize, ysize); } @@ -645,7 +645,7 @@ ImagingNewDirty(const Mode *mode, int xsize, int ysize) { } Imaging -ImagingNewBlock(const Mode *mode, int xsize, int ysize) { +ImagingNewBlock(const ModeID mode, int xsize, int ysize) { Imaging im; if (xsize < 0 || ysize < 0) { @@ -667,7 +667,7 @@ ImagingNewBlock(const Mode *mode, int xsize, int ysize) { Imaging ImagingNewArrow( - const Mode *mode, + const ModeID mode, int xsize, int ysize, PyObject *schema_capsule, @@ -740,7 +740,7 @@ ImagingNewArrow( } Imaging -ImagingNew2Dirty(const Mode *mode, Imaging imOut, Imaging imIn) { +ImagingNew2Dirty(const ModeID mode, Imaging imOut, Imaging imIn) { /* allocate or validate output image */ if (imOut) { diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 40e8fba505f..f987c608f8a 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -640,7 +640,7 @@ ImagingLibTiffDecode( ); TRACE( ("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", - im->mode->name, + getModeData(im->mode)->name, im->type, im->bands, im->xsize, @@ -987,7 +987,7 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt ); TRACE( ("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", - im->mode->name, + getModeData(im->mode)->name, im->type, im->bands, im->xsize, diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 78760215189..8d4bb86190a 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1313,7 +1313,7 @@ copy4skip2(UINT8 *_out, const UINT8 *in, int pixels) { /* Unpack to "I" and "F" images */ #define UNPACK_RAW(NAME, GET, INTYPE, OUTTYPE) \ - static void NAME(UINT8 *out_, const UINT8 *in, int pixels) { \ + static void NAME(UINT8 *out, const UINT8 *in, int pixels) { \ int i; \ OUTTYPE *out = (OUTTYPE *)out_; \ for (i = 0; i < pixels; i++, in += sizeof(INTYPE)) { \ @@ -1322,7 +1322,7 @@ copy4skip2(UINT8 *_out, const UINT8 *in, int pixels) { } #define UNPACK(NAME, COPY, INTYPE, OUTTYPE) \ - static void NAME(UINT8 *out_, const UINT8 *in, int pixels) { \ + static void NAME(UINT8 *out, const UINT8 *in, int pixels) { \ int i; \ OUTTYPE *out = (OUTTYPE *)out_; \ INTYPE tmp_; \ @@ -1541,13 +1541,12 @@ band316L(UINT8 *out, const UINT8 *in, int pixels) { } } -static struct Unpacker { - const Mode *mode; - const RawMode *rawmode; +static struct { + const ModeID mode; + const RawModeID rawmode; int bits; ImagingShuffler unpack; } unpackers[] = { - /* raw mode syntax is ";" where "bits" defaults depending on mode (1 for "1", 8 for "P" and "L", etc), and "flags" should be given in alphabetical order. if both bits @@ -1560,297 +1559,294 @@ static struct Unpacker { /* exception: rawmodes "I" and "F" are always native endian byte order */ /* bilevel */ - {"1", "1", 1, unpack1}, - {"1", "1;I", 1, unpack1I}, - {"1", "1;R", 1, unpack1R}, - {"1", "1;IR", 1, unpack1IR}, - {"1", "1;8", 8, unpack18}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1, 1, unpack1}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_I, 1, unpack1I}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_R, 1, unpack1R}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_IR, 1, unpack1IR}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_8, 8, unpack18}, /* grayscale */ - {"L", "L;2", 2, unpackL2}, - {"L", "L;2I", 2, unpackL2I}, - {"L", "L;2R", 2, unpackL2R}, - {"L", "L;2IR", 2, unpackL2IR}, - - {"L", "L;4", 4, unpackL4}, - {"L", "L;4I", 4, unpackL4I}, - {"L", "L;4R", 4, unpackL4R}, - {"L", "L;4IR", 4, unpackL4IR}, - - {"L", "L", 8, copy1}, - {"L", "L;I", 8, unpackLI}, - {"L", "L;R", 8, unpackLR}, - {"L", "L;16", 16, unpackL16}, - {"L", "L;16B", 16, unpackL16B}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_2, 2, unpackL2}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_2I, 2, unpackL2I}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_2R, 2, unpackL2R}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_2IR, 2, unpackL2IR}, + + {IMAGING_MODE_L, IMAGING_RAWMODE_L_4, 4, unpackL4}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_4I, 4, unpackL4I}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_4R, 4, unpackL4R}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_4IR, 4, unpackL4IR}, + + {IMAGING_MODE_L, IMAGING_RAWMODE_L, 8, copy1}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_I, 8, unpackLI}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_R, 8, unpackLR}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_16, 16, unpackL16}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_16B, 16, unpackL16B}, /* grayscale w. alpha */ - {"LA", "LA", 16, unpackLA}, - {"LA", "LA;L", 16, unpackLAL}, + {IMAGING_MODE_LA, IMAGING_RAWMODE_LA, 16, unpackLA}, + {IMAGING_MODE_LA, IMAGING_RAWMODE_LA_L, 16, unpackLAL}, /* grayscale w. alpha premultiplied */ - {"La", "La", 16, unpackLA}, + {IMAGING_MODE_La, IMAGING_RAWMODE_La, 16, unpackLA}, /* palette */ - {"P", "P;1", 1, unpackP1}, - {"P", "P;2", 2, unpackP2}, - {"P", "P;2L", 2, unpackP2L}, - {"P", "P;4", 4, unpackP4}, - {"P", "P;4L", 4, unpackP4L}, - {"P", "P", 8, copy1}, - {"P", "P;R", 8, unpackLR}, - {"P", "L", 8, copy1}, - {"P", "PX", 16, unpackL16B}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_1, 1, unpackP1}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_2, 2, unpackP2}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_2L, 2, unpackP2L}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_4, 4, unpackP4}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_4L, 4, unpackP4L}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P, 8, copy1}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_R, 8, unpackLR}, + {IMAGING_MODE_P, IMAGING_RAWMODE_L, 8, copy1}, + {IMAGING_MODE_P, IMAGING_RAWMODE_PX, 16, unpackL16B}, /* palette w. alpha */ - {"PA", "PA", 16, unpackLA}, - {"PA", "PA;L", 16, unpackLAL}, - {"PA", "LA", 16, unpackLA}, + {IMAGING_MODE_PA, IMAGING_RAWMODE_PA, 16, unpackLA}, + {IMAGING_MODE_PA, IMAGING_RAWMODE_PA_L, 16, unpackLAL}, + {IMAGING_MODE_PA, IMAGING_RAWMODE_LA, 16, unpackLA}, /* true colour */ - {"RGB", "RGB", 24, ImagingUnpackRGB}, - {"RGB", "RGB;L", 24, unpackRGBL}, - {"RGB", "RGB;R", 24, unpackRGBR}, - {"RGB", "RGB;16L", 48, unpackRGB16L}, - {"RGB", "RGB;16B", 48, unpackRGB16B}, - {"RGB", "BGR", 24, ImagingUnpackBGR}, - {"RGB", "RGB;15", 16, ImagingUnpackRGB15}, - {"RGB", "BGR;15", 16, ImagingUnpackBGR15}, - {"RGB", "RGB;16", 16, ImagingUnpackRGB16}, - {"RGB", "BGR;16", 16, ImagingUnpackBGR16}, - {"RGB", "RGBX;16L", 64, unpackRGBA16L}, - {"RGB", "RGBX;16B", 64, unpackRGBA16B}, - {"RGB", "RGB;4B", 16, ImagingUnpackRGB4B}, - {"RGB", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */ - {"RGB", "RGBX", 32, copy4}, - {"RGB", "RGBX;L", 32, unpackRGBAL}, - {"RGB", "RGBXX", 40, copy4skip1}, - {"RGB", "RGBXXX", 48, copy4skip2}, - {"RGB", "RGBA;L", 32, unpackRGBAL}, - {"RGB", "RGBA;15", 16, ImagingUnpackRGBA15}, - {"RGB", "BGRX", 32, ImagingUnpackBGRX}, - {"RGB", "BGXR", 32, ImagingUnpackBGXR}, - {"RGB", "XRGB", 32, ImagingUnpackXRGB}, - {"RGB", "XBGR", 32, ImagingUnpackXBGR}, - {"RGB", "YCC;P", 24, ImagingUnpackYCC}, - {"RGB", "R", 8, band0}, - {"RGB", "G", 8, band1}, - {"RGB", "B", 8, band2}, - {"RGB", "R;16L", 16, band016L}, - {"RGB", "G;16L", 16, band116L}, - {"RGB", "B;16L", 16, band216L}, - {"RGB", "R;16B", 16, band016B}, - {"RGB", "G;16B", 16, band116B}, - {"RGB", "B;16B", 16, band216B}, - {"RGB", "CMYK", 32, cmyk2rgb}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB, 24, ImagingUnpackRGB}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_L, 24, unpackRGBL}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_R, 24, unpackRGBR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16L, 48, unpackRGB16L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16B, 48, unpackRGB16B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR, 24, ImagingUnpackBGR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_15, 16, ImagingUnpackRGB15}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR_15, 16, ImagingUnpackBGR15}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16, 16, ImagingUnpackRGB16}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR_16, 16, ImagingUnpackBGR16}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_16L, 64, unpackRGBA16L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_16B, 64, unpackRGBA16B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_4B, 16, ImagingUnpackRGB4B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR_5, 16, ImagingUnpackBGR15}, /* compat */ + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX, 32, copy4}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_L, 32, unpackRGBAL}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBXX, 40, copy4skip1}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBXXX, 48, copy4skip2}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBA_L, 32, unpackRGBAL}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBA_15, 16, ImagingUnpackRGBA15}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGRX, 32, ImagingUnpackBGRX}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGXR, 32, ImagingUnpackBGXR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_XRGB, 32, ImagingUnpackXRGB}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_XBGR, 32, ImagingUnpackXBGR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_YCC_P, 24, ImagingUnpackYCC}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16L, 16, band016L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16L, 16, band116L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16L, 16, band216L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16B, 16, band016B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16B, 16, band116B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16B, 16, band216B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_CMYK, 32, cmyk2rgb}, /* true colour w. alpha */ - {"RGBA", "LA", 16, unpackRGBALA}, - {"RGBA", "LA;16B", 32, unpackRGBALA16B}, - {"RGBA", "RGBA", 32, copy4}, - {"RGBA", "RGBAX", 40, copy4skip1}, - {"RGBA", "RGBAXX", 48, copy4skip2}, - {"RGBA", "RGBa", 32, unpackRGBa}, - {"RGBA", "RGBaX", 40, unpackRGBaskip1}, - {"RGBA", "RGBaXX", 48, unpackRGBaskip2}, - {"RGBA", "RGBa;16L", 64, unpackRGBa16L}, - {"RGBA", "RGBa;16B", 64, unpackRGBa16B}, - {"RGBA", "BGR", 24, ImagingUnpackBGR}, - {"RGBA", "BGRa", 32, unpackBGRa}, - {"RGBA", "RGBA;I", 32, unpackRGBAI}, - {"RGBA", "RGBA;L", 32, unpackRGBAL}, - {"RGBA", "RGBA;15", 16, ImagingUnpackRGBA15}, - {"RGBA", "BGRA;15", 16, ImagingUnpackBGRA15}, - {"RGBA", "BGRA;15Z", 16, ImagingUnpackBGRA15Z}, - {"RGBA", "RGBA;4B", 16, ImagingUnpackRGBA4B}, - {"RGBA", "RGBA;16L", 64, unpackRGBA16L}, - {"RGBA", "RGBA;16B", 64, unpackRGBA16B}, - {"RGBA", "BGRA", 32, unpackBGRA}, - {"RGBA", "BGRA;16L", 64, unpackBGRA16L}, - {"RGBA", "BGRA;16B", 64, unpackBGRA16B}, - {"RGBA", "BGAR", 32, unpackBGAR}, - {"RGBA", "ARGB", 32, unpackARGB}, - {"RGBA", "ABGR", 32, unpackABGR}, - {"RGBA", "YCCA;P", 32, ImagingUnpackYCCA}, - {"RGBA", "R", 8, band0}, - {"RGBA", "G", 8, band1}, - {"RGBA", "B", 8, band2}, - {"RGBA", "A", 8, band3}, - {"RGBA", "R;16L", 16, band016L}, - {"RGBA", "G;16L", 16, band116L}, - {"RGBA", "B;16L", 16, band216L}, - {"RGBA", "A;16L", 16, band316L}, - {"RGBA", "R;16B", 16, band016B}, - {"RGBA", "G;16B", 16, band116B}, - {"RGBA", "B;16B", 16, band216B}, - {"RGBA", "A;16B", 16, band316B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_LA, 16, unpackRGBALA}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_LA_16B, 32, unpackRGBALA16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA, 32, copy4}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBAX, 40, copy4skip1}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBAXX, 48, copy4skip2}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa, 32, unpackRGBa}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBaX, 40, unpackRGBaskip1}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBaXX, 48, unpackRGBaskip2}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16L, 64, unpackRGBa16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16B, 64, unpackRGBa16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGR, 24, ImagingUnpackBGR}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRa, 32, unpackBGRa}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_I, 32, unpackRGBAI}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_L, 32, unpackRGBAL}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_15, 16, ImagingUnpackRGBA15}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_15, 16, ImagingUnpackBGRA15}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_15Z, 16, ImagingUnpackBGRA15Z}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_4B, 16, ImagingUnpackRGBA4B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16L, 64, unpackRGBA16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16B, 64, unpackRGBA16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA, 32, unpackBGRA}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_16L, 64, unpackBGRA16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_16B, 64, unpackBGRA16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGAR, 32, unpackBGAR}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_ARGB, 32, unpackARGB}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_ABGR, 32, unpackABGR}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_YCCA_P, 32, ImagingUnpackYCCA}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A, 8, band3}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16L, 16, band016L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16L, 16, band116L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16L, 16, band216L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16L, 16, band316L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16B, 16, band016B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16B, 16, band116B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16B, 16, band216B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16B, 16, band316B}, #ifdef WORDS_BIGENDIAN - {"RGB", "RGB;16N", 48, unpackRGB16B}, - {"RGB", "RGBX;16N", 64, unpackRGBA16B}, - {"RGBA", "RGBa;16N", 64, unpackRGBa16B}, - {"RGBA", "RGBA;16N", 64, unpackRGBA16B}, - {"RGBX", "RGBX;16N", 64, unpackRGBA16B}, - {"RGB", "R;16N", 16, band016B}, - {"RGB", "G;16N", 16, band116B}, - {"RGB", "B;16N", 16, band216B}, - - {"RGBA", "R;16N", 16, band016B}, - {"RGBA", "G;16N", 16, band116B}, - {"RGBA", "B;16N", 16, band216B}, - {"RGBA", "A;16N", 16, band316B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16N, 48, unpackRGB16B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_16N, 64, unpackRGBA16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16N, 64, unpackRGBa16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16N, 64, unpackRGBA16B}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16N, 64, unpackRGBA16B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16N, 16, band016B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16N, 16, band116B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16N, 16, band216B}, + + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16N, 16, band016B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16N, 16, band116B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16N, 16, band216B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16N, 16, band316B}, #else - {"RGB", "RGB;16N", 48, unpackRGB16L}, - {"RGB", "RGBX;16N", 64, unpackRGBA16L}, - {"RGBA", "RGBa;16N", 64, unpackRGBa16L}, - {"RGBA", "RGBA;16N", 64, unpackRGBA16L}, - {"RGBX", "RGBX;16N", 64, unpackRGBA16L}, - {"RGB", "R;16N", 16, band016L}, - {"RGB", "G;16N", 16, band116L}, - {"RGB", "B;16N", 16, band216L}, - - {"RGBA", "R;16N", 16, band016L}, - {"RGBA", "G;16N", 16, band116L}, - {"RGBA", "B;16N", 16, band216L}, - {"RGBA", "A;16N", 16, band316L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16N, 48, unpackRGB16L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_16N, 64, unpackRGBA16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16N, 64, unpackRGBa16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16N, 64, unpackRGBA16L}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16N, 64, unpackRGBA16L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16N, 16, band016L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16N, 16, band116L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16N, 16, band216L}, + + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16N, 16, band016L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16N, 16, band116L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16N, 16, band216L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16N, 16, band316L}, #endif /* true colour w. alpha premultiplied */ - {"RGBa", "RGBa", 32, copy4}, - {"RGBa", "BGRa", 32, unpackBGRA}, - {"RGBa", "aRGB", 32, unpackARGB}, - {"RGBa", "aBGR", 32, unpackABGR}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_RGBa, 32, copy4}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_BGRa, 32, unpackBGRA}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_aRGB, 32, unpackARGB}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_aBGR, 32, unpackABGR}, /* true colour w. padding */ - {"RGBX", "RGB", 24, ImagingUnpackRGB}, - {"RGBX", "RGB;L", 24, unpackRGBL}, - {"RGBX", "RGB;16B", 48, unpackRGB16B}, - {"RGBX", "BGR", 24, ImagingUnpackBGR}, - {"RGBX", "RGB;15", 16, ImagingUnpackRGB15}, - {"RGBX", "BGR;15", 16, ImagingUnpackBGR15}, - {"RGBX", "RGB;4B", 16, ImagingUnpackRGB4B}, - {"RGBX", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */ - {"RGBX", "RGBX", 32, copy4}, - {"RGBX", "RGBXX", 40, copy4skip1}, - {"RGBX", "RGBXXX", 48, copy4skip2}, - {"RGBX", "RGBX;L", 32, unpackRGBAL}, - {"RGBX", "RGBX;16L", 64, unpackRGBA16L}, - {"RGBX", "RGBX;16B", 64, unpackRGBA16B}, - {"RGBX", "BGRX", 32, ImagingUnpackBGRX}, - {"RGBX", "XRGB", 32, ImagingUnpackXRGB}, - {"RGBX", "XBGR", 32, ImagingUnpackXBGR}, - {"RGBX", "YCC;P", 24, ImagingUnpackYCC}, - {"RGBX", "R", 8, band0}, - {"RGBX", "G", 8, band1}, - {"RGBX", "B", 8, band2}, - {"RGBX", "X", 8, band3}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB, 24, ImagingUnpackRGB}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_L, 24, unpackRGBL}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_16B, 48, unpackRGB16B}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR, 24, ImagingUnpackBGR}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_15, 16, ImagingUnpackRGB15}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR_15, 16, ImagingUnpackBGR15}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_4B, 16, ImagingUnpackRGB4B}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR_5, 16, ImagingUnpackBGR15}, /* compat */ + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX, 32, copy4}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBXX, 40, copy4skip1}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBXXX, 48, copy4skip2}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_L, 32, unpackRGBAL}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16L, 64, unpackRGBA16L}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16B, 64, unpackRGBA16B}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGRX, 32, ImagingUnpackBGRX}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_XRGB, 32, ImagingUnpackXRGB}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_XBGR, 32, ImagingUnpackXBGR}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_YCC_P, 24, ImagingUnpackYCC}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_X, 8, band3}, /* colour separation */ - {"CMYK", "CMYK", 32, copy4}, - {"CMYK", "CMYKX", 40, copy4skip1}, - {"CMYK", "CMYKXX", 48, copy4skip2}, - {"CMYK", "CMYK;I", 32, unpackCMYKI}, - {"CMYK", "CMYK;L", 32, unpackRGBAL}, - {"CMYK", "CMYK;16L", 64, unpackRGBA16L}, - {"CMYK", "CMYK;16B", 64, unpackRGBA16B}, - {"CMYK", "C", 8, band0}, - {"CMYK", "M", 8, band1}, - {"CMYK", "Y", 8, band2}, - {"CMYK", "K", 8, band3}, - {"CMYK", "C;I", 8, band0I}, - {"CMYK", "M;I", 8, band1I}, - {"CMYK", "Y;I", 8, band2I}, - {"CMYK", "K;I", 8, band3I}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK, 32, copy4}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYKX, 40, copy4skip1}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYKXX, 48, copy4skip2}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_I, 32, unpackCMYKI}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_L, 32, unpackRGBAL}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16L, 64, unpackRGBA16L}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16B, 64, unpackRGBA16B}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_C, 8, band0}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_M, 8, band1}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y, 8, band2}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_K, 8, band3}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_C_I, 8, band0I}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_M_I, 8, band1I}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y_I, 8, band2I}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_K_I, 8, band3I}, #ifdef WORDS_BIGENDIAN - {"CMYK", "CMYK;16N", 64, unpackRGBA16B}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16N, 64, unpackRGBA16B}, #else - {"CMYK", "CMYK;16N", 64, unpackRGBA16L}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16N, 64, unpackRGBA16L}, #endif /* video (YCbCr) */ - {"YCbCr", "YCbCr", 24, ImagingUnpackRGB}, - {"YCbCr", "YCbCr;L", 24, unpackRGBL}, - {"YCbCr", "YCbCrX", 32, copy4}, - {"YCbCr", "YCbCrK", 32, copy4}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr, 24, ImagingUnpackRGB}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr_L, 24, unpackRGBL}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrX, 32, copy4}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrK, 32, copy4}, /* LAB Color */ - {"LAB", "LAB", 24, ImagingUnpackLAB}, - {"LAB", "L", 8, band0}, - {"LAB", "A", 8, band1}, - {"LAB", "B", 8, band2}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_LAB, 24, ImagingUnpackLAB}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_L, 8, band0}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_A, 8, band1}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_B, 8, band2}, /* HSV Color */ - {"HSV", "HSV", 24, ImagingUnpackRGB}, - {"HSV", "H", 8, band0}, - {"HSV", "S", 8, band1}, - {"HSV", "V", 8, band2}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_HSV, 24, ImagingUnpackRGB}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_H, 8, band0}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_S, 8, band1}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_V, 8, band2}, /* integer variations */ - {"I", "I", 32, copy4}, - {"I", "I;8", 8, unpackI8}, - {"I", "I;8S", 8, unpackI8S}, - {"I", "I;16", 16, unpackI16}, - {"I", "I;16S", 16, unpackI16S}, - {"I", "I;16B", 16, unpackI16B}, - {"I", "I;16BS", 16, unpackI16BS}, - {"I", "I;16N", 16, unpackI16N}, - {"I", "I;16NS", 16, unpackI16NS}, - {"I", "I;32", 32, unpackI32}, - {"I", "I;32S", 32, unpackI32S}, - {"I", "I;32B", 32, unpackI32B}, - {"I", "I;32BS", 32, unpackI32BS}, - {"I", "I;32N", 32, unpackI32N}, - {"I", "I;32NS", 32, unpackI32NS}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I, 32, copy4}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_8, 8, unpackI8}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_8S, 8, unpackI8S}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16, 16, unpackI16}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16S, 16, unpackI16S}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16B, 16, unpackI16B}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16BS, 16, unpackI16BS}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16N, 16, unpackI16N}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16NS, 16, unpackI16NS}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32, 32, unpackI32}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32S, 32, unpackI32S}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32B, 32, unpackI32B}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32BS, 32, unpackI32BS}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32N, 32, unpackI32N}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32NS, 32, unpackI32NS}, /* floating point variations */ - {"F", "F", 32, copy4}, - {"F", "F;8", 8, unpackF8}, - {"F", "F;8S", 8, unpackF8S}, - {"F", "F;16", 16, unpackF16}, - {"F", "F;16S", 16, unpackF16S}, - {"F", "F;16B", 16, unpackF16B}, - {"F", "F;16BS", 16, unpackF16BS}, - {"F", "F;16N", 16, unpackF16N}, - {"F", "F;16NS", 16, unpackF16NS}, - {"F", "F;32", 32, unpackF32}, - {"F", "F;32S", 32, unpackF32S}, - {"F", "F;32B", 32, unpackF32B}, - {"F", "F;32BS", 32, unpackF32BS}, - {"F", "F;32N", 32, unpackF32N}, - {"F", "F;32NS", 32, unpackF32NS}, - {"F", "F;32F", 32, unpackF32F}, - {"F", "F;32BF", 32, unpackF32BF}, - {"F", "F;32NF", 32, unpackF32NF}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F, 32, copy4}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_8, 8, unpackF8}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_8S, 8, unpackF8S}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16, 16, unpackF16}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16S, 16, unpackF16S}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16B, 16, unpackF16B}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16BS, 16, unpackF16BS}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16N, 16, unpackF16N}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16NS, 16, unpackF16NS}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32, 32, unpackF32}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32S, 32, unpackF32S}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32B, 32, unpackF32B}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32BS, 32, unpackF32BS}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32N, 32, unpackF32N}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32NS, 32, unpackF32NS}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32F, 32, unpackF32F}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32BF, 32, unpackF32BF}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32NF, 32, unpackF32NF}, #ifdef FLOAT64 - {"F", "F;64F", 64, unpackF64F}, - {"F", "F;64BF", 64, unpackF64BF}, - {"F", "F;64NF", 64, unpackF64NF}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_64F, 64, unpackF64F}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_64BF, 64, unpackF64BF}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_64NF, 64, unpackF64NF}, #endif /* storage modes */ - {"I;16", "I;16", 16, copy2}, - {"I;16B", "I;16B", 16, copy2}, - {"I;16L", "I;16L", 16, copy2}, - {"I;16N", "I;16N", 16, copy2}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16, 16, copy2}, + {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16B, 16, copy2}, + {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16L, 16, copy2}, + {IMAGING_MODE_I_16N, IMAGING_RAWMODE_I_16N, 16, copy2}, - {"I;16", "I;16B", 16, unpackI16B_I16}, - {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. - {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. - {"I;16B", "I;16N", 16, unpackI16N_I16B}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, unpackI16B_I16}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16}, // LibTiff native->image endian. + {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16}, // LibTiff native->image endian. + {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16B}, - {"I;16", "I;16R", 16, unpackI16R_I16}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16R, 16, unpackI16R_I16}, - {"I;16", "I;12", 12, unpackI12_I16}, // 12 bit Tiffs stored in 16bits. + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_12, 12, unpackI12_I16}, // 12 bit Tiffs stored in 16bits. {NULL} /* sentinel */ }; ImagingShuffler -ImagingFindUnpacker(const Mode *mode, const RawMode *rawmode, int *bits_out) { - int i; - - /* find a suitable pixel unpacker */ - for (i = 0; unpackers[i].rawmode; i++) { +ImagingFindUnpacker(const ModeID mode, const RawModeID rawmode, int *bits_out) { + for (size_t i = 0; i < sizeof(unpackers) / sizeof(*unpackers); i++) { if (unpackers[i].mode == mode && unpackers[i].rawmode == rawmode) { if (bits_out) { *bits_out = unpackers[i].bits; @@ -1863,317 +1859,3 @@ ImagingFindUnpacker(const Mode *mode, const RawMode *rawmode, int *bits_out) { return NULL; } - -void -ImagingUnpackInit(void) { - const struct Unpacker temp[] = { - /* raw mode syntax is ";" where "bits" defaults - depending on mode (1 for "1", 8 for "P" and "L", etc), and - "flags" should be given in alphabetical order. if both bits - and flags have their default values, the ; should be left out */ - - /* flags: "I" inverted data; "R" reversed bit order; "B" big - endian byte order (default is little endian); "L" line - interleave, "S" signed, "F" floating point, "Z" inverted alpha */ - - /* exception: rawmodes "I" and "F" are always native endian byte order */ - - /* bilevel */ - {IMAGING_MODE_1, IMAGING_RAWMODE_1, 1, unpack1}, - {IMAGING_MODE_1, IMAGING_RAWMODE_1_I, 1, unpack1I}, - {IMAGING_MODE_1, IMAGING_RAWMODE_1_R, 1, unpack1R}, - {IMAGING_MODE_1, IMAGING_RAWMODE_1_IR, 1, unpack1IR}, - {IMAGING_MODE_1, IMAGING_RAWMODE_1_8, 8, unpack18}, - - /* grayscale */ - {IMAGING_MODE_L, IMAGING_RAWMODE_L_2, 2, unpackL2}, - {IMAGING_MODE_L, IMAGING_RAWMODE_L_2I, 2, unpackL2I}, - {IMAGING_MODE_L, IMAGING_RAWMODE_L_2R, 2, unpackL2R}, - {IMAGING_MODE_L, IMAGING_RAWMODE_L_2IR, 2, unpackL2IR}, - - {IMAGING_MODE_L, IMAGING_RAWMODE_L_4, 4, unpackL4}, - {IMAGING_MODE_L, IMAGING_RAWMODE_L_4I, 4, unpackL4I}, - {IMAGING_MODE_L, IMAGING_RAWMODE_L_4R, 4, unpackL4R}, - {IMAGING_MODE_L, IMAGING_RAWMODE_L_4IR, 4, unpackL4IR}, - - {IMAGING_MODE_L, IMAGING_RAWMODE_L, 8, copy1}, - {IMAGING_MODE_L, IMAGING_RAWMODE_L_I, 8, unpackLI}, - {IMAGING_MODE_L, IMAGING_RAWMODE_L_R, 8, unpackLR}, - {IMAGING_MODE_L, IMAGING_RAWMODE_L_16, 16, unpackL16}, - {IMAGING_MODE_L, IMAGING_RAWMODE_L_16B, 16, unpackL16B}, - - /* grayscale w. alpha */ - {IMAGING_MODE_LA, IMAGING_RAWMODE_LA, 16, unpackLA}, - {IMAGING_MODE_LA, IMAGING_RAWMODE_LA_L, 16, unpackLAL}, - - /* grayscale w. alpha premultiplied */ - {IMAGING_MODE_La, IMAGING_RAWMODE_La, 16, unpackLA}, - - /* palette */ - {IMAGING_MODE_P, IMAGING_RAWMODE_P_1, 1, unpackP1}, - {IMAGING_MODE_P, IMAGING_RAWMODE_P_2, 2, unpackP2}, - {IMAGING_MODE_P, IMAGING_RAWMODE_P_2L, 2, unpackP2L}, - {IMAGING_MODE_P, IMAGING_RAWMODE_P_4, 4, unpackP4}, - {IMAGING_MODE_P, IMAGING_RAWMODE_P_4L, 4, unpackP4L}, - {IMAGING_MODE_P, IMAGING_RAWMODE_P, 8, copy1}, - {IMAGING_MODE_P, IMAGING_RAWMODE_P_R, 8, unpackLR}, - {IMAGING_MODE_P, IMAGING_RAWMODE_L, 8, copy1}, - {IMAGING_MODE_P, IMAGING_RAWMODE_PX, 16, unpackL16B}, - - /* palette w. alpha */ - {IMAGING_MODE_PA, IMAGING_RAWMODE_PA, 16, unpackLA}, - {IMAGING_MODE_PA, IMAGING_RAWMODE_PA_L, 16, unpackLAL}, - {IMAGING_MODE_PA, IMAGING_RAWMODE_LA, 16, unpackLA}, - - /* true colour */ - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB, 24, ImagingUnpackRGB}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_L, 24, unpackRGBL}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_R, 24, unpackRGBR}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16L, 48, unpackRGB16L}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16B, 48, unpackRGB16B}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR, 24, ImagingUnpackBGR}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_15, 16, ImagingUnpackRGB15}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR_15, 16, ImagingUnpackBGR15}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16, 16, ImagingUnpackRGB16}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR_16, 16, ImagingUnpackBGR16}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_16L, 64, unpackRGBA16L}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_16B, 64, unpackRGBA16B}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_4B, 16, ImagingUnpackRGB4B}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR_5, 16, ImagingUnpackBGR15}, /* compat */ - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX, 32, copy4}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_L, 32, unpackRGBAL}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBXX, 40, copy4skip1}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBXXX, 48, copy4skip2}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBA_L, 32, unpackRGBAL}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBA_15, 16, ImagingUnpackRGBA15}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGRX, 32, ImagingUnpackBGRX}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGXR, 32, ImagingUnpackBGXR}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_XRGB, 32, ImagingUnpackXRGB}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_XBGR, 32, ImagingUnpackXBGR}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_YCC_P, 24, ImagingUnpackYCC}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_R, 8, band0}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_G, 8, band1}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_B, 8, band2}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16L, 16, band016L}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16L, 16, band116L}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16L, 16, band216L}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16B, 16, band016B}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16B, 16, band116B}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16B, 16, band216B}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_CMYK, 32, cmyk2rgb}, - - {IMAGING_MODE_BGR_15, IMAGING_RAWMODE_BGR_15, 16, copy2}, - {IMAGING_MODE_BGR_16, IMAGING_RAWMODE_BGR_16, 16, copy2}, - {IMAGING_MODE_BGR_24, IMAGING_RAWMODE_BGR_24, 24, copy3}, - - /* true colour w. alpha */ - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_LA, 16, unpackRGBALA}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_LA_16B, 32, unpackRGBALA16B}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA, 32, copy4}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBAX, 40, copy4skip1}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBAXX, 48, copy4skip2}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa, 32, unpackRGBa}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBaX, 40, unpackRGBaskip1}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBaXX, 48, unpackRGBaskip2}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16L, 64, unpackRGBa16L}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16B, 64, unpackRGBa16B}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRa, 32, unpackBGRa}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_I, 32, unpackRGBAI}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_L, 32, unpackRGBAL}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_15, 16, ImagingUnpackRGBA15}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_15, 16, ImagingUnpackBGRA15}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_15Z, 16, ImagingUnpackBGRA15Z}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_4B, 16, ImagingUnpackRGBA4B}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16L, 64, unpackRGBA16L}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16B, 64, unpackRGBA16B}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA, 32, unpackBGRA}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_16L, 64, unpackBGRA16L}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_16B, 64, unpackBGRA16B}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGAR, 32, unpackBGAR}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_ARGB, 32, unpackARGB}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_ABGR, 32, unpackABGR}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_YCCA_P, 32, ImagingUnpackYCCA}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R, 8, band0}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G, 8, band1}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B, 8, band2}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A, 8, band3}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16L, 16, band016L}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16L, 16, band116L}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16L, 16, band216L}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16L, 16, band316L}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16B, 16, band016B}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16B, 16, band116B}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16B, 16, band216B}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16B, 16, band316B}, - -#ifdef WORDS_BIGENDIAN - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16N, 48, unpackRGB16B}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16N, 64, unpackRGBa16B}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16N, 64, unpackRGBA16B}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16N, 64, unpackRGBA16B}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16N, 16, band016B}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16N, 16, band116B}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16N, 16, band216B}, - - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16N, 16, band016B}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16N, 16, band116B}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16N, 16, band216B}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16N, 16, band316B}, -#else - {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16N, 48, unpackRGB16L}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16N, 64, unpackRGBa16L}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16N, 64, unpackRGBA16L}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16N, 64, unpackRGBA16L}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16N, 16, band016L}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16N, 16, band116L}, - {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16N, 16, band216L}, - - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16N, 16, band016L}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16N, 16, band116L}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16N, 16, band216L}, - {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16N, 16, band316L}, -#endif - - /* true colour w. alpha premultiplied */ - {IMAGING_MODE_RGBa, IMAGING_RAWMODE_RGBa, 32, copy4}, - {IMAGING_MODE_RGBa, IMAGING_RAWMODE_BGRa, 32, unpackBGRA}, - {IMAGING_MODE_RGBa, IMAGING_RAWMODE_aRGB, 32, unpackARGB}, - {IMAGING_MODE_RGBa, IMAGING_RAWMODE_aBGR, 32, unpackABGR}, - - /* true colour w. padding */ - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB, 24, ImagingUnpackRGB}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_L, 24, unpackRGBL}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_16B, 48, unpackRGB16B}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR, 24, ImagingUnpackBGR}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_15, 16, ImagingUnpackRGB15}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR_15, 16, ImagingUnpackBGR15}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_4B, 16, ImagingUnpackRGB4B}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR_5, 16, ImagingUnpackBGR15}, /* compat */ - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX, 32, copy4}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBXX, 40, copy4skip1}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBXXX, 48, copy4skip2}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_L, 32, unpackRGBAL}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16L, 64, unpackRGBA16L}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16B, 64, unpackRGBA16B}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGRX, 32, ImagingUnpackBGRX}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_XRGB, 32, ImagingUnpackXRGB}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_XBGR, 32, ImagingUnpackXBGR}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_YCC_P, 24, ImagingUnpackYCC}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_R, 8, band0}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_G, 8, band1}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_B, 8, band2}, - {IMAGING_MODE_RGBX, IMAGING_RAWMODE_X, 8, band3}, - - /* colour separation */ - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK, 32, copy4}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYKX, 40, copy4skip1}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYKXX, 48, copy4skip2}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_I, 32, unpackCMYKI}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_L, 32, unpackRGBAL}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16L, 64, unpackRGBA16L}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16B, 64, unpackRGBA16B}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_C, 8, band0}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_M, 8, band1}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y, 8, band2}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_K, 8, band3}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_C_I, 8, band0I}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_M_I, 8, band1I}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y_I, 8, band2I}, - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_K_I, 8, band3I}, - -#ifdef WORDS_BIGENDIAN - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16N, 64, unpackRGBA16B}, -#else - {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16N, 64, unpackRGBA16L}, -#endif - - /* video (YCbCr) */ - {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr, 24, ImagingUnpackRGB}, - {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr_L, 24, unpackRGBL}, - {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrX, 32, copy4}, - {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrK, 32, copy4}, - - /* LAB Color */ - {IMAGING_MODE_LAB, IMAGING_RAWMODE_LAB, 24, ImagingUnpackLAB}, - {IMAGING_MODE_LAB, IMAGING_RAWMODE_L, 8, band0}, - {IMAGING_MODE_LAB, IMAGING_RAWMODE_A, 8, band1}, - {IMAGING_MODE_LAB, IMAGING_RAWMODE_B, 8, band2}, - - /* HSV Color */ - {IMAGING_MODE_HSV, IMAGING_RAWMODE_HSV, 24, ImagingUnpackRGB}, - {IMAGING_MODE_HSV, IMAGING_RAWMODE_H, 8, band0}, - {IMAGING_MODE_HSV, IMAGING_RAWMODE_S, 8, band1}, - {IMAGING_MODE_HSV, IMAGING_RAWMODE_V, 8, band2}, - - /* integer variations */ - {IMAGING_MODE_I, IMAGING_RAWMODE_I, 32, copy4}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_8, 8, unpackI8}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_8S, 8, unpackI8S}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_16, 16, unpackI16}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_16S, 16, unpackI16S}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_16B, 16, unpackI16B}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_16BS, 16, unpackI16BS}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_16N, 16, unpackI16N}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_16NS, 16, unpackI16NS}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_32, 32, unpackI32}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_32S, 32, unpackI32S}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_32B, 32, unpackI32B}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_32BS, 32, unpackI32BS}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_32N, 32, unpackI32N}, - {IMAGING_MODE_I, IMAGING_RAWMODE_I_32NS, 32, unpackI32NS}, - - /* floating point variations */ - {IMAGING_MODE_F, IMAGING_RAWMODE_F, 32, copy4}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_8, 8, unpackF8}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_8S, 8, unpackF8S}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_16, 16, unpackF16}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_16S, 16, unpackF16S}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_16B, 16, unpackF16B}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_16BS, 16, unpackF16BS}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_16N, 16, unpackF16N}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_16NS, 16, unpackF16NS}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_32, 32, unpackF32}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_32S, 32, unpackF32S}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_32B, 32, unpackF32B}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_32BS, 32, unpackF32BS}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_32N, 32, unpackF32N}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_32NS, 32, unpackF32NS}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_32F, 32, unpackF32F}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_32BF, 32, unpackF32BF}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_32NF, 32, unpackF32NF}, -#ifdef FLOAT64 - {IMAGING_MODE_F, IMAGING_RAWMODE_F_64F, 64, unpackF64F}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_64BF, 64, unpackF64BF}, - {IMAGING_MODE_F, IMAGING_RAWMODE_F_64NF, 64, unpackF64NF}, -#endif - - /* storage modes */ - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16, 16, copy2}, - {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16B, 16, copy2}, - {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16L, 16, copy2}, - {IMAGING_MODE_I_16N, IMAGING_RAWMODE_I_16N, 16, copy2}, - - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, unpackI16B_I16}, - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16}, // LibTiff native->image endian. - {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16}, // LibTiff native->image endian. - {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16B}, - - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16R, 16, unpackI16R_I16}, - - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_12, 12, unpackI12_I16}, // 12 bit Tiffs stored in 16bits. - - {NULL} /* sentinel */ - }; - unpackers = malloc(sizeof(temp)); - if (unpackers == NULL) { - fprintf(stderr, "UnpackInit: failed to allocate memory for unpackers table\n"); - exit(1); - } - memcpy(unpackers, temp, sizeof(temp)); -} - -void -ImagingUnpackFree(void) { - free(unpackers); - unpackers = NULL; -} diff --git a/src/map.c b/src/map.c index 451cca58973..6f66b0cc57b 100644 --- a/src/map.c +++ b/src/map.c @@ -82,7 +82,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { return NULL; } - const Mode * const mode = findMode(mode_name); + const ModeID mode = findModeID(mode_name); if (stride <= 0) { if (mode == IMAGING_MODE_L || mode == IMAGING_MODE_P) { From 4d721bc5913986b31f451decb03f094c9f21a908 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 23 Apr 2024 12:41:49 -0500 Subject: [PATCH 1938/2374] use mode enums in _webp.c --- src/_webp.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index e84e786edfe..d065e329c6b 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -89,8 +89,8 @@ HandleMuxError(WebPMuxError err, char *chunk) { static int import_frame_libwebp(WebPPicture *frame, Imaging im) { - if (strcmp(im->mode, "RGBA") && strcmp(im->mode, "RGB") && - strcmp(im->mode, "RGBX")) { + if (im->mode != IMAGING_MODE_RGBA && im->mode != IMAGING_MODE_RGB && + im->mode != IMAGING_MODE_RGBX) { PyErr_SetString(PyExc_ValueError, "unsupported image mode"); return -1; } @@ -104,7 +104,7 @@ import_frame_libwebp(WebPPicture *frame, Imaging im) { return -2; } - int ignore_fourth_channel = strcmp(im->mode, "RGBA"); + int ignore_fourth_channel = im->mode != IMAGING_MODE_RGBA; for (int y = 0; y < im->ysize; ++y) { UINT8 *src = (UINT8 *)im->image32[y]; UINT32 *dst = frame->argb + frame->argb_stride * y; @@ -143,7 +143,7 @@ typedef struct { PyObject_HEAD WebPAnimDecoder *dec; WebPAnimInfo info; WebPData data; - char *mode; + ModeID mode; } WebPAnimDecoderObject; static PyTypeObject WebPAnimDecoder_Type; @@ -396,7 +396,7 @@ _anim_decoder_new(PyObject *self, PyObject *args) { const uint8_t *webp; Py_ssize_t size; WebPData webp_src; - char *mode; + ModeID mode; WebPDecoderConfig config; WebPAnimDecoderObject *decp = NULL; WebPAnimDecoder *dec = NULL; @@ -409,10 +409,10 @@ _anim_decoder_new(PyObject *self, PyObject *args) { webp_src.size = size; // Sniff the mode, since the decoder API doesn't tell us - mode = "RGBA"; + mode = IMAGING_MODE_RGBA; if (WebPGetFeatures(webp, size, &config.input) == VP8_STATUS_OK) { if (!config.input.has_alpha) { - mode = "RGBX"; + mode = IMAGING_MODE_RGBX; } } @@ -455,7 +455,7 @@ _anim_decoder_get_info(PyObject *self) { info->loop_count, info->bgcolor, info->frame_count, - decp->mode + getModeData(decp->mode)->name ); } From 85212dbbb6400255a0522a99ce7fa18a40ea3f81 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sat, 19 Jul 2025 16:55:52 +0200 Subject: [PATCH 1939/2374] Add image band metadata for the 4 channel images --- Tests/test_pyarrow.py | 27 +++++++++++++ src/libImaging/Arrow.c | 86 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index 8dad94fe035..a69504e78a0 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from typing import Any, NamedTuple import pytest @@ -244,3 +245,29 @@ def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) _test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel) + + +@pytest.mark.parametrize( + "mode, metadata", + ( + ("LA", ["L", "X", "X", "A"]), + ("RGB", ["R", "G", "B", "X"]), + ("RGBX", ["R", "G", "B", "X"]), + ("RGBA", ["R", "G", "B", "A"]), + ("CMYK", ["C", "M", "Y", "K"]), + ("YCbCr", ["Y", "Cb", "Cr", "X"]), + ("HSV", ["H", "S", "V", "X"]), + ), +) +def test_image_metadata(mode: str, metadata: list[str]) -> None: + img = hopper(mode) + + arr = pyarrow.array(img) # type: ignore[call-overload] + + assert arr.type.field(0).metadata + assert arr.type.field(0).metadata[b"image"] + + parsed_metadata = json.loads(arr.type.field(0).metadata[b"image"].decode("utf8")) + + assert "bands" in parsed_metadata + assert parsed_metadata["bands"] == metadata diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index ccafe33b97b..2ecec9b29bb 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -55,6 +55,77 @@ ReleaseExportedSchema(struct ArrowSchema *array) { // Mark array released array->release = NULL; } +char * +image_band_json(Imaging im) { + char *format = "{\"bands\": [\"%s\", \"%s\", \"%s\", \"%s\"]}"; + char *json; + // Bands can be 4 bands * 2 characters each + int len = strlen(format) + 8 + 1; + int err; + + json = calloc(1, len); + + if (!json) { + return NULL; + } + + err = PyOS_snprintf( + json, + len, + format, + im->band_names[0], + im->band_names[1], + im->band_names[2], + im->band_names[3] + ); + if (err < 0) { + return NULL; + } + return json; +} + +char * +assemble_metadata(const char *band_json) { + /* format is + int32: number of key/value pairs (noted N below) + int32: byte length of key 0 + key 0 (not null-terminated) + int32: byte length of value 0 + value 0 (not null-terminated) + ... + int32: byte length of key N - 1 + key N - 1 (not null-terminated) + int32: byte length of value N - 1 + value N - 1 (not null-terminated) + */ + const char *key = "image"; + INT32 key_len = strlen(key); + INT32 band_json_len = strlen(band_json); + + char *buf; + INT32 *dest_int; + char *dest; + + buf = calloc(1, key_len + band_json_len + 4 + 1 * 8); + if (!buf) { + return NULL; + } + + dest_int = (void *)buf; + + dest_int[0] = 1; + dest_int[1] = key_len; + dest_int += 2; + dest = (void *)dest_int; + memcpy(dest, key, key_len); + dest += key_len; + dest_int = (void *)dest; + dest_int[0] = band_json_len; + dest_int += 1; + memcpy(dest_int, band_json, band_json_len); + + return buf; +} int export_named_type(struct ArrowSchema *schema, char *format, char *name) { @@ -95,6 +166,8 @@ export_named_type(struct ArrowSchema *schema, char *format, char *name) { int export_imaging_schema(Imaging im, struct ArrowSchema *schema) { int retval = 0; + char *metadata; + char *band_json; if (strcmp(im->arrow_band_format, "") == 0) { return IMAGING_ARROW_INCOMPATIBLE_MODE; @@ -117,13 +190,24 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) { schema->n_children = 1; schema->children = calloc(1, sizeof(struct ArrowSchema *)); schema->children[0] = (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema)); - retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel"); + retval = export_named_type(schema->children[0], im->arrow_band_format, im->mode); if (retval != 0) { free(schema->children[0]); free(schema->children); schema->release(schema); return retval; } + + // band related metadata + band_json = image_band_json(im); + if (band_json) { + // adding the metadata to the child array. + // Accessible in pyarrow via pa.array(img).type.field(0).metadata + // adding it to the top level is not accessible. + schema->children[0]->metadata = assemble_metadata(band_json); + free(band_json); + } + return 0; } From aa39e84f7a465c91035d5ea0a3d522faf9f9159d Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 16:58:08 +0200 Subject: [PATCH 1940/2374] use mode enums in Jpeg2KDecode.c --- src/libImaging/Jpeg2KDecode.c | 44 +++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index 67f705ddd59..1b496f45ec0 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -71,7 +71,7 @@ typedef void (*j2k_unpacker_t)( ); struct j2k_decode_unpacker { - const char *mode; + const ModeID mode; OPJ_COLOR_SPACE color_space; unsigned components; /* bool indicating if unpacker supports subsampling */ @@ -599,26 +599,26 @@ j2ku_sycca_rgba( } static const struct j2k_decode_unpacker j2k_unpackers[] = { - {"L", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_l}, - {"P", OPJ_CLRSPC_SRGB, 1, 0, j2ku_gray_l}, - {"PA", OPJ_CLRSPC_SRGB, 2, 0, j2ku_graya_la}, - {"I;16", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, - {"I;16B", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, - {"LA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, - {"RGB", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, - {"RGB", OPJ_CLRSPC_GRAY, 2, 0, j2ku_gray_rgb}, - {"RGB", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, - {"RGB", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, - {"RGB", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgb_rgb}, - {"RGB", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycc_rgb}, - {"RGBA", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, - {"RGBA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, - {"RGBA", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, - {"RGBA", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, - {"RGBA", OPJ_CLRSPC_GRAY, 4, 1, j2ku_srgba_rgba}, - {"RGBA", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgba_rgba}, - {"RGBA", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycca_rgba}, - {"CMYK", OPJ_CLRSPC_CMYK, 4, 1, j2ku_srgba_rgba}, + {IMAGING_MODE_L, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_l}, + {IMAGING_MODE_P, OPJ_CLRSPC_SRGB, 1, 0, j2ku_gray_l}, + {IMAGING_MODE_PA, OPJ_CLRSPC_SRGB, 2, 0, j2ku_graya_la}, + {IMAGING_MODE_I_16, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, + {IMAGING_MODE_I_16B, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, + {IMAGING_MODE_LA, OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, + {IMAGING_MODE_RGB, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, + {IMAGING_MODE_RGB, OPJ_CLRSPC_GRAY, 2, 0, j2ku_gray_rgb}, + {IMAGING_MODE_RGB, OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, + {IMAGING_MODE_RGB, OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, + {IMAGING_MODE_RGB, OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgb_rgb}, + {IMAGING_MODE_RGB, OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycc_rgb}, + {IMAGING_MODE_RGBA, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, + {IMAGING_MODE_RGBA, OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, + {IMAGING_MODE_RGBA, OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, + {IMAGING_MODE_RGBA, OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, + {IMAGING_MODE_RGBA, OPJ_CLRSPC_GRAY, 4, 1, j2ku_srgba_rgba}, + {IMAGING_MODE_RGBA, OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgba_rgba}, + {IMAGING_MODE_RGBA, OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycca_rgba}, + {IMAGING_MODE_CMYK, OPJ_CLRSPC_CMYK, 4, 1, j2ku_srgba_rgba}, }; /* -------------------------------------------------------------------- */ @@ -771,7 +771,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { if (color_space == j2k_unpackers[n].color_space && image->numcomps == j2k_unpackers[n].components && (j2k_unpackers[n].subsampling || (subsampling == -1)) && - strcmp(getModeData(im->mode)->name, j2k_unpackers[n].mode) == 0) { + im->mode == j2k_unpackers[n].mode) { unpack = j2k_unpackers[n].unpacker; break; } From a53f83f023d3a4a166cc1107cb16aa5f0cc0f2c9 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 23 Apr 2024 13:13:19 -0500 Subject: [PATCH 1941/2374] use mode enums in _imagingft.c --- src/_imagingft.c | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 29d8e9e7112..a38ea507a33 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -525,7 +525,7 @@ font_getlength(FontObject *self, PyObject *args) { int horizontal_dir; /* is primary axis horizontal? */ int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int color = 0; /* is FT_LOAD_COLOR enabled? */ - const char *mode = NULL; + const char *mode_name = NULL; const char *dir = NULL; const char *lang = NULL; PyObject *features = Py_None; @@ -534,15 +534,16 @@ font_getlength(FontObject *self, PyObject *args) { /* calculate size and bearing for a given string */ if (!PyArg_ParseTuple( - args, "O|zzOz:getlength", &string, &mode, &dir, &features, &lang + args, "O|zzOz:getlength", &string, &mode_name, &dir, &features, &lang )) { return NULL; } horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - mask = mode && strcmp(mode, "1") == 0; - color = mode && strcmp(mode, "RGBA") == 0; + const ModeID mode = findModeID(mode_name); + mask = mode == IMAGING_MODE_1; + color = mode == IMAGING_MODE_RGBA; count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); if (PyErr_Occurred()) { @@ -754,7 +755,7 @@ font_getsize(FontObject *self, PyObject *args) { int horizontal_dir; /* is primary axis horizontal? */ int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int color = 0; /* is FT_LOAD_COLOR enabled? */ - const char *mode = NULL; + const char *mode_name = NULL; const char *dir = NULL; const char *lang = NULL; const char *anchor = NULL; @@ -764,15 +765,23 @@ font_getsize(FontObject *self, PyObject *args) { /* calculate size and bearing for a given string */ if (!PyArg_ParseTuple( - args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor + args, + "O|zzOzz:getsize", + &string, + &mode_name, + &dir, + &features, + &lang, + &anchor )) { return NULL; } horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - mask = mode && strcmp(mode, "1") == 0; - color = mode && strcmp(mode, "RGBA") == 0; + const ModeID mode = findModeID(mode_name); + mask = mode == IMAGING_MODE_1; + color = mode == IMAGING_MODE_RGBA; count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); if (PyErr_Occurred()) { @@ -839,7 +848,7 @@ font_render(FontObject *self, PyObject *args) { int stroke_filled = 0; PY_LONG_LONG foreground_ink_long = 0; unsigned int foreground_ink; - const char *mode = NULL; + const char *mode_name = NULL; const char *dir = NULL; const char *lang = NULL; const char *anchor = NULL; @@ -859,7 +868,7 @@ font_render(FontObject *self, PyObject *args) { "OO|zzOzfpzL(ff):render", &string, &fill, - &mode, + &mode_name, &dir, &features, &lang, @@ -873,8 +882,9 @@ font_render(FontObject *self, PyObject *args) { return NULL; } - mask = mode && strcmp(mode, "1") == 0; - color = mode && strcmp(mode, "RGBA") == 0; + const ModeID mode = findModeID(mode_name); + mask = mode == IMAGING_MODE_1; + color = mode == IMAGING_MODE_RGBA; foreground_ink = foreground_ink_long; From f8bfa2fe4e78fc5ea32844a0bd2ded59d2ef398d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 23 Apr 2024 13:19:49 -0500 Subject: [PATCH 1942/2374] use more mode enums in decode.c --- src/decode.c | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/decode.c b/src/decode.c index 41b2f6f3167..f95637fa619 100644 --- a/src/decode.c +++ b/src/decode.c @@ -291,17 +291,18 @@ PyObject * PyImaging_BitDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; + const char *mode_name; int bits = 8; int pad = 8; int fill = 0; int sign = 0; int ystep = 1; - if (!PyArg_ParseTuple(args, "s|iiiii", &mode, &bits, &pad, &fill, &sign, &ystep)) { + if (!PyArg_ParseTuple(args, "s|iiiii", &mode_name, &bits, &pad, &fill, &sign, &ystep)) { return NULL; } - if (strcmp(mode, "F") != 0) { + const ModeID mode = findModeID(mode_name); + if (mode != IMAGING_MODE_F) { PyErr_SetString(PyExc_ValueError, "bad image mode"); return NULL; } @@ -331,34 +332,36 @@ PyObject * PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *actual; + char *mode_name; int n = 0; char *pixel_format = ""; - if (!PyArg_ParseTuple(args, "si|s", &mode, &n, &pixel_format)) { + if (!PyArg_ParseTuple(args, "si|s", &mode_name, &n, &pixel_format)) { return NULL; } + const ModeID mode = findModeID(mode_name); + ModeID actual; + switch (n) { case 1: /* BC1: 565 color, 1-bit alpha */ case 2: /* BC2: 565 color, 4-bit alpha */ case 3: /* BC3: 565 color, 2-endpoint 8-bit interpolated alpha */ case 7: /* BC7: 4-channel 8-bit via everything */ - actual = "RGBA"; + actual = IMAGING_MODE_RGBA; break; case 4: /* BC4: 1-channel 8-bit via 1 BC3 alpha block */ - actual = "L"; + actual = IMAGING_MODE_L; break; case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */ case 6: /* BC6: 3-channel 16-bit float */ - actual = "RGB"; + actual = IMAGING_MODE_RGB; break; default: PyErr_SetString(PyExc_ValueError, "block compression type unknown"); return NULL; } - if (strcmp(mode, actual) != 0) { + if (mode != actual) { PyErr_SetString(PyExc_ValueError, "bad image mode"); return NULL; } @@ -401,15 +404,16 @@ PyObject * PyImaging_GifDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; + const char *mode_name; int bits = 8; int interlace = 0; int transparency = -1; - if (!PyArg_ParseTuple(args, "s|iii", &mode, &bits, &interlace, &transparency)) { + if (!PyArg_ParseTuple(args, "s|iii", &mode_name, &bits, &interlace, &transparency)) { return NULL; } - if (strcmp(mode, "L") != 0 && strcmp(mode, "P") != 0) { + const ModeID mode = findModeID(mode_name); + if (mode != IMAGING_MODE_L && mode != IMAGING_MODE_P) { PyErr_SetString(PyExc_ValueError, "bad image mode"); return NULL; } From 47503477d49922c085d6062035f2a7da68b3fd2d Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 17:00:35 +0200 Subject: [PATCH 1943/2374] add Mode.c as a dependency for _imagingft.c and _webp.c --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 93b5bcc7812..b5769b19157 100644 --- a/setup.py +++ b/setup.py @@ -1080,9 +1080,9 @@ def debug_build() -> bool: files.append(os.path.join("src/libImaging", src_file + ".c")) ext_modules = [ Extension("PIL._imaging", files), - Extension("PIL._imagingft", ["src/_imagingft.c"]), + Extension("PIL._imagingft", ["src/_imagingft.c", "src/libImaging/Mode.c"]), Extension("PIL._imagingcms", ["src/_imagingcms.c"]), - Extension("PIL._webp", ["src/_webp.c"]), + Extension("PIL._webp", ["src/_webp.c", "src/libImaging/Mode.c"]), Extension("PIL._avif", ["src/_avif.c"]), Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), Extension("PIL._imagingmath", ["src/_imagingmath.c"]), From e483a976d218ceb1120f00b624a4d0f30c0cd71e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 24 Apr 2024 17:33:09 -0500 Subject: [PATCH 1944/2374] use a different temp build dir for each module --- setup.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/setup.py b/setup.py index b5769b19157..2dca5e3805b 100644 --- a/setup.py +++ b/setup.py @@ -1008,6 +1008,17 @@ def build_extensions(self) -> None: self.summary_report(feature) + def build_extension(self, ext): + # Append the extension name (not including "PIL.") to the temp build directory + # so that each module builds to its own directory. We need to make a (shallow) + # copy of 'self' here so that we don't overwrite this value when running in + # parallel. + import copy + + self_copy = copy.copy(self) + self_copy.build_temp = os.path.join(self.build_temp, ext.name[4:]) + build_ext.build_extension(self_copy, ext) + def summary_report(self, feature: ext_feature) -> None: print("-" * 68) print("PIL SETUP SUMMARY") From 28adda9299daac9cd4ee349474d7618c16152d2d Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 17:02:00 +0200 Subject: [PATCH 1945/2374] build Mode.c as a common library --- setup.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/setup.py b/setup.py index 2dca5e3805b..3098a9ec61d 100644 --- a/setup.py +++ b/setup.py @@ -103,7 +103,6 @@ def get_version() -> str: "JpegDecode", "JpegEncode", "Matrix", - "Mode", "ModeFilter", "Negative", "Offset", @@ -1008,17 +1007,6 @@ def build_extensions(self) -> None: self.summary_report(feature) - def build_extension(self, ext): - # Append the extension name (not including "PIL.") to the temp build directory - # so that each module builds to its own directory. We need to make a (shallow) - # copy of 'self' here so that we don't overwrite this value when running in - # parallel. - import copy - - self_copy = copy.copy(self) - self_copy.build_temp = os.path.join(self.build_temp, ext.name[4:]) - build_ext.build_extension(self_copy, ext) - def summary_report(self, feature: ext_feature) -> None: print("-" * 68) print("PIL SETUP SUMMARY") @@ -1084,16 +1072,20 @@ def debug_build() -> bool: return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD +libraries = [ + ("pil_imaging_mode", {"sources": ["src/libImaging/Mode.c"]}), +] + files: list[str | os.PathLike[str]] = ["src/_imaging.c"] for src_file in _IMAGING: files.append("src/" + src_file + ".c") for src_file in _LIB_IMAGING: files.append(os.path.join("src/libImaging", src_file + ".c")) ext_modules = [ - Extension("PIL._imaging", files), - Extension("PIL._imagingft", ["src/_imagingft.c", "src/libImaging/Mode.c"]), + Extension("PIL._imaging", files, libraries=["pil_imaging_mode"]), + Extension("PIL._imagingft", ["src/_imagingft.c"], libraries=["pil_imaging_mode"]), Extension("PIL._imagingcms", ["src/_imagingcms.c"]), - Extension("PIL._webp", ["src/_webp.c", "src/libImaging/Mode.c"]), + Extension("PIL._webp", ["src/_webp.c"], libraries=["pil_imaging_mode"]), Extension("PIL._avif", ["src/_avif.c"]), Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), Extension("PIL._imagingmath", ["src/_imagingmath.c"]), @@ -1105,6 +1097,7 @@ def debug_build() -> bool: setup( cmdclass={"build_ext": pil_build_ext}, ext_modules=ext_modules, + libraries=libraries, zip_safe=not (debug_build() or PLATFORM_MINGW), ) except RequiredDependencyException as err: From 0567f064e4616a7e816a2fda493fc00c4a6ae23d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 25 Apr 2024 19:29:02 -0500 Subject: [PATCH 1946/2374] add debug check that all modes and rawmodes are defined --- src/libImaging/Mode.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c index 659e7aada49..1ec24aae839 100644 --- a/src/libImaging/Mode.c +++ b/src/libImaging/Mode.c @@ -1,6 +1,10 @@ #include "Mode.h" #include +#ifdef NDEBUG +#include +#include +#endif const ModeData MODES[] = { [IMAGING_MODE_UNKNOWN] = {""}, @@ -39,6 +43,11 @@ const ModeID findModeID(const char * const name) { return IMAGING_MODE_UNKNOWN; } for (size_t i = 0; i < sizeof(MODES) / sizeof(*MODES); i++) { +#ifdef NDEBUG + if (MODES[i].name == NULL) { + fprintf(stderr, "Mode ID %zu is not defined.\n", (size_t)i); + } else +#endif if (strcmp(MODES[i].name, name) == 0) { return (ModeID)i; } @@ -238,6 +247,11 @@ const RawModeID findRawModeID(const char * const name) { return IMAGING_RAWMODE_UNKNOWN; } for (size_t i = 0; i < sizeof(RAWMODES) / sizeof(*RAWMODES); i++) { +#ifdef NDEBUG + if (RAWMODES[i].name == NULL) { + fprintf(stderr, "Rawmode ID %zu is not defined.\n", (size_t)i); + } else +#endif if (strcmp(RAWMODES[i].name, name) == 0) { return (RawModeID)i; } From 2f169fa121dcb9e1319c8741075396e55aea378c Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 25 Apr 2024 19:54:47 -0500 Subject: [PATCH 1947/2374] use mode enums in _imagingcms.c --- setup.py | 2 +- src/_imagingcms.c | 46 +++++++++++++++++++++++++++++----------------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/setup.py b/setup.py index 3098a9ec61d..82986f14044 100644 --- a/setup.py +++ b/setup.py @@ -1084,7 +1084,7 @@ def debug_build() -> bool: ext_modules = [ Extension("PIL._imaging", files, libraries=["pil_imaging_mode"]), Extension("PIL._imagingft", ["src/_imagingft.c"], libraries=["pil_imaging_mode"]), - Extension("PIL._imagingcms", ["src/_imagingcms.c"]), + Extension("PIL._imagingcms", ["src/_imagingcms.c"], libraries=["pil_imaging_mode"]), Extension("PIL._webp", ["src/_webp.c"], libraries=["pil_imaging_mode"]), Extension("PIL._avif", ["src/_avif.c"]), Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), diff --git a/src/_imagingcms.c b/src/_imagingcms.c index e2f29d1b708..ad3b27896fb 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -212,32 +212,44 @@ cms_transform_dealloc(CmsTransformObject *self) { /* internal functions */ static cmsUInt32Number -findLCMStype(char *PILmode) { - if (strcmp(PILmode, "RGB") == 0 || strcmp(PILmode, "RGBA") == 0 || - strcmp(PILmode, "RGBX") == 0) { - return TYPE_RGBA_8; +findLCMStype(const char *const mode_name) { + const ModeID mode = findModeID(mode_name); + switch (mode) { + case IMAGING_MODE_RGB: + case IMAGING_MODE_RGBA: + case IMAGING_MODE_RGBX: + return TYPE_RGBA_8; + case IMAGING_MODE_CMYK: + return TYPE_CMYK_8; + case IMAGING_MODE_I_16: + case IMAGING_MODE_I_16L: + return TYPE_GRAY_16; + case IMAGING_MODE_I_16B: + return TYPE_GRAY_16_SE; + case IMAGING_MODE_YCbCr: + return TYPE_YCbCr_8; + case IMAGING_MODE_LAB: + // LabX equivalent like ALab, but not reversed -- no #define in lcms2 + return ( + COLORSPACE_SH(PT_LabV2) | CHANNELS_SH(3) | BYTES_SH(1) | EXTRA_SH(1) + ); + default: + // This function only accepts a subset of the imaging modes Pillow has. + break; } - if (strcmp(PILmode, "RGBA;16B") == 0) { + // The following modes are not valid PIL Image modes. + if (strcmp(mode_name, "RGBA;16B") == 0) { return TYPE_RGBA_16; } - if (strcmp(PILmode, "CMYK") == 0) { - return TYPE_CMYK_8; - } - if (strcmp(PILmode, "I;16") == 0 || strcmp(PILmode, "I;16L") == 0 || - strcmp(PILmode, "L;16") == 0) { + if (strcmp(mode_name, "L;16") == 0) { return TYPE_GRAY_16; } - if (strcmp(PILmode, "I;16B") == 0 || strcmp(PILmode, "L;16B") == 0) { + if (strcmp(mode_name, "L;16B") == 0) { return TYPE_GRAY_16_SE; } - if (strcmp(PILmode, "YCbCr") == 0 || strcmp(PILmode, "YCCA") == 0 || - strcmp(PILmode, "YCC") == 0) { + if (strcmp(mode_name, "YCCA") == 0 || strcmp(mode_name, "YCC") == 0) { return TYPE_YCbCr_8; } - if (strcmp(PILmode, "LAB") == 0) { - // LabX equivalent like ALab, but not reversed -- no #define in lcms2 - return (COLORSPACE_SH(PT_LabV2) | CHANNELS_SH(3) | BYTES_SH(1) | EXTRA_SH(1)); - } /* presume "1" or "L" by default */ return TYPE_GRAY_8; } From d82576ff3801b6e9bd87ebaf93b357768dfc08b6 Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 17:03:31 +0200 Subject: [PATCH 1948/2374] require types-setuptools>=75.2.0 this is necessary to have https://github.com/python/typeshed/pull/12791 --- .ci/requirements-mypy.txt | 2 +- setup.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 99eac602796..3519707f14c 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -11,4 +11,4 @@ sphinx types-atheris types-defusedxml types-olefile -types-setuptools +types-setuptools>=75.2.0 diff --git a/setup.py b/setup.py index 82986f14044..dcc07eaf691 100644 --- a/setup.py +++ b/setup.py @@ -16,11 +16,15 @@ import sys import warnings from collections.abc import Iterator +from typing import TYPE_CHECKING, Any from pybind11.setup_helpers import ParallelCompile from setuptools import Extension, setup from setuptools.command.build_ext import build_ext +if TYPE_CHECKING: + from setuptools import _BuildInfo + configuration: dict[str, list[str]] = {} # parse configuration from _custom_build/backend.py @@ -1072,7 +1076,7 @@ def debug_build() -> bool: return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD -libraries = [ +libraries: list[tuple[str, _BuildInfo]] = [ ("pil_imaging_mode", {"sources": ["src/libImaging/Mode.c"]}), ] From 84aa4372fd38069edbc792c235169422c0a799d6 Mon Sep 17 00:00:00 2001 From: eyedav <88885346+eyedav@users.noreply.github.com> Date: Sat, 19 Jul 2025 17:06:44 +0200 Subject: [PATCH 1949/2374] linter changes --- src/Tk/tkImaging.c | 6 ++-- src/_imaging.c | 7 +++-- src/decode.c | 22 +++++++++---- src/encode.c | 4 ++- src/libImaging/Convert.c | 17 +++++----- src/libImaging/Fill.c | 12 +++---- src/libImaging/GetBBox.c | 9 +++--- src/libImaging/Matrix.c | 7 ++--- src/libImaging/Mode.c | 63 +++++++++++++++---------------------- src/libImaging/Mode.h | 24 +++++++------- src/libImaging/Pack.c | 3 +- src/libImaging/Paste.c | 9 +++--- src/libImaging/Point.c | 4 +-- src/libImaging/TiffDecode.c | 28 ++++++++++++++--- 14 files changed, 113 insertions(+), 102 deletions(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 3e35f885f61..834634bd7fa 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -124,10 +124,8 @@ PyImagingPhotoPut( if (im->mode == IMAGING_MODE_1 || im->mode == IMAGING_MODE_L) { block.pixelSize = 1; block.offset[0] = block.offset[1] = block.offset[2] = block.offset[3] = 0; - } else if ( - im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA || - im->mode == IMAGING_MODE_RGBX || im->mode == IMAGING_MODE_RGBa - ) { + } else if (im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA || + im->mode == IMAGING_MODE_RGBX || im->mode == IMAGING_MODE_RGBa) { block.pixelSize = 4; block.offset[0] = 0; block.offset[1] = 1; diff --git a/src/_imaging.c b/src/_imaging.c index a940bb97448..4264cdb8748 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1773,7 +1773,9 @@ _quantize(ImagingObject *self, PyObject *args) { if (!self->image->xsize || !self->image->ysize) { /* no content; return an empty image */ - return PyImagingNew(ImagingNew(IMAGING_MODE_P, self->image->xsize, self->image->ysize)); + return PyImagingNew( + ImagingNew(IMAGING_MODE_P, self->image->xsize, self->image->ysize) + ); } return PyImagingNew(ImagingQuantize(self->image, colours, method, kmeans)); @@ -2053,7 +2055,8 @@ _reduce(ImagingObject *self, PyObject *args) { static int isRGB(const ModeID mode) { - return mode == IMAGING_MODE_RGB || mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_RGBX; + return mode == IMAGING_MODE_RGB || mode == IMAGING_MODE_RGBA || + mode == IMAGING_MODE_RGBX; } static PyObject * diff --git a/src/decode.c b/src/decode.c index f95637fa619..b7deee22854 100644 --- a/src/decode.c +++ b/src/decode.c @@ -266,7 +266,9 @@ static PyTypeObject ImagingDecoderType = { /* -------------------------------------------------------------------- */ int -get_unpacker(ImagingDecoderObject *decoder, const ModeID mode, const RawModeID rawmode) { +get_unpacker( + ImagingDecoderObject *decoder, const ModeID mode, const RawModeID rawmode +) { int bits; ImagingShuffler unpack; @@ -297,7 +299,9 @@ PyImaging_BitDecoderNew(PyObject *self, PyObject *args) { int fill = 0; int sign = 0; int ystep = 1; - if (!PyArg_ParseTuple(args, "s|iiiii", &mode_name, &bits, &pad, &fill, &sign, &ystep)) { + if (!PyArg_ParseTuple( + args, "s|iiiii", &mode_name, &bits, &pad, &fill, &sign, &ystep + )) { return NULL; } @@ -408,7 +412,9 @@ PyImaging_GifDecoderNew(PyObject *self, PyObject *args) { int bits = 8; int interlace = 0; int transparency = -1; - if (!PyArg_ParseTuple(args, "s|iii", &mode_name, &bits, &interlace, &transparency)) { + if (!PyArg_ParseTuple( + args, "s|iii", &mode_name, &bits, &interlace, &transparency + )) { return NULL; } @@ -481,7 +487,9 @@ PyImaging_LibTiffDecoderNew(PyObject *self, PyObject *args) { int fp; uint32_t ifdoffset; - if (!PyArg_ParseTuple(args, "sssiI", &mode_name, &rawmode_name, &compname, &fp, &ifdoffset)) { + if (!PyArg_ParseTuple( + args, "sssiI", &mode_name, &rawmode_name, &compname, &fp, &ifdoffset + )) { return NULL; } @@ -823,12 +831,14 @@ PyImaging_JpegDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; char *mode_name; - char *rawmode_name; /* what we want from the decoder */ + char *rawmode_name; /* what we want from the decoder */ char *jpegmode_name; /* what's in the file */ int scale = 1; int draft = 0; - if (!PyArg_ParseTuple(args, "ssz|ii", &mode_name, &rawmode_name, &jpegmode_name, &scale, &draft)) { + if (!PyArg_ParseTuple( + args, "ssz|ii", &mode_name, &rawmode_name, &jpegmode_name, &scale, &draft + )) { return NULL; } diff --git a/src/encode.c b/src/encode.c index 3a6b6d6d0a2..6a75a2fcc7e 100644 --- a/src/encode.c +++ b/src/encode.c @@ -411,7 +411,9 @@ PyImaging_GifEncoderNew(PyObject *self, PyObject *args) { char *rawmode_name; Py_ssize_t bits = 8; Py_ssize_t interlace = 0; - if (!PyArg_ParseTuple(args, "ss|nn", &mode_name, &rawmode_name, &bits, &interlace)) { + if (!PyArg_ParseTuple( + args, "ss|nn", &mode_name, &rawmode_name, &bits, &interlace + )) { return NULL; } diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 862f228e5a5..0c36b84494c 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1692,25 +1692,22 @@ ImagingConvertTransparent(Imaging imIn, const ModeID mode, int r, int g, int b) return (Imaging)ImagingError_ModeError(); } - if (imIn->mode == IMAGING_MODE_RGB && (mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_RGBa)) { + if (imIn->mode == IMAGING_MODE_RGB && + (mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_RGBa)) { convert = rgb2rgba; if (mode == IMAGING_MODE_RGBa) { premultiplied = 1; } - } else if (imIn->mode == IMAGING_MODE_RGB && (mode == IMAGING_MODE_LA || mode == IMAGING_MODE_La)) { + } else if (imIn->mode == IMAGING_MODE_RGB && + (mode == IMAGING_MODE_LA || mode == IMAGING_MODE_La)) { convert = rgb2la; source_transparency = 1; if (mode == IMAGING_MODE_La) { premultiplied = 1; } - } else if ((imIn->mode == IMAGING_MODE_1 || - imIn->mode == IMAGING_MODE_I || - imIn->mode == IMAGING_MODE_I_16 || - imIn->mode == IMAGING_MODE_L - ) && ( - mode == IMAGING_MODE_RGBA || - mode == IMAGING_MODE_LA - )) { + } else if ((imIn->mode == IMAGING_MODE_1 || imIn->mode == IMAGING_MODE_I || + imIn->mode == IMAGING_MODE_I_16 || imIn->mode == IMAGING_MODE_L) && + (mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_LA)) { if (imIn->mode == IMAGING_MODE_1) { convert = bit2rgb; } else if (imIn->mode == IMAGING_MODE_I) { diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index 0224d1ba95e..cbd303204d5 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -72,10 +72,8 @@ ImagingFillLinearGradient(const ModeID mode) { Imaging im; int y; - if (mode != IMAGING_MODE_1 && mode != IMAGING_MODE_F && - mode != IMAGING_MODE_I && mode != IMAGING_MODE_L && - mode != IMAGING_MODE_P - ) { + if (mode != IMAGING_MODE_1 && mode != IMAGING_MODE_F && mode != IMAGING_MODE_I && + mode != IMAGING_MODE_L && mode != IMAGING_MODE_P) { return (Imaging)ImagingError_ModeError(); } @@ -110,10 +108,8 @@ ImagingFillRadialGradient(const ModeID mode) { int x, y; int d; - if (mode != IMAGING_MODE_1 && mode != IMAGING_MODE_F && - mode != IMAGING_MODE_I && mode != IMAGING_MODE_L && - mode != IMAGING_MODE_P - ) { + if (mode != IMAGING_MODE_1 && mode != IMAGING_MODE_F && mode != IMAGING_MODE_I && + mode != IMAGING_MODE_L && mode != IMAGING_MODE_P) { return (Imaging)ImagingError_ModeError(); } diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 3719a9f1576..f94cf2a0e5f 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -89,11 +89,10 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { INT32 mask = 0xffffffff; if (im->bands == 3) { ((UINT8 *)&mask)[3] = 0; - } else if (alpha_only && ( - im->mode == IMAGING_MODE_RGBa || im->mode == IMAGING_MODE_RGBA || - im->mode == IMAGING_MODE_La || im->mode == IMAGING_MODE_LA || - im->mode == IMAGING_MODE_PA - )) { + } else if (alpha_only && + (im->mode == IMAGING_MODE_RGBa || im->mode == IMAGING_MODE_RGBA || + im->mode == IMAGING_MODE_La || im->mode == IMAGING_MODE_LA || + im->mode == IMAGING_MODE_PA)) { #ifdef WORDS_BIGENDIAN mask = 0x000000ff; #else diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index 6bc9fbc1de2..d28e04edfaa 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -46,11 +46,8 @@ ImagingConvertMatrix(Imaging im, const ModeID mode, float m[]) { } } ImagingSectionLeave(&cookie); - } else if ( - mode == IMAGING_MODE_HSV || - mode == IMAGING_MODE_LAB || - mode == IMAGING_MODE_RGB - ) { + } else if (mode == IMAGING_MODE_HSV || mode == IMAGING_MODE_LAB || + mode == IMAGING_MODE_RGB) { imOut = ImagingNewDirty(mode, im->xsize, im->ysize); if (!imOut) { return NULL; diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c index 1ec24aae839..8222c585b68 100644 --- a/src/libImaging/Mode.c +++ b/src/libImaging/Mode.c @@ -9,36 +9,25 @@ const ModeData MODES[] = { [IMAGING_MODE_UNKNOWN] = {""}, - [IMAGING_MODE_1] = {"1"}, - [IMAGING_MODE_CMYK] = {"CMYK"}, - [IMAGING_MODE_F] = {"F"}, - [IMAGING_MODE_HSV] = {"HSV"}, - [IMAGING_MODE_I] = {"I"}, - [IMAGING_MODE_L] = {"L"}, - [IMAGING_MODE_LA] = {"LA"}, - [IMAGING_MODE_LAB] = {"LAB"}, - [IMAGING_MODE_La] = {"La"}, - [IMAGING_MODE_P] = {"P"}, - [IMAGING_MODE_PA] = {"PA"}, - [IMAGING_MODE_RGB] = {"RGB"}, - [IMAGING_MODE_RGBA] = {"RGBA"}, - [IMAGING_MODE_RGBX] = {"RGBX"}, - [IMAGING_MODE_RGBa] = {"RGBa"}, - [IMAGING_MODE_YCbCr] = {"YCbCr"}, + [IMAGING_MODE_1] = {"1"}, [IMAGING_MODE_CMYK] = {"CMYK"}, + [IMAGING_MODE_F] = {"F"}, [IMAGING_MODE_HSV] = {"HSV"}, + [IMAGING_MODE_I] = {"I"}, [IMAGING_MODE_L] = {"L"}, + [IMAGING_MODE_LA] = {"LA"}, [IMAGING_MODE_LAB] = {"LAB"}, + [IMAGING_MODE_La] = {"La"}, [IMAGING_MODE_P] = {"P"}, + [IMAGING_MODE_PA] = {"PA"}, [IMAGING_MODE_RGB] = {"RGB"}, + [IMAGING_MODE_RGBA] = {"RGBA"}, [IMAGING_MODE_RGBX] = {"RGBX"}, + [IMAGING_MODE_RGBa] = {"RGBa"}, [IMAGING_MODE_YCbCr] = {"YCbCr"}, - [IMAGING_MODE_BGR_15] = {"BGR;15"}, - [IMAGING_MODE_BGR_16] = {"BGR;16"}, + [IMAGING_MODE_BGR_15] = {"BGR;15"}, [IMAGING_MODE_BGR_16] = {"BGR;16"}, [IMAGING_MODE_BGR_24] = {"BGR;24"}, - [IMAGING_MODE_I_16] = {"I;16"}, - [IMAGING_MODE_I_16L] = {"I;16L"}, - [IMAGING_MODE_I_16B] = {"I;16B"}, - [IMAGING_MODE_I_16N] = {"I;16N"}, - [IMAGING_MODE_I_32L] = {"I;32L"}, - [IMAGING_MODE_I_32B] = {"I;32B"}, + [IMAGING_MODE_I_16] = {"I;16"}, [IMAGING_MODE_I_16L] = {"I;16L"}, + [IMAGING_MODE_I_16B] = {"I;16B"}, [IMAGING_MODE_I_16N] = {"I;16N"}, + [IMAGING_MODE_I_32L] = {"I;32L"}, [IMAGING_MODE_I_32B] = {"I;32B"}, }; -const ModeID findModeID(const char * const name) { +const ModeID +findModeID(const char *const name) { if (name == NULL) { return IMAGING_MODE_UNKNOWN; } @@ -48,21 +37,21 @@ const ModeID findModeID(const char * const name) { fprintf(stderr, "Mode ID %zu is not defined.\n", (size_t)i); } else #endif - if (strcmp(MODES[i].name, name) == 0) { + if (strcmp(MODES[i].name, name) == 0) { return (ModeID)i; } } return IMAGING_MODE_UNKNOWN; } -const ModeData * const getModeData(const ModeID id) { +const ModeData *const +getModeData(const ModeID id) { if (id < 0 || id > sizeof(MODES) / sizeof(*MODES)) { return &MODES[IMAGING_MODE_UNKNOWN]; } return &MODES[id]; } - const RawModeData RAWMODES[] = { [IMAGING_RAWMODE_UNKNOWN] = {""}, @@ -242,7 +231,8 @@ const RawModeData RAWMODES[] = { [IMAGING_RAWMODE_aRGB] = {"aRGB"}, }; -const RawModeID findRawModeID(const char * const name) { +const RawModeID +findRawModeID(const char *const name) { if (name == NULL) { return IMAGING_RAWMODE_UNKNOWN; } @@ -252,24 +242,23 @@ const RawModeID findRawModeID(const char * const name) { fprintf(stderr, "Rawmode ID %zu is not defined.\n", (size_t)i); } else #endif - if (strcmp(RAWMODES[i].name, name) == 0) { + if (strcmp(RAWMODES[i].name, name) == 0) { return (RawModeID)i; } } return IMAGING_RAWMODE_UNKNOWN; } -const RawModeData * const getRawModeData(const RawModeID id) { +const RawModeData *const +getRawModeData(const RawModeID id) { if (id < 0 || id > sizeof(RAWMODES) / sizeof(*RAWMODES)) { return &RAWMODES[IMAGING_RAWMODE_UNKNOWN]; } return &RAWMODES[id]; } - -int isModeI16(const ModeID mode) { - return mode == IMAGING_MODE_I_16 - || mode == IMAGING_MODE_I_16L - || mode == IMAGING_MODE_I_16B - || mode == IMAGING_MODE_I_16N; +int +isModeI16(const ModeID mode) { + return mode == IMAGING_MODE_I_16 || mode == IMAGING_MODE_I_16L || + mode == IMAGING_MODE_I_16B || mode == IMAGING_MODE_I_16N; } diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index e21ad941a26..a20ad0cb68d 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -1,7 +1,6 @@ #ifndef __MODE_H__ #define __MODE_H__ - typedef enum { IMAGING_MODE_UNKNOWN, @@ -35,12 +34,13 @@ typedef enum { } ModeID; typedef struct { - const char * const name; + const char *const name; } ModeData; -const ModeID findModeID(const char * const name); -const ModeData * const getModeData(const ModeID id); - +const ModeID +findModeID(const char *const name); +const ModeData *const +getModeData(const ModeID id); typedef enum { IMAGING_RAWMODE_UNKNOWN, @@ -226,13 +226,15 @@ typedef enum { } RawModeID; typedef struct { - const char * const name; + const char *const name; } RawModeData; -const RawModeID findRawModeID(const char * const name); -const RawModeData * const getRawModeData(const RawModeID id); - +const RawModeID +findRawModeID(const char *const name); +const RawModeData *const +getRawModeData(const RawModeID id); -int isModeI16(const ModeID mode); +int +isModeI16(const ModeID mode); -#endif // __MODE_H__ +#endif // __MODE_H__ diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index 63bbc8acb35..a0652e0cafc 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -648,7 +648,8 @@ static struct { {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16B, 16, copy2}, {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16L, 16, copy2}, {IMAGING_MODE_I_16N, IMAGING_RAWMODE_I_16N, 16, copy2}, - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, packI16N_I16}, // LibTiff native->image endian. + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, packI16N_I16 + }, // LibTiff native->image endian. {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, packI16N_I16}, {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, packI16N_I16B}, diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index 54dd270e6f0..d4b973abc4d 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -450,11 +450,10 @@ fill_mask_L( } } else { - int alpha_channel = imOut->mode == IMAGING_MODE_RGBa || - imOut->mode == IMAGING_MODE_RGBA || - imOut->mode == IMAGING_MODE_La || - imOut->mode == IMAGING_MODE_LA || - imOut->mode == IMAGING_MODE_PA; + int alpha_channel = + imOut->mode == IMAGING_MODE_RGBa || imOut->mode == IMAGING_MODE_RGBA || + imOut->mode == IMAGING_MODE_La || imOut->mode == IMAGING_MODE_LA || + imOut->mode == IMAGING_MODE_PA; for (y = 0; y < ysize; y++) { UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx * pixelsize; UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index fa0b1027cd0..8f6d47c770f 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -210,8 +210,8 @@ ImagingPointTransform(Imaging imIn, double scale, double offset) { Imaging imOut; int x, y; - if (!imIn || (imIn->mode != IMAGING_MODE_I && - imIn->mode != IMAGING_MODE_I_16 && imIn->mode != IMAGING_MODE_F)) { + if (!imIn || (imIn->mode != IMAGING_MODE_I && imIn->mode != IMAGING_MODE_I_16 && + imIn->mode != IMAGING_MODE_F)) { return (Imaging)ImagingError_ModeError(); } diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index f987c608f8a..72e0d7b309a 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -246,10 +246,26 @@ _pickUnpackers( // We'll pick appropriate set of unpackers depending on planar_configuration // It does not matter if data is RGB(A), CMYK or LUV really, // we just copy it plane by plane - unpackers[0] = ImagingFindUnpacker(IMAGING_MODE_RGBA, bits_per_sample == 16 ? IMAGING_RAWMODE_R_16N : IMAGING_RAWMODE_R, NULL); - unpackers[1] = ImagingFindUnpacker(IMAGING_MODE_RGBA, bits_per_sample == 16 ? IMAGING_RAWMODE_G_16N : IMAGING_RAWMODE_G, NULL); - unpackers[2] = ImagingFindUnpacker(IMAGING_MODE_RGBA, bits_per_sample == 16 ? IMAGING_RAWMODE_B_16N : IMAGING_RAWMODE_B, NULL); - unpackers[3] = ImagingFindUnpacker(IMAGING_MODE_RGBA, bits_per_sample == 16 ? IMAGING_RAWMODE_A_16N : IMAGING_RAWMODE_A, NULL); + unpackers[0] = ImagingFindUnpacker( + IMAGING_MODE_RGBA, + bits_per_sample == 16 ? IMAGING_RAWMODE_R_16N : IMAGING_RAWMODE_R, + NULL + ); + unpackers[1] = ImagingFindUnpacker( + IMAGING_MODE_RGBA, + bits_per_sample == 16 ? IMAGING_RAWMODE_G_16N : IMAGING_RAWMODE_G, + NULL + ); + unpackers[2] = ImagingFindUnpacker( + IMAGING_MODE_RGBA, + bits_per_sample == 16 ? IMAGING_RAWMODE_B_16N : IMAGING_RAWMODE_B, + NULL + ); + unpackers[3] = ImagingFindUnpacker( + IMAGING_MODE_RGBA, + bits_per_sample == 16 ? IMAGING_RAWMODE_A_16N : IMAGING_RAWMODE_A, + NULL + ); return im->bands; } else { @@ -763,7 +779,9 @@ ImagingLibTiffDecode( if (extrasamples >= 1 && (sampleinfo[0] == EXTRASAMPLE_UNSPECIFIED || sampleinfo[0] == EXTRASAMPLE_ASSOCALPHA)) { - shuffle = ImagingFindUnpacker(IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa, NULL); + shuffle = ImagingFindUnpacker( + IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa, NULL + ); for (y = state->yoff; y < state->ysize; y++) { UINT8 *ptr = (UINT8 *)im->image[y + state->yoff] + From 64556405e29d076d95d284d3d146cecff7476b7d Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sat, 19 Jul 2025 17:34:39 +0200 Subject: [PATCH 1950/2374] WIP - Not working in pyarrow --- src/libImaging/Arrow.c | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index 2ecec9b29bb..ff98dfb5134 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -84,6 +84,33 @@ image_band_json(Imaging im) { return json; } +char * +single_band_json(Imaging im) { + char *format = "{\"bands\": [\"%s\"]}"; + char *json; + // Bands can be 1 band * (maybe but probably not) 2 characters each + int len = strlen(format) + 2 + 1; + int err; + + json = calloc(1, len); + + if (!json) { + return NULL; + } + + err = PyOS_snprintf( + json, + len, + format, + im->band_names[0] + ); + if (err < 0) { + return NULL; + } + return json; +} + + char * assemble_metadata(const char *band_json) { /* format is @@ -179,7 +206,17 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) { } if (im->bands == 1) { - return export_named_type(schema, im->arrow_band_format, im->band_names[0]); + retval = export_named_type(schema, im->arrow_band_format, im->band_names[0]); + if (retval != 0) { + return retval; + } + // band related metadata + band_json = single_band_json(im); + if (band_json) { + schema->metadata = assemble_metadata(band_json); + free(band_json); + } + return retval; } retval = export_named_type(schema, "+w:4", ""); From adfb66f1d6b61494f0c111ffb21a38dbfc720b4c Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 20 Jul 2025 10:18:59 +0200 Subject: [PATCH 1951/2374] Fix Compliation errors from rebase --- src/_imaging.c | 5 ++++- src/libImaging/BcnEncode.c | 2 +- src/libImaging/Convert.c | 3 --- src/libImaging/Jpeg2KEncode.c | 2 +- src/libImaging/Pack.c | 4 +--- src/libImaging/Unpack.c | 8 +++----- 6 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 4264cdb8748..7823745f0d6 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -297,6 +297,7 @@ ExportArrowArrayPyCapsule(ImagingObject *self) { static PyObject * _new_arrow(PyObject *self, PyObject *args) { char *mode; + ModeID mode_id; int xsize, ysize; PyObject *schema_capsule, *array_capsule; PyObject *ret; @@ -307,9 +308,11 @@ _new_arrow(PyObject *self, PyObject *args) { return NULL; } + mode_id = findModeID(mode); + // ImagingBorrowArrow is responsible for retaining the array_capsule ret = PyImagingNew( - ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule) + ImagingNewArrow(mode_id, xsize, ysize, schema_capsule, array_capsule) ); if (!ret) { return ImagingError_ValueError("Invalid Arrow array mode or size mismatch"); diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c index 7a5072ddee6..2101383fd42 100644 --- a/src/libImaging/BcnEncode.c +++ b/src/libImaging/BcnEncode.c @@ -253,7 +253,7 @@ int ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { int n = state->state; int has_alpha_channel = - strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "LA") == 0; + im->mode == IMAGING_MODE_RGBA || im->mode == IMAGING_MODE_LA; UINT8 *dst = buf; diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 0c36b84494c..330e5325c33 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1517,9 +1517,6 @@ static struct { {IMAGING_MODE_RGB, IMAGING_MODE_I_16N, rgb2i16l}, #endif {IMAGING_MODE_RGB, IMAGING_MODE_F, rgb2f}, - {IMAGING_MODE_RGB, IMAGING_MODE_BGR_15, rgb2bgr15}, - {IMAGING_MODE_RGB, IMAGING_MODE_BGR_16, rgb2bgr16}, - {IMAGING_MODE_RGB, IMAGING_MODE_BGR_24, rgb2bgr24}, {IMAGING_MODE_RGB, IMAGING_MODE_RGBA, rgb2rgba}, {IMAGING_MODE_RGB, IMAGING_MODE_RGBa, rgb2rgba}, {IMAGING_MODE_RGB, IMAGING_MODE_RGBX, rgb2rgba}, diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 67290f6748f..fdfbde2d76b 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -332,7 +332,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { pack = j2k_pack_rgba; #if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \ (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2) - } else if (strcmp(im->mode, "CMYK") == 0) { + } else if (im->mode == IMAGING_MODE_CMYK) { components = 4; color_space = OPJ_CLRSPC_CMYK; pack = j2k_pack_rgba; diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index a0652e0cafc..0a97c4872f9 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -651,9 +651,7 @@ static struct { {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, packI16N_I16 }, // LibTiff native->image endian. {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, packI16N_I16}, - {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, packI16N_I16B}, - - {NULL} /* sentinel */ + {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, packI16N_I16B} }; ImagingShuffler diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 8d4bb86190a..075ec5b950b 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1313,7 +1313,7 @@ copy4skip2(UINT8 *_out, const UINT8 *in, int pixels) { /* Unpack to "I" and "F" images */ #define UNPACK_RAW(NAME, GET, INTYPE, OUTTYPE) \ - static void NAME(UINT8 *out, const UINT8 *in, int pixels) { \ + static void NAME(UINT8 *out_, const UINT8 *in, int pixels) { \ int i; \ OUTTYPE *out = (OUTTYPE *)out_; \ for (i = 0; i < pixels; i++, in += sizeof(INTYPE)) { \ @@ -1322,7 +1322,7 @@ copy4skip2(UINT8 *_out, const UINT8 *in, int pixels) { } #define UNPACK(NAME, COPY, INTYPE, OUTTYPE) \ - static void NAME(UINT8 *out, const UINT8 *in, int pixels) { \ + static void NAME(UINT8 *out_, const UINT8 *in, int pixels) { \ int i; \ OUTTYPE *out = (OUTTYPE *)out_; \ INTYPE tmp_; \ @@ -1839,9 +1839,7 @@ static struct { {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16R, 16, unpackI16R_I16}, - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_12, 12, unpackI12_I16}, // 12 bit Tiffs stored in 16bits. - - {NULL} /* sentinel */ + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_12, 12, unpackI12_I16} // 12 bit Tiffs stored in 16bits. }; ImagingShuffler From 1159e65b4f60013f93c5b043743c407dbcf74777 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 20 Jul 2025 12:58:54 +0200 Subject: [PATCH 1952/2374] Added integration tests for Arro3, comparable to PyArrow tests --- Tests/test_arro3.py | 276 ++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 + 2 files changed, 278 insertions(+) create mode 100644 Tests/test_arro3.py diff --git a/Tests/test_arro3.py b/Tests/test_arro3.py new file mode 100644 index 00000000000..ddc7ecd0a90 --- /dev/null +++ b/Tests/test_arro3.py @@ -0,0 +1,276 @@ +from __future__ import annotations + +import json +from typing import Any, NamedTuple +from itertools import repeat, chain + +import pytest + +from PIL import Image + +from .helper import ( + assert_deep_equal, + assert_image_equal, + hopper, + is_big_endian, +) + +TYPE_CHECKING = False +if TYPE_CHECKING: + from arro3.core import Array, DataType, Field, fixed_size_list_array + from arro3 import compute +else: + arro3 = pytest.importorskip("arro3", reason="Arro3 not installed") + from arro3.core import Array, DataType, Field, fixed_size_list_array + from arro3 import compute + +TEST_IMAGE_SIZE = (10, 10) + + +def _test_img_equals_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 +) -> None: + assert img.height * img.width * elts_per_pixel == len(arr) + px = img.load() + assert px is not None + if elts_per_pixel > 1 and mask is None: + # have to do element-wise comparison when we're comparing + # flattened r,g,b,a to a pixel. + mask = list(range(elts_per_pixel)) + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): + if mask: + pixel = px[x, y] + assert isinstance(pixel, tuple) + for ix, elt in enumerate(mask): + if elts_per_pixel == 1: + assert pixel[ix] == arr[y * img.width + x].as_py()[elt] + else: + assert ( + pixel[ix] + == arr[(y * img.width + x) * elts_per_pixel + elt].as_py() + ) + else: + assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) + + +def _test_img_equals_int32_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 +) -> None: + assert img.height * img.width * elts_per_pixel == len(arr) + px = img.load() + assert px is not None + if mask is None: + # have to do element-wise comparison when we're comparing + # flattened rgba in an uint32 to a pixel. + mask = list(range(elts_per_pixel)) + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): + pixel = px[x, y] + assert isinstance(pixel, tuple) + arr_pixel_int = arr[y * img.width + x].as_py() + arr_pixel_tuple = ( + arr_pixel_int % 256, + (arr_pixel_int // 256) % 256, + (arr_pixel_int // 256**2) % 256, + (arr_pixel_int // 256**3), + ) + if is_big_endian(): + arr_pixel_tuple = arr_pixel_tuple[::-1] + + for ix, elt in enumerate(mask): + assert pixel[ix] == arr_pixel_tuple[elt] + +fl_uint8_4_type = DataType.list(Field("_", DataType.uint8()).with_nullable(False), 4) + +@pytest.mark.parametrize( + "mode, dtype, mask", + ( + ("L", DataType.uint8(), None), + ("I", DataType.int32(), None), + ("F", DataType.float32(), None), + ("LA", fl_uint8_4_type, [0, 3]), + ("RGB", fl_uint8_4_type, [0, 1, 2]), + ("RGBA", fl_uint8_4_type, None), + ("RGBX", fl_uint8_4_type, None), + ("CMYK", fl_uint8_4_type, None), + ("YCbCr", fl_uint8_4_type, [0, 1, 2]), + ("HSV", fl_uint8_4_type, [0, 1, 2]), + ), +) +def test_to_array(mode: str, dtype: DataType, mask: list[int] | None) -> None: + img = hopper(mode) + + # Resize to non-square + img = img.crop((3, 0, 124, 127)) + assert img.size == (121, 127) + + arr = Array(img) # type: ignore[call-overload] + _test_img_equals_pyarray(img, arr, mask) + assert arr.type == dtype + + reloaded = Image.fromarrow(arr, mode, img.size) + + assert reloaded + + assert_image_equal(img, reloaded) + + +def test_lifetime() -> None: + # valgrind shouldn't error out here. + # arrays should be accessible after the image is deleted. + + img = hopper("L") + + arr_1 = Array(img) # type: ignore[call-overload] + arr_2 = Array(img) # type: ignore[call-overload] + + del img + + assert compute.sum(arr_1).as_py() > 0 + del arr_1 + + assert compute.sum(arr_2).as_py() > 0 + del arr_2 + + +def test_lifetime2() -> None: + # valgrind shouldn't error out here. + # img should remain after the arrays are collected. + + img = hopper("L") + + arr_1 = Array(img) # type: ignore[call-overload] + arr_2 = Array(img) # type: ignore[call-overload] + + assert compute.sum(arr_1).as_py() > 0 + del arr_1 + + assert compute.sum(arr_2).as_py() > 0 + del arr_2 + + img2 = img.copy() + px = img2.load() + assert px # make mypy happy + assert isinstance(px[0, 0], int) + + +class DataShape(NamedTuple): + dtype: DataType + # Strictly speaking, elt should be a pixel or pixel component, so + # list[uint8][4], float, int, uint32, uint8, etc. But more + # correctly, it should be exactly the dtype from the line above. + elt: Any + elts_per_pixel: int + + +UINT_ARR = DataShape( + dtype=fl_uint8_4_type, + elt=[1, 2, 3, 4], # array of 4 uint8 per pixel + elts_per_pixel=1, # only one array per pixel +) + +UINT = DataShape( + dtype=DataType.uint8(), + elt=3, # one uint8, + elts_per_pixel=4, # but repeated 4x per pixel +) + +UINT32 = DataShape( + dtype=DataType.uint32(), + elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000 + elts_per_pixel=1, # one per pixel +) + +INT32 = DataShape( + dtype=DataType.uint32(), + elt=0x12CDEF45, # one packed int + elts_per_pixel=1, # one per pixel +) + + +@pytest.mark.parametrize( + "mode, data_tp, mask", + ( + ("L", DataShape(DataType.uint8(), 3, 1), None), + ("I", DataShape(DataType.int32(), 1 << 24, 1), None), + ("F", DataShape(DataType.float32(), 3.14159, 1), None), + ("LA", UINT_ARR, [0, 3]), + ("LA", UINT, [0, 3]), + ("RGB", UINT_ARR, [0, 1, 2]), + ("RGBA", UINT_ARR, None), + ("CMYK", UINT_ARR, None), + ("YCbCr", UINT_ARR, [0, 1, 2]), + ("HSV", UINT_ARR, [0, 1, 2]), + ("RGB", UINT, [0, 1, 2]), + ("RGBA", UINT, None), + ("CMYK", UINT, None), + ("YCbCr", UINT, [0, 1, 2]), + ("HSV", UINT, [0, 1, 2]), + ), +) +def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: + (dtype, elt, elts_per_pixel) = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + if dtype == fl_uint8_4_type: + tmp_arr = Array(elt * (ct_pixels * elts_per_pixel), type=DataType.uint8()) + arr = fixed_size_list_array(tmp_arr, 4) + else: + arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_pyarray(img, arr, mask, elts_per_pixel) + + +@pytest.mark.parametrize( + "mode, data_tp, mask", + ( + ("LA", UINT32, [0, 3]), + ("RGB", UINT32, [0, 1, 2]), + ("RGBA", UINT32, None), + ("CMYK", UINT32, None), + ("YCbCr", UINT32, [0, 1, 2]), + ("HSV", UINT32, [0, 1, 2]), + ("LA", INT32, [0, 3]), + ("RGB", INT32, [0, 1, 2]), + ("RGBA", INT32, None), + ("CMYK", INT32, None), + ("YCbCr", INT32, [0, 1, 2]), + ("HSV", INT32, [0, 1, 2]), + ), +) +def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: + (dtype, elt, elts_per_pixel) = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel) + + +@pytest.mark.parametrize( + "mode, metadata", + ( + ("LA", ["L", "X", "X", "A"]), + ("RGB", ["R", "G", "B", "X"]), + ("RGBX", ["R", "G", "B", "X"]), + ("RGBA", ["R", "G", "B", "A"]), + ("CMYK", ["C", "M", "Y", "K"]), + ("YCbCr", ["Y", "Cb", "Cr", "X"]), + ("HSV", ["H", "S", "V", "X"]), + ), +) +def test_image_metadata(mode: str, metadata: list[str]) -> None: + img = hopper(mode) + + arr = Array(img) # type: ignore[call-overload] + + assert arr.type.value_field.metadata + assert arr.type.value_field.metadata[b"image"] + + parsed_metadata = json.loads(arr.type.value_field.metadata[b"image"].decode("utf8")) + + assert "bands" in parsed_metadata + assert parsed_metadata["bands"] == metadata diff --git a/pyproject.toml b/pyproject.toml index 4e8623118ba..b1765e82cfc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,8 @@ optional-dependencies.mic = [ ] optional-dependencies.test-arrow = [ "pyarrow", + "arro3-core", + "arro3-compute", ] optional-dependencies.tests = [ From 1a02d4ed5a1672218249ed129f43d4109b32e183 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 20 Jul 2025 13:01:39 +0200 Subject: [PATCH 1953/2374] lint fixes --- Tests/test_arro3.py | 7 ++++--- pyproject.toml | 4 ++-- src/libImaging/Arrow.c | 8 +------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/Tests/test_arro3.py b/Tests/test_arro3.py index ddc7ecd0a90..a7c755fc2b6 100644 --- a/Tests/test_arro3.py +++ b/Tests/test_arro3.py @@ -2,7 +2,6 @@ import json from typing import Any, NamedTuple -from itertools import repeat, chain import pytest @@ -17,12 +16,12 @@ TYPE_CHECKING = False if TYPE_CHECKING: - from arro3.core import Array, DataType, Field, fixed_size_list_array from arro3 import compute + from arro3.core import Array, DataType, Field, fixed_size_list_array else: arro3 = pytest.importorskip("arro3", reason="Arro3 not installed") - from arro3.core import Array, DataType, Field, fixed_size_list_array from arro3 import compute + from arro3.core import Array, DataType, Field, fixed_size_list_array TEST_IMAGE_SIZE = (10, 10) @@ -81,8 +80,10 @@ def _test_img_equals_int32_pyarray( for ix, elt in enumerate(mask): assert pixel[ix] == arr_pixel_tuple[elt] + fl_uint8_4_type = DataType.list(Field("_", DataType.uint8()).with_nullable(False), 4) + @pytest.mark.parametrize( "mode, dtype, mask", ( diff --git a/pyproject.toml b/pyproject.toml index b1765e82cfc..899e833e35f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,9 +57,9 @@ optional-dependencies.mic = [ "olefile", ] optional-dependencies.test-arrow = [ - "pyarrow", - "arro3-core", "arro3-compute", + "arro3-core", + "pyarrow", ] optional-dependencies.tests = [ diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index ff98dfb5134..4519243ae4d 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -98,19 +98,13 @@ single_band_json(Imaging im) { return NULL; } - err = PyOS_snprintf( - json, - len, - format, - im->band_names[0] - ); + err = PyOS_snprintf(json, len, format, im->band_names[0]); if (err < 0) { return NULL; } return json; } - char * assemble_metadata(const char *band_json) { /* format is From 28c7645d8bab0a746f60e37cbfde1c79b8f69ca9 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Jul 2025 11:19:45 +0200 Subject: [PATCH 1954/2374] Added tests for integration with nanoarrow --- Tests/test_nanoarrow.py | 281 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + 2 files changed, 282 insertions(+) create mode 100644 Tests/test_nanoarrow.py diff --git a/Tests/test_nanoarrow.py b/Tests/test_nanoarrow.py new file mode 100644 index 00000000000..e0ae11baae1 --- /dev/null +++ b/Tests/test_nanoarrow.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +import json +from typing import Any, NamedTuple + +import pytest + +from PIL import Image + +from .helper import ( + assert_deep_equal, + assert_image_equal, + hopper, + is_big_endian, +) + +TYPE_CHECKING = False +if TYPE_CHECKING: + import nanoarrow +else: + nanoarrow = pytest.importorskip("nanoarrow", reason="Nanoarrow not installed") + +TEST_IMAGE_SIZE = (10, 10) + + +def _test_img_equals_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 +) -> None: + assert img.height * img.width * elts_per_pixel == len(arr) + px = img.load() + assert px is not None + if elts_per_pixel > 1 and mask is None: + # have to do element-wise comparison when we're comparing + # flattened r,g,b,a to a pixel. + mask = list(range(elts_per_pixel)) + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): + if mask: + pixel = px[x, y] + assert isinstance(pixel, tuple) + for ix, elt in enumerate(mask): + if elts_per_pixel == 1: + assert pixel[ix] == arr[y * img.width + x].as_py()[elt] + else: + assert ( + pixel[ix] + == arr[(y * img.width + x) * elts_per_pixel + elt].as_py() + ) + else: + assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) + + +def _test_img_equals_int32_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 +) -> None: + assert img.height * img.width * elts_per_pixel == len(arr) + px = img.load() + assert px is not None + if mask is None: + # have to do element-wise comparison when we're comparing + # flattened rgba in an uint32 to a pixel. + mask = list(range(elts_per_pixel)) + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): + pixel = px[x, y] + assert isinstance(pixel, tuple) + arr_pixel_int = arr[y * img.width + x].as_py() + arr_pixel_tuple = ( + arr_pixel_int % 256, + (arr_pixel_int // 256) % 256, + (arr_pixel_int // 256**2) % 256, + (arr_pixel_int // 256**3), + ) + if is_big_endian(): + arr_pixel_tuple = arr_pixel_tuple[::-1] + + for ix, elt in enumerate(mask): + assert pixel[ix] == arr_pixel_tuple[elt] + + +fl_uint8_4_type = nanoarrow.fixed_size_list(value_type=nanoarrow.uint8(nullable=False), + list_size=4, + nullable=False) + + +@pytest.mark.parametrize( + "mode, dtype, mask", + ( + ("L", nanoarrow.uint8(nullable=False), None), + ("I", nanoarrow.int32(nullable=False), None), + ("F", nanoarrow.float32(nullable=False), None), + ("LA", fl_uint8_4_type, [0, 3]), + ("RGB", fl_uint8_4_type, [0, 1, 2]), + ("RGBA", fl_uint8_4_type, None), + ("RGBX", fl_uint8_4_type, None), + ("CMYK", fl_uint8_4_type, None), + ("YCbCr", fl_uint8_4_type, [0, 1, 2]), + ("HSV", fl_uint8_4_type, [0, 1, 2]), + ), +) +def test_to_array(mode: str, dtype: nanoarrow, mask: list[int] | None) -> None: + img = hopper(mode) + + # Resize to non-square + img = img.crop((3, 0, 124, 127)) + assert img.size == (121, 127) + + arr = nanoarrow.Array(img) # type: ignore[call-overload] + _test_img_equals_pyarray(img, arr, mask) + assert arr.schema.type == dtype.type + assert arr.schema.nullable == dtype.nullable + + reloaded = Image.fromarrow(arr, mode, img.size) + + assert reloaded + + assert_image_equal(img, reloaded) + + +def test_lifetime() -> None: + # valgrind shouldn't error out here. + # arrays should be accessible after the image is deleted. + + img = hopper("L") + + arr_1 = nanoarrow.Array(img) # type: ignore[call-overload] + arr_2 = nanoarrow.Array(img) # type: ignore[call-overload] + + del img + + assert sum(arr_1.iter_py()) > 0 + del arr_1 + + assert sum(arr_2.iter_py()) > 0 + del arr_2 + + +def test_lifetime2() -> None: + # valgrind shouldn't error out here. + # img should remain after the arrays are collected. + + img = hopper("L") + + arr_1 = nanoarrow.Array(img) # type: ignore[call-overload] + arr_2 = nanoarrow.Array(img) # type: ignore[call-overload] + + assert sum(arr_1.iter_py()) > 0 + del arr_1 + + assert sum(arr_2.iter_py()) > 0 + del arr_2 + + img2 = img.copy() + px = img2.load() + assert px # make mypy happy + assert isinstance(px[0, 0], int) + + +class DataShape(NamedTuple): + dtype: nanoarrow + # Strictly speaking, elt should be a pixel or pixel component, so + # list[uint8][4], float, int, uint32, uint8, etc. But more + # correctly, it should be exactly the dtype from the line above. + elt: Any + elts_per_pixel: int + + +UINT_ARR = DataShape( + dtype=fl_uint8_4_type, + elt=[1, 2, 3, 4], # array of 4 uint8 per pixel + elts_per_pixel=1, # only one array per pixel +) + +UINT = DataShape( + dtype=nanoarrow.uint8(), + elt=3, # one uint8, + elts_per_pixel=4, # but repeated 4x per pixel +) + +UINT32 = DataShape( + dtype=nanoarrow.uint32(), + elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000 + elts_per_pixel=1, # one per pixel +) + +INT32 = DataShape( + dtype=nanoarrow.uint32(), + elt=0x12CDEF45, # one packed int + elts_per_pixel=1, # one per pixel +) + + +@pytest.mark.xfail(reason="Support for nested array creation is not available in nanoarrow/python") +@pytest.mark.parametrize( + "mode, data_tp, mask", + ( + ("L", DataShape(nanoarrow.uint8(), 3, 1), None), + ("I", DataShape(nanoarrow.int32(), 1 << 24, 1), None), + ("F", DataShape(nanoarrow.float32(), 3.14159, 1), None), + ("LA", UINT_ARR, [0, 3]), + ("LA", UINT, [0, 3]), + ("RGB", UINT_ARR, [0, 1, 2]), + ("RGBA", UINT_ARR, None), + ("CMYK", UINT_ARR, None), + ("YCbCr", UINT_ARR, [0, 1, 2]), + ("HSV", UINT_ARR, [0, 1, 2]), + ("RGB", UINT, [0, 1, 2]), + ("RGBA", UINT, None), + ("CMYK", UINT, None), + ("YCbCr", UINT, [0, 1, 2]), + ("HSV", UINT, [0, 1, 2]), + ), +) +def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: + (dtype, elt, elts_per_pixel) = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + if dtype == fl_uint8_4_type: + # Apparently there's no good way to create this array from python using nanoarrow + # https://github.com/apache/arrow-nanoarrow/issues/620 + # the following lines will fail. + tmp_arr = nanoarrow.c_array(elt * (ct_pixels * elts_per_pixel), schema=nanoarrow.uint8()) + arr = nanoarrow.Array(tmp_arr, schema=dtype) + else: + arr = nanoarrow.Array(nanoarrow.c_array([elt] * (ct_pixels * elts_per_pixel), schema=dtype)) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_pyarray(img, arr, mask, elts_per_pixel) + + +@pytest.mark.parametrize( + "mode, data_tp, mask", + ( + ("LA", UINT32, [0, 3]), + ("RGB", UINT32, [0, 1, 2]), + ("RGBA", UINT32, None), + ("CMYK", UINT32, None), + ("YCbCr", UINT32, [0, 1, 2]), + ("HSV", UINT32, [0, 1, 2]), + ("LA", INT32, [0, 3]), + ("RGB", INT32, [0, 1, 2]), + ("RGBA", INT32, None), + ("CMYK", INT32, None), + ("YCbCr", INT32, [0, 1, 2]), + ("HSV", INT32, [0, 1, 2]), + ), +) +def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: + (dtype, elt, elts_per_pixel) = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + arr = nanoarrow.Array(nanoarrow.c_array([elt] * (ct_pixels * elts_per_pixel), schema=dtype)) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel) + + +@pytest.mark.parametrize( + "mode, metadata", + ( + ("LA", ["L", "X", "X", "A"]), + ("RGB", ["R", "G", "B", "X"]), + ("RGBX", ["R", "G", "B", "X"]), + ("RGBA", ["R", "G", "B", "A"]), + ("CMYK", ["C", "M", "Y", "K"]), + ("YCbCr", ["Y", "Cb", "Cr", "X"]), + ("HSV", ["H", "S", "V", "X"]), + ), +) +def test_image_nested_metadata(mode: str, metadata: list[str]) -> None: + img = hopper(mode) + + arr = nanoarrow.Array(img) # type: ignore[call-overload] + + assert arr.schema.value_type.metadata + assert arr.schema.value_type.metadata[b"image"] + + parsed_metadata = json.loads(arr.schema.value_type.metadata[b"image"].decode("utf8")) + + assert "bands" in parsed_metadata + assert parsed_metadata["bands"] == metadata diff --git a/pyproject.toml b/pyproject.toml index 899e833e35f..5a4f752b319 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ optional-dependencies.test-arrow = [ "arro3-compute", "arro3-core", "pyarrow", + "nanoarrow", ] optional-dependencies.tests = [ From 7d2abbdcf91f01fd2bc83dace639f5eb5f7a562c Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Jul 2025 11:22:45 +0200 Subject: [PATCH 1955/2374] lint. --- Tests/test_nanoarrow.py | 26 ++++++++++++++++++-------- pyproject.toml | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Tests/test_nanoarrow.py b/Tests/test_nanoarrow.py index e0ae11baae1..3dc540043a8 100644 --- a/Tests/test_nanoarrow.py +++ b/Tests/test_nanoarrow.py @@ -78,9 +78,9 @@ def _test_img_equals_int32_pyarray( assert pixel[ix] == arr_pixel_tuple[elt] -fl_uint8_4_type = nanoarrow.fixed_size_list(value_type=nanoarrow.uint8(nullable=False), - list_size=4, - nullable=False) +fl_uint8_4_type = nanoarrow.fixed_size_list( + value_type=nanoarrow.uint8(nullable=False), list_size=4, nullable=False +) @pytest.mark.parametrize( @@ -190,7 +190,9 @@ class DataShape(NamedTuple): ) -@pytest.mark.xfail(reason="Support for nested array creation is not available in nanoarrow/python") +@pytest.mark.xfail( + reason="Support for nested array creation is not available in nanoarrow/python" +) @pytest.mark.parametrize( "mode, data_tp, mask", ( @@ -219,10 +221,14 @@ def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> Non # Apparently there's no good way to create this array from python using nanoarrow # https://github.com/apache/arrow-nanoarrow/issues/620 # the following lines will fail. - tmp_arr = nanoarrow.c_array(elt * (ct_pixels * elts_per_pixel), schema=nanoarrow.uint8()) + tmp_arr = nanoarrow.c_array( + elt * (ct_pixels * elts_per_pixel), schema=nanoarrow.uint8() + ) arr = nanoarrow.Array(tmp_arr, schema=dtype) else: - arr = nanoarrow.Array(nanoarrow.c_array([elt] * (ct_pixels * elts_per_pixel), schema=dtype)) + arr = nanoarrow.Array( + nanoarrow.c_array([elt] * (ct_pixels * elts_per_pixel), schema=dtype) + ) img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) _test_img_equals_pyarray(img, arr, mask, elts_per_pixel) @@ -249,7 +255,9 @@ def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) (dtype, elt, elts_per_pixel) = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] - arr = nanoarrow.Array(nanoarrow.c_array([elt] * (ct_pixels * elts_per_pixel), schema=dtype)) + arr = nanoarrow.Array( + nanoarrow.c_array([elt] * (ct_pixels * elts_per_pixel), schema=dtype) + ) img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) _test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel) @@ -275,7 +283,9 @@ def test_image_nested_metadata(mode: str, metadata: list[str]) -> None: assert arr.schema.value_type.metadata assert arr.schema.value_type.metadata[b"image"] - parsed_metadata = json.loads(arr.schema.value_type.metadata[b"image"].decode("utf8")) + parsed_metadata = json.loads( + arr.schema.value_type.metadata[b"image"].decode("utf8") + ) assert "bands" in parsed_metadata assert parsed_metadata["bands"] == metadata diff --git a/pyproject.toml b/pyproject.toml index 5a4f752b319..c450e427432 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,8 +59,8 @@ optional-dependencies.mic = [ optional-dependencies.test-arrow = [ "arro3-compute", "arro3-core", - "pyarrow", "nanoarrow", + "pyarrow", ] optional-dependencies.tests = [ From c07fe6e94313bf4172c670a8d6c4c3e7c78ac469 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Jul 2025 11:33:14 +0200 Subject: [PATCH 1956/2374] Added flat image metadata tests This metadata is available in nanoarrow, but not pyarrow or arro3 --- Tests/test_nanoarrow.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Tests/test_nanoarrow.py b/Tests/test_nanoarrow.py index 3dc540043a8..f0d60954521 100644 --- a/Tests/test_nanoarrow.py +++ b/Tests/test_nanoarrow.py @@ -289,3 +289,26 @@ def test_image_nested_metadata(mode: str, metadata: list[str]) -> None: assert "bands" in parsed_metadata assert parsed_metadata["bands"] == metadata + +@pytest.mark.parametrize( + "mode, metadata", + ( + ("L", ["L"]), + ("I", ["I"]), + ("F", ["F"]), + ), +) +def test_image_flat_metadata(mode: str, metadata: list[str]) -> None: + img = hopper(mode) + + arr = nanoarrow.Array(img) # type: ignore[call-overload] + + assert arr.schema.metadata + assert arr.schema.metadata[b"image"] + + parsed_metadata = json.loads( + arr.schema.metadata[b"image"].decode("utf8") + ) + + assert "bands" in parsed_metadata + assert parsed_metadata["bands"] == metadata From 9e415c787639d50fcfbd9a519174a60e3eb6c1e9 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Jul 2025 17:24:52 +0200 Subject: [PATCH 1957/2374] A way to make nested arrays in nano arrow but detouring through a buffer --- Tests/test_nanoarrow.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Tests/test_nanoarrow.py b/Tests/test_nanoarrow.py index f0d60954521..b08333ae97b 100644 --- a/Tests/test_nanoarrow.py +++ b/Tests/test_nanoarrow.py @@ -190,9 +190,6 @@ class DataShape(NamedTuple): ) -@pytest.mark.xfail( - reason="Support for nested array creation is not available in nanoarrow/python" -) @pytest.mark.parametrize( "mode, data_tp, mask", ( @@ -218,13 +215,13 @@ def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> Non ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] if dtype == fl_uint8_4_type: - # Apparently there's no good way to create this array from python using nanoarrow - # https://github.com/apache/arrow-nanoarrow/issues/620 - # the following lines will fail. - tmp_arr = nanoarrow.c_array( + tmp_arr = nanoarrow.Array( elt * (ct_pixels * elts_per_pixel), schema=nanoarrow.uint8() ) - arr = nanoarrow.Array(tmp_arr, schema=dtype) + c_array = nanoarrow.c_array_from_buffers( + dtype, ct_pixels, buffers=[], children=[tmp_arr] + ) + arr = nanoarrow.Array(c_array) else: arr = nanoarrow.Array( nanoarrow.c_array([elt] * (ct_pixels * elts_per_pixel), schema=dtype) @@ -290,6 +287,7 @@ def test_image_nested_metadata(mode: str, metadata: list[str]) -> None: assert "bands" in parsed_metadata assert parsed_metadata["bands"] == metadata + @pytest.mark.parametrize( "mode, metadata", ( @@ -306,9 +304,7 @@ def test_image_flat_metadata(mode: str, metadata: list[str]) -> None: assert arr.schema.metadata assert arr.schema.metadata[b"image"] - parsed_metadata = json.loads( - arr.schema.metadata[b"image"].decode("utf8") - ) + parsed_metadata = json.loads(arr.schema.metadata[b"image"].decode("utf8")) assert "bands" in parsed_metadata assert parsed_metadata["bands"] == metadata From f4d86e4f44dfe799b0a2d6484fa53945ab89220e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 24 Jul 2025 07:27:39 +1000 Subject: [PATCH 1958/2374] Use teardown_method --- Tests/test_image_access.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 2609b1e342a..a847264d27e 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -276,11 +276,10 @@ def test_embeddable(self) -> None: except Exception: pytest.skip("Compiler could not be initialized") - try: - with open("embed_pil.c", "w", encoding="utf-8") as fh: - home = sys.prefix.replace("\\", "\\\\") - fh.write( - f""" + with open("embed_pil.c", "w", encoding="utf-8") as fh: + home = sys.prefix.replace("\\", "\\\\") + fh.write( + f""" #include "Python.h" int main(int argc, char* argv[]) @@ -302,19 +301,20 @@ def test_embeddable(self) -> None: return 0; }} """ - ) + ) + + objects = compiler.compile(["embed_pil.c"]) + compiler.link_executable(objects, "embed_pil") - objects = compiler.compile(["embed_pil.c"]) - compiler.link_executable(objects, "embed_pil") + env = os.environ.copy() + env["PATH"] = sys.prefix + ";" + env["PATH"] - env = os.environ.copy() - env["PATH"] = sys.prefix + ";" + env["PATH"] + # Do not display the Windows Error Reporting dialog + getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) - # Do not display the Windows Error Reporting dialog - getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) + process = subprocess.Popen(["embed_pil.exe"], env=env) + process.communicate() + assert process.returncode == 0 - process = subprocess.Popen(["embed_pil.exe"], env=env) - process.communicate() - assert process.returncode == 0 - finally: - os.remove("embed_pil.c") + def teardown_method(self) -> None: + os.remove("embed_pil.c") From 103a5a0b5913ab403eeaaf0b589278bc8e2b067b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 23 Jul 2025 18:22:10 +1000 Subject: [PATCH 1959/2374] Fixed ZeroDivisionError --- Tests/test_imagestat.py | 10 ++++++++++ src/PIL/ImageStat.py | 13 ++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 0dfbc5a2abb..0baab7ce21e 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -57,3 +57,13 @@ def test_constant() -> None: assert st.rms[0] == 128 assert st.var[0] == 0 assert st.stddev[0] == 0 + + +def test_zero_count() -> None: + im = Image.new("L", (0, 0)) + + st = ImageStat.Stat(im) + + assert st.mean == [0] + assert st.rms == [0] + assert st.var == [0] diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index 8bc504526f0..3a1044ba449 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -120,7 +120,7 @@ def sum2(self) -> list[float]: @cached_property def mean(self) -> list[float]: """Average (arithmetic mean) pixel level for each band in the image.""" - return [self.sum[i] / self.count[i] for i in self.bands] + return [self.sum[i] / self.count[i] if self.count[i] else 0 for i in self.bands] @cached_property def median(self) -> list[int]: @@ -141,13 +141,20 @@ def median(self) -> list[int]: @cached_property def rms(self) -> list[float]: """RMS (root-mean-square) for each band in the image.""" - return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands] + return [ + math.sqrt(self.sum2[i] / self.count[i]) if self.count[i] else 0 + for i in self.bands + ] @cached_property def var(self) -> list[float]: """Variance for each band in the image.""" return [ - (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] + ( + (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] + if self.count[i] + else 0 + ) for i in self.bands ] From 24681a39270ee09dc869711d39b9ae183e981ae7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 21 Jul 2025 17:09:26 +1000 Subject: [PATCH 1960/2374] Added ImageText --- Tests/test_imagetext.py | 41 ++++ docs/reference/ImageText.rst | 30 +++ docs/reference/index.rst | 1 + src/PIL/ImageDraw.py | 390 +++++++++-------------------------- src/PIL/ImageText.py | 318 ++++++++++++++++++++++++++++ src/PIL/_typing.py | 2 + 6 files changed, 490 insertions(+), 292 deletions(-) create mode 100644 Tests/test_imagetext.py create mode 100644 docs/reference/ImageText.rst create mode 100644 src/PIL/ImageText.py diff --git a/Tests/test_imagetext.py b/Tests/test_imagetext.py new file mode 100644 index 00000000000..3a3a58975d1 --- /dev/null +++ b/Tests/test_imagetext.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import pytest + +from PIL import ImageFont, ImageText + +from .helper import skip_unless_feature + +FONT_PATH = "Tests/fonts/FreeMono.ttf" + + +@pytest.fixture( + scope="module", + params=[ + pytest.param(ImageFont.Layout.BASIC), + pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), + ], +) +def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout: + return request.param + + +@pytest.fixture(scope="module") +def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont: + return ImageFont.truetype(FONT_PATH, 20, layout_engine=layout_engine) + + +def test_get_length(font: ImageFont.FreeTypeFont) -> None: + assert ImageText.ImageText("A", font).get_length() == 12 + assert ImageText.ImageText("AB", font).get_length() == 24 + assert ImageText.ImageText("M", font).get_length() == 12 + assert ImageText.ImageText("y", font).get_length() == 12 + assert ImageText.ImageText("a", font).get_length() == 12 + + +def test_get_bbox(font: ImageFont.FreeTypeFont) -> None: + assert ImageText.ImageText("A", font).get_bbox() == (0, 4, 12, 16) + assert ImageText.ImageText("AB", font).get_bbox() == (0, 4, 24, 16) + assert ImageText.ImageText("M", font).get_bbox() == (0, 4, 12, 16) + assert ImageText.ImageText("y", font).get_bbox() == (0, 7, 12, 20) + assert ImageText.ImageText("a", font).get_bbox() == (0, 7, 12, 16) diff --git a/docs/reference/ImageText.rst b/docs/reference/ImageText.rst new file mode 100644 index 00000000000..ad5439751f9 --- /dev/null +++ b/docs/reference/ImageText.rst @@ -0,0 +1,30 @@ +.. py:module:: PIL.ImageText +.. py:currentmodule:: PIL.ImageText + +:py:mod:`~PIL.ImageText` module +=============================== + +The :py:mod:`~PIL.ImageText` module defines a class with the same name. Instances of +this class provide a way to use fonts with text strings or bytes. The result is a +simple API to apply styling to pieces of text and measure them. + +Example +------- + +:: + + from PIL import Image, ImageDraw, ImageFont, ImageText + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 24) + + text = ImageText.ImageText("Hello world", font) + text.embed_color() + text.stroke(2, "#0f0") + + print(text.get_length()) # 154.0 + print(text.get_bbox()) # (-2, 3, 156, 22) + +Methods +------- + +.. autoclass:: PIL.ImageText.ImageText + :members: diff --git a/docs/reference/index.rst b/docs/reference/index.rst index effcd3c46a1..1ce26c909e8 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -24,6 +24,7 @@ Reference ImageSequence ImageShow ImageStat + ImageText ImageTk ImageTransform ImageWin diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e95fa91f8b3..35ecbfb78b4 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -35,10 +35,10 @@ import struct from collections.abc import Sequence from types import ModuleType -from typing import Any, AnyStr, Callable, Union, cast +from typing import Any, AnyStr, Callable, cast -from . import Image, ImageColor -from ._typing import Coords +from . import Image, ImageColor, ImageText +from ._typing import Coords, _Ink # experimental access to the outline API Outline: Callable[[], Image.core._Outline] = Image.core.outline @@ -47,8 +47,6 @@ if TYPE_CHECKING: from . import ImageDraw2, ImageFont -_Ink = Union[float, tuple[int, ...], str] - """ A simple 2D drawing interface for PIL images.

BMP Suite Image List

- -

For BMP Suite -version 2.3

- -

This document describes the images in BMP Suite, and shows what -I allege to be the correct way to interpret them. PNG and JPEG images are -used for reference. -

- -

It also shows how your web browser displays the BMP images, -but that’s not its main purpose. -BMP is poor image format to use on web pages, so a web browser’s -level of support for it is arguably not important.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileVer.Correct displayIn your browserNotes
g/pal1.bmp31 bit/pixel paletted image, in which black is the first color in - the palette.
g/pal1wb.bmp31 bit/pixel paletted image, in which white is the first color in - the palette.
g/pal1bg.bmp31 bit/pixel paletted image, with colors other than black and white.
q/pal1p1.bmp31 bit/pixel paletted image, with only one color in the palette. - The documentation says that 1-bpp images have a palette size of 2 - (not “up to 2”), but it would be silly for a viewer not to - support a size of 1.
q/pal2.bmp3A paletted image with 2 bits/pixel. Usually only 1, 4, - and 8 are allowed, but 2 is legal on Windows CE.
g/pal4.bmp3Paletted image with 12 palette colors, and 4 bits/pixel.
g/pal4rle.bmp34-bit image that uses RLE compression.
q/pal4rletrns.bmp3
- or

- or
An RLE-compressed image that used “delta” - codes to skip over some pixels, leaving them undefined. Some viewers - make undefined pixels transparent, others make them black, and - others assign them palette color 0 (purple, in this case).
g/pal8.bmp3Our standard paletted image, with 252 palette colors, and 8 - bits/pixel.
g/pal8-0.bmp3Every field that can be set to 0 is set to 0: pixels/meter=0; - colors used=0 (meaning the default 256); size-of-image=0.
g/pal8rle.bmp38-bit image that uses RLE compression.
q/pal8rletrns.bmp3
- or

- or
8-bit version of q/pal4rletrns.bmp.
g/pal8w126.bmp3Images with different widths and heights. - In BMP format, rows are padded to a multiple of four bytes, so we - test all four possibilities.
g/pal8w125.bmp3
g/pal8w124.bmp3
g/pal8topdown.bmp3BMP images are normally stored from the bottom up, but - there is a way to store them from the top down.
q/pal8offs.bmp3A file with some unused bytes between the palette and the - image. This is probably valid, but I’m not 100% sure.
q/pal8oversizepal.bmp3An 8-bit image with 300 palette colors. This may be invalid, - because the documentation could - be interpreted to imply that 8-bit images aren’t allowed - to have more than 256 colors.
g/pal8nonsquare.bmp3 -
- or
- -
An image with non-square pixels: the X pixels/meter is twice - the Y pixels/meter. Image editors can be expected to - leave the image “squashed”; image viewers should - consider stretching it to its correct proportions.
g/pal8os2.bmpOS/2v1An OS/2-style bitmap.
q/pal8os2sp.bmpOS/2v1An OS/2v1 with a less-than-full-sized palette. - Probably not valid, but such files have been seen in the wild.
q/pal8os2v2.bmpOS/2v2My attempt to make an OS/2v2 bitmap.
q/pal8os2v2-16.bmpOS/2v2An OS/2v2 bitmap whose header has only 16 bytes, instead of the full 64.
g/pal8v4.bmp4A v4 bitmap. I’m not sure that the gamma and chromaticity values in - this file are sensible, because I can’t find any detailed documentation - of them.
g/pal8v5.bmp5A v5 bitmap. Version 5 has additional colorspace options over v4, so it - is easier to create, and ought to be more portable.
g/rgb16.bmp3A 16-bit image with the default color format: 5 bits each for red, - green, and blue, and 1 unused bit. - The whitest colors should (I assume) be displayed as pure white: - (255,255,255), not - (248,248,248).
g/rgb16-565.bmp3A 16-bit image with a BITFIELDS segment indicating 5 red, 6 green, - and 5 blue bits. This is a standard 16-bit format, even supported by - old versions of Windows that don’t support any other non-default 16-bit - formats. - The whitest colors should be displayed as pure white: - (255,255,255), not - (248,252,248).
g/rgb16-565pal.bmp3A 16-bit image with both a BITFIELDS segment and a palette.
q/rgb16-231.bmp3An unusual and silly 16-bit image, with 2 red bits, 3 green bits, and 1 - blue bit. Most viewers do support this image, but the colors may be darkened - with a yellow-green shadow. That’s because they’re doing simple - bit-shifting (possibly including one round of bit replication), instead of - proper scaling.
q/rgba16-4444.bmp5A 16-bit image with an alpha channel. There are 4 bits for each color - channel, and 4 bits for the alpha channel. - It’s not clear if this is valid, but I can’t find anything that - suggests it isn’t. -
g/rgb24.bmp3A perfectly ordinary 24-bit (truecolor) image.
g/rgb24pal.bmp3A 24-bit image, with a palette containing 256 colors. There is little if - any reason for a truecolor image to contain a palette, but it is legal.
q/rgb24largepal.bmp3A 24-bit image, with a palette containing 300 colors. - The fact that the palette has more than 256 colors may cause some viewers - to complain, but the documentation does not mention a size limit.
q/rgb24prof.bmp5My attempt to make a BMP file with an embedded color profile.
q/rgb24lprof.bmp5My attempt to make a BMP file with a linked color profile.
q/rgb24jpeg.bmp5My attempt to make BMP files with embedded JPEG and PNG images. - These are not likely to be supported by much of anything (they’re - intended for printers).
q/rgb24png.bmp5
g/rgb32.bmp3A 32-bit image using the default color format for 32-bit images (no - BITFIELDS segment). There are 8 bits per color channel, and 8 unused - bits. The unused bits are set to 0.
g/rgb32bf.bmp3A 32-bit image with a BITFIELDS segment. As usual, there are 8 bits per - color channel, and 8 unused bits. But the color channels are in an unusual - order, so the viewer must read the BITFIELDS, and not just guess.
q/rgb32fakealpha.bmp3
- or
- -
Same as g/rgb32.bmp, except that the unused bits are set to something - other than 0. - If the image becomes transparent toward the bottom, it probably means - the viewer uses heuristics to guess whether the undefined - data represents transparency.
q/rgb32-111110.bmp3A 32 bits/pixel image, with all 32 bits used: 11 each for red and - green, and 10 for blue. As far as I know, this is perfectly valid, but it - is unusual.
q/rgba32.bmp5A BMP with an alpha channel. Transparency is barely documented, - so it’s possible that this file is not correctly formed. - The color channels are in an unusual order, to prevent viewers from - passing this test by making a lucky guess.
q/rgba32abf.bmp3An image of type BI_ALHPABITFIELDS. Supposedly, this was used on - Windows CE. I don’t know whether it is constructed correctly.
b/badbitcount.bmp3N/AHeader indicates an absurdly large number of bits/pixel.
b/badbitssize.bmp3N/AHeader incorrectly indicates that the bitmap is several GB in size.
b/baddens1.bmp3N/ADensity (pixels per meter) suggests the image is much - larger in one dimension than the other.
b/baddens2.bmp3N/A
b/badfilesize.bmp3N/AHeader incorrectly indicates that the file is several GB in size.
b/badheadersize.bmp?N/AHeader size is 66 bytes, which is not a valid size for any known BMP - version.
b/badpalettesize.bmp3N/AHeader incorrectly indicates that the palette contains an absurdly large - number of colors.
b/badplanes.bmp3N/AThe “planes” setting, which is required to be 1, is not 1.
b/badrle.bmp3N/AAn invalid RLE-compressed image that tries to cause buffer overruns.
b/badwidth.bmp3N/AThe image claims to be a negative number of pixels in width.
b/pal8badindex.bmp3N/AMany of the palette indices used in the image are not present in the - palette.
b/reallybig.bmp3N/AAn image with a very large reported width and height.
b/rletopdown.bmp3N/AAn RLE-compressed image that tries to use top-down orientation, - which isn’t allowed.
b/shortfile.bmp3N/AA file that has been truncated in the middle of the bitmap.
- - - - diff --git a/Tests/images/bmp/html/fakealpha.png b/Tests/images/bmp/html/fakealpha.png deleted file mode 100644 index 89292bcbb4804bf7ab9f0a46b48c3664235b96ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1181 zcmeAS@N?(olHy`uVBq!ia0vp^^+4>v!3HFaC$7i_QjEnx?oJHr&dIz4aySb-B8wRq zxP?KOkzv*x2?hohCr=m0kcwMx=h*uQ2g)4Zf8HYIvC`t4O$XIhFBTS>9ih~$#xtX2 zLPpL~l>%L<7l}#=Zi|^!+Gbc(F?W_;x)^A7BcWN)%;VRj=Z6y4@7;Yb_I-cu{`a3I zWxxM^Z}0ctd+t~7|GxWM3zLWm?*$Wu$p@eAyqs=t#CV#6BSYMsZRY9X<>%{Cnx+Xd zWtPd`zI~g)MeFA(7L`jrFYnoYWz$$1wA=ilupM^vo#~HNfdAI-U$;EGp80c2%^`IR z#6Mxv;>Gi>pV#jTp7U!}ep>pmueTPzQD!*seeU~-hs@9FFg*D9c<$-SC@eyEd0LeqT|{^6AGVm_j)c3 zQMtM5{0T{+v+jrgzI*a+OI-fM?|J{$$6H3v{r39fm(?Lva*=c6ECYW8K6P`Qs(!lf zgYL}Rleaq`{{Odn)6+V?50775zZ3I^Sz&5sJjcb2%}b{I3hytOF6xx<<)Oy6eQ#K2 zeJd|t9wTEkGh+XvGPd1$XT_biXt`EthjKJdTq!71WA}lzzP|pJ^G~CVbze>X_%5Ar z|I_#7y;1wbZl!N#J8EGoQ+m>-SS^~ zjmO|a3Pbw)^Es{l{_}6HIa~X8+FbJ!%S*q1*&@%fFVe*NQt4-g6E>$E_J%U3oIBrb zchH@Ge~jLD^*@hVCpEkLn8g@+Kcx2if=Y&tMFnfO3vUn>KDv(MZ`suC_jx?xFV=3a z%TMK3=2q5aKCtrM!-t}8-s`LU(mB9c;(zGFl|`v%DXg=Y`aS$jsj*b#Q-8=9gtp z+k0C~{jR@0{rbZ5uL0IglYa{H?2+5B_3^K-Kl|qH^i!(o%`7amoanwccIz|q_1BMO zGhYody;AmJO-b9CEH;J(nM)b6D{Ee@3W;MB=(K(8FIwlQnIlr`_QNczk9onKsAExy zx+2D*0RqylEM;=H+ z-0{bM+nVCjDu3dd_KPYVa{p2L>g{f=?hm`!>#j^RIP_llbUs^rZGh^ZyaxGDmNQwf3r+`sj#(G(tC`$Z@<6 z)a^{*MpU-~{QHd!K<$AFQ=>Uciv$P6So z|6H>LCV`X`ctjR6FmMZlFeAgPITAoYAx{^_5R22v2@+ P$})Jm`njxgN@xNAt6>@< diff --git a/Tests/images/bmp/html/pal2.png b/Tests/images/bmp/html/pal2.png deleted file mode 100644 index 1bbfe175fc351d825b92a04467a466c71f12a682..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 961 zcmV;y13vtTP)Px#32;bRa{vGf6951U69E94oEQKA17=A?K~zY`g_OZ=+CUJ7hXkR5Dj5z`O7FdJ zi24AQYAMb=k-S#8^?{%=pmM{Pd!8^ED{fZ4-j}84#xMOx-+wDV?&~hkzmaF z-_QTdEJPSGP4krFU+##IZtQ0&p272cDP@u;7_YB+E`;zYmnxot6&C4UDWv;bF^po& zBaV0iuImS^Fh_|ME4YD7R&|B+7`A4>+p~xmgy&C5VTpkh`TYkBseM~zh$bLh+)~bI z&pBeO`phstQP|<5yBb4!R1ki-Cs81SNBMLr>3U)X+B03e#~@r^GmxW6ug?rC8Q?_> zVW=2(6r^B_Q-3;lDXV|*#2o;PDWTY@(-4qNqS;-22P~mcOvqpwNnwAXrYy`hW<{OaV$5D_T1u>8U zEx>1tPo@dTK(ARS^b`YG)AEguk)aQ4)P<`VxO(hlJ2KkiwUx1xhjyWGUohaC8=b$P zorG@%gG~p5f<%Q$(^3qLq93pa1^FPvo%sc80ib+vacD7|f?*SekLW{NT9)#m?5J*! z8BoD7`$kJL1J=UxN(LIn@rJfQg_s!zm7Z+$N+*?qW)LcvA=LeFK!xax0h9u_T#Z5C zRbk+!9iqY+12jm1=b|B?72vl6RR+tx4fR~~Lm&p2z&V3Vs3!Pps`6#76-Ln1hmy@j zi6YKrjR6k?QlO$>Yfw(-LzO{9F-^7Y(3mPFWC;19IaLf@Dhi&NDo^rKp~App`X&P& zQVABN{|aAntk8T#0f00000NkvXXu0mjf1fZdS diff --git a/Tests/images/bmp/html/pal4rletrns-0.png b/Tests/images/bmp/html/pal4rletrns-0.png deleted file mode 100644 index b689c842aa01a7d24c6bcad01bf1864a72e15544..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1441 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 UzIreXm;e9(07*qoM6N<$f*WyV$^ZZW diff --git a/Tests/images/bmp/html/pal4rletrns.png b/Tests/images/bmp/html/pal4rletrns.png deleted file mode 100644 index 9b0c044364123ce0c527082f3460f62380b96ec5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1465 zcmV;q1xEUbP)Px#Cs0gOMF0Q*00000fB*mh|Nj600RI30fdBu10RI600RR90{{a7h|NsC0&Xr&| z00001bW%=J06^y0W&i*H32;bRa{vGf6951U69E94oEQKA1s+L6K~z}7#gxxW<4hRG z)Bm6o#UFUt8EAq{E=AaTLD;+C&BI1PSnQ=D2x~9i3O#La1y3GJ51S~i&>q%-Rn+`Z z`+X*p&P;6g-QD+v_jw#mon+>dPrj+QyL+~Kdv<&F^z8|i=liH1)T;7)XXj|==IG|= z@$M0o=Q4BptGA2#@ZtRY;{4)b)+-jEI@mur$PZ|Not^jZkB?7|Pfli?JdcFcLG%s_ zw9n3NyWhS&Nf1aZum^-a>@I0wf!5K{O>%elC_x}|iEXX%T@h%69suS91wuif5jp_K zv9Z7if+(N?g7#VawhM#|2r_>Lz}^pPNu9p}pmmb~Ap?TUUzR#JuQ=GRI5=M9K)^w} z-R`0?x+B!{UQtrlYpc~tQ04rK3t&4qzv%U5E`j6V_@vXpJPZUyt0IDKJME@jB~?Bv z0!du}fFNn5NtzV%FAE*40w6>VA~?9(ZxnsdIEeE&pJD$M00hWE1P7DF4$_JPS#gj$ z4l-*H!73cALqUifT;+|j;zk_LW*}IBgEc4!kb?<1APYiUFb=YRaF8u^unGt3=OHXa zvpBzM7=(BR1OTi6AbSv zR+f)#SQJgCQ{{lIIa5{^+#EbGbDESiv;&qV)>Op-g9ASlun;75Pgjiz!PpcCD{KW9 z1>BwrOuIRFn4jfgdsg$cgE~u6d&+UZ<_JN|>otw()qKG`1_uOyYuqW51vdu|^RwzT zKciD$rv%`^fjwnK(Kv{dgW9HA8)t2SfDj14BL}suu-BZ7X@S5*(apgFEwG2#2ik^t z_zi-3%_9e71UaAs*MWL6J}tS;O9CAD5pEp_O#LATezQ3qkLf^&xrIs&u)rP=q;^1e z0svMx@YTD(77_Sr%^)zQJj#K`=0Ael62^q z3FsVHK@z5v0|o~iIT)HGGOh?R+BhK;JIa{L5C=v^os=o|>QK)~RD1L2Sy)HXRPCE%1W?(bWr zgPMOWuV2O)$Gw~WSk1wv@X$ti z8-tC)K`|e`aUfI|aHwj;i%NCf>-p2uYgOaaEjI_-pGG5d7aJQJ{eIufhs$!n zafb7J=Csqe8sg4Z<#f&{>e zaXlPx#32;bRa{vGf6951U69E94oEQKAEZs>&K~#9!oxFdHB*%5<`H6_E?&|8_Q|g}S z?99&U%pQt8B!{#pjYg)Zg=7=Z(z`GS)4=WC3cKq+Bp9M>?&=^}KK}5KJX)6RwIox* znmXK`_YMcFZNP_xVEc};d?I0sl9rY@99l9tnf1Gd(>*=aJyn(UGc)gx%&M;H?xB5` zn2Gt$H(x})tc-Z^;>F7p5dho`SK!{z~A!0dq>*yp}P*%zyBPsUgqL?fY<-{_3yp@J>D0~fAARoAbl@B#hr8dm37BniecUD`F9RAWI7tfd*uW z8juALKm^e7Sy<-S(V_F6Vf!swr#T&$F%yyWglyazG-zbW#^tz-kH5@h``shj)ABi; zjkPD%KZ&PvnOD$GjJgMqyxs1f(85t z1b!1(o;(Xcn-x}Q4@VcoShU3m(;-WiHUbf~qeGd_m`s~xmXot%?SQ{B)MZ?+@w*}N zvk_!`aX@tBE*Bv35zzFPnK%o6UDQX9c!-yQ`k_HRj^qDF$M8r-3?Db-0bS1n!pc)j z7{kt2N?_;lFv*v}u<{Wona9H*Ujdvv1=gK>;3COafn1&d)5(|Jq#dA&j!qQ|t9Y## zGd6aqTC%adsuy=&3yUfenJ`rxoWiw&gX31+n8}d~s)2*!UQ3vGkmx#)w*XxC)cpvth~FGB%j30ZgMbbQ zXhULf@Y<~{W8f)&3)}P6 zmWii2TcYa?T3gcbd~M6XQ;jVG&u?y-j#t*U*6~v9EplGV-7+0dXG zZl!C5mh>eXv?{H^#=x)nwqdJ^BBbMYx*OeArR8imT1~^CQE3EW(Da+mhND$9HfYz{ zIV0y*TqkriUt@zy&%Fg(Vb}B5wz^f%TH2C$u+?gN17piqp3Ro>J5+*V)?LH!7swBFo3xlVJu z^*sbah$H;?pL(;-Nf+4VA69=4fiJ}3?Pcaf{Wv+a*9`$8cC_jZF|_9=V=yMX7I6v{Y46)HHoUBGh;3Kblj0)b;Xg{teUQwVWT z1qzPo6v}Sg4klDVLKVxjiWFnUHtj)mU`yMpdYAypR59;Wy;$Bcoq)hG9k=SnwUkta zYQZrbmxKWa3g`~k3zqH_Da4FzID<;T#_=d%I+P2s6}EN9Beul_2PdH5n2uX5uidW^br9i@VfFAV2!9M8 zmdgU_2Z*^5Qy-T(?8xKYxdDRJ=IV+3xcstMKEh9Q{|1M;?AJ*Ps1qj4>Jd%-W4a@j zV|-9&E@A3NWrqF#V7R0~a4=fujpK0{I7lr180H98pMXBJyYzdsSGs+y_CWR(?RKkg zY_^rx$8P(oZ_+Lg`i5!;&3@l*`&!?itr~sow(s@>r@gB6CG9BaquQ-z-*nna>%%w7 zmk1#OMYlb)`vY2?)@HcrMV_r})mDTI{DB&%W~XU~wian98tsN1+OF?914rvFX}^>9*j$?xi)h9}L5%Y>p~VU-w4e#iv5wqI zR=qKwZDd0c9_Pp9msu7qF~L&@u`;g9sgt$KQV%nNgOkrfou*i(E{^gvL<5)~S@zrF z<@u47hxw89Kg8k)mMR_sx(+B|K~d=d%CM=F6gGIugfhyohytZ-@Ko&o;y^)}4uPvA zR8iyrWjd7IxMj34e_ykT)gltW*2esO4XRku3J7e?KJ@oBr%)|8(s6NPnH;&8cdIVR zI$Z&}t^iA@BA_e4HdLvCjq(6fQN}V<5Me9j0fwRy&r&$1as!ZxDmfG}Q%+jOpinDd z5C$X5emk5CgV^GO;S6!%U9dcy-IoxZBlG>v#D6mgvd7&JArOm*=Ep|*Hv@yB@7CqY5f4b_?-Rl91*ih5G@h}tb>3?k(#?8sNjBvKtEtZ3jVX-B@M z3?kJ~1W~7{)}5%Vsdb`MThWVJ4P`o!(iHr`f4js6p$h#uzgy{A85U&GR2+$Ji-p#-;eG7wPXH%6(v>iS}|wlCj9+s;7G^4cIfZNd}8;n4TGV& z;bjx{= zkjEel!jbj&Ku*jA@j4*p)tJRk@G;&Q;~mN9LGRN@BcLHi26lC#8~6zJV1BZDEXUoS zIh2qssg#s0rIeJdc-oTCfHpJ|EZRaeg^*&ku`1d^G=z{sG=yray<2hC@EV~C@EW6sibPj zhLQ_X3Z-OAN~KgwN(C)YQcAHTq?EEDrIa8VAP&UP`jc3EXe8EOjLif|XCI#x#^h0- zgaz=rZ1wO~x?3c19Rc)x3(i zH7I-2)_cuxdVa7l)ho+&r#n^NlulWeZRJd#nU-onsQBvf$@`HLENE?Qd?r{JkoR#8t$#8TEDU;iKX3x64c7q+!9Z&_76ya7-_V98 zbmbor>dA`CEpdR*Ci$Vm)2P<2!s1}?=AgeQ(r*UABJg*h!2mRN2fKZ(-y8JM@CW{* z+N0kZeCtu|(fz?b8na?{_Vlc12xQXqTW`O$tS!G$dc!ab*^r?Om&Nieaf<`c;1+1f z#IiCO>JdT;<`{di^U*#6BcVfsgZ`Cpi-)Jqm?-|Ek$Nz{WD}H_D&P)uVsc9G+0J7VpEZs+-ucj z66etiZ|yq5DnvyGrvTfAU8t0t%{b0;XQ7N^ScT|~Z6ynMj+;Mh)qg0t=v{?6T& z=l;w8@ke?1e~S5+2EB*Qo_{F_9s>Rd=AfCB`*Zsv{3mmNGAfzl{QJ&7@NXYbQh5V! z+uY`D{_6kwtAZ+g;%Pk;Km3umLt6fXSYzoA^P4Cnv;>;LseKlqDR%kUERL-byHh=*R{rH6QE zaQ6T-cnKcr4PH8b_Msqn2_Aa!#h0+b1NivGkKg;x&Tnnp`S#y?cN^P@ooI>r&Im7X;Ub)+FysKAohNCs04mqeVVRfia zk%Kxkpw3PXvkq1)lc65bppnQYd_5Og^+g&@zVQeD3-I`_JpNN42rs5mJ^09{pB>); z9BtkLA)!Qugg~#`d;WKxKmT**Z*YTCNhki+fBhElx&PsFZ&cryVMafrODUJ*K;y{w zXLtuobvhQ)`-06kue@Qc&iwlSc-Qm))0ck)7x*_&CzHGU$~P{<=YH*R_^D|n;9`&7 zMK1Q}(T|n72tByi?(|Zri_m-W>5K8e?RL9g`r?-s9#~jyy=Q361v(%aM8xx9GAqQ} zu8DgyFF3URx)|ey_(N#J!raQO_1#Q)$CLn?XCXa(%OEo~6vNTeCp9p0kv99zo37VkACI%zcN2Nuiw(IhF2ZwOo!7*#H0Ti+ADJlyX&`> z${EYnMt1 zETtd4;B-jO(ur}K={dSB-1NZdXzAJc zPOIUjXPkC>dTzcO<4!w0JD=G7I=kBN;<2p~V|d8#X?{v%g=ih&6Z!bibsgB>-v>k( z4sYl7B>-z{Yfn7!#BF)( zB6g|66jBziywjuf%rj4Rx{b9peeC&)J?g^-4>cq@97JOeV6qew8vd9cfBc?@6DxlJ zT4K)`;e?NOfEygt(X^rb5CaEw&4DYRbhwVATURJ59KbQ`E0x#OrVXBB-g%{rI;*JQ z++$yP+f&9i6&%~S!jF{cP<5U4bK`yag%@4`db_(*%PaB7jmu~djc_;!r6z?YfG3`K z;?tl0^mETW2Y6L)duyA*VQY{7`^VGi^e>$MgviFMsiGclD zabJJtnJ33P_*i!yW;jf0_>jR%O&*$Ka5pG=0aMT$aQ-Yo0Pr6#P4(y<)o^6Z1t1cO zFQQ32k`b|$KZel?AR(wBp%4`%RuCX1g^DK)C%~21g)-74CI>pU?t>o@mrM68^?uajKKvi~kKXs_x8C~J{_pJ{sbw2BW#SF5 z@vi#$&wd{G=coSpdE@*Gzx@K&zyJ!MxDGtA&qJU2%pb+VPdxF&SRzpYQwF8rxQ3to zwcq^EAoyub4YmW6>IXpz)gJ^G&#NE^KyA&c&;8!#j%tV<*08Z%a1tzCf=NtRE7;(| zZE&FNLl5*@jcBY_IH|+FjXR(NF19 z+f$rkX?tnm)Pgl_?Q`_kpBQ3eUG=fY9*c-R{Pc(0gZ9q;PVo1^`k8guhnwv4?6Xfs zQEwzX@$0h>-7K@mHO!ie=QA@|zdf@#L%TgcGt;0wpP4yxx(zeXPEB`D_7e^S8i!k2 zT0@Lk{~@lUX@^@{NAa@Mpa4?W3!n+BKvC)xKr`$@r6jQnc$!I}j5Mr56llsW;7Nl* z1r4V_L7Gk>aJ7?Mn-Z`F*U;DWxU5~P#f-UTmamm-e^WbN%d2@9aO}(rFTC(uU;eGn zKmPfrKl}7Ezx~WJfA9>~fhl+bo_OrBfB%`!eE#=;|3AbAec~`R$_2|fLB%2^OQ%Q) zpjO%dC~cgkbiEYc!C(H(U!I^Lo>bDqvlEO*;TT3LHL|ZK;hfeQMQoO6br4fY45tNx zod)J8!q~_lqHJp{^3wuAy&){INUM3muYb8()Myz@>JMGrsta6Zfv`^&eEw)W(cPkxk-@;C@m|00#rQq`YS;hkrneR4Ev zo_gxplR9b(1ewMO8kR55EzVsoUrx`>@#f{MoGWlSJ2UsUmoKZk=HT)-U;eY$=pPxK z4Gvpth(qsa>>8pm8ook|U%N5f1{S{x4!CnG&ibxfUUG7lJFhtn+{~15p$W1E}!HPx7r7-+juxcI{d^ zo&Me5{oQa7X2b0Fg71BJ?!(h_)A)Sq=}#SL@SBf)^OcoX+F|?YUwsM{;z7j>je|Rdh&lir=`s62m71LRmc7NyTuN>9T;GuhY>2*GOH@wQ77x@X?xyj|X zxyuH4@x?^Rc*>1yef62Ix}odfcud=LU8Wtn9@Dn$y5Y2g?~$=lvGw=BmdXu>cxe4S z=Cu>@d2S+7x69|TL^cwwN&Wz_e#A=i?6c1T!S1g57th9yrqINw9CtU*oH=vz=FQlX z`}$K~&rRhRzyZUY+KFT1%*OYh`2L;T$>RX&r_;}BPYy@MZvgk+{q_AT$LkIAd5&q= z>jb@iaPdCX>j&Jg+Wp`Hs@(}b{65w01fZUMRsmh2z{A2vq{s5tMm;>? z=OGXbtv?9AA<~)gNiEL*(QGJ!ty8>?ty@KkwsMMK8+Nhsn!RZk@oe+XVj26aDprd2 zJ-OoBo^5OwE7-PE@k3F=)Jz{QtQ&COTaJwyI(BTO6%sjhqtXJ z{Qc1}2nL_M_}Tm4fB&T~UqbTq<4;TE3om>HfJc_NP>k{?R5NYLMRnFbLBr@!9Y(4} z2tgD9$l+x1!dJf>ub_VB;?EQ-p1qax@NDib;@NB`xZ>Gc+a93}MG7|O;?7>I6zr`b zJ{#_E~X{-u8B>CULvKl$Wuz!~fAMWP$GQDP5Y@c;L?cJ12pU;NVZ zUwr=I2Omx^r}xITs`d_Va~zWlH6f1Sk&Muapunr4pcoYuR?HMDo-!N{S78^+D3f9- zP=+ejLWS*BPzFU@#a1!6s+3a>&6zzU6gKBq}NhizQ#pOUr<+uGu9{bHMul!8rXRZcUm0^2? zwy?d(RX83b*od+8qCjCEVq*-)9be!@XnT+XSdr|;c)W}QxI-qJ%G}yJAf-+6i=>*v zM|$E~_a8bw``OQ)IdkUMf9=j^FenPd)Xg=@)sOrF)n5zrVk9VQK&M z{mi`?UgzO^AO5q~|Lo`9_j7;zdw(1+O&|M}$DaS<^WT2;+jpM5bKBmge8R7o6S*Ij zf9RZb-m1QJr+(*C|KzC`zq1RWe%jpKuUkSDqmzF8VLs3CGtd3nBk#YP4?V<%``{t& zzvsfMeE;G(UcJl*?gH4Hy72w~_WcX*zfcISnmGAV-nQ*4(a6f%R~W+yU$D2%UBR>2 zPH;4albHZ}3n|C)d1NOJnvBXMpC@55F>Z{^vIIe0MuCC7buC1*BfAQCU!NKUIl;e?TmRZIptcOIbudTxwfGA=~ z(2*xk7qO<96INUL`r2~#%suDUHnL|nw%1Nui}zV;tEbQ0chB1T`kD3hwS_YaXYM+) zaAqMQ3ZwAG;k?q=8{1>pJG^XTPaH-@_r&3eAuoVfUdDfmwVFH)wW7pI*or3&t%xVF zDI-mYic&yOkl4iv(g0kEqU_3bXZRY)v7<&DqerJ!tLf-5Q?8YP*Z=1A<27t$bM0ii zU$|DtnK`%OI?AC2ArOMB)ry>!#TphTt+~r!$`gl05b$;(G)^-pgo9Pwe%|QwRv|Q2 z_dW6JhmA1Ggi#|5XP9a11)UB0VK@^64NYaB5k*r` z7)3<*f;34Eh%XY0e-ty65x2oGvi<@Y2MJR;h3lYO1&Z-bFNR&H3{uzyJmNTyP=yNU zb^$MCP^bV-fk2v0q3WLaWButf&+saI2VRA*!mI3mXWxI-_rK%wD*ktpnD3`Q{pll7 z6x0cE9zZEos15V?56yT1J;n8crBgg{nP5V>pibIl3n)0IbgKo{AuK3T1qzOdTaJUj z1sj_(wy>!bZEVWH81{}~&n8FNvxTJM*>DU)rCES=k|buT?T+Qvmr?CSzTiex4E&b9c=_Sx;E>4TFU_xeHI(yhrwg8-j> zfdQeo?YhH1VOT83>Cp!QC$8()5f+i|fP>-^48ijv=XeHnv;AF#y+Q%Pn7Zl*6RIf4J6_|EPW4 z7ryqwAQ}Moy!Rek*hIsv%VYk&(v(|s9pQ|vmThV-IK>DHd!Lt2vio*Wa!+ugu&~=eypqR;^6)SnujCG~J;uk__6RX;ujI1fcyJ8EhV22S^h&`= z`8=l(joan(?4OX&Gr2N-r&a^f>Gc1HmE=3uo`3FnU^}zDIloB@(!)1$;o3Xb;<#As z?>9OP1~7nh>>3@<=NT@cqlq|Ac6jRAwRcY1Ob|51^31VY7h{goQ7k6b^BftDhp)68 z!*OCmMw3tiTh~i(?cO1*QdG2cr}X#SVc4Zg$=1B=KX&gJ`ukt|t=|TwhGRjzXJtIF6GhRHP)`_zy?*Zz9=Y(b3qN#!Xh!DM z%GK?&+f))&zx=f?_lDahIgxv#@`khJxCQsz*>hB42d$**_U>CHOWaY6N{Jd@``Y2J zZ%WD~%dp{P;LBhA+SrUya!lL(ap|1AgK8E2?L;I|KrR>>;eWmQ#XYW3Sb7Oe>O4+l|ZeJ-EY~!5PgdNy`oh9@RJ5y}H z4(PDM&IaZXvyovZ%IYKegs*Q1YiAL?N%m8}1U&cF>kN9FJ)dlhKK0~3ru+Z$#a~`r zT-;pS4E+!ZZIJwI=4Uli`_gk?dWeU9Df3I12~UTS3=HpJX@`!^)J}FW3&cUwg|k_k zp-g7u;Rmz%e*a5fd=B{dFJuQ{zs`;&Hdx%DdF;+&>;s657{0&0Co&t8I6IUXTKS`R z{umDkkwH8NO*l2Ta&vumuDsODS*PZ;n+@hNvrVIMDm8x-doDfIG&wcBa8p=wJ-4aZ zr_$3m4d$k1n)s(?GdI_rx#INx(ha~J4E#YqbN*ud>-&bm$Kcz<<j+4%>%d*Gn3+E~y&8K?Kez}>a1oYR zxwxV)=~EY{SYq+wVtuK;d~tbxX+C{19T42$#s(Xaj7H1oJJ#+v&<-{)ZZ0n^&#cX$ zQI~a1XyeQA810T}l%7rlKYQ_K>x&1=)3IMFK<=?)Po;Bfrx#j_GqW(ueERfWzs|u9 z>r)_@ll5Rn8^2?wh5^gH6e$EClL(j?#*Zej7zHv@V?oU^88MD!9P`$6xQ?w`P>e%A zwqa8lXxQM{CX~^HqEfW=9C(@m6>J;`H04lr?RBF5ApT>n8d2e`C=QyhK`kluHN99X zMr%=2i_A5%U+bG|rdRXAwU8RhP{mp?XXHlvy2hH(tM!aEA2$h|NACjQ}Y} z^ZlwEoBpMbU{5eV>UMEHkBqms<{%2AFdz&G0>UsFk1&pv5mB@kMMNumD;NQ*cLFp7vGR+1+%%IA?cLS`ePh%k)8h%k)( zeaV4fZak)Sh!dyBAHBDN^hZ8THnIM(8#IPFltIQ8pE2t{(58NCJj2O(Zh}cS1PBos`VsM6i8&J)##Z{q%;Y& z-@8N)`p~1>?^-=e_9Z=9{nlo0)9ZV7&sKd!k3oN+dP?u>^SybkuhFB?Z`eKC?YmCT ziJ71u$ewKU8zbh@jo*hJ-H4v-u5>JVk_cq0-PtsHo{DUw@;jKlK{--})Pv4^zo+>T zhHR*gP0tM?M@en9gX{%SB$16)$1!`Fia`GHA1-yx?w~wS8>&<8bj>d1CL8@ux!E;0 z2W3xf3_9gj*R&~n4Dq1ssSUeR_PVA`InRdEDXXqIKPczbhSMo)U2}nQUhXD&V=Hag zJ^X&ii8$NN$lZfu{NfNd5iGZf#_MtuBr7*pkO{tBnZ22nQog&)^3CN)MiV@{&|Y~L zt%o>^keN0I$7B|VxY=GtM3Z&**aNa68(aU;iXtA#+2Kqu93*lNKx9WULX2-|Wk*|G z0)&t$D1p%R5~PGxq9~vQDZ?&RN&>rtmoh1p5r$QYNGVyu69%OUDV!1ovF?-tHzg@m z5#&k)Dbp#H-MF22*3*>^Sjs9=j2T&Hw^7Y4K*=^zrAo<0c$6?xluMQ&h)PnWJW3dXO35KrpyaFzHz*-fR7*KT zK*=#vZaHa3#d`y$MHyu(Y9zCWX;Us*BU~#ULWsStDHYFm2f#xz0;;JBMy` z7bH7ln06+{jY*k$J3=nfZI0?LcgNlFasOnp+z~s+`G#zaGj&lvBC{-(Cw-pb)s`fk z@hf+Cj@(VA4YI+6OdUzoW>L~i_~o%X1}Vov)-ukDi#z$@-geCI9m+6<5BbJeJB>Jj zPjw&%#cr5e^NZDPZpkVlb5Lxza>k&j{2X@CS2>fS>f}VXIB4ahQ}nf*K~Xhw1Vz7@ zGo4~t%dJyPwR7Z(EjMR6MWyB7`qwWZ38T=LemCk`(vp!x(u!IGIq)OjmbQu%l1|j= z%5E!aInvQ04M`(v1Tttw%@Na%+BunXBX`7H+W7+H!fw%D%XO_kE$a=1wD(1+!t)gS*G*yJ$H~-MU zWN7gR1PZ@HZ2iqar`%0uf?$B(!t4+E;Y^V5Hsw5~b2t+O4!)Pfz$ClF=k_k;9zJqW z+*W1|N&N^Pe^6>y?9v_kNYzPrPwde}2=Oj5bx}XcC$|3|(jE4Y_=v^Z%gj+ff#=ZT zCm$~yWd_N;F+3LJ8O{U~K^}296O806sDY(ZyN;z>HHryi*tN>Q!mi<2Cbcq_P_+t{ zZrAWEgIWa(r$%6zPOa)%>(oLlRE>gVI<>MJw}XHh2-v|ARy_|JCZ5v6dd8WH8++43u@>V*K3w;6{*FHZCHa!&BpSmVOo@H zN?25Cwr+XUFf1xH2P>fFn3h|uv2GF86jmW-%1JxK->66|zC{2N)_;ftFztz1A-4EI zz=ZW5@gAm~I1Rw=KBU7h2wy*_PsopS+217%>=Gu->Jd%-W4gm~j1P91OPKlzcZozMsn$!7U@58@}Og=FlhRHyX$Km zgHSbm?5^*20w-M6d`UM7I;gPK^i8L$v<~p(CDxz{eY%~lwQk9(q)#hoZLV#4RnP9* zs-swEP#vhgY6eYv&DN?KeHxvHy>7cz*XcW2ps_|!4OCw>J56WZiDfSF6!>(z^g7*@ z(88C4E~?XNhnt4)sjf{&`5~r1D0h|7X$GO)_cgzZ(P^mA#&`X$6LhrI5a|a|SJ7#< zLdWzq)rHPC|M32}`xAiQ!RQ|1@tMGgt$!cCnOOhW;v1Wj)_;f%o3ba9hs(<0gFZF( z(8oIfSu)}7!Et_woAH}UVs|3X9Rw?Kg>nC9$R8S5K~`o%!sGn7{IXmYhzXwEMEj9O zr%u)`3tt%dm(7lntI0f2|A$3YO^*xXG6o;!nhy4rSLGUF3T5DxIZU#p}iRZAu%v zbiHKjwugrf*RgafAh5B$>s~D5=uY8!!O93K1)_0#%1dJ z^>LZQZQ-PMCZ*>!yOI#Z1AMCKcfa(VH{BDW5kRFqIQ3rMMsf{Kv1P+UNiwMr#ckAnp)idv{(CIpd9Oe-oK zmWCD(2o)3;6qO0e?g%KRC@6_oq4)zySjqsyynmQ?-uKPi@6Mh3y?4$#XEOHg@ikh! zd@%$;Mmu+G4`edROrE|r6LkW<-OnV}sX*VYP*ul@VdlPduV1ha141s$gkdIxF!wOL z@QA<=j4)ZqfH1s(Bd`mMER-;e2~q|};1C#DC}FS*g1ErgLX|)ugkZ9qc^83&Fd~HD z5CjQkYa%GxuC-sBLE{J1%SbsZvj9s12D1xFeQe+Kw~%yDKBu_!j2o9*s=c< z1T8ULm@Fu-z!HKCf8Du#OYpZ(ORpJ(o9;Kikj3NewTre&HebfFxaVY*cf;nd3meQ> z2H!8q(%xn-?A;L-d@b3rtHkW+um7kz;>R94IhTmMItwPg+jhAsY1&KWf2O2GjGlW7 zqEYZ2c>7HLK>i>_o)W-cTEC*;kAX-HY5Gc*lbjs(s(du8uTN3FPZO&li+%CUpXN(z z41Hj?u5~5@$I#C`Cs0EoO z=sPG#`biY zGFBJplaWfUA^j!gdkekB*QXD3_1K;>rnYqHY25XX42g{uOIWv!ZyK`Zx|fs1>VO`; zF>dQv6gB&DnM)HO*pzr)22M+XaCl&0&5nH)Ti?^YIS_sccRyZxd%&oAyXeK&CP@k( z!S^m_MlGk5iJuN#*|ewumKSN}ho0lRdu4gKiOIu2c_1RP~Niv#$PJt{_Ij$O{` zZuw;wZA7{Bn>Dk&M}yU%3s2^jT(IQ!vn3}5sb}h78?&!j>z}Xp3h=Fqd;~|R^`55Z z`b8NpO4_M~+Q(UMxwYVR5Z*dpI@TSEUYXlRO z!6$VuI~x0`Eo|y_HSf}=Mcnic#iP4ka)UD6Wu$}C6Gs_2VZ!&ReovM@*!8g^C|9k> z`J@=h`M3D9_7)Cj4J{sef?6aR(B1r_0^JfVr3Hi!I+2h2g&sUSNc3>|xtCYh)QAsL zlW9slfxrxHDwPgbEiSiNZeOx1TD8 zs{^$&N?*PtZD~uCDo~^Kc}YNHM@H^WgzE--TQ6LIvwH+E?4um zw2nkmV!N#6ZEYF%oo@hCule7yA|2W=Pxy@TBW=Y+FIB z%55M_2aoCx+%op~J9Xc~(RX{QN3ISRh|sNSuv1gOeS;_bR1ll+XM2anH1s}S14X`Ww^wVYZcEbLf&$gal?arefq}SR-eDg?TvC0^~^ZSpq zE-h}EjN8+3yioCq`=U}>AN*k%9Zg?C1;6~G12nyB{B6qGWwC?&K*P!0prW6WIphOb zt!G1ZQLs9|?=QCu6+bj0A|ljh&FuIdN74jMDA1+~9*X!TivQZ$vU{wo(3lk~S2{Us zO`S=NaG*VvE#&B0(u|ZqppKsJe71G7d(mfEo!5-$Tggd{{P9)e9_o38Y#RK#WA7@r z5PqX1F^xPrkh>9F9^+8^_ourx>*|1;t^Dw9)b~q`ctaL6KR}D)L$Qe9vVe`+t+hkF zstu!S-HVh0%vpdSlcZggf>;IQ*#|n@LTCdBlgs)La6Cq z?!={J4(&D*RC%Dvy2^UYbY>!LA}xP)zw?i$|B+;p7OCjlE8SmF5jv>j!145Wn|f>N z#}&9Hbt*MAm0NqSreqbOeJBU_WVU)!P2$_3P8eFIrR4ueNuRZFmY_HR+yU<><)= zZgjFFV>G3UHG64xsruo&B_Cw`*S)>%5O<*B=o&#(S8)c1G#5NJoh&peR8A+DmkIIv zw{LHo4&GLoJNlQ1il&ex`<)GA$YKR=sebx5-bk5LAYwIWGXya;dD_(sVBSJIe@ zzRhGF%%iY3cThczr;tsb^mrOc1y2g@-pG@Rqv~USmk$nJPu1nAJTnvqN$P-~Exk1^ zq5|sKfMM$s?tBD6+&S8NlcndrS0y7GxMLAQp~C#0C3^CLzdCFQn0NvlNmujyrVBdnu)^1)Lh&s*Re+b= zuE`<$8UPg!E}=t(ZC%-}Ct0EScggF;<<=Ty>?yn74H6zYu=rZEkzbMFimPsn?(c&e=aOO-^*)czlUC_&+miP0 z=I>rI;A_IoBt4Y5Yiss(ggZD~b-0>cm_0h$*(b55WIMpV7~a$xQ*Yj(S(Nx==N7|Jz-NTe$J^W(>xJJyXs6Gf?NwV3iT)2W C+Q0+= diff --git a/Tests/images/bmp/html/pal8rletrns-b.png b/Tests/images/bmp/html/pal8rletrns-b.png deleted file mode 100644 index 1ede504d4e57bb6874733944294f2e65ca78c1ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3715 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 TO%d#03lth0vF!Ge?MMC}W~ANJ diff --git a/Tests/images/bmp/html/rgb16-231.png b/Tests/images/bmp/html/rgb16-231.png deleted file mode 100644 index 76efe526e5aba012c98601486a9c8488cd87ad40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2643 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 DL8_22 diff --git a/Tests/images/bmp/html/rgb24.jpg b/Tests/images/bmp/html/rgb24.jpg deleted file mode 100644 index c43698c9b15c34365e85abd4c6b86a3e52ad6310..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2319 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 LNkvXXu0mjfovQ&k diff --git a/Tests/images/bmp/html/rgba32.png b/Tests/images/bmp/html/rgba32.png deleted file mode 100644 index 25e542a6551acf16d58489d2d7fde3a5d423b477..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1229 zcmZ{kdr%T~0LOm>wZyqpG<$h=(?#XV>nULkBNeSCkJ>eSMa%4^DB_ESLSA5HnQTK* zS!$=VBJq_{ouXl)9)x8R^N~OoK_zM-f{8fy=Wg5G=llKM_wM`m_X;5r=K@~?2LQN4 zgolt8leMVe<;xa*2k-Xz#emRBI4lsW*1cOSPATDJIsndyzXbuM!c_pkQX@k4AI%yz z2{DO?2oHJtp4bNUt_neJTGbMCKeu3Wo~nO!?9~IeKyO!QUj5_no($ACW5P72V#H;C zcc)M!u`$$->3Q_B`=embM+8FQR$l$I7}XSYdLcd|Q|+HDZ3r8-sFY=l_Uz<@n}Z^l z>yIlb|4-#n-~Y3MCjY0sP#=dt_JRMzXFXH$crgpp@+FT(27hnfH zJu(I}ef?QBLO!TNjt@1Zn=nZme^4Z?6?_oZHH@XwEtBWc4(Mc0oav3bSECIqyAc{d zbz(wA8vWJFl?{?xjnOrh;huQ&)rr@<`HNsHP7;e=c!kmj5(S%$OTy| z&ZDyXpSr)nL!Na%-hTkm%CMO-=CWkT8~J{+K-ld$TwqZpBN+@3DouYtF6a+-D&h4# z^qi}vLJ)lZVfB{83yh!?7uP>09?})1X2$Ld_FH|ko0G0-XI`7KK2Jh}@`D6X0`xe9 zJG_scKs;Si+Vz0hkgI%^Gtr0N@cr>$_!`srPpM}=O-ZAGcKTgGSZB#OWp(y~H1=Cb zKros8iBi~Lu5fGuS@qNFNdjC^Uo=Z<9TFu%*a+mr(%o;)BOhI>QUW%IQ_XuagEP$_ zNh;5B7|OqcytGxqk~~k!u*7?>G3L;Hs`>7qqp_4Y!Nhi&@B%_)jd?BD)u^%IT-wL? z-N~oaR<71Xf5>iwj5((e+4=CSfw;3Lo25Tj-CLhP^19tDCz`X!&>RLzc5JX^yR@3?vl-yFmfuUD~I*=1I=2q^_2tFVa4v?MPXX7 zmhoTtuG03t`H)j*X1V?L73O+Mm*WJoQjgq-w^2*8%^P+;u}oS&9&cS&JyO$zKdE+- zrs!me+C#s!>7#3hI*i`BvYk~+GTBp3<^Kc&wK1VcO-F Date: Tue, 2 Dec 2025 22:26:23 +1100 Subject: [PATCH 2154/2374] Added release notes for #9070 --- docs/releasenotes/12.1.0.rst | 59 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 60 insertions(+) create mode 100644 docs/releasenotes/12.1.0.rst diff --git a/docs/releasenotes/12.1.0.rst b/docs/releasenotes/12.1.0.rst new file mode 100644 index 00000000000..b6e1810c60a --- /dev/null +++ b/docs/releasenotes/12.1.0.rst @@ -0,0 +1,59 @@ +12.1.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards incompatible changes +============================== + +TODO +^^^^ + +TODO + +Deprecations +============ + +TODO +^^^^ + +TODO + +API changes +=========== + +TODO +^^^^ + +TODO + +API additions +============= + +Specify window in ImageGrab on macOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When using :py:meth:`~PIL.ImageGrab.grab`, a specific window can now be selected on +macOS in addition to Windows. On macOS, this is a CGWindowID:: + + from PIL import ImageGrab + ImageGrab.grab(window=cgwindowid) + +Other changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index f66240c8957..b097770a3f8 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 12.1.0 12.0.0 11.3.0 11.2.1 From 04ee0cc3b1f3d5908efbe460097f133f3d73f2ec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Dec 2025 23:06:18 +1100 Subject: [PATCH 2155/2374] Raise error if subprocess gives non-zero returncode --- Tests/test_imagegrab.py | 18 +++++++++++++----- src/PIL/ImageGrab.py | 21 ++++++++++++++------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 01fa090dc3a..2f46d7c925e 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -60,12 +60,20 @@ def test_grab_invalid_xdisplay(self) -> None: ImageGrab.grab(xdisplay="error.test:0.0") assert str(e.value).startswith("X connection failed") - @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + @pytest.mark.skipif( + sys.platform not in ("darwin", "win32"), reason="macOS and Windows only" + ) def test_grab_invalid_handle(self) -> None: - with pytest.raises(OSError, match="unable to get device context for handle"): - ImageGrab.grab(window=-1) - with pytest.raises(OSError, match="screen grab failed"): - ImageGrab.grab(window=0) + if sys.platform == "darwin": + with pytest.raises(subprocess.CalledProcessError): + ImageGrab.grab(window=-1) + else: + with pytest.raises( + OSError, match="unable to get device context for handle" + ): + ImageGrab.grab(window=-1) + with pytest.raises(OSError, match="screen grab failed"): + ImageGrab.grab(window=0) def test_grabclipboard(self) -> None: if sys.platform == "darwin": diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 4228078b11b..66ee6dd33a3 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -43,25 +43,29 @@ def grab( fh, filepath = tempfile.mkstemp(".png") os.close(fh) args = ["screencapture"] - if window: + if window is not None: args += ["-l", str(window)] elif bbox: left, top, right, bottom = bbox args += ["-R", f"{left},{top},{right-left},{bottom-top}"] - subprocess.call(args + ["-x", filepath]) + args += ["-x", filepath] + retcode = subprocess.call(args) + if retcode: + raise subprocess.CalledProcessError(retcode, args) im = Image.open(filepath) im.load() os.unlink(filepath) if bbox: - if window: + if window is not None: # Determine if the window was in Retina mode or not # by capturing it without the shadow, # and checking how different the width is fh, filepath = tempfile.mkstemp(".png") os.close(fh) - subprocess.call( - ["screencapture", "-l", str(window), "-o", "-x", filepath] - ) + args = ["screencapture", "-l", str(window), "-o", "-x", filepath] + retcode = subprocess.call(args) + if retcode: + raise subprocess.CalledProcessError(retcode, args) with Image.open(filepath) as im_no_shadow: retina = im.width - im_no_shadow.width > 100 os.unlink(filepath) @@ -125,7 +129,10 @@ def grab( raise fh, filepath = tempfile.mkstemp(".png") os.close(fh) - subprocess.call(args + [filepath]) + args.append(filepath) + retcode = subprocess.call(args) + if retcode: + raise subprocess.CalledProcessError(retcode, args) im = Image.open(filepath) im.load() os.unlink(filepath) From b428f7209f81e7010ac419307f600a3995305043 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Dec 2025 23:53:45 +1100 Subject: [PATCH 2156/2374] Open a macOS window on CI --- Tests/test_imagegrab.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 2f46d7c925e..07cb69719d9 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -9,7 +9,7 @@ from PIL import Image, ImageGrab -from .helper import assert_image_equal_tofile, skip_unless_feature +from .helper import assert_image_equal_tofile, on_ci, skip_unless_feature class TestImageGrab: @@ -60,6 +60,30 @@ def test_grab_invalid_xdisplay(self) -> None: ImageGrab.grab(xdisplay="error.test:0.0") assert str(e.value).startswith("X connection failed") + @pytest.mark.skipif( + sys.platform != "darwin" or not on_ci(), reason="Only runs on macOS CI" + ) + def test_grab_handle(self) -> None: + p = subprocess.Popen( + [ + "osascript", + "-e", + 'tell application "Finder"\n' + 'open ("/" as POSIX file)\n' + "get id of front window\n" + "end tell", + ], + stdout=subprocess.PIPE, + ) + stdout = p.stdout + assert stdout is not None + window = int(stdout.read()) + + ImageGrab.grab(window=window) + + im = ImageGrab.grab((0, 0, 10, 10), window=window) + assert im.size == (10, 10) + @pytest.mark.skipif( sys.platform not in ("darwin", "win32"), reason="macOS and Windows only" ) From 7adecb792c07023cc6c7b410323bb4eabe5d2a23 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 07:14:12 +0000 Subject: [PATCH 2157/2374] Update dependency mypy to v1.19.0 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 6ca35d28642..5b0e2eaf8d7 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.18.2 +mypy==1.19.0 arro3-compute arro3-core IceSpringPySideStubs-PyQt6 From b633f49b9c58079975628f2a7eb5acf7118483a5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 07:14:19 +0000 Subject: [PATCH 2158/2374] Update actions/checkout action to v6 --- .github/workflows/docs.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test-docker.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- .github/workflows/test-valgrind-memory.yml | 2 +- .github/workflows/test-valgrind.yml | 2 +- .github/workflows/test-windows.yml | 6 +++--- .github/workflows/test.yml | 2 +- .github/workflows/wheels.yml | 8 ++++---- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cf917407c6c..e88abf16fbc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,7 +32,7 @@ jobs: name: Docs steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2addbaf674d..77d1d1caa40 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: name: Lint steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 213062ee2b8..091edb22264 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -68,7 +68,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 6c4206083c5..e247414c8fc 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -45,7 +45,7 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml index 0f36fe30dc2..bd244aa5a57 100644 --- a/.github/workflows/test-valgrind-memory.yml +++ b/.github/workflows/test-valgrind-memory.yml @@ -41,7 +41,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 30caa0d4e51..81cfb84566c 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -39,7 +39,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 02d4da999bf..c4d0fa04605 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -47,19 +47,19 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false - name: Checkout cached dependencies - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false repository: python-pillow/pillow-depends path: winbuild\depends - name: Checkout extra test images - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false repository: python-pillow/test-images diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef7b34b8d10..167faa23930 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,7 +65,7 @@ jobs: name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e33d74a81fe..fb71ead37b5 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -107,7 +107,7 @@ jobs: os: macos-15-intel cibw_arch: x86_64_iphonesimulator steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false submodules: true @@ -154,12 +154,12 @@ jobs: - cibw_arch: ARM64 os: windows-11-arm steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Checkout extra test images - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false repository: python-pillow/test-images @@ -235,7 +235,7 @@ jobs: if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false From fd3d44d2efb40df8066802c545bbecff687ac8be Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Dec 2025 22:38:32 +1100 Subject: [PATCH 2159/2374] Updated zlib-ng to 2.3.2 --- .github/workflows/wheels-dependencies.sh | 12 ++---------- winbuild/build_prepare.py | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 07ea75a75c5..12b5ea6c74a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -103,11 +103,7 @@ XZ_VERSION=5.8.1 ZSTD_VERSION=1.5.7 TIFF_VERSION=4.7.1 LCMS2_VERSION=2.17 -if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "aarch64" ]]; then - ZLIB_NG_VERSION=2.2.5 -else - ZLIB_NG_VERSION=2.3.1 -fi +ZLIB_NG_VERSION=2.3.2 LIBWEBP_VERSION=1.6.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 @@ -150,11 +146,7 @@ function build_zlib_ng { ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS unset HOST_CONFIGURE_FLAGS - if [[ "$ZLIB_NG_VERSION" == 2.2.5 ]]; then - build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat - else - build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --installnamedir=$BUILD_PREFIX/lib --zlib-compat - fi + build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --installnamedir=$BUILD_PREFIX/lib --zlib-compat HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS touch zlib-stamp diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index cd2ef13c11f..4fbb7e82043 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -126,7 +126,7 @@ def cmd_msbuild( "OPENJPEG": "2.5.4", "TIFF": "4.7.1", "XZ": "5.8.1", - "ZLIBNG": "2.3.1", + "ZLIBNG": "2.3.2", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) From 4d511d86edf5b78ec3778061c41c5b7ed8653339 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Dec 2025 19:31:40 +1100 Subject: [PATCH 2160/2374] Changed argument type to match use --- Tests/test_file_libtiff.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 7cb3ea8e40b..cf7676ab1dd 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -11,7 +11,15 @@ import pytest -from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features +from PIL import ( + Image, + ImageFile, + ImageFilter, + ImageOps, + TiffImagePlugin, + TiffTags, + features, +) from PIL.TiffImagePlugin import OSUBFILETYPE, SAMPLEFORMAT, STRIPOFFSETS, SUBIFD from .helper import ( @@ -27,7 +35,7 @@ @skip_unless_feature("libtiff") class LibTiffTestCase: - def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None: + def _assert_noerr(self, tmp_path: Path, im: ImageFile.ImageFile) -> None: """Helper tests that assert basic sanity about the g4 tiff reading""" # 1 bit assert im.mode == "1" From 4024f0287d4e8a1467da8ce4ac86ea087e53af1c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Dec 2025 19:25:47 +1100 Subject: [PATCH 2161/2374] Assert image type --- Tests/test_file_iptc.py | 1 + Tests/test_file_libtiff.py | 1 + Tests/test_file_png.py | 2 ++ src/PIL/Image.py | 3 +++ 4 files changed, 7 insertions(+) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 0376b99977b..9e2d8c06da1 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -143,6 +143,7 @@ def test_getiptcinfo_tiff() -> None: # Test with LONG tag type with Image.open("Tests/images/hopper.Lab.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.tag_v2.tagtype[TiffImagePlugin.IPTC_NAA_CHUNK] = TiffTags.LONG iptc = IptcImagePlugin.getiptcinfo(im) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index cf7676ab1dd..77e0b4bc9cb 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -378,6 +378,7 @@ def test_tag_type( im.save(out, tiffinfo=ifd) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[37000] == 100 def test_inknames_tag( diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 9875fe09676..7f163a4d6d6 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -787,7 +787,9 @@ def test_exif_from_jpg(self, tmp_path: Path) -> None: im.save(test_file, exif=im.getexif()) with Image.open(test_file) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) exif = reloaded._getexif() + assert exif is not None assert exif[305] == "Adobe Photoshop CS Macintosh" def test_exif_argument(self, tmp_path: Path) -> None: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9d50812eb3e..b71395c6234 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1519,6 +1519,9 @@ def getexif(self) -> Exif: "".join(self.info["Raw profile type exif"].split("\n")[3:]) ) elif hasattr(self, "tag_v2"): + from . import TiffImagePlugin + + assert isinstance(self, TiffImagePlugin.TiffImageFile) self._exif.bigtiff = self.tag_v2._bigtiff self._exif.endian = self.tag_v2._endian self._exif.load_from_fp(self.fp, self.tag_v2._offset) From 46ac30aa80e592fb3f58e8094c03547c57e04cd5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Dec 2025 18:42:58 +1100 Subject: [PATCH 2162/2374] Use different variables for Image and ImageFile instances --- Tests/test_file_libtiff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 77e0b4bc9cb..e36b5f39e3c 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1155,9 +1155,9 @@ def test_exif_transpose(self) -> None: with Image.open("Tests/images/g4_orientation_1.tif") as base_im: for i in range(2, 9): with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: - im = ImageOps.exif_transpose(im) + im_transposed = ImageOps.exif_transpose(im) - assert_image_similar(base_im, im, 0.7) + assert_image_similar(base_im, im_transposed, 0.7) @pytest.mark.parametrize( "test_file", From 61b1c3c841341cc6f22ab2caa321b7303f61bd35 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Dec 2025 23:51:00 +1100 Subject: [PATCH 2163/2374] Do not change variable type --- src/PIL/GifImagePlugin.py | 4 ++-- src/PIL/XVThumbImagePlugin.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 58c460ef3db..b00953a9df3 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -116,8 +116,8 @@ def _open(self) -> None: # check if palette contains colour indices p = self.fp.read(3 << bits) if self._is_palette_needed(p): - p = ImagePalette.raw("RGB", p) - self.global_palette = self.palette = p + palette = ImagePalette.raw("RGB", p) + self.global_palette = self.palette = palette self._fp = self.fp # FIXME: hack self.__rewind = self.fp.tell() diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index cde28388ff0..192c041d94e 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -66,10 +66,10 @@ def _open(self) -> None: break # parse header line (already read) - s = s.strip().split() + w, h = s.strip().split(maxsplit=2)[:2] self._mode = "P" - self._size = int(s[0]), int(s[1]) + self._size = int(w), int(h) self.palette = ImagePalette.raw("RGB", PALETTE) From 7c3ece07c9871ca992bd497961e023228a8ebd14 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Dec 2025 19:06:02 +1100 Subject: [PATCH 2164/2374] Changed type so that im has fp attribute --- Tests/test_file_gribstub.py | 2 +- Tests/test_file_hdf5stub.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index 960e5f4be1f..4dbed6b3190 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -59,7 +59,7 @@ class TestHandler(ImageFile.StubHandler): def open(self, im: Image.Image) -> None: self.opened = True - def load(self, im: Image.Image) -> Image.Image: + def load(self, im: ImageFile.ImageFile) -> Image.Image: self.loaded = True im.fp.close() return Image.new("RGB", (1, 1)) diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index e4f09a09c52..1e48597d366 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -61,7 +61,7 @@ class TestHandler(ImageFile.StubHandler): def open(self, im: Image.Image) -> None: self.opened = True - def load(self, im: Image.Image) -> Image.Image: + def load(self, im: ImageFile.ImageFile) -> Image.Image: self.loaded = True im.fp.close() return Image.new("RGB", (1, 1)) From fd1ddd6d56457c2611b5b947af9a2c1d5a9479b2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Dec 2025 19:12:58 +1100 Subject: [PATCH 2165/2374] Use consistent type --- src/PIL/GifImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index b00953a9df3..0560a5a7dc8 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -256,7 +256,7 @@ def _seek(self, frame: int, update_image: bool = True) -> None: info["comment"] += b"\n" + comment else: info["comment"] = comment - s = None + s = b"" continue elif s[0] == 255 and frame == 0 and block is not None: # @@ -299,7 +299,7 @@ def _seek(self, frame: int, update_image: bool = True) -> None: bits = self.fp.read(1)[0] self.__offset = self.fp.tell() break - s = None + s = b"" if interlace is None: msg = "image not found in GIF frame" From db7a994ad65f2e25c0e5c5c35ea72210062a4f2c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Dec 2025 10:15:33 +1100 Subject: [PATCH 2166/2374] Updated libpng to 1.6.53 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 07ea75a75c5..4201c335f35 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -96,7 +96,7 @@ else FREETYPE_VERSION=2.14.1 fi HARFBUZZ_VERSION=12.2.0 -LIBPNG_VERSION=1.6.51 +LIBPNG_VERSION=1.6.53 JPEGTURBO_VERSION=3.1.2 OPENJPEG_VERSION=2.5.4 XZ_VERSION=5.8.1 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index cd2ef13c11f..87cfef7d087 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -121,7 +121,7 @@ def cmd_msbuild( "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.4.1", - "LIBPNG": "1.6.51", + "LIBPNG": "1.6.53", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.4", "TIFF": "4.7.1", From a01fa7d08eacf0d64c47cd95c8846e34281edf1f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 7 Dec 2025 11:12:38 +0200 Subject: [PATCH 2167/2374] Test Python 3.15 pre-release --- .ci/install.sh | 5 ++--- .github/workflows/macos-install.sh | 5 ++--- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 3 +++ tox.ini | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 52b8214170c..aeb5e65145d 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -27,14 +27,13 @@ python3 -m pip install --upgrade wheel python3 -m pip install coverage python3 -m pip install defusedxml python3 -m pip install ipython -python3 -m pip install numpy python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -# optional test dependency, only install if there's a binary package. -# fails on beta 3.14 and PyPy +# optional test dependencies, only install if there's a binary package. +python3 -m pip install --only-binary=:all: numpy || true python3 -m pip install --only-binary=:all: pyarrow || true # PyQt6 doesn't support PyPy3 diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index b114d4a23f8..7c768af483e 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -26,9 +26,8 @@ python3 -m pip install -U pytest python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -python3 -m pip install numpy -# optional test dependency, only install if there's a binary package. -# fails on beta 3.14 and PyPy +# optional test dependencies, only install if there's a binary package. +python3 -m pip install --only-binary=:all: numpy || true python3 -m pip install --only-binary=:all: pyarrow || true # libavif diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index c4d0fa04605..3450de35556 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14"] + python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14", "3.15"] architecture: ["x64"] include: # Test the oldest Python on 32-bit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 167faa23930..da3eea06641 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,8 @@ jobs: ] python-version: [ "pypy3.11", + "3.15t", + "3.15", "3.14t", "3.14", "3.13t", @@ -54,6 +56,7 @@ jobs: - { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - { python-version: "3.11", PYTHONOPTIMIZE: 2 } # Free-threaded + - { python-version: "3.15t", disable-gil: true } - { python-version: "3.14t", disable-gil: true } - { python-version: "3.13t", disable-gil: true } # Intel diff --git a/tox.ini b/tox.ini index d58fd67b613..7f116c6e75f 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ requires = tox>=4.2 env_list = lint - py{py3, 314, 313, 312, 311, 310} + py{py3, 315, 314, 313, 312, 311, 310} [testenv] deps = From 76532808f4b350741669e776598f338e4d2e5a96 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:26:14 +0200 Subject: [PATCH 2168/2374] Fix ResourceWarning in selftest.py --- selftest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/selftest.py b/selftest.py index e9b5689a0d4..c484d4e2d42 100755 --- a/selftest.py +++ b/selftest.py @@ -40,12 +40,14 @@ def testimage() -> None: >>> with Image.open("Tests/images/hopper.gif") as im: ... _info(im) ('GIF', 'P', (128, 128)) - >>> _info(Image.open("Tests/images/hopper.ppm")) + >>> with Image.open("Tests/images/hopper.ppm") as im: + ... _info(im) ('PPM', 'RGB', (128, 128)) >>> try: - ... _info(Image.open("Tests/images/hopper.jpg")) + ... with Image.open("Tests/images/hopper.jpg") as im: + ... _info(im) ... except OSError as v: - ... print(v) + ... print(v) ('JPEG', 'RGB', (128, 128)) PIL doesn't actually load the image data until it's needed, From 9ac4edc54ba0ef720e91f28ba91b93c4f89ad4b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Oct 2025 07:29:51 +1100 Subject: [PATCH 2169/2374] Added wrap() --- Tests/test_imagetext.py | 17 +++++++++++ src/PIL/ImageText.py | 66 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/Tests/test_imagetext.py b/Tests/test_imagetext.py index 2b424629dfd..e9d6d788631 100644 --- a/Tests/test_imagetext.py +++ b/Tests/test_imagetext.py @@ -108,3 +108,20 @@ def test_stroke() -> None: assert_image_similar_tofile( im, "Tests/images/imagedraw_stroke_" + suffix + ".png", 3.1 ) + + +def test_wrap() -> None: + # No wrap required + text = ImageText.Text("Hello World!") + text.wrap(100) + assert text.text == "Hello World!" + + # Wrap word to a new line + text = ImageText.Text("Hello World!") + text.wrap(50) + assert text.text == "Hello\nWorld!" + + # Split word across lines + text = ImageText.Text("Hello World!") + text.wrap(25) + assert text.text == "Hello\nWorl\nd!" diff --git a/src/PIL/ImageText.py b/src/PIL/ImageText.py index e6ccd824332..34c0336c8be 100644 --- a/src/PIL/ImageText.py +++ b/src/PIL/ImageText.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import cast + from . import ImageFont from ._typing import _Ink @@ -88,6 +90,70 @@ def _get_fontmode(self) -> str: else: return "L" + def wrap(self, width: int) -> None: + str_type = isinstance(self.text, str) + wrapped_lines = [] + emptystring = "" if str_type else b"" + fontmode = self._get_fontmode() + for line in self.text.splitlines(): + wrapped_line = emptystring + words = line.split() + while words: + word = words[0] + + new_wrapped_line: str | bytes + if wrapped_line: + if str_type: + new_wrapped_line = ( + cast(str, wrapped_line) + " " + cast(str, word) + ) + else: + new_wrapped_line = ( + cast(bytes, wrapped_line) + b" " + cast(bytes, word) + ) + else: + new_wrapped_line = word + + def get_width(text) -> float: + left, _, right, _ = self.font.getbbox( + text, + fontmode, + self.direction, + self.features, + self.language, + self.stroke_width, + ) + return right - left + + if get_width(new_wrapped_line) > width: + if wrapped_line: + wrapped_lines.append(wrapped_line) + wrapped_line = emptystring + else: + # This word is too long for a single line, so split the word + characters = word + i = len(characters) + while i > 1 and get_width(characters[:i]) > width: + i -= 1 + wrapped_line = characters[:i] + if str_type: + cast(list[str], words)[0] = cast(str, characters[i:]) + else: + cast(list[bytes], words)[0] = cast(bytes, characters[i:]) + else: + words.pop(0) + wrapped_line = new_wrapped_line + if wrapped_line: + wrapped_lines.append(wrapped_line) + if str_type: + self.text = "\n".join( + [line for line in wrapped_lines if isinstance(line, str)] + ) + else: + self.text = b"\n".join( + [line for line in wrapped_lines if isinstance(line, bytes)] + ) + def get_length(self) -> float: """ Returns length (in pixels with 1/64 precision) of text. From 16691657cc6795889fa9712f48c1e36784d0c70e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Nov 2025 00:00:50 +1100 Subject: [PATCH 2170/2374] Added height argument to wrap() --- Tests/test_imagetext.py | 40 ++++--- src/PIL/ImageDraw.py | 25 ++--- src/PIL/ImageText.py | 235 +++++++++++++++++++++++++++------------- 3 files changed, 196 insertions(+), 104 deletions(-) diff --git a/Tests/test_imagetext.py b/Tests/test_imagetext.py index e9d6d788631..9b3c711f4a1 100644 --- a/Tests/test_imagetext.py +++ b/Tests/test_imagetext.py @@ -110,18 +110,28 @@ def test_stroke() -> None: ) -def test_wrap() -> None: - # No wrap required - text = ImageText.Text("Hello World!") - text.wrap(100) - assert text.text == "Hello World!" - - # Wrap word to a new line - text = ImageText.Text("Hello World!") - text.wrap(50) - assert text.text == "Hello\nWorld!" - - # Split word across lines - text = ImageText.Text("Hello World!") - text.wrap(25) - assert text.text == "Hello\nWorl\nd!" +@pytest.mark.parametrize( + "text, width, expected", + ( + ("Hello World!", 100, "Hello World!"), # No wrap required + ("Hello World!", 50, "Hello\nWorld!"), # Wrap word to a new line + ("Hello World!", 25, "Hello\nWorl\nd!"), # Split word across lines + # Keep multiple spaces within a line + ("Keep multiple spaces", 75, "Keep multiple\nspaces"), + ), +) +@pytest.mark.parametrize("string", (True, False)) +def test_wrap(text: str, width: int, expected: str, string: bool) -> None: + text = ImageText.Text(text if string else text.encode()) + assert text.wrap(width) is None + assert text.text == expected if string else expected.encode() + + +def test_wrap_height() -> None: + text = ImageText.Text("Text does not fit within height") + assert text.wrap(50, 25).text == " within height" + assert text.text == "Text does\nnot fit" + + text = ImageText.Text("Text does not fit singlelongword") + assert text.wrap(50, 25).text == " singlelongword" + assert text.text == "Text does\nnot fit" diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 8bcf2d8ee06..dfdbb622d04 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -591,49 +591,49 @@ def getink(fill: _Ink | None) -> int: else ink ) - for xy, anchor, line in image_text._split(xy, anchor, align): + for line in image_text._split(xy, anchor, align): def draw_text(ink: int, stroke_width: float = 0) -> None: mode = self.fontmode if stroke_width == 0 and embedded_color: mode = "RGBA" - coord = [] - for i in range(2): - coord.append(int(xy[i])) - start = (math.modf(xy[0])[0], math.modf(xy[1])[0]) + x = int(line.x) + y = int(line.y) + start = (math.modf(line.x)[0], math.modf(line.y)[0]) try: mask, offset = image_text.font.getmask2( # type: ignore[union-attr,misc] - line, + line.text, mode, direction=direction, features=features, language=language, stroke_width=stroke_width, stroke_filled=True, - anchor=anchor, + anchor=line.anchor, ink=ink, start=start, *args, **kwargs, ) - coord = [coord[0] + offset[0], coord[1] + offset[1]] + x += offset[0] + y += offset[1] except AttributeError: try: mask = image_text.font.getmask( # type: ignore[misc] - line, + line.text, mode, direction, features, language, stroke_width, - anchor, + line.anchor, ink, start=start, *args, **kwargs, ) except TypeError: - mask = image_text.font.getmask(line) + mask = image_text.font.getmask(line.text) if mode == "RGBA": # image_text.font.getmask2(mode="RGBA") # returns color in RGB bands and mask in A @@ -641,13 +641,12 @@ def draw_text(ink: int, stroke_width: float = 0) -> None: color, mask = mask, mask.getband(3) ink_alpha = struct.pack("i", ink)[3] color.fillband(3, ink_alpha) - x, y = coord if self.im is not None: self.im.paste( color, (x, y, x + mask.size[0], y + mask.size[1]), mask ) else: - self.draw.draw_bitmap(coord, mask, ink) + self.draw.draw_bitmap((x, y), mask, ink) if stroke_ink is not None: # Draw stroked text diff --git a/src/PIL/ImageText.py b/src/PIL/ImageText.py index 34c0336c8be..7cd5f957e75 100644 --- a/src/PIL/ImageText.py +++ b/src/PIL/ImageText.py @@ -1,11 +1,18 @@ from __future__ import annotations -from typing import cast +from typing import NamedTuple, cast from . import ImageFont from ._typing import _Ink +class _Line(NamedTuple): + x: float + y: float + anchor: str + text: str | bytes + + class Text: def __init__( self, @@ -90,69 +97,140 @@ def _get_fontmode(self) -> str: else: return "L" - def wrap(self, width: int) -> None: - str_type = isinstance(self.text, str) - wrapped_lines = [] - emptystring = "" if str_type else b"" + def wrap( + self, + width: int, + height: int | None = None, + ) -> Text | None: + wrapped_lines: list[str] | list[bytes] = [] + emptystring = "" if isinstance(self.text, str) else b"" + newline = "\n" if isinstance(self.text, str) else b"\n" fontmode = self._get_fontmode() - for line in self.text.splitlines(): - wrapped_line = emptystring - words = line.split() - while words: - word = words[0] - - new_wrapped_line: str | bytes - if wrapped_line: - if str_type: - new_wrapped_line = ( - cast(str, wrapped_line) + " " + cast(str, word) - ) - else: - new_wrapped_line = ( - cast(bytes, wrapped_line) + b" " + cast(bytes, word) - ) - else: - new_wrapped_line = word - - def get_width(text) -> float: - left, _, right, _ = self.font.getbbox( - text, - fontmode, - self.direction, - self.features, - self.language, - self.stroke_width, - ) - return right - left - if get_width(new_wrapped_line) > width: - if wrapped_line: - wrapped_lines.append(wrapped_line) - wrapped_line = emptystring - else: - # This word is too long for a single line, so split the word - characters = word - i = len(characters) - while i > 1 and get_width(characters[:i]) > width: - i -= 1 - wrapped_line = characters[:i] - if str_type: - cast(list[str], words)[0] = cast(str, characters[i:]) - else: - cast(list[bytes], words)[0] = cast(bytes, characters[i:]) + def getbbox(text) -> tuple[float, float]: + _, _, right, bottom = self.font.getbbox( + text, + fontmode, + self.direction, + self.features, + self.language, + self.stroke_width, + ) + return right, bottom + + wrapped_line = emptystring + word = emptystring + reached_end = False + remaining_position = 0 + + def join_text(a: str | bytes, b: str | bytes) -> str | bytes: + if isinstance(a, str): + return a + cast(str, b) + else: + return a + cast(bytes, b) + + for i in range(len(self.text)): + last_character = i == len(self.text) - 1 + + def add_line() -> bool: + nonlocal wrapped_lines, remaining_position + lines = cast( + list[str] | list[bytes], wrapped_lines + [wrapped_line.rstrip()] + ) + if height is not None: + last_line_y = self._split(lines=lines)[-1].y + last_line_height = getbbox(wrapped_line)[1] + if last_line_y + last_line_height > height: + return False + + wrapped_lines = lines + remaining_position = i - len(word) + if last_character: + remaining_position += 1 + return True + + character = self.text[i : i + 1] + if last_character: + word = join_text(word, character) + character = newline + if character.isspace(): + if not word or word.isspace(): + # Do not use whitespace until a non-whitespace character is reached + # Trimming whitespace from the end of the line + word = join_text(word, character) else: - words.pop(0) - wrapped_line = new_wrapped_line - if wrapped_line: - wrapped_lines.append(wrapped_line) - if str_type: - self.text = "\n".join( - [line for line in wrapped_lines if isinstance(line, str)] + # Append the word to the current line + if not wrapped_line: + word = word.lstrip() + new_wrapped_line = join_text(wrapped_line, word) + if getbbox(new_wrapped_line)[0] > width: + + def split_word(): + nonlocal wrapped_line, word, reached_end + # This word is too long for a single line, so split the word + j = len(word) + while j > 1 and getbbox(word[:j])[0] > width: + j -= 1 + wrapped_line = word[:j] + if not add_line(): + reached_end = True + return + word = word[j:] + wrapped_line = word + if getbbox(wrapped_line)[0] > width: + split_word() + + if wrapped_line: + # This word does not fit on the line + if not add_line(): + reached_end = True + break + word = word.lstrip() + if getbbox(word)[0] > width: + split_word() + else: + wrapped_line = word + else: + split_word() + if reached_end: + break + else: + # This word fits on the line + wrapped_line = new_wrapped_line + word = emptystring + + word = emptystring if character == newline else character + + if character == newline: + if not add_line(): + break + wrapped_line = emptystring + elif not character.isspace(): + # Word is not finished yet + word = join_text(word, character) + + remaining_text = self.text[remaining_position:] + if remaining_text: + text = Text( + text=remaining_text, + font=self.font, + mode=self.mode, + spacing=self.spacing, + direction=self.direction, + features=self.features, + language=self.language, ) + text.embedded_color = self.embedded_color + text.stroke_width = self.stroke_width + text.stroke_fill = self.stroke_fill else: - self.text = b"\n".join( - [line for line in wrapped_lines if isinstance(line, bytes)] - ) + text = None + + if isinstance(self.text, str): + self.text = "\n".join(cast(list[str], wrapped_lines)) + else: + self.text = b"\n".join(cast(list[bytes], wrapped_lines)) + return text def get_length(self) -> float: """ @@ -212,21 +290,26 @@ def get_length(self) -> float: ) def _split( - self, xy: tuple[float, float], anchor: str | None, align: str - ) -> list[tuple[tuple[float, float], str, str | bytes]]: + self, + xy: tuple[float, float] = (0, 0), + anchor: str | None = None, + align: str = "left", + lines: list[str] | list[bytes] | None = None, + ) -> list[_Line]: if anchor is None: anchor = "lt" if self.direction == "ttb" else "la" elif len(anchor) != 2: msg = "anchor must be a 2 character string" raise ValueError(msg) - lines = ( - self.text.split("\n") - if isinstance(self.text, str) - else self.text.split(b"\n") - ) + if lines is None: + lines = ( + self.text.split("\n") + if isinstance(self.text, str) + else self.text.split(b"\n") + ) if len(lines) == 1: - return [(xy, anchor, self.text)] + return [_Line(xy[0], xy[1], anchor, lines[0])] if anchor[1] in "tb" and self.direction != "ttb": msg = "anchor not supported for multiline text" @@ -251,7 +334,7 @@ def _split( if self.direction == "ttb": left = xy[0] for line in lines: - parts.append(((left, top), anchor, line)) + parts.append(_Line(left, top, anchor, line)) left += line_spacing else: widths = [] @@ -314,7 +397,7 @@ def _split( width_difference = max_width - sum(word_widths) i = 0 for word in words: - parts.append(((left, top), word_anchor, word)) + parts.append(_Line(left, top, word_anchor, word)) left += word_widths[i] + width_difference / (len(words) - 1) i += 1 top += line_spacing @@ -325,7 +408,7 @@ def _split( left -= width_difference / 2.0 elif anchor[0] == "r": left -= width_difference - parts.append(((left, top), anchor, line)) + parts.append(_Line(left, top, anchor, line)) top += line_spacing return parts @@ -356,9 +439,9 @@ def get_bbox( """ bbox: tuple[float, float, float, float] | None = None fontmode = self._get_fontmode() - for xy, anchor, line in self._split(xy, anchor, align): + for x, y, anchor, text in self._split(xy, anchor, align): bbox_line = self.font.getbbox( - line, + text, fontmode, self.direction, self.features, @@ -367,10 +450,10 @@ def get_bbox( anchor, ) bbox_line = ( - bbox_line[0] + xy[0], - bbox_line[1] + xy[1], - bbox_line[2] + xy[0], - bbox_line[3] + xy[1], + bbox_line[0] + x, + bbox_line[1] + y, + bbox_line[2] + x, + bbox_line[3] + y, ) if bbox is None: bbox = bbox_line From 4b2d4811e18516469793210e41080d21bf5d33c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Nov 2025 20:30:16 +1100 Subject: [PATCH 2171/2374] Added scaling argument to wrap() --- Tests/test_imagetext.py | 113 ++++++++++++++-- src/PIL/ImageDraw.py | 2 +- src/PIL/ImageText.py | 290 ++++++++++++++++++++++------------------ 3 files changed, 262 insertions(+), 143 deletions(-) diff --git a/Tests/test_imagetext.py b/Tests/test_imagetext.py index 9b3c711f4a1..507d8240918 100644 --- a/Tests/test_imagetext.py +++ b/Tests/test_imagetext.py @@ -111,27 +111,120 @@ def test_stroke() -> None: @pytest.mark.parametrize( - "text, width, expected", + "data, width, expected", ( ("Hello World!", 100, "Hello World!"), # No wrap required ("Hello World!", 50, "Hello\nWorld!"), # Wrap word to a new line - ("Hello World!", 25, "Hello\nWorl\nd!"), # Split word across lines # Keep multiple spaces within a line - ("Keep multiple spaces", 75, "Keep multiple\nspaces"), + ("Keep multiple spaces", 90, "Keep multiple\nspaces"), + (" Keep\n leading space", 100, " Keep\n leading space"), ), ) @pytest.mark.parametrize("string", (True, False)) -def test_wrap(text: str, width: int, expected: str, string: bool) -> None: - text = ImageText.Text(text if string else text.encode()) - assert text.wrap(width) is None - assert text.text == expected if string else expected.encode() +def test_wrap(data: str, width: int, expected: str, string: bool) -> None: + if string: + text = ImageText.Text(data) + assert text.wrap(width) is None + assert text.text == expected + else: + text_bytes = ImageText.Text(data.encode()) + assert text_bytes.wrap(width) is None + assert text_bytes.text == expected.encode() + + +def test_wrap_long_word() -> None: + text = ImageText.Text("Hello World!") + with pytest.raises(ValueError, match="Word does not fit within line"): + text.wrap(25) + + +def test_wrap_unsupported(font: ImageFont.FreeTypeFont) -> None: + transposed_font = ImageFont.TransposedFont(font) + text = ImageText.Text("Hello World!", transposed_font) + with pytest.raises(ValueError, match="TransposedFont not supported"): + text.wrap(50) + + text = ImageText.Text("Hello World!", direction="ttb") + with pytest.raises(ValueError, match="Only ltr direction supported"): + text.wrap(50) def test_wrap_height() -> None: + width = 50 if features.check_module("freetype2") else 60 text = ImageText.Text("Text does not fit within height") - assert text.wrap(50, 25).text == " within height" + wrapped = text.wrap(width, 25 if features.check_module("freetype2") else 40) + assert wrapped is not None + assert wrapped.text == " within height" assert text.text == "Text does\nnot fit" - text = ImageText.Text("Text does not fit singlelongword") - assert text.wrap(50, 25).text == " singlelongword" + text = ImageText.Text("Text does not fit\nwithin height") + wrapped = text.wrap(width, 20) + assert wrapped is not None + assert wrapped.text == " not fit\nwithin height" + assert text.text == "Text does" + + text = ImageText.Text("Text does not fit\n\nwithin height") + wrapped = text.wrap(width, 25 if features.check_module("freetype2") else 40) + assert wrapped is not None + assert wrapped.text == "\nwithin height" assert text.text == "Text does\nnot fit" + + +def test_wrap_scaling_unsupported() -> None: + font = ImageFont.load_default_imagefont() + text = ImageText.Text("Hello World!", font) + with pytest.raises(ValueError, match="'scaling' only supports FreeTypeFont"): + text.wrap(50, scaling="shrink") + + if features.check_module("freetype2"): + text = ImageText.Text("Hello World!") + with pytest.raises(ValueError, match="'scaling' requires 'height'"): + text.wrap(50, scaling="shrink") + + +@skip_unless_feature("freetype2") +def test_wrap_shrink() -> None: + # No scaling required + text = ImageText.Text("Hello World!") + assert isinstance(text.font, ImageFont.FreeTypeFont) + assert text.font.size == 10 + assert text.wrap(50, 50, "shrink") is None + assert isinstance(text.font, ImageFont.FreeTypeFont) + assert text.font.size == 10 + + with pytest.raises(ValueError, match="Text could not be scaled"): + text.wrap(50, 15, ("shrink", 9)) + + assert text.wrap(50, 15, "shrink") is None + assert text.font.size == 8 + + text = ImageText.Text("Hello World!") + assert text.wrap(50, 15, ("shrink", 7)) is None + assert isinstance(text.font, ImageFont.FreeTypeFont) + assert text.font.size == 8 + + +@skip_unless_feature("freetype2") +def test_wrap_grow() -> None: + # No scaling required + text = ImageText.Text("Hello World!") + assert isinstance(text.font, ImageFont.FreeTypeFont) + assert text.font.size == 10 + assert text.wrap(58, 10, "grow") is None + assert isinstance(text.font, ImageFont.FreeTypeFont) + assert text.font.size == 10 + + with pytest.raises(ValueError, match="Text could not be scaled"): + text.wrap(50, 50, ("grow", 12)) + + assert text.wrap(50, 50, "grow") is None + assert text.font.size == 16 + + text = ImageText.Text("A\nB") + with pytest.raises(ValueError, match="Text could not be scaled"): + text.wrap(50, 10, "grow") + + text = ImageText.Text("Hello World!") + assert text.wrap(50, 50, ("grow", 18)) is None + assert isinstance(text.font, ImageFont.FreeTypeFont) + assert text.font.size == 16 diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index dfdbb622d04..07fa43b0687 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -538,7 +538,7 @@ def draw_corners(pieslice: bool) -> None: def text( self, xy: tuple[float, float], - text: AnyStr | ImageText.Text, + text: AnyStr | ImageText.Text[AnyStr], fill: _Ink | None = None, font: ( ImageFont.ImageFont diff --git a/src/PIL/ImageText.py b/src/PIL/ImageText.py index 7cd5f957e75..723ab9f8c1c 100644 --- a/src/PIL/ImageText.py +++ b/src/PIL/ImageText.py @@ -1,10 +1,14 @@ from __future__ import annotations -from typing import NamedTuple, cast +import math +import re +from typing import AnyStr, Generic, NamedTuple from . import ImageFont from ._typing import _Ink +Font = ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont + class _Line(NamedTuple): x: float @@ -13,16 +17,87 @@ class _Line(NamedTuple): text: str | bytes -class Text: +class _Wrap(Generic[AnyStr]): + lines: list[AnyStr] = [] + position = 0 + offset = 0 + def __init__( self, - text: str | bytes, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ) = None, + text: Text[AnyStr], + width: int, + height: int | None = None, + font: Font | None = None, + ) -> None: + self.text: Text[AnyStr] = text + self.width = width + self.height = height + self.font = font + + input_text = self.text.text + emptystring = "" if isinstance(input_text, str) else b"" + line = emptystring + + for word in re.findall( + r"\s*\S+" if isinstance(input_text, str) else rb"\s*\S+", input_text + ): + newlines = re.findall( + r"[^\S\n]*\n" if isinstance(input_text, str) else rb"[^\S\n]*\n", word + ) + if newlines: + if not self.add_line(line): + break + for i, line in enumerate(newlines): + if i != 0 and not self.add_line(emptystring): + break + self.position += len(line) + word = word[len(line) :] + line = emptystring + + new_line = line + word + if self.text._get_bbox(new_line, self.font)[2] <= width: + # This word fits on the line + line = new_line + continue + + # This word does not fit on the line + if line and not self.add_line(line): + break + + original_length = len(word) + word = word.lstrip() + self.offset = original_length - len(word) + + if self.text._get_bbox(word, self.font)[2] > width: + if font is None: + msg = "Word does not fit within line" + raise ValueError(msg) + break + line = word + else: + if line: + self.add_line(line) + self.remaining_text: AnyStr = input_text[self.position :] + + def add_line(self, line: AnyStr) -> bool: + lines = self.lines + [line] + if self.height is not None: + last_line_y = self.text._split(lines=lines)[-1].y + last_line_height = self.text._get_bbox(line, self.font)[3] + if last_line_y + last_line_height > self.height: + return False + + self.lines = lines + self.position += len(line) + self.offset + self.offset = 0 + return True + + +class Text(Generic[AnyStr]): + def __init__( + self, + text: AnyStr, + font: Font | None = None, mode: str = "RGB", spacing: float = 4, direction: str | None = None, @@ -56,7 +131,7 @@ def __init__( It should be a `BCP 47 language code`_. Requires libraqm. """ - self.text = text + self.text: AnyStr = text self.font = font or ImageFont.load_default() self.mode = mode @@ -101,118 +176,67 @@ def wrap( self, width: int, height: int | None = None, - ) -> Text | None: - wrapped_lines: list[str] | list[bytes] = [] - emptystring = "" if isinstance(self.text, str) else b"" - newline = "\n" if isinstance(self.text, str) else b"\n" - fontmode = self._get_fontmode() - - def getbbox(text) -> tuple[float, float]: - _, _, right, bottom = self.font.getbbox( - text, - fontmode, - self.direction, - self.features, - self.language, - self.stroke_width, - ) - return right, bottom - - wrapped_line = emptystring - word = emptystring - reached_end = False - remaining_position = 0 + scaling: str | tuple[str, int] | None = None, + ) -> Text[AnyStr] | None: + if isinstance(self.font, ImageFont.TransposedFont): + msg = "TransposedFont not supported" + raise ValueError(msg) + if self.direction not in (None, "ltr"): + msg = "Only ltr direction supported" + raise ValueError(msg) - def join_text(a: str | bytes, b: str | bytes) -> str | bytes: - if isinstance(a, str): - return a + cast(str, b) + if scaling is None: + wrap = _Wrap(self, width, height) + else: + if not isinstance(self.font, ImageFont.FreeTypeFont): + msg = "'scaling' only supports FreeTypeFont" + raise ValueError(msg) + if height is None: + msg = "'scaling' requires 'height'" + raise ValueError(msg) + + if isinstance(scaling, str): + limit = 1 else: - return a + cast(bytes, b) - - for i in range(len(self.text)): - last_character = i == len(self.text) - 1 - - def add_line() -> bool: - nonlocal wrapped_lines, remaining_position - lines = cast( - list[str] | list[bytes], wrapped_lines + [wrapped_line.rstrip()] - ) - if height is not None: - last_line_y = self._split(lines=lines)[-1].y - last_line_height = getbbox(wrapped_line)[1] - if last_line_y + last_line_height > height: - return False - - wrapped_lines = lines - remaining_position = i - len(word) - if last_character: - remaining_position += 1 - return True - - character = self.text[i : i + 1] - if last_character: - word = join_text(word, character) - character = newline - if character.isspace(): - if not word or word.isspace(): - # Do not use whitespace until a non-whitespace character is reached - # Trimming whitespace from the end of the line - word = join_text(word, character) - else: - # Append the word to the current line - if not wrapped_line: - word = word.lstrip() - new_wrapped_line = join_text(wrapped_line, word) - if getbbox(new_wrapped_line)[0] > width: - - def split_word(): - nonlocal wrapped_line, word, reached_end - # This word is too long for a single line, so split the word - j = len(word) - while j > 1 and getbbox(word[:j])[0] > width: - j -= 1 - wrapped_line = word[:j] - if not add_line(): - reached_end = True - return - word = word[j:] - wrapped_line = word - if getbbox(wrapped_line)[0] > width: - split_word() - - if wrapped_line: - # This word does not fit on the line - if not add_line(): - reached_end = True - break - word = word.lstrip() - if getbbox(word)[0] > width: - split_word() - else: - wrapped_line = word - else: - split_word() - if reached_end: - break - else: - # This word fits on the line - wrapped_line = new_wrapped_line - word = emptystring - - word = emptystring if character == newline else character - - if character == newline: - if not add_line(): - break - wrapped_line = emptystring - elif not character.isspace(): - # Word is not finished yet - word = join_text(word, character) + scaling, limit = scaling + + font = self.font + wrap = _Wrap(self, width, height, font) + if scaling == "shrink": + if not wrap.remaining_text: + return None + + size = math.ceil(font.size) + while wrap.remaining_text: + if size == max(limit, 1): + msg = "Text could not be scaled" + raise ValueError(msg) + size -= 1 + font = self.font.font_variant(size=size) + wrap = _Wrap(self, width, height, font) + self.font = font + else: + if wrap.remaining_text: + msg = "Text could not be scaled" + raise ValueError(msg) - remaining_text = self.text[remaining_position:] - if remaining_text: + size = math.floor(font.size) + while not wrap.remaining_text: + if size == limit: + msg = "Text could not be scaled" + raise ValueError(msg) + size += 1 + font = self.font.font_variant(size=size) + last_wrap = wrap + wrap = _Wrap(self, width, height, font) + size -= 1 + if size != self.font.size: + self.font = self.font.font_variant(size=size) + wrap = last_wrap + + if wrap.remaining_text: text = Text( - text=remaining_text, + text=wrap.remaining_text, font=self.font, mode=self.mode, spacing=self.spacing, @@ -226,10 +250,8 @@ def split_word(): else: text = None - if isinstance(self.text, str): - self.text = "\n".join(cast(list[str], wrapped_lines)) - else: - self.text = b"\n".join(cast(list[bytes], wrapped_lines)) + newline = "\n" if isinstance(self.text, str) else b"\n" + self.text = newline.join(wrap.lines) return text def get_length(self) -> float: @@ -413,6 +435,19 @@ def _split( return parts + def _get_bbox( + self, text: str | bytes, font: Font | None = None, anchor: str | None = None + ) -> tuple[float, float, float, float]: + return (font or self.font).getbbox( + text, + self._get_fontmode(), + self.direction, + self.features, + self.language, + self.stroke_width, + anchor, + ) + def get_bbox( self, xy: tuple[float, float] = (0, 0), @@ -438,17 +473,8 @@ def get_bbox( :return: ``(left, top, right, bottom)`` bounding box """ bbox: tuple[float, float, float, float] | None = None - fontmode = self._get_fontmode() for x, y, anchor, text in self._split(xy, anchor, align): - bbox_line = self.font.getbbox( - text, - fontmode, - self.direction, - self.features, - self.language, - self.stroke_width, - anchor, - ) + bbox_line = self._get_bbox(text, anchor=anchor) bbox_line = ( bbox_line[0] + x, bbox_line[1] + y, From b3da65df94ba1902502b59e9b70b15d535ccd902 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Dec 2025 10:53:04 +1100 Subject: [PATCH 2172/2374] Updated libjpeg-turbo to 3.1.3 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index b246d925511..b4d7ca45fa7 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -97,7 +97,7 @@ else fi HARFBUZZ_VERSION=12.2.0 LIBPNG_VERSION=1.6.53 -JPEGTURBO_VERSION=3.1.2 +JPEGTURBO_VERSION=3.1.3 OPENJPEG_VERSION=2.5.4 XZ_VERSION=5.8.1 ZSTD_VERSION=1.5.7 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index c42a1fcf518..12960330d48 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -117,7 +117,7 @@ def cmd_msbuild( "FREETYPE": "2.14.1", "FRIBIDI": "1.0.16", "HARFBUZZ": "12.2.0", - "JPEGTURBO": "3.1.2", + "JPEGTURBO": "3.1.3", "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.4.1", From 11d599c798da46a84669a3f7e5b25dc3ebafb45e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Dec 2025 18:20:58 +1100 Subject: [PATCH 2173/2374] Added documentation --- docs/releasenotes/12.1.0.rst | 25 +++++++++++++++++++++++++ src/PIL/ImageText.py | 13 +++++++++++++ 2 files changed, 38 insertions(+) diff --git a/docs/releasenotes/12.1.0.rst b/docs/releasenotes/12.1.0.rst index b6e1810c60a..5c363e2397c 100644 --- a/docs/releasenotes/12.1.0.rst +++ b/docs/releasenotes/12.1.0.rst @@ -41,6 +41,31 @@ TODO API additions ============= +ImageText.Text.wrap +^^^^^^^^^^^^^^^^^^^ + +:py:meth:`.ImageText.Text.wrap` has been added, to wrap text to fit within a given +width:: + + from PIL import ImageText + text = ImageText.Text("Hello World!") + text.wrap(50) + print(text.text) # "Hello\nWorld!" + +or within a certain width and height, returning a new :py:class:`.ImageText.Text` +instance if the text does not fit:: + + text = ImageText.Text("Text does not fit within height") + print(text.wrap(50, 25).text == " within height") + print(text.text) # "Text does\nnot fit" + +or scaling, optionally with a font size limit:: + + text.wrap(50, 15, "shrink") + text.wrap(50, 15, ("shrink", 7)) + text.wrap(58, 10, "grow") + text.wrap(50, 50, ("grow", 12)) + Specify window in ImageGrab on macOS ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/ImageText.py b/src/PIL/ImageText.py index 723ab9f8c1c..008d20d38e1 100644 --- a/src/PIL/ImageText.py +++ b/src/PIL/ImageText.py @@ -178,6 +178,19 @@ def wrap( height: int | None = None, scaling: str | tuple[str, int] | None = None, ) -> Text[AnyStr] | None: + """ + Wrap text to fit within a given width. + + :param width: The width to fit within. + :param height: An optional height limit. Any text that does not fit within this + will be returned as a new :py:class:`.Text` object. + :param scaling: An optional directive to scale the text, either "grow" as much + as possible within the given dimensions, or "shrink" until it + fits. It can also be a tuple of (direction, limit), with an + integer limit to stop scaling at. + + :returns: An :py:class:`.Text` object, or None. + """ if isinstance(self.font, ImageFont.TransposedFont): msg = "TransposedFont not supported" raise ValueError(msg) From 79ae888d4574333ca666696453870dcc2bb7a8c3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:29:54 +0200 Subject: [PATCH 2174/2374] Docs: update major bump cadence --- docs/releasenotes/index.rst | 2 +- docs/releasenotes/versioning.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index b097770a3f8..4b25bb6a2d1 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + versioning 12.1.0 12.0.0 11.3.0 @@ -80,4 +81,3 @@ expected to be backported to earlier versions. 2.5.2 2.3.2 2.3.1 - versioning diff --git a/docs/releasenotes/versioning.rst b/docs/releasenotes/versioning.rst index 2a0af9e59ec..884102d16f7 100644 --- a/docs/releasenotes/versioning.rst +++ b/docs/releasenotes/versioning.rst @@ -17,8 +17,8 @@ prior three months. A quarterly release bumps the MAJOR version when incompatible API changes are made, such as removing deprecated APIs or dropping an EOL Python version. In practice, -these occur every 12-18 months, guided by -`Python's EOL schedule `_, and +these occur every October, guided by +`Python's EOL schedule `__, and any APIs that have been deprecated for at least a year are removed at the same time. PATCH versions ("`Point Release `_" From 6a769da21bc6e58e09f849e647920c51a5a82cd7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Dec 2025 23:27:29 +1100 Subject: [PATCH 2175/2374] Corrected variable type --- Tests/test_file_iptc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 9e2d8c06da1..3eb5cde8e50 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -6,7 +6,7 @@ from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags -from .helper import assert_image_equal, hopper +from .helper import assert_image_equal TEST_FILE = "Tests/images/iptc.jpg" @@ -85,7 +85,7 @@ def test_getiptcinfo() -> None: def test_getiptcinfo_jpg_none() -> None: # Arrange - with hopper() as im: + with Image.open("Tests/images/hopper.jpg") as im: # Act iptc = IptcImagePlugin.getiptcinfo(im) From 6bf4313a68cd4d4e6ba4b4cf87c8d4e97e6ac278 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 Dec 2025 07:44:40 +1100 Subject: [PATCH 2176/2374] Updated xz to 5.8.2 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index b4d7ca45fa7..e1477634d35 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -99,7 +99,7 @@ HARFBUZZ_VERSION=12.2.0 LIBPNG_VERSION=1.6.53 JPEGTURBO_VERSION=3.1.3 OPENJPEG_VERSION=2.5.4 -XZ_VERSION=5.8.1 +XZ_VERSION=5.8.2 ZSTD_VERSION=1.5.7 TIFF_VERSION=4.7.1 LCMS2_VERSION=2.17 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 12960330d48..30fe26d1415 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -125,7 +125,7 @@ def cmd_msbuild( "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.4", "TIFF": "4.7.1", - "XZ": "5.8.1", + "XZ": "5.8.2", "ZLIBNG": "2.3.2", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) @@ -183,11 +183,7 @@ def cmd_msbuild( "filename": f"xz-{V['XZ']}.tar.gz", "license": "COPYING", "build": [ - *cmds_cmake( - "liblzma", - "-DBUILD_SHARED_LIBS:BOOL=OFF" - + (" -DXZ_CLMUL_CRC:BOOL=OFF" if struct.calcsize("P") == 4 else ""), - ), + *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), cmd_mkdir(r"{inc_dir}\lzma"), cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), ], From 6df6cd448069ac09bafc360b6f7a470773c74707 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 20 Dec 2025 23:51:05 +1100 Subject: [PATCH 2177/2374] Pin docutils to 0.21 (#9344) --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index f4514925d1c..f2262080598 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ dynamic = [ "version", ] optional-dependencies.docs = [ + "docutils==0.21", # Pending https://github.com/pradyunsg/sphinx-inline-tabs/pull/51 "furo", "olefile", "sphinx>=8.2", From 9d3555c37e888a419105ec177ebb6d8aaebf306f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Dec 2025 22:39:19 +1100 Subject: [PATCH 2178/2374] Test Windows Server 2022 --- .github/workflows/test-windows.yml | 5 +++-- docs/installation/platform-support.rst | 4 ++-- winbuild/README.md | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 3450de35556..7afafe07c91 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -31,15 +31,16 @@ env: jobs: build: - runs-on: windows-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14", "3.15"] architecture: ["x64"] + os: ["windows-latest"] include: # Test the oldest Python on 32-bit - - { python-version: "3.10", architecture: "x86" } + - { python-version: "3.10", architecture: "x86", os: "windows-2022" } timeout-minutes: 45 diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 17e38719a93..ee70d84019a 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -53,8 +53,8 @@ These platforms are built and tested for every change. | | | s390x | +----------------------------------+----------------------------+---------------------+ | Windows Server 2022 | 3.10 | x86 | -| +----------------------------+---------------------+ -| | 3.11, 3.12, 3.13, 3.14, | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 | | | PyPy3 | | | +----------------------------+---------------------+ | | 3.12 (MinGW) | x86-64 | diff --git a/winbuild/README.md b/winbuild/README.md index db71f094e0e..b1c9262c2e4 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -11,7 +11,8 @@ For more extensive info, see the [Windows build instructions](build.rst). * Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires NASM for libjpeg-turbo, a required dependency when using this script. * Requires CMake 3.15 or newer (available as Visual Studio component). -* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). +* Tested on Windows Server 2025 and 2022 with Visual Studio 2022 Enterprise (GitHub + Actions). Here's an example script to build on Windows: From 9dd756f9fe574ab2b3c97585f299600582551310 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Dec 2025 09:33:28 +1100 Subject: [PATCH 2179/2374] Revert "Pin docutils to 0.21 (#9344)" This reverts commit 6df6cd448069ac09bafc360b6f7a470773c74707. --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f2262080598..f4514925d1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ dynamic = [ "version", ] optional-dependencies.docs = [ - "docutils==0.21", # Pending https://github.com/pradyunsg/sphinx-inline-tabs/pull/51 "furo", "olefile", "sphinx>=8.2", From ca21683316a96b348197a6f5ca97ea9c6c142bb0 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:12:10 +1100 Subject: [PATCH 2180/2374] Cast to UINT32 before shifting bits (#9347) --- src/libImaging/BcnDecode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index ac81ed6df3d..d99b0e28e8a 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -663,7 +663,7 @@ half_to_float(UINT16 h) { if (o.f >= m.f) { o.u |= 255 << 23; } - o.u |= (h & 0x8000) << 16; + o.u |= (UINT32)(h & 0x8000) << 16; return o.f; } From 9b7200d2b439a4a41411e074e0d6cc453a8e3e57 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Dec 2025 12:50:26 +1100 Subject: [PATCH 2181/2374] Allow 1 mode images in MorphOp get_on_pixels() --- Tests/test_imagemorph.py | 9 +++++---- src/PIL/ImageMorph.py | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index ca192a809c4..59d1b0645ef 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -188,9 +188,10 @@ def test_corner() -> None: assert len(coords) == 4 assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) - coords = mop.get_on_pixels(Aout) - assert len(coords) == 4 - assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) + for image in (Aout, Aout.convert("1")): + coords = mop.get_on_pixels(image) + assert len(coords) == 4 + assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) def test_mirroring() -> None: @@ -239,7 +240,7 @@ def test_incorrect_mode() -> None: mop.apply(im) with pytest.raises(ValueError, match="Image mode must be L"): mop.match(im) - with pytest.raises(ValueError, match="Image mode must be L"): + with pytest.raises(ValueError, match="Image mode must be 1 or L"): mop.get_on_pixels(im) diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index bd70aff7b48..d6d0f829658 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -232,13 +232,13 @@ def match(self, image: Image.Image) -> list[tuple[int, int]]: return _imagingmorph.match(bytes(self.lut), image.getim()) def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: - """Get a list of all turned on pixels in a binary image + """Get a list of all turned on pixels in a 1 or L mode image. Returns a list of tuples of (x,y) coordinates of all matching pixels. See :ref:`coordinate-system`.""" - if image.mode != "L": - msg = "Image mode must be L" + if image.mode not in ("1", "L"): + msg = "Image mode must be 1 or L" raise ValueError(msg) return _imagingmorph.get_on_pixels(image.getim()) From a7047114040f38314a970d271d967cc17004c5e1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Dec 2025 14:13:51 +1100 Subject: [PATCH 2182/2374] Allow 1 mode images in apply() and match() --- Tests/images/morph_a.png | Bin 83 -> 79 bytes Tests/test_imagemorph.py | 38 ++++++++++++++++++-------------------- src/PIL/ImageMorph.py | 8 ++++---- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/Tests/images/morph_a.png b/Tests/images/morph_a.png index 19f6b777ffe17d924b984ba4ae4679257f01d31f..035fbc4bb84d6b67ee99cd6bbb5e8996059dd142 100644 GIT binary patch delta 60 zcmWIcpCDn*$N&UyG_&e}l$fWBV@SoE Image.Image: rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)] height = len(rows) width = len(rows[0]) - im = Image.new("L", (width, height)) - for i in range(width): - for j in range(height): - c = rows[j][i] - v = c in "X1" - im.putpixel((i, j), v) - + im = Image.new("1", (width, height)) + for x in range(width): + for y in range(height): + im.putpixel((x, y), rows[y][x] in "X1") return im @@ -42,10 +39,10 @@ def img_to_string(im: Image.Image) -> str: """Turn a (small) binary image into a string representation""" chars = ".1" result = [] - for r in range(im.height): + for y in range(im.height): line = "" - for c in range(im.width): - value = im.getpixel((c, r)) + for x in range(im.width): + value = im.getpixel((x, y)) assert not isinstance(value, tuple) assert value is not None line += chars[value > 0] @@ -165,10 +162,12 @@ def test_edge() -> None: ) -def test_corner() -> None: +@pytest.mark.parametrize("mode", ("1", "L")) +def test_corner(mode: str) -> None: # Create a corner detector pattern mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) - count, Aout = mop.apply(A) + image = A.convert(mode) if mode == "L" else A + count, Aout = mop.apply(image) assert count == 5 assert_img_equal_img_string( Aout, @@ -184,14 +183,13 @@ def test_corner() -> None: ) # Test the coordinate counting with the same operator - coords = mop.match(A) + coords = mop.match(image) assert len(coords) == 4 assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) - for image in (Aout, Aout.convert("1")): - coords = mop.get_on_pixels(image) - assert len(coords) == 4 - assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) + coords = mop.get_on_pixels(Aout) + assert len(coords) == 4 + assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) def test_mirroring() -> None: @@ -233,12 +231,12 @@ def test_negate() -> None: def test_incorrect_mode() -> None: - im = hopper("RGB") + im = hopper() mop = ImageMorph.MorphOp(op_name="erosion8") - with pytest.raises(ValueError, match="Image mode must be L"): + with pytest.raises(ValueError, match="Image mode must be 1 or L"): mop.apply(im) - with pytest.raises(ValueError, match="Image mode must be L"): + with pytest.raises(ValueError, match="Image mode must be 1 or L"): mop.match(im) with pytest.raises(ValueError, match="Image mode must be 1 or L"): mop.get_on_pixels(im) diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index d6d0f829658..69b0d170061 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -209,8 +209,8 @@ def apply(self, image: Image.Image) -> tuple[int, Image.Image]: msg = "No operator loaded" raise Exception(msg) - if image.mode != "L": - msg = "Image mode must be L" + if image.mode not in ("1", "L"): + msg = "Image mode must be 1 or L" raise ValueError(msg) outimage = Image.new(image.mode, image.size, None) count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim()) @@ -226,8 +226,8 @@ def match(self, image: Image.Image) -> list[tuple[int, int]]: msg = "No operator loaded" raise Exception(msg) - if image.mode != "L": - msg = "Image mode must be L" + if image.mode not in ("1", "L"): + msg = "Image mode must be 1 or L" raise ValueError(msg) return _imagingmorph.match(bytes(self.lut), image.getim()) From e85700fe483f014ffcbaf91e81456a78f6303337 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 28 Dec 2025 00:54:10 +1100 Subject: [PATCH 2183/2374] Test PyQt6 on Python 3.14 on Windows (#9353) --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 7afafe07c91..e864763da21 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -84,7 +84,7 @@ jobs: python3 -m pip install --upgrade pip - name: Install CPython dependencies - if: "!contains(matrix.python-version, 'pypy') && !contains(matrix.python-version, '3.14') && matrix.architecture != 'x86'" + if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'" run: | python3 -m pip install PyQt6 From 4be5b8a2fb0aad9777dc00db9ae353605e745129 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:34:57 +1100 Subject: [PATCH 2184/2374] Use unsigned long for DWORD (#9352) --- src/libImaging/FliDecode.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index 44994823e5d..9b494dfa2ca 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -18,9 +18,9 @@ #define I16(ptr) ((ptr)[0] + ((int)(ptr)[1] << 8)) -#define I32(ptr) \ - ((ptr)[0] + ((INT32)(ptr)[1] << 8) + ((INT32)(ptr)[2] << 16) + \ - ((INT32)(ptr)[3] << 24)) +#define I32(ptr) \ + ((ptr)[0] + ((unsigned long)(ptr)[1] << 8) + ((unsigned long)(ptr)[2] << 16) + \ + ((unsigned long)(ptr)[3] << 24)) #define ERR_IF_DATA_OOB(offset) \ if ((data + (offset)) > ptr + bytes) { \ @@ -31,8 +31,8 @@ int ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { UINT8 *ptr; - int framesize; - int c, chunks, advance; + unsigned long framesize, advance; + int c, chunks; int l, lines; int i, j, x = 0, y, ymax; From 66e3d65a72e100e0919b757460099c916d79b5d3 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 29 Dec 2025 02:04:44 +1100 Subject: [PATCH 2185/2374] Update harfbuzz to 12.3.0 (#9355) --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index e1477634d35..e1586b7c5a3 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -95,7 +95,7 @@ if [[ -n "$IOS_SDK" ]]; then else FREETYPE_VERSION=2.14.1 fi -HARFBUZZ_VERSION=12.2.0 +HARFBUZZ_VERSION=12.3.0 LIBPNG_VERSION=1.6.53 JPEGTURBO_VERSION=3.1.3 OPENJPEG_VERSION=2.5.4 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 30fe26d1415..3377d952c0d 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ def cmd_msbuild( "BROTLI": "1.2.0", "FREETYPE": "2.14.1", "FRIBIDI": "1.0.16", - "HARFBUZZ": "12.2.0", + "HARFBUZZ": "12.3.0", "JPEGTURBO": "3.1.3", "LCMS2": "2.17", "LIBAVIF": "1.3.0", From faa843e9c2ba5be8ddf42bede72f63c93ac75158 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 29 Dec 2025 08:01:23 +1100 Subject: [PATCH 2186/2374] Simplify WebP code (#9329) --- src/PIL/WebPImagePlugin.py | 41 +++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 2847fed20a0..e20e40d913f 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -45,33 +45,30 @@ class WebPImageFile(ImageFile.ImageFile): def _open(self) -> None: # Use the newer AnimDecoder API to parse the (possibly) animated file, # and access muxed chunks like ICC/EXIF/XMP. + assert self.fp is not None self._decoder = _webp.WebPAnimDecoder(self.fp.read()) # Get info from decoder - self._size, loop_count, bgcolor, frame_count, mode = self._decoder.get_info() - self.info["loop"] = loop_count - bg_a, bg_r, bg_g, bg_b = ( - (bgcolor >> 24) & 0xFF, - (bgcolor >> 16) & 0xFF, - (bgcolor >> 8) & 0xFF, - bgcolor & 0xFF, + self._size, self.info["loop"], bgcolor, self.n_frames, self.rawmode = ( + self._decoder.get_info() + ) + self.info["background"] = ( + (bgcolor >> 16) & 0xFF, # R + (bgcolor >> 8) & 0xFF, # G + bgcolor & 0xFF, # B + (bgcolor >> 24) & 0xFF, # A ) - self.info["background"] = (bg_r, bg_g, bg_b, bg_a) - self.n_frames = frame_count self.is_animated = self.n_frames > 1 - self._mode = "RGB" if mode == "RGBX" else mode - self.rawmode = mode + self._mode = "RGB" if self.rawmode == "RGBX" else self.rawmode # Attempt to read ICC / EXIF / XMP chunks from file - icc_profile = self._decoder.get_chunk("ICCP") - exif = self._decoder.get_chunk("EXIF") - xmp = self._decoder.get_chunk("XMP ") - if icc_profile: - self.info["icc_profile"] = icc_profile - if exif: - self.info["exif"] = exif - if xmp: - self.info["xmp"] = xmp + for key, chunk_name in { + "icc_profile": "ICCP", + "exif": "EXIF", + "xmp": "XMP ", + }.items(): + if value := self._decoder.get_chunk(chunk_name): + self.info[key] = value # Initialize seek state self._reset(reset=False) @@ -129,9 +126,7 @@ def load(self) -> Image.core.PixelAccess | None: self._seek(self.__logical_frame) # We need to load the image data for this frame - data, timestamp, duration = self._get_next() - self.info["timestamp"] = timestamp - self.info["duration"] = duration + data, self.info["timestamp"], self.info["duration"] = self._get_next() self.__loaded = self.__logical_frame # Set tile From a04c9806b12723b8882536325b69b4a1f96733aa Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 29 Dec 2025 08:03:47 +1100 Subject: [PATCH 2187/2374] Return LUT from LutBuilder build_default_lut() (#9350) --- Tests/test_imagemorph.py | 5 +++++ src/PIL/ImageMorph.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index ca192a809c4..12423ebf627 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -281,6 +281,11 @@ def test_pattern_syntax_error(pattern: str) -> None: lb.build_lut() +def test_build_default_lut() -> None: + lb = ImageMorph.LutBuilder(op_name="corner") + assert lb.build_default_lut() == lb.lut + + def test_load_invalid_mrl() -> None: # Arrange invalid_mrl = "Tests/images/hopper.png" diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index bd70aff7b48..4d9517d02ce 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -92,10 +92,11 @@ def __init__( def add_patterns(self, patterns: list[str]) -> None: self.patterns += patterns - def build_default_lut(self) -> None: + def build_default_lut(self) -> bytearray: symbols = [0, 1] m = 1 << 4 # pos of current pixel self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE)) + return self.lut def get_lut(self) -> bytearray | None: return self.lut From 2ebb3e9964bcfb46e2b8dcaec1917caf67730a7d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 29 Dec 2025 08:09:46 +1100 Subject: [PATCH 2188/2374] Use different variables for Image and ImageFile instances (#9316) --- Tests/helper.py | 7 ++-- Tests/test_bmp_reference.py | 18 +++++----- Tests/test_file_apng.py | 18 +++++----- Tests/test_file_bmp.py | 4 +-- Tests/test_file_dds.py | 8 ++--- Tests/test_file_eps.py | 32 ++++++++--------- Tests/test_file_gif.py | 68 ++++++++++++++++++------------------ Tests/test_file_libtiff.py | 14 ++++---- Tests/test_file_png.py | 37 ++++++++++---------- Tests/test_imageops.py | 6 ++-- Tests/test_pickle.py | 14 ++++---- src/PIL/IptcImagePlugin.py | 7 ++-- src/PIL/SpiderImagePlugin.py | 4 +-- 13 files changed, 118 insertions(+), 119 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index dbdd30b426a..b93828f5832 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -104,10 +104,9 @@ def assert_image_equal_tofile( msg: str | None = None, mode: str | None = None, ) -> None: - with Image.open(filename) as img: - if mode: - img = img.convert(mode) - assert_image_equal(a, img, msg) + with Image.open(filename) as im: + converted_im = im.convert(mode) if mode else im + assert_image_equal(a, converted_im, msg) def assert_image_similar( diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 3cd0fbb2d2e..8fbd737484d 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -95,16 +95,16 @@ def get_compare(f: str) -> str: for f in get_files("g"): try: with Image.open(f) as im: - im.load() with Image.open(get_compare(f)) as compare: - compare.load() - if im.mode == "P": - # assert image similar doesn't really work - # with paletized image, since the palette might - # be differently ordered for an equivalent image. - im = im.convert("RGBA") - compare = compare.convert("RGBA") - assert_image_similar(im, compare, 5) + # assert image similar doesn't really work + # with paletized image, since the palette might + # be differently ordered for an equivalent image. + im_converted = im.convert("RGBA") if im.mode == "P" else im + compare_converted = ( + compare.convert("RGBA") if im.mode == "P" else compare + ) + + assert_image_similar(im_converted, compare_converted, 5) except Exception as msg: # there are three here that are unsupported: diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index d918a24a799..644b7807a66 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -278,25 +278,25 @@ def test_apng_mode() -> None: assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) - im = im.convert("RGB") - assert im.getpixel((0, 0)) == (0, 255, 0) - assert im.getpixel((64, 32)) == (0, 255, 0) + im_rgb = im.convert("RGB") + assert im_rgb.getpixel((0, 0)) == (0, 255, 0) + assert im_rgb.getpixel((64, 32)) == (0, 255, 0) with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) - im = im.convert("RGBA") - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) + im_rgba = im.convert("RGBA") + assert im_rgba.getpixel((0, 0)) == (0, 255, 0, 255) + assert im_rgba.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) - im = im.convert("RGBA") - assert im.getpixel((0, 0)) == (0, 0, 255, 128) - assert im.getpixel((64, 32)) == (0, 0, 255, 128) + im_rgba = im.convert("RGBA") + assert im_rgba.getpixel((0, 0)) == (0, 0, 255, 128) + assert im_rgba.getpixel((64, 32)) == (0, 0, 255, 128) def test_apng_chunk_errors() -> None: diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index c1c430aa5b5..28e863459a6 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -165,9 +165,9 @@ def test_rgba_bitfields() -> None: with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: # So before the comparing the image, swap the channels b, g, r = im.split()[1:] - im = Image.merge("RGB", (r, g, b)) + im_rgb = Image.merge("RGB", (r, g, b)) - assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp") + assert_image_equal_tofile(im_rgb, "Tests/images/bmp/q/rgb32bf-xbgr.bmp") # This test image has been manually hexedited # to change the bitfield compression in the header from XBGR to ABGR diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 60d0c09bce1..931ff02f1fb 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -57,7 +57,7 @@ def test_sanity_dxt1_bc1(image_path: str) -> None: """Check DXT1 and BC1 images can be opened""" with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: - target = target.convert("RGBA") + target_rgba = target.convert("RGBA") with Image.open(image_path) as im: im.load() @@ -65,7 +65,7 @@ def test_sanity_dxt1_bc1(image_path: str) -> None: assert im.mode == "RGBA" assert im.size == (256, 256) - assert_image_equal(im, target) + assert_image_equal(im, target_rgba) def test_sanity_dxt3() -> None: @@ -520,9 +520,9 @@ def test_save_dx10_bc5(tmp_path: Path) -> None: im.save(out, pixel_format="BC5") assert_image_similar_tofile(im, out, 9.56) - im = hopper("L") + im_l = hopper("L") with pytest.raises(OSError, match="only RGB mode can be written as BC5"): - im.save(out, pixel_format="BC5") + im_l.save(out, pixel_format="BC5") @pytest.mark.parametrize( diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index b50915f28c3..d4e8db4f43c 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -265,9 +265,9 @@ def test_bytesio_object() -> None: img.load() with Image.open(FILE1_COMPARE) as image1_scale1_compare: - image1_scale1_compare = image1_scale1_compare.convert("RGB") - image1_scale1_compare.load() - assert_image_similar(img, image1_scale1_compare, 5) + image1_scale1_compare_rgb = image1_scale1_compare.convert("RGB") + image1_scale1_compare_rgb.load() + assert_image_similar(img, image1_scale1_compare_rgb, 5) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @@ -301,17 +301,17 @@ def test_render_scale1() -> None: with Image.open(FILE1) as image1_scale1: image1_scale1.load() with Image.open(FILE1_COMPARE) as image1_scale1_compare: - image1_scale1_compare = image1_scale1_compare.convert("RGB") - image1_scale1_compare.load() - assert_image_similar(image1_scale1, image1_scale1_compare, 5) + image1_scale1_compare_rgb = image1_scale1_compare.convert("RGB") + image1_scale1_compare_rgb.load() + assert_image_similar(image1_scale1, image1_scale1_compare_rgb, 5) # Non-zero bounding box with Image.open(FILE2) as image2_scale1: image2_scale1.load() with Image.open(FILE2_COMPARE) as image2_scale1_compare: - image2_scale1_compare = image2_scale1_compare.convert("RGB") - image2_scale1_compare.load() - assert_image_similar(image2_scale1, image2_scale1_compare, 10) + image2_scale1_compare_rgb = image2_scale1_compare.convert("RGB") + image2_scale1_compare_rgb.load() + assert_image_similar(image2_scale1, image2_scale1_compare_rgb, 10) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @@ -324,18 +324,16 @@ def test_render_scale2() -> None: assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile) image1_scale2.load(scale=2) with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: - image1_scale2_compare = image1_scale2_compare.convert("RGB") - image1_scale2_compare.load() - assert_image_similar(image1_scale2, image1_scale2_compare, 5) + image1_scale2_compare_rgb = image1_scale2_compare.convert("RGB") + assert_image_similar(image1_scale2, image1_scale2_compare_rgb, 5) # Non-zero bounding box with Image.open(FILE2) as image2_scale2: assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile) image2_scale2.load(scale=2) with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: - image2_scale2_compare = image2_scale2_compare.convert("RGB") - image2_scale2_compare.load() - assert_image_similar(image2_scale2, image2_scale2_compare, 10) + image2_scale2_compare_rgb = image2_scale2_compare.convert("RGB") + assert_image_similar(image2_scale2, image2_scale2_compare_rgb, 10) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @@ -345,8 +343,8 @@ def test_render_scale2() -> None: def test_resize(filename: str) -> None: with Image.open(filename) as im: new_size = (100, 100) - im = im.resize(new_size) - assert im.size == new_size + im_resized = im.resize(new_size) + assert im_resized.size == new_size @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index acf79374e9a..2615f5a6028 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -327,14 +327,13 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None: im.seek(1) assert im.mode == mode - if mode == "RGBA": - im = im.convert("RGB") + im_rgb = im.convert("RGB") if mode == "RGBA" else im # Check a color only from the old palette - assert im.getpixel((0, 0)) == original_color + assert im_rgb.getpixel((0, 0)) == original_color # Check a color from the new palette - assert im.getpixel((24, 24)) not in first_frame_colors + assert im_rgb.getpixel((24, 24)) not in first_frame_colors def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: @@ -354,16 +353,16 @@ def test_palette_handling(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/513 with Image.open(TEST_GIF) as im: - im = im.convert("RGB") + im_rgb = im.convert("RGB") - im = im.resize((100, 100), Image.Resampling.LANCZOS) - im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) + im_rgb = im_rgb.resize((100, 100), Image.Resampling.LANCZOS) + im_p = im_rgb.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) - f = tmp_path / "temp.gif" - im2.save(f, optimize=True) + f = tmp_path / "temp.gif" + im_p.save(f, optimize=True) with Image.open(f) as reloaded: - assert_image_similar(im, reloaded.convert("RGB"), 10) + assert_image_similar(im_rgb, reloaded.convert("RGB"), 10) def test_palette_434(tmp_path: Path) -> None: @@ -383,35 +382,36 @@ def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: with roundtrip(im, optimize=True) as reloaded: assert_image_similar(im, reloaded, 1) - im = im.convert("RGB") - # check automatic P conversion - with roundtrip(im) as reloaded: - reloaded = reloaded.convert("RGB") - assert_image_equal(im, reloaded) + im_rgb = im.convert("RGB") + + # check automatic P conversion + with roundtrip(im_rgb) as reloaded: + reloaded = reloaded.convert("RGB") + assert_image_equal(im_rgb, reloaded) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") def test_save_netpbm_bmp_mode(tmp_path: Path) -> None: with Image.open(TEST_GIF) as img: - img = img.convert("RGB") + img_rgb = img.convert("RGB") - tempfile = str(tmp_path / "temp.gif") - b = BytesIO() - GifImagePlugin._save_netpbm(img, b, tempfile) - with Image.open(tempfile) as reloaded: - assert_image_similar(img, reloaded.convert("RGB"), 0) + tempfile = str(tmp_path / "temp.gif") + b = BytesIO() + GifImagePlugin._save_netpbm(img_rgb, b, tempfile) + with Image.open(tempfile) as reloaded: + assert_image_similar(img_rgb, reloaded.convert("RGB"), 0) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") def test_save_netpbm_l_mode(tmp_path: Path) -> None: with Image.open(TEST_GIF) as img: - img = img.convert("L") + img_l = img.convert("L") tempfile = str(tmp_path / "temp.gif") b = BytesIO() - GifImagePlugin._save_netpbm(img, b, tempfile) + GifImagePlugin._save_netpbm(img_l, b, tempfile) with Image.open(tempfile) as reloaded: - assert_image_similar(img, reloaded.convert("L"), 0) + assert_image_similar(img_l, reloaded.convert("L"), 0) def test_seek() -> None: @@ -1038,9 +1038,9 @@ def test_webp_background(tmp_path: Path) -> None: im.save(out) # Test non-opaque WebP background - im = Image.new("L", (100, 100), "#000") - im.info["background"] = (0, 0, 0, 0) - im.save(out) + im2 = Image.new("L", (100, 100), "#000") + im2.info["background"] = (0, 0, 0, 0) + im2.save(out) def test_comment(tmp_path: Path) -> None: @@ -1048,16 +1048,16 @@ def test_comment(tmp_path: Path) -> None: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" out = tmp_path / "temp.gif" - im = Image.new("L", (100, 100), "#000") - im.info["comment"] = b"Test comment text" - im.save(out) + im2 = Image.new("L", (100, 100), "#000") + im2.info["comment"] = b"Test comment text" + im2.save(out) with Image.open(out) as reread: - assert reread.info["comment"] == im.info["comment"] + assert reread.info["comment"] == im2.info["comment"] - im.info["comment"] = "Test comment text" - im.save(out) + im2.info["comment"] = "Test comment text" + im2.save(out) with Image.open(out) as reread: - assert reread.info["comment"] == im.info["comment"].encode() + assert reread.info["comment"] == im2.info["comment"].encode() # Test that GIF89a is used for comments assert reread.info["version"] == b"GIF89a" diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index e36b5f39e3c..e05563c6aa4 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -516,12 +516,12 @@ def test_blur(self, tmp_path: Path) -> None: # and save to compressed tif. out = tmp_path / "temp.tif" with Image.open("Tests/images/pport_g4.tif") as im: - im = im.convert("L") + im_l = im.convert("L") - im = im.filter(ImageFilter.GaussianBlur(4)) - im.save(out, compression="tiff_adobe_deflate") + im_l = im_l.filter(ImageFilter.GaussianBlur(4)) + im_l.save(out, compression="tiff_adobe_deflate") - assert_image_equal_tofile(im, out) + assert_image_equal_tofile(im_l, out) def test_compressions(self, tmp_path: Path) -> None: # Test various tiff compressions and assert similar image content but reduced @@ -1087,8 +1087,10 @@ def test_old_style_jpeg_orientation(self) -> None: data = data[:102] + b"\x02" + data[103:] with Image.open(io.BytesIO(data)) as im: - im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + im_transposed = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + assert_image_equal_tofile( + im_transposed, "Tests/images/old-style-jpeg-compression.png" + ) def test_open_missing_samplesperpixel(self) -> None: with Image.open( diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 7f163a4d6d6..e9830cd3dad 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -101,12 +101,13 @@ def test_sanity(self, tmp_path: Path) -> None: assert im.get_format_mimetype() == "image/png" for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]: - im = hopper(mode) - im.save(test_file) + im1 = hopper(mode) + im1.save(test_file) with Image.open(test_file) as reloaded: - if mode == "I;16B": - reloaded = reloaded.convert(mode) - assert_image_equal(reloaded, im) + converted_reloaded = ( + reloaded.convert(mode) if mode == "I;16B" else reloaded + ) + assert_image_equal(converted_reloaded, im1) def test_invalid_file(self) -> None: invalid_file = "Tests/images/flower.jpg" @@ -225,11 +226,11 @@ def test_load_transparent_p(self) -> None: test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (162, 150)) + im_rgba = im.convert("RGBA") + assert_image(im_rgba, "RGBA", (162, 150)) # image has 124 unique alpha values - colors = im.getchannel("A").getcolors() + colors = im_rgba.getchannel("A").getcolors() assert colors is not None assert len(colors) == 124 @@ -239,11 +240,11 @@ def test_load_transparent_rgb(self) -> None: assert im.info["transparency"] == (0, 255, 52) assert_image(im, "RGB", (64, 64)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (64, 64)) + im_rgba = im.convert("RGBA") + assert_image(im_rgba, "RGBA", (64, 64)) # image has 876 transparent pixels - colors = im.getchannel("A").getcolors() + colors = im_rgba.getchannel("A").getcolors() assert colors is not None assert colors[0][0] == 876 @@ -262,11 +263,11 @@ def test_save_p_transparent_palette(self, tmp_path: Path) -> None: assert len(im.info["transparency"]) == 256 assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (162, 150)) + im_rgba = im.convert("RGBA") + assert_image(im_rgba, "RGBA", (162, 150)) # image has 124 unique alpha values - colors = im.getchannel("A").getcolors() + colors = im_rgba.getchannel("A").getcolors() assert colors is not None assert len(colors) == 124 @@ -285,13 +286,13 @@ def test_save_p_single_transparency(self, tmp_path: Path) -> None: assert im.info["transparency"] == 164 assert im.getpixel((31, 31)) == 164 assert_image(im, "P", (64, 64)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (64, 64)) + im_rgba = im.convert("RGBA") + assert_image(im_rgba, "RGBA", (64, 64)) - assert im.getpixel((31, 31)) == (0, 255, 52, 0) + assert im_rgba.getpixel((31, 31)) == (0, 255, 52, 0) # image has 876 transparent pixels - colors = im.getchannel("A").getcolors() + colors = im_rgba.getchannel("A").getcolors() assert colors is not None assert colors[0][0] == 876 diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 63cd0e4d4a9..35fe3bb8a57 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -457,9 +457,9 @@ def check(orientation_im: Image.Image) -> None: assert 0x0112 not in transposed_im.getexif() # Orientation set directly on Image.Exif - im = hopper() - im.getexif()[0x0112] = 3 - transposed_im = ImageOps.exif_transpose(im) + im1 = hopper() + im1.getexif()[0x0112] = 3 + transposed_im = ImageOps.exif_transpose(im1) assert 0x0112 not in transposed_im.getexif() diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index fc76f81e945..2447ae67ad7 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -19,30 +19,28 @@ def helper_pickle_file( # Arrange with Image.open(test_file) as im: filename = tmp_path / "temp.pkl" - if mode: - im = im.convert(mode) + converted_im = im.convert(mode) if mode else im # Act with open(filename, "wb") as f: - pickle.dump(im, f, protocol) + pickle.dump(converted_im, f, protocol) with open(filename, "rb") as f: loaded_im = pickle.load(f) # Assert - assert im == loaded_im + assert converted_im == loaded_im def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> None: with Image.open(test_file) as im: - if mode: - im = im.convert(mode) + converted_im = im.convert(mode) if mode else im # Act - dumped_string = pickle.dumps(im, protocol) + dumped_string = pickle.dumps(converted_im, protocol) loaded_im = pickle.loads(dumped_string) # Assert - assert im == loaded_im + assert converted_im == loaded_im @pytest.mark.parametrize( diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index c28f4dcc797..cf7067daa79 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -154,10 +154,11 @@ def load(self) -> Image.core.PixelAccess | None: if band is not None: bands = [Image.new("L", _im.size)] * Image.getmodebands(self.mode) bands[band] = _im - _im = Image.merge(self.mode, bands) + im = Image.merge(self.mode, bands) else: - _im.load() - self.im = _im.im + im = _im + im.load() + self.im = im.im self.tile = [] return ImageFile.ImageFile.load(self) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 868019e80a8..7c3e84d7495 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -323,9 +323,9 @@ def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: outfile = sys.argv[2] # perform some image operation - im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + transposed_im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) print( f"saving a flipped version of {os.path.basename(filename)} " f"as {outfile} " ) - im.save(outfile, SpiderImageFile.format) + transposed_im.save(outfile, SpiderImageFile.format) From 080afe1bf7757208ba2c7995d1ca2754e3f78c2f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 28 Dec 2025 23:46:02 +0200 Subject: [PATCH 2189/2374] Replace pre-commit with prek --- .github/workflows/lint.yml | 17 ++++++++--------- .github/zizmor.yml | 1 - tox.ini | 7 ++++--- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 77d1d1caa40..5f72550c09c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,18 +2,17 @@ name: Lint on: [push, pull_request, workflow_dispatch] +permissions: {} + env: FORCE_COLOR: 1 -permissions: - contents: read - concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - build: + lint: runs-on: ubuntu-latest @@ -24,13 +23,13 @@ jobs: with: persist-credentials: false - - name: pre-commit cache + - name: prek cache uses: actions/cache@v4 with: - path: ~/.cache/pre-commit - key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} + path: ~/.cache/prek + key: lint-prek-${{ hashFiles('**/.pre-commit-config.yaml') }} restore-keys: | - lint-pre-commit- + lint-prek- - name: Set up Python uses: actions/setup-python@v6 @@ -50,7 +49,7 @@ jobs: - name: Lint run: tox -e lint env: - PRE_COMMIT_COLOR: always + PREK_COLOR: always - name: Mypy run: tox -e mypy diff --git a/.github/zizmor.yml b/.github/zizmor.yml index e60c79441ca..f4949c30c43 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,4 +1,3 @@ -# Configuration for the zizmor static analysis tool, run via pre-commit in CI # https://docs.zizmor.sh/configuration/ rules: obfuscation: diff --git a/tox.ini b/tox.ini index 7f116c6e75f..de18946efa7 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ requires = tox>=4.2 env_list = lint + mypy py{py3, 315, 314, 313, 312, 311, 310} [testenv] @@ -18,11 +19,11 @@ commands = skip_install = true deps = check-manifest - pre-commit + prek pass_env = - PRE_COMMIT_COLOR + PREK_COLOR commands = - pre-commit run --all-files --show-diff-on-failure + prek run --all-files --show-diff-on-failure check-manifest [testenv:mypy] From 3abb62ed2935592c5046621da304e4aa1f171c7a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Dec 2025 08:19:24 +1100 Subject: [PATCH 2190/2374] Do not use cmd shell --- .github/workflows/test-windows.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e864763da21..913d6a23cb9 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -188,8 +188,9 @@ jobs: # trim ~150MB for each job - name: Optimize build cache if: steps.build-cache.outputs.cache-hit != 'true' - run: rmdir /S /Q winbuild\build\src - shell: cmd + run: | + rm -rf winbuild\build\src + shell: bash - name: Build Pillow run: | @@ -206,9 +207,7 @@ jobs: - name: Test Pillow run: | - path %GITHUB_WORKSPACE%\winbuild\build\bin;%PATH% .ci\test.cmd - shell: cmd - name: Prepare to upload errors if: failure() From 79357a271866050776ac587b1871c6841e2505d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 Dec 2025 22:24:33 +1100 Subject: [PATCH 2191/2374] Revert "Disable https://docs.zizmor.sh/audits/#obfuscation" This reverts commit 9342e209b2176bde761b321a74846857257ea78c. --- .github/zizmor.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/zizmor.yml b/.github/zizmor.yml index e60c79441ca..b567097811a 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,8 +1,6 @@ # Configuration for the zizmor static analysis tool, run via pre-commit in CI # https://docs.zizmor.sh/configuration/ rules: - obfuscation: - disable: true unpinned-uses: config: policies: From 72931475f2ba773d42b550b17ac10c19ae4a505c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:44:32 +0200 Subject: [PATCH 2192/2374] Replace shell: cmd with shell: bash --- .github/workflows/wheels.yml | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index fb71ead37b5..c9a738db3f2 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -186,24 +186,18 @@ jobs: - name: Build wheels run: | - setlocal EnableDelayedExpansion - for %%f in (winbuild\build\license\*) do ( - set x=%%~nf - rem Skip FriBiDi license, it is not included in the wheel. - set fribidi=!x:~0,7! - if NOT !fribidi!==fribidi ( - rem Skip imagequant license, it is not included in the wheel. - set libimagequant=!x:~0,13! - if NOT !libimagequant!==libimagequant ( - echo. >> LICENSE - echo ===== %%~nf ===== >> LICENSE - echo. >> LICENSE - type %%f >> LICENSE - ) - ) - ) - call winbuild\\build\\build_env.cmd - %pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse + for f in winbuild/build/license/*; do + name=$(basename "${f%.*}") + # Skip FriBiDi license, it is not included in the wheel. + [[ $name == fribidi* ]] && continue + # Skip imagequant license, it is not included in the wheel. + [[ $name == libimagequant* ]] && continue + echo "" >> LICENSE + echo "===== $name =====" >> LICENSE + echo "" >> LICENSE + cat "$f" >> LICENSE + done + cmd //c "winbuild\\build\\build_env.cmd && $pythonLocation\\python.exe -m cibuildwheel . --output-dir wheelhouse" env: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" @@ -217,7 +211,7 @@ jobs: -e CI -e GITHUB_ACTIONS mcr.microsoft.com/windows/servercore:ltsc2022 powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test' - shell: cmd + shell: bash - name: Upload wheels uses: actions/upload-artifact@v5 From 81e80f7a50e2e884d67d09ed7c735ee87f0f0f0a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 29 Dec 2025 19:35:41 +0200 Subject: [PATCH 2193/2374] Install and run tox/lint/mypy via uv --- .github/workflows/lint.yml | 51 +++++++++++--------------------------- 1 file changed, 14 insertions(+), 37 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5f72550c09c..e2f8bf47ac4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,6 +6,8 @@ permissions: {} env: FORCE_COLOR: 1 + PREK_COLOR: always + RUFF_OUTPUT_FORMAT: github concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -13,43 +15,18 @@ concurrency: jobs: lint: - runs-on: ubuntu-latest - name: Lint - steps: - - uses: actions/checkout@v6 - with: - persist-credentials: false - - - name: prek cache - uses: actions/cache@v4 - with: - path: ~/.cache/prek - key: lint-prek-${{ hashFiles('**/.pre-commit-config.yaml') }} - restore-keys: | - lint-prek- - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.x" - cache: pip - cache-dependency-path: "setup.py" - - - name: Build system information - run: python3 .github/workflows/system-info.py - - - name: Install dependencies - run: | - python3 -m pip install -U pip - python3 -m pip install -U tox - - - name: Lint - run: tox -e lint - env: - PREK_COLOR: always - - - name: Mypy - run: tox -e mypy + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-python@v6 + with: + python-version: "3.x" + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Lint + run: uvx --with tox-uv tox -e lint + - name: Mypy + run: uvx --with tox-uv tox -e mypy From 0a9a47fb9b9780fed4cf859da12cfa5fc867a71e Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 31 Dec 2025 23:02:31 +1100 Subject: [PATCH 2194/2374] Update ImageMorph documentation (#9349) Co-authored-by: Andrew Murray Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/reference/ImageMorph.rst | 46 ++++++++++++++- src/PIL/ImageMorph.py | 108 +++++++++++++++++++++++++--------- 2 files changed, 122 insertions(+), 32 deletions(-) diff --git a/docs/reference/ImageMorph.rst b/docs/reference/ImageMorph.rst index 30b89a54df5..77b96058a7d 100644 --- a/docs/reference/ImageMorph.rst +++ b/docs/reference/ImageMorph.rst @@ -4,10 +4,50 @@ :py:mod:`~PIL.ImageMorph` module ================================ -The :py:mod:`~PIL.ImageMorph` module provides morphology operations on images. +The :py:mod:`~PIL.ImageMorph` module allows `morphology`_ operators ("MorphOp") to be +applied to L mode images:: -.. automodule:: PIL.ImageMorph + from PIL import Image, ImageMorph + img = Image.open("Tests/images/hopper.bw") + mop = ImageMorph.MorphOp(op_name="erosion4") + count, imgOut = mop.apply(img) + imgOut.show() + +.. _morphology: https://en.wikipedia.org/wiki/Mathematical_morphology + +In addition to applying operators, you can also analyse images. + +You can inspect an image in isolation to determine which pixels are non-empty:: + + print(mop.get_on_pixels(img)) # [(0, 0), (1, 0), (2, 0), ...] + +Or you can retrieve a list of pixels that match the operator. This is the number of +pixels that will be non-empty after the operator is applied:: + + coords = mop.match(img) + print(coords) # [(17, 1), (18, 1), (34, 1), ...] + print(len(coords)) # 550 + + imgOut = mop.apply(img)[1] + print(len(mop.get_on_pixels(imgOut))) # 550 + +If you would like more customized operators, you can pass patterns to the MorphOp +class:: + + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) + +Or you can pass lookup table ("LUT") data directly. This LUT data can be constructed +with the :py:class:`~PIL.ImageMorph.LutBuilder`:: + + builder = ImageMorph.LutBuilder() + mop = ImageMorph.MorphOp(lut=builder.build_lut()) + +.. autoclass:: LutBuilder + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: MorphOp :members: :undoc-members: :show-inheritance: - :noindex: diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 4d9517d02ce..90e19911758 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -65,10 +65,12 @@ class LutBuilder: def __init__( self, patterns: list[str] | None = None, op_name: str | None = None ) -> None: - if patterns is not None: - self.patterns = patterns - else: - self.patterns = [] + """ + :param patterns: A list of input patterns, or None. + :param op_name: The name of a known pattern. One of "corner", "dilation4", + "dilation8", "erosion4", "erosion8" or "edge". + :exception Exception: If the op_name is not recognized. + """ self.lut: bytearray | None = None if op_name is not None: known_patterns = { @@ -88,21 +90,38 @@ def __init__( raise Exception(msg) self.patterns = known_patterns[op_name] + elif patterns is not None: + self.patterns = patterns + else: + self.patterns = [] def add_patterns(self, patterns: list[str]) -> None: + """ + Append to list of patterns. + + :param patterns: Additional patterns. + """ self.patterns += patterns def build_default_lut(self) -> bytearray: + """ + Set the current LUT, and return it. + + This is the default LUT that patterns will be applied against when building. + """ symbols = [0, 1] m = 1 << 4 # pos of current pixel self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE)) return self.lut def get_lut(self) -> bytearray | None: + """ + Returns the current LUT + """ return self.lut def _string_permute(self, pattern: str, permutation: list[int]) -> str: - """string_permute takes a pattern and a permutation and returns the + """Takes a pattern and a permutation and returns the string permuted according to the permutation list. """ assert len(permutation) == 9 @@ -111,7 +130,7 @@ def _string_permute(self, pattern: str, permutation: list[int]) -> str: def _pattern_permute( self, basic_pattern: str, options: str, basic_result: int ) -> list[tuple[str, int]]: - """pattern_permute takes a basic pattern and its result and clones + """Takes a basic pattern and its result and clones the pattern according to the modifications described in the $options parameter. It returns a list of all cloned patterns.""" patterns = [(basic_pattern, basic_result)] @@ -141,10 +160,9 @@ def _pattern_permute( return patterns def build_lut(self) -> bytearray: - """Compile all patterns into a morphology lut. + """Compile all patterns into a morphology LUT, and return it. - TBD :Build based on (file) morphlut:modify_lut - """ + This is the data to be passed into MorphOp.""" self.build_default_lut() assert self.lut is not None patterns = [] @@ -164,15 +182,14 @@ def build_lut(self) -> bytearray: patterns += self._pattern_permute(pattern, options, result) - # compile the patterns into regular expressions for speed + # Compile the patterns into regular expressions for speed compiled_patterns = [] for pattern in patterns: p = pattern[0].replace(".", "X").replace("X", "[01]") compiled_patterns.append((re.compile(p), pattern[1])) # Step through table and find patterns that match. - # Note that all the patterns are searched. The last one - # caught overrides + # Note that all the patterns are searched. The last one found takes priority for i in range(LUT_SIZE): # Build the bit pattern bitpattern = bin(i)[2:] @@ -194,18 +211,30 @@ def __init__( op_name: str | None = None, patterns: list[str] | None = None, ) -> None: - """Create a binary morphological operator""" - self.lut = lut - if op_name is not None: - self.lut = LutBuilder(op_name=op_name).build_lut() - elif patterns is not None: - self.lut = LutBuilder(patterns=patterns).build_lut() + """Create a binary morphological operator. + + If the LUT is not provided, then it is built using LutBuilder from the op_name + or the patterns. + + :param lut: The LUT data. + :param patterns: A list of input patterns, or None. + :param op_name: The name of a known pattern. One of "corner", "dilation4", + "dilation8", "erosion4", "erosion8", "edge". + :exception Exception: If the op_name is not recognized. + """ + if patterns is None and op_name is None: + self.lut = lut + else: + self.lut = LutBuilder(patterns, op_name).build_lut() def apply(self, image: Image.Image) -> tuple[int, Image.Image]: - """Run a single morphological operation on an image + """Run a single morphological operation on an image. Returns a tuple of the number of changed pixels and the - morphed image""" + morphed image. + + :exception Exception: If the current operator is None. + :exception ValueError: If the image is not L mode.""" if self.lut is None: msg = "No operator loaded" raise Exception(msg) @@ -213,7 +242,7 @@ def apply(self, image: Image.Image) -> tuple[int, Image.Image]: if image.mode != "L": msg = "Image mode must be L" raise ValueError(msg) - outimage = Image.new(image.mode, image.size, None) + outimage = Image.new(image.mode, image.size) count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim()) return count, outimage @@ -221,8 +250,12 @@ def match(self, image: Image.Image) -> list[tuple[int, int]]: """Get a list of coordinates matching the morphological operation on an image. - Returns a list of tuples of (x,y) coordinates - of all matching pixels. See :ref:`coordinate-system`.""" + Returns a list of tuples of (x,y) coordinates of all matching pixels. See + :ref:`coordinate-system`. + + :param image: An L-mode image. + :exception Exception: If the current operator is None. + :exception ValueError: If the image is not L mode.""" if self.lut is None: msg = "No operator loaded" raise Exception(msg) @@ -233,10 +266,13 @@ def match(self, image: Image.Image) -> list[tuple[int, int]]: return _imagingmorph.match(bytes(self.lut), image.getim()) def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: - """Get a list of all turned on pixels in a binary image + """Get a list of all turned on pixels in a grayscale image + + Returns a list of tuples of (x,y) coordinates of all non-empty pixels. See + :ref:`coordinate-system`. - Returns a list of tuples of (x,y) coordinates - of all matching pixels. See :ref:`coordinate-system`.""" + :param image: An L-mode image. + :exception ValueError: If the image is not L mode.""" if image.mode != "L": msg = "Image mode must be L" @@ -244,7 +280,12 @@ def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: return _imagingmorph.get_on_pixels(image.getim()) def load_lut(self, filename: str) -> None: - """Load an operator from an mrl file""" + """ + Load an operator from an mrl file + + :param filename: The file to read from. + :exception Exception: If the length of the file data is not 512. + """ with open(filename, "rb") as f: self.lut = bytearray(f.read()) @@ -254,7 +295,12 @@ def load_lut(self, filename: str) -> None: raise Exception(msg) def save_lut(self, filename: str) -> None: - """Save an operator to an mrl file""" + """ + Save an operator to an mrl file. + + :param filename: The destination file. + :exception Exception: If the current operator is None. + """ if self.lut is None: msg = "No operator loaded" raise Exception(msg) @@ -262,5 +308,9 @@ def save_lut(self, filename: str) -> None: f.write(self.lut) def set_lut(self, lut: bytearray | None) -> None: - """Set the lut from an external source""" + """ + Set the LUT from an external source + + :param lut: A new LUT. + """ self.lut = lut From 19910ed03ecf6a845a2ad1dbe05f060959c595d9 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 31 Dec 2025 23:47:33 +1100 Subject: [PATCH 2195/2374] Call parent verify method (#9357) --- src/PIL/PngImagePlugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 967308221ad..e3588735f20 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -855,9 +855,7 @@ def verify(self) -> None: self.png.verify() self.png.close() - if self._exclusive_fp: - self.fp.close() - self.fp = None + super().verify() def seek(self, frame: int) -> None: if not self._seek_check(frame): From 2ebfe30ae39d6d016b5239321ada65c85644e99e Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 31 Dec 2025 23:47:50 +1100 Subject: [PATCH 2196/2374] Added return type to ImageFile _close_fp() (#9356) --- src/PIL/ImageFile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index a1d98bd5103..ecb51b193e8 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -131,6 +131,7 @@ def __init__( self.decoderconfig: tuple[Any, ...] = () self.decodermaxblock = MAXBLOCK + self._fp: IO[bytes] | DeferredError if is_path(fp): # filename self.fp = open(fp, "rb") @@ -167,7 +168,7 @@ def __init__( def _open(self) -> None: pass - def _close_fp(self): + def _close_fp(self) -> None: if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError): if self._fp != self.fp: self._fp.close() From d62955031b593b6f993c655eb1e4332c15818df7 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 1 Jan 2026 08:53:04 +1100 Subject: [PATCH 2197/2374] Allow for duplicate font variation styles (#9362) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/fonts/AdobeVFPrototypeDuplicates.ttf | Bin 0 -> 6696 bytes Tests/fonts/LICENSE.txt | 2 +- Tests/test_imagefont.py | 17 ++++++++++++++++- src/PIL/ImageFont.py | 8 ++++++-- src/_imagingft.c | 1 - 5 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 Tests/fonts/AdobeVFPrototypeDuplicates.ttf diff --git a/Tests/fonts/AdobeVFPrototypeDuplicates.ttf b/Tests/fonts/AdobeVFPrototypeDuplicates.ttf new file mode 100644 index 0000000000000000000000000000000000000000..acf0bc156c8bec3c79de96968766795951b3a8fb GIT binary patch literal 6696 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 None: font.get_variation_axes() font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf") - assert font.get_variation_names(), [ + assert font.get_variation_names() == [ b"ExtraLight", b"Light", b"Regular", @@ -742,6 +742,21 @@ def test_variation_get(font: ImageFont.FreeTypeFont) -> None: ] +def test_variation_duplicates() -> None: + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototypeDuplicates.ttf") + assert font.get_variation_names() == [ + b"ExtraLight", + b"Light", + b"Regular", + b"Semibold", + b"Bold", + b"Black", + b"Black Medium Contrast", + b"Black High Contrast", + b"Default", + ] + + def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None: im = Image.new("RGB", (100, 75), "white") d = ImageDraw.Draw(im) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 2e8ace98dda..d11f7bf01ad 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -675,8 +675,12 @@ def get_variation_names(self) -> list[bytes]: :returns: A list of the named styles in a variation font. :exception OSError: If the font is not a variation font. """ - names = self.font.getvarnames() - return [name.replace(b"\x00", b"") for name in names] + names = [] + for name in self.font.getvarnames(): + name = name.replace(b"\x00", b"") + if name not in names: + names.append(name) + return names def set_variation_by_name(self, name: str | bytes) -> None: """ diff --git a/src/_imagingft.c b/src/_imagingft.c index d0af25b30e3..a371173d61c 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1287,7 +1287,6 @@ font_getvarnames(FontObject *self) { } PyList_SetItem(list_names, j, list_name); list_names_filled[j] = 1; - break; } } } From 900636e7dbc8ead2caaae0e387096fa8bf6474d5 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 1 Jan 2026 17:31:36 +1100 Subject: [PATCH 2198/2374] Use minimum supported Python version for Lint (#9364) --- .github/workflows/lint.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e2f8bf47ac4..4f67be6f705 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,7 +23,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v6 with: - python-version: "3.x" + python-version: "3.10" - name: Install uv uses: astral-sh/setup-uv@v7 - name: Lint diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8477729e636..10343f91ae3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,7 +76,7 @@ repos: rev: v0.24.1 hooks: - id: validate-pyproject - additional_dependencies: [trove-classifiers>=2024.10.12] + additional_dependencies: [tomli, trove-classifiers>=2024.10.12] - repo: https://github.com/tox-dev/tox-ini-fmt rev: 1.7.0 From 91f219fdcff8bc3eaafa2a0c2269286ebff0cca1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Jan 2026 13:02:21 +1100 Subject: [PATCH 2199/2374] Support saving float durations --- Tests/test_file_apng.py | 18 ++++++++++++++++++ docs/handbook/image-file-formats.rst | 5 ++--- src/PIL/PngImagePlugin.py | 11 ++++++++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 644b7807a66..b57a1d1ad8c 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -518,6 +518,24 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: assert im.info["duration"] == 600 +def test_apng_save_duration_float(tmp_path: Path) -> None: + test_file = tmp_path / "temp.png" + im = Image.new("1", (1, 1)) + im2 = Image.new("1", (1, 1), 1) + im.save(test_file, save_all=True, append_images=[im2], duration=0.5) + + with Image.open(test_file) as reloaded: + assert reloaded.info["duration"] == 0.5 + + +def test_apng_save_large_duration(tmp_path: Path) -> None: + test_file = tmp_path / "temp.png" + im = Image.new("1", (1, 1)) + im2 = Image.new("1", (1, 1), 1) + with pytest.raises(ValueError, match="cannot write duration"): + im.save(test_file, save_all=True, append_images=[im2], duration=65536000) + + def test_apng_save_disposal(tmp_path: Path) -> None: test_file = tmp_path / "temp.png" size = (128, 64) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 03ee96c0f00..d26c0fbae5c 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1041,9 +1041,8 @@ following parameters can also be set: Defaults to 0. **duration** - Integer (or list or tuple of integers) length of time to display this APNG frame - (in milliseconds). - Defaults to 0. + The length of time (or list or tuple of lengths of time) to display this APNG frame + (in milliseconds). Defaults to 0. **disposal** An integer (or list or tuple of integers) specifying the APNG disposal diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index e3588735f20..5f536035a2d 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -39,6 +39,7 @@ import warnings import zlib from enum import IntEnum +from fractions import Fraction from typing import IO, NamedTuple, cast from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence @@ -1272,7 +1273,11 @@ def _write_multiple_frames( im_frame = im_frame.crop(bbox) size = im_frame.size encoderinfo = frame_data.encoderinfo - frame_duration = int(round(encoderinfo.get("duration", 0))) + frame_duration = encoderinfo.get("duration", 0) + delay = Fraction(frame_duration / 1000).limit_denominator(65535) + if delay.numerator > 65535: + msg = "cannot write duration" + raise ValueError(msg) frame_disposal = encoderinfo.get("disposal", disposal) frame_blend = encoderinfo.get("blend", blend) # frame control @@ -1284,8 +1289,8 @@ def _write_multiple_frames( o32(size[1]), # height o32(bbox[0]), # x_offset o32(bbox[1]), # y_offset - o16(frame_duration), # delay_numerator - o16(1000), # delay_denominator + o16(delay.numerator), # delay_numerator + o16(delay.denominator), # delay_denominator o8(frame_disposal), # dispose_op o8(frame_blend), # blend_op ) From 43f8efad7988bee378408d8acafeaab6f0f70b85 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:41:34 +1100 Subject: [PATCH 2200/2374] Added release notes for #9350 (#9366) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/releasenotes/12.1.0.rst | 44 ++++-------------------------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/docs/releasenotes/12.1.0.rst b/docs/releasenotes/12.1.0.rst index b6e1810c60a..0541f882feb 100644 --- a/docs/releasenotes/12.1.0.rst +++ b/docs/releasenotes/12.1.0.rst @@ -1,42 +1,14 @@ 12.1.0 ------ -Security -======== - -TODO -^^^^ - -TODO - -:cve:`YYYY-XXXXX`: TODO -^^^^^^^^^^^^^^^^^^^^^^^ - -TODO - -Backwards incompatible changes -============================== - -TODO -^^^^ - -TODO - -Deprecations -============ - -TODO -^^^^ - -TODO - API changes =========== -TODO -^^^^ +ImageMorph build_default_lut() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +To match the behaviour of :py:meth:`~PIL.ImageMorph.LutBuilder.build_lut`, +:py:meth:`~PIL.ImageMorph.LutBuilder.build_default_lut()` now returns the new LUT. API additions ============= @@ -49,11 +21,3 @@ macOS in addition to Windows. On macOS, this is a CGWindowID:: from PIL import ImageGrab ImageGrab.grab(window=cgwindowid) - -Other changes -============= - -TODO -^^^^ - -TODO From a868c29eb196a082536e9a0895c4972f18c443ad Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 1 Jan 2026 20:01:38 +1100 Subject: [PATCH 2201/2374] Assert fp is not None (#8617) --- Tests/test_file_bufrstub.py | 1 + Tests/test_file_gribstub.py | 1 + Tests/test_file_hdf5stub.py | 1 + Tests/test_file_jpeg.py | 3 ++- Tests/test_file_libtiff.py | 3 ++- Tests/test_file_tiff.py | 5 ++++- Tests/test_image_load.py | 1 + docs/example/DdsImagePlugin.py | 1 + src/PIL/AvifImagePlugin.py | 2 ++ src/PIL/BlpImagePlugin.py | 1 + src/PIL/BmpImagePlugin.py | 2 ++ src/PIL/BufrStubImagePlugin.py | 1 + src/PIL/DcxImagePlugin.py | 1 + src/PIL/EpsImagePlugin.py | 2 ++ src/PIL/FpxImagePlugin.py | 2 ++ src/PIL/FtexImagePlugin.py | 1 + src/PIL/GbrImagePlugin.py | 2 ++ src/PIL/GifImagePlugin.py | 2 ++ src/PIL/GribStubImagePlugin.py | 1 + src/PIL/Hdf5StubImagePlugin.py | 1 + src/PIL/IcnsImagePlugin.py | 1 + src/PIL/IcoImagePlugin.py | 1 + src/PIL/ImImagePlugin.py | 1 + src/PIL/Image.py | 2 ++ src/PIL/ImageFile.py | 1 + src/PIL/IptcImagePlugin.py | 3 +++ src/PIL/Jpeg2KImagePlugin.py | 2 ++ src/PIL/JpegImagePlugin.py | 7 +++++++ src/PIL/MicImagePlugin.py | 1 + src/PIL/MpoImagePlugin.py | 2 ++ src/PIL/PngImagePlugin.py | 3 +++ src/PIL/PsdImagePlugin.py | 1 + src/PIL/QoiImagePlugin.py | 1 + src/PIL/SpiderImagePlugin.py | 1 + src/PIL/WalImageFile.py | 2 ++ src/PIL/WmfImagePlugin.py | 2 ++ 36 files changed, 62 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 362578c56ef..8c6bb1a69f7 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -61,6 +61,7 @@ def open(self, im: ImageFile.StubImageFile) -> None: def load(self, im: ImageFile.StubImageFile) -> Image.Image: self.loaded = True + assert im.fp is not None im.fp.close() return Image.new("RGB", (1, 1)) diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index 4dbed6b3190..05925d50202 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -61,6 +61,7 @@ def open(self, im: Image.Image) -> None: def load(self, im: ImageFile.ImageFile) -> Image.Image: self.loaded = True + assert im.fp is not None im.fp.close() return Image.new("RGB", (1, 1)) diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 1e48597d366..e1a56309b90 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -63,6 +63,7 @@ def open(self, im: Image.Image) -> None: def load(self, im: ImageFile.ImageFile) -> Image.Image: self.loaded = True + assert im.fp is not None im.fp.close() return Image.new("RGB", (1, 1)) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 96e7f4239b8..f818927f6a3 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1133,8 +1133,9 @@ def test_fd_leak(self, tmp_path: Path) -> None: im.save(tmpfile) im = Image.open(tmpfile) + assert im.fp is not None + assert not im.fp.closed fp = im.fp - assert not fp.closed with pytest.raises(OSError): os.remove(tmpfile) im.load() diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index e05563c6aa4..90ca2cf1216 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -610,8 +610,9 @@ def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None: im.save(out, compression=compression) def test_fp_leak(self) -> None: - im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif") + im: ImageFile.ImageFile | None = Image.open("Tests/images/hopper_g4_500.tif") assert im is not None + assert im.fp is not None fn = im.fp.fileno() os.fstat(fn) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 556c886476e..c6c8467d629 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -971,6 +971,7 @@ def test_close_on_load_exclusive(self, tmp_path: Path) -> None: im = Image.open(tmpfile) fp = im.fp + assert fp is not None assert not fp.closed im.load() assert fp.closed @@ -984,6 +985,7 @@ def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None: with open(tmpfile, "rb") as f: im = Image.open(f) fp = im.fp + assert fp is not None assert not fp.closed im.load() assert not fp.closed @@ -1034,8 +1036,9 @@ def test_fd_leak(self, tmp_path: Path) -> None: im.save(tmpfile) im = Image.open(tmpfile) + assert im.fp is not None + assert not im.fp.closed fp = im.fp - assert not fp.closed with pytest.raises(OSError): os.remove(tmpfile) im.load() diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 4f1d63b8f43..1d5f0d17cd6 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -38,6 +38,7 @@ def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None: def test_contextmanager() -> None: fn = None with Image.open("Tests/images/hopper.gif") as im: + assert im.fp is not None fn = im.fp.fileno() os.fstat(fn) diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index e4b6b9c01b6..e0557976c28 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -213,6 +213,7 @@ class DdsImageFile(ImageFile.ImageFile): format_description = "DirectDraw Surface" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(4)): msg = "not a DDS file" raise SyntaxError(msg) diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index 366e0c864bf..43c39a9fbe7 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -77,6 +77,8 @@ def _open(self) -> None: ): msg = "Invalid opening codec" raise ValueError(msg) + + assert self.fp is not None self._decoder = _avif.AvifDecoder( self.fp.read(), DECODE_CODEC_CHOICE, diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index f7be7746d84..6bb92edf891 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -258,6 +258,7 @@ class BlpImageFile(ImageFile.ImageFile): format_description = "Blizzard Mipmap Format" def _open(self) -> None: + assert self.fp is not None self.magic = self.fp.read(4) if not _accept(self.magic): msg = f"Bad BLP magic {repr(self.magic)}" diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 54fc69ab454..a1227137017 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -76,6 +76,7 @@ class BmpImageFile(ImageFile.ImageFile): def _bitmap(self, header: int = 0, offset: int = 0) -> None: """Read relevant info about the BMP""" + assert self.fp is not None read, seek = self.fp.read, self.fp.seek if header: seek(header) @@ -311,6 +312,7 @@ def _bitmap(self, header: int = 0, offset: int = 0) -> None: def _open(self) -> None: """Open file, check magic number and read header""" # read 14 bytes: magic number, filesize, reserved, header final offset + assert self.fp is not None head_data = self.fp.read(14) # choke if the file does not have the required magic bytes if not _accept(head_data): diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 8c5da14f5f6..264564d2bbb 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -41,6 +41,7 @@ class BufrStubImageFile(ImageFile.StubImageFile): format_description = "BUFR" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(4)): msg = "Not a BUFR file" raise SyntaxError(msg) diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index aea661b9cb6..d3f456ddcc4 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -45,6 +45,7 @@ class DcxImageFile(PcxImageFile): def _open(self) -> None: # Header + assert self.fp is not None s = self.fp.read(4) if not _accept(s): msg = "not a DCX file" diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 69f3062b4d4..2effb816cfb 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -189,6 +189,7 @@ class EpsImageFile(ImageFile.ImageFile): mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"} def _open(self) -> None: + assert self.fp is not None (length, offset) = self._find_offset(self.fp) # go to offset - start of "%!PS" @@ -403,6 +404,7 @@ def load( ) -> Image.core.PixelAccess | None: # Load EPS via Ghostscript if self.tile: + assert self.fp is not None self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) self._mode = self.im.mode self._size = self.im.size diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index fd992cd9e20..297971234d8 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -58,6 +58,7 @@ def _open(self) -> None: # read the OLE directory and see if this is a likely # to be a FlashPix file + assert self.fp is not None try: self.ole = olefile.OleFileIO(self.fp) except OSError as e: @@ -229,6 +230,7 @@ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: if y >= ysize: break # isn't really required + assert self.fp is not None self.stream = stream self._fp = self.fp self.fp = None diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index d60e75bb60b..e4d836cbdb2 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -72,6 +72,7 @@ class FtexImageFile(ImageFile.ImageFile): format_description = "Texture File Format (IW2:EOC)" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(4)): msg = "not an FTEX file" raise SyntaxError(msg) diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index d69295363f3..ec666c81c2c 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -42,6 +42,7 @@ class GbrImageFile(ImageFile.ImageFile): format_description = "GIMP brush file" def _open(self) -> None: + assert self.fp is not None header_size = i32(self.fp.read(4)) if header_size < 20: msg = "not a GIMP brush" @@ -88,6 +89,7 @@ def _open(self) -> None: def load(self) -> Image.core.PixelAccess | None: if self._im is None: + assert self.fp is not None self.im = Image.core.new(self.mode, self.size) self.frombytes(self.fp.read(self._data_size)) return Image.Image.load(self) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 0560a5a7dc8..b8755422d05 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -87,6 +87,7 @@ class GifImageFile(ImageFile.ImageFile): global_palette = None def data(self) -> bytes | None: + assert self.fp is not None s = self.fp.read(1) if s and s[0]: return self.fp.read(s[0]) @@ -100,6 +101,7 @@ def _is_palette_needed(self, p: bytes) -> bool: def _open(self) -> None: # Screen + assert self.fp is not None s = self.fp.read(13) if not _accept(s): msg = "not a GIF file" diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index dfa798893cb..146a6fa0df0 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -41,6 +41,7 @@ class GribStubImageFile(ImageFile.StubImageFile): format_description = "GRIB" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(8)): msg = "Not a GRIB file" raise SyntaxError(msg) diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index 76e640f15ab..1523e95d58c 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -41,6 +41,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile): format_description = "HDF5" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(8)): msg = "Not an HDF file" raise SyntaxError(msg) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 197ea7a2bb2..058861d67e5 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -265,6 +265,7 @@ class IcnsImageFile(ImageFile.ImageFile): format_description = "Mac OS icns resource" def _open(self) -> None: + assert self.fp is not None self.icns = IcnsFile(self.fp) self._mode = "RGBA" self.info["sizes"] = self.icns.itersizes() diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index d5da07d470c..8dd57ff858a 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -340,6 +340,7 @@ class IcoImageFile(ImageFile.ImageFile): format_description = "Windows Icon" def _open(self) -> None: + assert self.fp is not None self.ico = IcoFile(self.fp) self.info["sizes"] = self.ico.sizes() self.size = self.ico.entry[0].dim diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 71b9996780c..ef54f16e97e 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -125,6 +125,7 @@ def _open(self) -> None: # Quick rejection: if there's not an LF among the first # 100 bytes, this is (probably) not a text header. + assert self.fp is not None if b"\n" not in self.fp.read(100): msg = "not an IM file" raise SyntaxError(msg) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b71395c6234..4e61900f6fd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1524,6 +1524,8 @@ def getexif(self) -> Exif: assert isinstance(self, TiffImagePlugin.TiffImageFile) self._exif.bigtiff = self.tag_v2._bigtiff self._exif.endian = self.tag_v2._endian + + assert self.fp is not None self._exif.load_from_fp(self.fp, self.tag_v2._offset) if exif_info is not None: self._exif.load(exif_info) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index ecb51b193e8..f609c7d13c2 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -286,6 +286,7 @@ def load(self) -> Image.core.PixelAccess | None: self.map: mmap.mmap | None = None use_mmap = self.filename and len(self.tile) == 1 + assert self.fp is not None readonly = 0 # look for read/seek overrides diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index cf7067daa79..6fc824e4caa 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -49,6 +49,7 @@ def getint(self, key: tuple[int, int]) -> int: def field(self) -> tuple[tuple[int, int] | None, int]: # # get a IPTC field header + assert self.fp is not None s = self.fp.read(5) if not s.strip(b"\x00"): return None, 0 @@ -76,6 +77,7 @@ def field(self) -> tuple[tuple[int, int] | None, int]: def _open(self) -> None: # load descriptive fields + assert self.fp is not None while True: offset = self.fp.tell() tag, size = self.field() @@ -131,6 +133,7 @@ def load(self) -> Image.core.PixelAccess | None: assert isinstance(args, tuple) compression, band = args + assert self.fp is not None self.fp.seek(self.tile[0].offset) # Copy image data to temporary file diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 4c85dd4e281..d6ec38d4310 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -252,6 +252,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): format_description = "JPEG 2000 (ISO 15444)" def _open(self) -> None: + assert self.fp is not None sig = self.fp.read(4) if sig == b"\xff\x4f\xff\x51": self.codec = "j2k" @@ -304,6 +305,7 @@ def _open(self) -> None: ] def _parse_comment(self) -> None: + assert self.fp is not None while True: marker = self.fp.read(2) if not marker: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 755ca648e55..894c1547d7b 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -61,6 +61,7 @@ def Skip(self: JpegImageFile, marker: int) -> None: + assert self.fp is not None n = i16(self.fp.read(2)) - 2 ImageFile._safe_read(self.fp, n) @@ -70,6 +71,7 @@ def APP(self: JpegImageFile, marker: int) -> None: # Application marker. Store these in the APP dictionary. # Also look for well-known application markers. + assert self.fp is not None n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) @@ -174,6 +176,7 @@ def APP(self: JpegImageFile, marker: int) -> None: def COM(self: JpegImageFile, marker: int) -> None: # # Comment marker. Store these in the APP dictionary. + assert self.fp is not None n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) @@ -190,6 +193,7 @@ def SOF(self: JpegImageFile, marker: int) -> None: # mode. Note that this could be made a bit brighter, by # looking for JFIF and Adobe APP markers. + assert self.fp is not None n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) self._size = i16(s, 3), i16(s, 1) @@ -240,6 +244,7 @@ def DQT(self: JpegImageFile, marker: int) -> None: # FIXME: The quantization tables can be used to estimate the # compression quality. + assert self.fp is not None n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) while len(s): @@ -340,6 +345,7 @@ class JpegImageFile(ImageFile.ImageFile): format_description = "JPEG (ISO 10918)" def _open(self) -> None: + assert self.fp is not None s = self.fp.read(3) if not _accept(s): @@ -408,6 +414,7 @@ def load_read(self, read_bytes: int) -> bytes: For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker so libjpeg can finish decoding """ + assert self.fp is not None s = self.fp.read(read_bytes) if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"): diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 9ce38c427b6..99a07bae02c 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -67,6 +67,7 @@ def _open(self) -> None: self._n_frames = len(self.images) self.is_animated = self._n_frames > 1 + assert self.fp is not None self.__fp = self.fp self.seek(0) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index b1ae07873ac..9360061ba18 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -106,6 +106,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): _close_exclusive_fp_after_loading = False def _open(self) -> None: + assert self.fp is not None self.fp.seek(0) # prep the fp in order to pass the JPEG test JpegImagePlugin.JpegImageFile._open(self) self._after_jpeg_open() @@ -125,6 +126,7 @@ def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None: assert self.n_frames == len(self.__mpoffsets) del self.info["mpoffset"] # no longer needed self.is_animated = self.n_frames > 1 + assert self.fp is not None self._fp = self.fp # FIXME: hack self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame self.__frame = 0 diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index e3588735f20..2508f856644 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -759,6 +759,7 @@ class PngImageFile(ImageFile.ImageFile): format_description = "Portable network graphics" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(8)): msg = "not a PNG file" raise SyntaxError(msg) @@ -988,6 +989,7 @@ def load_read(self, read_bytes: int) -> bytes: """internal: read more image data""" assert self.png is not None + assert self.fp is not None while self.__idat == 0: # end of chunk, skip forward to next one @@ -1021,6 +1023,7 @@ def load_read(self, read_bytes: int) -> bytes: def load_end(self) -> None: """internal: finished reading image data""" assert self.png is not None + assert self.fp is not None if self.__idat != 0: self.fp.read(self.__idat) while True: diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index f49aaeeb1f5..69a8703dd8b 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -61,6 +61,7 @@ class PsdImageFile(ImageFile.ImageFile): _close_exclusive_fp_after_loading = False def _open(self) -> None: + assert self.fp is not None read = self.fp.read # diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index dba5d809fef..d0709b1198a 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -25,6 +25,7 @@ class QoiImageFile(ImageFile.ImageFile): format_description = "Quite OK Image" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(4)): msg = "not a QOI file" raise SyntaxError(msg) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 7c3e84d7495..8662922437e 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -104,6 +104,7 @@ class SpiderImageFile(ImageFile.ImageFile): def _open(self) -> None: # check header n = 27 * 4 # read 27 float values + assert self.fp is not None f = self.fp.read(n) try: diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index 5494f62e892..fb3e1c06a32 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -39,6 +39,7 @@ def _open(self) -> None: self._mode = "P" # read header fields + assert self.fp is not None header = self.fp.read(32 + 24 + 32 + 12) self._size = i32(header, 32), i32(header, 36) Image._decompression_bomb_check(self.size) @@ -54,6 +55,7 @@ def _open(self) -> None: def load(self) -> Image.core.PixelAccess | None: if self._im is None: + assert self.fp is not None self.im = Image.core.new(self.mode, self.size) self.frombytes(self.fp.read(self.size[0] * self.size[1])) self.putpalette(quake2palette) diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index de714d33794..3ae86242a8b 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -49,6 +49,7 @@ def open(self, im: ImageFile.StubImageFile) -> None: self.bbox = im.info["wmf_bbox"] def load(self, im: ImageFile.StubImageFile) -> Image.Image: + assert im.fp is not None im.fp.seek(0) # rewind return Image.frombytes( "RGB", @@ -81,6 +82,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): def _open(self) -> None: # check placeable header + assert self.fp is not None s = self.fp.read(44) if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"): From 51b35d17e1429a7a1fda48e61832fb9dd9bc2adf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Jan 2026 20:15:05 +1100 Subject: [PATCH 2202/2374] Added fp type hint --- src/PIL/ImageFile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index f609c7d13c2..1df1d33a149 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -131,6 +131,7 @@ def __init__( self.decoderconfig: tuple[Any, ...] = () self.decodermaxblock = MAXBLOCK + self.fp: IO[bytes] | None self._fp: IO[bytes] | DeferredError if is_path(fp): # filename @@ -268,7 +269,7 @@ def verify(self) -> None: # raise exception if something's wrong. must be called # directly after open, and closes file when finished. - if self._exclusive_fp: + if self._exclusive_fp and self.fp: self.fp.close() self.fp = None From ce11a0c4993c284791b3100227cbd0338937074d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Jan 2026 20:27:48 +1100 Subject: [PATCH 2203/2374] Added ImageFile context manager --- Tests/test_file_jpeg2k.py | 2 +- Tests/test_file_png.py | 2 +- Tests/test_file_ppm.py | 2 +- Tests/test_image_transform.py | 4 ++-- src/PIL/Image.py | 11 +++-------- src/PIL/ImageFile.py | 9 +++++++++ 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index a5365a90d44..575d911def5 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -164,7 +164,7 @@ def test_reduce() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert callable(im.reduce) - im.reduce = 2 + im.reduce = 2 # type: ignore[assignment, method-assign] assert im.reduce == 2 im.load() diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index e9830cd3dad..ed3a91285db 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -823,7 +823,7 @@ class MyStdOut: monkeypatch.setattr(sys, "stdout", mystdout) with Image.open(TEST_PNG_FILE) as im: - im.save(sys.stdout, "PNG") + im.save(sys.stdout, "PNG") # type: ignore[arg-type] if isinstance(mystdout, MyStdOut): mystdout = mystdout.buffer diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 598e9a445b6..fbca46be513 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -389,7 +389,7 @@ class MyStdOut: monkeypatch.setattr(sys, "stdout", mystdout) with Image.open(TEST_FILE) as im: - im.save(sys.stdout, "PPM") + im.save(sys.stdout, "PPM") # type: ignore[arg-type] if isinstance(mystdout, MyStdOut): mystdout = mystdout.buffer diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 7cf52ddbabe..3e2b9fee8ed 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -250,14 +250,14 @@ def test_blank_fill(self) -> None: def test_missing_method_data(self) -> None: with hopper() as im: with pytest.raises(ValueError): - im.transform((100, 100), None) + im.transform((100, 100), None) # type: ignore[arg-type] @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None: with hopper() as im: (w, h) = im.size with pytest.raises(ValueError): - im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) + im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) # type: ignore[arg-type] class TestImageTransformAffine: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4e61900f6fd..b4de099be53 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -590,16 +590,11 @@ def _new(self, im: core.ImagingCore) -> Image: return new # Context manager support - def __enter__(self): + def __enter__(self) -> Image: return self - def __exit__(self, *args): - from . import ImageFile - - if isinstance(self, ImageFile.ImageFile): - if getattr(self, "_exclusive_fp", False): - self._close_fp() - self.fp = None + def __exit__(self, *args: object) -> None: + pass def close(self) -> None: """ diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 1df1d33a149..3390dfa97dd 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -169,6 +169,10 @@ def __init__( def _open(self) -> None: pass + # Context manager support + def __enter__(self) -> ImageFile: + return self + def _close_fp(self) -> None: if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError): if self._fp != self.fp: @@ -177,6 +181,11 @@ def _close_fp(self) -> None: if self.fp: self.fp.close() + def __exit__(self, *args: object) -> None: + if getattr(self, "_exclusive_fp", False): + self._close_fp() + self.fp = None + def close(self) -> None: """ Closes the file pointer, if possible. From 2d589107fb3a4aba8389932a65ff771bf9b4deb1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jan 2026 03:49:56 +1100 Subject: [PATCH 2204/2374] Specify APNG duration type when opening --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index d26c0fbae5c..35ec99ece45 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -999,7 +999,7 @@ where applicable: The number of times to loop this APNG, 0 indicates infinite looping. **duration** - The time to display this APNG frame (in milliseconds). + The time to display this APNG frame (in milliseconds), given as a float. .. note:: From 432707ea810ae619e2a9e4a9737c169cacaa8eda Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jan 2026 04:18:15 +1100 Subject: [PATCH 2205/2374] Added release notes for #9348 --- docs/releasenotes/12.1.0.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/releasenotes/12.1.0.rst b/docs/releasenotes/12.1.0.rst index 0541f882feb..5aaa52c905d 100644 --- a/docs/releasenotes/12.1.0.rst +++ b/docs/releasenotes/12.1.0.rst @@ -21,3 +21,11 @@ macOS in addition to Windows. On macOS, this is a CGWindowID:: from PIL import ImageGrab ImageGrab.grab(window=cgwindowid) + +Other changes +============= + +Added MorphOp support for 1 mode images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:class:`~PIL.ImageMorph.MorphOp` now supports both 1 mode and L mode images. From 3baedf264804d199bc19458d11bcff02ce7598eb Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 2 Jan 2026 10:59:56 +1100 Subject: [PATCH 2206/2374] Deprecate getdata(), in favour of new get_flattened_data() (#9292) --- Tests/helper.py | 4 ++-- Tests/test_box_blur.py | 8 +++++-- Tests/test_file_avif.py | 2 -- Tests/test_file_libtiff.py | 1 - Tests/test_file_webp.py | 2 -- Tests/test_file_webp_alpha.py | 4 ---- Tests/test_file_webp_lossless.py | 1 - Tests/test_format_lab.py | 12 +++++----- Tests/test_image.py | 4 ++-- Tests/test_image_array.py | 2 +- Tests/test_image_crop.py | 4 ++-- Tests/test_image_getdata.py | 20 ++++++++++++---- Tests/test_image_putdata.py | 39 ++++++++++++++++---------------- Tests/test_image_resize.py | 2 +- Tests/test_imagecms.py | 12 +++++----- Tests/test_numpy.py | 14 +++++------- docs/deprecations.rst | 10 ++++++++ docs/reference/Image.rst | 1 + docs/releasenotes/12.1.0.rst | 18 +++++++++++++++ src/PIL/GifImagePlugin.py | 2 +- src/PIL/Image.py | 19 ++++++++++++++++ src/PIL/_deprecate.py | 2 ++ 22 files changed, 117 insertions(+), 66 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index b93828f5832..d77b4b807ec 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -55,8 +55,8 @@ def convert_to_comparable( if a.mode == "P": new_a = Image.new("L", a.size) new_b = Image.new("L", b.size) - new_a.putdata(a.getdata()) - new_b.putdata(b.getdata()) + new_a.putdata(a.get_flattened_data()) + new_b.putdata(b.get_flattened_data()) elif a.mode == "I;16": new_a = a.convert("I") new_b = b.convert("I") diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index cb267b2048f..07e62db8ced 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -28,9 +28,13 @@ def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image: def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None: - it = iter(im.getdata()) + it = iter(im.get_flattened_data()) for data_row in data: - im_row = [next(it) for _ in range(im.size[0])] + im_row = [] + for _ in range(im.width): + im_v = next(it) + assert isinstance(im_v, (int, float)) + im_row.append(im_v) if any(abs(data_v - im_v) > delta for data_v, im_v in zip(data_row, im_row)): assert im_row == data_row with pytest.raises(StopIteration): diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 727191153a1..ffc4ce0210d 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -121,7 +121,6 @@ def test_read(self) -> None: assert image.size == (128, 128) assert image.format == "AVIF" assert image.get_format_mimetype() == "image/avif" - image.getdata() # generated with: # avifdec hopper.avif hopper_avif_write.png @@ -143,7 +142,6 @@ def test_write_rgb(self, tmp_path: Path) -> None: assert reloaded.mode == "RGB" assert reloaded.size == (128, 128) assert reloaded.format == "AVIF" - reloaded.getdata() # avifdec hopper.avif avif/hopper_avif_write.png assert_image_similar_tofile( diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 90ca2cf1216..c2336c05856 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -42,7 +42,6 @@ def _assert_noerr(self, tmp_path: Path, im: ImageFile.ImageFile) -> None: # Does the data actually load im.load() - im.getdata() assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im._compression == "group4" diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 5456adf59d2..f996cce675c 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -60,7 +60,6 @@ def test_read_rgb(self) -> None: assert image.size == (128, 128) assert image.format == "WEBP" image.load() - image.getdata() # generated with: # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm @@ -77,7 +76,6 @@ def _roundtrip( assert image.size == (128, 128) assert image.format == "WEBP" image.load() - image.getdata() if mode == self.rgb_mode: # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index c573390c403..b1aa45f6b52 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -29,7 +29,6 @@ def test_read_rgba() -> None: assert image.size == (200, 150) assert image.format == "WEBP" image.load() - image.getdata() image.tobytes() @@ -60,7 +59,6 @@ def test_write_lossless_rgb(tmp_path: Path) -> None: assert image.size == pil_image.size assert image.format == "WEBP" image.load() - image.getdata() assert_image_equal(image, pil_image) @@ -83,7 +81,6 @@ def test_write_rgba(tmp_path: Path) -> None: assert image.size == (10, 10) assert image.format == "WEBP" image.load() - image.getdata() assert_image_similar(image, pil_image, 1.0) @@ -133,7 +130,6 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None: assert image.format == "WEBP" image.load() - image.getdata() with Image.open(file_path) as im: target = im.convert("RGBA") diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 5eaa4f59906..b4c0448ac75 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -24,6 +24,5 @@ def test_write_lossless_rgb(tmp_path: Path) -> None: assert image.size == (128, 128) assert image.format == "WEBP" image.load() - image.getdata() assert_image_equal(image, hopper(RGB_MODE)) diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index 4fcc37e88cc..5f4a704f213 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -13,15 +13,15 @@ def test_white() -> None: k = i.getpixel((0, 0)) - L = i.getdata(0) - a = i.getdata(1) - b = i.getdata(2) + L = i.get_flattened_data(0) + a = i.get_flattened_data(1) + b = i.get_flattened_data(2) assert k == (255, 128, 128) - assert list(L) == [255] * 100 - assert list(a) == [128] * 100 - assert list(b) == [128] * 100 + assert L == (255,) * 100 + assert a == (128,) * 100 + assert b == (128,) * 100 def test_green() -> None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 88f55638ee9..afc6e8e166b 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1181,10 +1181,10 @@ def test_roundtrip_bytes_method(self, mode: str) -> None: assert reloaded.tobytes() == source_bytes @pytest.mark.parametrize("mode", Image.MODES) - def test_getdata_putdata(self, mode: str) -> None: + def test_get_flattened_data_putdata(self, mode: str) -> None: im = hopper(mode) reloaded = Image.new(mode, im.size) - reloaded.putdata(im.getdata()) + reloaded.putdata(im.get_flattened_data()) assert_image_equal(im, reloaded) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index abb22f94967..220e128d168 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -78,7 +78,7 @@ def test(mode: str) -> tuple[str, tuple[int, int], bool]: }, ) out = Image.fromarray(wrapped) - return out.mode, out.size, list(i.getdata()) == list(out.getdata()) + return out.mode, out.size, i.get_flattened_data() == out.get_flattened_data() # assert test("1") == ("1", (128, 100), True) assert test("L") == ("L", (128, 100), True) diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index b90ce84bc02..9df8883a4f0 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -95,10 +95,10 @@ def test_crop_zero() -> None: cropped = im.crop((10, 10, 20, 20)) assert cropped.size == (10, 10) - assert cropped.getdata()[0] == (0, 0, 0) + assert cropped.getpixel((0, 0)) == (0, 0, 0) im = Image.new("RGB", (0, 0)) cropped = im.crop((10, 10, 20, 20)) assert cropped.size == (10, 10) - assert cropped.getdata()[2] == (0, 0, 0) + assert cropped.getpixel((2, 0)) == (0, 0, 0) diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index c8b213d841b..94d6cbaa2e7 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,23 +1,23 @@ from __future__ import annotations +import pytest + from PIL import Image from .helper import hopper def test_sanity() -> None: - data = hopper().getdata() - - len(data) - list(data) + data = hopper().get_flattened_data() + assert len(data) == 128 * 128 assert data[0] == (20, 20, 70) def test_mode() -> None: def getdata(mode: str) -> tuple[float | tuple[int, ...] | None, int, int]: im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) - data = im.getdata() + data = im.get_flattened_data() return data[0], len(data), len(list(data)) assert getdata("1") == (0, 960, 960) @@ -28,3 +28,13 @@ def getdata(mode: str) -> tuple[float | tuple[int, ...] | None, int, int]: assert getdata("RGBA") == ((11, 13, 52, 255), 960, 960) assert getdata("CMYK") == ((244, 242, 203, 0), 960, 960) assert getdata("YCbCr") == ((16, 147, 123), 960, 960) + + +def test_deprecation() -> None: + im = hopper() + with pytest.warns(DeprecationWarning, match="getdata"): + data = im.getdata() + + assert len(data) == 128 * 128 + assert data[0] == (20, 20, 70) + assert list(data)[0] == (20, 20, 70) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index bf8e89b53ce..226cb4c14d8 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -2,6 +2,7 @@ import sys from array import array +from typing import cast import pytest @@ -12,21 +13,19 @@ def test_sanity() -> None: im1 = hopper() + for data in (im1.get_flattened_data(), im1.im): + im2 = Image.new(im1.mode, im1.size, 0) + im2.putdata(data) - data = list(im1.getdata()) + assert_image_equal(im1, im2) - im2 = Image.new(im1.mode, im1.size, 0) - im2.putdata(data) + # readonly + im2 = Image.new(im1.mode, im2.size, 0) + im2.readonly = 1 + im2.putdata(data) - assert_image_equal(im1, im2) - - # readonly - im2 = Image.new(im1.mode, im2.size, 0) - im2.readonly = 1 - im2.putdata(data) - - assert not im2.readonly - assert_image_equal(im1, im2) + assert not im2.readonly + assert_image_equal(im1, im2) def test_long_integers() -> None: @@ -60,22 +59,22 @@ def test_mode_with_L_with_float() -> None: @pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) def test_mode_i(mode: str) -> None: src = hopper("L") - data = list(src.getdata()) + data = src.get_flattened_data() im = Image.new(mode, src.size, 0) im.putdata(data, 2, 256) - target = [2 * elt + 256 for elt in data] - assert list(im.getdata()) == target + target = tuple(2 * elt + 256 for elt in cast(tuple[int, ...], data)) + assert im.get_flattened_data() == target def test_mode_F() -> None: src = hopper("L") - data = list(src.getdata()) + data = src.get_flattened_data() im = Image.new("F", src.size, 0) im.putdata(data, 2.0, 256.0) - target = [2.0 * float(elt) + 256.0 for elt in data] - assert list(im.getdata()) == target + target = tuple(2.0 * float(elt) + 256.0 for elt in cast(tuple[int, ...], data)) + assert im.get_flattened_data() == target def test_array_B() -> None: @@ -86,7 +85,7 @@ def test_array_B() -> None: im = Image.new("L", (150, 100)) im.putdata(arr) - assert len(im.getdata()) == len(arr) + assert len(im.get_flattened_data()) == len(arr) def test_array_F() -> None: @@ -97,7 +96,7 @@ def test_array_F() -> None: arr = array("f", [0.0]) * 15000 im.putdata(arr) - assert len(im.getdata()) == len(arr) + assert len(im.get_flattened_data()) == len(arr) def test_not_flattened() -> None: diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 323d31f51fd..3e8979a5b11 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -160,7 +160,7 @@ def test_enlarge_zero(self, resample: Image.Resampling) -> None: r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample) assert r.mode == "RGB" assert r.size == (212, 195) - assert r.getdata()[0] == (0, 0, 0) + assert r.getpixel((0, 0)) == (0, 0, 0) def test_unknown_filter(self) -> None: with pytest.raises(ValueError): diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 5fd7caa7cc7..a30fb18b80a 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -274,13 +274,13 @@ def test_simple_lab() -> None: # not a linear luminance map. so L != 128: assert k == (137, 128, 128) - l_data = i_lab.getdata(0) - a_data = i_lab.getdata(1) - b_data = i_lab.getdata(2) + l_data = i_lab.get_flattened_data(0) + a_data = i_lab.get_flattened_data(1) + b_data = i_lab.get_flattened_data(2) - assert list(l_data) == [137] * 100 - assert list(a_data) == [128] * 100 - assert list(b_data) == [128] * 100 + assert l_data == (137,) * 100 + assert a_data == (128,) * 100 + assert b_data == (128,) * 100 def test_lab_color() -> None: diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index f6acb3affb6..113d3075532 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -20,21 +20,19 @@ def test_numpy_to_image() -> None: def to_image(dtype: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Image: + data = tuple(range(100)) if bands == 1: if boolean: - data = [0, 255] * 50 - else: - data = list(range(100)) + data = (0, 255) * 50 a = numpy.array(data, dtype=dtype) a.shape = TEST_IMAGE_SIZE i = Image.fromarray(a) - assert list(i.getdata()) == data + assert i.get_flattened_data() == data else: - data = list(range(100)) a = numpy.array([[x] * bands for x in data], dtype=dtype) a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands i = Image.fromarray(a) - assert list(i.getchannel(0).getdata()) == list(range(100)) + assert i.get_flattened_data(0) == tuple(range(100)) return i # Check supported 1-bit integer formats @@ -191,7 +189,7 @@ def test_putdata() -> None: arr = numpy.zeros((15000,), numpy.float32) im.putdata(arr) - assert len(im.getdata()) == len(arr) + assert len(im.get_flattened_data()) == len(arr) def test_resize() -> None: @@ -248,7 +246,7 @@ def test_bool() -> None: a[0][0] = True im2 = Image.fromarray(a) - assert im2.getdata()[0] == 255 + assert im2.getpixel((0, 0)) == 255 def test_no_resource_warning_for_numpy_array() -> None: diff --git a/docs/deprecations.rst b/docs/deprecations.rst index cc5ac283fbf..b6a7af0a824 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -73,6 +73,16 @@ Image._show ``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15). Use :py:meth:`~PIL.ImageShow.show` instead. +Image getdata() +~~~~~~~~~~~~~~~ + +.. deprecated:: 12.1.0 + +:py:meth:`~PIL.Image.Image.getdata` has been deprecated. +:py:meth:`~PIL.Image.Image.get_flattened_data` can be used instead. This new method is +identical, except that it returns a tuple of pixel values, instead of an internal +Pillow data type. + Removed features ---------------- diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index e687229000b..adee49228d2 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -191,6 +191,7 @@ This helps to get the bounding box coordinates of the input image:: .. automethod:: PIL.Image.Image.getchannel .. automethod:: PIL.Image.Image.getcolors .. automethod:: PIL.Image.Image.getdata +.. automethod:: PIL.Image.Image.get_flattened_data .. automethod:: PIL.Image.Image.getexif .. automethod:: PIL.Image.Image.getextrema .. automethod:: PIL.Image.Image.getpalette diff --git a/docs/releasenotes/12.1.0.rst b/docs/releasenotes/12.1.0.rst index 5aaa52c905d..9740b700842 100644 --- a/docs/releasenotes/12.1.0.rst +++ b/docs/releasenotes/12.1.0.rst @@ -1,6 +1,17 @@ 12.1.0 ------ +Deprecations +============ + +Image getdata() +^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.getdata` has been deprecated. +:py:meth:`~PIL.Image.Image.get_flattened_data` can be used instead. This new method is +identical, except that it returns a tuple of pixel values, instead of an internal +Pillow data type. + API changes =========== @@ -13,6 +24,13 @@ To match the behaviour of :py:meth:`~PIL.ImageMorph.LutBuilder.build_lut`, API additions ============= +Image get_flattened_data() +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.get_flattened_data` is identical to the deprecated +:py:meth:`~PIL.Image.Image.getdata`, except that the new method returns a tuple of +pixel values, instead of an internal Pillow data type. + Specify window in ImageGrab on macOS ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index b8755422d05..76a0d4ab99f 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -753,7 +753,7 @@ def _write_multiple_frames( if delta.mode == "P": # Convert to L without considering palette delta_l = Image.new("L", delta.size) - delta_l.putdata(delta.getdata()) + delta_l.putdata(delta.get_flattened_data()) delta = delta_l mask = ImageMath.lambda_eval( lambda args: args["convert"](args["im"] * 255, "1"), diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b4de099be53..57ebea68964 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1435,12 +1435,31 @@ def getdata(self, band: int | None = None) -> core.ImagingCore: value (e.g. 0 to get the "R" band from an "RGB" image). :returns: A sequence-like object. """ + deprecate("Image.Image.getdata", 14, "get_flattened_data") self.load() if band is not None: return self.im.getband(band) return self.im # could be abused + def get_flattened_data( + self, band: int | None = None + ) -> tuple[tuple[int, ...], ...] | tuple[float, ...]: + """ + Returns the contents of this image as a tuple containing pixel values. + The sequence object is flattened, so that values for line one follow + directly after the values of line zero, and so on. + + :param band: What band to return. The default is to return + all bands. To return a single band, pass in the index + value (e.g. 0 to get the "R" band from an "RGB" image). + :returns: A tuple containing pixel values. + """ + self.load() + if band is not None: + return tuple(self.im.getband(band)) + return tuple(self.im) + def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]: """ Gets the minimum and maximum pixel values for each band in diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 616a9aace9f..711c62ab2b1 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -48,6 +48,8 @@ def deprecate( raise RuntimeError(msg) elif when == 13: removed = "Pillow 13 (2026-10-15)" + elif when == 14: + removed = "Pillow 14 (2027-10-15)" else: msg = f"Unknown removal version: {when}. Update {__name__}?" raise ValueError(msg) From 46f45f674d47b5d8bc54230dda8fe9e214598b87 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jan 2026 17:03:05 +1100 Subject: [PATCH 2207/2374] 12.1.0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 41cb17a3697..b32ff446a9e 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "12.1.0.dev0" +__version__ = "12.1.0" From 4337139f0caa7d858131ce8f36e2ded54c60fd60 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jan 2026 20:16:49 +1100 Subject: [PATCH 2208/2374] 12.2.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index b32ff446a9e..96363e9f154 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "12.1.0" +__version__ = "12.2.0.dev0" From 5b677ca1c69b3b06b4a97ae5f22103dae12e8d6d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jan 2026 20:31:47 +1100 Subject: [PATCH 2209/2374] Assert palette is not None --- Tests/test_imagedraw.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 44aec4a9e15..3c61833fe04 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -71,10 +71,12 @@ def test_sanity() -> None: def test_new_color() -> None: with Image.open("Tests/images/chi.gif") as im: draw = ImageDraw.Draw(im) + assert im.palette is not None assert len(im.palette.colors) == 249 # Test drawing a new color onto the palette draw.line((0, 0), fill=(0, 0, 0)) + assert im.palette is not None assert len(im.palette.colors) == 250 assert im.palette.dirty From 499b796556a67bf3a44434ee45a8bd3deed7e645 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:30:14 +0200 Subject: [PATCH 2210/2374] Remove Sphinx dependency from mypy --- .ci/requirements-mypy.txt | 1 - docs/dater.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 5b0e2eaf8d7..375b8fc5d40 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -9,7 +9,6 @@ packaging pyarrow-stubs pybind11 pytest -sphinx types-atheris types-defusedxml types-olefile diff --git a/docs/dater.py b/docs/dater.py index c0302b55c35..87dacbd5a12 100644 --- a/docs/dater.py +++ b/docs/dater.py @@ -11,7 +11,7 @@ TYPE_CHECKING = False if TYPE_CHECKING: - from sphinx.application import Sphinx + from typing import Any DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+") VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") @@ -28,7 +28,7 @@ def get_date_for(git_version: str) -> str | None: return out.split()[0] -def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None: +def add_date(app: Any, doc_name: str, source: list[str]) -> None: if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])): old_title = m.group(1) @@ -43,6 +43,6 @@ def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None: source[0] = result -def setup(app: Sphinx) -> dict[str, bool]: +def setup(app: Any) -> dict[str, bool]: app.connect("source-read", add_date) return {"parallel_read_safe": True} From 2360d0df1757747af4150253f170672cfafece91 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:31:22 +0200 Subject: [PATCH 2211/2374] Revert "Use minimum supported Python version for Lint (#9364)" This reverts commit 900636e7dbc8ead2caaae0e387096fa8bf6474d5. --- .github/workflows/lint.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4f67be6f705..e2f8bf47ac4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,7 +23,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version: "3.x" - name: Install uv uses: astral-sh/setup-uv@v7 - name: Lint diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10343f91ae3..8477729e636 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,7 +76,7 @@ repos: rev: v0.24.1 hooks: - id: validate-pyproject - additional_dependencies: [tomli, trove-classifiers>=2024.10.12] + additional_dependencies: [trove-classifiers>=2024.10.12] - repo: https://github.com/tox-dev/tox-ini-fmt rev: 1.7.0 From e924cfd181258f3201a91f276283ab83939a6744 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jan 2026 21:32:22 +1100 Subject: [PATCH 2212/2374] Fix unclosed file warning --- Tests/test_imagemorph.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index daba3001557..aa8356b0b33 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -231,15 +231,15 @@ def test_negate() -> None: def test_incorrect_mode() -> None: - im = hopper() mop = ImageMorph.MorphOp(op_name="erosion8") - with pytest.raises(ValueError, match="Image mode must be 1 or L"): - mop.apply(im) - with pytest.raises(ValueError, match="Image mode must be 1 or L"): - mop.match(im) - with pytest.raises(ValueError, match="Image mode must be 1 or L"): - mop.get_on_pixels(im) + with hopper() as im: + with pytest.raises(ValueError, match="Image mode must be 1 or L"): + mop.apply(im) + with pytest.raises(ValueError, match="Image mode must be 1 or L"): + mop.match(im) + with pytest.raises(ValueError, match="Image mode must be 1 or L"): + mop.get_on_pixels(im) def test_add_patterns() -> None: From 555fb8371c9ea1c1492e7790f64e73ffb18cbfd8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Jan 2026 08:16:37 +1100 Subject: [PATCH 2213/2374] Move from deprecated getdata to get_flattened_data --- selftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selftest.py b/selftest.py index c484d4e2d42..64898fc920d 100755 --- a/selftest.py +++ b/selftest.py @@ -76,7 +76,7 @@ def testimage() -> None: ('R', 'G', 'B') >>> im.getbbox() (0, 0, 128, 128) - >>> len(im.getdata()) + >>> len(im.get_flattened_data()) 16384 >>> im.getextrema() ((0, 255), (0, 255), (0, 255)) From 844b10f894569b95e195075c2927cec5ecdfcd95 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Jan 2026 13:55:50 +1100 Subject: [PATCH 2214/2374] Update github-actions (#9375) --- .github/workflows/cifuzz.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/test-windows.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- .github/workflows/wheels.yml | 16 ++++++++-------- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 6a86b8aeb3b..7e771f1b7d1 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -44,13 +44,13 @@ jobs: language: python dry-run: false - name: Upload New Crash - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: failure() && steps.build.outcome == 'success' with: name: artifacts path: ./out/artifacts - name: Upload Legacy Crash - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: steps.run.outcome == 'success' with: name: crash diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e88abf16fbc..44af3e3dfdb 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -49,7 +49,7 @@ jobs: run: python3 .github/workflows/system-info.py - name: Cache libimagequant - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-libimagequant with: path: ~/cache-libimagequant diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e864763da21..f123a4f22d8 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -112,7 +112,7 @@ jobs: - name: Cache build id: build-cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: winbuild\build key: @@ -217,7 +217,7 @@ jobs: shell: bash - name: Upload errors - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: failure() with: name: errors diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da3eea06641..103d915c047 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -92,7 +92,7 @@ jobs: - name: Cache libimagequant if: startsWith(matrix.os, 'ubuntu') - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-libimagequant with: path: ~/cache-libimagequant @@ -143,7 +143,7 @@ jobs: mkdir -p Tests/errors - name: Upload errors - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: failure() with: name: errors diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index fb71ead37b5..82f1b5e903f 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -134,7 +134,7 @@ jobs: CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: dist-${{ matrix.name }} path: ./wheelhouse/*.whl @@ -220,13 +220,13 @@ jobs: shell: cmd - name: Upload wheels - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: dist-windows-${{ matrix.cibw_arch }} path: ./wheelhouse/*.whl - name: Upload fribidi.dll - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: fribidi-windows-${{ matrix.cibw_arch }} path: winbuild\build\bin\fribidi* @@ -246,7 +246,7 @@ jobs: - run: make sdist - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: dist-sdist path: dist/*.tar.gz @@ -256,7 +256,7 @@ jobs: runs-on: ubuntu-latest name: Count dists steps: - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: pattern: dist-* path: dist @@ -275,13 +275,13 @@ jobs: runs-on: ubuntu-latest name: Upload wheels to scientific-python-nightly-wheels steps: - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: pattern: dist-!(sdist)* path: dist merge-multiple: true - name: Upload wheels to scientific-python-nightly-wheels - uses: scientific-python/upload-nightly-action@b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2 + uses: scientific-python/upload-nightly-action@5748273c71e2d8d3a61f3a11a16421c8954f9ecf # 0.6.3 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} @@ -297,7 +297,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: pattern: dist-* path: dist From 525842215f422589efcd78b01721e444fe60611a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Jan 2026 13:59:38 +1100 Subject: [PATCH 2215/2374] Update dependency mypy to v1.19.1 (#9374) --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 375b8fc5d40..c64343a7347 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.19.0 +mypy==1.19.1 arro3-compute arro3-core IceSpringPySideStubs-PyQt6 From 36cf82ae76f5e3a728b9752308513aaceb2250f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Jan 2026 16:25:37 +1100 Subject: [PATCH 2216/2374] Updated xorgproto to 2025.1 --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index e1586b7c5a3..1485f074a99 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -267,7 +267,7 @@ function build { build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [[ -n "$IS_MACOS" ]]; then - build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto + build_simple xorgproto 2025.1 https://www.x.org/pub/individual/proto build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else From b8351fde412985f6f03dda40ae97019ab2e63401 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 3 Jan 2026 17:08:17 +1100 Subject: [PATCH 2217/2374] Added type hints to map_metadata_keys() (#9337) --- Tests/test_pyroma.py | 21 +++++++++++++-------- pyproject.toml | 1 + 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 5871a72134a..915dbe7b685 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -6,10 +6,15 @@ from PIL import __version__ +TYPE_CHECKING = False + +if TYPE_CHECKING: + from importlib.metadata import PackageMetadata + pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") -def map_metadata_keys(md): +def map_metadata_keys(md: PackageMetadata) -> dict[str, str | list[str] | None]: # 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 implementation is constructed from the relevant logic from @@ -17,16 +22,16 @@ def map_metadata_keys(md): # upstream to Pyroma as https://github.com/regebro/pyroma/pull/116, # so it may be possible to simplify this test in future. data = {} - for key in set(md.keys()): + for key in set(md): value = md.get_all(key) key = pyroma.projectdata.normalize(key) - if len(value) == 1: - value = value[0] - if value.strip() == "UNKNOWN": - continue - - data[key] = value + if value is not None and len(value) == 1: + first_value = value[0] + if first_value.strip() != "UNKNOWN": + data[key] = first_value + else: + data[key] = value return data diff --git a/pyproject.toml b/pyproject.toml index f4514925d1c..cc616bc547c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -217,6 +217,7 @@ testpaths = [ python_version = "3.10" pretty = true disallow_any_generics = true +disallow_untyped_defs = true enable_error_code = "ignore-without-code" extra_checks = true follow_imports = "silent" From bc0e2c0e61a548dbf566a25a53812b969b616b6d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 3 Jan 2026 20:18:57 +1100 Subject: [PATCH 2218/2374] Remove add-imaging-libs option from setup.py (#9378) Co-authored-by: Alexander Karpinsky --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index 032c1c6d263..35bea6fa44e 100644 --- a/setup.py +++ b/setup.py @@ -363,7 +363,6 @@ def __iter__(self) -> Iterator[str]: ("disable-platform-guessing", None, "Disable platform guessing"), ("debug", None, "Debug logging"), ] - + [("add-imaging-libs=", None, "Add libs to _imaging build")] ) @staticmethod @@ -374,7 +373,6 @@ def initialize_options(self) -> None: self.disable_platform_guessing = self.check_configuration( "platform-guessing", "disable" ) - self.add_imaging_libs = "" build_ext.initialize_options(self) for x in self.feature: setattr(self, f"disable_{x}", self.check_configuration(x, "disable")) @@ -901,7 +899,6 @@ def build_extensions(self) -> None: # core library libs: list[str | bool | None] = [] - libs.extend(self.add_imaging_libs.split()) defs: list[tuple[str, str | None]] = [] if feature.get("tiff"): libs.append(feature.get("tiff")) From fe236d77a55c648538149665fcd8dc76093afdad Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 3 Jan 2026 11:32:19 +0200 Subject: [PATCH 2219/2374] Add seven-day cooldown to Renovate --- .github/renovate.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/renovate.json b/.github/renovate.json index 91fa0426f9e..8187fc15b95 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -6,6 +6,7 @@ "labels": [ "Dependency" ], + "minimumReleaseAge": "7 days", "packageRules": [ { "groupName": "github-actions", From 2210714a436f1f9c5b70cd8912c99d67c48e6105 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:20:09 +0000 Subject: [PATCH 2220/2374] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.7 → v0.14.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.7...v0.14.10) - [github.com/psf/black-pre-commit-mirror: 25.11.0 → 25.12.0](https://github.com/psf/black-pre-commit-mirror/compare/25.11.0...25.12.0) - [github.com/pre-commit/mirrors-clang-format: v21.1.6 → v21.1.8](https://github.com/pre-commit/mirrors-clang-format/compare/v21.1.6...v21.1.8) - [github.com/python-jsonschema/check-jsonschema: 0.35.0 → 0.36.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.35.0...0.36.0) - [github.com/zizmorcore/zizmor-pre-commit: v1.18.0 → v1.19.0](https://github.com/zizmorcore/zizmor-pre-commit/compare/v1.18.0...v1.19.0) - [github.com/tox-dev/tox-ini-fmt: 1.7.0 → 1.7.1](https://github.com/tox-dev/tox-ini-fmt/compare/1.7.0...1.7.1) --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8477729e636..12b3d4b4a3d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.7 + rev: v0.14.10 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 25.11.0 + rev: 25.12.0 hooks: - id: black @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v21.1.6 + rev: v21.1.8 hooks: - id: clang-format types: [c] @@ -51,14 +51,14 @@ repos: exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.35.0 + rev: 0.36.0 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.18.0 + rev: v1.19.0 hooks: - id: zizmor @@ -79,7 +79,7 @@ repos: additional_dependencies: [trove-classifiers>=2024.10.12] - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.7.0 + rev: 1.7.1 hooks: - id: tox-ini-fmt From dcd52ebf652399d4f900cbc5579e97902ed1ff4d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Jan 2026 09:56:56 +1100 Subject: [PATCH 2221/2374] Simplified code --- Tests/test_file_psd.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 38a88cd17a8..8f2ca58a662 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -100,7 +100,7 @@ def test_seek_tell() -> None: im.seek(2) layer_number = im.tell() - assert layer_number == 2 + assert layer_number == 2 def test_seek_eoferror() -> None: @@ -138,7 +138,7 @@ def test_icc_profile() -> None: assert "icc_profile" in im.info icc_profile = im.info["icc_profile"] - assert len(icc_profile) == 3144 + assert len(icc_profile) == 3144 def test_no_icc_profile() -> None: @@ -158,17 +158,16 @@ def test_combined_larger_than_size() -> None: @pytest.mark.parametrize( - "test_file,raises", + "test_file", [ - ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError), - ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), + "Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", + "Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", ], ) -def test_crashes(test_file: str, raises: type[Exception]) -> None: - with open(test_file, "rb") as f: - with pytest.raises(raises): - with Image.open(f): - pass +def test_crashes(test_file: str) -> None: + with pytest.raises(OSError): + with Image.open(test_file): + pass @pytest.mark.parametrize( @@ -179,8 +178,7 @@ def test_crashes(test_file: str, raises: type[Exception]) -> None: ], ) def test_layer_crashes(test_file: str) -> None: - with open(test_file, "rb") as f: - with Image.open(f) as im: - assert isinstance(im, PsdImagePlugin.PsdImageFile) - with pytest.raises(SyntaxError): - im.layers + with Image.open(test_file) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) + with pytest.raises(SyntaxError): + im.layers From 426ad8307dd6220a1158a7f895bdc371c5343356 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 Jan 2026 19:27:19 +1100 Subject: [PATCH 2222/2374] Fix joining rounded rectangle corners --- .../images/imagedraw_rounded_rectangle_radius.png | Bin 0 -> 456 bytes Tests/test_imagedraw.py | 12 ++++++++++++ src/PIL/ImageDraw.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 Tests/images/imagedraw_rounded_rectangle_radius.png diff --git a/Tests/images/imagedraw_rounded_rectangle_radius.png b/Tests/images/imagedraw_rounded_rectangle_radius.png new file mode 100644 index 0000000000000000000000000000000000000000..e2acf7be1b16f947afabda958c9ff8c7644fa15c GIT binary patch literal 456 zcmeAS@N?(olHy`uVBq!ia0vp^DImA@g)0_Y2$bOh1E8w9k6vq4A$JpwZw&~tk*KS4c<7F>U zTDCSZ<@zbVSyI{C|9S71xEyA`sq}049PuBoa-wHk*>LNUSoFp6!&^OnX7$}TwCXS0 z{S{VIZTH$FUaf2X{eJ#Q?Lw)vb5k6|og=pHd*Zou$GqA!Rs93+;#MDCb@c1HRa1}d zG1j(R%Otz`zEzFIAt4>!)p2#F<~RS{ch&lZNZPc94J<9H3ObC5oE+W`5ln}K1VBQ^ z>dT*fpJ4l~c6Ip$%`+g`J&8}xyJWlX3lqEHfB4ChqVJEwW+)Ukp7&YjF#W>Y>hB&_ zUwLJVxt(h&QzvCTJRjM&XHvex%cD8j$Hn4)9-kGobmhix>!sc$ZY}cn{kEC!-vYZ; tZy5U*b5v;g9QfSEG%tk}7Oo5o2hHWSct?B*OYZ@RdAj None: ) +def test_rounded_rectangle_radius() -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGB") + + # Act + draw.rounded_rectangle((25, 25, 75, 75), 24, fill="red", outline="green", width=5) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle_radius.png") + + @pytest.mark.parametrize( "xy, radius, type", [ diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 8bcf2d8ee06..eb108ac41ca 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -487,7 +487,7 @@ def draw_corners(pieslice: bool) -> None: if full_x: self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1) - elif x1 - r - 1 > x0 + r + 1: + elif x1 - r - 1 >= x0 + r + 1: self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1) if not full_x and not full_y: left = [x0, y0, x0 + r, y1] From d7dfeeb7adb365fb7416865d54daec344e17d427 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jan 2026 06:46:04 +1100 Subject: [PATCH 2223/2374] Updated lcms2 to 2.18 --- .github/workflows/wheels-dependencies.sh | 2 +- docs/installation/building-from-source.rst | 2 +- winbuild/build_prepare.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 1485f074a99..f62bdec3d5d 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -102,7 +102,7 @@ OPENJPEG_VERSION=2.5.4 XZ_VERSION=5.8.2 ZSTD_VERSION=1.5.7 TIFF_VERSION=4.7.1 -LCMS2_VERSION=2.17 +LCMS2_VERSION=2.18 ZLIB_NG_VERSION=2.3.2 LIBWEBP_VERSION=1.6.0 BZIP2_VERSION=1.0.8 diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index c86ebe896a7..1655b8f6015 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -51,7 +51,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.17**. + above uses liblcms2. Tested with **1.19** and **2.7-2.18**. * **libwebp** provides the WebP format. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 3377d952c0d..1cbb0695d09 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -118,7 +118,7 @@ def cmd_msbuild( "FRIBIDI": "1.0.16", "HARFBUZZ": "12.3.0", "JPEGTURBO": "3.1.3", - "LCMS2": "2.17", + "LCMS2": "2.18", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.4.1", "LIBPNG": "1.6.53", From 400ffbc18d43ea41b5d47c3dad3a2d7a4510ad56 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jan 2026 14:37:18 +1100 Subject: [PATCH 2224/2374] Raise EOFError when seeking too far --- Tests/test_file_psd.py | 5 +++++ src/PIL/PsdImagePlugin.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 8f2ca58a662..da572ae6338 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -85,6 +85,11 @@ def test_eoferror() -> None: # Test that seeking to the last frame does not raise an error im.seek(n_frames - 1) + # Test seeking past the last frame without calling n_frames first + with Image.open(test_file) as im: + with pytest.raises(EOFError): + im.seek(3) + def test_seek_tell() -> None: with Image.open(test_file) as im: diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 69a8703dd8b..dd3d5ab95fd 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -175,6 +175,9 @@ def seek(self, layer: int) -> None: raise self._fp.ex # seek to given layer (1..max) + if layer > len(self.layers): + msg = "no more images in PSD file" + raise EOFError(msg) _, mode, _, tile = self.layers[layer - 1] self._mode = mode self.tile = tile From 0f4becea731185977f4724e1cd05bd4558daeb82 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Jan 2026 19:01:09 +1100 Subject: [PATCH 2225/2374] Link to m from _imagingmath, except on Windows --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 35bea6fa44e..3d975950b6c 100644 --- a/setup.py +++ b/setup.py @@ -1089,7 +1089,11 @@ def debug_build() -> bool: Extension("PIL._webp", ["src/_webp.c"]), Extension("PIL._avif", ["src/_avif.c"]), Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), - Extension("PIL._imagingmath", ["src/_imagingmath.c"]), + Extension( + "PIL._imagingmath", + ["src/_imagingmath.c"], + libraries=None if sys.platform == "win32" else ["m"], + ), Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), ] From 7e208ccf9d1cae412c6d9d3e335b903e4e595fa6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Jan 2026 23:49:10 +1100 Subject: [PATCH 2226/2374] Change to ValueError when encoding an empty image --- Tests/test_file_jpeg.py | 2 +- Tests/test_file_libtiff.py | 2 +- src/PIL/JpegImagePlugin.py | 4 ---- src/encode.c | 4 ++++ 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index f818927f6a3..d78ed0401e2 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -85,7 +85,7 @@ def test_sanity(self) -> None: def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: f = tmp_path / "temp.jpg" im = Image.new("RGB", size) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="cannot write empty image"): im.save(f) def test_app(self) -> None: diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index c2336c05856..2ff5de4496b 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1244,7 +1244,7 @@ def test_save_single_strip( def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: im = Image.new("RGB", (0, 0)) out = tmp_path / "temp.tif" - with pytest.raises(SystemError): + with pytest.raises(ValueError, match="cannot write empty image"): im.save(out, compression=compression) def test_save_many_compressed(self, tmp_path: Path) -> None: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 894c1547d7b..2f11cbfe313 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -661,10 +661,6 @@ def get_sampling(im: Image.Image) -> int: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.width == 0 or im.height == 0: - msg = "cannot write empty image as JPEG" - raise ValueError(msg) - try: rawmode = RAWMODE[im.mode] except KeyError as e: diff --git a/src/encode.c b/src/encode.c index 513309c8d7d..7afe36cb3a0 100644 --- a/src/encode.c +++ b/src/encode.c @@ -239,6 +239,10 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { if (!im) { return NULL; } + if (im->xsize == 0 || im->ysize == 0) { + PyErr_SetString(PyExc_ValueError, "cannot write empty image"); + return NULL; + } encoder->im = im; From 2e9d54887b71467e0494eec06d7c242c6d6e1575 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jan 2026 19:42:18 +1100 Subject: [PATCH 2227/2374] Improved coverage --- Tests/test_imagepalette.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 6ad21502f9f..10b89a2c0c2 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,10 +1,11 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path import pytest -from PIL import Image, ImagePalette +from PIL import Image, ImagePalette, PaletteFile from .helper import assert_image_equal, assert_image_equal_tofile @@ -202,6 +203,19 @@ def test_2bit_palette(tmp_path: Path) -> None: assert_image_equal_tofile(img, outfile) +def test_getpalette() -> None: + b = BytesIO(b"0 1\n1 2 3 4") + p = PaletteFile.PaletteFile(b) + + palette, rawmode = p.getpalette() + assert palette[:6] == b"\x01\x01\x01\x02\x03\x04" + assert rawmode == "RGB" + + def test_invalid_palette() -> None: with pytest.raises(OSError): ImagePalette.load("Tests/images/hopper.jpg") + + b = BytesIO(b"1" * 101) + with pytest.raises(SyntaxError, match="bad palette file"): + PaletteFile.PaletteFile(b) From ef8ff756fa5973f7fc08c573ba97d5a411bc0751 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Jan 2026 12:10:01 +1100 Subject: [PATCH 2228/2374] Updated libpng to 1.6.54 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 1485f074a99..7c00f4b8028 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -96,7 +96,7 @@ else FREETYPE_VERSION=2.14.1 fi HARFBUZZ_VERSION=12.3.0 -LIBPNG_VERSION=1.6.53 +LIBPNG_VERSION=1.6.54 JPEGTURBO_VERSION=3.1.3 OPENJPEG_VERSION=2.5.4 XZ_VERSION=5.8.2 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 3377d952c0d..6ebc1669067 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -121,7 +121,7 @@ def cmd_msbuild( "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.4.1", - "LIBPNG": "1.6.53", + "LIBPNG": "1.6.54", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.4", "TIFF": "4.7.1", From 6b9de40533cf4ec60fcbb6c7351eed417dd2b042 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 18 Jan 2026 22:59:28 +0200 Subject: [PATCH 2229/2374] Lazy import only required plugin --- src/PIL/Image.py | 126 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 11 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index eb561611718..cd1abb9353b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -323,6 +323,99 @@ def getmodebands(mode: str) -> int: _initialized = 0 +# Mapping from file extension to plugin module name for lazy loading +_EXTENSION_PLUGIN: dict[str, str] = { + # Common formats (preinit) + ".bmp": "BmpImagePlugin", + ".dib": "BmpImagePlugin", + ".gif": "GifImagePlugin", + ".jfif": "JpegImagePlugin", + ".jpe": "JpegImagePlugin", + ".jpg": "JpegImagePlugin", + ".jpeg": "JpegImagePlugin", + ".pbm": "PpmImagePlugin", + ".pgm": "PpmImagePlugin", + ".pnm": "PpmImagePlugin", + ".ppm": "PpmImagePlugin", + ".pfm": "PpmImagePlugin", + ".png": "PngImagePlugin", + ".apng": "PngImagePlugin", + # Less common formats (init) + ".avif": "AvifImagePlugin", + ".avifs": "AvifImagePlugin", + ".blp": "BlpImagePlugin", + ".bufr": "BufrStubImagePlugin", + ".cur": "CurImagePlugin", + ".dcx": "DcxImagePlugin", + ".dds": "DdsImagePlugin", + ".ps": "EpsImagePlugin", + ".eps": "EpsImagePlugin", + ".fit": "FitsImagePlugin", + ".fits": "FitsImagePlugin", + ".fli": "FliImagePlugin", + ".flc": "FliImagePlugin", + ".fpx": "FpxImagePlugin", + ".ftc": "FtexImagePlugin", + ".ftu": "FtexImagePlugin", + ".gbr": "GbrImagePlugin", + ".grib": "GribStubImagePlugin", + ".h5": "Hdf5StubImagePlugin", + ".hdf": "Hdf5StubImagePlugin", + ".icns": "IcnsImagePlugin", + ".ico": "IcoImagePlugin", + ".im": "ImImagePlugin", + ".iim": "IptcImagePlugin", + ".jp2": "Jpeg2KImagePlugin", + ".j2k": "Jpeg2KImagePlugin", + ".jpc": "Jpeg2KImagePlugin", + ".jpf": "Jpeg2KImagePlugin", + ".jpx": "Jpeg2KImagePlugin", + ".j2c": "Jpeg2KImagePlugin", + ".mic": "MicImagePlugin", + ".mpg": "MpegImagePlugin", + ".mpeg": "MpegImagePlugin", + ".mpo": "MpoImagePlugin", + ".msp": "MspImagePlugin", + ".palm": "PalmImagePlugin", + ".pcd": "PcdImagePlugin", + ".pcx": "PcxImagePlugin", + ".pdf": "PdfImagePlugin", + ".pxr": "PixarImagePlugin", + ".psd": "PsdImagePlugin", + ".qoi": "QoiImagePlugin", + ".bw": "SgiImagePlugin", + ".rgb": "SgiImagePlugin", + ".rgba": "SgiImagePlugin", + ".sgi": "SgiImagePlugin", + ".ras": "SunImagePlugin", + ".tga": "TgaImagePlugin", + ".icb": "TgaImagePlugin", + ".vda": "TgaImagePlugin", + ".vst": "TgaImagePlugin", + ".tif": "TiffImagePlugin", + ".tiff": "TiffImagePlugin", + ".webp": "WebPImagePlugin", + ".wmf": "WmfImagePlugin", + ".emf": "WmfImagePlugin", + ".xbm": "XbmImagePlugin", + ".xpm": "XpmImagePlugin", +} + + +def _load_plugin_for_extension(ext: str | bytes) -> bool: + """Load only the plugin needed for a specific file extension.""" + if isinstance(ext, bytes): + ext = ext.decode() + plugin = _EXTENSION_PLUGIN.get(ext.lower()) + if plugin is None: + return False + + try: + __import__(f"PIL.{plugin}", globals(), locals(), []) + return True + except ImportError: + return False + def preinit() -> None: """ @@ -2535,11 +2628,13 @@ def save( # only set the name for metadata purposes filename = os.fspath(fp.name) - preinit() - filename_ext = os.path.splitext(filename)[1].lower() ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext + # Try loading only the plugin for this extension first + if not _load_plugin_for_extension(ext): + preinit() + if not format: if ext not in EXTENSION: init() @@ -3524,7 +3619,11 @@ def open( prefix = fp.read(16) - preinit() + # Try to load just the plugin needed for this file extension + # before falling back to preinit() which loads common plugins + ext = os.path.splitext(filename)[1] if filename else "" + if not (ext and _load_plugin_for_extension(ext)): + preinit() warning_messages: list[str] = [] @@ -3560,14 +3659,19 @@ def _open_core( im = _open_core(fp, filename, prefix, formats) if im is None and formats is ID: - checked_formats = ID.copy() - if init(): - im = _open_core( - fp, - filename, - prefix, - tuple(format for format in formats if format not in checked_formats), - ) + # Try preinit (few common plugins) then init (all plugins) + for loader in (preinit, init): + checked_formats = ID.copy() + loader() + if formats != checked_formats: + im = _open_core( + fp, + filename, + prefix, + tuple(f for f in formats if f not in checked_formats), + ) + if im is not None: + break if im: im._exclusive_fp = exclusive_fp From 1baf141146fdc46262460d8ca8617f1e46d29969 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Jan 2026 17:13:43 +1100 Subject: [PATCH 2230/2374] Check that _EXTENSION_PLUGIN contains all registered extensions --- Tests/test_image.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/test_image.py b/Tests/test_image.py index afc6e8e166b..c064939e81a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -466,6 +466,9 @@ def test_registered_extensions_uninitialized(self) -> None: # Assert assert Image._initialized == 2 + for extension in Image.EXTENSION: + assert extension in Image._EXTENSION_PLUGIN + def test_registered_extensions(self) -> None: # Arrange # Open an image to trigger plugin registration From 9c8059fdea6d4272b9eacc2eeb05dc32efe40d4f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Jan 2026 17:18:30 +1100 Subject: [PATCH 2231/2374] Cleanup .spider extension registered by test code during save --- Tests/test_file_spider.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 3b3c3b4a518..03494523b61 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -14,6 +14,10 @@ TEST_FILE = "Tests/images/hopper.spider" +def teardown_module() -> None: + del Image.EXTENSION[".spider"] + + def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() From b06118c2b3287d8061162c3bb90f2b672a5713e3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Jan 2026 17:24:28 +1100 Subject: [PATCH 2232/2374] Do not register empty extension --- src/PIL/SpiderImagePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 8662922437e..848dccda577 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -290,9 +290,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # get the filename extension and register it with Image - filename_ext = os.path.splitext(filename)[1] - ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext - Image.register_extension(SpiderImageFile.format, ext) + if filename_ext := os.path.splitext(filename)[1]: + ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext + Image.register_extension(SpiderImageFile.format, ext) _save(im, fp, filename) From 096c479cfb2f71fecbb38a9b2dc12e5a49518ed8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:28:42 +0200 Subject: [PATCH 2233/2374] If plugin has already been imported and registered the extension, return early Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/Image.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index cd1abb9353b..dfd64ef1922 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -406,6 +406,9 @@ def _load_plugin_for_extension(ext: str | bytes) -> bool: """Load only the plugin needed for a specific file extension.""" if isinstance(ext, bytes): ext = ext.decode() + if ext in EXTENSION: + return True + plugin = _EXTENSION_PLUGIN.get(ext.lower()) if plugin is None: return False From 5ea2d3a05619aff38e194f3b1c6a9c906755db9f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 20 Jan 2026 18:16:34 +1100 Subject: [PATCH 2234/2374] Updated MinGW Python version --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index ee70d84019a..0789b02b75a 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -57,7 +57,7 @@ These platforms are built and tested for every change. | Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 | | | PyPy3 | | | +----------------------------+---------------------+ -| | 3.12 (MinGW) | x86-64 | +| | 3.13 (MinGW) | x86-64 | +----------------------------------+----------------------------+---------------------+ From a6a701c4dbecde4392465a1bc5ffe8a82445a52f Mon Sep 17 00:00:00 2001 From: Steve Dougherty Date: Wed, 21 Jan 2026 06:01:00 -0500 Subject: [PATCH 2235/2374] Match PSDraw text() encoding to the latin-1 specification in setfont() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, characters that are in latin-1 but reflected differently in UTF-8 will not be properly rendered. For example,"ó" becomes "ó". --- src/PIL/PSDraw.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index 7fd4c5c94cf..e6b74a91888 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -100,7 +100,8 @@ def text(self, xy: tuple[int, int], text: str) -> None: Draws text at the given position. You must use :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. """ - text_bytes = bytes(text, "UTF-8") + # The font is loaded as ISOLatin1Encoding, so use latin-1 here. + text_bytes = bytes(text, "latin-1") text_bytes = b"\\(".join(text_bytes.split(b"(")) text_bytes = b"\\)".join(text_bytes.split(b")")) self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,))) From a0f51493ca8ce7e90169a41d96d037ef413b659f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Jan 2026 00:23:58 +1100 Subject: [PATCH 2236/2374] Refer to lazy importing, as lazy loading of images is separate --- src/PIL/Image.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index dfd64ef1922..88f79a79295 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -323,7 +323,7 @@ def getmodebands(mode: str) -> int: _initialized = 0 -# Mapping from file extension to plugin module name for lazy loading +# Mapping from file extension to plugin module name for lazy importing _EXTENSION_PLUGIN: dict[str, str] = { # Common formats (preinit) ".bmp": "BmpImagePlugin", @@ -402,8 +402,8 @@ def getmodebands(mode: str) -> int: } -def _load_plugin_for_extension(ext: str | bytes) -> bool: - """Load only the plugin needed for a specific file extension.""" +def _import_plugin_for_extension(ext: str | bytes) -> bool: + """Import only the plugin needed for a specific file extension.""" if isinstance(ext, bytes): ext = ext.decode() if ext in EXTENSION: @@ -2634,8 +2634,8 @@ def save( filename_ext = os.path.splitext(filename)[1].lower() ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext - # Try loading only the plugin for this extension first - if not _load_plugin_for_extension(ext): + # Try importing only the plugin for this extension first + if not _import_plugin_for_extension(ext): preinit() if not format: @@ -3625,7 +3625,7 @@ def open( # Try to load just the plugin needed for this file extension # before falling back to preinit() which loads common plugins ext = os.path.splitext(filename)[1] if filename else "" - if not (ext and _load_plugin_for_extension(ext)): + if not (ext and _import_plugin_for_extension(ext)): preinit() warning_messages: list[str] = [] From a6b36f0b6bd91a899b6a4038b7f875ce5382776b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Jan 2026 00:28:26 +1100 Subject: [PATCH 2237/2374] format overrides file extension when saving --- src/PIL/Image.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 88f79a79295..e7357cc27d5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2631,14 +2631,20 @@ def save( # only set the name for metadata purposes filename = os.fspath(fp.name) - filename_ext = os.path.splitext(filename)[1].lower() - ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext - - # Try importing only the plugin for this extension first - if not _import_plugin_for_extension(ext): + if format: preinit() + else: + filename_ext = os.path.splitext(filename)[1].lower() + ext = ( + filename_ext.decode() + if isinstance(filename_ext, bytes) + else filename_ext + ) + + # Try importing only the plugin for this extension first + if not _import_plugin_for_extension(ext): + preinit() - if not format: if ext not in EXTENSION: init() try: From 76d3116ef030e9abb960f6544a272d3060ed2353 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Jan 2026 18:48:38 +1100 Subject: [PATCH 2238/2374] Added logger messages to match init() --- src/PIL/Image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e7357cc27d5..7d007718df3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -414,9 +414,11 @@ def _import_plugin_for_extension(ext: str | bytes) -> bool: return False try: + logger.debug("Importing %s", plugin) __import__(f"PIL.{plugin}", globals(), locals(), []) return True - except ImportError: + except ImportError as e: + logger.debug("Image: failed to import %s: %s", plugin, e) return False From 34814d8d2fbcace1f31ad5857e1d61d540100812 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 24 Jan 2026 12:52:49 +0200 Subject: [PATCH 2239/2374] Improve wording Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7d007718df3..9ed251496c3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3630,8 +3630,8 @@ def open( prefix = fp.read(16) - # Try to load just the plugin needed for this file extension - # before falling back to preinit() which loads common plugins + # Try to import just the plugin needed for this file extension + # before falling back to preinit() which imports common plugins ext = os.path.splitext(filename)[1] if filename else "" if not (ext and _import_plugin_for_extension(ext)): preinit() From d737687fc3365eca286573a6cb10f08ed394bdaf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 25 Jan 2026 06:45:13 +1100 Subject: [PATCH 2240/2374] Updated harfbuzz to 12.3.2 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 7c00f4b8028..5ca65aca3b5 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -95,7 +95,7 @@ if [[ -n "$IOS_SDK" ]]; then else FREETYPE_VERSION=2.14.1 fi -HARFBUZZ_VERSION=12.3.0 +HARFBUZZ_VERSION=12.3.2 LIBPNG_VERSION=1.6.54 JPEGTURBO_VERSION=3.1.3 OPENJPEG_VERSION=2.5.4 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 6ebc1669067..54fc905e3dc 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ def cmd_msbuild( "BROTLI": "1.2.0", "FREETYPE": "2.14.1", "FRIBIDI": "1.0.16", - "HARFBUZZ": "12.3.0", + "HARFBUZZ": "12.3.2", "JPEGTURBO": "3.1.3", "LCMS2": "2.17", "LIBAVIF": "1.3.0", From c03618551492c55e98425254496112a3bee501b1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:48:41 +0200 Subject: [PATCH 2241/2374] Ensure lower before checking if ext in EXTENSION Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/Image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9ed251496c3..743632e1fb7 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -406,10 +406,11 @@ def _import_plugin_for_extension(ext: str | bytes) -> bool: """Import only the plugin needed for a specific file extension.""" if isinstance(ext, bytes): ext = ext.decode() + ext = ext.lower() if ext in EXTENSION: return True - plugin = _EXTENSION_PLUGIN.get(ext.lower()) + plugin = _EXTENSION_PLUGIN.get(ext) if plugin is None: return False From 2b186fceb8b3da3104e1ad7635b522aca481663f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:02:39 +0200 Subject: [PATCH 2242/2374] Use __spec__.parent instead of calculating each time --- src/PIL/Image.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 743632e1fb7..3e37754e14a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -416,7 +416,7 @@ def _import_plugin_for_extension(ext: str | bytes) -> bool: try: logger.debug("Importing %s", plugin) - __import__(f"PIL.{plugin}", globals(), locals(), []) + __import__(f"{__spec__.parent}.{plugin}", globals(), locals(), []) return True except ImportError as e: logger.debug("Image: failed to import %s: %s", plugin, e) @@ -481,11 +481,10 @@ def init() -> bool: if _initialized >= 2: return False - parent_name = __name__.rpartition(".")[0] for plugin in _plugins: try: logger.debug("Importing %s", plugin) - __import__(f"{parent_name}.{plugin}", globals(), locals(), []) + __import__(f"{__spec__.parent}.{plugin}", globals(), locals(), []) except ImportError as e: logger.debug("Image: failed to import %s: %s", plugin, e) From d08d7ee99e4f5f986f37709ab7308dcbc8c4a1d8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 25 Jan 2026 22:55:19 +1100 Subject: [PATCH 2243/2374] Check ext is not empty during save --- src/PIL/Image.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3e37754e14a..8a28b56bd3c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -404,6 +404,9 @@ def getmodebands(mode: str) -> int: def _import_plugin_for_extension(ext: str | bytes) -> bool: """Import only the plugin needed for a specific file extension.""" + if not ext: + return False + if isinstance(ext, bytes): ext = ext.decode() ext = ext.lower() @@ -3633,7 +3636,7 @@ def open( # Try to import just the plugin needed for this file extension # before falling back to preinit() which imports common plugins ext = os.path.splitext(filename)[1] if filename else "" - if not (ext and _import_plugin_for_extension(ext)): + if not _import_plugin_for_extension(ext): preinit() warning_messages: list[str] = [] From b6178303a1ea210f2a535543f5f7369a511e4201 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 26 Jan 2026 08:01:02 +1100 Subject: [PATCH 2244/2374] Improve error message (#9392) Co-authored-by: Andrew Murray Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/ImageFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 3390dfa97dd..78abe3c77be 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -814,7 +814,7 @@ def setimage( self.state.ysize = y1 - y0 if self.state.xsize <= 0 or self.state.ysize <= 0: - msg = "Size cannot be negative" + msg = "Size must be positive" raise ValueError(msg) if ( From a293273b31a23f2245116baf425f30c64485ced4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:10:37 +0200 Subject: [PATCH 2245/2374] Fix docstring typo --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index eb561611718..f150365d3b6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -326,7 +326,7 @@ def getmodebands(mode: str) -> int: def preinit() -> None: """ - Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers. + Explicitly loads BMP, GIF, JPEG, PPM and PNG file format drivers. It is called when opening or saving images. """ From 29ff5fcb5527ff38b15a724cdb03a115eea43be9 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 28 Jan 2026 08:43:14 +1100 Subject: [PATCH 2246/2374] Use monkeypatch (#9406) Co-authored-by: Andrew Murray --- Tests/test_file_pcx.py | 38 +++++++++++++++++++------------------- Tests/test_file_png.py | 10 +++------- Tests/test_font_leaks.py | 30 +++++++++++++++++------------- Tests/test_image.py | 6 ++++-- Tests/test_imagedraw.py | 18 ++++++------------ Tests/test_imagefile.py | 20 +++++++------------- Tests/test_imagefontpil.py | 20 +++++++++----------- Tests/test_psdraw.py | 11 ++++------- 8 files changed, 69 insertions(+), 84 deletions(-) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 2e999eff6e1..90740ab5761 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -119,36 +119,36 @@ def test_large_count(tmp_path: Path) -> None: _roundtrip(tmp_path, im) -def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None: - _last = ImageFile.MAXBLOCK - ImageFile.MAXBLOCK = size - try: - _roundtrip(tmp_path, im) - finally: - ImageFile.MAXBLOCK = _last +def _test_buffer_overflow( + tmp_path: Path, im: Image.Image, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(ImageFile, "MAXBLOCK", 1024) + _roundtrip(tmp_path, im) -def test_break_in_count_overflow(tmp_path: Path) -> None: +def test_break_in_count_overflow( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: im = Image.new("L", (256, 5)) px = im.load() assert px is not None for y in range(4): for x in range(256): px[x, y] = x % 128 - _test_buffer_overflow(tmp_path, im) + _test_buffer_overflow(tmp_path, im, monkeypatch) -def test_break_one_in_loop(tmp_path: Path) -> None: +def test_break_one_in_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: im = Image.new("L", (256, 5)) px = im.load() assert px is not None for y in range(5): for x in range(256): px[x, y] = x % 128 - _test_buffer_overflow(tmp_path, im) + _test_buffer_overflow(tmp_path, im, monkeypatch) -def test_break_many_in_loop(tmp_path: Path) -> None: +def test_break_many_in_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: im = Image.new("L", (256, 5)) px = im.load() assert px is not None @@ -157,10 +157,10 @@ def test_break_many_in_loop(tmp_path: Path) -> None: px[x, y] = x % 128 for x in range(8): px[x, 4] = 16 - _test_buffer_overflow(tmp_path, im) + _test_buffer_overflow(tmp_path, im, monkeypatch) -def test_break_one_at_end(tmp_path: Path) -> None: +def test_break_one_at_end(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: im = Image.new("L", (256, 5)) px = im.load() assert px is not None @@ -168,10 +168,10 @@ def test_break_one_at_end(tmp_path: Path) -> None: for x in range(256): px[x, y] = x % 128 px[0, 3] = 128 + 64 - _test_buffer_overflow(tmp_path, im) + _test_buffer_overflow(tmp_path, im, monkeypatch) -def test_break_many_at_end(tmp_path: Path) -> None: +def test_break_many_at_end(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: im = Image.new("L", (256, 5)) px = im.load() assert px is not None @@ -181,10 +181,10 @@ def test_break_many_at_end(tmp_path: Path) -> None: for x in range(4): px[x * 2, 3] = 128 + 64 px[x + 256 - 4, 3] = 0 - _test_buffer_overflow(tmp_path, im) + _test_buffer_overflow(tmp_path, im, monkeypatch) -def test_break_padding(tmp_path: Path) -> None: +def test_break_padding(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: im = Image.new("L", (257, 5)) px = im.load() assert px is not None @@ -193,4 +193,4 @@ def test_break_padding(tmp_path: Path) -> None: px[x, y] = x % 128 for x in range(5): px[x, 3] = 0 - _test_buffer_overflow(tmp_path, im) + _test_buffer_overflow(tmp_path, im, monkeypatch) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ed3a91285db..2e0af504183 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -654,21 +654,17 @@ def test_unknown_compression_method(self) -> None: with pytest.raises(SyntaxError, match="Unknown compression method"): PngImagePlugin.PngImageFile("Tests/images/unknown_compression_method.png") - def test_padded_idat(self) -> None: + def test_padded_idat(self, monkeypatch: pytest.MonkeyPatch) -> None: # This image has been manually hexedited # so that the IDAT chunk has padding at the end # Set MAXBLOCK to the length of the actual data # so that the decoder finishes reading before the chunk ends - MAXBLOCK = ImageFile.MAXBLOCK - ImageFile.MAXBLOCK = 45 - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "MAXBLOCK", 45) + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) with Image.open("Tests/images/padded_idat.png") as im: im.load() - ImageFile.MAXBLOCK = MAXBLOCK - ImageFile.LOAD_TRUNCATED_IMAGES = False - assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") @pytest.mark.parametrize( diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index ab8a7f9ecca..a5da76faa0b 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from PIL import Image, ImageDraw, ImageFont, _util from .helper import PillowLeakTestCase, features, skip_unless_feature @@ -7,11 +9,7 @@ original_core = ImageFont.core -class TestTTypeFontLeak(PillowLeakTestCase): - # fails at iteration 3 in main - iterations = 10 - mem_limit = 4096 # k - +class TestFontLeak(PillowLeakTestCase): def _test_font(self, font: ImageFont.FreeTypeFont | ImageFont.ImageFont) -> None: im = Image.new("RGB", (255, 255), "white") draw = ImageDraw.ImageDraw(im) @@ -21,23 +19,29 @@ def _test_font(self, font: ImageFont.FreeTypeFont | ImageFont.ImageFont) -> None ) ) + +class TestTTypeFontLeak(TestFontLeak): + # fails at iteration 3 in main + iterations = 10 + mem_limit = 4096 # k + @skip_unless_feature("freetype2") def test_leak(self) -> None: ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) self._test_font(ttype) -class TestDefaultFontLeak(TestTTypeFontLeak): +class TestDefaultFontLeak(TestFontLeak): # fails at iteration 37 in main iterations = 100 mem_limit = 1024 # k - def test_leak(self) -> None: + def test_leak(self, monkeypatch: pytest.MonkeyPatch) -> None: if features.check_module("freetype2"): - ImageFont.core = _util.DeferredError(ImportError("Disabled for testing")) - try: - default_font = ImageFont.load_default() - finally: - ImageFont.core = original_core - + monkeypatch.setattr( + ImageFont, + "core", + _util.DeferredError(ImportError("Disabled for testing")), + ) + default_font = ImageFont.load_default() self._test_font(default_font) diff --git a/Tests/test_image.py b/Tests/test_image.py index c064939e81a..32c79919595 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -456,9 +456,11 @@ def test_register_open_duplicates(self) -> None: # Assert assert len(Image.ID) == id_length - def test_registered_extensions_uninitialized(self) -> None: + def test_registered_extensions_uninitialized( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: # Arrange - Image._initialized = 0 + monkeypatch.setattr(Image, "_initialized", 0) # Act Image.registered_extensions() diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 1eae053837e..3bcb7b90178 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1485,21 +1485,15 @@ def test_stroke_multiline() -> None: @skip_unless_feature("freetype2") -def test_setting_default_font() -> None: - # Arrange +def test_setting_default_font(monkeypatch: pytest.MonkeyPatch) -> None: im = Image.new("RGB", (100, 250)) draw = ImageDraw.Draw(im) - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) - - # Act - ImageDraw.ImageDraw.font = font + assert isinstance(draw.getfont(), ImageFont.load_default().__class__) - # Assert - try: - assert draw.getfont() == font - finally: - ImageDraw.ImageDraw.font = None - assert isinstance(draw.getfont(), ImageFont.load_default().__class__) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + monkeypatch.setattr(ImageDraw.ImageDraw, "font", font) + assert draw.getfont() == font def test_default_font_size() -> None: diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 7dfb3abf986..8a0abbd39b1 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -31,7 +31,7 @@ class TestImageFile: - def test_parser(self) -> None: + def test_parser(self, monkeypatch: pytest.MonkeyPatch) -> None: def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): @@ -55,12 +55,9 @@ def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: assert_image_equal(*roundtrip("IM")) assert_image_equal(*roundtrip("MSP")) if features.check("zlib"): - try: - # force multiple blocks in PNG driver - ImageFile.MAXBLOCK = 8192 - assert_image_equal(*roundtrip("PNG")) - finally: - ImageFile.MAXBLOCK = MAXBLOCK + # force multiple blocks in PNG driver + monkeypatch.setattr(ImageFile, "MAXBLOCK", 8192) + assert_image_equal(*roundtrip("PNG")) assert_image_equal(*roundtrip("PPM")) assert_image_equal(*roundtrip("TIFF")) assert_image_equal(*roundtrip("XBM")) @@ -120,14 +117,11 @@ def test_incremental_webp(self) -> None: assert (128, 128) == p.image.size @skip_unless_feature("zlib") - def test_safeblock(self) -> None: + def test_safeblock(self, monkeypatch: pytest.MonkeyPatch) -> None: im1 = hopper() - try: - ImageFile.SAFEBLOCK = 1 - im2 = fromstring(tostring(im1, "PNG")) - finally: - ImageFile.SAFEBLOCK = SAFEBLOCK + monkeypatch.setattr(ImageFile, "SAFEBLOCK", 1) + im2 = fromstring(tostring(im1, "PNG")) assert_image_equal(im1, im2) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 8c1cb3f5860..883df051d1e 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -38,20 +38,18 @@ def test_invalid_mode() -> None: font._load_pilfont_data(fp, im) -def test_without_freetype() -> None: - original_core = ImageFont.core +def test_without_freetype(monkeypatch: pytest.MonkeyPatch) -> None: if features.check_module("freetype2"): - ImageFont.core = _util.DeferredError(ImportError("Disabled for testing")) - try: - with pytest.raises(ImportError): - ImageFont.truetype("Tests/fonts/FreeMono.ttf") + monkeypatch.setattr( + ImageFont, "core", _util.DeferredError(ImportError("Disabled for testing")) + ) + with pytest.raises(ImportError): + ImageFont.truetype("Tests/fonts/FreeMono.ttf") - assert isinstance(ImageFont.load_default(), ImageFont.ImageFont) + assert isinstance(ImageFont.load_default(), ImageFont.ImageFont) - with pytest.raises(ImportError): - ImageFont.load_default(size=14) - finally: - ImageFont.core = original_core + with pytest.raises(ImportError): + ImageFont.load_default(size=14) @pytest.mark.parametrize("font", fonts) diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 78f5632c5d8..e5c6f7d85cc 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -5,6 +5,8 @@ from io import BytesIO from pathlib import Path +import pytest + from PIL import Image, PSDraw @@ -47,21 +49,16 @@ def test_draw_postscript(tmp_path: Path) -> None: assert os.path.getsize(tempfile) > 0 -def test_stdout() -> None: +def test_stdout(monkeypatch: pytest.MonkeyPatch) -> None: # Temporarily redirect stdout - old_stdout = sys.stdout - class MyStdOut: buffer = BytesIO() mystdout = MyStdOut() - sys.stdout = mystdout + monkeypatch.setattr(sys, "stdout", mystdout) ps = PSDraw.PSDraw() _create_document(ps) - # Reset stdout - sys.stdout = old_stdout - assert mystdout.buffer.getvalue() != b"" From f86ad8b36d75417f872723eba1c3a2ffc19c9c70 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 29 Jan 2026 23:26:20 +0100 Subject: [PATCH 2247/2374] Patch libavif for svt-av1 4.0 compatibility --- depends/install_libavif.sh | 4 ++++ depends/libavif-svt4.patch | 14 ++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 depends/libavif-svt4.patch diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh index 50ba0175567..a6686f3ef3a 100755 --- a/depends/install_libavif.sh +++ b/depends/install_libavif.sh @@ -7,6 +7,10 @@ version=1.3.0 pushd libavif-$version +# Apply patch for SVT-AV1 4.0 compatibility +# Pending release of https://github.com/AOMediaCodec/libavif/pull/2971 +patch -p1 < ../libavif-svt4.patch + if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then PREFIX=$(brew --prefix) else diff --git a/depends/libavif-svt4.patch b/depends/libavif-svt4.patch new file mode 100644 index 00000000000..7abfc529970 --- /dev/null +++ b/depends/libavif-svt4.patch @@ -0,0 +1,14 @@ +--- a/src/codec_svt.c ++++ b/src/codec_svt.c +@@ -162,7 +162,11 @@ static avifResult svtCodecEncodeImage(avifEncoder * encoder, + #else + svt_config->logical_processors = encoder->maxThreads; + #endif ++#if SVT_AV1_CHECK_VERSION(4, 0, 0) ++ svt_config->aq_mode = 2; ++#else + svt_config->enable_adaptive_quantization = 2; ++#endif + // disable 2-pass + #if SVT_AV1_CHECK_VERSION(0, 9, 0) + svt_config->rc_stats_buffer = (SvtAv1FixedBuf) { NULL, 0 }; From 799564dd52a306bbe3f90aa080825e5f5ffe9fc0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Jan 2026 20:22:29 +1100 Subject: [PATCH 2248/2374] Always call StubHandler open() when opening StubImageFile --- src/PIL/BufrStubImagePlugin.py | 4 ---- src/PIL/GribStubImagePlugin.py | 4 ---- src/PIL/Hdf5StubImagePlugin.py | 4 ---- src/PIL/ImageFile.py | 4 ++++ src/PIL/WmfImagePlugin.py | 4 ---- 5 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 264564d2bbb..d82c4c746c3 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -52,10 +52,6 @@ def _open(self) -> None: self._mode = "F" self._size = 1, 1 - loader = self._load() - if loader: - loader.open(self) - def _load(self) -> ImageFile.StubHandler | None: return _handler diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 146a6fa0df0..3784ef2f134 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -52,10 +52,6 @@ def _open(self) -> None: self._mode = "F" self._size = 1, 1 - loader = self._load() - if loader: - loader.open(self) - def _load(self) -> ImageFile.StubHandler | None: return _handler diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index 1523e95d58c..1a56660f7bd 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -52,10 +52,6 @@ def _open(self) -> None: self._mode = "F" self._size = 1, 1 - loader = self._load() - if loader: - loader.open(self) - def _load(self) -> ImageFile.StubHandler | None: return _handler diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 78abe3c77be..45df3be0363 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -148,6 +148,10 @@ def __init__( try: try: self._open() + + if isinstance(self, StubImageFile): + if loader := self._load(): + loader.open(self) except ( IndexError, # end of data TypeError, # end of data (ord) diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 3ae86242a8b..79d54df4ed7 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -148,10 +148,6 @@ def _open(self) -> None: self._mode = "RGB" self._size = size - loader = self._load() - if loader: - loader.open(self) - def _load(self) -> ImageFile.StubHandler | None: return _handler From fc4dbc3810f4791d59dd9ea5c4d3e3a26de58c2f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 31 Jan 2026 02:41:44 +1100 Subject: [PATCH 2249/2374] Remove unnecessary code in `WmfHandler` (#9411) Co-authored-by: Andrew Murray --- src/PIL/WmfImagePlugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 3ae86242a8b..a85c62a9342 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -45,7 +45,6 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: class WmfHandler(ImageFile.StubHandler): def open(self, im: ImageFile.StubImageFile) -> None: - im._mode = "RGB" self.bbox = im.info["wmf_bbox"] def load(self, im: ImageFile.StubImageFile) -> Image.Image: From 27924be4fd326246f44d7d7f2100455f9954c501 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:17:12 +0000 Subject: [PATCH 2250/2374] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.10 → v0.14.14](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.10...v0.14.14) - [github.com/psf/black-pre-commit-mirror: 25.12.0 → 26.1.0](https://github.com/psf/black-pre-commit-mirror/compare/25.12.0...26.1.0) - [github.com/PyCQA/bandit: 1.9.2 → 1.9.3](https://github.com/PyCQA/bandit/compare/1.9.2...1.9.3) - [github.com/Lucas-C/pre-commit-hooks: v1.5.5 → v1.5.6](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.5.5...v1.5.6) - [github.com/python-jsonschema/check-jsonschema: 0.36.0 → 0.36.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.36.0...0.36.1) - [github.com/zizmorcore/zizmor-pre-commit: v1.19.0 → v1.22.0](https://github.com/zizmorcore/zizmor-pre-commit/compare/v1.19.0...v1.22.0) - [github.com/tox-dev/pyproject-fmt: v2.11.1 → v2.12.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.11.1...v2.12.1) --- .pre-commit-config.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 12b3d4b4a3d..7eb69d1642c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,24 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.10 + rev: v0.14.14 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 25.12.0 + rev: 26.1.0 hooks: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.9.2 + rev: 1.9.3 hooks: - id: bandit args: [--severity-level=high] files: ^src/ - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.5 + rev: v1.5.6 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) @@ -51,14 +51,14 @@ repos: exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.36.0 + rev: 0.36.1 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.19.0 + rev: v1.22.0 hooks: - id: zizmor @@ -68,7 +68,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.11.1 + rev: v2.12.1 hooks: - id: pyproject-fmt From 7cbe8c4924f34fb3d17be8955ddc384cbb68ab16 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:17:52 +0000 Subject: [PATCH 2251/2374] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_arro3.py | 4 ++-- Tests/test_arrow.py | 8 ++++---- Tests/test_file_gif.py | 2 +- Tests/test_file_jpeg.py | 18 ++++-------------- Tests/test_font_pcf.py | 2 +- Tests/test_font_pcf_charsets.py | 2 +- Tests/test_format_hsv.py | 4 ++-- Tests/test_image_access.py | 6 ++---- Tests/test_image_transform.py | 14 +++++++------- Tests/test_imagemorph.py | 6 ++---- Tests/test_imagewin_pointers.py | 2 +- Tests/test_nanoarrow.py | 4 ++-- Tests/test_pyarrow.py | 4 ++-- pyproject.toml | 16 ++++++++-------- src/PIL/BdfFontFile.py | 1 + src/PIL/EpsImagePlugin.py | 2 +- src/PIL/ExifTags.py | 1 + src/PIL/GdImageFile.py | 1 + src/PIL/GimpGradientFile.py | 1 + src/PIL/IcnsImagePlugin.py | 6 +++--- src/PIL/Image.py | 4 ++-- src/PIL/ImageDraw2.py | 3 ++- src/PIL/ImageFile.py | 4 ++-- src/PIL/ImageFont.py | 26 ++++++-------------------- src/PIL/MspImagePlugin.py | 2 +- src/PIL/WalImageFile.py | 1 + src/PIL/_binary.py | 1 + 27 files changed, 62 insertions(+), 83 deletions(-) diff --git a/Tests/test_arro3.py b/Tests/test_arro3.py index 672eedc9ba4..42d032f7660 100644 --- a/Tests/test_arro3.py +++ b/Tests/test_arro3.py @@ -213,7 +213,7 @@ class DataShape(NamedTuple): ), ) def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: - (dtype, elt, elts_per_pixel) = data_tp + dtype, elt, elts_per_pixel = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] if dtype == fl_uint8_4_type: @@ -239,7 +239,7 @@ def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> Non ) @pytest.mark.parametrize("data_tp", (UINT32, INT32)) def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None: - (dtype, elt, elts_per_pixel) = data_tp + dtype, elt, elts_per_pixel = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype) diff --git a/Tests/test_arrow.py b/Tests/test_arrow.py index b86c77b9aa8..9f84a75a472 100644 --- a/Tests/test_arrow.py +++ b/Tests/test_arrow.py @@ -68,7 +68,7 @@ def test_multiblock_l_image() -> None: img = Image.new("L", size, 128) with pytest.raises(ValueError): - (schema, arr) = img.__arrow_c_array__() + schema, arr = img.__arrow_c_array__() def test_multiblock_rgba_image() -> None: @@ -79,7 +79,7 @@ def test_multiblock_rgba_image() -> None: img = Image.new("RGBA", size, (128, 127, 126, 125)) with pytest.raises(ValueError): - (schema, arr) = img.__arrow_c_array__() + schema, arr = img.__arrow_c_array__() def test_multiblock_l_schema() -> None: @@ -114,7 +114,7 @@ def test_singleblock_l_image() -> None: img = Image.new("L", size, 128) assert img.im.isblock() - (schema, arr) = img.__arrow_c_array__() + schema, arr = img.__arrow_c_array__() assert schema assert arr @@ -130,7 +130,7 @@ def test_singleblock_rgba_image() -> None: img = Image.new("RGBA", size, (128, 127, 126, 125)) assert img.im.isblock() - (schema, arr) = img.__arrow_c_array__() + schema, arr = img.__arrow_c_array__() assert schema assert arr Image.core.set_use_block_allocator(0) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 2615f5a6028..7924af99f74 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1433,7 +1433,7 @@ def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None: # with open('Tests/images/gif_header_data.pkl', 'wb') as f: # pickle.dump((h, d), f, 1) with open("Tests/images/gif_header_data.pkl", "rb") as f: - (h_target, d_target) = pickle.load(f) + h_target, d_target = pickle.load(f) assert h == h_target assert d == d_target diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index f818927f6a3..f4c8318a926 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -590,9 +590,7 @@ def _n_qtables_helper(n: int, test_file: str) -> None: assert im2.quantization == {0: bounds_qtable} # values from wizard.txt in jpeg9-a src package. - standard_l_qtable = [ - int(s) - for s in """ + standard_l_qtable = [int(s) for s in """ 16 11 10 16 24 40 51 61 12 12 14 19 26 58 60 55 14 13 16 24 40 57 69 56 @@ -601,14 +599,9 @@ def _n_qtables_helper(n: int, test_file: str) -> None: 24 35 55 64 81 104 113 92 49 64 78 87 103 121 120 101 72 92 95 98 112 100 103 99 - """.split( - None - ) - ] + """.split(None)] - standard_chrominance_qtable = [ - int(s) - for s in """ + standard_chrominance_qtable = [int(s) for s in """ 17 18 24 47 99 99 99 99 18 21 26 66 99 99 99 99 24 26 56 99 99 99 99 99 @@ -617,10 +610,7 @@ def _n_qtables_helper(n: int, test_file: str) -> None: 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 - """.split( - None - ) - ] + """.split(None)] for quality in range(101): qtable_from_qtable_quality = self.roundtrip( diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 567ddaf13a0..1be7a4d1e39 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -80,7 +80,7 @@ def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) for i in range(255): - (ox, oy, dx, dy) = font.getbbox(chr(i)) + ox, oy, dx, dy = font.getbbox(chr(i)) assert ox == 0 assert oy == 0 assert dy == 20 diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 895458d9d82..f1ade907b0e 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -95,7 +95,7 @@ def test_textsize( tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) for i in range(255): - (ox, oy, dx, dy) = font.getbbox(bytearray([i])) + ox, oy, dx, dy = font.getbbox(bytearray([i])) assert ox == 0 assert oy == 0 assert dy == 20 diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 861eccc1170..2aeff5c34c3 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -29,7 +29,7 @@ def linear_gradient() -> Image.Image: im = Image.linear_gradient(mode="L") im90 = im.rotate(90) - (px, h) = im.size + px, h = im.size r = Image.new("L", (px * 3, h)) g = r.copy() @@ -54,7 +54,7 @@ def to_xxx_colorsys( ) -> Image.Image: # convert the hard way using the library colorsys routines. - (r, g, b) = im.split() + r, g, b = im.split() conv_func = int_to_float diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 07c12594a8c..6470ac9fc6f 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -278,8 +278,7 @@ def test_embeddable(self) -> None: with open("embed_pil.c", "w", encoding="utf-8") as fh: home = sys.prefix.replace("\\", "\\\\") - fh.write( - f""" + fh.write(f""" #include "Python.h" int main(int argc, char* argv[]) @@ -300,8 +299,7 @@ def test_embeddable(self) -> None: return 0; }} - """ - ) + """) objects = compiler.compile(["embed_pil.c"]) compiler.link_executable(objects, "embed_pil") diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 3e2b9fee8ed..12a05ec18b1 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -56,7 +56,7 @@ def test_palette(self) -> None: def test_extent(self) -> None: im = hopper("RGB") - (w, h) = im.size + w, h = im.size transformed = im.transform( im.size, Image.Transform.EXTENT, @@ -72,7 +72,7 @@ def test_extent(self) -> None: def test_quad(self) -> None: # one simple quad transform, equivalent to scale & crop upper left quad im = hopper("RGB") - (w, h) = im.size + w, h = im.size transformed = im.transform( im.size, Image.Transform.QUAD, @@ -99,7 +99,7 @@ def test_quad(self) -> None: ) def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None: im = hopper(mode) - (w, h) = im.size + w, h = im.size transformed = im.transform( im.size, Image.Transform.EXTENT, @@ -112,7 +112,7 @@ def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None: def test_mesh(self) -> None: # this should be a checkerboard of halfsized hoppers in ul, lr im = hopper("RGBA") - (w, h) = im.size + w, h = im.size transformed = im.transform( im.size, Image.Transform.MESH, @@ -174,7 +174,7 @@ def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: def test_alpha_premult_transform(self) -> None: def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: - (w, h) = im.size + w, h = im.size return im.transform( sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR ) @@ -216,7 +216,7 @@ def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: @pytest.mark.parametrize("mode", ("RGBA", "LA")) def test_nearest_transform(self, mode: str) -> None: def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: - (w, h) = im.size + w, h = im.size return im.transform( sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST ) @@ -255,7 +255,7 @@ def test_missing_method_data(self) -> None: @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None: with hopper() as im: - (w, h) = im.size + w, h = im.size with pytest.raises(ValueError): im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) # type: ignore[arg-type] diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index aa8356b0b33..1d2fae1a6fa 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -22,8 +22,7 @@ def string_to_img(image_string: str) -> Image.Image: return im -A = string_to_img( - """ +A = string_to_img(""" ....... ....... ..111.. @@ -31,8 +30,7 @@ def string_to_img(image_string: str) -> Image.Image: ..111.. ....... ....... - """ -) + """) def img_to_string(im: Image.Image) -> str: diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index e8468e59f54..b7421051384 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -87,7 +87,7 @@ def serialize_dib(bi: BITMAPINFOHEADER, pixels: ctypes.c_void_p) -> bytearray: def test_pointer(tmp_path: Path) -> None: im = hopper() - (width, height) = im.size + width, height = im.size opath = tmp_path / "temp.png" imdib = ImageWin.Dib(im) diff --git a/Tests/test_nanoarrow.py b/Tests/test_nanoarrow.py index 69980e71909..047be16c5f5 100644 --- a/Tests/test_nanoarrow.py +++ b/Tests/test_nanoarrow.py @@ -208,7 +208,7 @@ class DataShape(NamedTuple): ), ) def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: - (dtype, elt, elts_per_pixel) = data_tp + dtype, elt, elts_per_pixel = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] if dtype == fl_uint8_4_type: @@ -241,7 +241,7 @@ def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> Non ) @pytest.mark.parametrize("data_tp", (UINT32, INT32)) def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None: - (dtype, elt, elts_per_pixel) = data_tp + dtype, elt, elts_per_pixel = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] arr = nanoarrow.Array( diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index a69504e78a0..7a161f2ac12 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -211,7 +211,7 @@ class DataShape(NamedTuple): ), ) def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: - (dtype, elt, elts_per_pixel) = data_tp + dtype, elt, elts_per_pixel = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype) @@ -238,7 +238,7 @@ def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> Non ), ) def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: - (dtype, elt, elts_per_pixel) = data_tp + dtype, elt, elts_per_pixel = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype) diff --git a/pyproject.toml b/pyproject.toml index cc616bc547c..91f4750e46a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,14 +112,6 @@ test-requires = [ ] xbuild-tools = [ ] -[tool.cibuildwheel.macos] -# Disable platform guessing on macOS to avoid picking up Homebrew etc. -config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" - -[tool.cibuildwheel.macos.environment] -# Isolate macOS build environment from Homebrew etc. -PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" - [tool.cibuildwheel.ios] # Disable platform guessing on iOS, and disable raqm (since there won't be a # vendor version, and we can't distribute it due to licensing) @@ -139,6 +131,14 @@ test-command = [ # There's no numpy wheel for iOS (yet...) test-requires = [ ] +[tool.cibuildwheel.macos] +# Disable platform guessing on macOS to avoid picking up Homebrew etc. +config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" + +[tool.cibuildwheel.macos.environment] +# Isolate macOS build environment from Homebrew etc. +PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" + [[tool.cibuildwheel.overrides]] # iOS environment is isolated by cibuildwheel, but needs the dependencies select = "*_iphoneos" diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index f175e2f4f80..1c8c28ff038 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -20,6 +20,7 @@ """ Parse X Bitmap Distribution Format (BDF) """ + from __future__ import annotations from typing import BinaryIO diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 2effb816cfb..aeb7b0c93b3 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -190,7 +190,7 @@ class EpsImageFile(ImageFile.ImageFile): def _open(self) -> None: assert self.fp is not None - (length, offset) = self._find_offset(self.fp) + length, offset = self._find_offset(self.fp) # go to offset - start of "%!PS" self.fp.seek(offset) diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 2280d5ce84b..c1c05cdba71 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -13,6 +13,7 @@ This module provides constants and clear-text names for various well-known EXIF tags. """ + from __future__ import annotations from enum import IntEnum diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 891225ce2fd..d73bc1982f8 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -25,6 +25,7 @@ class is not registered for use with :py:func:`PIL.Image.open()`. To open a implementation is provided for convenience and demonstrational purposes only. """ + from __future__ import annotations from typing import IO diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index 5f2691882c4..fb958721882 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -18,6 +18,7 @@ the corresponding code in GIMP, written by Federico Mena Quintero. See the GIMP distribution for more information.) """ + from __future__ import annotations from math import log, pi, sin, sqrt diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 058861d67e5..023835fb71e 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -42,7 +42,7 @@ def read_32t( fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] ) -> dict[str, Image.Image]: # The 128x128 icon seems to have an extra header for some reason. - (start, length) = start_length + start, length = start_length fobj.seek(start) sig = fobj.read(4) if sig != b"\x00\x00\x00\x00": @@ -58,7 +58,7 @@ def read_32( Read a 32bit RGB icon resource. Seems to be either uncompressed or an RLE packbits-like scheme. """ - (start, length) = start_length + start, length = start_length fobj.seek(start) pixel_size = (size[0] * size[2], size[1] * size[2]) sizesq = pixel_size[0] * pixel_size[1] @@ -111,7 +111,7 @@ def read_mk( def read_png_or_jpeg2000( fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] ) -> dict[str, Image.Image]: - (start, length) = start_length + start, length = start_length fobj.seek(start) sig = fobj.read(12) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index fdcc680b900..cc431a86a5d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2546,7 +2546,7 @@ def rotate( ] def transform(x: float, y: float, matrix: list[float]) -> tuple[float, float]: - (a, b, c, d, e, f) = matrix + a, b, c, d, e, f = matrix return a * x + b * y + c, d * x + e * y + f matrix[2], matrix[5] = transform( @@ -3489,7 +3489,7 @@ def fromarrow( msg = "arrow_c_array interface not found" raise ValueError(msg) - (schema_capsule, array_capsule) = obj.__arrow_c_array__() + schema_capsule, array_capsule = obj.__arrow_c_array__() _im = core.new_arrow(mode, size, schema_capsule, array_capsule) if _im: return Image()._new(_im) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 3d68658ed5b..2c9e39b2c41 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -22,6 +22,7 @@ .. seealso:: :py:mod:`PIL.ImageDraw` """ + from __future__ import annotations from typing import Any, AnyStr, BinaryIO @@ -117,7 +118,7 @@ def render( def settransform(self, offset: tuple[float, float]) -> None: """Sets a transformation offset.""" - (xoffset, yoffset) = offset + xoffset, yoffset = offset self.transform = (1, 0, xoffset, 0, 1, yoffset) def arc( diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 78abe3c77be..34143543733 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -801,9 +801,9 @@ def setimage( self.im = im if extents: - (x0, y0, x1, y1) = extents + x0, y0, x1, y1 = extents else: - (x0, y0, x1, y1) = (0, 0, 0, 0) + x0, y0, x1, y1 = (0, 0, 0, 0) if x0 == 0 and x1 == 0: self.state.xsize, self.state.ysize = self.im.size diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index d11f7bf01ad..ae003d139c9 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -940,9 +940,7 @@ def load_default_imagefont() -> ImageFont: f = ImageFont() f._load_pilfont_data( # courB08 - BytesIO( - base64.b64decode( - b""" + BytesIO(base64.b64decode(b""" UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA @@ -1034,13 +1032,8 @@ def load_default_imagefont() -> ImageFont: pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA//// +QAGAAIAzgAKANUAEw== -""" - ) - ), - Image.open( - BytesIO( - base64.b64decode( - b""" +""")), + Image.open(BytesIO(base64.b64decode(b""" iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9 M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g @@ -1064,10 +1057,7 @@ def load_default_imagefont() -> ImageFont: AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v// Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR w7IkEbzhVQAAAABJRU5ErkJggg== -""" - ) - ) - ), +"""))), ) return f @@ -1088,9 +1078,7 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: """ if isinstance(core, ModuleType) or size is not None: return truetype( - BytesIO( - base64.b64decode( - b""" + BytesIO(base64.b64decode(b""" AAEAAAAPAIAAAwBwRkZUTYwDlUAAADFoAAAAHEdERUYAqADnAAAo8AAAACRHUE9ThhmITwAAKfgAA AduR1NVQnHxefoAACkUAAAA4k9TLzJovoHLAAABeAAAAGBjbWFw5lFQMQAAA6gAAAGqZ2FzcP//AA MAACjoAAAACGdseWYmRXoPAAAGQAAAHfhoZWFkE18ayQAAAPwAAAA2aGhlYQboArEAAAE0AAAAJGh @@ -1311,9 +1299,7 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: gAWAAAAEQADAAoAFAAMAA0ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEgAGAAEAHgAkAC YAJwApACoALQAuAC8AMgAzADcAOAA5ADoAPAA9AEUASABOAE8AUgBTAFUAVwBZAFoAWwBcAF0AcwA AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ== -""" - ) - ), +""")), 10 if size is None else size, layout_engine=Layout.BASIC, ) diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 277087a8677..fa0f52fe8db 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -140,7 +140,7 @@ def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int runtype = row[idx] idx += 1 if runtype == 0: - (runcount, runval) = struct.unpack_from("Bc", row, idx) + runcount, runval = struct.unpack_from("Bc", row, idx) img.write(runval * runcount) idx += 2 else: diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index fb3e1c06a32..07bbf747155 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -22,6 +22,7 @@ is not registered for use with :py:func:`PIL.Image.open()`. To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead. """ + from __future__ import annotations from typing import IO diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 4594ccce361..d3236c17abb 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -13,6 +13,7 @@ """Binary input/output support routines.""" + from __future__ import annotations from struct import pack, unpack_from From 508e9c998429cf1a6989fc7af913d98e92c3d2a9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 01:32:07 +0000 Subject: [PATCH 2252/2374] Update dependency cibuildwheel to v3.3.1 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 485866de67c..6e869a5c2e9 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.3.0 +cibuildwheel==3.3.1 From 1ac7691fe597f4a1bc8ffe088d40c3a136d87831 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Feb 2026 20:39:31 +1100 Subject: [PATCH 2253/2374] Updated zlib-ng to 2.3.3 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index c17bb7765be..6dadeb775d6 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -103,7 +103,7 @@ XZ_VERSION=5.8.2 ZSTD_VERSION=1.5.7 TIFF_VERSION=4.7.1 LCMS2_VERSION=2.18 -ZLIB_NG_VERSION=2.3.2 +ZLIB_NG_VERSION=2.3.3 LIBWEBP_VERSION=1.6.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b9bfc0c6e17..e62a94cc40b 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -126,7 +126,7 @@ def cmd_msbuild( "OPENJPEG": "2.5.4", "TIFF": "4.7.1", "XZ": "5.8.2", - "ZLIBNG": "2.3.2", + "ZLIBNG": "2.3.3", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) From a90075a66803b30cd02c0607d47c0d912913b6aa Mon Sep 17 00:00:00 2001 From: Frank Henigman Date: Wed, 4 Feb 2026 20:52:52 -0500 Subject: [PATCH 2254/2374] Add FontFile.to_imagefont(). --- Tests/test_font_pcf.py | 10 ++++++++++ src/PIL/FontFile.py | 42 ++++++++++++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 1be7a4d1e39..5b8832977db 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -76,6 +76,16 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) +def test_to_imagefont(request: pytest.FixtureRequest, tmp_path: Path) -> None: + with open(fontname, "rb") as test_file: + pcffont = PcfFontFile.PcfFontFile(test_file) + imgfont = pcffont.to_imagefont() + im = Image.new("L", (130, 30), "white") + draw = ImageDraw.Draw(im) + draw.text((0, 0), message, "black", font=imgfont) + assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) + + def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 1e0c1c166b5..b50385daac6 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -15,10 +15,11 @@ # from __future__ import annotations +import io import os from typing import BinaryIO -from . import Image, _binary +from . import Image, ImageFont, _binary WIDTH = 800 @@ -123,12 +124,33 @@ def save(self, filename: str) -> None: # font metrics with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp: - fp.write(b"PILfont\n") - fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! - fp.write(b"DATA\n") - for id in range(256): - m = self.metrics[id] - if not m: - puti16(fp, (0,) * 10) - else: - puti16(fp, m[0] + m[1] + m[2]) + self.save_metrics(fp) + + def save_metrics(self, fp: BinaryIO) -> None: + """Save font metrics to a file-like object""" + fp.write(b"PILfont\n") + fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! + fp.write(b"DATA\n") + for id in range(256): + m = self.metrics[id] + if not m: + puti16(fp, (0,) * 10) + else: + puti16(fp, m[0] + m[1] + m[2]) + + def to_imagefont(self) -> ImageFont.ImageFont: + """Convert to ImageFont""" + + self.compile() + + # font data + if not self.bitmap: + msg = "No bitmap created" + raise ValueError(msg) + + buf = io.BytesIO() + self.save_metrics(buf) + buf.seek(0) + imgfont = ImageFont.ImageFont() + imgfont._load_pilfont_data(buf, self.bitmap) + return imgfont From 18cab114370184efee9dde50c19d71263d7c7ec5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Feb 2026 08:34:13 +1100 Subject: [PATCH 2255/2374] Use assert_image_equal* when similarity is zero --- Tests/test_file_gif.py | 4 ++-- Tests/test_file_libtiff.py | 2 +- Tests/test_file_wmf.py | 2 +- Tests/test_font_pcf.py | 5 ++--- Tests/test_font_pcf_charsets.py | 3 +-- Tests/test_uploader.py | 4 ++-- 6 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 7924af99f74..e3fcec49084 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -399,7 +399,7 @@ def test_save_netpbm_bmp_mode(tmp_path: Path) -> None: b = BytesIO() GifImagePlugin._save_netpbm(img_rgb, b, tempfile) with Image.open(tempfile) as reloaded: - assert_image_similar(img_rgb, reloaded.convert("RGB"), 0) + assert_image_equal(img_rgb, reloaded.convert("RGB")) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") @@ -411,7 +411,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None: b = BytesIO() GifImagePlugin._save_netpbm(img_l, b, tempfile) with Image.open(tempfile) as reloaded: - assert_image_similar(img_l, reloaded.convert("L"), 0) + assert_image_equal(img_l, reloaded.convert("L")) def test_seek() -> None: diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index c2336c05856..a71c65cac2c 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -738,7 +738,7 @@ def save_bytesio(compression: str | None = None) -> None: buffer_io.seek(0) with Image.open(buffer_io) as saved_im: - assert_image_similar(pilim, saved_im, 0) + assert_image_equal(pilim, saved_im) save_bytesio() save_bytesio("raw") diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 906080d15a5..56901f46b07 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -18,7 +18,7 @@ def test_load_raw() -> None: # Currently, support for WMF/EMF is Windows-only im.load() # Compare to reference rendering - assert_image_similar_tofile(im, "Tests/images/drawing_emf_ref.png", 0) + assert_image_equal_tofile(im, "Tests/images/drawing_emf_ref.png") # Test basic WMF open and rendering with Image.open("Tests/images/drawing.wmf") as im: diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 1be7a4d1e39..569c2e85bb4 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -10,7 +10,6 @@ from .helper import ( assert_image_equal_tofile, - assert_image_similar_tofile, skip_unless_feature, ) @@ -73,7 +72,7 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: im = Image.new("L", (130, 30), "white") draw = ImageDraw.Draw(im) draw.text((0, 0), message, "black", font=font) - assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) + assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png") def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: @@ -100,7 +99,7 @@ def _test_high_characters( im = Image.new("L", (750, 30), "white") draw = ImageDraw.Draw(im) draw.text((0, 0), message, "black", font=font) - assert_image_similar_tofile(im, "Tests/images/high_ascii_chars.png", 0) + assert_image_equal_tofile(im, "Tests/images/high_ascii_chars.png") def test_high_characters(request: pytest.FixtureRequest, tmp_path: Path) -> None: diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index f1ade907b0e..6ebaa35ffa8 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -10,7 +10,6 @@ from .helper import ( assert_image_equal_tofile, - assert_image_similar_tofile, skip_unless_feature, ) @@ -85,7 +84,7 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> draw = ImageDraw.Draw(im) message = charsets[encoding]["message"].encode(encoding) draw.text((0, 0), message, "black", font=font) - assert_image_similar_tofile(im, charsets[encoding]["image1"], 0) + assert_image_equal_tofile(im, charsets[encoding]["image1"]) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py index d55ceb4be17..0491a22a11c 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -1,6 +1,6 @@ from __future__ import annotations -from .helper import assert_image_equal, assert_image_similar, hopper +from .helper import assert_image_equal, hopper def check_upload_equal() -> None: @@ -12,4 +12,4 @@ def check_upload_equal() -> None: def check_upload_similar() -> None: result = hopper("P").convert("RGB") target = hopper("RGB") - assert_image_similar(result, target, 0) + assert_image_equal(result, target) From fd8fa7df798423eaeae07d8b29727b7de328c548 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Feb 2026 11:19:18 +1100 Subject: [PATCH 2256/2374] Simplified code --- src/PIL/FpxImagePlugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 297971234d8..0b06aac965f 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -141,7 +141,7 @@ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: size = i32(s, 4), i32(s, 8) # tilecount = i32(s, 12) - tilesize = i32(s, 16), i32(s, 20) + xtile, ytile = i32(s, 16), i32(s, 20) # channels = i32(s, 24) offset = i32(s, 28) length = i32(s, 32) @@ -156,7 +156,6 @@ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: x = y = 0 xsize, ysize = size - xtile, ytile = tilesize self.tile = [] for i in range(0, len(s), length): @@ -224,7 +223,7 @@ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: msg = "unknown/invalid compression" raise OSError(msg) - x = x + xtile + x += xtile if x >= xsize: x, y = 0, y + ytile if y >= ysize: From 657d6ea4b6247f0b23d428fc523a12b07a934f84 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:07:07 +0200 Subject: [PATCH 2257/2374] CI: Disable pip upgrade warning --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 103d915c047..3a206e26945 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,7 @@ concurrency: env: COVERAGE_CORE: sysmon FORCE_COLOR: 1 + PIP_DISABLE_PIP_VERSION_CHECK: 1 jobs: build: From 3e14bea593449d76c2b3e6e63aafa26e3c0aedcb Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 9 Feb 2026 22:18:01 +1100 Subject: [PATCH 2258/2374] Use `assert_image_equal_tofile` when similarity is zero --- Tests/test_font_pcf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index ce5b36413cf..01d56dfd3e5 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -82,7 +82,7 @@ def test_to_imagefont(request: pytest.FixtureRequest, tmp_path: Path) -> None: im = Image.new("L", (130, 30), "white") draw = ImageDraw.Draw(im) draw.text((0, 0), message, "black", font=imgfont) - assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) + assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png") def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: From 0604d6a2c9cd6ac0c86baeda436abed06638d374 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Feb 2026 21:12:31 +1100 Subject: [PATCH 2259/2374] Remove unused argument --- Tests/test_font_pcf.py | 6 +++--- src/PIL/FontFile.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 01d56dfd3e5..8ac63ea61ae 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -75,13 +75,13 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png") -def test_to_imagefont(request: pytest.FixtureRequest, tmp_path: Path) -> None: +def test_to_imagefont(tmp_path: Path) -> None: with open(fontname, "rb") as test_file: pcffont = PcfFontFile.PcfFontFile(test_file) - imgfont = pcffont.to_imagefont() + imagefont = pcffont.to_imagefont() im = Image.new("L", (130, 30), "white") draw = ImageDraw.Draw(im) - draw.text((0, 0), message, "black", font=imgfont) + draw.text((0, 0), message, "black", font=imagefont) assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png") diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index b50385daac6..71a08b05e71 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -151,6 +151,6 @@ def to_imagefont(self) -> ImageFont.ImageFont: buf = io.BytesIO() self.save_metrics(buf) buf.seek(0) - imgfont = ImageFont.ImageFont() - imgfont._load_pilfont_data(buf, self.bitmap) - return imgfont + imagefont = ImageFont.ImageFont() + imagefont._load_pilfont_data(buf, self.bitmap) + return imagefont From 612e3c24a4f38837a6d915fe8eac15a7d1eeca17 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Feb 2026 21:39:39 +1100 Subject: [PATCH 2260/2374] Remove temporary buffer --- src/PIL/FontFile.py | 39 +++++++++++++++++++++------------------ src/PIL/ImageFont.py | 3 +++ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 71a08b05e71..c0c64fe6842 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -15,7 +15,6 @@ # from __future__ import annotations -import io import os from typing import BinaryIO @@ -111,6 +110,22 @@ def compile(self) -> None: self.bitmap.paste(im.crop(src), s) self.metrics[i] = d, dst, s + def _encode_metrics(self) -> bytes: + values: tuple[int, ...] = () + for id in range(256): + m = self.metrics[id] + if m: + values += m[0] + m[1] + m[2] + else: + values += (0,) * 10 + + metrics = b"" + for v in values: + if v < 0: + v += 65536 + metrics += _binary.o16be(v) + return metrics + def save(self, filename: str) -> None: """Save font""" @@ -124,19 +139,10 @@ def save(self, filename: str) -> None: # font metrics with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp: - self.save_metrics(fp) - - def save_metrics(self, fp: BinaryIO) -> None: - """Save font metrics to a file-like object""" - fp.write(b"PILfont\n") - fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! - fp.write(b"DATA\n") - for id in range(256): - m = self.metrics[id] - if not m: - puti16(fp, (0,) * 10) - else: - puti16(fp, m[0] + m[1] + m[2]) + fp.write(b"PILfont\n") + fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! + fp.write(b"DATA\n") + fp.write(self._encode_metrics()) def to_imagefont(self) -> ImageFont.ImageFont: """Convert to ImageFont""" @@ -148,9 +154,6 @@ def to_imagefont(self) -> ImageFont.ImageFont: msg = "No bitmap created" raise ValueError(msg) - buf = io.BytesIO() - self.save_metrics(buf) - buf.seek(0) imagefont = ImageFont.ImageFont() - imagefont._load_pilfont_data(buf, self.bitmap) + imagefont._load(self.bitmap, self._encode_metrics()) return imagefont diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ae003d139c9..ea7f4dc5477 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -149,6 +149,9 @@ def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: # read PILfont metrics data = file.read(256 * 20) + self._load(image, data) + + def _load(self, image: Image.Image, data: bytes) -> None: image.load() self.font = Image.core.font(image.im, data) From 723e7648267b5205b349d3d5a482202e0372fed5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Feb 2026 21:27:22 +1100 Subject: [PATCH 2261/2374] Improved coverage --- Tests/test_fontfile.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py index 575dada86cb..1a9069fd892 100644 --- a/Tests/test_fontfile.py +++ b/Tests/test_fontfile.py @@ -1,5 +1,6 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path import pytest @@ -7,6 +8,15 @@ from PIL import FontFile, Image +def test_puti16() -> None: + fp = BytesIO() + FontFile.puti16(fp, (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) + assert fp.getvalue() == ( + b"\x00\x00\x00\x01\x00\x02\x00\x03\x00\x04" + b"\x00\x05\x00\x06\x00\x07\x00\x08\x00\t" + ) + + def test_compile() -> None: font = FontFile.FontFile() font.glyph[0] = ((0, 0), (0, 0, 0, 0), (0, 0, 0, 1), Image.new("L", (0, 0))) @@ -24,5 +34,11 @@ def test_save(tmp_path: Path) -> None: tempname = str(tmp_path / "temp.pil") font = FontFile.FontFile() - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="No bitmap created"): font.save(tempname) + + +def test_to_imagefont() -> None: + font = FontFile.FontFile() + with pytest.raises(ValueError, match="No bitmap created"): + font.to_imagefont() From 54ba4db542ad3c7b918812a4e2d69c27735a3199 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:24:50 +1100 Subject: [PATCH 2262/2374] Fix OOB Write with invalid tile extents (#9427) Co-authored-by: Eric Soroos --- Tests/images/psd-oob-write-x.psd | Bin 0 -> 1126 bytes Tests/images/psd-oob-write-y.psd | Bin 0 -> 1126 bytes Tests/images/psd-oob-write.psd | Bin 0 -> 37212 bytes Tests/test_file_psd.py | 17 +++++++++++++++++ Tests/test_imagefile.py | 7 +++++++ docs/releasenotes/12.1.1.rst | 24 ++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + src/decode.c | 3 ++- src/encode.c | 3 ++- 9 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 Tests/images/psd-oob-write-x.psd create mode 100644 Tests/images/psd-oob-write-y.psd create mode 100644 Tests/images/psd-oob-write.psd create mode 100644 docs/releasenotes/12.1.1.rst diff --git a/Tests/images/psd-oob-write-x.psd b/Tests/images/psd-oob-write-x.psd new file mode 100644 index 0000000000000000000000000000000000000000..86359f4cb7e826a69a8e69a4b85947498ec18923 GIT binary patch literal 1126 zcma)5J!lkB5dL=WC-F>3z$=1SY;juU8Wp`VZp09|z;cO@XbSh|ZgXUJ@7TRX4pIuX z0SkW`qZT&S+FIBOg5VE`wTL!~HWJqFz0GA0$%PEez3@QFhrkNP zAuvV#TGJPoazEr{TAAgkKpC9EmzOT6P~@#DuSJ zUAwN0ePiq~9H(A1?WlXnFzSMFu>5&1Gvi%VuLMjIY_sPYVGiO`^5AHhE<`36}Q zS#8*4Tt){zOv#6s0b?jxZ==?^v(ltY=s@91lKeUijNJuxx0B@W<0RRA0^~jeuY!!< z*#T<5Y2VIll}EtTZQ#Z0%x2vKUfuy_K6TB|l>Z~PO>MP+pU;5FHQ>ZspmZbc8-2o$ zryqb7_Nx8{c<>N7<1+X9h9@HB$WwFjZhIlIc)`9T z6kgJL@)8%N^RTLn0liQ+`%UH?s%V)+x7mhM3JvQ>aRNJcNX=y?XE}2!cN#o<;Pc=tauOPS24s{WiA%d1_AHZ7(DiFOZT@ z1~9EBFYiTZJFF^Wz(S#J_M6N(Qqe4Z1=LwlA5GSi`m##ox9j~=340;B^S{69u$O-T DuU)5R literal 0 HcmV?d00001 diff --git a/Tests/images/psd-oob-write.psd b/Tests/images/psd-oob-write.psd new file mode 100644 index 0000000000000000000000000000000000000000..65a4472cf263a94277952c06903709afb0c8213f GIT binary patch literal 37212 zcmeI!I|{-;5CG8e2f;Js6jo_XXCVk)LDH$<2|S2L%6V+#=3`?OM1sW|nCvc@* zMR_>JEc#fa;nZao?RL4W`O0t5&U7$GptyKKZoln@|5fB*pk1ildPmiYor z3jqQI2oNCfHv$oNL4W`O0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ t009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+0D&I~yZ}A8uQLDu literal 0 HcmV?d00001 diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 8f2ca58a662..8a2636dfe22 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -182,3 +182,20 @@ def test_layer_crashes(test_file: str) -> None: assert isinstance(im, PsdImagePlugin.PsdImageFile) with pytest.raises(SyntaxError): im.layers + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/psd-oob-write.psd", + "Tests/images/psd-oob-write-x.psd", + "Tests/images/psd-oob-write-y.psd", + ], +) +def test_bounds_crash(test_file: str) -> None: + with Image.open(test_file) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) + im.seek(im.n_frames) + + with pytest.raises(ValueError): + im.load() diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 8a0abbd39b1..6656ee506ca 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -163,6 +163,13 @@ def test_negative_offset(self) -> None: with pytest.raises(ValueError, match="Tile offset cannot be negative"): im.load() + @pytest.mark.parametrize("xy", ((-1, 0), (0, -1))) + def test_negative_tile_extents(self, xy: tuple[int, int]) -> None: + im = Image.new("1", (1, 1)) + fp = BytesIO() + with pytest.raises(SystemError, match="tile cannot extend outside image"): + ImageFile._save(im, fp, [ImageFile._Tile("raw", xy + (1, 1), 0, "1")]) + def test_no_format(self) -> None: buf = BytesIO(b"\x00" * 255) diff --git a/docs/releasenotes/12.1.1.rst b/docs/releasenotes/12.1.1.rst new file mode 100644 index 00000000000..9c119ceb8b4 --- /dev/null +++ b/docs/releasenotes/12.1.1.rst @@ -0,0 +1,24 @@ +12.1.1 +------ + +Security +======== + +:cve:`2021-25289`: Fix OOB write with invalid tile extents +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Check that tile extents do not use negative x or y offsets when decoding or encoding, +and raise an error if they do, rather than allowing an OOB write. + +An out-of-bounds write may be triggered when opening a specially crafted PSD image. +This only affects Pillow >= 10.3.0. Reported by +`Yarden Porat `__. + +Other changes +============= + +Patch libavif for svt-av1 4.0 compatibility +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A patch has been added to ``depends/install_libavif.sh``, to allow libavif 1.3.0 to be +compatible with the recently released svt-av1 4.0.0. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 4b25bb6a2d1..690be20729e 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -15,6 +15,7 @@ expected to be backported to earlier versions. :maxdepth: 2 versioning + 12.1.1 12.1.0 12.0.0 11.3.0 diff --git a/src/decode.c b/src/decode.c index 051623ed448..7ec461c0e2c 100644 --- a/src/decode.c +++ b/src/decode.c @@ -186,7 +186,8 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { state->ysize = y1 - y0; } - if (state->xsize <= 0 || state->xsize + state->xoff > (int)im->xsize || + if (state->xoff < 0 || state->xsize <= 0 || + state->xsize + state->xoff > (int)im->xsize || state->yoff < 0 || state->ysize <= 0 || state->ysize + state->yoff > (int)im->ysize) { PyErr_SetString(PyExc_ValueError, "tile cannot extend outside image"); return NULL; diff --git a/src/encode.c b/src/encode.c index 513309c8d7d..06e4a089380 100644 --- a/src/encode.c +++ b/src/encode.c @@ -254,7 +254,8 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { state->ysize = y1 - y0; } - if (state->xsize <= 0 || state->xsize + state->xoff > im->xsize || + if (state->xoff < 0 || state->xsize <= 0 || + state->xsize + state->xoff > im->xsize || state->yoff < 0 || state->ysize <= 0 || state->ysize + state->yoff > im->ysize) { PyErr_SetString(PyExc_SystemError, "tile cannot extend outside image"); return NULL; From a15f9c6121b236e1463b798131c07bd1ab08ffc6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:48:11 +0200 Subject: [PATCH 2263/2374] Fix CVE number (#9430) --- docs/releasenotes/12.1.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/12.1.1.rst b/docs/releasenotes/12.1.1.rst index 9c119ceb8b4..86083b4ad08 100644 --- a/docs/releasenotes/12.1.1.rst +++ b/docs/releasenotes/12.1.1.rst @@ -4,7 +4,7 @@ Security ======== -:cve:`2021-25289`: Fix OOB write with invalid tile extents +:cve:`2026-25990`: Fix OOB write with invalid tile extents ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Check that tile extents do not use negative x or y offsets when decoding or encoding, From 27765189c8d7edb7a17ea0dbefcc258e5ee7e427 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 11 Feb 2026 23:51:33 +1100 Subject: [PATCH 2264/2374] Updated macOS tested Pillow versions --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 0789b02b75a..7a8707b9a30 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -75,7 +75,7 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+=============================+==================+==============+ -| macOS 26 Tahoe | 3.10, 3.11, 3.12, 3.13, 3.14| 12.0.0 |arm | +| macOS 26 Tahoe | 3.10, 3.11, 3.12, 3.13, 3.14| 12.1.1 |arm | | +-----------------------------+------------------+ | | | 3.9 | 11.3.0 | | +----------------------------------+-----------------------------+------------------+--------------+ From 3795a1b916b690d1f23d9230c65718a75b9dfa26 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Feb 2026 19:58:36 +1100 Subject: [PATCH 2265/2374] Use uppercase format id --- src/PIL/PalmImagePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 15f71290816..232adf3d3bb 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -210,8 +210,8 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # # -------------------------------------------------------------------- -Image.register_save("Palm", _save) +Image.register_save("PALM", _save) -Image.register_extension("Palm", ".palm") +Image.register_extension("PALM", ".palm") -Image.register_mime("Palm", "image/palm") +Image.register_mime("PALM", "image/palm") From 657d0414f07d37ec8abecc02879154834bf7a009 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Feb 2026 21:51:01 +1100 Subject: [PATCH 2266/2374] Merge PFM into PPM --- docs/handbook/image-file-formats.rst | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 35ec99ece45..add42a3a0ef 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -828,16 +828,6 @@ PCX Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data. -PFM -^^^ - -.. versionadded:: 10.3.0 - -Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files -containing ``F`` data. - -Color (PF format) PFM files are not supported. - Opening ~~~~~~~ @@ -1081,13 +1071,18 @@ following parameters can also be set: PPM ^^^ -Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or -``RGB`` data. +Pillow reads and writes PBM, PGM, PPM, PNM and PFM files containing ``1``, ``L``, ``I``, +``RGB`` or ``F`` data. "Raw" (P4 to P6) formats can be read, and are used when writing. Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well. +Since Pillow 10.3.0, grayscale (Pf format) Portable FloatMap (PFM) files containing +``F`` data can be read and used when writing as well. + +Color (PF format) PFM files are not supported. + QOI ^^^ From 0ce21f98e7aab659e858475e728bb1cf98587f8a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Feb 2026 18:06:29 +1100 Subject: [PATCH 2267/2374] Updated documentation --- docs/reference/ImageFont.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index aac55fe6b05..d4d66988b0c 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -8,10 +8,14 @@ The :py:mod:`~PIL.ImageFont` module defines a class with the same name. Instance this class store bitmap fonts, and are used with the :py:meth:`PIL.ImageDraw.ImageDraw.text` method. -PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use -`pilfont.py `_ -from :pypi:`pillow-scripts` to convert BDF and -PCF font descriptors (X window font formats) to this format. +PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You +can use :py:meth:`~PIL.FontFile.FontFile.to_imagefont` to convert BDF and PCF font +descriptors (X window font formats) to this format:: + + from PIL import PcfFontFile + with open("Tests/fonts/10x20-ISO8859-1.pcf", "rb") as fp: + font = PcfFontFile.PcfFontFile(fp) + imagefont = font.to_imagefont() Starting with version 1.1.4, PIL can be configured to support TrueType and OpenType fonts (as well as other font formats supported by the FreeType From f71d74eec2300c42578632f6cb83d3bf3e9dbebb Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 13 Feb 2026 18:29:41 +1100 Subject: [PATCH 2268/2374] Use versionadded Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/handbook/image-file-formats.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index add42a3a0ef..a9fd764e613 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1076,10 +1076,12 @@ Pillow reads and writes PBM, PGM, PPM, PNM and PFM files containing ``1``, ``L`` "Raw" (P4 to P6) formats can be read, and are used when writing. -Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well. +.. versionadded:: 9.2.0 + "Plain" (P1 to P3) formats can be read. -Since Pillow 10.3.0, grayscale (Pf format) Portable FloatMap (PFM) files containing -``F`` data can be read and used when writing as well. +.. versionadded:: 10.3.0 + Grayscale (Pf format) Portable FloatMap (PFM) files containing + ``F`` data can be read and used when writing. Color (PF format) PFM files are not supported. From 2c00c6f80e0b268f1ecc4287553e4d96084d996a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:29:42 +0200 Subject: [PATCH 2269/2374] GHA: Cache libavif and webp builds for Ubuntu (#9437) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> Co-authored-by: Andrew Murray --- .ci/install.sh | 2 +- .github/workflows/docs.yml | 16 +++++ .github/workflows/test.yml | 18 +++++ depends/install_libavif.sh | 134 +++++++++++++++++++++---------------- depends/install_webp.sh | 28 ++++++-- 5 files changed, 137 insertions(+), 61 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index aeb5e65145d..9553eb8f442 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -53,7 +53,7 @@ pushd depends && ./install_imagequant.sh && popd pushd depends && sudo ./install_raqm.sh && popd # libavif -pushd depends && sudo ./install_libavif.sh && popd +pushd depends && ./install_libavif.sh && popd # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 44af3e3dfdb..857881c0102 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -48,6 +48,13 @@ jobs: - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libavif + uses: actions/cache@v5 + id: cache-libavif + with: + path: ~/cache-libavif + key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }} + - name: Cache libimagequant uses: actions/cache@v5 id: cache-libimagequant @@ -55,12 +62,21 @@ jobs: path: ~/cache-libimagequant key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Cache libwebp + uses: actions/cache@v5 + id: cache-libwebp + with: + path: ~/cache-libwebp + key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }} + - name: Install Linux dependencies run: | .ci/install.sh env: GHA_PYTHON_VERSION: "3.x" + GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }} GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} + GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }} - name: Build run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3a206e26945..471cab90e3d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,6 +91,14 @@ jobs: - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libavif + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@v5 + id: cache-libavif + with: + path: ~/cache-libavif + key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }} + - name: Cache libimagequant if: startsWith(matrix.os, 'ubuntu') uses: actions/cache@v5 @@ -99,13 +107,23 @@ jobs: path: ~/cache-libimagequant key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Cache libwebp + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@v5 + id: cache-libwebp + with: + path: ~/cache-libwebp + key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }} + - name: Install Linux dependencies if: startsWith(matrix.os, 'ubuntu') run: | .ci/install.sh env: GHA_PYTHON_VERSION: ${{ matrix.python-version }} + GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }} GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} + GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }} - name: Install macOS dependencies if: startsWith(matrix.os, 'macOS') diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh index a6686f3ef3a..0089bf2b5fd 100755 --- a/depends/install_libavif.sh +++ b/depends/install_libavif.sh @@ -3,66 +3,88 @@ set -eo pipefail version=1.3.0 -./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz +if [[ "$GHA_LIBAVIF_CACHE_HIT" == "true" ]]; then -pushd libavif-$version + LIBDIR=/usr/lib/x86_64-linux-gnu -# Apply patch for SVT-AV1 4.0 compatibility -# Pending release of https://github.com/AOMediaCodec/libavif/pull/2971 -patch -p1 < ../libavif-svt4.patch + # Copy cached files into place + sudo cp ~/cache-libavif/lib/* $LIBDIR/ + sudo cp -r ~/cache-libavif/include/avif /usr/include/ -if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then - PREFIX=$(brew --prefix) else - PREFIX=/usr -fi - -PKGCONFIG=${PKGCONFIG:-pkg-config} - -LIBAVIF_CMAKE_FLAGS=() -HAS_DECODER=0 -HAS_ENCODER=0 - -if $PKGCONFIG --exists aom; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM) - HAS_ENCODER=1 - HAS_DECODER=1 -fi - -if $PKGCONFIG --exists dav1d; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM) - HAS_DECODER=1 -fi -if $PKGCONFIG --exists libgav1; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM) - HAS_DECODER=1 -fi + ./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz + + pushd libavif-$version + + # Apply patch for SVT-AV1 4.0 compatibility + # Pending release of https://github.com/AOMediaCodec/libavif/pull/2971 + patch -p1 < ../libavif-svt4.patch + + if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then + PREFIX=$(brew --prefix) + else + PREFIX=/usr + fi + + PKGCONFIG=${PKGCONFIG:-pkg-config} + + LIBAVIF_CMAKE_FLAGS=() + HAS_DECODER=0 + HAS_ENCODER=0 + + if $PKGCONFIG --exists aom; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM) + HAS_ENCODER=1 + HAS_DECODER=1 + fi + + if $PKGCONFIG --exists dav1d; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM) + HAS_DECODER=1 + fi + + if $PKGCONFIG --exists libgav1; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM) + HAS_DECODER=1 + fi + + if $PKGCONFIG --exists rav1e; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM) + HAS_ENCODER=1 + fi + + if $PKGCONFIG --exists SvtAv1Enc; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM) + HAS_ENCODER=1 + fi + + if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL) + fi + + cmake \ + -DCMAKE_INSTALL_PREFIX=$PREFIX \ + -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_MACOSX_RPATH=OFF \ + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + "${LIBAVIF_CMAKE_FLAGS[@]}" \ + . + + sudo make install + + if [ -n "$GITHUB_ACTIONS" ] && [ "$(uname)" != "Darwin" ]; then + # Copy to cache + LIBDIR=/usr/lib/x86_64-linux-gnu + rm -rf ~/cache-libavif + mkdir -p ~/cache-libavif/lib + mkdir -p ~/cache-libavif/include + cp $LIBDIR/libavif.so* ~/cache-libavif/lib/ + cp -r /usr/include/avif ~/cache-libavif/include/ + fi + + popd -if $PKGCONFIG --exists rav1e; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM) - HAS_ENCODER=1 fi - -if $PKGCONFIG --exists SvtAv1Enc; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM) - HAS_ENCODER=1 -fi - -if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL) -fi - -cmake \ - -DCMAKE_INSTALL_PREFIX=$PREFIX \ - -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_MACOSX_RPATH=OFF \ - -DAVIF_LIBSHARPYUV=LOCAL \ - -DAVIF_LIBYUV=LOCAL \ - "${LIBAVIF_CMAKE_FLAGS[@]}" \ - . - -make install - -popd diff --git a/depends/install_webp.sh b/depends/install_webp.sh index d7f3cd2f5d0..c328fe2c86a 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -3,10 +3,30 @@ archive=libwebp-1.6.0 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz +if [[ "$GHA_LIBWEBP_CACHE_HIT" == "true" ]]; then -pushd $archive + # Copy cached files into place + sudo cp ~/cache-libwebp/lib/* /usr/lib/ + sudo cp -r ~/cache-libwebp/include/webp /usr/include/ -./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install +else -popd + ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz + + pushd $archive + + ./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install + + if [ -n "$GITHUB_ACTIONS" ]; then + # Copy to cache + rm -rf ~/cache-libwebp + mkdir -p ~/cache-libwebp/lib + mkdir -p ~/cache-libwebp/include + cp /usr/lib/libwebp*.so* /usr/lib/libwebp*.a ~/cache-libwebp/lib/ + cp /usr/lib/libsharpyuv.so* /usr/lib/libsharpyuv.a ~/cache-libwebp/lib/ + cp -r /usr/include/webp ~/cache-libwebp/include/ + fi + + popd + +fi From a5c9eba30a4d2463cc0e9de67ae886b318af1fd2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:57:27 +1100 Subject: [PATCH 2270/2374] Fix unexpected error when saving zero dimension images (#9391) Co-authored-by: Andrew Murray --- Tests/test_file_gif.py | 8 ++++++++ Tests/test_file_pcx.py | 8 ++++++++ Tests/test_file_spider.py | 8 ++++++++ src/PIL/GifImagePlugin.py | 8 +++++++- src/PIL/PcxImagePlugin.py | 4 ++++ src/PIL/SpiderImagePlugin.py | 2 +- 6 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index e3fcec49084..b52816fdc1a 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -310,6 +310,14 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None: assert reloaded.getpixel((0, 0)) == 255 +@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0))) +def test_save_zero(size: tuple[int, int]) -> None: + b = BytesIO() + im = Image.new("RGB", size) + with pytest.raises(SystemError): + im.save(b, "GIF") + + @pytest.mark.parametrize( "path, mode", ( diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 90740ab5761..509d93469e6 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -37,6 +37,14 @@ def test_sanity(tmp_path: Path) -> None: im.save(f) +@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0))) +def test_save_zero(size: tuple[int, int]) -> None: + b = io.BytesIO() + im = Image.new("1", size) + with pytest.raises(ValueError): + im.save(b, "PCX") + + def test_p_4_planes() -> None: with Image.open("Tests/images/p_4_planes.pcx") as im: assert im.getpixel((0, 0)) == 3 diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 03494523b61..903632cffb0 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -68,6 +68,14 @@ def test_save(tmp_path: Path) -> None: assert im2.format == "SPIDER" +@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0))) +def test_save_zero(size: tuple[int, int]) -> None: + b = BytesIO() + im = Image.new("1", size) + with pytest.raises(SystemError): + im.save(b, "SPIDER") + + def test_tempfile() -> None: # Arrange im = hopper() diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 76a0d4ab99f..390b3b374ab 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -937,7 +937,13 @@ def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None: :param info: encoderinfo :returns: list of indexes of palette entries in use, or None """ - if im.mode in ("P", "L") and info and info.get("optimize"): + if ( + im.mode in ("P", "L") + and info + and info.get("optimize") + and im.width != 0 + and im.height != 0 + ): # Potentially expensive operation. # The palette saves 3 bytes per color not used, but palette diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 6b16d538537..3e34e3c63ba 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -146,6 +146,10 @@ def _open(self) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.width == 0 or im.height == 0: + msg = "Cannot write empty image as PCX" + raise ValueError(msg) + try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 848dccda577..11d90699d10 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -244,7 +244,7 @@ def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | No def makeSpiderHeader(im: Image.Image) -> list[bytes]: nsam, nrow = im.size - lenbyt = nsam * 4 # There are labrec records in the header + lenbyt = max(1, nsam) * 4 # There are labrec records in the header labrec = int(1024 / lenbyt) if 1024 % lenbyt != 0: labrec += 1 From 3cd69cb12f10d18a58c94dc01b54c70deba19289 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 17 Feb 2026 18:57:29 +1100 Subject: [PATCH 2271/2374] Specify platform when pulling docker image (#9440) Co-authored-by: Andrew Murray --- .github/workflows/test-docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 091edb22264..8f24bef3d6a 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -83,7 +83,7 @@ jobs: - name: Docker pull run: | - docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + docker pull ${{ matrix.qemu-arch && format('--platform=linux/{0}', matrix.qemu-arch)}} pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} - name: Docker build run: | From 02764a0077d0c267fafd41e63c31beee1e5b6a7e Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:09:59 +1100 Subject: [PATCH 2272/2374] Correct error check when encoding AVIF images (#9442) Co-authored-by: Andrew Murray --- src/_avif.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_avif.c b/src/_avif.c index 3585297fe87..5e8b9fe8e93 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -485,7 +485,7 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) { frame = image; } else { frame = avifImageCreateEmpty(); - if (image == NULL) { + if (frame == NULL) { PyErr_SetString(PyExc_ValueError, "Image creation failed"); return NULL; } From 4777a0b31820f184c01d550fa526400dc9b53eaf Mon Sep 17 00:00:00 2001 From: Kadir Can Ozden <101993364+bysiber@users.noreply.github.com> Date: Sat, 21 Feb 2026 15:21:48 +0300 Subject: [PATCH 2273/2374] Fix BMP RLE delta escape reading from wrong file position (#9443) Co-authored-by: Andrew Murray --- Tests/images/pal8rletrns.png | Bin 0 -> 4089 bytes Tests/test_file_bmp.py | 5 +++++ src/PIL/BmpImagePlugin.py | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 Tests/images/pal8rletrns.png diff --git a/Tests/images/pal8rletrns.png b/Tests/images/pal8rletrns.png new file mode 100644 index 0000000000000000000000000000000000000000..2362266ef0287fd543aaae2b3769cbdf49386ce5 GIT binary patch literal 4089 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 I2Cqo{A2mRU6951J literal 0 HcmV?d00001 diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 28e863459a6..2e0394b3b2a 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -221,6 +221,11 @@ def test_rle8_eof(file_name: str, length: int) -> None: im.load() +def test_rle_delta() -> None: + with Image.open("Tests/images/bmp/q/pal8rletrns.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/pal8rletrns.png") + + def test_unsupported_bmp_bitfields_layout() -> None: fp = io.BytesIO( o32(40) # header size diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index a1227137017..5ee61b35b17 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -369,7 +369,7 @@ def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int bytes_read = self.fd.read(2) if len(bytes_read) < 2: break - right, up = self.fd.read(2) + right, up = bytes_read data += b"\x00" * (right + up * self.state.xsize) x = len(data) % self.state.xsize else: From 43c12af7301a77c461f23df3a1dcda25857bea5c Mon Sep 17 00:00:00 2001 From: Kadir Can Ozden <101993364+bysiber@users.noreply.github.com> Date: Sat, 21 Feb 2026 15:23:38 +0300 Subject: [PATCH 2274/2374] Fix `self.decode` typo (#9445) Co-authored-by: Andrew Murray --- src/PIL/ImageFile.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 34143543733..50e0075a29d 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -579,10 +579,7 @@ def feed(self, data: bytes) -> None: pass # not enough data else: flag = hasattr(im, "load_seek") or hasattr(im, "load_read") - if flag or len(im.tile) != 1: - # custom load code, or multiple tiles - self.decode = None - else: + if not flag and len(im.tile) == 1: # initialize decoder im.load_prepare() d, e, o, a = im.tile[0] From 81e0cf2bc4023a054f80a1b6de0e8634db5d19e5 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:17:59 +1100 Subject: [PATCH 2275/2374] Add check-case-conflict hook (#9446) --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7eb69d1642c..19be90c1610 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,6 +38,7 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: + - id: check-case-conflict - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable - id: check-merge-conflict From 2fe7c421486a1f10e1bae1d7abe08b873d5be098 Mon Sep 17 00:00:00 2001 From: Kadir Can Ozden <101993364+bysiber@users.noreply.github.com> Date: Tue, 24 Feb 2026 10:01:24 +0300 Subject: [PATCH 2276/2374] Only close file handle in ImagePalette.save() if it was opened internally (#9444) Co-authored-by: Andrew Murray --- Tests/test_imagepalette.py | 13 ++++++++++--- src/PIL/ImagePalette.py | 27 ++++++++++++++++----------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 10b89a2c0c2..526beb656e8 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,6 +1,6 @@ from __future__ import annotations -from io import BytesIO +import io from pathlib import Path import pytest @@ -23,6 +23,13 @@ def test_reload() -> None: assert_image_equal(im.convert("RGB"), original.convert("RGB")) +def test_save_fp() -> None: + palette = ImagePalette.ImagePalette() + with io.StringIO() as fp: + palette.save(fp) + assert not fp.closed + + def test_getcolor() -> None: palette = ImagePalette.ImagePalette() assert len(palette.palette) == 0 @@ -204,7 +211,7 @@ def test_2bit_palette(tmp_path: Path) -> None: def test_getpalette() -> None: - b = BytesIO(b"0 1\n1 2 3 4") + b = io.BytesIO(b"0 1\n1 2 3 4") p = PaletteFile.PaletteFile(b) palette, rawmode = p.getpalette() @@ -216,6 +223,6 @@ def test_invalid_palette() -> None: with pytest.raises(OSError): ImagePalette.load("Tests/images/hopper.jpg") - b = BytesIO(b"1" * 101) + b = io.BytesIO(b"1" * 101) with pytest.raises(SyntaxError, match="bad palette file"): PaletteFile.PaletteFile(b) diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index eae7aea8fc3..99ad2771b4b 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -191,19 +191,24 @@ def save(self, fp: str | IO[str]) -> None: if self.rawmode: msg = "palette contains raw palette data" raise ValueError(msg) + open_fp = False if isinstance(fp, str): fp = open(fp, "w") - fp.write("# Palette\n") - fp.write(f"# Mode: {self.mode}\n") - for i in range(256): - fp.write(f"{i}") - for j in range(i * len(self.mode), (i + 1) * len(self.mode)): - try: - fp.write(f" {self.palette[j]}") - except IndexError: - fp.write(" 0") - fp.write("\n") - fp.close() + open_fp = True + try: + fp.write("# Palette\n") + fp.write(f"# Mode: {self.mode}\n") + for i in range(256): + fp.write(f"{i}") + for j in range(i * len(self.mode), (i + 1) * len(self.mode)): + try: + fp.write(f" {self.palette[j]}") + except IndexError: + fp.write(" 0") + fp.write("\n") + finally: + if open_fp: + fp.close() # -------------------------------------------------------------------- From e96c5a5a5321a7a5541811b41940b89bd1a59f9e Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:17:12 +1100 Subject: [PATCH 2277/2374] Updated libpng to 1.6.55 (#9425) --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index a9b779e818c..0057f62d6b4 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -96,7 +96,7 @@ else FREETYPE_VERSION=2.14.1 fi HARFBUZZ_VERSION=12.3.2 -LIBPNG_VERSION=1.6.54 +LIBPNG_VERSION=1.6.55 JPEGTURBO_VERSION=3.1.3 OPENJPEG_VERSION=2.5.4 XZ_VERSION=5.8.2 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index bd9bd06b6fa..43c58d15fdf 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -121,7 +121,7 @@ def cmd_msbuild( "LCMS2": "2.18", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.4.1", - "LIBPNG": "1.6.54", + "LIBPNG": "1.6.55", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.4", "TIFF": "4.7.1", From 26c70950e9a5880dab8b510acf35f9faff5cbfba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Feb 2026 08:13:18 +1100 Subject: [PATCH 2278/2374] Use walrus operator --- src/PIL/MpoImagePlugin.py | 9 ++++----- src/PIL/PngImagePlugin.py | 12 ++++-------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 9360061ba18..bee0a56f9ac 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -59,11 +59,10 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + b"MPF\0" + b" " * ifd_length ) - exif = im_frame.encoderinfo.get("exif") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - im_frame.encoderinfo["exif"] = exif - if exif: + if exif := im_frame.encoderinfo.get("exif"): + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + im_frame.encoderinfo["exif"] = exif mpf_offset += 4 + len(exif) JpegImagePlugin._save(im_frame, fp, filename) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 9826a4cd148..572762e6c83 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1403,8 +1403,7 @@ def _save( chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"] - icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) - if icc: + if icc := im.encoderinfo.get("icc_profile", im.info.get("icc_profile")): # ICC profile # according to PNG spec, the iCCP chunk contains: # Profile name 1-79 bytes (character string) @@ -1419,8 +1418,7 @@ def _save( # Disallow sRGB chunks when an iCCP-chunk has been emitted. chunks.remove(b"sRGB") - info = im.encoderinfo.get("pnginfo") - if info: + if info := im.encoderinfo.get("pnginfo"): chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"] for info_chunk in info.chunks: cid, data = info_chunk[:2] @@ -1472,8 +1470,7 @@ def _save( alpha_bytes = colors chunk(fp, b"tRNS", alpha[:alpha_bytes]) - dpi = im.encoderinfo.get("dpi") - if dpi: + if dpi := im.encoderinfo.get("dpi"): chunk( fp, b"pHYs", @@ -1490,8 +1487,7 @@ def _save( chunks.remove(cid) chunk(fp, cid, data) - exif = im.encoderinfo.get("exif") - if exif: + if exif := im.encoderinfo.get("exif"): if isinstance(exif, Image.Exif): exif = exif.tobytes(8) if exif.startswith(b"Exif\x00\x00"): From f273619682634fe6437d0cdb7858c18745ea31bc Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:43:06 +0200 Subject: [PATCH 2279/2374] Test on macos-26-intel --- .github/workflows/test.yml | 2 +- .github/workflows/wheels.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 471cab90e3d..80bbfb45f67 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,7 +61,7 @@ jobs: - { python-version: "3.14t", disable-gil: true } - { python-version: "3.13t", disable-gil: true } # Intel - - { os: "macos-15-intel", python-version: "3.10" } + - { os: "macos-26-intel", python-version: "3.10" } exclude: - { os: "macos-latest", python-version: "3.10" } diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 20379c75370..910c7202b91 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -53,19 +53,19 @@ jobs: include: - name: "macOS 10.10 x86_64" platform: macos - os: macos-15-intel + os: macos-26-intel cibw_arch: x86_64 build: "cp3{10,11}*" macosx_deployment_target: "10.10" - name: "macOS 10.13 x86_64" platform: macos - os: macos-15-intel + os: macos-26-intel cibw_arch: x86_64 build: "cp3{12,13}*" macosx_deployment_target: "10.13" - name: "macOS 10.15 x86_64" platform: macos - os: macos-15-intel + os: macos-26-intel cibw_arch: x86_64 build: "{cp314,pp3}*" macosx_deployment_target: "10.15" @@ -104,7 +104,7 @@ jobs: cibw_arch: arm64_iphonesimulator - name: "iOS x86_64 simulator" platform: ios - os: macos-15-intel + os: macos-26-intel cibw_arch: x86_64_iphonesimulator steps: - uses: actions/checkout@v6 From 0c2dc2047edcd69a29445b7c91a75dfe0b85f3eb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:25:35 +0000 Subject: [PATCH 2280/2374] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.14 → v0.15.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.14...v0.15.4) - [github.com/PyCQA/bandit: 1.9.3 → 1.9.4](https://github.com/PyCQA/bandit/compare/1.9.3...1.9.4) - [github.com/pre-commit/mirrors-clang-format: v21.1.8 → v22.1.0](https://github.com/pre-commit/mirrors-clang-format/compare/v21.1.8...v22.1.0) - [github.com/python-jsonschema/check-jsonschema: 0.36.1 → 0.37.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.36.1...0.37.0) - [github.com/tox-dev/pyproject-fmt: v2.12.1 → v2.16.2](https://github.com/tox-dev/pyproject-fmt/compare/v2.12.1...v2.16.2) - [github.com/abravalheri/validate-pyproject: v0.24.1 → v0.25](https://github.com/abravalheri/validate-pyproject/compare/v0.24.1...v0.25) --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 19be90c1610..53fd0a3ca59 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.14 + rev: v0.15.4 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.9.3 + rev: 1.9.4 hooks: - id: bandit args: [--severity-level=high] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v21.1.8 + rev: v22.1.0 hooks: - id: clang-format types: [c] @@ -52,7 +52,7 @@ repos: exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.36.1 + rev: 0.37.0 hooks: - id: check-github-workflows - id: check-readthedocs @@ -69,12 +69,12 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.12.1 + rev: v2.16.2 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.24.1 + rev: v0.25 hooks: - id: validate-pyproject additional_dependencies: [trove-classifiers>=2024.10.12] From 7fc49a5cf413833d14c857d5829cd908d09ec7b6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:27:42 +0000 Subject: [PATCH 2281/2374] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyproject.toml | 60 ++++++++--------------- src/Tk/tkImaging.c | 6 ++- src/encode.c | 5 +- src/libImaging/Arrow.c | 90 ++++++++++++++++++----------------- src/libImaging/Convert.c | 14 ++++-- src/libImaging/Draw.c | 11 +++-- src/libImaging/GetBBox.c | 10 ++-- src/libImaging/Jpeg2KEncode.c | 6 ++- src/libImaging/JpegDecode.c | 6 ++- src/libImaging/Matrix.c | 5 +- 10 files changed, 106 insertions(+), 107 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 91f4750e46a..4b969dbc426 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,6 @@ optional-dependencies.test-arrow = [ "nanoarrow", "pyarrow", ] - optional-dependencies.tests = [ "check-manifest", "coverage>=7.4.2", @@ -77,16 +76,15 @@ optional-dependencies.tests = [ "pytest-xdist", "trove-classifiers>=2024.10.12", ] - optional-dependencies.xmp = [ "defusedxml", ] +urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" urls.Changelog = "https://github.com/python-pillow/Pillow/releases" urls.Documentation = "https://pillow.readthedocs.io" urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" urls.Homepage = "https://python-pillow.github.io" urls.Mastodon = "https://fosstodon.org/@pillow" -urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" urls.Source = "https://github.com/python-pillow/Pillow" [tool.setuptools] @@ -95,70 +93,50 @@ packages = [ ] include-package-data = true package-dir = { "" = "src" } - -[tool.setuptools.dynamic] -version = { attr = "PIL.__version__" } +dynamic.version = { attr = "PIL.__version__" } [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 - config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" - test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" test-requires = [ "numpy", ] -xbuild-tools = [ ] - -[tool.cibuildwheel.ios] +xbuild-tools = [] # Disable platform guessing on iOS, and disable raqm (since there won't be a # vendor version, and we can't distribute it due to licensing) -config-settings = "raqm=disable imagequant=disable platform-guessing=disable" - +ios.config-settings = "raqm=disable imagequant=disable platform-guessing=disable" # iOS needs to be given a specific pytest invocation and list of test sources. -test-sources = [ +ios.test-sources = [ "checks", "Tests", "selftest.py", ] -test-command = [ +ios.test-command = [ "python -m selftest", "python -m pytest -vv -x -W always checks/check_wheel.py Tests", ] - # There's no numpy wheel for iOS (yet...) -test-requires = [ ] - -[tool.cibuildwheel.macos] +ios.test-requires = [] # Disable platform guessing on macOS to avoid picking up Homebrew etc. -config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" - -[tool.cibuildwheel.macos.environment] +macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" # Isolate macOS build environment from Homebrew etc. -PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" - -[[tool.cibuildwheel.overrides]] -# iOS environment is isolated by cibuildwheel, but needs the dependencies -select = "*_iphoneos" -environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH" - -[[tool.cibuildwheel.overrides]] -# iOS simulator environment is isolated by cibuildwheel, but needs the dependencies -select = "*_iphonesimulator" -environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH" - -[[tool.cibuildwheel.overrides]] -select = "*-win32" -test-requires = [ ] +macos.environment.PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +overrides = [ + # iOS environment is isolated by cibuildwheel, but needs the dependencies + { select = "*_iphoneos", environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH" }, + # iOS simulator environment is isolated by cibuildwheel, but needs the dependencies + { select = "*_iphonesimulator", environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH" }, + { select = "*-win32", test-requires = [] }, +] [tool.black] exclude = "wheels/multibuild" [tool.ruff] exclude = [ "wheels/multibuild" ] - fix = true lint.select = [ "C4", # flake8-comprehensions @@ -207,9 +185,9 @@ lint.isort.required-imports = [ [tool.pyproject-fmt] max_supported_python = "3.14" -[tool.pytest.ini_options] -addopts = "-ra --color=auto" -testpaths = [ +[tool.pytest] +ini_options.addopts = "-ra --color=auto" +ini_options.testpaths = [ "Tests", ] diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 834634bd7fa..3e35f885f61 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -124,8 +124,10 @@ PyImagingPhotoPut( if (im->mode == IMAGING_MODE_1 || im->mode == IMAGING_MODE_L) { block.pixelSize = 1; block.offset[0] = block.offset[1] = block.offset[2] = block.offset[3] = 0; - } else if (im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA || - im->mode == IMAGING_MODE_RGBX || im->mode == IMAGING_MODE_RGBa) { + } else if ( + im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA || + im->mode == IMAGING_MODE_RGBX || im->mode == IMAGING_MODE_RGBa + ) { block.pixelSize = 4; block.offset[0] = 0; block.offset[1] = 1; diff --git a/src/encode.c b/src/encode.c index 06e4a089380..ea57615bec8 100644 --- a/src/encode.c +++ b/src/encode.c @@ -999,8 +999,9 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, PyBytes_AsString(value) ); - } else if (type == TIFF_DOUBLE || type == TIFF_SRATIONAL || - type == TIFF_RATIONAL) { + } else if ( + type == TIFF_DOUBLE || type == TIFF_SRATIONAL || type == TIFF_RATIONAL + ) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value) ); diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index d2ed10f0a6e..de4d3568eda 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -170,16 +170,17 @@ export_named_type(struct ArrowSchema *schema, char *format, const char *name) { strncpy(formatp, format, format_len); strncpy(namep, name, name_len); - *schema = (struct ArrowSchema){// Type description - .format = formatp, - .name = namep, - .metadata = NULL, - .flags = 0, - .n_children = 0, - .children = NULL, - .dictionary = NULL, - // Bookkeeping - .release = &ReleaseExportedSchema + *schema = (struct ArrowSchema){ + // Type description + .format = formatp, + .name = namep, + .metadata = NULL, + .flags = 0, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &ReleaseExportedSchema }; return 0; } @@ -287,17 +288,18 @@ export_single_channel_array(Imaging im, struct ArrowArray *array) { im->refcount++; MUTEX_UNLOCK(&im->mutex); // Initialize primitive fields - *array = (struct ArrowArray){// Data description - .length = length, - .offset = 0, - .null_count = 0, - .n_buffers = 2, - .n_children = 0, - .children = NULL, - .dictionary = NULL, - // Bookkeeping - .release = &release_const_array, - .private_data = im + *array = (struct ArrowArray){ + // Data description + .length = length, + .offset = 0, + .null_count = 0, + .n_buffers = 2, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im }; // Allocate list of buffers @@ -332,17 +334,18 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { // Initialize primitive fields // Fixed length arrays are 1 buffer of validity, and the length in pixels. // Data is in a child array. - *array = (struct ArrowArray){// Data description - .length = length, - .offset = 0, - .null_count = 0, - .n_buffers = 1, - .n_children = 1, - .children = NULL, - .dictionary = NULL, - // Bookkeeping - .release = &release_const_array, - .private_data = im + *array = (struct ArrowArray){ + // Data description + .length = length, + .offset = 0, + .null_count = 0, + .n_buffers = 1, + .n_children = 1, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im }; // Allocate list of buffers @@ -367,17 +370,18 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { MUTEX_LOCK(&im->mutex); im->refcount++; MUTEX_UNLOCK(&im->mutex); - *array->children[0] = (struct ArrowArray){// Data description - .length = length * 4, - .offset = 0, - .null_count = 0, - .n_buffers = 2, - .n_children = 0, - .children = NULL, - .dictionary = NULL, - // Bookkeeping - .release = &release_const_array, - .private_data = im + *array->children[0] = (struct ArrowArray){ + // Data description + .length = length * 4, + .offset = 0, + .null_count = 0, + .n_buffers = 2, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im }; array->children[0]->buffers = diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 330e5325c33..002497c3264 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1695,16 +1695,20 @@ ImagingConvertTransparent(Imaging imIn, const ModeID mode, int r, int g, int b) if (mode == IMAGING_MODE_RGBa) { premultiplied = 1; } - } else if (imIn->mode == IMAGING_MODE_RGB && - (mode == IMAGING_MODE_LA || mode == IMAGING_MODE_La)) { + } else if ( + imIn->mode == IMAGING_MODE_RGB && + (mode == IMAGING_MODE_LA || mode == IMAGING_MODE_La) + ) { convert = rgb2la; source_transparency = 1; if (mode == IMAGING_MODE_La) { premultiplied = 1; } - } else if ((imIn->mode == IMAGING_MODE_1 || imIn->mode == IMAGING_MODE_I || - imIn->mode == IMAGING_MODE_I_16 || imIn->mode == IMAGING_MODE_L) && - (mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_LA)) { + } else if ( + (imIn->mode == IMAGING_MODE_1 || imIn->mode == IMAGING_MODE_I || + imIn->mode == IMAGING_MODE_I_16 || imIn->mode == IMAGING_MODE_L) && + (mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_LA) + ) { if (imIn->mode == IMAGING_MODE_1) { convert = bit2rgb; } else if (imIn->mode == IMAGING_MODE_I) { diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index d2898043256..0d28069f000 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -537,8 +537,9 @@ polygon_generic( // Needed to draw consistent polygons xx[j] = xx[j - 1]; j++; - } else if ((ymin == current->ymin || ymin == current->ymax) && - current->dx != 0) { + } else if ( + (ymin == current->ymin || ymin == current->ymax) && current->dx != 0 + ) { // Connect discontiguous corners for (k = 0; k < i; k++) { Edge *other_edge = edge_table[k]; @@ -570,8 +571,10 @@ polygon_generic( adjacent_line_x, adjacent_line_x_other_edge )) + 1; - } else if (xx[j - 1] < adjacent_line_x - 1 && - xx[j - 1] < adjacent_line_x_other_edge - 1) { + } else if ( + xx[j - 1] < adjacent_line_x - 1 && + xx[j - 1] < adjacent_line_x_other_edge - 1 + ) { xx[j - 1] = roundf(fmin( adjacent_line_x, adjacent_line_x_other_edge diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index d336121d5cc..7a57f6894a0 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -89,10 +89,12 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { INT32 mask = 0xffffffff; if (im->bands == 3) { ((UINT8 *)&mask)[3] = 0; - } else if (alpha_only && - (im->mode == IMAGING_MODE_RGBa || im->mode == IMAGING_MODE_RGBA || - im->mode == IMAGING_MODE_La || im->mode == IMAGING_MODE_LA || - im->mode == IMAGING_MODE_PA)) { + } else if ( + alpha_only && + (im->mode == IMAGING_MODE_RGBa || im->mode == IMAGING_MODE_RGBA || + im->mode == IMAGING_MODE_La || im->mode == IMAGING_MODE_LA || + im->mode == IMAGING_MODE_PA) + ) { #ifdef WORDS_BIGENDIAN mask = 0x000000ff; #else diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index fdfbde2d76b..3012783a2e4 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -330,8 +330,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { components = 4; color_space = OPJ_CLRSPC_SRGB; pack = j2k_pack_rgba; -#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \ - (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2) +#if ( \ + (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \ + (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2 \ +) } else if (im->mode == IMAGING_MODE_CMYK) { components = 4; color_space = OPJ_CLRSPC_CMYK; diff --git a/src/libImaging/JpegDecode.c b/src/libImaging/JpegDecode.c index ae3274456d3..05cb37554b3 100644 --- a/src/libImaging/JpegDecode.c +++ b/src/libImaging/JpegDecode.c @@ -206,8 +206,10 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by context->cinfo.out_color_space = JCS_EXT_RGBX; } #endif - else if (context->rawmode == IMAGING_RAWMODE_CMYK || - context->rawmode == IMAGING_RAWMODE_CMYK_I) { + else if ( + context->rawmode == IMAGING_RAWMODE_CMYK || + context->rawmode == IMAGING_RAWMODE_CMYK_I + ) { context->cinfo.out_color_space = JCS_CMYK; } else if (context->rawmode == IMAGING_RAWMODE_YCbCr) { context->cinfo.out_color_space = JCS_YCbCr; diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index d28e04edfaa..acd59ba7f39 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -46,8 +46,9 @@ ImagingConvertMatrix(Imaging im, const ModeID mode, float m[]) { } } ImagingSectionLeave(&cookie); - } else if (mode == IMAGING_MODE_HSV || mode == IMAGING_MODE_LAB || - mode == IMAGING_MODE_RGB) { + } else if ( + mode == IMAGING_MODE_HSV || mode == IMAGING_MODE_LAB || mode == IMAGING_MODE_RGB + ) { imOut = ImagingNewDirty(mode, im->xsize, im->ysize); if (!imOut) { return NULL; From 0fae74731d4b15f8496c263879d6de3895f9e7e1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:36:24 +1100 Subject: [PATCH 2282/2374] Update actions/download-artifact action to v8 (#9451) --- .github/workflows/wheels.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 20379c75370..a697ff47760 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -250,7 +250,7 @@ jobs: runs-on: ubuntu-latest name: Count dists steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: dist-* path: dist @@ -269,7 +269,7 @@ jobs: runs-on: ubuntu-latest name: Upload wheels to scientific-python-nightly-wheels steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: dist-!(sdist)* path: dist @@ -291,7 +291,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: dist-* path: dist From a8cf13010b7c76203b26dcd5c7534363a351b5ed Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:02:49 +1100 Subject: [PATCH 2283/2374] Use native configuration Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4b969dbc426..6d9910ca140 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -186,8 +186,8 @@ lint.isort.required-imports = [ max_supported_python = "3.14" [tool.pytest] -ini_options.addopts = "-ra --color=auto" -ini_options.testpaths = [ +addopts = [ "-ra", "--color=auto" ] +testpaths = [ "Tests", ] From 04470d5151069736f136210d3170bdab402baa47 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:51:41 +1100 Subject: [PATCH 2284/2374] Removed unused argument Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_font_pcf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 8ac63ea61ae..321dd85603a 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -75,7 +75,7 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png") -def test_to_imagefont(tmp_path: Path) -> None: +def test_to_imagefont() -> None: with open(fontname, "rb") as test_file: pcffont = PcfFontFile.PcfFontFile(test_file) imagefont = pcffont.to_imagefont() From f7ee26575ec7915c5d5d6b4b76bf1b3186383e1f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:57:34 +1100 Subject: [PATCH 2285/2374] Avoid shadowing built-in Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/FontFile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index c0c64fe6842..9e12a8bfede 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -112,8 +112,8 @@ def compile(self) -> None: def _encode_metrics(self) -> bytes: values: tuple[int, ...] = () - for id in range(256): - m = self.metrics[id] + for i in range(256): + m = self.metrics[i] if m: values += m[0] + m[1] + m[2] else: From f7582b8d5828a52a4276c22dc05ab8a4823d1748 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:04:00 +1100 Subject: [PATCH 2286/2374] Updated documentation terms Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/reference/ImageFont.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index d4d66988b0c..920a05e65e9 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -8,9 +8,9 @@ The :py:mod:`~PIL.ImageFont` module defines a class with the same name. Instance this class store bitmap fonts, and are used with the :py:meth:`PIL.ImageDraw.ImageDraw.text` method. -PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You +Pillow uses its own font file format to store bitmap fonts, limited to 256 characters. You can use :py:meth:`~PIL.FontFile.FontFile.to_imagefont` to convert BDF and PCF font -descriptors (X window font formats) to this format:: +descriptors (X Window font formats) to this format:: from PIL import PcfFontFile with open("Tests/fonts/10x20-ISO8859-1.pcf", "rb") as fp: From 55b0cbc27364b8b0ea01d7532ab509bdc6e65d32 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:01:13 +0200 Subject: [PATCH 2287/2374] Update CI targets docs --- docs/installation/platform-support.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 7a8707b9a30..74c63fb062f 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -39,15 +39,15 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 15 Sequoia | 3.10 | x86-64 | -| +----------------------------+---------------------+ -| | 3.11, 3.12, 3.13, 3.14, | arm64 | -| | PyPy3 | | +| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14, | arm64 | +| | 3.15, PyPy3 | | ++----------------------------------+----------------------------+---------------------+ +| macOS 26 Tahoe | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 | -| | 3.14, PyPy3 | | +| | 3.14, 3.15, PyPy3 | | | +----------------------------+---------------------+ | | 3.12 | arm64v8, ppc64le, | | | | s390x | @@ -55,7 +55,7 @@ These platforms are built and tested for every change. | Windows Server 2022 | 3.10 | x86 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 | -| | PyPy3 | | +| | 3.15, PyPy3 | | | +----------------------------+---------------------+ | | 3.13 (MinGW) | x86-64 | +----------------------------------+----------------------------+---------------------+ From abbd515e9bf0f96c1f220d98742b581ce4a7d8d6 Mon Sep 17 00:00:00 2001 From: Frank Henigman Date: Fri, 6 Mar 2026 22:20:21 -0500 Subject: [PATCH 2288/2374] Improve efficiency of FontFile._encode_metrics() Build up mutable sequences instead of recreating mutable ones. --- src/PIL/FontFile.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 9e12a8bfede..341431d3f45 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -111,20 +111,20 @@ def compile(self) -> None: self.metrics[i] = d, dst, s def _encode_metrics(self) -> bytes: - values: tuple[int, ...] = () + values: list[int] = [] for i in range(256): m = self.metrics[i] if m: - values += m[0] + m[1] + m[2] + values.extend(m[0] + m[1] + m[2]) else: - values += (0,) * 10 + values.extend((0,) * 10) - metrics = b"" + data = bytearray() for v in values: if v < 0: v += 65536 - metrics += _binary.o16be(v) - return metrics + data += _binary.o16be(v) + return bytes(data) def save(self, filename: str) -> None: """Save font""" From 5450f9d08a70bf770e20ba6b416f76d77d1e2146 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 8 Mar 2026 07:12:10 +1100 Subject: [PATCH 2289/2374] Updated harfbuzz to 13.0.1 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0057f62d6b4..211d08fdd8b 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -95,7 +95,7 @@ if [[ -n "$IOS_SDK" ]]; then else FREETYPE_VERSION=2.14.1 fi -HARFBUZZ_VERSION=12.3.2 +HARFBUZZ_VERSION=13.0.1 LIBPNG_VERSION=1.6.55 JPEGTURBO_VERSION=3.1.3 OPENJPEG_VERSION=2.5.4 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 43c58d15fdf..9e00772de14 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ def cmd_msbuild( "BROTLI": "1.2.0", "FREETYPE": "2.14.1", "FRIBIDI": "1.0.16", - "HARFBUZZ": "12.3.2", + "HARFBUZZ": "13.0.1", "JPEGTURBO": "3.1.3", "LCMS2": "2.18", "LIBAVIF": "1.3.0", From 28524f2069582750cdc9e295cf46b22a2ce8be56 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:39:27 +1100 Subject: [PATCH 2290/2374] Update freetype to 2.14.2 (#9449) --- .github/workflows/wheels-dependencies.sh | 10 +--------- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0057f62d6b4..bbc52352c18 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -90,11 +90,7 @@ fi ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds. -if [[ -n "$IOS_SDK" ]]; then - FREETYPE_VERSION=2.13.3 -else - FREETYPE_VERSION=2.14.1 -fi +FREETYPE_VERSION=2.14.2 HARFBUZZ_VERSION=12.3.2 LIBPNG_VERSION=1.6.55 JPEGTURBO_VERSION=3.1.3 @@ -310,10 +306,6 @@ function build { if [[ -n "$IS_MACOS" ]]; then # 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 else build_freetype diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 43c58d15fdf..34f3065484c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -114,7 +114,7 @@ def cmd_msbuild( V = { "BROTLI": "1.2.0", - "FREETYPE": "2.14.1", + "FREETYPE": "2.14.2", "FRIBIDI": "1.0.16", "HARFBUZZ": "12.3.2", "JPEGTURBO": "3.1.3", From de2845b19a8c2d475a68df77b137119de95975c0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 5 Mar 2026 10:59:57 +1100 Subject: [PATCH 2291/2374] Revert "Patch libavif for svt-av1 4.0 compatibility" This reverts commit f86ad8b36d75417f872723eba1c3a2ffc19c9c70. --- depends/install_libavif.sh | 4 ---- depends/libavif-svt4.patch | 14 -------------- 2 files changed, 18 deletions(-) delete mode 100644 depends/libavif-svt4.patch diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh index 0089bf2b5fd..2ee47903580 100755 --- a/depends/install_libavif.sh +++ b/depends/install_libavif.sh @@ -17,10 +17,6 @@ else pushd libavif-$version - # Apply patch for SVT-AV1 4.0 compatibility - # Pending release of https://github.com/AOMediaCodec/libavif/pull/2971 - patch -p1 < ../libavif-svt4.patch - if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then PREFIX=$(brew --prefix) else diff --git a/depends/libavif-svt4.patch b/depends/libavif-svt4.patch deleted file mode 100644 index 7abfc529970..00000000000 --- a/depends/libavif-svt4.patch +++ /dev/null @@ -1,14 +0,0 @@ ---- a/src/codec_svt.c -+++ b/src/codec_svt.c -@@ -162,7 +162,11 @@ static avifResult svtCodecEncodeImage(avifEncoder * encoder, - #else - svt_config->logical_processors = encoder->maxThreads; - #endif -+#if SVT_AV1_CHECK_VERSION(4, 0, 0) -+ svt_config->aq_mode = 2; -+#else - svt_config->enable_adaptive_quantization = 2; -+#endif - // disable 2-pass - #if SVT_AV1_CHECK_VERSION(0, 9, 0) - svt_config->rc_stats_buffer = (SvtAv1FixedBuf) { NULL, 0 }; From 686174b5cc34fc8a13d1e1d38bc32302b13ebd08 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Mar 2026 20:26:31 +1100 Subject: [PATCH 2292/2374] Updated libavif to 1.4.0 --- .github/workflows/wheels-dependencies.sh | 2 +- Tests/test_file_avif.py | 13 +++++-------- depends/install_libavif.sh | 2 +- winbuild/build_prepare.py | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index bbc52352c18..32492d20ff7 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -104,7 +104,7 @@ LIBWEBP_VERSION=1.6.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.2.0 -LIBAVIF_VERSION=1.3.0 +LIBAVIF_VERSION=1.4.0 function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index ffc4ce0210d..a25f7717797 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -145,14 +145,14 @@ def test_write_rgb(self, tmp_path: Path) -> None: # avifdec hopper.avif avif/hopper_avif_write.png assert_image_similar_tofile( - reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02 + reloaded, "Tests/images/avif/hopper_avif_write.png", 6.88 ) # This test asserts that the images are similar. If the average pixel # difference between the two images is less than the epsilon value, # then we're going to accept that it's a reasonable lossy version of # the image. - assert_image_similar(reloaded, im, 8.62) + assert_image_similar(reloaded, im, 9.28) def test_AvifEncoder_with_invalid_args(self) -> None: """ @@ -461,12 +461,9 @@ def test_decoder_codec_cannot_encode(self, tmp_path: Path) -> None: @pytest.mark.parametrize( "advanced", [ - { - "aq-mode": "1", - "enable-chroma-deltaq": "1", - }, - (("aq-mode", "1"), ("enable-chroma-deltaq", "1")), - [("aq-mode", "1"), ("enable-chroma-deltaq", "1")], + {"tune": "psnr"}, + (("tune", "psnr"),), + [("tune", "psnr")], ], ) def test_encoder_advanced_codec_options( diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh index 2ee47903580..5c5c6f3f7c2 100755 --- a/depends/install_libavif.sh +++ b/depends/install_libavif.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -eo pipefail -version=1.3.0 +version=1.4.0 if [[ "$GHA_LIBAVIF_CACHE_HIT" == "true" ]]; then diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 34f3065484c..f240cb02ad8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -119,7 +119,7 @@ def cmd_msbuild( "HARFBUZZ": "12.3.2", "JPEGTURBO": "3.1.3", "LCMS2": "2.18", - "LIBAVIF": "1.3.0", + "LIBAVIF": "1.4.0", "LIBIMAGEQUANT": "4.4.1", "LIBPNG": "1.6.55", "LIBWEBP": "1.6.0", From dd042da9c2bc7aff503a8894def9e9f7681f3dc5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Mar 2026 06:32:15 +1100 Subject: [PATCH 2293/2374] Update tests to change for ValueError when encoding an empty image --- Tests/test_file_gif.py | 2 +- Tests/test_file_spider.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index b52816fdc1a..7b504233d56 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -314,7 +314,7 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None: def test_save_zero(size: tuple[int, int]) -> None: b = BytesIO() im = Image.new("RGB", size) - with pytest.raises(SystemError): + with pytest.raises(ValueError, match="cannot write empty image"): im.save(b, "GIF") diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 903632cffb0..3b1953aac5d 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -72,7 +72,7 @@ def test_save(tmp_path: Path) -> None: def test_save_zero(size: tuple[int, int]) -> None: b = BytesIO() im = Image.new("1", size) - with pytest.raises(SystemError): + with pytest.raises(ValueError, match="cannot write empty image"): im.save(b, "SPIDER") From 3a44ba1c75d72e7f375be9c6b7c71e2aef5e35e7 Mon Sep 17 00:00:00 2001 From: Gareth Davidson Date: Fri, 13 Mar 2026 22:42:15 +0000 Subject: [PATCH 2294/2374] Add Amiga Workbench .info loader to 3rd party plugins list (#9459) --- docs/handbook/third-party-plugins.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/handbook/third-party-plugins.rst b/docs/handbook/third-party-plugins.rst index 1c7dfb5e95b..20086649906 100644 --- a/docs/handbook/third-party-plugins.rst +++ b/docs/handbook/third-party-plugins.rst @@ -7,6 +7,7 @@ itself. Here is a list of PyPI projects that offer additional plugins: +* :pypi:`amigainfo`: Adds support for Amiga Workbench .info icon files. * :pypi:`DjvuRleImagePlugin`: Plugin for the DjVu RLE image format as defined in the DjVuLibre docs. * :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library. * :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL. From d5d0734169ac4c593caac4f5769e60a01574fa09 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Mar 2026 20:48:25 +1100 Subject: [PATCH 2295/2374] Add CMYK palettes --- Tests/test_image_putpalette.py | 15 +++++++++++++++ src/PIL/Image.py | 11 ++++++++--- src/libImaging/Pack.c | 14 ++++++++++++++ src/libImaging/Palette.c | 3 ++- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 661764b608a..237de63305a 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -91,6 +91,21 @@ def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None: assert im.palette.colors == {(1, 2, 3, 4): 0} +@pytest.mark.parametrize( + "mode, palette", + ( + ("CMYK", (1, 2, 3, 4)), + ("CMYKX", (1, 2, 3, 4, 0)), + ), +) +def test_cmyk_palette(mode: str, palette: tuple[int, ...]) -> None: + im = Image.new("P", (1, 1)) + im.putpalette(palette, mode) + assert im.getpalette() == [250, 249, 248] + assert im.palette is not None + assert im.palette.colors == {(1, 2, 3, 4): 0} + + def test_empty_palette() -> None: im = Image.new("P", (1, 1)) assert im.getpalette() == [] diff --git a/src/PIL/Image.py b/src/PIL/Image.py index cc431a86a5d..c9db9731996 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2145,8 +2145,8 @@ def putpalette( Alternatively, an 8-bit string may be used instead of an integer sequence. :param data: A palette sequence (either a list or a string). - :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode - that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L"). + :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", "CMYK", or a + mode that can be transformed to one of those modes (e.g. "R", "RGBA;L"). """ from . import ImagePalette @@ -2165,7 +2165,12 @@ def putpalette( palette = ImagePalette.raw(rawmode, data) self._mode = "PA" if "A" in self.mode else "P" self.palette = palette - self.palette.mode = "RGBA" if "A" in rawmode else "RGB" + if rawmode.startswith("CMYK"): + self.palette.mode = "CMYK" + elif "A" in rawmode: + self.palette.mode = "RGBA" + else: + self.palette.mode = "RGB" self.load() # install new palette def putpixel( diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index fdf5a72aa9e..161d82f2e5f 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -325,6 +325,19 @@ ImagingPackXBGR(UINT8 *out, const UINT8 *in, int pixels) { } } +void +ImagingPackCMYK2RGB(UINT8 *out, const UINT8 *in, int xsize) { + int x, nk, tmp; + for (x = 0; x < xsize; x++) { + nk = 255 - in[3]; + out[0] = CLIP8(nk - MULDIV255(in[0], nk, tmp)); + out[1] = CLIP8(nk - MULDIV255(in[1], nk, tmp)); + out[2] = CLIP8(nk - MULDIV255(in[2], nk, tmp)); + out += 3; + in += 4; + } +} + void ImagingPackBGRA(UINT8 *out, const UINT8 *in, int pixels) { int i; @@ -605,6 +618,7 @@ static struct { {IMAGING_MODE_CMYK, IMAGING_RAWMODE_M, 8, band1}, {IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y, 8, band2}, {IMAGING_MODE_CMYK, IMAGING_RAWMODE_K, 8, band3}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_RGB, 24, ImagingPackCMYK2RGB}, /* video (YCbCr) */ {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr, 24, ImagingPackRGB}, diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c index 371ba644b50..b2dacf656b5 100644 --- a/src/libImaging/Palette.c +++ b/src/libImaging/Palette.c @@ -27,7 +27,8 @@ ImagingPaletteNew(const ModeID mode) { int i; ImagingPalette palette; - if (mode != IMAGING_MODE_RGB && mode != IMAGING_MODE_RGBA) { + if (mode != IMAGING_MODE_RGB && mode != IMAGING_MODE_RGBA && + mode != IMAGING_MODE_CMYK) { return (ImagingPalette)ImagingError_ModeError(); } From 29509ffa753011d250e976e941a88a28a8277e12 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Mar 2026 20:21:53 +1100 Subject: [PATCH 2296/2374] Detect CMYK palette in JPEG2000 images --- Tests/test_file_jpeg2k.py | 1 + src/PIL/Jpeg2KImagePlugin.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 575d911def5..df686df3222 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -445,6 +445,7 @@ def test_pclr() -> None: ) as im: assert im.mode == "P" assert im.palette is not None + assert im.palette.mode == "CMYK" assert len(im.palette.colors) == 139 assert im.palette.colors[(0, 0, 0, 0)] == 0 diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index d6ec38d4310..e5d735c7328 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -176,6 +176,7 @@ def _parse_jp2_header( nc = None dpi = None # 2-tuple of DPI info, or None palette = None + cmyk = False while header.has_next_box(): tbox = header.next_box_type() @@ -196,10 +197,11 @@ def _parse_jp2_header( mode = "RGB" elif nc == 4: mode = "RGBA" - elif tbox == b"colr" and nc == 4: + elif tbox == b"colr": meth, _, _, enumcs = header.read_fields(">BBBI") - if meth == 1 and enumcs == 12: - mode = "CMYK" + if cmyk := (meth == 1 and enumcs == 12): + if nc == 4: + mode = "CMYK" elif tbox == b"pclr" and mode in ("L", "LA"): ne, npc = header.read_fields(">HB") assert isinstance(ne, int) @@ -210,7 +212,11 @@ def _parse_jp2_header( if bitdepth > max_bitdepth: max_bitdepth = bitdepth if max_bitdepth <= 8: - palette = ImagePalette.ImagePalette("RGBA" if npc == 4 else "RGB") + if npc == 4: + palette_mode = "CMYK" if cmyk else "RGBA" + else: + palette_mode = "RGB" + palette = ImagePalette.ImagePalette(palette_mode) for i in range(ne): color: list[int] = [] for value in header.read_fields(">" + ("B" * npc)): From 4f5802b6b17a3c463f734e3c5cc3484dca9b1d44 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Mar 2026 23:45:22 +1100 Subject: [PATCH 2297/2374] Do not use palette from grayscale or bilevel colorspace --- Tests/test_file_jpeg2k.py | 7 +++++++ src/PIL/Jpeg2KImagePlugin.py | 15 +++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 575d911def5..4f6376ed7e5 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -440,6 +440,13 @@ def test_pclr() -> None: assert len(im.palette.colors) == 256 assert im.palette.colors[(255, 255, 255)] == 0 + for enumcs in (0, 15, 17): + with open(f"{EXTRA_DIR}/issue104_jpxstream.jp2", "rb") as fp: + data = bytearray(fp.read()) + data[114:115] = bytes([enumcs]) + with Image.open(BytesIO(data)) as im: + assert im.mode == "L" + with Image.open( f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2" ) as im: diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index d6ec38d4310..b08c466e266 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -176,6 +176,7 @@ def _parse_jp2_header( nc = None dpi = None # 2-tuple of DPI info, or None palette = None + colr = None while header.has_next_box(): tbox = header.next_box_type() @@ -196,11 +197,17 @@ def _parse_jp2_header( mode = "RGB" elif nc == 4: mode = "RGBA" - elif tbox == b"colr" and nc == 4: + elif tbox == b"colr": meth, _, _, enumcs = header.read_fields(">BBBI") - if meth == 1 and enumcs == 12: - mode = "CMYK" - elif tbox == b"pclr" and mode in ("L", "LA"): + if meth == 1: + if enumcs in (0, 15): + colr = "1" + elif enumcs == 12: + if nc == 4: + mode = "CMYK" + elif enumcs == 17: + colr = "L" + elif tbox == b"pclr" and mode in ("L", "LA") and colr not in ("1", "L"): ne, npc = header.read_fields(">HB") assert isinstance(ne, int) assert isinstance(npc, int) From 6a06285bf8832a87cc5a1c8d4d850e90051a87dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Mar 2026 23:52:33 +1100 Subject: [PATCH 2298/2374] Support reading JPEG2000 images with CMYK palettes --- src/libImaging/Jpeg2KDecode.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index 1b496f45ec0..1123d7bc915 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -601,6 +601,7 @@ j2ku_sycca_rgba( static const struct j2k_decode_unpacker j2k_unpackers[] = { {IMAGING_MODE_L, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_l}, {IMAGING_MODE_P, OPJ_CLRSPC_SRGB, 1, 0, j2ku_gray_l}, + {IMAGING_MODE_P, OPJ_CLRSPC_CMYK, 1, 0, j2ku_gray_l}, {IMAGING_MODE_PA, OPJ_CLRSPC_SRGB, 2, 0, j2ku_graya_la}, {IMAGING_MODE_I_16, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, {IMAGING_MODE_I_16B, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, From 8442a8541c486eb19a3bc3d940c43a45241f2e44 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Mar 2026 22:12:58 +1100 Subject: [PATCH 2299/2374] Support saving images with non-RGB palettes as PNGs --- Tests/test_file_png.py | 10 ++++++++++ src/PIL/PngImagePlugin.py | 7 +++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 2e0af504183..3f08d1ad3aa 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -707,6 +707,16 @@ def test_plte_length(self, tmp_path: Path) -> None: assert reloaded.png.im_palette is not None assert len(reloaded.png.im_palette[1]) == 3 + def test_plte_cmyk(self, tmp_path: Path) -> None: + im = Image.new("P", (1, 1)) + im.putpalette((0, 100, 150, 200), "CMYK") + + out = tmp_path / "temp.png" + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.convert("CMYK").getpixel((0, 0)) == (200, 222, 232, 0) + def test_getxmp(self) -> None: with Image.open("Tests/images/color_snakes.png") as im: if ElementTree is None: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 572762e6c83..4e082a293ff 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1353,6 +1353,9 @@ def _save( mode = im.mode outmode = mode + palette = [] + if im.palette: + palette = im.getpalette() or [] if mode == "P": # # attempt to minimize storage requirements for palette images @@ -1362,7 +1365,7 @@ def _save( else: # check palette contents if im.palette: - colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1) + colors = max(min(len(palette) // 3, 256), 1) else: colors = 256 @@ -1435,7 +1438,7 @@ def _save( if im.mode == "P": palette_byte_number = colors * 3 - palette_bytes = im.im.getpalette("RGB")[:palette_byte_number] + palette_bytes = bytes(palette[:palette_byte_number]) while len(palette_bytes) < palette_byte_number: palette_bytes += b"\0" chunk(fp, b"PLTE", palette_bytes) From e34c7bee9113dcf36ff1c40ce0d13a2611a02b54 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 17 Mar 2026 10:56:32 +1100 Subject: [PATCH 2300/2374] Updated Ghostscript to 10.7.0 --- .github/workflows/test-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index ed32be26d41..3bc70e337a8 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -98,8 +98,8 @@ jobs: choco install nasm --no-progress echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - choco install ghostscript --version=10.6.0 --no-progress - echo "C:\Program Files\gs\gs10.06.0\bin" >> $env:GITHUB_PATH + choco install ghostscript --version=10.7.0 --no-progress + echo "C:\Program Files\gs\gs10.07.0\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images From e6bb8626c8e6a556e912c4c15bd46b1fb45f7de6 Mon Sep 17 00:00:00 2001 From: Zhiyuan Ouyang Date: Tue, 17 Mar 2026 10:30:40 -0700 Subject: [PATCH 2301/2374] Add a ExifTag "FrameRate" to be supported in PIL. Reference: https://exiftool.org/TagNames/EXIF.html --- src/PIL/ExifTags.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index c1c05cdba71..a9522e761b0 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -289,6 +289,7 @@ class Base(IntEnum): OpcodeList2 = 0xC741 OpcodeList3 = 0xC74E NoiseProfile = 0xC761 + FrameRate = 0xC764 """Maps EXIF tags to tag names.""" From 98c149f0307e077d9aa0c2018710c9354afe6907 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 Mar 2026 09:26:58 +1100 Subject: [PATCH 2302/2374] Simplified code --- src/PIL/BmpImagePlugin.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 5ee61b35b17..00975624ae3 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -179,11 +179,8 @@ def _bitmap(self, header: int = 0, offset: int = 0) -> None: # ------- If color count was not found in the header, compute from bits assert isinstance(file_info["bits"], int) - file_info["colors"] = ( - file_info["colors"] - if file_info.get("colors", 0) - else (1 << file_info["bits"]) - ) + if not file_info.get("colors", 0): + file_info["colors"] = 1 << file_info["bits"] assert isinstance(file_info["colors"], int) if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8: offset += 4 * file_info["colors"] From 93de6a78d8cee9b2e9cdbe1f4d1da014d090ce8b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 Mar 2026 10:09:38 +1100 Subject: [PATCH 2303/2374] Generate test image programmatically --- Tests/images/pal8_offset.bmp | Bin 9254 -> 0 bytes Tests/test_file_bmp.py | 12 +++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) delete mode 100644 Tests/images/pal8_offset.bmp diff --git a/Tests/images/pal8_offset.bmp b/Tests/images/pal8_offset.bmp deleted file mode 100644 index 24be65f22c3c7b77adb011ab4d635d95b31ee15d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9254 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 T<(E}?03>_8_`3k(yk-3da10=X diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 2e0394b3b2a..b61497812cb 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -42,7 +42,7 @@ def test_fallback_if_mmap_errors() -> None: # This image has been truncated, # so that the buffer is not large enough when using mmap with Image.open("Tests/images/mmap_error.bmp") as im: - assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp") + assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") def test_save_to_bytes() -> None: @@ -239,10 +239,12 @@ def test_unsupported_bmp_bitfields_layout() -> None: def test_offset() -> None: - # This image has been hexedited - # to exclude the palette size from the pixel data offset - with Image.open("Tests/images/pal8_offset.bmp") as im: - assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") + # Exclude the palette size from the pixel data offset + with open("Tests/images/bmp/g/pal8.bmp", "rb") as fp: + data = fp.read() + data = data[:10] + o32(54) + data[14:] + with Image.open(io.BytesIO(data)) as im: + assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") def test_use_raw_alpha(monkeypatch: pytest.MonkeyPatch) -> None: From 735d02584b4db8a1b923d546b7be94de28f0876a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 Mar 2026 10:38:28 +1100 Subject: [PATCH 2304/2374] Allow for different palette entry sizes when correcting offset --- Tests/test_file_bmp.py | 16 ++++++++++++---- src/PIL/BmpImagePlugin.py | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index b61497812cb..c8ac4652434 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -238,13 +238,21 @@ def test_unsupported_bmp_bitfields_layout() -> None: Image.open(fp) -def test_offset() -> None: +@pytest.mark.parametrize( + "offset, path", + ( + (26, "pal8os2.bmp"), + (54, "pal8.bmp"), + ), +) +def test_offset(offset: int, path: str) -> None: + image_path = "Tests/images/bmp/g/" + path # Exclude the palette size from the pixel data offset - with open("Tests/images/bmp/g/pal8.bmp", "rb") as fp: + with open(image_path, "rb") as fp: data = fp.read() - data = data[:10] + o32(54) + data[14:] + data = data[:10] + o32(offset) + data[14:] with Image.open(io.BytesIO(data)) as im: - assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") + assert_image_equal_tofile(im, image_path) def test_use_raw_alpha(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 00975624ae3..a6724cab437 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -181,9 +181,10 @@ def _bitmap(self, header: int = 0, offset: int = 0) -> None: assert isinstance(file_info["bits"], int) if not file_info.get("colors", 0): file_info["colors"] = 1 << file_info["bits"] + assert isinstance(file_info["palette_padding"], int) assert isinstance(file_info["colors"], int) if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8: - offset += 4 * file_info["colors"] + offset += file_info["palette_padding"] * file_info["colors"] # ---------------------- Check bit depth for unusual unsupported values self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", "")) @@ -262,7 +263,6 @@ def _bitmap(self, header: int = 0, offset: int = 0) -> None: msg = f"Unsupported BMP Palette size ({file_info['colors']})" raise OSError(msg) else: - assert isinstance(file_info["palette_padding"], int) padding = file_info["palette_padding"] palette = read(padding * file_info["colors"]) grayscale = True From c3041861905e690b3f71901fd1cab4667a82c46a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 20 Mar 2026 10:02:14 +1100 Subject: [PATCH 2305/2374] Simplified code --- Tests/test_file_tga.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bb8d3eefcc6..277515fd425 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -13,8 +13,6 @@ _TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") -_ORIGINS = ("tl", "bl") - _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} @@ -29,7 +27,7 @@ ("200x32", "RGBA"), ), ) -@pytest.mark.parametrize("origin", _ORIGINS) +@pytest.mark.parametrize("origin", _ORIGIN_TO_ORIENTATION) @pytest.mark.parametrize("rle", (True, False)) def test_sanity( size_mode: tuple[str, str], origin: str, rle: str, tmp_path: Path From 3b1f70da61df02378594ba6f490e937da127d40f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:01:20 +1100 Subject: [PATCH 2306/2374] Simplify `setimage()` by always passing extents (#9395) --- src/PIL/Image.py | 4 ++-- src/decode.c | 15 +++++---------- src/encode.c | 15 +++++---------- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index cc431a86a5d..01f49ccc03f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -885,7 +885,7 @@ def tobytes(self, encoder_name: str = "raw", *args: Any) -> bytes: # unpack data e = _getencoder(self.mode, encoder_name, encoder_args) - e.setimage(self.im) + e.setimage(self.im, (0, 0) + self.size) from . import ImageFile @@ -956,7 +956,7 @@ def frombytes( # unpack data d = _getdecoder(self.mode, decoder_name, decoder_args) - d.setimage(self.im) + d.setimage(self.im, (0, 0) + self.size) s = d.decode(data) if s[0] >= 0: diff --git a/src/decode.c b/src/decode.c index 7ec461c0e2c..c5c9cf56f70 100644 --- a/src/decode.c +++ b/src/decode.c @@ -163,7 +163,7 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { x0 = y0 = x1 = y1 = 0; /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O|(iiii)", &op, &x0, &y0, &x1, &y1)) { + if (!PyArg_ParseTuple(args, "O(iiii)", &op, &x0, &y0, &x1, &y1)) { return NULL; } im = PyImaging_AsImaging(op); @@ -176,15 +176,10 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { state = &decoder->state; /* Setup decoding tile extent */ - if (x0 == 0 && x1 == 0) { - state->xsize = im->xsize; - state->ysize = im->ysize; - } else { - state->xoff = x0; - state->yoff = y0; - state->xsize = x1 - x0; - state->ysize = y1 - y0; - } + state->xoff = x0; + state->yoff = y0; + state->xsize = x1 - x0; + state->ysize = y1 - y0; if (state->xoff < 0 || state->xsize <= 0 || state->xsize + state->xoff > (int)im->xsize || state->yoff < 0 || diff --git a/src/encode.c b/src/encode.c index 01b3af13fa1..f2bb464fa27 100644 --- a/src/encode.c +++ b/src/encode.c @@ -232,7 +232,7 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { x0 = y0 = x1 = y1 = 0; /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O|(nnnn)", &op, &x0, &y0, &x1, &y1)) { + if (!PyArg_ParseTuple(args, "O(nnnn)", &op, &x0, &y0, &x1, &y1)) { return NULL; } im = PyImaging_AsImaging(op); @@ -248,15 +248,10 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { state = &encoder->state; - if (x0 == 0 && x1 == 0) { - state->xsize = im->xsize; - state->ysize = im->ysize; - } else { - state->xoff = x0; - state->yoff = y0; - state->xsize = x1 - x0; - state->ysize = y1 - y0; - } + state->xoff = x0; + state->yoff = y0; + state->xsize = x1 - x0; + state->ysize = y1 - y0; if (state->xoff < 0 || state->xsize <= 0 || state->xsize + state->xoff > im->xsize || state->yoff < 0 || From 4d0089141c3b0bacfff5f5a3e949866a1fe851bf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Mar 2026 19:26:55 +1100 Subject: [PATCH 2307/2374] Fixed invalid test font --- Tests/fonts/fuzz_font-5203009437302784 | 6 +++--- Tests/test_font_crash.py | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Tests/fonts/fuzz_font-5203009437302784 b/Tests/fonts/fuzz_font-5203009437302784 index 0465e48c204..fb401fea225 100644 --- a/Tests/fonts/fuzz_font-5203009437302784 +++ b/Tests/fonts/fuzz_font-5203009437302784 @@ -1,10 +1,10 @@ STARTFONT FONT SIZE 10 -FONTBOUNDINGBOX -CHARS +FONTBOUNDINGBOX 1 1 0 0 +CHARS 1 STARTCHAR -ENCODING +ENCODING 65 BBX 2 5 ENDCHAR ENDFONT diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index c3e62abf62e..72a0f353477 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -2,8 +2,6 @@ from PIL import Image, ImageDraw, ImageFont -from .helper import skip_unless_feature_version - class TestFontCrash: def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: @@ -16,7 +14,6 @@ def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) draw.text((10, 10), "Test Text", font=font, fill="#000") - @skip_unless_feature_version("freetype2", "2.12.0") def test_segfault(self) -> None: font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") self._fuzz_font(font) From 0d7f5077a75341b6af35636541e97299a1c5af69 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Mar 2026 00:58:27 +1100 Subject: [PATCH 2308/2374] If v2 extension area specifies no alpha, fill alpha channel --- Tests/test_file_tga.py | 22 +++++++++++++++++++++- src/PIL/TgaImagePlugin.py | 16 ++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 277515fd425..7ec562342e1 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,11 +1,12 @@ from __future__ import annotations import os +from io import BytesIO from pathlib import Path import pytest -from PIL import Image, UnidentifiedImageError +from PIL import Image, UnidentifiedImageError, _binary from .helper import assert_image_equal, assert_image_equal_tofile, hopper @@ -92,6 +93,25 @@ def test_rgba_16() -> None: assert im.getpixel((1, 0)) == (0, 255, 82, 0) +def test_v2_no_alpha() -> None: + test_file = "Tests/images/tga/common/200x32_rgba_tl_rle.tga" + with open(test_file, "rb") as fp: + data = fp.read() + data += ( + b"\x00" * 495 + + _binary.o32le(len(data)) + + _binary.o32le(0) + + b"TRUEVISION-XFILE.\x00" + ) + with Image.open(BytesIO(data)) as im: + with Image.open(test_file) as im2: + r, g, b = im2.split()[:3] + a = Image.new("L", im2.size, 255) + expected = Image.merge("RGBA", (r, g, b, a)) + + assert_image_equal(im, expected) + + def test_id_field() -> None: # tga file with id field test_file = "Tests/images/tga_id_field.tga" diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 90d5b5cf4ee..b2989a4b764 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -17,11 +17,13 @@ # from __future__ import annotations +import os import warnings from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 +from ._binary import i32le as i32 from ._binary import o8 from ._binary import o16le as o16 @@ -157,6 +159,20 @@ def _open(self) -> None: pass # cannot decode def load_end(self) -> None: + if self.mode == "RGBA": + assert self.fp is not None + self.fp.seek(-26, os.SEEK_END) + footer = self.fp.read(26) + if footer.endswith(b"TRUEVISION-XFILE.\x00"): + # version 2 + extension_offset = i32(footer) + if extension_offset: + self.fp.seek(extension_offset + 494) + attributes_type = self.fp.read(1) + if attributes_type == b"\x00": + # No alpha + self.im.fillband(3, 255) + if self._flip_horizontally: self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) From fc0f65998fd5c6ddd37c4b3388d164314a4c68f1 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:21:51 +1100 Subject: [PATCH 2309/2374] Updated harfbuzz to 13.2.1 (#9461) --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 6f3a02f80d8..dbb7bc9773e 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -91,7 +91,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds. FREETYPE_VERSION=2.14.2 -HARFBUZZ_VERSION=13.0.1 +HARFBUZZ_VERSION=13.2.1 LIBPNG_VERSION=1.6.55 JPEGTURBO_VERSION=3.1.3 OPENJPEG_VERSION=2.5.4 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 95bb3e7d960..4a813820664 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ def cmd_msbuild( "BROTLI": "1.2.0", "FREETYPE": "2.14.2", "FRIBIDI": "1.0.16", - "HARFBUZZ": "13.0.1", + "HARFBUZZ": "13.2.1", "JPEGTURBO": "3.1.3", "LCMS2": "2.18", "LIBAVIF": "1.4.0", From 4e85badfc1cf83418744386f5da26db5e309a207 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 23 Mar 2026 21:23:24 +1100 Subject: [PATCH 2310/2374] Updated freetype to 2.14.3 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index dbb7bc9773e..76498d5f3e3 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -90,7 +90,7 @@ fi ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds. -FREETYPE_VERSION=2.14.2 +FREETYPE_VERSION=2.14.3 HARFBUZZ_VERSION=13.2.1 LIBPNG_VERSION=1.6.55 JPEGTURBO_VERSION=3.1.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 4a813820664..4183d92b544 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -114,7 +114,7 @@ def cmd_msbuild( V = { "BROTLI": "1.2.0", - "FREETYPE": "2.14.2", + "FREETYPE": "2.14.3", "FRIBIDI": "1.0.16", "HARFBUZZ": "13.2.1", "JPEGTURBO": "3.1.3", From f0b5f56e9f9d095b52de995a94c8387f0b4230ad Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:34:11 +1100 Subject: [PATCH 2311/2374] Updated libavif to 1.4.1 (#9479) --- .github/workflows/wheels-dependencies.sh | 2 +- depends/install_libavif.sh | 2 +- winbuild/build_prepare.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index dbb7bc9773e..85ce69e0728 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -104,7 +104,7 @@ LIBWEBP_VERSION=1.6.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.2.0 -LIBAVIF_VERSION=1.4.0 +LIBAVIF_VERSION=1.4.1 function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh index 5c5c6f3f7c2..2c5687391a9 100755 --- a/depends/install_libavif.sh +++ b/depends/install_libavif.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -eo pipefail -version=1.4.0 +version=1.4.1 if [[ "$GHA_LIBAVIF_CACHE_HIT" == "true" ]]; then diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 4a813820664..be72ae93b44 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -119,7 +119,7 @@ def cmd_msbuild( "HARFBUZZ": "13.2.1", "JPEGTURBO": "3.1.3", "LCMS2": "2.18", - "LIBAVIF": "1.4.0", + "LIBAVIF": "1.4.1", "LIBIMAGEQUANT": "4.4.1", "LIBPNG": "1.6.55", "LIBWEBP": "1.6.0", From ffd32a861a690a0dcb49d62379c20c4184810f19 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Tue, 24 Mar 2026 21:14:16 +0000 Subject: [PATCH 2312/2374] Check all allocs in the Arrow tree * handle alloc failure * Ensure we're calling release so the refcount on the image is decremented * Ensure that release array/schema can handle partially allocated children arrays. --- src/_imaging.c | 3 ++ src/libImaging/Arrow.c | 76 ++++++++++++++++++++++++------------------ 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index d2a195887fa..af836036857 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -286,6 +286,9 @@ PyObject * ExportArrowArrayPyCapsule(ImagingObject *self) { struct ArrowArray *array = (struct ArrowArray *)calloc(1, sizeof(struct ArrowArray)); + if (!array) { + return ArrowError(IMAGING_CODEC_MEMORY); + } int err = export_imaging_array(self->image, array); if (err == 0) { return PyCapsule_New(array, "arrow_array", ReleaseArrowArrayPyCapsule); diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index de4d3568eda..3ca227d4f8e 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -10,8 +10,8 @@ static void ReleaseExportedSchema(struct ArrowSchema *array) { - // This should not be called on already released array - // assert(array->release != NULL); + // TODO here: release and/or deallocate all data directly owned by + // the ArrowArray struct, such as the private_data. if (!array->release) { return; @@ -30,31 +30,36 @@ ReleaseExportedSchema(struct ArrowSchema *array) { } // Release children - for (int64_t i = 0; i < array->n_children; ++i) { - struct ArrowSchema *child = array->children[i]; - if (child->release != NULL) { - child->release(child); - child->release = NULL; - } - free(array->children[i]); - } if (array->children) { + for (int64_t i = 0; i < array->n_children; ++i) { + struct ArrowSchema *child = array->children[i]; + if (child != NULL) { + if (child->release != NULL) { + child->release(child); + child->release = NULL; + } + free(array->children[i]); + } + } free(array->children); + array->children = NULL; } // Release dictionary struct ArrowSchema *dict = array->dictionary; - if (dict != NULL && dict->release != NULL) { - dict->release(dict); - dict->release = NULL; + if (dict != NULL) { + if (dict->release != NULL) { + dict->release(dict); + dict->release = NULL; + } + free(dict); + array->dictionary = NULL; } - // TODO here: release and/or deallocate all data directly owned by - // the ArrowArray struct, such as the private_data. - // Mark array released array->release = NULL; } + char * image_band_json(Imaging im) { char *format = "{\"bands\": [\"%s\", \"%s\", \"%s\", \"%s\"]}"; @@ -220,13 +225,19 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) { // if it's not 1 band, it's an int32 at the moment. 4 uint8 bands. schema->n_children = 1; schema->children = calloc(1, sizeof(struct ArrowSchema *)); + if (!schema->children) { + schema->release(schema); + return IMAGING_CODEC_MEMORY; + } schema->children[0] = (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema)); + if (!schema->children[0]) { + schema->release(schema); + return IMAGING_CODEC_MEMORY; + } retval = export_named_type( schema->children[0], im->arrow_band_format, getModeData(im->mode)->name ); if (retval != 0) { - free(schema->children[0]); - free(schema->children); schema->release(schema); return retval; } @@ -256,11 +267,12 @@ release_const_array(struct ArrowArray *array) { array->buffers = NULL; } if (array->children) { - // undone -- does arrow release all the children recursively? for (int i = 0; i < array->n_children; i++) { - if (array->children[i]->release) { - array->children[i]->release(array->children[i]); - array->children[i]->release = NULL; + if (array->children[i]) { + if (array->children[i]->release) { + array->children[i]->release(array->children[i]); + array->children[i]->release = NULL; + } free(array->children[i]); } } @@ -303,8 +315,11 @@ export_single_channel_array(Imaging im, struct ArrowArray *array) { }; // Allocate list of buffers - array->buffers = (const void **)malloc(sizeof(void *) * array->n_buffers); - // assert(array->buffers != NULL); + array->buffers = (const void **)calloc(1, sizeof(void *) * array->n_buffers); + if (!array->buffers) { + array->release(array); + return IMAGING_CODEC_MEMORY; + } array->buffers[0] = NULL; // no nulls, null bitmap can be omitted if (im->block) { @@ -386,6 +401,9 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { array->children[0]->buffers = (const void **)calloc(2, sizeof(void *) * array->n_buffers); + if (!array->children[0]->buffers) { + goto err; + } if (im->block) { array->children[0]->buffers[1] = im->block; @@ -395,15 +413,7 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { return 0; err: - if (array->children[0]) { - free(array->children[0]); - } - if (array->children) { - free(array->children); - } - if (array->buffers) { - free(array->buffers); - } + array->release(array); return IMAGING_CODEC_MEMORY; } From 3a83d6abc3d2f51efc5b16e4f7d0351a1f9e877d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 25 Mar 2026 01:54:16 +0200 Subject: [PATCH 2313/2374] Enable colour in CI logs (#9486) --- .github/workflows/cifuzz.yml | 3 +++ .github/workflows/release-drafter.yml | 3 +++ .github/workflows/stale.yml | 3 +++ .github/workflows/test-docker.yml | 3 +++ .github/workflows/test-mingw.yml | 1 + .github/workflows/test-valgrind-memory.yml | 3 +++ .github/workflows/test-valgrind.yml | 3 +++ .github/workflows/test-windows.yml | 1 + 8 files changed, 20 insertions(+) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 7e771f1b7d1..3f78c98b6f6 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -24,6 +24,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: Fuzzing: runs-on: ubuntu-latest diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index a8ddef22c86..12633284f49 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -14,6 +14,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: update_release_draft: permissions: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 1b0c3c654e7..9d19028387d 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,6 +12,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: stale: if: github.repository_owner == 'python-pillow' diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 8f24bef3d6a..08226738e11 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -26,6 +26,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index e247414c8fc..808373a65a1 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -28,6 +28,7 @@ concurrency: env: COVERAGE_CORE: sysmon + FORCE_COLOR: 1 jobs: build: diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml index bd244aa5a57..87eace64376 100644 --- a/.github/workflows/test-valgrind-memory.yml +++ b/.github/workflows/test-valgrind-memory.yml @@ -26,6 +26,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 81cfb84566c..f14dab61623 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -24,6 +24,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 3bc70e337a8..45392a689b1 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -28,6 +28,7 @@ concurrency: env: COVERAGE_CORE: sysmon + FORCE_COLOR: 1 jobs: build: From 47386d191ca5b3a82ff7c2fabb3a440db921c48c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Mar 2026 22:33:37 +1100 Subject: [PATCH 2314/2374] Set image pixels individually on 32-bit Windows --- src/libImaging/Paste.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index 25941ab3dbb..f01bce93388 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -352,16 +352,16 @@ ImagingPaste( static inline void fill( - Imaging imOut, const void *ink_, int dx, int dy, int xsize, int ysize, int pixelsize + Imaging imOut, const UINT8 *ink, int dx, int dy, int xsize, int ysize, int pixelsize ) { /* fill opaque region */ - int x, y; + int x, y, i; UINT8 ink8 = 0; INT32 ink32 = 0L; - memcpy(&ink32, ink_, pixelsize); - memcpy(&ink8, ink_, sizeof(ink8)); + memcpy(&ink32, ink, pixelsize); + memcpy(&ink8, ink, sizeof(ink8)); if (imOut->image8 || ink32 == 0L) { dx *= pixelsize; @@ -371,12 +371,24 @@ fill( } } else { +#if defined _WIN32 && !defined _WIN64 + dx *= pixelsize; + for (y = 0; y < ysize; y++) { + UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx; + for (x = 0; x < xsize; x++) { + for (i = 0; i < pixelsize; i++) { + *out++ = ink[i]; + } + } + } +#else for (y = 0; y < ysize; y++) { INT32 *out = imOut->image32[y + dy] + dx; for (x = 0; x < xsize; x++) { out[x] = ink32; } } +#endif } } From 9a89944e735283ce4bbdf2dde1ea7c44798d9683 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:00:18 +0200 Subject: [PATCH 2315/2374] Fix `_getxy` refcount leaks (#9487) --- src/_imaging.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_imaging.c b/src/_imaging.c index d2a195887fa..b8b8df5c2a1 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1216,7 +1216,9 @@ _getxy(PyObject *xy, int *x, int *y) { PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); if (int_value != NULL && PyLong_Check(int_value)) { *x = PyLong_AS_LONG(int_value); + Py_DECREF(int_value); } else { + Py_XDECREF(int_value); goto badval; } } @@ -1230,7 +1232,9 @@ _getxy(PyObject *xy, int *x, int *y) { PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); if (int_value != NULL && PyLong_Check(int_value)) { *y = PyLong_AS_LONG(int_value); + Py_DECREF(int_value); } else { + Py_XDECREF(int_value); goto badval; } } From 93729a006271c5757dc706a67f3077817bdf32b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Mar 2026 23:04:35 +1100 Subject: [PATCH 2316/2374] Removed unused code --- src/encode.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/encode.c b/src/encode.c index f2bb464fa27..b9feb28d227 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1343,8 +1343,6 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { if (strcmp(format, "j2k") == 0) { codec_format = OPJ_CODEC_J2K; - } else if (strcmp(format, "jpt") == 0) { - codec_format = OPJ_CODEC_JPT; } else if (strcmp(format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; } else { From 33d62fc8a1a15f26376bfdd9fc292449a4e0827e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Mar 2026 23:11:59 +1100 Subject: [PATCH 2317/2374] Added error messages --- Tests/test_file_jpeg2k.py | 16 ++++++++++++++++ src/decode.c | 1 + src/encode.c | 3 +++ 3 files changed, 20 insertions(+) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index df686df3222..85851ddad2d 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -148,6 +148,22 @@ def test_prog_res_rt(card: ImageFile.ImageFile) -> None: assert_image_equal(im, card) +def test_unknown_progression(tmp_path: Path) -> None: + outfile = tmp_path / "temp.jp2" + + im = Image.new("1", (1, 1)) + with pytest.raises(ValueError, match="unknown progression"): + im.save(outfile, progression="invalid") + + +def test_unknown_cinema_mode(tmp_path: Path) -> None: + outfile = tmp_path / "temp.jp2" + + im = Image.new("1", (1, 1)) + with pytest.raises(ValueError, match="unknown cinema mode"): + im.save(outfile, cinema_mode="invalid") + + @pytest.mark.parametrize("num_resolutions", range(2, 6)) def test_default_num_resolutions( card: ImageFile.ImageFile, num_resolutions: int diff --git a/src/decode.c b/src/decode.c index c5c9cf56f70..cda4ce7027f 100644 --- a/src/decode.c +++ b/src/decode.c @@ -905,6 +905,7 @@ PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args) { } else if (strcmp(format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; } else { + PyErr_SetString(PyExc_ValueError, "unknown codec format"); return NULL; } diff --git a/src/encode.c b/src/encode.c index b9feb28d227..b268ad74141 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1346,6 +1346,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } else if (strcmp(format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; } else { + PyErr_SetString(PyExc_ValueError, "unknown codec format"); return NULL; } @@ -1360,6 +1361,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } else if (strcmp(progression, "CPRL") == 0) { prog_order = OPJ_CPRL; } else { + PyErr_SetString(PyExc_ValueError, "unknown progression"); return NULL; } @@ -1372,6 +1374,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } else if (strcmp(cinema_mode, "cinema4k-24") == 0) { cine_mode = OPJ_CINEMA4K_24; } else { + PyErr_SetString(PyExc_ValueError, "unknown cinema mode"); return NULL; } From 5b69607c35943f782cf854bb06596c34ca1a3628 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:35:27 +1100 Subject: [PATCH 2318/2374] Skip build 1.4.1 for lint (#9491) Co-authored-by: Andrew Murray Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index de18946efa7..37e2296fc1f 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ commands = [testenv:lint] skip_install = true deps = + build!=1.4.1 # pending https://github.com/pypa/build/pull/1003 check-manifest prek pass_env = From d4f78128abdc2ffd32e10f66024ee331241053ce Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:47:22 +0200 Subject: [PATCH 2319/2374] Revert "Skip build 1.4.1 for lint" (#9495) --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 37e2296fc1f..de18946efa7 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,6 @@ commands = [testenv:lint] skip_install = true deps = - build!=1.4.1 # pending https://github.com/pypa/build/pull/1003 check-manifest prek pass_env = From e4d72b53f5e885338ac55c570055d29e35c7c385 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Wed, 25 Mar 2026 14:50:05 -0400 Subject: [PATCH 2320/2374] Use critical sections to protect FontObject FreeType FT_Face objects are not thread-safe. Use per-object critical sections to protect FontObject methods that access the underlying FT_Face in the free-threaded build. Fixes #9497 --- src/_imagingft.c | 110 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 100 insertions(+), 10 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 3be1bcb9a74..534dd028651 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -518,7 +518,7 @@ text_layout( } static PyObject * -font_getlength(FontObject *self, PyObject *args) { +font_getlength_impl(FontObject *self, PyObject *args) { int length; /* length along primary axis, in 26.6 precision */ GlyphInfo *glyph_info = NULL; /* computed text layout */ size_t i, count; /* glyph_info index and length */ @@ -567,6 +567,15 @@ font_getlength(FontObject *self, PyObject *args) { return PyLong_FromLong(length); } +static PyObject * +font_getlength(FontObject *self, PyObject *args) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_getlength_impl(self, args); + Py_END_CRITICAL_SECTION(); + return result; +} + static int bounding_box_and_anchors( FT_Face face, @@ -746,7 +755,7 @@ bounding_box_and_anchors( } static PyObject * -font_getsize(FontObject *self, PyObject *args) { +font_getsize_impl(FontObject *self, PyObject *args) { int width, height, x_offset, y_offset; int load_flags; /* FreeType load_flags parameter */ int error; @@ -820,7 +829,16 @@ font_getsize(FontObject *self, PyObject *args) { } static PyObject * -font_render(FontObject *self, PyObject *args) { +font_getsize(FontObject *self, PyObject *args) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_getsize_impl(self, args); + Py_END_CRITICAL_SECTION(); + return result; +} + +static PyObject * +font_render_impl(FontObject *self, PyObject *args) { int x, y; /* pen position, in 26.6 precision */ int px, py; /* position of current glyph, in pixels */ int x_min, y_max; /* text offset in 26.6 precision */ @@ -1233,6 +1251,15 @@ font_render(FontObject *self, PyObject *args) { return NULL; } +static PyObject * +font_render(FontObject *self, PyObject *args) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_render_impl(self, args); + Py_END_CRITICAL_SECTION(); + return result; +} + static PyObject * font_getvarnames(FontObject *self) { int error; @@ -1372,7 +1399,7 @@ font_getvaraxes(FontObject *self) { } static PyObject * -font_setvarname(FontObject *self, PyObject *args) { +font_setvarname_impl(FontObject *self, PyObject *args) { int error; int instance_index; @@ -1389,7 +1416,16 @@ font_setvarname(FontObject *self, PyObject *args) { } static PyObject * -font_setvaraxes(FontObject *self, PyObject *args) { +font_setvarname(FontObject *self, PyObject *args) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_setvarname_impl(self, args); + Py_END_CRITICAL_SECTION(); + return result; +} + +static PyObject * +font_setvaraxes_impl(FontObject *self, PyObject *args) { int error; PyObject *axes, *item; @@ -1442,6 +1478,15 @@ font_setvaraxes(FontObject *self, PyObject *args) { Py_RETURN_NONE; } +static PyObject * +font_setvaraxes(FontObject *self, PyObject *args) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_setvaraxes_impl(self, args); + Py_END_CRITICAL_SECTION(); + return result; +} + static void font_dealloc(FontObject *self) { if (self->face) { @@ -1483,30 +1528,75 @@ font_getattr_style(FontObject *self, void *closure) { } static PyObject * -font_getattr_ascent(FontObject *self, void *closure) { +font_getattr_ascent_impl(FontObject *self, void *closure) { return PyLong_FromLong(PIXEL(self->face->size->metrics.ascender)); } static PyObject * -font_getattr_descent(FontObject *self, void *closure) { +font_getattr_ascent(FontObject *self, void *closure) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_getattr_ascent_impl(self, closure); + Py_END_CRITICAL_SECTION(); + return result; +} + +static PyObject * +font_getattr_descent_impl(FontObject *self, void *closure) { return PyLong_FromLong(-PIXEL(self->face->size->metrics.descender)); } static PyObject * -font_getattr_height(FontObject *self, void *closure) { +font_getattr_descent(FontObject *self, void *closure) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_getattr_descent_impl(self, closure); + Py_END_CRITICAL_SECTION(); + return result; +} + +static PyObject * +font_getattr_height_impl(FontObject *self, void *closure) { return PyLong_FromLong(PIXEL(self->face->size->metrics.height)); } static PyObject * -font_getattr_x_ppem(FontObject *self, void *closure) { +font_getattr_height(FontObject *self, void *closure) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_getattr_height_impl(self, closure); + Py_END_CRITICAL_SECTION(); + return result; +} + +static PyObject * +font_getattr_x_ppem_impl(FontObject *self, void *closure) { return PyLong_FromLong(self->face->size->metrics.x_ppem); } static PyObject * -font_getattr_y_ppem(FontObject *self, void *closure) { +font_getattr_x_ppem(FontObject *self, void *closure) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_getattr_x_ppem_impl(self, closure); + Py_END_CRITICAL_SECTION(); + return result; +} + +static PyObject * +font_getattr_y_ppem_impl(FontObject *self, void *closure) { return PyLong_FromLong(self->face->size->metrics.y_ppem); } +static PyObject * +font_getattr_y_ppem(FontObject *self, void *closure) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_getattr_y_ppem_impl(self, closure); + Py_END_CRITICAL_SECTION(); + return result; +} + static PyObject * font_getattr_glyphs(FontObject *self, void *closure) { return PyLong_FromLong(self->face->num_glyphs); From f551ecdc430425846483b4a55bfc2956c8044b44 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Mar 2026 09:57:10 +1100 Subject: [PATCH 2321/2374] If Makernote is truncated, do not raise struct.error --- Tests/test_file_mpo.py | 35 +++++++++- src/PIL/Image.py | 149 +++++++++++++++++++++-------------------- 2 files changed, 110 insertions(+), 74 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 4db62bd6d0a..7f35693f504 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -6,7 +6,14 @@ import pytest -from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin +from PIL import ( + Image, + ImageFile, + JpegImagePlugin, + MpoImagePlugin, + TiffImagePlugin, + _binary, +) from .helper import ( assert_image_equal, @@ -145,6 +152,32 @@ def test_parallax() -> None: assert exif.get_ifd(0x927C)[0xB211] == -3.125 +def test_truncated_makernote() -> None: + def check(ifd: TiffImagePlugin.ImageFileDirectory_v2) -> None: + fp = BytesIO() + ifd.save(fp) + + e = Image.Exif() + e.load(fp.getvalue()) + assert e.get_ifd(37500) == {} + + # Nintendo + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[271] = "Nintendo" + ifd[34665] = {37500: b" "} + check(ifd) + + # Fujifilm + for data in ( + b"FUJIFILM", + b"FUJIFILM" + _binary.o32le(50), + b"FUJIFILM" + _binary.o32le(0), + ): + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[34665] = {37500: data} + check(ifd) + + def test_reload_exif_after_seek() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: exif = im.getexif() diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f154cda2be8..bde335504e8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -4234,80 +4234,83 @@ def get_ifd(self, tag: int) -> dict[int, Any]: if tag == ExifTags.IFD.MakerNote: from .TiffImagePlugin import ImageFileDirectory_v2 - if tag_data.startswith(b"FUJIFILM"): - ifd_offset = i32le(tag_data, 8) - ifd_data = tag_data[ifd_offset:] - - makernote = {} - for i in range(struct.unpack(" 4: - (offset,) = struct.unpack(" 4: + (offset,) = struct.unpack("H", tag_data[:2])[0]): + ifd_tag, typ, count, data = struct.unpack( + ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] ) - continue - - if not data: - continue - - makernote[ifd_tag] = handler( - ImageFileDirectory_v2(), data, False - ) - self._ifds[tag] = dict(self._fixup_dict(makernote)) - elif self.get(0x010F) == "Nintendo": - makernote = {} - for i in range(struct.unpack(">H", tag_data[:2])[0]): - ifd_tag, typ, count, data = struct.unpack( - ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] - ) - if ifd_tag == 0x1101: - # CameraInfo - (offset,) = struct.unpack(">L", data) - self.fp.seek(offset) - - camerainfo: dict[str, int | bytes] = { - "ModelID": self.fp.read(4) - } - - self.fp.read(4) - # Seconds since 2000 - camerainfo["TimeStamp"] = i32le(self.fp.read(12)) - - self.fp.read(4) - camerainfo["InternalSerialNumber"] = self.fp.read(4) - - self.fp.read(12) - parallax = self.fp.read(4) - handler = ImageFileDirectory_v2._load_dispatch[ - TiffTags.FLOAT - ][1] - camerainfo["Parallax"] = handler( - ImageFileDirectory_v2(), parallax, False - )[0] - - self.fp.read(4) - camerainfo["Category"] = self.fp.read(2) - - makernote = {0x1101: camerainfo} - self._ifds[tag] = makernote + if ifd_tag == 0x1101: + # CameraInfo + (offset,) = struct.unpack(">L", data) + self.fp.seek(offset) + + camerainfo: dict[str, int | bytes] = { + "ModelID": self.fp.read(4) + } + + self.fp.read(4) + # Seconds since 2000 + camerainfo["TimeStamp"] = i32le(self.fp.read(12)) + + self.fp.read(4) + camerainfo["InternalSerialNumber"] = self.fp.read(4) + + self.fp.read(12) + parallax = self.fp.read(4) + handler = ImageFileDirectory_v2._load_dispatch[ + TiffTags.FLOAT + ][1] + camerainfo["Parallax"] = handler( + ImageFileDirectory_v2(), parallax, False + )[0] + + self.fp.read(4) + camerainfo["Category"] = self.fp.read(2) + + makernote = {0x1101: camerainfo} + self._ifds[tag] = makernote + except struct.error: + pass else: # Interop ifd = self._get_ifd_dict(tag_data, tag) From 67c0767b6487811f04498017f8c85fe45c510661 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Mar 2026 09:53:27 +1100 Subject: [PATCH 2322/2374] If Photoshop blocks are truncated, do not raise struct.error --- Tests/test_file_tiff.py | 10 ++++++++++ src/PIL/TiffImagePlugin.py | 7 +++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index c6c8467d629..e442471d1ca 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -16,6 +16,7 @@ TiffImagePlugin, TiffTags, UnidentifiedImageError, + _binary, ) from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION @@ -941,6 +942,15 @@ def test_get_photoshop_blocks(self) -> None: 4001, ] + def test_truncated_photoshop_blocks(self) -> None: + with Image.open("Tests/images/hopper.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + im.tag_v2[34377] = b"8BIM" + assert im.get_photoshop_blocks() == {} + + im.tag_v2[34377] = b"8BIM" + _binary.o16be(0) + _binary.o8(2) + b" " * 5 + assert im.get_photoshop_blocks() == {} + def test_tiff_chunks(self, tmp_path: Path) -> None: tmpfile = tmp_path / "temp.tif" diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index de2ce066ebf..3eec94dca57 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1287,10 +1287,13 @@ def get_photoshop_blocks(self) -> dict[int, dict[str, bytes]]: blocks = {} val = self.tag_v2.get(ExifTags.Base.ImageResources) if val: - while val.startswith(b"8BIM"): + while val.startswith(b"8BIM") and len(val) >= 12: id = i16(val[4:6]) n = math.ceil((val[6] + 1) / 2) * 2 - size = i32(val[6 + n : 10 + n]) + try: + size = i32(val[6 + n : 10 + n]) + except struct.error: + break data = val[10 + n : 10 + n + size] blocks[id] = {"data": data} From da729c832c26facdcc3afac52417cecc7641f232 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:43:32 +1100 Subject: [PATCH 2323/2374] Check if PyObject_CallMethod result is NULL (#9494) --- src/libImaging/SgiRleDecode.c | 13 ++++++++++--- src/libImaging/codec_fd.c | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index a562f582cb0..2f5268b80b8 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -175,8 +175,15 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t /* Get all data from File descriptor */ c = (SGISTATE *)state->context; - _imaging_seek_pyFd(state->fd, 0L, SEEK_END); + if (_imaging_seek_pyFd(state->fd, 0L, SEEK_END) == -1) { + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } c->bufsize = _imaging_tell_pyFd(state->fd); + if (c->bufsize == -1) { + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } c->bufsize -= SGI_HEADER_SIZE; c->tablen = im->bands * im->ysize; @@ -194,8 +201,8 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t state->errcode = IMAGING_CODEC_MEMORY; return -1; } - _imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET); - if (_imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) { + if (_imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET) == -1 || + _imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) { free(ptr); state->errcode = IMAGING_CODEC_UNKNOWN; return -1; diff --git a/src/libImaging/codec_fd.c b/src/libImaging/codec_fd.c index 5261681107b..dc85772983c 100644 --- a/src/libImaging/codec_fd.c +++ b/src/libImaging/codec_fd.c @@ -12,6 +12,9 @@ _imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes) { int bytes_result; result = PyObject_CallMethod(fd, "read", "n", bytes); + if (result == NULL) { + goto err; + } bytes_result = PyBytes_AsStringAndSize(result, &buffer, &length); if (bytes_result == -1) { @@ -28,7 +31,7 @@ _imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes) { return length; err: - Py_DECREF(result); + Py_XDECREF(result); return -1; } @@ -41,6 +44,10 @@ _imaging_write_pyFd(PyObject *fd, char *src, Py_ssize_t bytes) { result = PyObject_CallMethod(fd, "write", "O", byteObj); Py_DECREF(byteObj); + if (result == NULL) { + return -1; + } + Py_DECREF(result); return bytes; @@ -51,6 +58,9 @@ _imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence) { PyObject *result; result = PyObject_CallMethod(fd, "seek", "ni", offset, whence); + if (result == NULL) { + return -1; + } Py_DECREF(result); return 0; @@ -62,6 +72,9 @@ _imaging_tell_pyFd(PyObject *fd) { Py_ssize_t location; result = PyObject_CallMethod(fd, "tell", NULL); + if (result == NULL) { + return -1; + } location = PyLong_AsSsize_t(result); Py_DECREF(result); From d305ee6a258a1c97b9187160553410c5610b75c4 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:45:02 +1100 Subject: [PATCH 2324/2374] Check PyType_Ready return values (#9502) --- src/_imagingcms.c | 5 +++-- src/_imagingft.c | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index ad3b27896fb..7db1baef0fb 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1447,8 +1447,9 @@ setup_module(PyObject *m) { int vn; /* Ready object types */ - PyType_Ready(&CmsProfile_Type); - PyType_Ready(&CmsTransform_Type); + if (PyType_Ready(&CmsProfile_Type) < 0 || PyType_Ready(&CmsTransform_Type) < 0) { + return -1; + } Py_INCREF(&CmsProfile_Type); PyModule_AddObject(m, "CmsProfile", (PyObject *)&CmsProfile_Type); diff --git a/src/_imagingft.c b/src/_imagingft.c index 3be1bcb9a74..8395eee2c0e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1545,7 +1545,9 @@ setup_module(PyObject *m) { d = PyModule_GetDict(m); /* Ready object type */ - PyType_Ready(&Font_Type); + if (PyType_Ready(&Font_Type) < 0) { + return -1; + } if (FT_Init_FreeType(&library)) { return 0; /* leave it uninitialized */ From fcecc8c6c4b16a8dbb1b02e97be1f8d2f7d60965 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:45:40 +1100 Subject: [PATCH 2325/2374] Fixed AVIF and WEBP dealloc (#9501) --- src/_avif.c | 8 ++++---- src/_webp.c | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/_avif.c b/src/_avif.c index 5e8b9fe8e93..a9ae89f233b 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -425,7 +425,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { return (PyObject *)self; } -PyObject * +void _encoder_dealloc(AvifEncoderObject *self) { if (self->encoder) { avifEncoderDestroy(self->encoder); @@ -433,7 +433,7 @@ _encoder_dealloc(AvifEncoderObject *self) { if (self->image) { avifImageDestroy(self->image); } - Py_RETURN_NONE; + Py_TYPE(self)->tp_free(self); } PyObject * @@ -687,13 +687,13 @@ AvifDecoderNew(PyObject *self_, PyObject *args) { return (PyObject *)self; } -PyObject * +void _decoder_dealloc(AvifDecoderObject *self) { if (self->decoder) { avifDecoderDestroy(self->decoder); } PyBuffer_Release(&self->buffer); - Py_RETURN_NONE; + Py_TYPE(self)->tp_free(self); } PyObject * diff --git a/src/_webp.c b/src/_webp.c index d065e329c6b..ea7e77133d4 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -219,6 +219,7 @@ _anim_encoder_dealloc(PyObject *self) { WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; WebPPictureFree(&(encp->frame)); WebPAnimEncoderDelete(encp->enc); + Py_TYPE(self)->tp_free(self); } PyObject * @@ -441,6 +442,7 @@ _anim_decoder_dealloc(PyObject *self) { WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; WebPDataClear(&(decp->data)); WebPAnimDecoderDelete(decp->dec); + Py_TYPE(self)->tp_free(self); } PyObject * From 92ccedea87cfbee4c40aac2a71ce3c1ede715c32 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:46:33 +1100 Subject: [PATCH 2326/2374] Release reference to encoder on error (#9500) --- src/encode.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/encode.c b/src/encode.c index b268ad74141..1fc31404d9e 100644 --- a/src/encode.c +++ b/src/encode.c @@ -727,6 +727,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { const RawModeID rawmode = findRawModeID(rawmode_name); if (get_packer(encoder, mode, rawmode) < 0) { + Py_DECREF(encoder); return NULL; } @@ -742,6 +743,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { for (pos = 0; pos < tags_size; pos++) { item = PyList_GetItemRef(tags, pos); if (item == NULL) { + Py_DECREF(encoder); return NULL; } @@ -766,6 +768,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { if (!is_core_tag) { PyObject *tag_type; if (PyDict_GetItemRef(types, key, &tag_type) < 0) { + Py_DECREF(encoder); return NULL; // Exception has been already set } if (tag_type) { @@ -837,6 +840,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { if (key_int == TIFFTAG_COLORMAP) { int stride = 256; if (len != 768) { + Py_DECREF(encoder); PyErr_SetString( PyExc_ValueError, "Requiring 768 items for Colormap" ); From 9b7dccfe32dfa620afa54050e83d673903ce81e4 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:47:58 +1100 Subject: [PATCH 2327/2374] Use PyModule_AddObjectRef (#9503) --- src/_imaging.c | 25 +++++++++++++++---------- src/_imagingcms.c | 9 ++++----- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index b8b8df5c2a1..697fe0ce5d0 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4324,8 +4324,9 @@ setup_module(PyObject *m) { #else have_libjpegturbo = Py_False; #endif - Py_INCREF(have_libjpegturbo); - PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo); + if (PyModule_AddObjectRef(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo) < 0) { + return -1; + } PyObject *have_mozjpeg; #ifdef JPEG_C_PARAM_SUPPORTED @@ -4333,8 +4334,9 @@ setup_module(PyObject *m) { #else have_mozjpeg = Py_False; #endif - Py_INCREF(have_mozjpeg); - PyModule_AddObject(m, "HAVE_MOZJPEG", have_mozjpeg); + if (PyModule_AddObjectRef(m, "HAVE_MOZJPEG", have_mozjpeg) < 0) { + return -1; + } PyObject *have_libimagequant; #ifdef HAVE_LIBIMAGEQUANT @@ -4348,8 +4350,9 @@ setup_module(PyObject *m) { #else have_libimagequant = Py_False; #endif - Py_INCREF(have_libimagequant); - PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", have_libimagequant); + if (PyModule_AddObjectRef(m, "HAVE_LIBIMAGEQUANT", have_libimagequant) < 0) { + return -1; + } #ifdef HAVE_LIBZ /* zip encoding strategies */ @@ -4377,8 +4380,9 @@ setup_module(PyObject *m) { #else have_zlibng = Py_False; #endif - Py_INCREF(have_zlibng); - PyModule_AddObject(m, "HAVE_ZLIBNG", have_zlibng); + if (PyModule_AddObjectRef(m, "HAVE_ZLIBNG", have_zlibng) < 0) { + return -1; + } #ifdef HAVE_LIBTIFF { @@ -4395,8 +4399,9 @@ setup_module(PyObject *m) { #else have_xcb = Py_False; #endif - Py_INCREF(have_xcb); - PyModule_AddObject(m, "HAVE_XCB", have_xcb); + if (PyModule_AddObjectRef(m, "HAVE_XCB", have_xcb) < 0) { + return -1; + } PyObject *pillow_version = PyUnicode_FromString(version); PyDict_SetItemString( diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 7db1baef0fb..7b4fb00a470 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1451,11 +1451,10 @@ setup_module(PyObject *m) { return -1; } - Py_INCREF(&CmsProfile_Type); - PyModule_AddObject(m, "CmsProfile", (PyObject *)&CmsProfile_Type); - - Py_INCREF(&CmsTransform_Type); - PyModule_AddObject(m, "CmsTransform", (PyObject *)&CmsTransform_Type); + if (PyModule_AddObjectRef(m, "CmsProfile", (PyObject *)&CmsProfile_Type) < 0 || + PyModule_AddObjectRef(m, "CmsTransform", (PyObject *)&CmsTransform_Type) < 0) { + return -1; + } d = PyModule_GetDict(m); From f176f5dad642f16663da4c587a85aa671f00abba Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:57:45 +1100 Subject: [PATCH 2328/2374] Update libpng to 1.6.56 (#9499) --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index fd5b598703e..107eeae9b95 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -92,7 +92,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds. FREETYPE_VERSION=2.14.3 HARFBUZZ_VERSION=13.2.1 -LIBPNG_VERSION=1.6.55 +LIBPNG_VERSION=1.6.56 JPEGTURBO_VERSION=3.1.3 OPENJPEG_VERSION=2.5.4 XZ_VERSION=5.8.2 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b82885e8efa..d958a459277 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -121,7 +121,7 @@ def cmd_msbuild( "LCMS2": "2.18", "LIBAVIF": "1.4.1", "LIBIMAGEQUANT": "4.4.1", - "LIBPNG": "1.6.55", + "LIBPNG": "1.6.56", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.4", "TIFF": "4.7.1", From ef6951d1a5b7c0cb789a56cae16f06e8260a4587 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:57:43 +0200 Subject: [PATCH 2329/2374] CI: Retry failed downloads (#9506) --- depends/download-and-extract.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index 04bfbc7556b..52010475376 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -5,7 +5,10 @@ archive=$1 url=$2 if [ ! -f $archive.tar.gz ]; then - wget --no-verbose -O $archive.tar.gz $url + wget -O $archive.tar.gz $url \ + --no-verbose \ + --retry-connrefused \ + --retry-on-http-error=429,503,504 fi rmdir $archive From 7672b19af4107748301187dae539c38d5126fa34 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 27 Mar 2026 04:23:01 +0000 Subject: [PATCH 2330/2374] Fix missing null dereference checks (#9489) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/_avif.c | 16 ++++++++++++++++ src/_imaging.c | 6 ++++++ src/_imagingft.c | 7 ++++++- src/_imagingmorph.c | 11 +++++++++++ src/_webp.c | 3 +++ 5 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/_avif.c b/src/_avif.c index a9ae89f233b..1fe0cb98676 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -708,15 +708,27 @@ _decoder_get_info(AvifDecoderObject *self) { if (image->xmp.size) { xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size); + if (!xmp) { + return NULL; + } } if (image->exif.size) { exif = PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size); + if (!exif) { + Py_XDECREF(xmp); + return NULL; + } } if (image->icc.size) { icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size); + if (!icc) { + Py_XDECREF(xmp); + Py_XDECREF(exif); + return NULL; + } } ret = Py_BuildValue( @@ -799,6 +811,7 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) { if (rgb.height > PY_SSIZE_T_MAX / rgb.rowBytes) { PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size"); + avifRGBImageFreePixels(&rgb); return NULL; } @@ -806,6 +819,9 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) { bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size); avifRGBImageFreePixels(&rgb); + if (!bytes) { + return NULL; + } ret = Py_BuildValue( "SKKK", diff --git a/src/_imaging.c b/src/_imaging.c index 697fe0ce5d0..8ec9b14cd6c 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2473,6 +2473,9 @@ _split(ImagingObject *self) { } list = PyTuple_New(self->image->bands); + if (!list) { + return NULL; + } for (i = 0; i < self->image->bands; i++) { imaging_object = PyImagingNew(bands[i]); if (!imaging_object) { @@ -3769,6 +3772,9 @@ _ptr_destructor(PyObject *capsule) { static PyObject * _getattr_ptr(ImagingObject *self, void *closure) { PyObject *capsule = PyCapsule_New(self->image, IMAGING_MAGIC, _ptr_destructor); + if (!capsule) { + return NULL; + } Py_INCREF(self); PyCapsule_SetContext(capsule, self); return capsule; diff --git a/src/_imagingft.c b/src/_imagingft.c index 8395eee2c0e..1ac9d95ee38 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -941,8 +941,13 @@ font_render(FontObject *self, PyObject *args) { return NULL; } PyObject *imagePtr = PyObject_GetAttrString(image, "ptr"); + if (!imagePtr) { + PyMem_Del(glyph_info); + Py_DECREF(image); + return NULL; + } im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC); - Py_XDECREF(imagePtr); + Py_DECREF(imagePtr); x_offset = round(x_offset - stroke_width); y_offset = round(y_offset - stroke_width); diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 5995f9d429e..c1b772760fa 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -143,6 +143,9 @@ match(PyObject *self, PyObject *args) { } imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!imgin) { + return NULL; + } if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); @@ -185,6 +188,10 @@ match(PyObject *self, PyObject *args) { (b6 << 6) | (b7 << 7) | (b8 << 8)); if (lut[lut_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); + if (!coordObj) { + Py_DECREF(ret); + return NULL; + } PyList_Append(ret, coordObj); Py_XDECREF(coordObj); } @@ -230,6 +237,10 @@ get_on_pixels(PyObject *self, PyObject *args) { for (col_idx = 0; col_idx < width; col_idx++) { if (row[col_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); + if (!coordObj) { + Py_DECREF(ret); + return NULL; + } PyList_Append(ret, coordObj); Py_XDECREF(coordObj); } diff --git a/src/_webp.c b/src/_webp.c index ea7e77133d4..a936e13d8c4 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -505,6 +505,9 @@ _anim_decoder_get_next(PyObject *self) { bytes = PyBytes_FromStringAndSize( (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height ); + if (!bytes) { + return NULL; + } ret = Py_BuildValue("Si", bytes, timestamp); From 40400edd6229e9c26c22b0d3d4bcfa1cc7ec5cb0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Mar 2026 15:26:25 +1100 Subject: [PATCH 2331/2374] Check PyCapsule_GetPointer return value --- src/_imaging.c | 6 ++++++ src/_imagingcms.c | 6 ++++++ src/_imagingft.c | 5 +++++ src/_imagingmath.c | 21 +++++++++++++++++++++ src/_imagingmorph.c | 9 +++++++++ src/_webp.c | 6 ++++++ src/libImaging/Storage.c | 6 ++++++ 7 files changed, 59 insertions(+) diff --git a/src/_imaging.c b/src/_imaging.c index 8ec9b14cd6c..ac0317f781f 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -254,6 +254,9 @@ void ReleaseArrowSchemaPyCapsule(PyObject *capsule) { struct ArrowSchema *schema = (struct ArrowSchema *)PyCapsule_GetPointer(capsule, "arrow_schema"); + if (!schema) { + return; + } if (schema->release != NULL) { schema->release(schema); } @@ -276,6 +279,9 @@ void ReleaseArrowArrayPyCapsule(PyObject *capsule) { struct ArrowArray *array = (struct ArrowArray *)PyCapsule_GetPointer(capsule, "arrow_array"); + if (!array) { + return; + } if (array->release != NULL) { array->release(array); } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 7b4fb00a470..469be05f4a3 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -558,7 +558,13 @@ cms_transform_apply(CmsTransformObject *self, PyObject *args) { } im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!im) { + return NULL; + } imOut = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!imOut) { + return NULL; + } return Py_BuildValue("i", pyCMSdoTransform(im, imOut, self->transform)); } diff --git a/src/_imagingft.c b/src/_imagingft.c index 1ac9d95ee38..5d91eaad6c3 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -948,6 +948,11 @@ font_render(FontObject *self, PyObject *args) { } im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC); Py_DECREF(imagePtr); + if (!im) { + PyMem_Del(glyph_info); + Py_DECREF(image); + return NULL; + } x_offset = round(x_offset - stroke_width); y_offset = round(y_offset - stroke_width); diff --git a/src/_imagingmath.c b/src/_imagingmath.c index 48c3959003e..c04361468ed 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -187,8 +187,17 @@ _unop(PyObject *self, PyObject *args) { } unop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_UNOP_MAGIC); + if (!unop) { + return NULL; + } out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!out) { + return NULL; + } im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!im1) { + return NULL; + } unop(out, im1); @@ -219,9 +228,21 @@ _binop(PyObject *self, PyObject *args) { } binop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_BINOP_MAGIC); + if (!binop) { + return NULL; + } out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!out) { + return NULL; + } im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!im1) { + return NULL; + } im2 = (Imaging)PyCapsule_GetPointer(i2, IMAGING_MAGIC); + if (!im2) { + return NULL; + } binop(out, im1, im2); diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index c1b772760fa..b6f307c849e 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -53,7 +53,13 @@ apply(PyObject *self, PyObject *args) { } imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!imgin) { + return NULL; + } imgout = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!imgout) { + return NULL; + } width = imgin->xsize; height = imgin->ysize; @@ -223,6 +229,9 @@ get_on_pixels(PyObject *self, PyObject *args) { } img = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!img) { + return NULL; + } rows = img->image8; width = img->xsize; height = img->ysize; diff --git a/src/_webp.c b/src/_webp.c index a936e13d8c4..115141273f5 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -262,6 +262,9 @@ _anim_encoder_add(PyObject *self, PyObject *args) { } im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!im) { + return NULL; + } // Setup config for this frame if (!WebPConfigInit(&config)) { @@ -610,6 +613,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { } im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!im) { + return NULL; + } // Setup config for this frame if (!WebPConfigInit(&config)) { diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index c09062c92e5..c8175612e35 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -677,9 +677,15 @@ ImagingNewArrow( Imaging im; struct ArrowSchema *schema = (struct ArrowSchema *)PyCapsule_GetPointer(schema_capsule, "arrow_schema"); + if (!schema) { + return NULL; + } struct ArrowArray *external_array = (struct ArrowArray *)PyCapsule_GetPointer(array_capsule, "arrow_array"); + if (!external_array) { + return NULL; + } if (xsize < 0 || ysize < 0) { return (Imaging)ImagingError_ValueError("bad image size"); From 20a9401971c46961f49091979b7c65dac0fae202 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Mar 2026 21:21:22 +1100 Subject: [PATCH 2332/2374] Check PyBytes_FromStringAndSize return value --- src/display.c | 3 +++ src/libImaging/codec_fd.c | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/display.c b/src/display.c index 5b5853a3cb8..944c60b704a 100644 --- a/src/display.c +++ b/src/display.c @@ -480,6 +480,9 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { GlobalUnlock(handle); CloseClipboard(); + if (!result) { + return NULL; + } return Py_BuildValue("zN", format_names[format], result); } diff --git a/src/libImaging/codec_fd.c b/src/libImaging/codec_fd.c index dc85772983c..c5614e6039b 100644 --- a/src/libImaging/codec_fd.c +++ b/src/libImaging/codec_fd.c @@ -41,6 +41,9 @@ _imaging_write_pyFd(PyObject *fd, char *src, Py_ssize_t bytes) { PyObject *byteObj; byteObj = PyBytes_FromStringAndSize(src, bytes); + if (!byteObj) { + return -1; + } result = PyObject_CallMethod(fd, "write", "O", byteObj); Py_DECREF(byteObj); From 27de86483d8c23d9375b071993c04c2ff8388ca3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Mar 2026 21:54:45 +1100 Subject: [PATCH 2333/2374] Switch iOS back to macos-15-intel --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index ed49f15c0b8..af2f9b3e825 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -104,7 +104,7 @@ jobs: cibw_arch: arm64_iphonesimulator - name: "iOS x86_64 simulator" platform: ios - os: macos-26-intel + os: macos-15-intel cibw_arch: x86_64_iphonesimulator steps: - uses: actions/checkout@v6 From b337b33564da0b21d244b46c2b3e954ae6afc099 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:45:59 +0200 Subject: [PATCH 2334/2374] PERF101 --- pyproject.toml | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6d9910ca140..4190f091ad0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,21 +139,22 @@ exclude = "wheels/multibuild" exclude = [ "wheels/multibuild" ] fix = true lint.select = [ - "C4", # flake8-comprehensions - "E", # pycodestyle errors - "EM", # flake8-errmsg - "F", # pyflakes errors - "I", # isort - "ISC", # flake8-implicit-str-concat - "LOG", # flake8-logging - "PGH", # pygrep-hooks - "PIE", # flake8-pie - "PT", # flake8-pytest-style - "PYI", # flake8-pyi - "RUF100", # unused noqa (yesqa) - "UP", # pyupgrade - "W", # pycodestyle warnings - "YTT", # flake8-2020 + "C4", # flake8-comprehensions + "E", # pycodestyle errors + "EM", # flake8-errmsg + "F", # pyflakes errors + "I", # isort + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "PERF101", # perflint: unnecessary-list-cast + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PT", # flake8-pytest-style + "PYI", # flake8-pyi + "RUF100", # unused noqa (yesqa) + "UP", # pyupgrade + "W", # pycodestyle warnings + "YTT", # flake8-2020 ] lint.ignore = [ "E203", # Whitespace before ':' From 624fc87d2d91f9bd763f3cd721f999f58a6752bb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:46:16 +0200 Subject: [PATCH 2335/2374] PERF102 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 4190f091ad0..7178cb2deca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,6 +147,7 @@ lint.select = [ "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging "PERF101", # perflint: unnecessary-list-cast + "PERF102", # perflint: incorrect-dict-iterator "PGH", # pygrep-hooks "PIE", # flake8-pie "PT", # flake8-pytest-style From b85b8534d7ca1132cfe4067ae2963c26f07b7811 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:11:12 +0200 Subject: [PATCH 2336/2374] PERF401 and fixes --- pyproject.toml | 1 + setup.py | 8 ++++---- src/PIL/IcnsImagePlugin.py | 3 +-- src/PIL/ImageDraw.py | 4 +--- src/PIL/ImageFile.py | 6 ++++-- winbuild/build_prepare.py | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7178cb2deca..ea43e9cb1cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,6 +148,7 @@ lint.select = [ "LOG", # flake8-logging "PERF101", # perflint: unnecessary-list-cast "PERF102", # perflint: incorrect-dict-iterator + "PERF401", # perflint: manual-list-comprehension "PGH", # pygrep-hooks "PIE", # flake8-pie "PT", # flake8-pytest-style diff --git a/setup.py b/setup.py index 3d975950b6c..175aed25a02 100644 --- a/setup.py +++ b/setup.py @@ -1078,10 +1078,10 @@ def debug_build() -> bool: ] files: list[str | os.PathLike[str]] = ["src/_imaging.c"] -for src_file in _IMAGING: - files.append("src/" + src_file + ".c") -for src_file in _LIB_IMAGING: - files.append(os.path.join("src/libImaging", src_file + ".c")) +files.extend("src/" + src_file + ".c" for src_file in _IMAGING) +files.extend( + os.path.join("src/libImaging", src_file + ".c") for src_file in _LIB_IMAGING +) ext_modules = [ Extension("PIL._imaging", files), Extension("PIL._imagingft", ["src/_imagingft.c"]), diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 023835fb71e..cb7a74c2e3b 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -80,8 +80,7 @@ def read_32( if byte_int & 0x80: blocksize = byte_int - 125 byte = fobj.read(1) - for i in range(blocksize): - data.append(byte) + data.extend([byte] * blocksize) else: blocksize = byte_int + 1 data.append(fobj.read(blocksize)) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index eb108ac41ca..506bb3b43b0 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -597,9 +597,7 @@ def draw_text(ink: int, stroke_width: float = 0) -> None: mode = self.fontmode if stroke_width == 0 and embedded_color: mode = "RGBA" - coord = [] - for i in range(2): - coord.append(int(xy[i])) + coord = [int(xy[i]) for i in range(2)] start = (math.modf(xy[0])[0], math.modf(xy[1])[0]) try: mask, offset = image_text.font.getmask2( # type: ignore[union-attr,misc] diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 50e0075a29d..df2a82b7366 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -215,8 +215,10 @@ def get_child_images(self) -> list[ImageFile]: if subifd_offsets: if not isinstance(subifd_offsets, tuple): subifd_offsets = (subifd_offsets,) - for subifd_offset in subifd_offsets: - ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) + ifds = [ + (exif._get_ifd_dict(subifd_offset), subifd_offset) + for subifd_offset in subifd_offsets + ] ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset): assert exif._info is not None diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d958a459277..1438827cabb 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -544,11 +544,11 @@ def write_script( def get_footer(dep: dict[str, Any]) -> list[str]: lines = [] for out in dep.get("headers", []): - lines.append(cmd_copy(out, "{inc_dir}")) + lines.append(cmd_copy(out, "{inc_dir}")) # noqa: PERF401 for out in dep.get("libs", []): - lines.append(cmd_copy(out, "{lib_dir}")) + lines.append(cmd_copy(out, "{lib_dir}")) # noqa: PERF401 for out in dep.get("bins", []): - lines.append(cmd_copy(out, "{bin_dir}")) + lines.append(cmd_copy(out, "{bin_dir}")) # noqa: PERF401 return lines From 9a358fa289e87b849d08ead61a6dccabf5961121 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:12:32 +0200 Subject: [PATCH 2337/2374] PERF402 and fixes --- Tests/test_file_container.py | 4 +--- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 597ab508342..c73f2a40cba 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -179,9 +179,7 @@ def test_iter(bytesmode: bool) -> None: container = ContainerIO.ContainerIO(fh, 0, 120) # Act - data = [] - for line in container: - data.append(line) + data = list(container) # Assert if bytesmode: diff --git a/pyproject.toml b/pyproject.toml index ea43e9cb1cc..149dbbac5dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,6 +149,7 @@ lint.select = [ "PERF101", # perflint: unnecessary-list-cast "PERF102", # perflint: incorrect-dict-iterator "PERF401", # perflint: manual-list-comprehension + "PERF402", # perflint: manual-list-copy "PGH", # pygrep-hooks "PIE", # flake8-pie "PT", # flake8-pytest-style From 090ca9461b1ce0e0f91644f6cc7a1d1416e7b915 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:20:06 +0200 Subject: [PATCH 2338/2374] PERF403 and fixes --- pyproject.toml | 1 + src/PIL/IptcImagePlugin.py | 11 ++--------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 149dbbac5dd..bda99c3bf43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,6 +150,7 @@ lint.select = [ "PERF102", # perflint: incorrect-dict-iterator "PERF401", # perflint: manual-list-comprehension "PERF402", # perflint: manual-list-copy + "PERF403", # perflint: manual-dict-comprehension "PGH", # pygrep-hooks "PIE", # flake8-pie "PT", # flake8-pytest-style diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 6fc824e4caa..9c8be8b4e36 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -185,13 +185,9 @@ def getiptcinfo( data = None - info: dict[tuple[int, int], bytes | list[bytes]] = {} if isinstance(im, IptcImageFile): # return info dictionary right away - for k, v in im.info.items(): - if isinstance(k, tuple): - info[k] = v - return info + return {k: v for k, v in im.info.items() if isinstance(k, tuple)} elif isinstance(im, JpegImagePlugin.JpegImageFile): # extract the IPTC/NAA resource @@ -227,7 +223,4 @@ class FakeImage: except (IndexError, KeyError): pass # expected failure - for k, v in iptc_im.info.items(): - if isinstance(k, tuple): - info[k] = v - return info + return {k: v for k, v in iptc_im.info.items() if isinstance(k, tuple)} From 754c7ea3a0aa47429809a1675f249263de3eac7b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:28:11 +0200 Subject: [PATCH 2339/2374] PERF203 and fixes --- Tests/test_bmp_reference.py | 4 ++-- Tests/test_file_libtiff.py | 5 +---- pyproject.toml | 36 ++++++++++++++++-------------------- setup.py | 2 +- src/PIL/GifImagePlugin.py | 2 +- src/PIL/Image.py | 2 +- src/PIL/ImageFont.py | 2 +- src/PIL/ImagePalette.py | 6 ++---- src/PIL/JpegImagePlugin.py | 4 ++-- src/PIL/PcfFontFile.py | 2 +- src/PIL/PngImagePlugin.py | 2 +- 11 files changed, 29 insertions(+), 38 deletions(-) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 8fbd737484d..ea0853100cc 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -56,7 +56,7 @@ def test_questionable() -> None: im.load() if os.path.basename(f) not in supported: print(f"Please add {f} to the partially supported bmp specs.") - except Exception: # as msg: + except Exception: # noqa: PERF203 if os.path.basename(f) in supported: raise @@ -106,7 +106,7 @@ def get_compare(f: str) -> str: assert_image_similar(im_converted, compare_converted, 5) - except Exception as msg: + except Exception as msg: # noqa: PERF203 # there are three here that are unsupported: unsupported = ( os.path.join(base, "g", "rgb32bf.bmp"), diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index b453e3aa5c5..6f20900e490 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -224,10 +224,7 @@ def test_additional_metadata( with Image.open("Tests/images/hopper_g4.tif") as im: assert isinstance(im, TiffImagePlugin.TiffImageFile) for tag in im.tag_v2: - try: - del core_items[tag] - except KeyError: - pass + core_items.pop(tag, None) del core_items[320] # colormap is special, tested below # Type codes: diff --git a/pyproject.toml b/pyproject.toml index bda99c3bf43..7eb9a3fbdde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,26 +139,22 @@ exclude = "wheels/multibuild" exclude = [ "wheels/multibuild" ] fix = true lint.select = [ - "C4", # flake8-comprehensions - "E", # pycodestyle errors - "EM", # flake8-errmsg - "F", # pyflakes errors - "I", # isort - "ISC", # flake8-implicit-str-concat - "LOG", # flake8-logging - "PERF101", # perflint: unnecessary-list-cast - "PERF102", # perflint: incorrect-dict-iterator - "PERF401", # perflint: manual-list-comprehension - "PERF402", # perflint: manual-list-copy - "PERF403", # perflint: manual-dict-comprehension - "PGH", # pygrep-hooks - "PIE", # flake8-pie - "PT", # flake8-pytest-style - "PYI", # flake8-pyi - "RUF100", # unused noqa (yesqa) - "UP", # pyupgrade - "W", # pycodestyle warnings - "YTT", # flake8-2020 + "C4", # flake8-comprehensions + "E", # pycodestyle errors + "EM", # flake8-errmsg + "F", # pyflakes errors + "I", # isort + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "PERF", # perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PT", # flake8-pytest-style + "PYI", # flake8-pyi + "RUF100", # unused noqa (yesqa) + "UP", # pyupgrade + "W", # pycodestyle warnings + "YTT", # flake8-2020 ] lint.ignore = [ "E203", # Whitespace before ':' diff --git a/setup.py b/setup.py index 175aed25a02..496c8cb1f0e 100644 --- a/setup.py +++ b/setup.py @@ -302,7 +302,7 @@ def _pkg_config(name: str) -> tuple[list[str], list[str]] | None: subprocess.check_output(command_cflags).decode("utf8").strip(), )[::2][1:] return libs, cflags - except Exception: + except Exception: # noqa: PERF203 pass return None diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 390b3b374ab..1ffb18b9c8f 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -167,7 +167,7 @@ def seek(self, frame: int) -> None: for f in range(self.__frame + 1, frame + 1): try: self._seek(f) - except EOFError as e: + except EOFError as e: # noqa: PERF203 self.seek(last_frame) msg = "no more images in GIF file" raise EOFError(msg) from e diff --git a/src/PIL/Image.py b/src/PIL/Image.py index bde335504e8..6062857da06 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -488,7 +488,7 @@ def init() -> bool: try: logger.debug("Importing %s", plugin) __import__(f"{__spec__.parent}.{plugin}", globals(), locals(), []) - except ImportError as e: + except ImportError as e: # noqa: PERF203 logger.debug("Image: failed to import %s: %s", plugin, e) if OPEN or SAVE: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ea7f4dc5477..ec7c7cb0813 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -930,7 +930,7 @@ def load_path(filename: str | bytes) -> ImageFont: for directory in sys.path: try: return load(os.path.join(directory, filename)) - except OSError: + except OSError: # noqa: PERF203 pass msg = f'cannot find font file "{filename}" in sys.path' if os.path.exists(filename): diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 99ad2771b4b..2abbd46eaf1 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -198,13 +198,11 @@ def save(self, fp: str | IO[str]) -> None: try: fp.write("# Palette\n") fp.write(f"# Mode: {self.mode}\n") + palette_len = len(self.palette) for i in range(256): fp.write(f"{i}") for j in range(i * len(self.mode), (i + 1) * len(self.mode)): - try: - fp.write(f" {self.palette[j]}") - except IndexError: - fp.write(" 0") + fp.write(f" {self.palette[j] if j < palette_len else 0}") fp.write("\n") finally: if open_fp: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 2f11cbfe313..d5b67bba56c 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -153,7 +153,7 @@ def APP(self: JpegImageFile, marker: int) -> None: photoshop[code] = data offset += size offset += offset & 1 # align - except struct.error: + except struct.error: # noqa: PERF203 break # insufficient data elif marker == 0xFFEE and s.startswith(b"Adobe"): @@ -744,7 +744,7 @@ def validate_qtables( msg = "Invalid quantization table" raise TypeError(msg) table_array = array.array("H", table) - except TypeError as e: + except TypeError as e: # noqa: PERF203 msg = "Invalid quantization table" raise ValueError(msg) from e else: diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index a00e9b91984..b923293b06a 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -251,7 +251,7 @@ def _load_encoding(self) -> list[int | None]: ] if encoding_offset != 0xFFFF: encoding[i] = encoding_offset - except UnicodeDecodeError: + except UnicodeDecodeError: # noqa: PERF203 # character is not supported in selected encoding pass diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 4e082a293ff..d58426c5511 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -869,7 +869,7 @@ def seek(self, frame: int) -> None: for f in range(self.__frame + 1, frame + 1): try: self._seek(f) - except EOFError as e: + except EOFError as e: # noqa: PERF203 self.seek(last_frame) msg = "no more images in APNG file" raise EOFError(msg) from e From 65c4f4ea8dc3ff6eaf663d85296ee9fdd71dbd0a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Mar 2026 13:19:27 +1100 Subject: [PATCH 2340/2374] Updated libjpeg-turbo to 3.1.4 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 107eeae9b95..97011f4a00f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -93,7 +93,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.14.3 HARFBUZZ_VERSION=13.2.1 LIBPNG_VERSION=1.6.56 -JPEGTURBO_VERSION=3.1.3 +JPEGTURBO_VERSION=3.1.4.1 OPENJPEG_VERSION=2.5.4 XZ_VERSION=5.8.2 ZSTD_VERSION=1.5.7 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d958a459277..300cbf149f9 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -117,7 +117,7 @@ def cmd_msbuild( "FREETYPE": "2.14.3", "FRIBIDI": "1.0.16", "HARFBUZZ": "13.2.1", - "JPEGTURBO": "3.1.3", + "JPEGTURBO": "3.1.4.1", "LCMS2": "2.18", "LIBAVIF": "1.4.1", "LIBIMAGEQUANT": "4.4.1", From 018801805f496245d3c3d85512809d8c0013b6fd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Mar 2026 23:03:51 +1100 Subject: [PATCH 2341/2374] Simplify setimage() --- src/PIL/ImageFile.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 50e0075a29d..5275496e827 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -799,16 +799,13 @@ def setimage( if extents: x0, y0, x1, y1 = extents - else: - x0, y0, x1, y1 = (0, 0, 0, 0) - if x0 == 0 and x1 == 0: - self.state.xsize, self.state.ysize = self.im.size - else: self.state.xoff = x0 self.state.yoff = y0 self.state.xsize = x1 - x0 self.state.ysize = y1 - y0 + else: + self.state.xsize, self.state.ysize = self.im.size if self.state.xsize <= 0 or self.state.ysize <= 0: msg = "Size must be positive" From 9a7b91e5dbb6630ea4e3d5d6eccbf48f4463eda4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Mar 2026 15:11:04 +1100 Subject: [PATCH 2342/2374] PERF203 fixes --- src/PIL/GifImagePlugin.py | 12 ++++++------ src/PIL/JpegImagePlugin.py | 22 ++++++++++------------ src/PIL/PngImagePlugin.py | 12 ++++++------ 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 1ffb18b9c8f..b8db5d83284 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -164,13 +164,13 @@ def seek(self, frame: int) -> None: self._seek(0) last_frame = self.__frame - for f in range(self.__frame + 1, frame + 1): - try: + try: + for f in range(self.__frame + 1, frame + 1): self._seek(f) - except EOFError as e: # noqa: PERF203 - self.seek(last_frame) - msg = "no more images in GIF file" - raise EOFError(msg) from e + except EOFError as e: + self.seek(last_frame) + msg = "no more images in GIF file" + raise EOFError(msg) from e def _seek(self, frame: int, update_image: bool = True) -> None: if isinstance(self._fp, DeferredError): diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index d5b67bba56c..46320eb3b5b 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -127,8 +127,8 @@ def APP(self: JpegImageFile, marker: int) -> None: # parse the image resource block offset = 14 photoshop = self.info.setdefault("photoshop", {}) - while s[offset : offset + 4] == b"8BIM": - try: + try: + while s[offset : offset + 4] == b"8BIM": offset += 4 # resource code code = i16(s, offset) @@ -153,8 +153,8 @@ def APP(self: JpegImageFile, marker: int) -> None: photoshop[code] = data offset += size offset += offset & 1 # align - except struct.error: # noqa: PERF203 - break # insufficient data + except struct.error: + pass # insufficient data elif marker == 0xFFEE and s.startswith(b"Adobe"): self.info["adobe"] = i16(s, 5) @@ -738,17 +738,15 @@ def validate_qtables( if not (0 < len(qtables) < 5): msg = "None or too many quantization tables" raise ValueError(msg) - for idx, table in enumerate(qtables): - try: + try: + for idx, table in enumerate(qtables): if len(table) != 64: msg = "Invalid quantization table" raise TypeError(msg) - table_array = array.array("H", table) - except TypeError as e: # noqa: PERF203 - msg = "Invalid quantization table" - raise ValueError(msg) from e - else: - qtables[idx] = list(table_array) + qtables[idx] = list(array.array("H", table)) + except TypeError as e: + msg = "Invalid quantization table" + raise ValueError(msg) from e return qtables if qtables == "keep": diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index d58426c5511..76a15bd0dc6 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -866,13 +866,13 @@ def seek(self, frame: int) -> None: self._seek(0, True) last_frame = self.__frame - for f in range(self.__frame + 1, frame + 1): - try: + try: + for f in range(self.__frame + 1, frame + 1): self._seek(f) - except EOFError as e: # noqa: PERF203 - self.seek(last_frame) - msg = "no more images in APNG file" - raise EOFError(msg) from e + except EOFError as e: + self.seek(last_frame) + msg = "no more images in APNG file" + raise EOFError(msg) from e def _seek(self, frame: int, rewind: bool = False) -> None: assert self.png is not None From 701b49adc5d3f64dfb9eb2c42f0411b3d8490fef Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Mar 2026 15:13:26 +1100 Subject: [PATCH 2343/2374] PERF401 fix --- winbuild/build_prepare.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1438827cabb..466cca17696 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -542,14 +542,11 @@ def write_script( def get_footer(dep: dict[str, Any]) -> list[str]: - lines = [] - for out in dep.get("headers", []): - lines.append(cmd_copy(out, "{inc_dir}")) # noqa: PERF401 - for out in dep.get("libs", []): - lines.append(cmd_copy(out, "{lib_dir}")) # noqa: PERF401 - for out in dep.get("bins", []): - lines.append(cmd_copy(out, "{bin_dir}")) # noqa: PERF401 - return lines + return ( + [cmd_copy(out, "{inc_dir}") for out in dep.get("headers", [])] + + [cmd_copy(out, "{lib_dir}") for out in dep.get("libs", [])] + + [cmd_copy(out, "{bin_dir}") for out in dep.get("bins", [])] + ) def build_env(prefs: dict[str, str], verbose: bool) -> None: From 9f3f6de10982f46b3864adb6533326c87be6ab14 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Mar 2026 18:31:49 +1100 Subject: [PATCH 2344/2374] Allow None extents in C setimage --- Tests/test_imagefile.py | 21 +++++++++++++++++++++ src/decode.c | 35 +++++++++++++++++++++++++++++++---- src/encode.c | 37 +++++++++++++++++++++++++++++++------ 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 6656ee506ca..6cb0d36a3f8 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -170,6 +170,27 @@ def test_negative_tile_extents(self, xy: tuple[int, int]) -> None: with pytest.raises(SystemError, match="tile cannot extend outside image"): ImageFile._save(im, fp, [ImageFile._Tile("raw", xy + (1, 1), 0, "1")]) + def test_extents_none(self) -> None: + with Image.open("Tests/images/hopper.jpg") as im: + im.tile = [im.tile[0]._replace(extents=None)] + im.load() + + for extents in ("invalid", (0,), ("0", "0", "0", "0")): + with Image.open("Tests/images/hopper.jpg") as im: + im.tile = [im.tile[0]._replace(extents=extents)] # type: ignore[arg-type] + with pytest.raises(ValueError, match="invalid extents"): + im.load() + + im2 = Image.new("L", (1, 1)) + fp = BytesIO() + tile = ImageFile._Tile("jpeg", None, 0, "L") + ImageFile._save(im2, fp, [tile]) + + for extents in ("invalid", (0,), ("0", "0", "0", "0")): + tile = tile._replace(extents=extents) # type: ignore[arg-type] + with pytest.raises(ValueError, match="invalid extents"): + ImageFile._save(im2, fp, [tile]) + def test_no_format(self) -> None: buf = BytesIO(b"\x00" * 255) diff --git a/src/decode.c b/src/decode.c index cda4ce7027f..71f8d73d29a 100644 --- a/src/decode.c +++ b/src/decode.c @@ -155,21 +155,48 @@ PyImaging_AsImaging(PyObject *op); static PyObject * _setimage(ImagingDecoderObject *decoder, PyObject *args) { - PyObject *op; + PyObject *op, *extents; Imaging im; ImagingCodecState state; int x0, y0, x1, y1; - x0 = y0 = x1 = y1 = 0; - /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O(iiii)", &op, &x0, &y0, &x1, &y1)) { + if (!PyArg_ParseTuple(args, "OO", &op, &extents)) { return NULL; } im = PyImaging_AsImaging(op); if (!im) { return NULL; } + if (extents == Py_None) { + x0 = 0; + y0 = 0; + x1 = im->xsize; + y1 = im->ysize; + } else { + if (!PyTuple_Check(extents) || PyTuple_GET_SIZE(extents) != 4) { + PyErr_SetString(PyExc_ValueError, "invalid extents"); + return NULL; + } + for (int i = 0; i < 4; i++) { + PyObject *extent = PyTuple_GetItem(extents, i); + if (!PyLong_Check(extent)) { + PyErr_SetString(PyExc_ValueError, "invalid extents"); + return NULL; + } + int e = (int)PyLong_AsLong(extent); + + if (i == 0) { + x0 = e; + } else if (i == 1) { + y0 = e; + } else if (i == 2) { + x1 = e; + } else { + y1 = e; + } + } + } decoder->im = im; diff --git a/src/encode.c b/src/encode.c index 1fc31404d9e..26b744935d3 100644 --- a/src/encode.c +++ b/src/encode.c @@ -222,23 +222,48 @@ PyImaging_AsImaging(PyObject *op); static PyObject * _setimage(ImagingEncoderObject *encoder, PyObject *args) { - PyObject *op; + PyObject *op, *extents; Imaging im; ImagingCodecState state; Py_ssize_t x0, y0, x1, y1; - /* Define where image data should be stored */ - - x0 = y0 = x1 = y1 = 0; - /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O(nnnn)", &op, &x0, &y0, &x1, &y1)) { + if (!PyArg_ParseTuple(args, "OO", &op, &extents)) { return NULL; } im = PyImaging_AsImaging(op); if (!im) { return NULL; } + if (extents == Py_None) { + x0 = 0; + y0 = 0; + x1 = im->xsize; + y1 = im->ysize; + } else { + if (!PyTuple_Check(extents) || PyTuple_GET_SIZE(extents) != 4) { + PyErr_SetString(PyExc_ValueError, "invalid extents"); + return NULL; + } + for (int i = 0; i < 4; i++) { + PyObject *extent = PyTuple_GetItem(extents, i); + if (!PyLong_Check(extent)) { + PyErr_SetString(PyExc_ValueError, "invalid extents"); + return NULL; + } + Py_ssize_t e = (Py_ssize_t)PyLong_AsLong(extent); + + if (i == 0) { + x0 = e; + } else if (i == 1) { + y0 = e; + } else if (i == 2) { + x1 = e; + } else { + y1 = e; + } + } + } if (im->xsize == 0 || im->ysize == 0) { PyErr_SetString(PyExc_ValueError, "cannot write empty image"); return NULL; From 1ed39726c57cd2d094f9a9a90be08814678f6190 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Mar 2026 13:23:30 +1100 Subject: [PATCH 2345/2374] Added release notes for #9419 --- docs/releasenotes/12.2.0.rst | 63 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 64 insertions(+) create mode 100644 docs/releasenotes/12.2.0.rst diff --git a/docs/releasenotes/12.2.0.rst b/docs/releasenotes/12.2.0.rst new file mode 100644 index 00000000000..aa1206cd09a --- /dev/null +++ b/docs/releasenotes/12.2.0.rst @@ -0,0 +1,63 @@ +12.2.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards incompatible changes +============================== + +TODO +^^^^ + +TODO + +Deprecations +============ + +TODO +^^^^ + +TODO + +API changes +=========== + +TODO +^^^^ + +TODO + +API additions +============= + +FontFile.to_imagefont() +^^^^^^^^^^^^^^^^^^^^^^^ + +:py:class:`~PIL.FontFile.FontFile` instances can now be directly converted to +:py:class:`~PIL.ImageFont.ImageFont` instances:: + + >>> from PIL import PcfFontFile + >>> with open("Tests/fonts/10x20-ISO8859-1.pcf", "rb") as fp: + ... pcffont = PcfFontFile.PcfFontFile(fp) + ... pcffont.to_imagefont() + ... + + +Other changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 690be20729e..07687297933 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -15,6 +15,7 @@ expected to be backported to earlier versions. :maxdepth: 2 versioning + 12.2.0 12.1.1 12.1.0 12.0.0 From ccf9863ba8595b5920ee3883ffc9aba01e92ff7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Mar 2026 13:23:44 +1100 Subject: [PATCH 2346/2374] Added release notes for #9394 --- docs/releasenotes/12.2.0.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/12.2.0.rst b/docs/releasenotes/12.2.0.rst index aa1206cd09a..0bbb9b82473 100644 --- a/docs/releasenotes/12.2.0.rst +++ b/docs/releasenotes/12.2.0.rst @@ -33,10 +33,14 @@ TODO API changes =========== -TODO -^^^^ +Error when encoding an empty image +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Attempting to encode an image with zero width or height would previously raise +a :py:exc:`SystemError`. That has now been changed to a :py:exc:`ValueError`. + +This does not add any new errors. SGI, ICNS and ICO formats are still able to +save (0, 0) images. API additions ============= From 3121c77cad919703c2d5c77116a03422ba744d7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Mar 2026 19:19:48 +1100 Subject: [PATCH 2347/2374] Added release notes for #9456 --- docs/releasenotes/12.2.0.rst | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/docs/releasenotes/12.2.0.rst b/docs/releasenotes/12.2.0.rst index 0bbb9b82473..66526592a6c 100644 --- a/docs/releasenotes/12.2.0.rst +++ b/docs/releasenotes/12.2.0.rst @@ -14,22 +14,6 @@ TODO TODO -Backwards incompatible changes -============================== - -TODO -^^^^ - -TODO - -Deprecations -============ - -TODO -^^^^ - -TODO - API changes =========== @@ -61,7 +45,8 @@ FontFile.to_imagefont() Other changes ============= -TODO -^^^^ +Support reading JPEG2000 images with CMYK palettes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +JPEG2000 images with CMYK palettes can now be read. This is the first integration of +CMYK palettes into Pillow. From 7ef54f6bfd3b8c3666781ea2d27e20509201097f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Mar 2026 19:40:16 +1100 Subject: [PATCH 2348/2374] Image will never be None Co-authored-by: jorenham --- Tests/test_pyarrow.py | 2 -- src/PIL/ImageFont.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index 7a161f2ac12..f282f2c0059 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -112,8 +112,6 @@ def test_to_array(mode: str, dtype: pyarrow.DataType, mask: list[int] | None) -> reloaded = Image.fromarrow(arr, mode, img.size) - assert reloaded - assert_image_equal(img, reloaded) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ec7c7cb0813..06ea0359c45 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -110,7 +110,7 @@ def _load_pilfont(self, filename: str) -> None: except Exception: pass else: - if image and image.mode in ("1", "L"): + if image.mode in ("1", "L"): break else: if image: From 7c121637c9b0062694069f8f6d173e17dba8c46f Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Sun, 29 Mar 2026 10:05:18 -0400 Subject: [PATCH 2349/2374] Jeffrey A. Clark -> Jeffrey 'Alex' Clark Follow up to 4197263dff19a79f13cd86f6cdc9a0ec6c06da92. People cannot figure out my preferred name, hence this final (I hope!) update to my name in Pillow. --- LICENSE | 2 +- README.md | 2 +- docs/COPYING | 2 +- docs/conf.py | 4 ++-- docs/index.rst | 2 +- pyproject.toml | 2 +- src/PIL/__init__.py | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/LICENSE b/LICENSE index 10dd42d9eda..c011511a4ab 100644 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010 by Jeffrey A. Clark and contributors + Copyright © 2010 by Jeffrey 'Alex' Clark and contributors Like PIL, Pillow is licensed under the open source MIT-CMU License: diff --git a/README.md b/README.md index 8585ef6cbd4..04c9ae8abd8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Python Imaging Library (Fork) -Pillow is the friendly PIL fork by [Jeffrey A. Clark and +Pillow is the friendly PIL fork by [Jeffrey 'Alex' Clark and contributors](https://github.com/python-pillow/Pillow/graphs/contributors). PIL is the Python Imaging Library by Fredrik Lundh and contributors. As of 2019, Pillow development is diff --git a/docs/COPYING b/docs/COPYING index 17fba5b87ff..1852f9e4786 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010 by Jeffrey A. Clark and contributors + Copyright © 2010 by Jeffrey 'Alex' Clark and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/conf.py b/docs/conf.py index 040301433f9..189758944d9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,9 +55,9 @@ project = "Pillow (PIL Fork)" copyright = ( "1995-2011 Fredrik Lundh and contributors, " - "2010 Jeffrey A. Clark and contributors." + "2010 Jeffrey 'Alex' Clark and contributors." ) -author = "Fredrik Lundh (PIL), Jeffrey A. Clark (Pillow)" +author = "Fredrik Lundh (PIL), Jeffrey 'Alex' Clark (Pillow)" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/index.rst b/docs/index.rst index ee51621ac7a..8612f77a55d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ Pillow ====== -Pillow is the friendly PIL fork by `Jeffrey A. Clark and contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and contributors. +Pillow is the friendly PIL fork by `Jeffrey 'Alex' Clark and contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and contributors. Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_. diff --git a/pyproject.toml b/pyproject.toml index 7eb9a3fbdde..65e3b76591e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ keywords = [ license = "MIT-CMU" license-files = [ "LICENSE" ] authors = [ - { name = "Jeffrey A. Clark", email = "aclark@aclark.net" }, + { name = "Jeffrey 'Alex' Clark", email = "aclark@aclark.net" }, ] requires-python = ">=3.10" classifiers = [ diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 6e4c23f897f..faf3e76e0ae 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -1,6 +1,6 @@ """Pillow (Fork of the Python Imaging Library) -Pillow is the friendly PIL fork by Jeffrey A. Clark and contributors. +Pillow is the friendly PIL fork by Jeffrey 'Alex' Clark and contributors. https://github.com/python-pillow/Pillow/ Pillow is forked from PIL 1.1.7. From 07c180b21e9de56b9cec064306c149581fe2ca50 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Mar 2026 19:40:04 +1100 Subject: [PATCH 2350/2374] Simplify SAMPLEFORMAT when all values match for values other than 1 --- src/PIL/TiffImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3eec94dca57..f11b6ce9730 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1473,13 +1473,13 @@ def _setup(self) -> None: logger.debug("- size: %s", self.size) sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,)) - if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1: + if len(sample_format) > 1 and max(sample_format) == min(sample_format): # SAMPLEFORMAT is properly per band, so an RGB image will # be (1,1,1). But, we don't support per band pixel types, # and anything more than one band is a uint8. So, just # take the first element. Revisit this if adding support # for more exotic images. - sample_format = (1,) + sample_format = (sample_format[0],) bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) From 84cb30d7a7388ff984dcf284e32e7c4fb2abb44b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Mar 2026 19:42:07 +1100 Subject: [PATCH 2351/2374] For separate planar configuration, ignore unspecified extra components --- Tests/images/separate_planar_extra_samples.tiff | Bin 0 -> 202 bytes Tests/test_file_libtiff.py | 4 ++++ src/PIL/TiffImagePlugin.py | 14 ++++++++++---- 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 Tests/images/separate_planar_extra_samples.tiff diff --git a/Tests/images/separate_planar_extra_samples.tiff b/Tests/images/separate_planar_extra_samples.tiff new file mode 100644 index 0000000000000000000000000000000000000000..be51a7570ee92e579e41d178e6646c645eed56e1 GIT binary patch literal 202 zcmebD)MC(KU|^`2^Y-*YMg|5RCWZg?Rl5&lHTea_Ty9E~Xak~+Qv5qN-Hqm9U|?is z04icI0%AraHWQG|1Qg={LT0Eq2awMOWrOqxGO~cx90IaMq2eGtVo)~7OmQF^B&Gty XGDvEIplqNpLoieg6Idn4P6z-1gKZPS literal 0 HcmV?d00001 diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 6f20900e490..ea0550a5fd9 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1055,6 +1055,10 @@ def test_strip_planar_16bit_RGBa(self) -> None: with Image.open("Tests/images/tiff_strip_planar_16bit_RGBa.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") + def test_separate_planar_extra_samples(self) -> None: + with Image.open("Tests/images/separate_planar_extra_samples.tiff") as im: + assert im.mode == "L" + @pytest.mark.parametrize("compression", (None, "jpeg")) def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: im = hopper() diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index f11b6ce9730..669dc8a3e34 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1483,18 +1483,24 @@ def _setup(self) -> None: bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) + samples_per_pixel = self.tag_v2.get( + SAMPLESPERPIXEL, + 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, + ) if photo in (2, 6, 8): # RGB, YCbCr, LAB bps_count = 3 elif photo == 5: # CMYK bps_count = 4 else: bps_count = 1 + if self._planar_configuration == 2 and extra_tuple and max(extra_tuple) == 0: + # If components are stored separately, + # then unspecified extra components at the end can be ignored + bps_tuple = bps_tuple[: -len(extra_tuple)] + samples_per_pixel -= len(extra_tuple) + extra_tuple = () bps_count += len(extra_tuple) bps_actual_count = len(bps_tuple) - samples_per_pixel = self.tag_v2.get( - SAMPLESPERPIXEL, - 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, - ) if samples_per_pixel > MAX_SAMPLESPERPIXEL: # DOS check, samples_per_pixel can be a Long, and we extend the tuple below From 007974d35b197030ee34ee8d959361263125ab6d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Mar 2026 20:04:39 +1100 Subject: [PATCH 2352/2374] Ignore EXTRASAMPLES tag from separate planes image when saving --- Tests/test_file_libtiff.py | 7 ++++++- src/PIL/TiffImagePlugin.py | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index ea0550a5fd9..ca3c055f91a 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1055,10 +1055,15 @@ def test_strip_planar_16bit_RGBa(self) -> None: with Image.open("Tests/images/tiff_strip_planar_16bit_RGBa.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") - def test_separate_planar_extra_samples(self) -> None: + def test_separate_planar_extra_samples(self, tmp_path: Path) -> None: + out = tmp_path / "temp.tif" with Image.open("Tests/images/separate_planar_extra_samples.tiff") as im: assert im.mode == "L" + im.save(out) + with Image.open(out) as reloaded: + assert reloaded.mode == "L" + @pytest.mark.parametrize("compression", (None, "jpeg")) def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: im = hopper() diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 669dc8a3e34..5094faa1325 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1768,6 +1768,12 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: legacy_ifd = im.tag.to_v2() supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})} + if supplied_tags.get(PLANAR_CONFIGURATION) == 2 and EXTRASAMPLES in supplied_tags: + # If the image used separate component planes, + # then EXTRASAMPLES should be ignored when saving contiguously + if SAMPLESPERPIXEL in supplied_tags: + supplied_tags[SAMPLESPERPIXEL] -= len(supplied_tags[EXTRASAMPLES]) + del supplied_tags[EXTRASAMPLES] for tag in ( # IFD offset that may not be correct in the saved image EXIFIFD, From a03b7b52f9858f438a7ca86b4e4307a72c7d95ac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Mar 2026 22:57:51 +1100 Subject: [PATCH 2353/2374] Updated Python versions --- docs/installation/platform-support.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 74c63fb062f..7e6ad1e7790 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -23,7 +23,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Amazon Linux 2023 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Arch | 3.13 | x86-64 | +| Arch | 3.14 | x86-64 | +----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ @@ -37,7 +37,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 43 | 3.14 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Gentoo | 3.12 | x86-64 | +| Gentoo | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14, | arm64 | | | 3.15, PyPy3 | | @@ -57,7 +57,7 @@ These platforms are built and tested for every change. | Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 | | | 3.15, PyPy3 | | | +----------------------------+---------------------+ -| | 3.13 (MinGW) | x86-64 | +| | 3.14 (MinGW) | x86-64 | +----------------------------------+----------------------------+---------------------+ From f80de2152ccf84f430b2072362202ab0249b8c5f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:45:15 +0200 Subject: [PATCH 2354/2374] Run tests in parallel via tox --- tox.ini | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index de18946efa7..aede5fcdc54 100644 --- a/tox.ini +++ b/tox.ini @@ -9,11 +9,16 @@ env_list = [testenv] deps = numpy + pytest-sugar extras = tests commands = {envpython} selftest.py - {envpython} -m pytest -W always {posargs} + {envpython} -m pytest \ + --dist worksteal \ + --numprocesses auto \ + -W always \ + {posargs} [testenv:lint] skip_install = true From 73e1ed91e3556a1dad7eb81b43fe14b66c73f009 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jun 2025 00:34:30 +1000 Subject: [PATCH 2355/2374] For DXT1, only check if 8 bytes are left --- src/libImaging/BcnEncode.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c index 973a7a2faef..c6989dc1cc4 100644 --- a/src/libImaging/BcnEncode.c +++ b/src/libImaging/BcnEncode.c @@ -257,9 +257,9 @@ ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { UINT8 *dst = buf; + int will_write = (n == 2 || n == 3 || n == 5) ? 16 : 8; for (;;) { - // Loop writes a max of 16 bytes per iteration - if (dst + 16 >= bytes + buf) { + if (dst + will_write >= bytes + buf) { break; } if (n == 5) { From 228a85e56eacbd4b16184f2fbdaa162a23aeae42 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:22:11 +0300 Subject: [PATCH 2356/2374] Safer test_file_spider teardown under pytest-xdist --- Tests/test_file_spider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 3b1953aac5d..df385208f02 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -15,7 +15,7 @@ def teardown_module() -> None: - del Image.EXTENSION[".spider"] + Image.EXTENSION.pop(".spider", None) def test_sanity() -> None: From 09c585dc2195e339566f488bc47000f038a6b5f1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Mar 2026 22:02:23 +1100 Subject: [PATCH 2357/2374] Cleanup .spider extension in the same test where it is added --- Tests/test_file_spider.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index df385208f02..71fb434ccf4 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -14,10 +14,6 @@ TEST_FILE = "Tests/images/hopper.spider" -def teardown_module() -> None: - Image.EXTENSION.pop(".spider", None) - - def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() @@ -67,6 +63,8 @@ def test_save(tmp_path: Path) -> None: assert im2.size == (128, 128) assert im2.format == "SPIDER" + del Image.EXTENSION[".spider"] + @pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0))) def test_save_zero(size: tuple[int, int]) -> None: From 4bada07dc6c24319edd1eb76f1dd28d968d58207 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Feb 2026 22:24:03 +1100 Subject: [PATCH 2358/2374] Avoid overflow by not adding extents together --- Tests/images/psd-oob-write-overflow.psd | Bin 0 -> 496 bytes Tests/test_file_psd.py | 2 ++ src/decode.c | 13 ++++++------- src/encode.c | 12 +++++------- 4 files changed, 13 insertions(+), 14 deletions(-) create mode 100644 Tests/images/psd-oob-write-overflow.psd diff --git a/Tests/images/psd-oob-write-overflow.psd b/Tests/images/psd-oob-write-overflow.psd new file mode 100644 index 0000000000000000000000000000000000000000..c2bb10d614ed8a2130a28338f474b74f6e67d486 GIT binary patch literal 496 zcmcC;3J7LkWPkt=%>~9Ba4{g4F$IVd7?>c6z$8Q!L|>YPlc#T9eo^j!gaWWk1BA~7 bHETJhx&}G`21d@^a4>k8z_6l2U^D;#tMlDs literal 0 HcmV?d00001 diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 3b145b13983..9964a68e18e 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -195,11 +195,13 @@ def test_layer_crashes(test_file: str) -> None: "Tests/images/psd-oob-write.psd", "Tests/images/psd-oob-write-x.psd", "Tests/images/psd-oob-write-y.psd", + "Tests/images/psd-oob-write-overflow.psd", ], ) def test_bounds_crash(test_file: str) -> None: with Image.open(test_file) as im: assert isinstance(im, PsdImagePlugin.PsdImageFile) + im.load() im.seek(im.n_frames) with pytest.raises(ValueError): diff --git a/src/decode.c b/src/decode.c index cda4ce7027f..2268b353306 100644 --- a/src/decode.c +++ b/src/decode.c @@ -171,6 +171,12 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { return NULL; } + if (x0 < 0 || y0 < 0 || x1 <= x0 || y1 <= y0 || x1 > (int)im->xsize || + y1 > (int)im->ysize) { + PyErr_SetString(PyExc_ValueError, "tile cannot extend outside image"); + return NULL; + } + decoder->im = im; state = &decoder->state; @@ -181,13 +187,6 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { state->xsize = x1 - x0; state->ysize = y1 - y0; - if (state->xoff < 0 || state->xsize <= 0 || - state->xsize + state->xoff > (int)im->xsize || state->yoff < 0 || - state->ysize <= 0 || state->ysize + state->yoff > (int)im->ysize) { - PyErr_SetString(PyExc_ValueError, "tile cannot extend outside image"); - return NULL; - } - /* Allocate memory buffer (if bits field is set) */ if (state->bits > 0) { if (!state->bytes) { diff --git a/src/encode.c b/src/encode.c index 1fc31404d9e..02356d5648b 100644 --- a/src/encode.c +++ b/src/encode.c @@ -244,6 +244,11 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { return NULL; } + if (x0 < 0 || y0 < 0 || x1 <= x0 || y1 <= y0 || x1 > im->xsize || y1 > im->ysize) { + PyErr_SetString(PyExc_SystemError, "tile cannot extend outside image"); + return NULL; + } + encoder->im = im; state = &encoder->state; @@ -253,13 +258,6 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { state->xsize = x1 - x0; state->ysize = y1 - y0; - if (state->xoff < 0 || state->xsize <= 0 || - state->xsize + state->xoff > im->xsize || state->yoff < 0 || - state->ysize <= 0 || state->ysize + state->yoff > im->ysize) { - PyErr_SetString(PyExc_SystemError, "tile cannot extend outside image"); - return NULL; - } - /* Allocate memory buffer (if bits field is set) */ if (state->bits > 0) { if (state->xsize > ((INT_MAX / state->bits) - 7)) { From 591ce38ca56fa7516df4f4ee0525730dee049144 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Feb 2026 23:24:05 +1100 Subject: [PATCH 2359/2374] Skip OverflowError on Windows Python 3.10 --- Tests/test_file_psd.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 9964a68e18e..a5223cacea7 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,12 +1,19 @@ from __future__ import annotations +import sys import warnings import pytest from PIL import Image, PsdImagePlugin -from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy +from .helper import ( + assert_image_equal_tofile, + assert_image_similar, + hopper, + is_pypy, + is_win32, +) test_file = "Tests/images/hopper.psd" @@ -195,11 +202,23 @@ def test_layer_crashes(test_file: str) -> None: "Tests/images/psd-oob-write.psd", "Tests/images/psd-oob-write-x.psd", "Tests/images/psd-oob-write-y.psd", - "Tests/images/psd-oob-write-overflow.psd", ], ) def test_bounds_crash(test_file: str) -> None: with Image.open(test_file) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) + im.seek(im.n_frames) + + with pytest.raises(ValueError): + im.load() + + +@pytest.mark.skipif( + is_win32() and sys.version_info < (3, 11), + reason="OverflowError on Windows Python 3.10", +) +def test_bounds_crash_overflow() -> None: + with Image.open("Tests/images/psd-oob-write-overflow.psd") as im: assert isinstance(im, PsdImagePlugin.PsdImageFile) im.load() im.seek(im.n_frames) From b2a16f0dbe80d4add20294b3dca0618cfe2c1660 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Mar 2026 23:45:57 +1100 Subject: [PATCH 2360/2374] Copy offset check from C into Python --- Tests/test_imagefile.py | 49 +++++++++++++++++++++++++++++++++++++++-- src/PIL/ImageFile.py | 11 ++++----- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 6656ee506ca..2dcebc4b15f 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -308,7 +308,20 @@ def test_extents_none(self) -> None: assert MockPyDecoder.last.state.xsize == 200 assert MockPyDecoder.last.state.ysize == 200 - def test_negsize(self) -> None: + def test_negative_offset(self) -> None: + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + im.tile = [ImageFile._Tile("MOCK", (-10, yoff, xsize, ysize), 32, None)] + + with pytest.raises(ValueError): + im.load() + + im.tile = [ImageFile._Tile("MOCK", (xoff, -10, xsize, ysize), 32, None)] + with pytest.raises(ValueError): + im.load() + + def test_negative_size(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -386,7 +399,39 @@ def test_extents_none(self) -> None: assert MockPyEncoder.last.state.xsize == 200 assert MockPyEncoder.last.state.ysize == 200 - def test_negsize(self) -> None: + def test_negative_offset(self) -> None: + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + fp = BytesIO() + MockPyEncoder.last = None + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [ + ImageFile._Tile( + "MOCK", (-10, yoff, xoff + xsize, yoff + ysize), 0, "RGB" + ) + ], + ) + last: MockPyEncoder | None = MockPyEncoder.last + assert last + assert last.cleanup_called + + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [ + ImageFile._Tile( + "MOCK", (xoff, -10, xoff + xsize, yoff + ysize), 0, "RGB" + ) + ], + ) + + def test_negative_size(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index b79f23f0d64..dd1116ab986 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -805,6 +805,10 @@ def setimage( if extents: x0, y0, x1, y1 = extents + + if x0 < 0 or y0 < 0 or x1 > self.im.size[0] or y1 > self.im.size[1]: + msg = "Tile cannot extend outside image" + raise ValueError(msg) else: x0, y0, x1, y1 = (0, 0, 0, 0) @@ -820,13 +824,6 @@ def setimage( msg = "Size must be positive" raise ValueError(msg) - if ( - self.state.xsize + self.state.xoff > self.im.size[0] - or self.state.ysize + self.state.yoff > self.im.size[1] - ): - msg = "Tile cannot extend outside image" - raise ValueError(msg) - class PyDecoder(PyCodec): """ From cc22efda7a296c3e6ca9b40a4a4eb4d1af1741ac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Mar 2026 23:54:09 +1100 Subject: [PATCH 2361/2374] Parametrize tests --- Tests/test_imagefile.py | 168 ++++++++++------------------------------ 1 file changed, 42 insertions(+), 126 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 2dcebc4b15f..5f4ed2eb082 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -295,65 +295,38 @@ def test_setimage(self) -> None: with pytest.raises(ValueError): MockPyDecoder.last.set_as_raw(b"\x00") - def test_extents_none(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - im.tile = [ImageFile._Tile("MOCK", None, 32, None)] - - im.load() - - assert MockPyDecoder.last.state.xoff == 0 - assert MockPyDecoder.last.state.yoff == 0 - assert MockPyDecoder.last.state.xsize == 200 - assert MockPyDecoder.last.state.ysize == 200 - - def test_negative_offset(self) -> None: + @pytest.mark.parametrize( + "extents", + ( + (-10, yoff, xoff + xsize, yoff + ysize), + (xoff, -10, xoff + xsize, yoff + ysize), + (xoff, yoff, -10, yoff + ysize), + (xoff, yoff, xoff + xsize, -10), + (xoff, yoff, xoff + xsize + 100, yoff + ysize), + (xoff, yoff, xoff + xsize, yoff + ysize + 100), + ), + ) + def test_extents(self, extents: tuple[int, int, int, int]) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - im.tile = [ImageFile._Tile("MOCK", (-10, yoff, xsize, ysize), 32, None)] - - with pytest.raises(ValueError): - im.load() + im.tile = [ImageFile._Tile("MOCK", extents, 32, None)] - im.tile = [ImageFile._Tile("MOCK", (xoff, -10, xsize, ysize), 32, None)] with pytest.raises(ValueError): im.load() - def test_negative_size(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] - - with pytest.raises(ValueError): - im.load() - - im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)] - with pytest.raises(ValueError): - im.load() - - def test_oversize(self) -> None: + def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - im.tile = [ - ImageFile._Tile( - "MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None - ) - ] + im.tile = [ImageFile._Tile("MOCK", None, 32, None)] - with pytest.raises(ValueError): - im.load() + im.load() - im.tile = [ - ImageFile._Tile( - "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None - ) - ] - with pytest.raises(ValueError): - im.load() + assert MockPyDecoder.last.state.xoff == 0 + assert MockPyDecoder.last.state.yoff == 0 + assert MockPyDecoder.last.state.xsize == 200 + assert MockPyDecoder.last.state.ysize == 200 def test_decode(self) -> None: decoder = ImageFile.PyDecoder("") @@ -384,22 +357,18 @@ def test_setimage(self) -> None: assert MockPyEncoder.last.state.xsize == xsize assert MockPyEncoder.last.state.ysize == ysize - def test_extents_none(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - im.tile = [ImageFile._Tile("MOCK", None, 32, None)] - - fp = BytesIO() - ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")]) - - assert MockPyEncoder.last - assert MockPyEncoder.last.state.xoff == 0 - assert MockPyEncoder.last.state.yoff == 0 - assert MockPyEncoder.last.state.xsize == 200 - assert MockPyEncoder.last.state.ysize == 200 - - def test_negative_offset(self) -> None: + @pytest.mark.parametrize( + "extents", + ( + (-10, yoff, xoff + xsize, yoff + ysize), + (xoff, -10, xoff + xsize, yoff + ysize), + (xoff, yoff, -10, yoff + ysize), + (xoff, yoff, xoff + xsize, -10), + (xoff, yoff, xoff + xsize + 100, yoff + ysize), + (xoff, yoff, xoff + xsize, yoff + ysize + 100), + ), + ) + def test_extents(self, extents: tuple[int, int, int, int]) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -407,81 +376,28 @@ def test_negative_offset(self) -> None: fp = BytesIO() MockPyEncoder.last = None with pytest.raises(ValueError): - ImageFile._save( - im, - fp, - [ - ImageFile._Tile( - "MOCK", (-10, yoff, xoff + xsize, yoff + ysize), 0, "RGB" - ) - ], - ) + ImageFile._save(im, fp, [ImageFile._Tile("MOCK", extents, 0, "RGB")]) last: MockPyEncoder | None = MockPyEncoder.last assert last assert last.cleanup_called with pytest.raises(ValueError): - ImageFile._save( - im, - fp, - [ - ImageFile._Tile( - "MOCK", (xoff, -10, xoff + xsize, yoff + ysize), 0, "RGB" - ) - ], - ) - - def test_negative_size(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) + ImageFile._save(im, fp, [ImageFile._Tile("MOCK", extents, 0, "RGB")]) - fp = BytesIO() - MockPyEncoder.last = None - with pytest.raises(ValueError): - ImageFile._save( - im, - fp, - [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")], - ) - last: MockPyEncoder | None = MockPyEncoder.last - assert last - assert last.cleanup_called - - with pytest.raises(ValueError): - ImageFile._save( - im, - fp, - [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")], - ) - - def test_oversize(self) -> None: + def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) + im.tile = [ImageFile._Tile("MOCK", None, 32, None)] fp = BytesIO() - with pytest.raises(ValueError): - ImageFile._save( - im, - fp, - [ - ImageFile._Tile( - "MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB" - ) - ], - ) + ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")]) - with pytest.raises(ValueError): - ImageFile._save( - im, - fp, - [ - ImageFile._Tile( - "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB" - ) - ], - ) + assert MockPyEncoder.last + assert MockPyEncoder.last.state.xoff == 0 + assert MockPyEncoder.last.state.yoff == 0 + assert MockPyEncoder.last.state.xsize == 200 + assert MockPyEncoder.last.state.ysize == 200 def test_encode(self) -> None: encoder = ImageFile.PyEncoder("") From 2696e962c2b145d7b354c12ac6ef1e4a95558fc9 Mon Sep 17 00:00:00 2001 From: Gareth Davidson Date: Tue, 31 Mar 2026 21:03:12 +0100 Subject: [PATCH 2362/2374] Add loader plugins: AMOS abk, Atari Degas, 40+ more obscure formats via Netpbm (#9482) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/handbook/third-party-plugins.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/handbook/third-party-plugins.rst b/docs/handbook/third-party-plugins.rst index 20086649906..51181a59663 100644 --- a/docs/handbook/third-party-plugins.rst +++ b/docs/handbook/third-party-plugins.rst @@ -8,12 +8,15 @@ itself. Here is a list of PyPI projects that offer additional plugins: * :pypi:`amigainfo`: Adds support for Amiga Workbench .info icon files. +* :pypi:`amos-abk`: AMOS BASIC sprite and image banks. * :pypi:`DjvuRleImagePlugin`: Plugin for the DjVu RLE image format as defined in the DjVuLibre docs. * :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library. * :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL. +* :pypi:`pillow-degas`: Adds reading Atari ST Degas image files. * :pypi:`pillow-heif`: Python bindings to libheif for working with HEIF images. * :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implementation. Python bindings implemented using pybind11. * :pypi:`pillow-jxl-plugin`: Plugin for JPEG-XL, using Rust for bindings. * :pypi:`pillow-mbm`: Adds support for KSP's proprietary MBM texture format. +* :pypi:`pillow-netpbm`: Adds .pam support, and loads images using `Netpbm `__'s converter collection. * :pypi:`pillow-svg`: Implements basic SVG read support. Supports basic paths, shapes, and text. * :pypi:`raw-pillow-opener`: Simple camera raw opener, based on the rawpy library. From 3cb814f33899b4d50eb04c316e66a737751c70a8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 31 Mar 2026 23:15:06 +0300 Subject: [PATCH 2363/2374] Update 12.2.0 release notes --- docs/releasenotes/12.2.0.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/releasenotes/12.2.0.rst b/docs/releasenotes/12.2.0.rst index 209fa782f83..d02d6541466 100644 --- a/docs/releasenotes/12.2.0.rst +++ b/docs/releasenotes/12.2.0.rst @@ -67,6 +67,11 @@ or scaling, optionally with a font size limit:: text.wrap(58, 10, "grow") text.wrap(50, 50, ("grow", 12)) +EXIF tag FrameRate +^^^^^^^^^^^^^^^^^^ + +The EXIF tag ``FrameRate`` has been added. + Other changes ============= @@ -75,3 +80,16 @@ Support reading JPEG2000 images with CMYK palettes JPEG2000 images with CMYK palettes can now be read. This is the first integration of CMYK palettes into Pillow. + +Lazy plugin loading +^^^^^^^^^^^^^^^^^^^ + +When opening or saving an image, Pillow now lazily loads only the required plugin +based on the file extension, instead of importing all plugins upfront. This makes +``open`` 2.3-15.6x faster and ``save`` 2.2-9x faster for common formats. + +Thread safety for free-threaded Python +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Critical sections are now used to protect FreeType font objects, improving thread +safety when using fonts in the free-threaded build of Python. From 3cb854e8b2bab43f40e342e665f9340d861aa628 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 1 Apr 2026 00:02:08 +0300 Subject: [PATCH 2364/2374] Only read as much data from gzip-decompressed data as necessary (#9521) --- src/PIL/FitsImagePlugin.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index a3fdc0efeec..e918407784d 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -128,17 +128,18 @@ class FitsGzipDecoder(ImageFile.PyDecoder): def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None - value = gzip.decompress(self.fd.read()) - - rows = [] - offset = 0 - number_of_bits = min(self.args[0] // 8, 4) - for y in range(self.state.ysize): - row = bytearray() - for x in range(self.state.xsize): - row += value[offset + (4 - number_of_bits) : offset + 4] - offset += 4 - rows.append(row) + with gzip.open(self.fd) as fp: + value = fp.read(self.state.xsize * self.state.ysize * 4) + + rows = [] + offset = 0 + number_of_bits = min(self.args[0] // 8, 4) + for y in range(self.state.ysize): + row = bytearray() + for x in range(self.state.xsize): + row += value[offset + (4 - number_of_bits) : offset + 4] + offset += 4 + rows.append(row) self.set_as_raw(bytes([pixel for row in rows[::-1] for pixel in row])) return -1, 0 From 3bf614e4b8615d0ce1d5039efaf6db447fe7c468 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 1 Apr 2026 00:03:15 +0300 Subject: [PATCH 2365/2374] Raise an error if the trailer chain loops back on itself (#9519) --- Tests/images/trailer_loop.pdf | Bin 0 -> 1818 bytes Tests/test_pdfparser.py | 5 +++++ src/PIL/PdfParser.py | 10 ++++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 Tests/images/trailer_loop.pdf diff --git a/Tests/images/trailer_loop.pdf b/Tests/images/trailer_loop.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7bf27ca370ec635039e274ddb65685fa147a2abc GIT binary patch literal 1818 zcmd5*OK;Oa5O!&W%HFuZg~MJVl|#E;zmlLThd2bHQZ=NJKp+l|y-l|`-m>2G!KuF- znDL_tE}&kJn#A$$?9A+Z-#6154~DyZ&m%1wYY`cd(AhN|%QRJ&6wYX(<%Q71qc&Bu zv;BR-rq}0!vM@4Hs^)}^qq)eb59bro>xnD@H-g*W+zT(lLbc2c<%Au`B&VOLgJJZ` zDv&n=KOW7_L~IB@%BBpu4s2n}gj7>=gXVRSVRu}TD7 zz{=G)(hIy7aU9THi0-FR{B@LbYV;DahALeyvK;eH)Fr-qJq+(llaGZC)#6-bqQn5c zN*|v`G0-s(7cv%abaYMFJCV(?G~R*W+yJc$a4t~7d~t2M{DcNYP|(M zkJs!^H@1qnZLmLEvp=uw=>KBP4qJIx!l`0~B-fxu8@2B^xZTliOvgPnj~qmji%+n{4r zT}7hjt~mUL$%{{Y(`pkhC@YH_DEgk<1s<$YPo+r(-TCp;Qr6Nkmh%#7#pahP8^8$A zoxv-|b^_bAeTO@3-}?j}hbsB&;ceevX>meq+9qY4_)i1hcLRDtYa91qn2M9^*5-Ag zzJ@LE@A}yuaNO{-JMBBRwg=4Cenv+!g&9VfCrTNL!#shBlHUcC%0}6VqR3C7X>KBI z3LG7;OnW zadsX8PUnv}E9_1e2KZjsyES%1S4xcVn%I$ None: pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf") assert pdf.xref_table.existing_entries[6][0] == 1197 pdf.close() + + +def test_trailer_loop() -> None: + with pytest.raises(PdfFormatError, match="trailer loop found"): + PdfParser("Tests/images/trailer_loop.pdf") diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 2c9031469ad..f7f3a46431d 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -685,7 +685,9 @@ def read_trailer(self) -> None: if b"Prev" in self.trailer_dict: self.read_prev_trailer(self.trailer_dict[b"Prev"]) - def read_prev_trailer(self, xref_section_offset: int) -> None: + def read_prev_trailer( + self, xref_section_offset: int, processed_offsets: list[int] = [] + ) -> None: assert self.buf is not None trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset) m = self.re_trailer_prev.search( @@ -700,7 +702,11 @@ def read_prev_trailer(self, xref_section_offset: int) -> None: ) trailer_dict = self.interpret_trailer(trailer_data) if b"Prev" in trailer_dict: - self.read_prev_trailer(trailer_dict[b"Prev"]) + processed_offsets.append(xref_section_offset) + check_format_condition( + trailer_dict[b"Prev"] not in processed_offsets, "trailer loop found" + ) + self.read_prev_trailer(trailer_dict[b"Prev"], processed_offsets) re_whitespace_optional = re.compile(whitespace_optional) re_name = re.compile( From cf4a8ee0b955ee8039624a654d9cab2b75ce179a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Apr 2026 08:26:13 +1100 Subject: [PATCH 2366/2374] Updated xz to 5.8.3 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 97011f4a00f..331eae2249b 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -95,7 +95,7 @@ HARFBUZZ_VERSION=13.2.1 LIBPNG_VERSION=1.6.56 JPEGTURBO_VERSION=3.1.4.1 OPENJPEG_VERSION=2.5.4 -XZ_VERSION=5.8.2 +XZ_VERSION=5.8.3 ZSTD_VERSION=1.5.7 TIFF_VERSION=4.7.1 LCMS2_VERSION=2.18 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e2580f482fe..3b16da58aef 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -125,7 +125,7 @@ def cmd_msbuild( "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.4", "TIFF": "4.7.1", - "XZ": "5.8.2", + "XZ": "5.8.3", "ZLIBNG": "2.3.3", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) From ec8272044d2adfc97a5f4b6e921c1a908318d9cb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 1 Apr 2026 00:52:09 +0300 Subject: [PATCH 2367/2374] Use long for glyph position (#9518) Co-authored-by: Andrew Murray --- src/_imagingft.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 8e56c57ab2e..8330439f0b1 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -589,9 +589,9 @@ bounding_box_and_anchors( int *x_offset, int *y_offset ) { - int position; /* pen position along primary axis, in 26.6 precision */ - int advanced; /* pen position along primary axis, in pixels */ - int px, py; /* position of current glyph, in pixels */ + long position; /* pen position along primary axis, in 26.6 precision */ + long advanced; /* pen position along primary axis, in pixels */ + int px, py; /* position of current glyph, in pixels */ int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */ int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ int error; From f5e893e46e869a9e275298207c70cf915173a072 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Apr 2026 09:46:09 +1100 Subject: [PATCH 2368/2374] Seek raises OverFlowError on 32-bit --- Tests/test_file_psd.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index a5223cacea7..538b1406b36 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -12,7 +12,6 @@ assert_image_similar, hopper, is_pypy, - is_win32, ) test_file = "Tests/images/hopper.psd" @@ -213,15 +212,15 @@ def test_bounds_crash(test_file: str) -> None: im.load() -@pytest.mark.skipif( - is_win32() and sys.version_info < (3, 11), - reason="OverflowError on Windows Python 3.10", -) def test_bounds_crash_overflow() -> None: with Image.open("Tests/images/psd-oob-write-overflow.psd") as im: assert isinstance(im, PsdImagePlugin.PsdImageFile) im.load() - im.seek(im.n_frames) - - with pytest.raises(ValueError): - im.load() + if sys.maxsize <= 2**32: + with pytest.raises(OverflowError): + im.seek(im.n_frames) + else: + im.seek(im.n_frames) + + with pytest.raises(ValueError): + im.load() From 4ef0ac611dd42602365c8b0506f796982c20fac2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Apr 2026 10:00:39 +1100 Subject: [PATCH 2369/2374] Resize tall images vertically first --- src/PIL/Image.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6062857da06..574980771f9 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2428,7 +2428,14 @@ def resize( (box[3] - reduce_box[1]) / factor_y, ) - return self._new(self.im.resize(size, resample, box)) + if self.size[1] > self.size[0] * 100 and size[1] < self.size[1]: + im = self.im.resize( + (self.size[0], size[1]), resample, (0, box[1], self.size[0], box[3]) + ) + im = im.resize(size, resample, (box[0], 0, box[2], size[1])) + else: + im = self.im.resize(size, resample, box) + return self._new(im) def reduce( self, From 459bdf766fec2e6ed106c650e79d388284ead2eb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Apr 2026 10:38:22 +1100 Subject: [PATCH 2370/2374] Move variable declaration inside define --- src/libImaging/Paste.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index f01bce93388..f4b72c5d814 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -356,7 +356,7 @@ fill( ) { /* fill opaque region */ - int x, y, i; + int x, y; UINT8 ink8 = 0; INT32 ink32 = 0L; @@ -372,6 +372,7 @@ fill( } else { #if defined _WIN32 && !defined _WIN64 + int i; dx *= pixelsize; for (y = 0; y < ysize; y++) { UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx; From c4f7aa5dfb4dbd1242978ac235e01b9934ec6d3c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Apr 2026 16:49:20 +1100 Subject: [PATCH 2371/2374] Added security release notes --- docs/releasenotes/12.2.0.rst | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/docs/releasenotes/12.2.0.rst b/docs/releasenotes/12.2.0.rst index d02d6541466..05d5dee2567 100644 --- a/docs/releasenotes/12.2.0.rst +++ b/docs/releasenotes/12.2.0.rst @@ -4,15 +4,34 @@ Security ======== -TODO -^^^^ +Prevent FITS decompression bomb +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +When decompressing GZIP data from a FITS image, Pillow did not limit the amount of data +being read, meaning that it was vulnerable to GZIP decompression bombs. This was +introduced in Pillow 10.3.0. -:cve:`YYYY-XXXXX`: TODO -^^^^^^^^^^^^^^^^^^^^^^^ +The data being read is now limited to only the necessary amount. + +Fix OOB write with invalid tile extents +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 12.1.1 added improved checks for tile extents to prevent an OOB write from +specially crafted PSD images in Pillow >= 10.3.0. However, these checks did not +consider integer overflow. This has been corrected. + +Prevent PDF parsing trailer infinite loop +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When parsing a PDF, if a trailer refers to itself, or a more complex cyclic loop +exists, then an infinite loop occurs. Pillow now keeps a record of which trailers it +has already processed. PdfParser was added in Pillow 4.2.0. + +Integer overflow when processing fonts +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +If a font advances for each glyph by an exceeding large amount, when Pillow keeps track +of the current position, it may lead to an integer overflow. This has been fixed. API changes =========== From cf6de8ca9b23e714aa5310e1c791eda66fc0b670 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:50:45 +0300 Subject: [PATCH 2372/2374] Reject non-numeric elements inside list coords (#9526) --- Tests/test_imagepath.py | 29 +++++++++++++++++++++++++++++ docs/releasenotes/12.2.0.rst | 10 ++++++++++ src/path.c | 15 ++++++++++++++- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index ad8acde4938..8d230eb564c 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -51,6 +51,7 @@ def test_path() -> None: [0.0, 1.0], ((0, 1),), [(0, 1)], + [[0, 1]], ((0.0, 1.0),), [(0.0, 1.0)], array.array("f", [0, 1]), @@ -68,6 +69,34 @@ def test_path_constructors( assert list(p) == [(0.0, 1.0)] +@pytest.mark.parametrize( + "coords, expected", + ( + ([[0, 1], [2, 3]], [(0.0, 1.0), (2.0, 3.0)]), + ([[0.0, 1.0], [2.0, 3.0]], [(0.0, 1.0), (2.0, 3.0)]), + ), +) +def test_path_list_of_lists( + coords: list[list[float]], expected: list[tuple[float, float]] +) -> None: + p = ImagePath.Path(coords) + assert list(p) == expected + + +@pytest.mark.parametrize( + "coords, message", + ( + ([[1, 2, 3]], "coordinate list must contain exactly 2 coordinates"), + ([[1]], "coordinate list must contain exactly 2 coordinates"), + ([[[1, 2], [3, 4]]], "coordinate list must contain numbers"), + ([["a", "b"]], "coordinate list must contain numbers"), + ), +) +def test_invalid_list_coords(coords: list[list[object]], message: str) -> None: + with pytest.raises(ValueError, match=message): + ImagePath.Path(coords) + + def test_invalid_path_constructors() -> None: # Arrange / Act with pytest.raises(ValueError, match="incorrect coordinate type"): diff --git a/docs/releasenotes/12.2.0.rst b/docs/releasenotes/12.2.0.rst index 05d5dee2567..b03afb6651f 100644 --- a/docs/releasenotes/12.2.0.rst +++ b/docs/releasenotes/12.2.0.rst @@ -33,6 +33,16 @@ Integer overflow when processing fonts If a font advances for each glyph by an exceeding large amount, when Pillow keeps track of the current position, it may lead to an integer overflow. This has been fixed. +Heap buffer overflow with nested list coordinates +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Passing nested lists as coordinates to APIs that accept coordinates such as +``ImagePath.Path``, :py:meth:`~PIL.ImageDraw.ImageDraw.polygon` +and :py:meth:`~PIL.ImageDraw.ImageDraw.line` could cause a heap buffer overflow, +as nested lists were recursively unpacked beyond the allocated buffer. +Coordinate lists are now validated to contain exactly two numeric coordinates. +This was introduced in Pillow 11.2.1. + API changes =========== diff --git a/src/path.c b/src/path.c index 38300547c60..b88346d5f8f 100644 --- a/src/path.c +++ b/src/path.c @@ -118,14 +118,27 @@ assign_item_to_array(double *xy, Py_ssize_t j, PyObject *op) { } else if (PyNumber_Check(op)) { xy[j++] = PyFloat_AsDouble(op); } else if (PyList_Check(op)) { + if (PyList_GET_SIZE(op) != 2) { + PyErr_SetString( + PyExc_ValueError, "coordinate list must contain exactly 2 coordinates" + ); + return -1; + } for (int k = 0; k < 2; k++) { PyObject *op1 = PyList_GetItemRef(op, k); if (op1 == NULL) { return -1; } - j = assign_item_to_array(xy, j, op1); + if (PyFloat_Check(op1) || PyLong_Check(op1) || PyNumber_Check(op1)) { + j = assign_item_to_array(xy, j, op1); + } else { + j = -1; + } Py_DECREF(op1); if (j == -1) { + PyErr_SetString( + PyExc_ValueError, "coordinate list must contain numbers" + ); return -1; } } From 585b2f5a780722c8a5bfffb3a40f7f42e8a205be Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Mar 2026 18:11:36 +1100 Subject: [PATCH 2373/2374] Check calloc return value --- src/_imaging.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/_imaging.c b/src/_imaging.c index 55d29d1bf77..980f827ae78 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -267,6 +267,9 @@ PyObject * ExportArrowSchemaPyCapsule(ImagingObject *self) { struct ArrowSchema *schema = (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema)); + if (!schema) { + return ArrowError(IMAGING_CODEC_MEMORY); + } int err = export_imaging_schema(self->image, schema); if (err == 0) { return PyCapsule_New(schema, "arrow_schema", ReleaseArrowSchemaPyCapsule); From 3c41c095064200a02672d89cc5ff629eaf4b0d4f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:11:14 +0300 Subject: [PATCH 2374/2374] 12.2.0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 96363e9f154..72d11ae9267 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "12.2.0.dev0" +__version__ = "12.2.0"

@@ -536,11 +534,6 @@ def draw_corners(pieslice: bool) -> None: right[3] -= r + 1 self.draw.draw_rectangle(right, ink, 1) - def _multiline_check(self, text: AnyStr) -> bool: - split_character = "\n" if isinstance(text, str) else b"\n" - - return split_character in text - def text( self, xy: tuple[float, float], @@ -565,29 +558,15 @@ def text( **kwargs: Any, ) -> None: """Draw text.""" - if embedded_color and self.mode not in ("RGB", "RGBA"): - msg = "Embedded color supported only in RGB and RGBA modes" - raise ValueError(msg) - if font is None: font = self._getfont(kwargs.get("font_size")) - - if self._multiline_check(text): - return self.multiline_text( - xy, - text, - fill, - font, - anchor, - spacing, - align, - direction, - features, - language, - stroke_width, - stroke_fill, - embedded_color, - ) + imagetext = ImageText.ImageText( + text, font, self.mode, spacing, direction, features, language + ) + if embedded_color: + imagetext.embed_color() + if stroke_width: + imagetext.stroke(stroke_width, stroke_fill) def getink(fill: _Ink | None) -> int: ink, fill_ink = self._getink(fill) @@ -596,70 +575,79 @@ def getink(fill: _Ink | None) -> int: return fill_ink return ink - def draw_text(ink: int, stroke_width: float = 0) -> None: - mode = self.fontmode - if stroke_width == 0 and embedded_color: - mode = "RGBA" - coord = [] - for i in range(2): - coord.append(int(xy[i])) - start = (math.modf(xy[0])[0], math.modf(xy[1])[0]) - try: - mask, offset = font.getmask2( # type: ignore[union-attr,misc] - text, - mode, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, - stroke_filled=True, - anchor=anchor, - ink=ink, - start=start, - *args, - **kwargs, - ) - coord = [coord[0] + offset[0], coord[1] + offset[1]] - except AttributeError: + ink = getink(fill) + if ink is None: + return + + stroke_ink = None + if imagetext.stroke_width: + stroke_ink = ( + getink(imagetext.stroke_fill) + if imagetext.stroke_fill is not None + else ink + ) + + for xy, anchor, line in imagetext._split(xy, anchor, align): + + def draw_text(ink: int, stroke_width: float = 0) -> None: + mode = self.fontmode + if stroke_width == 0 and embedded_color: + mode = "RGBA" + coord = [] + for i in range(2): + coord.append(int(xy[i])) + start = (math.modf(xy[0])[0], math.modf(xy[1])[0]) try: - mask = font.getmask( # type: ignore[misc] - text, + mask, offset = imagetext.font.getmask2( # type: ignore[union-attr,misc] + line, mode, - direction, - features, - language, - stroke_width, - anchor, - ink, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + stroke_filled=True, + anchor=anchor, + ink=ink, start=start, *args, **kwargs, ) - except TypeError: - mask = font.getmask(text) - if mode == "RGBA": - # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A - # extract mask and set text alpha - color, mask = mask, mask.getband(3) - ink_alpha = struct.pack("i", ink)[3] - color.fillband(3, ink_alpha) - x, y = coord - if self.im is not None: - self.im.paste( - color, (x, y, x + mask.size[0], y + mask.size[1]), mask - ) - else: - self.draw.draw_bitmap(coord, mask, ink) - - ink = getink(fill) - if ink is not None: - stroke_ink = None - if stroke_width: - stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink + coord = [coord[0] + offset[0], coord[1] + offset[1]] + except AttributeError: + try: + mask = imagetext.font.getmask( # type: ignore[misc] + line, + mode, + direction, + features, + language, + stroke_width, + anchor, + ink, + start=start, + *args, + **kwargs, + ) + except TypeError: + mask = imagetext.font.getmask(line) + if mode == "RGBA": + # imagetext.font.getmask2(mode="RGBA") + # returns color in RGB bands and mask in A + # extract mask and set text alpha + color, mask = mask, mask.getband(3) + ink_alpha = struct.pack("i", ink)[3] + color.fillband(3, ink_alpha) + x, y = coord + if self.im is not None: + self.im.paste( + color, (x, y, x + mask.size[0], y + mask.size[1]), mask + ) + else: + self.draw.draw_bitmap(coord, mask, ink) if stroke_ink is not None: # Draw stroked text - draw_text(stroke_ink, stroke_width) + draw_text(stroke_ink, imagetext.stroke_width) # Draw normal text if ink != stroke_ink: @@ -668,132 +656,6 @@ def draw_text(ink: int, stroke_width: float = 0) -> None: # Only draw normal text draw_text(ink) - def _prepare_multiline_text( - self, - xy: tuple[float, float], - text: AnyStr, - font: ( - ImageFont.ImageFont - | ImageFont.FreeTypeFont - | ImageFont.TransposedFont - | None - ), - anchor: str | None, - spacing: float, - align: str, - direction: str | None, - features: list[str] | None, - language: str | None, - stroke_width: float, - embedded_color: bool, - font_size: float | None, - ) -> tuple[ - ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, - list[tuple[tuple[float, float], str, AnyStr]], - ]: - if anchor is None: - anchor = "lt" if direction == "ttb" else "la" - elif len(anchor) != 2: - msg = "anchor must be a 2 character string" - raise ValueError(msg) - elif anchor[1] in "tb" and direction != "ttb": - msg = "anchor not supported for multiline text" - raise ValueError(msg) - - if font is None: - font = self._getfont(font_size) - - lines = text.split("\n" if isinstance(text, str) else b"\n") - line_spacing = ( - self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] - + stroke_width - + spacing - ) - - top = xy[1] - parts = [] - if direction == "ttb": - left = xy[0] - for line in lines: - parts.append(((left, top), anchor, line)) - left += line_spacing - else: - widths = [] - max_width: float = 0 - for line in lines: - line_width = self.textlength( - line, - font, - direction=direction, - features=features, - language=language, - embedded_color=embedded_color, - ) - widths.append(line_width) - max_width = max(max_width, line_width) - - if anchor[1] == "m": - top -= (len(lines) - 1) * line_spacing / 2.0 - elif anchor[1] == "d": - top -= (len(lines) - 1) * line_spacing - - for idx, line in enumerate(lines): - left = xy[0] - width_difference = max_width - widths[idx] - - # align by align parameter - if align in ("left", "justify"): - pass - elif align == "center": - left += width_difference / 2.0 - elif align == "right": - left += width_difference - else: - msg = 'align must be "left", "center", "right" or "justify"' - raise ValueError(msg) - - if ( - align == "justify" - and width_difference != 0 - and idx != len(lines) - 1 - ): - words = line.split(" " if isinstance(text, str) else b" ") - if len(words) > 1: - # align left by anchor - if anchor[0] == "m": - left -= max_width / 2.0 - elif anchor[0] == "r": - left -= max_width - - word_widths = [ - self.textlength( - word, - font, - direction=direction, - features=features, - language=language, - embedded_color=embedded_color, - ) - for word in words - ] - word_anchor = "l" + anchor[1] - width_difference = max_width - sum(word_widths) - for i, word in enumerate(words): - parts.append(((left, top), word_anchor, word)) - left += word_widths[i] + width_difference / (len(words) - 1) - top += line_spacing - continue - - # align left by anchor - if anchor[0] == "m": - left -= width_difference / 2.0 - elif anchor[0] == "r": - left -= width_difference - parts.append(((left, top), anchor, line)) - top += line_spacing - - return font, parts - def multiline_text( self, xy: tuple[float, float], @@ -817,9 +679,10 @@ def multiline_text( *, font_size: float | None = None, ) -> None: - font, lines = self._prepare_multiline_text( + return self.text( xy, text, + fill, font, anchor, spacing, @@ -828,25 +691,11 @@ def multiline_text( features, language, stroke_width, + stroke_fill, embedded_color, - font_size, + font_size=font_size, ) - for xy, anchor, line in lines: - self.text( - xy, - line, - fill, - font, - anchor, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, - stroke_fill=stroke_fill, - embedded_color=embedded_color, - ) - def textlength( self, text: AnyStr, @@ -864,17 +713,19 @@ def textlength( font_size: float | None = None, ) -> float: """Get the length of a given string, in pixels with 1/64 precision.""" - if self._multiline_check(text): - msg = "can't measure length of multiline text" - raise ValueError(msg) - if embedded_color and self.mode not in ("RGB", "RGBA"): - msg = "Embedded color supported only in RGB and RGBA modes" - raise ValueError(msg) - if font is None: font = self._getfont(font_size) - mode = "RGBA" if embedded_color else self.fontmode - return font.getlength(text, mode, direction, features, language) + imagetext = ImageText.ImageText( + text, + font, + self.mode, + direction=direction, + features=features, + language=language, + ) + if embedded_color: + imagetext.embed_color() + return imagetext.get_length() def textbbox( self, @@ -898,33 +749,16 @@ def textbbox( font_size: float | None = None, ) -> tuple[float, float, float, float]: """Get the bounding box of a given string, in pixels.""" - if embedded_color and self.mode not in ("RGB", "RGBA"): - msg = "Embedded color supported only in RGB and RGBA modes" - raise ValueError(msg) - if font is None: font = self._getfont(font_size) - - if self._multiline_check(text): - return self.multiline_textbbox( - xy, - text, - font, - anchor, - spacing, - align, - direction, - features, - language, - stroke_width, - embedded_color, - ) - - mode = "RGBA" if embedded_color else self.fontmode - bbox = font.getbbox( - text, mode, direction, features, language, stroke_width, anchor + imagetext = ImageText.ImageText( + text, font, self.mode, spacing, direction, features, language ) - return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1] + if embedded_color: + imagetext.embed_color() + if stroke_width: + imagetext.stroke(stroke_width) + return imagetext.get_bbox(xy, anchor, align) def multiline_textbbox( self, @@ -947,7 +781,7 @@ def multiline_textbbox( *, font_size: float | None = None, ) -> tuple[float, float, float, float]: - font, lines = self._prepare_multiline_text( + return self.textbbox( xy, text, font, @@ -959,37 +793,9 @@ def multiline_textbbox( language, stroke_width, embedded_color, - font_size, + font_size=font_size, ) - bbox: tuple[float, float, float, float] | None = None - - for xy, anchor, line in lines: - bbox_line = self.textbbox( - xy, - line, - font, - anchor, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, - embedded_color=embedded_color, - ) - if bbox is None: - bbox = bbox_line - else: - bbox = ( - min(bbox[0], bbox_line[0]), - min(bbox[1], bbox_line[1]), - max(bbox[2], bbox_line[2]), - max(bbox[3], bbox_line[3]), - ) - - if bbox is None: - return xy[0], xy[1], xy[0], xy[1] - return bbox - def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: """ diff --git a/src/PIL/ImageText.py b/src/PIL/ImageText.py new file mode 100644 index 00000000000..9bb31a1c8c0 --- /dev/null +++ b/src/PIL/ImageText.py @@ -0,0 +1,318 @@ +from __future__ import annotations + +from . import ImageFont +from ._typing import _Ink + + +class ImageText: + def __init__( + self, + text: str | bytes, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, + mode: str = "RGB", + spacing: float = 4, + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + ) -> None: + """ + :param text: String to be drawn. + :param font: Either an :py:class:`~PIL.ImageFont.ImageFont` instance, + :py:class:`~PIL.ImageFont.FreeTypeFont` instance, + :py:class:`~PIL.ImageFont.TransposedFont` instance or ``None``. If + ``None``, the default font from :py:meth:`.ImageFont.load_default` + will be used. + :param mode: The image mode this will be used with. + :param spacing: The number of pixels between lines. + :param direction: Direction of the text. It can be ``"rtl"`` (right to left), + ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional font features + that are not enabled by default, for example ``"dlig"`` or + ``"ss01"``, but can be also used to turn off default font + features, for example ``"-liga"`` to disable ligatures or + ``"-kern"`` to disable kerning. To get all supported + features, see `OpenType docs`_. + Requires libraqm. + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + """ + self.text = text + self.font = font or ImageFont.load_default() + + self.mode = mode + self.spacing = spacing + self.direction = direction + self.features = features + self.language = language + + self.embedded_color = False + + self.stroke_width: float = 0 + self.stroke_fill: _Ink | None = None + + def embed_color(self) -> None: + """ + Use embedded color glyphs (COLR, CBDT, SBIX). + """ + if self.mode not in ("RGB", "RGBA"): + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) + self.embedded_color = True + + def stroke(self, width: float = 0, fill: _Ink | None = None) -> None: + """ + :param width: The width of the text stroke. + :param fill: Color to use for the text stroke when drawing. If not given, will + default to the ``fill`` parameter from + :py:meth:`.ImageDraw.ImageDraw.text`. + """ + self.stroke_width = width + self.stroke_fill = fill + + def _get_fontmode(self) -> str: + if self.mode in ("1", "P", "I", "F"): + return "1" + elif self.embedded_color: + return "RGBA" + else: + return "L" + + def get_length(self): + """ + Returns length (in pixels with 1/64 precision) of text. + + This is the amount by which following text should be offset. + Text bounding box may extend past the length in some fonts, + e.g. when using italics or accents. + + The result is returned as a float; it is a whole number if using basic layout. + + Note that the sum of two lengths may not equal the length of a concatenated + string due to kerning. If you need to adjust for kerning, include the following + character and subtract its length. + + For example, instead of:: + + hello = ImageText.ImageText("Hello", font).get_length() + world = ImageText.ImageText("World", font).get_length() + helloworld = ImageText.ImageText("HelloWorld", font).get_length() + assert hello + world == helloworld + + use:: + + hello = ( + ImageText.ImageText("HelloW", font).get_length() - + ImageText.ImageText("W", font).get_length() + ) # adjusted for kerning + world = ImageText.ImageText("World", font).get_length() + helloworld = ImageText.ImageText("HelloWorld", font).get_length() + assert hello + world == helloworld + + or disable kerning with (requires libraqm):: + + hello = ImageText.ImageText("Hello", font, features=["-kern"]).get_length() + world = ImageText.ImageText("World", font, features=["-kern"]).get_length() + helloworld = ImageText.ImageText( + "HelloWorld", font, features=["-kern"] + ).get_length() + assert hello + world == helloworld + + :return: Either width for horizontal text, or height for vertical text. + """ + split_character = "\n" if isinstance(self.text, str) else b"\n" + if split_character in self.text: + msg = "can't measure length of multiline text" + raise ValueError(msg) + return self.font.getlength( + self.text, + self._get_fontmode(), + self.direction, + self.features, + self.language, + ) + + def _split( + self, xy: tuple[float, float], anchor: str | None, align: str + ) -> list[tuple[tuple[float, float], str, str | bytes]]: + if anchor is None: + anchor = "lt" if self.direction == "ttb" else "la" + elif len(anchor) != 2: + msg = "anchor must be a 2 character string" + raise ValueError(msg) + + lines = ( + self.text.split("\n") + if isinstance(self.text, str) + else self.text.split(b"\n") + ) + if len(lines) == 1: + return [(xy, anchor, self.text)] + + if anchor[1] in "tb" and self.direction != "ttb": + msg = "anchor not supported for multiline text" + raise ValueError(msg) + + fontmode = self._get_fontmode() + line_spacing = ( + self.font.getbbox( + "A", + fontmode, + None, + self.features, + self.language, + self.stroke_width, + )[3] + + self.stroke_width + + self.spacing + ) + + top = xy[1] + parts = [] + if self.direction == "ttb": + left = xy[0] + for line in lines: + parts.append(((left, top), anchor, line)) + left += line_spacing + else: + widths = [] + max_width: float = 0 + for line in lines: + line_width = self.font.getlength( + line, fontmode, self.direction, self.features, self.language + ) + widths.append(line_width) + max_width = max(max_width, line_width) + + if anchor[1] == "m": + top -= (len(lines) - 1) * line_spacing / 2.0 + elif anchor[1] == "d": + top -= (len(lines) - 1) * line_spacing + + idx = -1 + for line in lines: + left = xy[0] + idx += 1 + width_difference = max_width - widths[idx] + + # align by align parameter + if align in ("left", "justify"): + pass + elif align == "center": + left += width_difference / 2.0 + elif align == "right": + left += width_difference + else: + msg = 'align must be "left", "center", "right" or "justify"' + raise ValueError(msg) + + if ( + align == "justify" + and width_difference != 0 + and idx != len(lines) - 1 + ): + words = ( + line.split(" ") if isinstance(line, str) else line.split(b" ") + ) + if len(words) > 1: + # align left by anchor + if anchor[0] == "m": + left -= max_width / 2.0 + elif anchor[0] == "r": + left -= max_width + + word_widths = [ + self.font.getlength( + word, + fontmode, + self.direction, + self.features, + self.language, + ) + for word in words + ] + word_anchor = "l" + anchor[1] + width_difference = max_width - sum(word_widths) + i = 0 + for word in words: + parts.append(((left, top), word_anchor, word)) + left += word_widths[i] + width_difference / (len(words) - 1) + i += 1 + top += line_spacing + continue + + # align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + parts.append(((left, top), anchor, line)) + top += line_spacing + + return parts + + def get_bbox( + self, + xy: tuple[float, float] = (0, 0), + anchor: str | None = None, + align: str = "left", + ) -> tuple[float, float, float, float]: + """ + Returns bounding box (in pixels) of text. + + Use :py:meth:`get_length` to get the offset of following text with 1/64 pixel + precision. The bounding box includes extra margins for some fonts, e.g. italics + or accents. + + :param xy: The anchor coordinates of the text. + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left, + specifically ``la`` for horizontal text and ``lt`` for + vertical text. See :ref:`text-anchors` for details. + :param align: For multiline text, ``"left"``, ``"center"``, ``"right"`` or + ``"justify"`` determines the relative alignment of lines. Use the + ``anchor`` parameter to specify the alignment to ``xy``. + + :return: ``(left, top, right, bottom)`` bounding box + """ + bbox: tuple[float, float, float, float] | None = None + fontmode = self._get_fontmode() + for xy, anchor, line in self._split(xy, anchor, align): + bbox_line = self.font.getbbox( + line, + fontmode, + self.direction, + self.features, + self.language, + self.stroke_width, + anchor, + ) + bbox_line = ( + bbox_line[0] + xy[0], + bbox_line[1] + xy[1], + bbox_line[2] + xy[0], + bbox_line[3] + xy[1], + ) + if bbox is None: + bbox = bbox_line + else: + bbox = ( + min(bbox[0], bbox_line[0]), + min(bbox[1], bbox_line[1]), + max(bbox[2], bbox_line[2]), + max(bbox[3], bbox_line[3]), + ) + + if bbox is None: + return xy[0], xy[1], xy[0], xy[1] + return bbox diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 373938e71e0..685c425d5e8 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -38,6 +38,8 @@ def __class_getitem__(cls, item: Any) -> type[bool]: return bool +_Ink = Union[float, tuple[int, ...], str] + Coords = Union[Sequence[float], Sequence[Sequence[float]]] From 969e4687497d447581e950ed26043a988f50e21b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 21 Jul 2025 13:07:38 +1000 Subject: [PATCH 1961/2374] Allow ImageDraw text() to use ImageText --- Tests/test_imagetext.py | 35 +++++++++++++++++++++++++++++++++-- docs/reference/ImageText.rst | 6 +++++- src/PIL/ImageDraw.py | 23 +++++++++++++---------- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/Tests/test_imagetext.py b/Tests/test_imagetext.py index 3a3a58975d1..b58d048b5df 100644 --- a/Tests/test_imagetext.py +++ b/Tests/test_imagetext.py @@ -2,9 +2,9 @@ import pytest -from PIL import ImageFont, ImageText +from PIL import Image, ImageDraw, ImageFont, ImageText -from .helper import skip_unless_feature +from .helper import assert_image_similar_tofile, skip_unless_feature FONT_PATH = "Tests/fonts/FreeMono.ttf" @@ -39,3 +39,34 @@ def test_get_bbox(font: ImageFont.FreeTypeFont) -> None: assert ImageText.ImageText("M", font).get_bbox() == (0, 4, 12, 16) assert ImageText.ImageText("y", font).get_bbox() == (0, 7, 12, 20) assert ImageText.ImageText("a", font).get_bbox() == (0, 7, 12, 16) + + +def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: + font = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) + text = ImageText.ImageText("Hello World!", font) + text.embed_color() + + im = Image.new("RGB", (300, 64), "white") + draw = ImageDraw.Draw(im) + draw.text((10, 10), text, "#fa6") + + assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1) + + +@skip_unless_feature("freetype2") +def test_stroke() -> None: + for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype(FONT_PATH, 120) + text = ImageText.ImageText("A", font) + text.stroke(2, stroke_fill) + + # Act + draw.text((12, 12), text, "#f00") + + # Assert + assert_image_similar_tofile( + im, "Tests/images/imagedraw_stroke_" + suffix + ".png", 3.1 + ) diff --git a/docs/reference/ImageText.rst b/docs/reference/ImageText.rst index ad5439751f9..fa55b4f306e 100644 --- a/docs/reference/ImageText.rst +++ b/docs/reference/ImageText.rst @@ -6,7 +6,7 @@ The :py:mod:`~PIL.ImageText` module defines a class with the same name. Instances of this class provide a way to use fonts with text strings or bytes. The result is a -simple API to apply styling to pieces of text and measure them. +simple API to apply styling to pieces of text and measure or draw them. Example ------- @@ -23,6 +23,10 @@ Example print(text.get_length()) # 154.0 print(text.get_bbox()) # (-2, 3, 156, 22) + im = Image.new("RGB", text.get_bbox()[2:]) + d = ImageDraw.Draw(im) + d.text((0, 0), text, "#f00") + Methods ------- diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 35ecbfb78b4..852e0269849 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -537,7 +537,7 @@ def draw_corners(pieslice: bool) -> None: def text( self, xy: tuple[float, float], - text: AnyStr, + text: AnyStr | ImageText.ImageText, fill: _Ink | None = None, font: ( ImageFont.ImageFont @@ -558,15 +558,18 @@ def text( **kwargs: Any, ) -> None: """Draw text.""" - if font is None: - font = self._getfont(kwargs.get("font_size")) - imagetext = ImageText.ImageText( - text, font, self.mode, spacing, direction, features, language - ) - if embedded_color: - imagetext.embed_color() - if stroke_width: - imagetext.stroke(stroke_width, stroke_fill) + if isinstance(text, ImageText.ImageText): + imagetext = text + else: + if font is None: + font = self._getfont(kwargs.get("font_size")) + imagetext = ImageText.ImageText( + text, font, self.mode, spacing, direction, features, language + ) + if embedded_color: + imagetext.embed_color() + if stroke_width: + imagetext.stroke(stroke_width, stroke_fill) def getink(fill: _Ink | None) -> int: ink, fill_ink = self._getink(fill) From 63163d065d632cb75466d554fb1d6ea27cc43577 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Jul 2025 19:59:47 +1000 Subject: [PATCH 1962/2374] Removed WebP feature handling --- Tests/test_features.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index 520c25b4645..7af3fffeafe 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -18,11 +18,7 @@ def test_check() -> None: for codec in features.codecs: assert features.check_codec(codec) == features.check(codec) for feature in features.features: - if "webp" in feature: - with pytest.warns(DeprecationWarning, match="webp"): - assert features.check_feature(feature) == features.check(feature) - else: - assert features.check_feature(feature) == features.check(feature) + assert features.check_feature(feature) == features.check(feature) def test_version() -> None: @@ -48,11 +44,7 @@ def test(name: str, function: Callable[[str], str | None]) -> None: for codec in features.codecs: test(codec, features.version_codec) for feature in features.features: - if "webp" in feature: - with pytest.warns(DeprecationWarning, match="webp"): - test(feature, features.version_feature) - else: - test(feature, features.version_feature) + test(feature, features.version_feature) @skip_unless_feature("libjpeg_turbo") From ec6d5efe4d02dc6d68e569abfd7523e21a89539f Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Sat, 26 Jul 2025 08:33:11 +0100 Subject: [PATCH 1963/2374] Deprecate ImageCmsProfile product_name and product_info (#8995) Co-authored-by: Andrew Murray --- Tests/test_imagecms.py | 14 ++++++++++++++ docs/deprecations.rst | 9 +++++++++ docs/releasenotes/12.0.0.rst | 8 +++++--- src/PIL/ImageCms.py | 14 ++++++++++---- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 55a4a87fb2f..8b5d88ac883 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -690,3 +690,17 @@ def test_cmyk_lab() -> None: im = Image.new("CMYK", (1, 1)) converted_im = im.convert("LAB") assert converted_im.getpixel((0, 0)) == (255, 128, 128) + + +def test_deprecation() -> None: + profile = ImageCmsProfile(ImageCms.createProfile("sRGB")) + with pytest.warns( + DeprecationWarning, match="ImageCms.ImageCmsProfile.product_name" + ): + profile.product_name + with pytest.warns( + DeprecationWarning, match="ImageCms.ImageCmsProfile.product_info" + ): + profile.product_info + with pytest.raises(AttributeError): + profile.this_attribute_does_not_exist diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4e65dc8078a..3f95cf7f545 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -52,6 +52,15 @@ another mode before saving:: im = Image.new("I", (1, 1)) im.convert("I;16").save("out.png") +ImageCms.ImageCmsProfile.product_name and .product_info +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 12.0.0 + +``ImageCms.ImageCmsProfile.product_name`` and the corresponding +``.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. + Removed features ---------------- diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index 68b6644438f..6c0cd4dba01 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -110,10 +110,12 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). Deprecations ============ -TODO -^^^^ +ImageCms.ImageCmsProfile.product_name and .product_info +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +``ImageCms.ImageCmsProfile.product_name`` and the corresponding +``.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. API changes =========== diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index d3555694a51..513e28acf33 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -23,9 +23,10 @@ import sys from enum import IntEnum, IntFlag from functools import reduce -from typing import Literal, SupportsFloat, SupportsInt, Union +from typing import Any, Literal, SupportsFloat, SupportsInt, Union from . import Image +from ._deprecate import deprecate from ._typing import SupportsRead try: @@ -233,9 +234,7 @@ def __init__(self, profile: str | SupportsRead[bytes] | core.CmsProfile) -> None low-level profile object """ - self.filename = None - self.product_name = None # profile.product_name - self.product_info = None # profile.product_info + self.filename: str | None = None if isinstance(profile, str): if sys.platform == "win32": @@ -256,6 +255,13 @@ def __init__(self, profile: str | SupportsRead[bytes] | core.CmsProfile) -> None msg = "Invalid type for Profile" # type: ignore[unreachable] raise TypeError(msg) + def __getattr__(self, name: str) -> Any: + if name in ("product_name", "product_info"): + deprecate(f"ImageCms.ImageCmsProfile.{name}", 13) + return None + msg = f"'{self.__class__.__name__}' object has no attribute '{name}'" + raise AttributeError(msg) + def tobytes(self) -> bytes: """ Returns the profile in a format suitable for embedding in From 7afbafd1e2ae9a3fe822b422cebe94092ad68b9c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Jul 2025 19:21:50 +1000 Subject: [PATCH 1964/2374] Support saving variable length rational TIFF tags --- Tests/test_file_libtiff.py | 12 ++++++++++++ src/encode.c | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 958e2749f61..d4d50e5b4a2 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -355,6 +355,18 @@ def test_subifd(self, tmp_path: Path) -> None: # Should not segfault im.save(outfile) + def test_whitepoint_tag( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) + + out = tmp_path / "temp.tif" + hopper().save(out, tiffinfo={318: (0.3127, 0.3289)}) + + with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) + assert reloaded.tag_v2[318] == pytest.approx((0.3127, 0.3289)) + def test_xmlpacket_tag( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: diff --git a/src/encode.c b/src/encode.c index e56494036ff..a8da323188f 100644 --- a/src/encode.c +++ b/src/encode.c @@ -922,6 +922,18 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { ); free(av); } + } else if (type == TIFF_RATIONAL) { + FLOAT32 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(FLOAT32)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (FLOAT32)PyFloat_AsDouble(PyTuple_GetItem(value, i)); + } + status = + ImagingLibTiffSetField(&encoder->state, (ttag_t)key_int, av); + free(av); + } } } else { if (type == TIFF_SHORT) { From 7dbcb32cbe524a8ec4c12f21c762cd7153b2b03b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 26 Jul 2025 19:32:57 +1000 Subject: [PATCH 1965/2374] Update cygwin/cygwin-install-action action to v6 (#9108) Co-authored-by: Andrew Murray --- .github/workflows/test-cygwin.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index abfeaa77f9c..581cd63704b 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -52,7 +52,7 @@ jobs: persist-credentials: false - name: Install Cygwin - uses: cygwin/cygwin-install-action@v5 + uses: cygwin/cygwin-install-action@v6 with: packages: > gcc-g++ @@ -89,10 +89,6 @@ jobs: with: dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' - - name: Select Python version - run: | - ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 - - name: pip cache uses: actions/cache@v4 with: From 53b6d57b730a68ea58680483f8628c5e25301a1e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Jul 2025 19:39:54 +1000 Subject: [PATCH 1966/2374] Drop support for PyPy3.10 --- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 6d8acc44f25..766c506e761 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"] + python-version: ["pypy3.11", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"] architecture: ["x64"] include: # Test the oldest Python on 32-bit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b4b5162281c..d18023dbc97 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,6 @@ jobs: ] python-version: [ "pypy3.11", - "pypy3.10", "3.14t", "3.14", "3.13t", From a6acc67660f4d2a157341c05d11e47e36c79802d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Jul 2025 21:00:26 +1000 Subject: [PATCH 1967/2374] Always check XMLPacket value --- Tests/test_file_libtiff.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 958e2749f61..f61f79f1704 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -365,8 +365,7 @@ def test_xmlpacket_tag( with Image.open(out) as reloaded: 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: # issue #1765 From 283dcfc024113f6d0d8bcbb216d1735cb05a16e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Jul 2025 23:39:11 +1000 Subject: [PATCH 1968/2374] Removed unused code --- src/decode.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/decode.c b/src/decode.c index 03db1ce3516..e7a6e632370 100644 --- a/src/decode.c +++ b/src/decode.c @@ -870,8 +870,6 @@ PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args) { if (strcmp(format, "j2k") == 0) { codec_format = OPJ_CODEC_J2K; - } else if (strcmp(format, "jpt") == 0) { - codec_format = OPJ_CODEC_JPT; } else if (strcmp(format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; } else { From 98d38a3bffe572459939cbb6ab730229b4a5a833 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:52:06 +1000 Subject: [PATCH 1969/2374] Updated libpng to 1.6.50 (#9058) --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 6b5aedb697f..4519271b9d3 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -95,7 +95,7 @@ ARCHIVE_SDIR=pillow-depends-main # you change those versions, ensure the patch is also updated. FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=11.2.1 -LIBPNG_VERSION=1.6.49 +LIBPNG_VERSION=1.6.50 JPEGTURBO_VERSION=3.1.1 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 2307fc8b294..fbff0daf2b2 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -121,7 +121,7 @@ def cmd_msbuild( "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.3.4", - "LIBPNG": "1.6.49", + "LIBPNG": "1.6.50", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.3", "TIFF": "4.7.0", From e8b3c17ebc90f36c8ec326e765246814c21f1f48 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Jul 2025 07:28:03 +1000 Subject: [PATCH 1970/2374] Updated documentation --- docs/deprecations.rst | 7 +++++-- docs/releasenotes/11.3.0.rst | 7 +++++++ docs/releasenotes/12.0.0.rst | 10 +++++++--- src/PIL/Image.py | 3 ++- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 2365545656f..851f3e8d811 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -42,8 +42,11 @@ Image.fromarray mode parameter .. deprecated:: 11.3.0 -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. +Using the ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` to change data types +has been deprecated. 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index 409d50295dd..5c04a037316 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -29,6 +29,13 @@ Image.fromarray mode parameter 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. +.. 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index 68b6644438f..19508b08a8a 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -134,7 +134,11 @@ TODO Other changes ============= -TODO -^^^^ +Image.fromarray mode parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +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. diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c98630cc24f..20917b1a414 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3253,7 +3253,8 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: :param obj: Object with array interface :param mode: Optional mode to use when reading ``obj``. Since pixel values do not 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. + grayscale L mode data within a P mode image, or read RGB data as YCbCr for + example. See: :ref:`concept-modes` for general information about modes. :returns: An image object. From bae97e1a2b75a1e3c01efc168d10b8d7ecdf3392 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:50:45 +1000 Subject: [PATCH 1971/2374] Update dependency cibuildwheel to v3.1.2 (#9118) --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index e1eb52eb8ae..823671828f7 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.0.1 +cibuildwheel==3.1.2 From ba5f81fb6b4bd143b2ceba6875b33870eaa366ce Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:23:39 +0300 Subject: [PATCH 1972/2374] Add support for Python 3.14 (#9120) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/installation/newer-versions.csv | 19 ++++++++++--------- docs/releasenotes/12.0.0.rst | 10 +++++++--- pyproject.toml | 3 ++- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/installation/newer-versions.csv b/docs/installation/newer-versions.csv index 19816af58da..e948dd5400e 100644 --- a/docs/installation/newer-versions.csv +++ b/docs/installation/newer-versions.csv @@ -1,9 +1,10 @@ -Python,3.13,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5 -Pillow >= 11,Yes,Yes,Yes,Yes,Yes,,,, -Pillow 10.1 - 10.4,,Yes,Yes,Yes,Yes,Yes,,, -Pillow 10.0,,,Yes,Yes,Yes,Yes,,, -Pillow 9.3 - 9.5,,,Yes,Yes,Yes,Yes,Yes,, -Pillow 9.0 - 9.2,,,,Yes,Yes,Yes,Yes,, -Pillow 8.3.2 - 8.4,,,,Yes,Yes,Yes,Yes,Yes, -Pillow 8.0 - 8.3.1,,,,,Yes,Yes,Yes,Yes, -Pillow 7.0 - 7.2,,,,,,Yes,Yes,Yes,Yes +Python,3.14,3.13,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5 +Pillow 12,Yes,Yes,Yes,Yes,Yes,,,,, +Pillow 11,,Yes,Yes,Yes,Yes,Yes,,,, +Pillow 10.1 - 10.4,,,Yes,Yes,Yes,Yes,Yes,,, +Pillow 10.0,,,,Yes,Yes,Yes,Yes,,, +Pillow 9.3 - 9.5,,,,Yes,Yes,Yes,Yes,Yes,, +Pillow 9.0 - 9.2,,,,,Yes,Yes,Yes,Yes,, +Pillow 8.3.2 - 8.4,,,,,Yes,Yes,Yes,Yes,Yes, +Pillow 8.0 - 8.3.1,,,,,,Yes,Yes,Yes,Yes, +Pillow 7.0 - 7.2,,,,,,,Yes,Yes,Yes,Yes diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index 6c0cd4dba01..46cf64cf1f7 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -136,7 +136,11 @@ TODO Other changes ============= -TODO -^^^^ +Python 3.14 +^^^^^^^^^^^ -TODO +Pillow 11.3.0 had wheels built against Python 3.14 beta, available as a preview to help +others prepare for 3.14, and to ensure Pillow could be used immediately at the release +of 3.14.0 final (2025-10-07, :pep:`745`). + +Pillow 12.0.0 now officially supports Python 3.14. diff --git a/pyproject.toml b/pyproject.toml index 4e8623118ba..3693ddb8da0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Multimedia :: Graphics", @@ -206,7 +207,7 @@ lint.isort.required-imports = [ ] [tool.pyproject-fmt] -max_supported_python = "3.13" +max_supported_python = "3.14" [tool.pytest.ini_options] addopts = "-ra --color=auto" From 98d6c3bf8818849e2414ef4de8c9e02b03de3886 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 1 Aug 2025 08:22:28 +0800 Subject: [PATCH 1973/2374] Restore pyroma test for iOS (#9116) Co-authored-by: Andrew Murray --- Tests/test_image_access.py | 6 +++++- Tests/test_pyroma.py | 25 ++++++++++++++++++++++++- pyproject.toml | 2 +- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index a847264d27e..07c12594a8c 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -317,4 +317,8 @@ def test_embeddable(self) -> None: assert process.returncode == 0 def teardown_method(self) -> None: - os.remove("embed_pil.c") + try: + os.remove("embed_pil.c") + except FileNotFoundError: + # If the test was skipped or failed, the file won't exist + pass diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index c2f7fe22ecb..35f3fd076da 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,5 +1,7 @@ from __future__ import annotations +from importlib.metadata import metadata + import pytest from PIL import __version__ @@ -7,9 +9,30 @@ pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") +def map_metadata_keys(metadata): + # 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 implementation is constructed from the relevant logic from + # Pyroma 5.0's `build_metadata()` implementation. This has been submitted + # upstream to Pyroma as https://github.com/regebro/pyroma/pull/116, + # so it may be possible to simplify this test in future. + data = {} + for key in set(metadata.keys()): + value = metadata.get_all(key) + key = pyroma.projectdata.normalize(key) + + if len(value) == 1: + value = value[0] + if value.strip() == "UNKNOWN": + continue + + data[key] = value + return data + + def test_pyroma() -> None: # Arrange - data = pyroma.projectdata.get_data(".") + data = map_metadata_keys(metadata("Pillow")) # Act rating = pyroma.ratings.rate(data) diff --git a/pyproject.toml b/pyproject.toml index 3693ddb8da0..4980a9cb880 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ optional-dependencies.tests = [ "markdown2", "olefile", "packaging", - "pyroma", + "pyroma>=5", "pytest", "pytest-cov", "pytest-timeout", From 19829c3d95e9ee581d5ecd3f46e6c0af878482f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Jul 2025 18:55:45 +1000 Subject: [PATCH 1974/2374] Updated harfbuzz to 11.3.3 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4519271b9d3..f2b9a7f40cb 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -94,7 +94,7 @@ ARCHIVE_SDIR=pillow-depends-main # annotations have a source code patch that is required for some platforms. If # you change those versions, ensure the patch is also updated. FREETYPE_VERSION=2.13.3 -HARFBUZZ_VERSION=11.2.1 +HARFBUZZ_VERSION=11.3.3 LIBPNG_VERSION=1.6.50 JPEGTURBO_VERSION=3.1.1 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index fbff0daf2b2..7067fc3c432 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ def cmd_msbuild( "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "11.2.1", + "HARFBUZZ": "11.3.3", "JPEGTURBO": "3.1.1", "LCMS2": "2.17", "LIBAVIF": "1.3.0", From 27a7582b3541ad92df9900c2a9edcfe91c44a313 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Aug 2025 11:40:35 +1000 Subject: [PATCH 1975/2374] Moved imports into TYPE_CHECKING --- Tests/test_imagecms.py | 5 ++++- src/PIL/GimpPaletteFile.py | 5 ++++- src/PIL/Image.py | 11 ++++++++--- src/PIL/Jpeg2KImagePlugin.py | 8 ++++++-- src/PIL/JpegImagePlugin.py | 3 ++- src/PIL/PngImagePlugin.py | 6 ++++-- src/PIL/WebPImagePlugin.py | 4 +++- 7 files changed, 31 insertions(+), 11 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 8b5d88ac883..46c1baa2d13 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -7,7 +7,7 @@ import sys from io import BytesIO from pathlib import Path -from typing import Any, Literal, cast +from typing import Literal, cast import pytest @@ -31,6 +31,9 @@ # Skipped via setup_module() pass +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Any SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc" HAVE_PROFILE = os.path.exists(SRGB) diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 379ffd73918..016257d3dd2 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -17,7 +17,10 @@ import re from io import BytesIO -from typing import IO + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import IO class GimpPaletteFile: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 262b5478b62..b7c185e0d6e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -38,10 +38,9 @@ import sys import tempfile import warnings -from collections.abc import Callable, Iterator, MutableMapping, Sequence +from collections.abc import MutableMapping from enum import IntEnum -from types import ModuleType -from typing import IO, Any, Literal, Protocol, cast +from typing import IO, Protocol, cast # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -64,6 +63,12 @@ except ImportError: ElementTree = None +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable, Iterator, Sequence + from types import ModuleType + from typing import Any, Literal + logger = logging.getLogger(__name__) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index e0f4ecae595..4c85dd4e281 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -18,11 +18,15 @@ import io import os import struct -from collections.abc import Callable -from typing import IO, cast +from typing import cast from . import Image, ImageFile, ImagePalette, _binary +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from typing import IO + class BoxReader: """ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index efe8eff3b29..0d110035e1a 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -42,7 +42,6 @@ import sys import tempfile import warnings -from typing import IO, Any from . import Image, ImageFile from ._binary import i16be as i16 @@ -53,6 +52,8 @@ TYPE_CHECKING = False if TYPE_CHECKING: + from typing import IO, Any + from .MpoImagePlugin import MpoImageFile # diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 1b9a89aef0d..d0f22f812e5 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -38,9 +38,8 @@ import struct import warnings import zlib -from collections.abc import Callable from enum import IntEnum -from typing import IO, Any, NamedTuple, NoReturn, cast +from typing import IO, NamedTuple, cast from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16be as i16 @@ -53,6 +52,9 @@ TYPE_CHECKING = False if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, NoReturn + from . import _imaging logger = logging.getLogger(__name__) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 1716a18ccda..2847fed20a0 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -1,7 +1,6 @@ from __future__ import annotations from io import BytesIO -from typing import IO, Any from . import Image, ImageFile @@ -12,6 +11,9 @@ except ImportError: SUPPORTED = False +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import IO, Any _VP8_MODES_BY_IDENTIFIER = { b"VP8 ": "RGB", From 0620daf860d4d7a5cff6e29079ff1f9773423dc4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Aug 2025 13:10:18 +1000 Subject: [PATCH 1976/2374] Renamed variable to not shadow import --- Tests/test_pyroma.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 35f3fd076da..5871a72134a 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -9,7 +9,7 @@ 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. # 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 @@ -17,8 +17,8 @@ def map_metadata_keys(metadata): # upstream to Pyroma as https://github.com/regebro/pyroma/pull/116, # so it may be possible to simplify this test in future. data = {} - for key in set(metadata.keys()): - value = metadata.get_all(key) + for key in set(md.keys()): + value = md.get_all(key) key = pyroma.projectdata.normalize(key) if len(value) == 1: From ae6bb29b8207023c704490405c254808b04643dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Aug 2025 18:35:16 +1000 Subject: [PATCH 1977/2374] Removed support for NumPy 1.20 when type checking --- src/PIL/_typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 373938e71e0..e940452600f 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -12,8 +12,8 @@ try: import numpy.typing as npt - NumpyArray = npt.NDArray[Any] # requires numpy>=1.21 - except (ImportError, AttributeError): + NumpyArray = npt.NDArray[Any] + except ImportError: pass if sys.version_info >= (3, 13): From 2ab301dcc95bee3b655aa0a2299907271b7a435a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 2 Aug 2025 15:02:20 +0300 Subject: [PATCH 1978/2374] Drop support for Python 3.9 (#9119) Co-authored-by: Andrew Murray Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .ci/install.sh | 59 ++++------ .github/mergify.yml | 1 - .github/workflows/test-cygwin.yml | 150 ------------------------- .github/workflows/test-windows.yml | 4 +- .github/workflows/test.yml | 11 +- README.md | 3 - Tests/helper.py | 9 +- Tests/test_features.py | 5 +- Tests/test_format_hsv.py | 5 +- Tests/test_image_transform.py | 5 +- Tests/test_imagechops.py | 6 +- Tests/test_imagecms.py | 7 +- Tests/test_imagedraw.py | 9 +- Tests/test_qt_image_qapplication.py | 46 ++++---- Tests/test_qt_image_toqimage.py | 8 +- Tests/test_shell_injection.py | 8 +- checks/check_imaging_leaks.py | 3 +- docs/index.rst | 4 - docs/installation/platform-support.rst | 24 ++-- docs/reference/internal_modules.rst | 5 - docs/releasenotes/12.0.0.rst | 6 + pyproject.toml | 10 +- src/PIL/GifImagePlugin.py | 6 +- src/PIL/GimpGradientFile.py | 6 +- src/PIL/ImageDraw.py | 17 +-- src/PIL/ImageFilter.py | 7 +- src/PIL/ImageMath.py | 8 +- src/PIL/ImageQt.py | 21 ++-- src/PIL/ImageSequence.py | 6 +- src/PIL/PcfFontFile.py | 6 +- src/PIL/PdfParser.py | 17 +-- src/PIL/TiffImagePlugin.py | 10 +- src/PIL/_imagingcms.pyi | 8 +- src/PIL/_imagingft.pyi | 3 +- src/PIL/_typing.py | 19 +--- src/PIL/_util.py | 7 +- tox.ini | 4 +- 37 files changed, 193 insertions(+), 340 deletions(-) delete mode 100644 .github/workflows/test-cygwin.yml diff --git a/.ci/install.sh b/.ci/install.sh index acb84f046d1..2178c664626 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -13,24 +13,21 @@ aptget_update() return 1 fi } -if [[ $(uname) != CYGWIN* ]]; then - aptget_update || aptget_update retry || aptget_update retry -fi +aptget_update || aptget_update retry || aptget_update retry set -e -if [[ $(uname) != CYGWIN* ]]; then - sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\ - ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ - cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ - sway wl-clipboard libopenblas-dev nasm -fi +sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\ + ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ + cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ + sway wl-clipboard libopenblas-dev nasm python3 -m pip install --upgrade pip python3 -m pip install --upgrade wheel python3 -m pip install coverage python3 -m pip install defusedxml python3 -m pip install ipython +python3 -m pip install numpy python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov @@ -40,36 +37,24 @@ python3 -m pip install pyroma # fails on beta 3.14 and PyPy python3 -m pip install --only-binary=:all: pyarrow || true -if [[ $(uname) != CYGWIN* ]]; then - python3 -m pip install numpy - - # PyQt6 doesn't support PyPy3 - if [[ $GHA_PYTHON_VERSION == 3.* ]]; then - sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 - # TODO Update condition when pyqt6 supports free-threading - if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi - fi - - # Pyroma uses non-isolated build and fails with old setuptools - if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then - # To match pyproject.toml - python3 -m pip install "setuptools>=77" - fi +# PyQt6 doesn't support PyPy3 +if [[ $GHA_PYTHON_VERSION == 3.* ]]; then + sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 + # TODO Update condition when pyqt6 supports free-threading + if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi +fi - # webp - pushd depends && ./install_webp.sh && popd +# webp +pushd depends && ./install_webp.sh && popd - # libimagequant - pushd depends && ./install_imagequant.sh && popd +# libimagequant +pushd depends && ./install_imagequant.sh && popd - # raqm - pushd depends && ./install_raqm.sh && popd +# raqm +pushd depends && ./install_raqm.sh && popd - # libavif - pushd depends && ./install_libavif.sh && popd +# libavif +pushd depends && ./install_libavif.sh && popd - # extra test images - pushd depends && ./install_extra_test_images.sh && popd -else - cd depends && ./install_extra_test_images.sh && cd .. -fi +# extra test images +pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/mergify.yml b/.github/mergify.yml index 9bb089615be..14222db1094 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -8,7 +8,6 @@ pull_request_rules: - status-success=Docker Test Successful - status-success=Windows Test Successful - status-success=MinGW - - status-success=Cygwin Test Successful actions: merge: method: merge diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml deleted file mode 100644 index 581cd63704b..00000000000 --- a/.github/workflows/test-cygwin.yml +++ /dev/null @@ -1,150 +0,0 @@ -name: Test Cygwin - -on: - push: - branches: - - "**" - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - COVERAGE_CORE: sysmon - -jobs: - build: - runs-on: windows-latest - strategy: - fail-fast: false - matrix: - python-minor-version: [9] - - timeout-minutes: 40 - - name: Python 3.${{ matrix.python-minor-version }} - - steps: - - name: Fix line endings - run: | - git config --global core.autocrlf input - - - name: Checkout Pillow - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Install Cygwin - uses: cygwin/cygwin-install-action@v6 - with: - packages: > - gcc-g++ - ghostscript - git - ImageMagick - jpeg - libfreetype-devel - libimagequant-devel - libjpeg-devel - liblapack-devel - liblcms2-devel - libopenjp2-devel - libraqm-devel - libtiff-devel - libwebp-devel - libxcb-devel - libxcb-xinerama0 - make - netpbm - perl - python3${{ matrix.python-minor-version }}-cython - python3${{ matrix.python-minor-version }}-devel - python3${{ matrix.python-minor-version }}-ipython - python3${{ matrix.python-minor-version }}-numpy - python3${{ matrix.python-minor-version }}-sip - python3${{ matrix.python-minor-version }}-tkinter - wget - xorg-server-extra - zlib-devel - - - name: Add Lapack to PATH - uses: egor-tensin/cleanup-path@v4 - with: - dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' - - - name: pip cache - uses: actions/cache@v4 - with: - path: 'C:\cygwin\home\runneradmin\.cache\pip' - key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }} - restore-keys: | - ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}- - - - name: Build system information - run: | - dash.exe -c "python3 .github/workflows/system-info.py" - - - name: Install dependencies - run: | - bash.exe .ci/install.sh - - - name: Build - shell: bash.exe -eo pipefail -o igncr "{0}" - run: | - .ci/build.sh - - - name: Test - run: | - bash.exe xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh - - - name: Prepare to upload errors - if: failure() - run: | - dash.exe -c "mkdir -p Tests/errors" - - - name: Upload errors - uses: actions/upload-artifact@v4 - if: failure() - with: - name: errors - path: Tests/errors - - - name: After success - run: | - bash.exe .ci/after_success.sh - rm C:\cygwin\bin\bash.EXE - - - name: Upload coverage - uses: codecov/codecov-action@v5 - with: - files: ./coverage.xml - flags: GHA_Cygwin - name: Cygwin Python 3.${{ matrix.python-minor-version }} - token: ${{ secrets.CODECOV_ORG_TOKEN }} - - success: - permissions: - contents: none - needs: build - runs-on: ubuntu-latest - name: Cygwin Test Successful - steps: - - name: Success - run: echo Cygwin Test Successful diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 766c506e761..c80bb6eb602 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -35,11 +35,11 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.11", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"] + python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14"] architecture: ["x64"] include: # Test the oldest Python on 32-bit - - { python-version: "3.9", architecture: "x86" } + - { python-version: "3.10", architecture: "x86" } timeout-minutes: 45 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d18023dbc97..c075f04d7cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,18 +49,17 @@ jobs: "3.12", "3.11", "3.10", - "3.9", ] include: - - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - - { python-version: "3.10", PYTHONOPTIMIZE: 2 } + - { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } + - { python-version: "3.11", PYTHONOPTIMIZE: 2 } # Free-threaded - { python-version: "3.14t", disable-gil: true } - { python-version: "3.13t", disable-gil: true } - # M1 only available for 3.10+ - - { os: "macos-13", python-version: "3.9" } + # Intel + - { os: "macos-13", python-version: "3.10" } exclude: - - { os: "macos-latest", python-version: "3.9" } + - { os: "macos-latest", python-version: "3.10" } runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} diff --git a/README.md b/README.md index 365d356a00c..8585ef6cbd4 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,6 @@ As of 2019, Pillow development is GitHub Actions build status (Test MinGW) - GitHub Actions build status (Test Cygwin) GitHub Actions build status (Test Docker) diff --git a/Tests/helper.py b/Tests/helper.py index df99f5f5571..e0dc8a9d4aa 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -10,17 +10,20 @@ import subprocess import sys import tempfile -from collections.abc import Sequence from functools import lru_cache from io import BytesIO -from pathlib import Path -from typing import Any, Callable import pytest from packaging.version import parse as parse_version from PIL import Image, ImageFile, ImageMath, features +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from pathlib import Path + from typing import Any + logger = logging.getLogger(__name__) uploader = None diff --git a/Tests/test_features.py b/Tests/test_features.py index d9212daee97..93d803fc133 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -2,7 +2,6 @@ import io import re -from typing import Callable import pytest @@ -10,6 +9,10 @@ from .helper import skip_unless_feature +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + def test_check() -> None: # Check the correctness of the convenience function diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 9cbf18566ca..861eccc1170 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -2,12 +2,15 @@ import colorsys import itertools -from typing import Callable from PIL import Image from .helper import assert_image_similar, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + def int_to_float(i: int) -> float: return i / 255 diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 0429eb99d83..7cf52ddbabe 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,7 +1,6 @@ from __future__ import annotations import math -from typing import Callable import pytest @@ -9,6 +8,10 @@ from .helper import assert_image_equal, assert_image_similar, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + class TestImageTransform: def test_sanity(self) -> None: diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 4309214f5cc..61812ca7dc9 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,11 +1,13 @@ from __future__ import annotations -from typing import Callable - from PIL import Image, ImageChops from .helper import assert_image_equal, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + BLACK = (0, 0, 0) BROWN = (127, 64, 0) CYAN = (0, 255, 255) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 46c1baa2d13..5fd7caa7cc7 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -211,9 +211,10 @@ def test_exceptions() -> None: ImageCms.getProfileName(None) # type: ignore[arg-type] skip_missing() - # Python <= 3.9: "an integer is required (got type NoneType)" - # Python > 3.9: "'NoneType' object cannot be interpreted as an integer" - with pytest.raises(ImageCms.PyCMSError, match="integer"): + with pytest.raises( + ImageCms.PyCMSError, + match="'NoneType' object cannot be interpreted as an integer", + ): ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type] diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index e1dcbc52c61..406d965b4e5 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,13 +1,10 @@ from __future__ import annotations import os.path -from collections.abc import Sequence -from typing import Callable import pytest from PIL import Image, ImageColor, ImageDraw, ImageFont, features -from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -17,6 +14,12 @@ skip_unless_feature, ) +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + from PIL._typing import Coords + BLACK = (0, 0, 0) WHITE = (255, 255, 255) GRAY = (190, 190, 190) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 82a3e074120..b31e2a4ef25 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,8 +1,5 @@ from __future__ import annotations -from pathlib import Path -from typing import Union - import pytest from PIL import Image, ImageQt @@ -11,18 +8,8 @@ TYPE_CHECKING = False if TYPE_CHECKING: - import PyQt6 - import PySide6 - - QApplication = Union[PyQt6.QtWidgets.QApplication, PySide6.QtWidgets.QApplication] - QHBoxLayout = Union[PyQt6.QtWidgets.QHBoxLayout, PySide6.QtWidgets.QHBoxLayout] - QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage] - QLabel = Union[PyQt6.QtWidgets.QLabel, PySide6.QtWidgets.QLabel] - QPainter = Union[PyQt6.QtGui.QPainter, PySide6.QtGui.QPainter] - QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap] - QPoint = Union[PyQt6.QtCore.QPoint, PySide6.QtCore.QPoint] - QRegion = Union[PyQt6.QtGui.QRegion, PySide6.QtGui.QRegion] - QWidget = Union[PyQt6.QtWidgets.QWidget, PySide6.QtWidgets.QWidget] + from pathlib import Path + if ImageQt.qt_is_installed: from PIL.ImageQt import QPixmap @@ -32,11 +19,16 @@ from PyQt6.QtGui import QImage, QPainter, QRegion from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget elif ImageQt.qt_version == "side6": - from PySide6.QtCore import QPoint - from PySide6.QtGui import QImage, QPainter, QRegion - from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget - - class Example(QWidget): # type: ignore[misc] + from PySide6.QtCore import QPoint # type: ignore[assignment] + from PySide6.QtGui import QImage, QPainter, QRegion # type: ignore[assignment] + from PySide6.QtWidgets import ( # type: ignore[assignment] + QApplication, + QHBoxLayout, + QLabel, + QWidget, + ) + + class Example(QWidget): def __init__(self) -> None: super().__init__() @@ -47,9 +39,9 @@ def __init__(self) -> None: pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage) # hbox - QHBoxLayout(self) # type: ignore[operator] + QHBoxLayout(self) - lbl = QLabel(self) # type: ignore[operator] + lbl = QLabel(self) # Segfault in the problem lbl.setPixmap(pixmap1.copy()) @@ -63,7 +55,7 @@ def roundtrip(expected: Image.Image) -> None: @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") def test_sanity(tmp_path: Path) -> None: # Segfault test - app: QApplication | None = QApplication([]) # type: ignore[operator] + app: QApplication | None = QApplication([]) ex = Example() assert app # Silence warning assert ex # Silence warning @@ -84,11 +76,11 @@ def test_sanity(tmp_path: Path) -> None: imageqt = ImageQt.ImageQt(im) data = getattr(QPixmap, "fromImage")(imageqt) qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage - qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) # type: ignore[operator] - painter = QPainter(qimage) # type: ignore[operator] - image_label = QLabel() # type: ignore[operator] + qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) + painter = QPainter(qimage) + image_label = QLabel() image_label.setPixmap(data) - image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) # type: ignore[operator] + image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) painter.end() rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png") qimage.save(rendered_tempfile) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 8cb7ffb9b40..0004b552153 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,13 +1,15 @@ from __future__ import annotations -from pathlib import Path - import pytest from PIL import ImageQt from .helper import assert_image_equal, assert_image_equal_tofile, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from pathlib import Path + pytestmark = pytest.mark.skipif( not ImageQt.qt_is_installed, reason="Qt bindings are not installed" ) @@ -21,7 +23,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None: src = hopper(mode) data = ImageQt.toqimage(src) - assert isinstance(data, QImage) # type: ignore[arg-type, misc] + assert isinstance(data, QImage) assert not data.isNull() # reload directly from the qimage diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 38d46f312ed..465517bb699 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -2,8 +2,6 @@ import shutil from io import BytesIO -from pathlib import Path -from typing import IO, Callable import pytest @@ -11,6 +9,12 @@ from .helper import djpeg_available, is_win32, netpbm_available +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from pathlib import Path + from typing import IO + TEST_JPG = "Tests/images/hopper.jpg" TEST_GIF = "Tests/images/hopper.gif" diff --git a/checks/check_imaging_leaks.py b/checks/check_imaging_leaks.py index 231789ca0a0..a1d59ed9c8b 100755 --- a/checks/check_imaging_leaks.py +++ b/checks/check_imaging_leaks.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 from __future__ import annotations -from typing import Any, Callable +from collections.abc import Callable +from typing import Any import pytest diff --git a/docs/index.rst b/docs/index.rst index 689088d48ce..ee51621ac7a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,10 +29,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more =2024.10.12", ] -optional-dependencies.typing = [ - "typing-extensions; python_version<'3.10'", -] optional-dependencies.xmp = [ "defusedxml", ] @@ -189,8 +185,8 @@ lint.ignore = [ "PT011", # pytest-raises-too-broad "PT012", # pytest-raises-with-multiple-statements "PT017", # pytest-assert-in-except - "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI034", # flake8-pyi: typing.Self added in Python 3.11 + "UP038", # pyupgrade: deprecated rule ] lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [ "I002", @@ -216,7 +212,7 @@ testpaths = [ ] [tool.mypy] -python_version = "3.9" +python_version = "3.10" pretty = true disallow_any_generics = true enable_error_code = "ignore-without-code" diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index b03aa7f1505..58c460ef3db 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,7 +31,7 @@ import subprocess from enum import IntEnum from functools import cached_property -from typing import IO, Any, Literal, NamedTuple, Union, cast +from typing import Any, NamedTuple, cast from . import ( Image, @@ -49,6 +49,8 @@ TYPE_CHECKING = False if TYPE_CHECKING: + from typing import IO, Literal + from . import _imaging from ._typing import Buffer @@ -535,7 +537,7 @@ def _normalize_mode(im: Image.Image) -> Image.Image: return im.convert("L") -_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette] +_Palette = bytes | bytearray | list[int] | ImagePalette.ImagePalette def _normalize_palette( diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index ec62f8e4ebc..5f2691882c4 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -21,10 +21,14 @@ from __future__ import annotations from math import log, pi, sin, sqrt -from typing import IO, Callable from ._binary import o8 +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from typing import IO + EPSILON = 1e-10 """""" # Enable auto-doc for data member diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e95fa91f8b3..ed46899b455 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -34,20 +34,23 @@ import math import struct from collections.abc import Sequence -from types import ModuleType -from typing import Any, AnyStr, Callable, Union, cast +from typing import cast from . import Image, ImageColor -from ._typing import Coords - -# experimental access to the outline API -Outline: Callable[[], Image.core._Outline] = Image.core.outline TYPE_CHECKING = False if TYPE_CHECKING: + from collections.abc import Callable + from types import ModuleType + from typing import Any, AnyStr + from . import ImageDraw2, ImageFont + from ._typing import Coords + +# experimental access to the outline API +Outline: Callable[[], Image.core._Outline] = Image.core.outline -_Ink = Union[float, tuple[int, ...], str] +_Ink = float | tuple[int, ...] | str """ A simple 2D drawing interface for PIL images. diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index b9ed54ab20a..9326eeeda9d 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -19,11 +19,14 @@ import abc import functools from collections.abc import Sequence -from types import ModuleType -from typing import Any, Callable, cast +from typing import cast TYPE_CHECKING = False if TYPE_CHECKING: + from collections.abc import Callable + from types import ModuleType + from typing import Any + from . import _imaging from ._typing import NumpyArray diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index d2504b1ae5a..dfdc50c0552 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -17,11 +17,15 @@ from __future__ import annotations import builtins -from types import CodeType -from typing import Any, Callable from . import Image, _imagingmath +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from types import CodeType + from typing import Any + class _Operand: """Wraps an image operand, providing standard operators""" diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index df7a57b652c..af4d0742d6b 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,23 +19,18 @@ import sys from io import BytesIO -from typing import Any, Callable, Union from . import Image from ._util import is_path TYPE_CHECKING = False if TYPE_CHECKING: - import PyQt6 - import PySide6 + from collections.abc import Callable + from typing import Any from . import ImageFile QBuffer: type - QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray] - QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice] - QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage] - QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap] qt_version: str | None qt_versions = [ @@ -49,11 +44,15 @@ try: qRgba: Callable[[int, int, int, int], int] if qt_module == "PyQt6": - from PyQt6.QtCore import QBuffer, QIODevice + from PyQt6.QtCore import QBuffer, QByteArray, QIODevice from PyQt6.QtGui import QImage, QPixmap, qRgba elif qt_module == "PySide6": - from PySide6.QtCore import QBuffer, QIODevice - from PySide6.QtGui import QImage, QPixmap, qRgba + from PySide6.QtCore import ( # type: ignore[assignment] + QBuffer, + QByteArray, + QIODevice, + ) + from PySide6.QtGui import QImage, QPixmap, qRgba # type: ignore[assignment] except (ImportError, RuntimeError): continue qt_is_installed = True @@ -183,7 +182,7 @@ def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]: if qt_is_installed: - class ImageQt(QImage): # type: ignore[misc] + class ImageQt(QImage): def __init__(self, im: Image.Image | str | QByteArray) -> None: """ An PIL image wrapper for Qt. This is a subclass of PyQt's QImage diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index a6fc340d55f..361be48971e 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -16,10 +16,12 @@ ## from __future__ import annotations -from typing import Callable - from . import Image +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + class Iterator: """ diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 0d1968b140a..a00e9b91984 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -18,7 +18,6 @@ from __future__ import annotations import io -from typing import BinaryIO, Callable from . import FontFile, Image from ._binary import i8 @@ -27,6 +26,11 @@ from ._binary import i32be as b32 from ._binary import i32le as l32 +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from typing import BinaryIO + # -------------------------------------------------------------------- # declarations diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 73d8c21c023..2c9031469ad 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -8,7 +8,15 @@ import re import time import zlib -from typing import IO, Any, NamedTuple, Union +from typing import Any, NamedTuple + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import IO + + _DictBase = collections.UserDict[str | bytes, Any] +else: + _DictBase = collections.UserDict # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set @@ -251,13 +259,6 @@ def __bytes__(self) -> bytes: return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" -TYPE_CHECKING = False -if TYPE_CHECKING: - _DictBase = collections.UserDict[Union[str, bytes], Any] -else: - _DictBase = collections.UserDict - - class PdfDict(_DictBase): def __setattr__(self, key: str, value: Any) -> None: if key == "data": diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index c1850f084c0..c1741284b9f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -47,22 +47,24 @@ import os import struct import warnings -from collections.abc import Iterator, MutableMapping +from collections.abc import Callable, MutableMapping from fractions import Fraction from numbers import Number, Rational -from typing import IO, Any, Callable, NoReturn, cast +from typing import IO, Any, cast from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 -from ._typing import StrOrBytesPath from ._util import DeferredError, is_path from .TiffTags import TYPES TYPE_CHECKING = False if TYPE_CHECKING: - from ._typing import Buffer, IntegralLike + from collections.abc import Iterator + from typing import NoReturn + + from ._typing import Buffer, IntegralLike, StrOrBytesPath logger = logging.getLogger(__name__) diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi index ddcf93ab1eb..4fc0d60ab79 100644 --- a/src/PIL/_imagingcms.pyi +++ b/src/PIL/_imagingcms.pyi @@ -1,14 +1,14 @@ import datetime import sys -from typing import Literal, SupportsFloat, TypedDict +from typing import Literal, SupportsFloat, TypeAlias, TypedDict from ._typing import CapsuleType littlecms_version: str | None -_Tuple3f = tuple[float, float, float] -_Tuple2x3f = tuple[_Tuple3f, _Tuple3f] -_Tuple3x3f = tuple[_Tuple3f, _Tuple3f, _Tuple3f] +_Tuple3f: TypeAlias = tuple[float, float, float] +_Tuple2x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f] +_Tuple3x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f, _Tuple3f] class _IccMeasurementCondition(TypedDict): observer: int diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 1cb1429d6cf..2136810ba6a 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -1,4 +1,5 @@ -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from . import ImageFont, _imaging diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index e940452600f..979147e0c2b 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -3,7 +3,7 @@ import os import sys from collections.abc import Sequence -from typing import Any, Protocol, TypeVar, Union +from typing import Any, Protocol, TypeVar TYPE_CHECKING = False if TYPE_CHECKING: @@ -26,19 +26,8 @@ else: Buffer = Any -if sys.version_info >= (3, 10): - from typing import TypeGuard -else: - try: - from typing_extensions import TypeGuard - except ImportError: - - class TypeGuard: # type: ignore[no-redef] - def __class_getitem__(cls, item: Any) -> type[bool]: - return bool - -Coords = Union[Sequence[float], Sequence[Sequence[float]]] +Coords = Sequence[float] | Sequence[Sequence[float]] _T_co = TypeVar("_T_co", covariant=True) @@ -48,7 +37,7 @@ class SupportsRead(Protocol[_T_co]): def read(self, length: int = ..., /) -> _T_co: ... -StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] +StrOrBytesPath = str | bytes | os.PathLike[str] | os.PathLike[bytes] -__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] +__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead"] diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 8ef0d36f754..b1fa6a0f39e 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,9 +1,12 @@ from __future__ import annotations import os -from typing import Any, NoReturn -from ._typing import StrOrBytesPath, TypeGuard +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Any, NoReturn, TypeGuard + + from ._typing import StrOrBytesPath def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: diff --git a/tox.ini b/tox.ini index 967d4b53768..8933945b1af 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ requires = tox>=4.2 env_list = lint - py{py3, 314, 313, 312, 311, 310, 39} + py{py3, 314, 313, 312, 311, 310} [testenv] deps = @@ -29,7 +29,5 @@ commands = skip_install = true deps = -r .ci/requirements-mypy.txt -extras = - typing commands = mypy conftest.py selftest.py setup.py docs src winbuild Tests {posargs} From 148e1ac914c411925df2de1972d88f7a01ccde9e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 2 Aug 2025 20:10:55 +0800 Subject: [PATCH 1979/2374] Add libavif support for iOS (#9117) Co-authored-by: Andrew Murray --- .github/workflows/wheels-dependencies.sh | 39 +++++++++++++++++------- checks/check_wheel.py | 3 +- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4519271b9d3..d58c6512669 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -186,30 +186,43 @@ function build_libavif { python3 -m pip install meson ninja - if [[ "$PLAT" == "x86_64" ]] || [ -n "$SANITIZER" ]; then + if ([[ "$PLAT" == "x86_64" ]] && [[ -z "$IOS_SDK" ]]) || [ -n "$SANITIZER" ]; then build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03 fi local build_type=MinSizeRel + local build_shared=ON local lto=ON local libavif_cmake_flags - if [ -n "$IS_MACOS" ]; then + if [[ -n "$IS_MACOS" ]]; then lto=OFF libavif_cmake_flags=( -DCMAKE_C_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \ -DCMAKE_CXX_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \ -DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,-S,-x,-dead_strip_dylibs" \ ) + if [[ -n "$IOS_SDK" ]]; then + build_shared=OFF + fi else if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "x86_64" ]]; then build_type=Release fi libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now") fi + if [[ -n "$IOS_SDK" ]] && [[ "$PLAT" == "x86_64" ]]; then + libavif_cmake_flags+=(-DAOM_TARGET_CPU=generic) + else + libavif_cmake_flags+=( + -DAVIF_CODEC_AOM_DECODE=OFF \ + -DAVIF_CODEC_DAV1D=LOCAL + ) + fi local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz) + # CONFIG_AV1_HIGHBITDEPTH=0 is a flag for libaom (included as a subproject # of libavif) that disables support for encoding high bit depth images. (cd $out_dir \ @@ -217,20 +230,27 @@ function build_libavif { -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \ -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \ -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \ - -DBUILD_SHARED_LIBS=ON \ + -DBUILD_SHARED_LIBS=$build_shared \ -DAVIF_LIBSHARPYUV=LOCAL \ -DAVIF_LIBYUV=LOCAL \ -DAVIF_CODEC_AOM=LOCAL \ -DCONFIG_AV1_HIGHBITDEPTH=0 \ - -DAVIF_CODEC_AOM_DECODE=OFF \ - -DAVIF_CODEC_DAV1D=LOCAL \ -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \ -DCMAKE_C_VISIBILITY_PRESET=hidden \ -DCMAKE_CXX_VISIBILITY_PRESET=hidden \ -DCMAKE_BUILD_TYPE=$build_type \ "${libavif_cmake_flags[@]}" \ - . \ - && make install) + $HOST_CMAKE_FLAGS . ) + + if [[ -n "$IOS_SDK" ]]; then + # libavif's CMake configuration generates a meson cross file... but it + # doesn't work for iOS cross-compilation. Copy in Pillow-generated + # meson-cross config to replace the cmake-generated version. + cp $WORKDIR/meson-cross.txt $out_dir/crossfile-apple.meson + fi + + (cd $out_dir && make install) + touch libavif-stamp } @@ -268,10 +288,7 @@ function build { build_tiff fi - if [[ -z "$IOS_SDK" ]]; then - # Short term workaround; don't build libavif on iOS - build_libavif - fi + build_libavif build_libpng build_lcms2 build_openjpeg diff --git a/checks/check_wheel.py b/checks/check_wheel.py index 3d806eb71e2..937722c4bab 100644 --- a/checks/check_wheel.py +++ b/checks/check_wheel.py @@ -25,8 +25,7 @@ def test_wheel_modules() -> None: elif sys.platform == "ios": # tkinter is not available on iOS - # libavif is not available on iOS (for now) - expected_modules -= {"tkinter", "avif"} + expected_modules.remove("tkinter") assert set(features.get_supported_modules()) == expected_modules From 77247b62833afc78d561ce16ec8c34aae35f58c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 Aug 2025 12:48:47 +1000 Subject: [PATCH 1980/2374] Update dependency cibuildwheel to v3.1.3 (#9129) --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 823671828f7..9f91365576e 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.1.2 +cibuildwheel==3.1.3 From 4677cf3b1600dae5687efe651c2814f6f0f48541 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 Aug 2025 13:58:41 +1000 Subject: [PATCH 1981/2374] Update dependency mypy to v1.17.1 (#9130) --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 99eac602796..bd95638008d 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.17.0 +mypy==1.17.1 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 2973f69a756283fef2609ff473495827591b4551 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 4 Aug 2025 21:36:17 +1000 Subject: [PATCH 1982/2374] Updated libimagequant to 4.4.0 (#9074) --- depends/install_imagequant.sh | 2 +- docs/installation/building-from-source.rst | 2 +- winbuild/build_prepare.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 88756f8f9b9..357214f1fd6 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -2,7 +2,7 @@ # install libimagequant archive_name=libimagequant -archive_version=4.3.4 +archive_version=4.4.0 archive=$archive_name-$archive_version diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 59c595742fe..fc7ef7646c5 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -64,7 +64,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.3.4** + * Pillow has been tested with libimagequant **2.6-4.4.0** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index fbff0daf2b2..4fab5f4c4b5 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -120,7 +120,7 @@ def cmd_msbuild( "JPEGTURBO": "3.1.1", "LCMS2": "2.17", "LIBAVIF": "1.3.0", - "LIBIMAGEQUANT": "4.3.4", + "LIBIMAGEQUANT": "4.4.0", "LIBPNG": "1.6.50", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.3", From cee238bcb8ca6a49e21064dd5c40440bed838503 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 06:57:50 +1000 Subject: [PATCH 1983/2374] [pre-commit.ci] pre-commit autoupdate (#9131) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75c7d36324e..cff17cd361a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.2 + rev: v0.12.7 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.7 + rev: v20.1.8 hooks: - id: clang-format types: [c] @@ -79,7 +79,7 @@ repos: additional_dependencies: [trove-classifiers>=2024.10.12] - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.5.0 + rev: 1.6.0 hooks: - id: tox-ini-fmt From 0465627f0c43eb6a9a9b971d0ca0406e5b82cc8b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Aug 2025 13:00:33 +1000 Subject: [PATCH 1984/2374] Fill alpha channel when quantizing RGB images --- Tests/test_image_quantize.py | 9 +++++++++ src/libImaging/Quant.c | 28 ++++++++++++++++------------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 6d313cb8cb3..4a0732269f0 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -116,6 +116,15 @@ def test_quantize_kmeans(method: Image.Quantize) -> None: im.quantize(kmeans=-1, method=method) +@skip_unless_feature("libimagequant") +def test_resize() -> None: + im = hopper().resize((100, 100)) + converted = im.quantize(100, Image.Quantize.LIBIMAGEQUANT) + colors = converted.getcolors() + assert colors is not None + assert len(colors) == 100 + + def test_colors() -> None: im = hopper() colors = 2 diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index a489a882db2..2ad9902275d 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -1745,19 +1745,23 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { for (i = y = 0; y < im->ysize; y++) { for (x = 0; x < im->xsize; x++, i++) { p[i].v = im->image32[y][x]; - if (withAlpha && p[i].c.a == 0) { - if (transparency == 0) { - transparency = 1; - r = p[i].c.r; - g = p[i].c.g; - b = p[i].c.b; - } else { - /* Set all subsequent transparent pixels - to the same colour as the first */ - p[i].c.r = r; - p[i].c.g = g; - p[i].c.b = b; + if (withAlpha) { + if (p[i].c.a == 0) { + if (transparency == 0) { + transparency = 1; + r = p[i].c.r; + g = p[i].c.g; + b = p[i].c.b; + } else { + /* Set all subsequent transparent pixels + to the same colour as the first */ + p[i].c.r = r; + p[i].c.g = g; + p[i].c.b = b; + } } + } else { + p[i].c.a = 255; } } } From d3fa549ec941997dfa48e59ecf0aa3cdeb007070 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Aug 2025 18:03:47 +1000 Subject: [PATCH 1985/2374] Use Python 3.14 for gcc problem matching --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c075f04d7cc..0fad22a03c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -111,7 +111,7 @@ jobs: GHA_PYTHON_VERSION: ${{ matrix.python-version }} - name: Register gcc problem matcher - if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'" + if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'" run: echo "::add-matcher::.github/problem-matchers/gcc.json" - name: Build From b07dbc167c3040f076ad679c5459979cb2ca71d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 6 Aug 2025 08:17:09 +1000 Subject: [PATCH 1986/2374] Fixed typo --- docs/handbook/third-party-plugins.rst | 2 +- src/PIL/WmfImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/third-party-plugins.rst b/docs/handbook/third-party-plugins.rst index a189a5773c7..1c7dfb5e95b 100644 --- a/docs/handbook/third-party-plugins.rst +++ b/docs/handbook/third-party-plugins.rst @@ -11,7 +11,7 @@ Here is a list of PyPI projects that offer additional plugins: * :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library. * :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL. * :pypi:`pillow-heif`: Python bindings to libheif for working with HEIF images. -* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implemetation. Python bindings implemented using pybind11. +* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implementation. Python bindings implemented using pybind11. * :pypi:`pillow-jxl-plugin`: Plugin for JPEG-XL, using Rust for bindings. * :pypi:`pillow-mbm`: Adds support for KSP's proprietary MBM texture format. * :pypi:`pillow-svg`: Implements basic SVG read support. Supports basic paths, shapes, and text. diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index d569cb4b819..de714d33794 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -80,7 +80,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): format_description = "Windows Metafile" def _open(self) -> None: - # check placable header + # check placeable header s = self.fp.read(44) if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"): From 4f8ac76407f6dbaf0563b55700731955850170cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 6 Aug 2025 09:00:36 +1000 Subject: [PATCH 1987/2374] Updated raqm to 0.10.3 --- depends/install_raqm.sh | 2 +- src/thirdparty/raqm/COPYING | 2 +- src/thirdparty/raqm/NEWS | 16 ++++++ src/thirdparty/raqm/raqm-version.h | 4 +- src/thirdparty/raqm/raqm.c | 84 ++++++++++++++++-------------- 5 files changed, 65 insertions(+), 43 deletions(-) diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index 5d862403ee0..b5a05100ba2 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,7 +2,7 @@ # 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 diff --git a/src/thirdparty/raqm/COPYING b/src/thirdparty/raqm/COPYING index 97e2489b779..964318a8ae7 100644 --- a/src/thirdparty/raqm/COPYING +++ b/src/thirdparty/raqm/COPYING @@ -1,7 +1,7 @@ The MIT License (MIT) Copyright © 2015 Information Technology Authority (ITA) -Copyright © 2016-2023 Khaled Hosny +Copyright © 2016-2025 Khaled Hosny Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/thirdparty/raqm/NEWS b/src/thirdparty/raqm/NEWS index e8bf32e0bbb..fb432cffb0e 100644 --- a/src/thirdparty/raqm/NEWS +++ b/src/thirdparty/raqm/NEWS @@ -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 Wednesday, April 12, 2023 ==================================== diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h index 62d2d206459..f2dd61cf6a1 100644 --- a/src/thirdparty/raqm/raqm-version.h +++ b/src/thirdparty/raqm/raqm-version.h @@ -33,9 +33,9 @@ #define RAQM_VERSION_MAJOR 0 #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) \ ((major)*10000+(minor)*100+(micro) <= \ diff --git a/src/thirdparty/raqm/raqm.c b/src/thirdparty/raqm/raqm.c index 2b331e1afb0..9ecc5cac8cf 100644 --- a/src/thirdparty/raqm/raqm.c +++ b/src/thirdparty/raqm/raqm.c @@ -30,7 +30,11 @@ #include #ifdef RAQM_SHEENBIDI +#ifdef RAQM_SHEENBIDI_GT_2_9 +#include +#else #include +#endif #else #ifdef HAVE_FRIBIDI_SYSTEM #include @@ -546,34 +550,32 @@ raqm_set_text (raqm_t *rq, return true; } -static void * -_raqm_get_utf8_codepoint (const void *str, +static const char * +_raqm_get_utf8_codepoint (const char *str, uint32_t *out_codepoint) { - const char *s = (const char *)str; - - if (0xf0 == (0xf8 & s[0])) + if (0xf0 == (0xf8 & str[0])) { - *out_codepoint = ((0x07 & s[0]) << 18) | ((0x3f & s[1]) << 12) | ((0x3f & s[2]) << 6) | (0x3f & s[3]); - s += 4; + *out_codepoint = ((0x07 & str[0]) << 18) | ((0x3f & str[1]) << 12) | ((0x3f & str[2]) << 6) | (0x3f & str[3]); + 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]); - s += 3; + *out_codepoint = ((0x0f & str[0]) << 12) | ((0x3f & str[1]) << 6) | (0x3f & str[2]); + str += 3; } - else if (0xc0 == (0xe0 & s[0])) + else if (0xc0 == (0xe0 & str[0])) { - *out_codepoint = ((0x1f & s[0]) << 6) | (0x3f & s[1]); - s += 2; + *out_codepoint = ((0x1f & str[0]) << 6) | (0x3f & str[1]); + str += 2; } else { - *out_codepoint = s[0]; - s += 1; + *out_codepoint = str[0]; + str += 1; } - return (void *)s; + return str; } 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)) { - 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; - ++in_len; } return (out_utf32 - unicode); } -static void * -_raqm_get_utf16_codepoint (const void *str, - uint32_t *out_codepoint) +static const uint16_t * +_raqm_get_utf16_codepoint (const uint16_t *str, + uint32_t *out_codepoint) { - const uint16_t *s = (const uint16_t *)str; - - if (s[0] > 0xD800 && s[0] < 0xDBFF) + if (str[0] >= 0xD800 && str[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 W = (s[0] >> 6) & ((1 << 5) - 1); + uint32_t X = ((str[0] & ((1 << 6) -1)) << 10) | (str[1] & ((1 << 10) -1)); + uint32_t W = (str[0] >> 6) & ((1 << 5) - 1); *out_codepoint = (W+1) << 16 | X; - s += 2; + str += 2; } else { /* A single high surrogate, this is an error. */ - *out_codepoint = s[0]; - s += 1; + *out_codepoint = str[0]; + str += 1; } } else { - *out_codepoint = s[0]; - s += 1; + *out_codepoint = str[0]; + str += 1; } - return (void *)s; + return str; } 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)) { - 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; - ++in_len; } 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])) { - /* 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 */ rq->text[i] == 0x00A0 || /* No Break Space */ rq->text[i] == 0x1361 || /* Ethiopic Word Space */ - rq->text[i] == 0x10100 || /* Aegean Word Seperator Line */ - rq->text[i] == 0x10101 || /* Aegean Word Seperator Dot */ + rq->text[i] == 0x10100 || /* Aegean Word Separator Line */ + rq->text[i] == 0x10101 || /* Aegean Word Separator Dot */ rq->text[i] == 0x1039F || /* Ugaric Word Divider */ rq->text[i] == 0x1091F) /* Phoenician Word Separator */ { @@ -2167,6 +2169,10 @@ _raqm_ft_transform (int *x, *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 _raqm_shape (raqm_t *rq) { @@ -2199,7 +2205,7 @@ _raqm_shape (raqm_t *rq) hb_glyph_position_t *pos; 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); info = hb_buffer_get_glyph_infos (run->buffer, &len); From 35c92308ad84b28cb8956b9b19cf6f769a9250e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 6 Aug 2025 11:41:26 +1000 Subject: [PATCH 1988/2374] Allow RGBA palettes to work with expand() --- Tests/test_imageops.py | 15 +++++++++++++++ src/PIL/ImageOps.py | 5 +++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 9f2fd5ba257..27ac6f308fc 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -186,6 +186,21 @@ def test_palette(mode: str) -> None: ) +def test_rgba_palette() -> None: + im = Image.new("P", (1, 1)) + + red = (255, 0, 0, 255) + translucent_black = (0, 0, 0, 127) + im.putpalette(red + translucent_black, "RGBA") + + expanded_im = ImageOps.expand(im, 1, 1) + + palette = expanded_im.palette + assert palette is not None + assert palette.mode == "RGBA" + assert expanded_im.convert("RGBA").getpixel((0, 0)) == translucent_black + + def test_pil163() -> None: # Division by zero in equalize if < 255 pixels in image (@PIL163) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index da28854b574..42b10bd7bc8 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -499,14 +499,15 @@ def expand( height = top + image.size[1] + bottom color = _color(fill, image.mode) if image.palette: - palette = ImagePalette.ImagePalette(palette=image.getpalette()) + mode = image.palette.mode + palette = ImagePalette.ImagePalette(mode, image.getpalette(mode)) if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4): color = palette.getcolor(color) else: palette = None out = Image.new(image.mode, (width, height), color) if palette: - out.putpalette(palette.palette) + out.putpalette(palette.palette, mode) out.paste(image, (left, top)) return out From d975e312e288630cd25973497afdf92ffdc6ba2e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 8 Aug 2025 05:46:10 +1000 Subject: [PATCH 1989/2374] Updated zlib-ng to 2.2.5 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index fb86b6c7dbb..920dd1cc6ff 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -102,7 +102,7 @@ XZ_VERSION=5.8.1 TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 ZLIB_VERSION=1.3.1 -ZLIB_NG_VERSION=2.2.4 +ZLIB_NG_VERSION=2.2.5 LIBWEBP_VERSION=1.6.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5633519ddb9..86485868c7f 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -126,7 +126,7 @@ def cmd_msbuild( "OPENJPEG": "2.5.3", "TIFF": "4.7.0", "XZ": "5.8.1", - "ZLIBNG": "2.2.4", + "ZLIBNG": "2.2.5", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) From b8ffea2c56808661e460ecb4bca71b8c0a81265b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 8 Aug 2025 06:05:30 +1000 Subject: [PATCH 1990/2374] Revert "Revert to zlib on macOS < 10.15" This reverts commit 6c7917d7a6031ae22e1d9eaccc2e536123ea25c2. --- .github/workflows/wheels-dependencies.sh | 7 +------ checks/check_wheel.py | 3 --- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 920dd1cc6ff..c37ef79967b 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -101,7 +101,6 @@ OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 -ZLIB_VERSION=1.3.1 ZLIB_NG_VERSION=2.2.5 LIBWEBP_VERSION=1.6.0 BZIP2_VERSION=1.0.8 @@ -259,11 +258,7 @@ function build { if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi - if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then - build_new_zlib - else - build_zlib_ng - fi + build_zlib_ng build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [[ -n "$IS_MACOS" ]]; then diff --git a/checks/check_wheel.py b/checks/check_wheel.py index 937722c4bab..f716c8498bb 100644 --- a/checks/check_wheel.py +++ b/checks/check_wheel.py @@ -4,7 +4,6 @@ import sys from PIL import features -from Tests.helper import is_pypy def test_wheel_modules() -> None: @@ -48,8 +47,6 @@ def test_wheel_features() -> None: if sys.platform == "win32": 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": # 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. From b1cfa7769ba64c0546a25c06fc7b3289a8b041c0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 9 Aug 2025 07:13:41 +1000 Subject: [PATCH 1991/2374] Update actions/download-artifact action to v5 (#9141) --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 5cc4f03552c..d217d929215 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -256,7 +256,7 @@ jobs: runs-on: ubuntu-latest name: Upload wheels to scientific-python-nightly-wheels steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: dist-* path: dist @@ -278,7 +278,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: dist-* path: dist From ee8fbc0ac9510551f3dea5d24938af8be3b196de Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Aug 2025 14:58:31 +1000 Subject: [PATCH 1992/2374] Make in parallel when building brotli and libavif --- .github/workflows/wheels-dependencies.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index fb86b6c7dbb..c79cd2f17d1 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -165,7 +165,7 @@ function build_brotli { local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \ - && make install) + && make -j4 install) touch brotli-stamp } @@ -249,7 +249,7 @@ function build_libavif { cp $WORKDIR/meson-cross.txt $out_dir/crossfile-apple.meson fi - (cd $out_dir && make install) + (cd $out_dir && make -j4 install) touch libavif-stamp } From 5a90fb81cb75c9b33f5505659a9c5aa4f9d7881a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Aug 2025 18:37:17 +1000 Subject: [PATCH 1993/2374] Added checks directory to mypy --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8933945b1af..d58fd67b613 100644 --- a/tox.ini +++ b/tox.ini @@ -30,4 +30,4 @@ skip_install = true deps = -r .ci/requirements-mypy.txt commands = - mypy conftest.py selftest.py setup.py docs src winbuild Tests {posargs} + mypy conftest.py selftest.py setup.py checks docs src winbuild Tests {posargs} From f69c221376aebb7a2db019ae46c53814234e9a1e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Aug 2025 18:56:55 +1000 Subject: [PATCH 1994/2374] Do not import from Tests directory --- checks/check_imaging_leaks.py | 7 ++++--- checks/check_j2k_leaks.py | 11 ++++++----- checks/check_jpeg_leaks.py | 32 +++++++++++++++++--------------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/checks/check_imaging_leaks.py b/checks/check_imaging_leaks.py index a1d59ed9c8b..e9f202f3d3b 100755 --- a/checks/check_imaging_leaks.py +++ b/checks/check_imaging_leaks.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from __future__ import annotations +import sys from collections.abc import Callable from typing import Any @@ -8,12 +9,12 @@ from PIL import Image -from .helper import is_win32 - min_iterations = 100 max_iterations = 10000 -pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") +pytestmark = pytest.mark.skipif( + sys.platform.startswith("win32"), reason="requires Unix or macOS" +) def _get_mem_usage() -> float: diff --git a/checks/check_j2k_leaks.py b/checks/check_j2k_leaks.py index bbe35b591fc..7103d502e40 100644 --- a/checks/check_j2k_leaks.py +++ b/checks/check_j2k_leaks.py @@ -1,12 +1,11 @@ from __future__ import annotations +import sys from io import BytesIO import pytest -from PIL import Image - -from .helper import is_win32, skip_unless_feature +from PIL import Image, features # Limits for testing the leak mem_limit = 1024 * 1048576 @@ -15,8 +14,10 @@ test_file = "Tests/images/rgb_trns_ycbc.jp2" pytestmark = [ - pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"), - skip_unless_feature("jpg_2000"), + pytest.mark.skipif( + sys.platform.startswith("win32"), reason="requires Unix or macOS" + ), + pytest.mark.skipif(not features.check("jpg_2000"), reason="jpg_2000 not available"), ] diff --git a/checks/check_jpeg_leaks.py b/checks/check_jpeg_leaks.py index 2f42ad734a8..2c27ce1d5af 100644 --- a/checks/check_jpeg_leaks.py +++ b/checks/check_jpeg_leaks.py @@ -1,10 +1,11 @@ from __future__ import annotations +import sys from io import BytesIO import pytest -from .helper import hopper, is_win32 +from PIL import Image iterations = 5000 @@ -18,7 +19,9 @@ """ -pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") +pytestmark = pytest.mark.skipif( + sys.platform.startswith("win32"), reason="requires Unix or macOS" +) """ pre patch: @@ -112,10 +115,10 @@ ), ) def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None: - im = hopper("RGB") - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", qtables=qtables) + with Image.open("Tests/images/hopper.ppm") as im: + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", qtables=qtables) def test_exif_leak() -> None: @@ -173,12 +176,12 @@ def test_exif_leak() -> None: 0 +----------------------------------------------------------------------->Gi 0 11.33 """ - im = hopper("RGB") exif = b"12345678" * 4096 - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", exif=exif) + with Image.open("Tests/images/hopper.ppm") as im: + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", exif=exif) def test_base_save() -> None: @@ -207,8 +210,7 @@ def test_base_save() -> None: | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: 0 +----------------------------------------------------------------------->Gi 0 7.882""" - im = hopper("RGB") - - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG") + with Image.open("Tests/images/hopper.ppm") as im: + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG") From 1a5acabd32fcb505350c84e35dc78319c3f63899 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Aug 2025 19:53:05 +1000 Subject: [PATCH 1995/2374] Make in parallel when building libjpeg-turbo and openjpeg --- wheels/multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels/multibuild b/wheels/multibuild index 42d761728d1..64739327166 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 42d761728d141d8462cd9943f4329f12fe62b155 +Subproject commit 64739327166fcad1fa41ad9b23fa910fa244c84f From 5e7f1312874dfd01a7b03af26b5f836bf0aa65ac Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 11 Aug 2025 10:40:32 +0300 Subject: [PATCH 1996/2374] Add Debian 13 Trixie (#9147) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/test-docker.yml | 2 ++ docs/installation/platform-support.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 0b90732eba7..47f2d3f0ae7 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -47,6 +47,8 @@ jobs: centos-stream-10-amd64, debian-12-bookworm-x86, debian-12-bookworm-amd64, + debian-13-trixie-x86, + debian-13-trixie-amd64, fedora-41-amd64, fedora-42-amd64, gentoo, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 5cf0276d188..d97895c8644 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -31,6 +31,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ +| Debian 13 Trixie | 3.13 | x86, x86-64 | ++----------------------------------+----------------------------+---------------------+ | Fedora 41 | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Fedora 42 | 3.13 | x86-64 | From a72c6318771ca8e385a5dcd5a72a721df403dc21 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 12 Aug 2025 12:36:33 +1000 Subject: [PATCH 1997/2374] Updated URLs --- .github/zizmor.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 5bdc48c301b..b567097811a 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,5 +1,5 @@ # Configuration for the zizmor static analysis tool, run via pre-commit in CI -# https://woodruffw.github.io/zizmor/configuration/ +# https://docs.zizmor.sh/configuration/ rules: unpinned-uses: config: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cff17cd361a..2be509d54de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: - id: check-readthedocs - id: check-renovate - - repo: https://github.com/woodruffw/zizmor-pre-commit + - repo: https://github.com/zizmorcore/zizmor-pre-commit rev: v1.11.0 hooks: - id: zizmor From 6d974b61d6144fab9e65aff1b84bedd377737db8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 15 Aug 2025 14:37:31 +1000 Subject: [PATCH 1998/2374] Load image palette into Python after converting to PA --- Tests/test_image_convert.py | 8 ++++++++ src/PIL/Image.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 33f8444378d..7ba3fb55536 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -107,6 +107,14 @@ def test_rgba_p() -> None: assert_image_similar(im, comparable, 20) +def test_pa() -> None: + im = hopper().convert("PA") + + palette = im.palette + assert palette is not None + assert palette.colors != {} + + def test_rgba() -> None: with Image.open("Tests/images/transparent.png") as im: assert im.mode == "RGBA" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b7c185e0d6e..b435b17ece1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1143,7 +1143,7 @@ def convert_transparency( raise ValueError(msg) from e new_im = self._new(im) - if mode == "P" and palette != Palette.ADAPTIVE: + if mode in ("P", "PA") and palette != Palette.ADAPTIVE: from . import ImagePalette new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB")) From 0ae2611b4438c847054d43d54ff21366eb1456bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 15 Aug 2025 15:56:56 +1000 Subject: [PATCH 1999/2374] Copy C palette when merging --- Tests/test_image.py | 6 ++++++ src/_imaging.c | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 83b027aa238..4b8ef02cd10 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1076,6 +1076,12 @@ def test_apply_transparency(self) -> None: assert im.palette is not None assert im.palette.colors[(27, 35, 6, 214)] == 24 + def test_merge_pa(self) -> None: + p = hopper("P") + a = Image.new("L", p.size) + pa = Image.merge("PA", (p, a)) + assert p.getpalette() == pa.getpalette() + def test_constants(self) -> None: for enum in ( Image.Transpose, diff --git a/src/_imaging.c b/src/_imaging.c index fbfc0e41ae2..6ab8e010d8c 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2419,7 +2419,12 @@ _merge(PyObject *self, PyObject *args) { bands[3] = band3->image; } - return PyImagingNew(ImagingMerge(mode, bands)); + Imaging imOut = ImagingMerge(mode, bands); + if (!imOut) { + return NULL; + } + ImagingCopyPalette(imOut, bands[0]); + return PyImagingNew(imOut); } static PyObject * From ba66fec3d242fc1a8b287ba00baaed766b60786a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 15 Aug 2025 23:39:33 +1000 Subject: [PATCH 2000/2374] When converting RGBA to PA, use RGB to P quantization --- Tests/test_image_convert.py | 7 +++++++ src/PIL/Image.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 33f8444378d..6c7026d47eb 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -107,6 +107,13 @@ def test_rgba_p() -> None: assert_image_similar(im, comparable, 20) +def test_rgba_pa() -> None: + im = hopper("RGBA").convert("PA").convert("RGB") + expected = hopper("RGB") + + assert_image_similar(im, expected, 9.3) + + def test_rgba() -> None: with Image.open("Tests/images/transparent.png") as im: assert im.mode == "RGBA" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b7c185e0d6e..55309adbc07 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1010,8 +1010,14 @@ def convert_transparency( new_im.info["transparency"] = transparency return new_im - if mode == "P" and self.mode == "RGBA": - return self.quantize(colors) + if self.mode == "RGBA": + if mode == "P": + return self.quantize(colors) + elif mode == "PA": + r, g, b, a = self.split() + rgb = merge("RGB", (r, g, b)) + p = rgb.quantize(colors) + return merge("PA", (p, a)) trns = None delete_trns = False From 425a3a1af07c262f39e6f0d4fd5fa47e7f711859 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 16 Aug 2025 11:33:02 +1000 Subject: [PATCH 2001/2374] Updated macOS version in CI targets --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index d97895c8644..3c5e4cd519f 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -41,7 +41,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | macOS 13 Ventura | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 14 Sonoma | 3.11, 3.12, 3.13, 3.14 | arm64 | +| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14 | arm64 | | | PyPy3 | | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | From 62546924b5c890c4b7eebb163afc11fd8a71f0d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 18 Aug 2025 08:07:12 +1000 Subject: [PATCH 2002/2374] Remove support for FreeType <= 2.9.0 --- src/PIL/ImageFont.py | 18 +++--------------- src/_imagingft.c | 6 ------ 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index bf3f471f5e3..a2bf9ccf92b 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -671,11 +671,7 @@ def get_variation_names(self) -> list[bytes]: :returns: A list of the named styles in a variation font. :exception OSError: If the font is not a variation font. """ - try: - names = self.font.getvarnames() - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e + names = self.font.getvarnames() return [name.replace(b"\x00", b"") for name in names] def set_variation_by_name(self, name: str | bytes) -> None: @@ -702,11 +698,7 @@ def get_variation_axes(self) -> list[Axis]: :returns: A list of the axes in a variation font. :exception OSError: If the font is not a variation font. """ - try: - axes = self.font.getvaraxes() - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e + axes = self.font.getvaraxes() for axis in axes: if axis["name"]: axis["name"] = axis["name"].replace(b"\x00", b"") @@ -717,11 +709,7 @@ def set_variation_by_axes(self, axes: list[float]) -> None: :param axes: A list of values for each axis. :exception OSError: If the font is not a variation font. """ - try: - self.font.setvaraxes(axes) - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e + self.font.setvaraxes(axes) class TransposedFont: diff --git a/src/_imagingft.c b/src/_imagingft.c index 29d8e9e7112..c9938fd3eac 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1221,8 +1221,6 @@ font_render(FontObject *self, PyObject *args) { return NULL; } -#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ - (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) static PyObject * font_getvarnames(FontObject *self) { int error; @@ -1432,7 +1430,6 @@ font_setvaraxes(FontObject *self, PyObject *args) { Py_RETURN_NONE; } -#endif static void font_dealloc(FontObject *self) { @@ -1451,13 +1448,10 @@ static PyMethodDef font_methods[] = { {"render", (PyCFunction)font_render, METH_VARARGS}, {"getsize", (PyCFunction)font_getsize, METH_VARARGS}, {"getlength", (PyCFunction)font_getlength, METH_VARARGS}, -#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ - (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) {"getvarnames", (PyCFunction)font_getvarnames, METH_NOARGS}, {"getvaraxes", (PyCFunction)font_getvaraxes, METH_NOARGS}, {"setvarname", (PyCFunction)font_setvarname, METH_VARARGS}, {"setvaraxes", (PyCFunction)font_setvaraxes, METH_VARARGS}, -#endif {NULL, NULL} }; From c214ad8c8d40c785c8aca6226b5033085f24cb3d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 19 Aug 2025 06:43:07 +1000 Subject: [PATCH 2003/2374] Use macos-14 for iOS arm64 simulator (#9161) --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d217d929215..d5aacb162fd 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -99,7 +99,7 @@ jobs: cibw_arch: arm64_iphoneos - name: "iOS arm64 simulator" platform: ios - os: macos-latest + os: macos-14 cibw_arch: arm64_iphonesimulator - name: "iOS x86_64 simulator" platform: ios From 1435339290f8112999dfa85a07718cdac2ce2cfc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:13:56 +1000 Subject: [PATCH 2004/2374] Update actions/checkout action to v5 (#9156) --- .github/workflows/docs.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test-docker.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- .github/workflows/test-valgrind-memory.yml | 2 +- .github/workflows/test-valgrind.yml | 2 +- .github/workflows/test-windows.yml | 6 +++--- .github/workflows/test.yml | 2 +- .github/workflows/wheels.yml | 8 ++++---- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 626824f3830..761dc112520 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,7 +32,7 @@ jobs: name: Docs steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8e789a73489..9827ef1cd7e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: name: Lint steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 47f2d3f0ae7..30e5c494db9 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -68,7 +68,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 5a83c16c329..6c4206083c5 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -45,7 +45,7 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml index e6a5f6e779f..0f36fe30dc2 100644 --- a/.github/workflows/test-valgrind-memory.yml +++ b/.github/workflows/test-valgrind-memory.yml @@ -41,7 +41,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 8818b3b2357..30caa0d4e51 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -39,7 +39,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index c80bb6eb602..d55a8e5f51f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -47,19 +47,19 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Checkout cached dependencies - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false repository: python-pillow/pillow-depends path: winbuild\depends - name: Checkout extra test images - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false repository: python-pillow/test-images diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0fad22a03c4..b17d08892a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,7 +65,7 @@ jobs: name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d5aacb162fd..24e78f965c5 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -106,7 +106,7 @@ jobs: os: macos-13 cibw_arch: x86_64_iphonesimulator steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false submodules: true @@ -153,12 +153,12 @@ jobs: - cibw_arch: ARM64 os: windows-11-arm steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false - name: Checkout extra test images - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false repository: python-pillow/test-images @@ -234,7 +234,7 @@ jobs: if: github.event_name != 'schedule' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false From c826b932c07522272eb1297a595c8c90726970db Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 19 Aug 2025 15:45:42 +1000 Subject: [PATCH 2005/2374] Document MAXBLOCK --- docs/reference/ImageFile.rst | 1 + src/PIL/ImageFile.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index 043559352ab..4c34ff8128b 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -74,5 +74,6 @@ Constants --------- .. autodata:: PIL.ImageFile.LOAD_TRUNCATED_IMAGES +.. autodata:: PIL.ImageFile.MAXBLOCK .. autodata:: PIL.ImageFile.ERRORS :annotation: diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 27b27127e79..e33b846d416 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -46,6 +46,18 @@ logger = logging.getLogger(__name__) MAXBLOCK = 65536 +""" +By default, Pillow processes image data in blocks. This helps to prevent excessive use +of resources. Codecs may disable this behaviour with ``_pulls_fd`` or ``_pushes_fd``. + +When reading an image, this is the number of bytes to read at once. + +When writing an image, this is the number of bytes to write at once. +If the image width times 4 is greater, then that will be used instead. +Plugins may also set a greater number. + +User code may set this to another number. +""" SAFEBLOCK = 1024 * 1024 From 34c651deb8390ef752425a31e1473af4d6c9db3d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 08:48:38 +1000 Subject: [PATCH 2006/2374] Update dependency cibuildwheel to v3.1.4 (#9164) --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 9f91365576e..d87d7956f75 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.1.3 +cibuildwheel==3.1.4 From 6a3bde05a46a8326fe02fb53fcab5a6f915d7193 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 20 Aug 2025 15:32:12 +1000 Subject: [PATCH 2007/2374] Do not set core to DeferredError --- src/PIL/Image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b7c185e0d6e..683c807622d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -103,7 +103,6 @@ class DecompressionBombError(Exception): raise ImportError(msg) except ImportError as v: - core = DeferredError.new(ImportError("The _imaging C module is not installed.")) # Explanations for ways that we know we might have an import error if str(v).startswith("Module use of python"): # The _imaging C module is present, but not compiled for From 009444f9c51d2d008fd0256769c93a7c2acb670a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 21 Aug 2025 21:56:03 +1000 Subject: [PATCH 2008/2374] Improved _accept length check --- src/PIL/FliImagePlugin.py | 2 +- src/PIL/GribStubImagePlugin.py | 2 +- src/PIL/PcxImagePlugin.py | 2 +- src/PIL/PpmImagePlugin.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 7c5bfeefa1b..ccb8a5953d6 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -30,7 +30,7 @@ def _accept(prefix: bytes) -> bool: return ( - len(prefix) >= 6 + len(prefix) >= 16 and i16(prefix, 4) in [0xAF11, 0xAF12] and i16(prefix, 14) in [0, 3] # flags ) diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 439fc5a3eda..dfa798893cb 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"GRIB") and prefix[7] == 1 + return len(prefix) >= 8 and prefix.startswith(b"GRIB") and prefix[7] == 1 class GribStubImageFile(ImageFile.StubImageFile): diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 458d586c463..6b16d538537 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -39,7 +39,7 @@ 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] ## diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index db34d107a4f..307bc97ff65 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -47,7 +47,7 @@ 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" ## From 84122a20c70589ee4d68986507b9b54c91a19620 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Aug 2025 18:29:25 +1000 Subject: [PATCH 2009/2374] Replaced print with assert --- Tests/test_numpy.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index ef54deeeb2b..f6acb3affb6 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -28,15 +28,13 @@ def to_image(dtype: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Im a = numpy.array(data, dtype=dtype) a.shape = TEST_IMAGE_SIZE i = Image.fromarray(a) - if list(i.getdata()) != data: - print("data mismatch for", dtype) + assert list(i.getdata()) == data else: data = list(range(100)) a = numpy.array([[x] * bands for x in data], dtype=dtype) a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands i = Image.fromarray(a) - if list(i.getchannel(0).getdata()) != list(range(100)): - print("data mismatch for", dtype) + assert list(i.getchannel(0).getdata()) == list(range(100)) return i # Check supported 1-bit integer formats From 54f4a346ef89e33eec0f889569a6d280eca70656 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Aug 2025 19:06:19 +1000 Subject: [PATCH 2010/2374] Added has_feature_version --- Tests/helper.py | 8 ++++++++ Tests/test_file_webp_animated.py | 18 ++++++------------ Tests/test_image_quantize.py | 18 ++++++++++-------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index e0dc8a9d4aa..dbdd30b426a 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -175,6 +175,14 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator: return pytest.mark.skipif(not features.check(feature), reason=reason) +def has_feature_version(feature: str, required: str) -> bool: + version = features.version(feature) + assert version is not None + version_required = parse_version(required) + version_available = parse_version(version) + return version_available >= version_required + + def skip_unless_feature_version( feature: str, required: str, reason: str | None = None ) -> pytest.MarkDecorator: diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 5037613742b..600448fb9e1 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -4,13 +4,13 @@ from pathlib import Path import pytest -from packaging.version import parse as parse_version -from PIL import GifImagePlugin, Image, WebPImagePlugin, features +from PIL import GifImagePlugin, Image, WebPImagePlugin from .helper import ( assert_image_equal, assert_image_similar, + has_feature_version, is_big_endian, skip_unless_feature, ) @@ -53,11 +53,8 @@ def test_write_animation_L(tmp_path: Path) -> None: im.load() assert_image_similar(im, orig.convert("RGBA"), 32.9) - if is_big_endian(): - version = features.version_module("webp") - assert version is not None - if parse_version(version) < parse_version("1.2.2"): - pytest.skip("Fails with libwebp earlier than 1.2.2") + if is_big_endian() and not has_feature_version("webp", "1.2.2"): + pytest.skip("Fails with libwebp earlier than 1.2.2") orig.seek(orig.n_frames - 1) im.seek(im.n_frames - 1) orig.load() @@ -81,11 +78,8 @@ def check(temp_file: Path) -> None: assert_image_equal(im, frame1.convert("RGBA")) # Compare second frame to original - if is_big_endian(): - version = features.version_module("webp") - assert version is not None - if parse_version(version) < parse_version("1.2.2"): - pytest.skip("Fails with libwebp earlier than 1.2.2") + if is_big_endian() and not has_feature_version("webp", "1.2.2"): + pytest.skip("Fails with libwebp earlier than 1.2.2") im.seek(1) im.load() assert_image_equal(im, frame2.convert("RGBA")) diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 6d313cb8cb3..d847c74403e 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,11 +1,16 @@ from __future__ import annotations import pytest -from packaging.version import parse as parse_version -from PIL import Image, features +from PIL import Image -from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature +from .helper import ( + assert_image_similar, + has_feature_version, + hopper, + is_ppc64le, + skip_unless_feature, +) def test_sanity() -> None: @@ -23,11 +28,8 @@ def test_sanity() -> None: @skip_unless_feature("libimagequant") def test_libimagequant_quantize() -> None: image = hopper() - if is_ppc64le(): - version = features.version_feature("libimagequant") - assert version is not None - if parse_version(version) < parse_version("4"): - pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") + if is_ppc64le() and not has_feature_version("libimagequant", "4"): + pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 15) From f80ac8d6b8915a7150d6179798e284f6002b8bd9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Aug 2025 19:16:38 +1000 Subject: [PATCH 2011/2374] Check version independently --- Tests/test_imagefontctl.py | 81 ++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 48 deletions(-) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 5954de87429..95af3fda8c4 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -7,6 +7,7 @@ from .helper import ( assert_image_equal_tofile, assert_image_similar_tofile, + has_feature_version, skip_unless_feature, ) @@ -104,11 +105,9 @@ def test_text_direction_ttb() -> None: im = Image.new(mode="RGB", size=(100, 300)) draw = ImageDraw.Draw(im) - try: - draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb") - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") + if not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb") target = "Tests/images/test_direction_ttb.png" assert_image_similar_tofile(im, target, 2.8) @@ -119,19 +118,17 @@ def test_text_direction_ttb_stroke() -> None: im = Image.new(mode="RGB", size=(100, 300)) draw = ImageDraw.Draw(im) - try: - draw.text( - (27, 27), - "あい", - font=ttf, - fill=500, - direction="ttb", - stroke_width=2, - stroke_fill="#0f0", - ) - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") + if not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + draw.text( + (27, 27), + "あい", + font=ttf, + fill=500, + direction="ttb", + stroke_width=2, + stroke_fill="#0f0", + ) target = "Tests/images/test_direction_ttb_stroke.png" assert_image_similar_tofile(im, target, 19.4) @@ -219,14 +216,9 @@ def test_getlength( im = Image.new(mode, (1, 1), 0) d = ImageDraw.Draw(im) - try: - assert d.textlength(text, ttf, direction) == expected - except ValueError as ex: - if ( - direction == "ttb" - and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" - ): - pytest.skip("libraqm 0.7 or greater not available") + if direction == "ttb" and not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + assert d.textlength(text, ttf, direction) == expected @pytest.mark.parametrize("mode", ("L", "1")) @@ -242,17 +234,12 @@ def test_getlength_combine(mode: str, direction: str, text: str) -> None: ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) - try: - target = ttf.getlength("ii", mode, direction) - actual = ttf.getlength(text, mode, direction) + if direction == "ttb" and not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + target = ttf.getlength("ii", mode, direction) + actual = ttf.getlength(text, mode, direction) - assert actual == target - except ValueError as ex: - if ( - direction == "ttb" - and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" - ): - pytest.skip("libraqm 0.7 or greater not available") + assert actual == target @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) @@ -265,11 +252,9 @@ def test_anchor_ttb(anchor: str) -> None: d = ImageDraw.Draw(im) d.line(((0, 200), (200, 200)), "gray") d.line(((100, 0), (100, 400)), "gray") - try: - d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f) - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") + if not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f) assert_image_similar_tofile(im, path, 1) # fails at 5 @@ -310,10 +295,12 @@ def test_anchor_ttb(anchor: str) -> None: # this tests various combining characters for anchor alignment and clipping @pytest.mark.parametrize( - "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] + "name, text, anchor, direction, epsilon", + combine_tests, + ids=[r[0] for r in combine_tests], ) def test_combine( - name: str, text: str, dir: str | None, anchor: str | None, epsilon: float + name: str, text: str, direction: str | None, anchor: str | None, epsilon: float ) -> None: path = f"Tests/images/test_combine_{name}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) @@ -322,11 +309,9 @@ def test_combine( d = ImageDraw.Draw(im) d.line(((0, 200), (400, 200)), "gray") d.line(((200, 0), (200, 400)), "gray") - try: - d.text((200, 200), text, fill="black", anchor=anchor, direction=dir, font=f) - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") + if direction == "ttb" and not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + d.text((200, 200), text, fill="black", anchor=anchor, direction=direction, font=f) assert_image_similar_tofile(im, path, epsilon) From 0d72707d4f1da4f72ee6b5ece10d13080f877796 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 26 Aug 2025 08:55:11 +1000 Subject: [PATCH 2012/2374] Removed version from PDF comment --- src/PIL/PdfImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index e9c20ddc159..5594c7e0f2b 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -27,7 +27,7 @@ import time from typing import IO, Any -from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features +from . import Image, ImageFile, ImageSequence, PdfParser, features # # -------------------------------------------------------------------- @@ -221,7 +221,7 @@ def _save( existing_pdf.start_writing() existing_pdf.write_header() - existing_pdf.write_comment(f"created by Pillow {__version__} PDF driver") + existing_pdf.write_comment("created by Pillow PDF driver") # # pages From 59d6f313d6e70f78e41a6e8c3c8848553a0e37c7 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 26 Aug 2025 21:07:32 +1000 Subject: [PATCH 2013/2374] Removed setuptools version requirement --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 3519707f14c..99eac602796 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -11,4 +11,4 @@ sphinx types-atheris types-defusedxml types-olefile -types-setuptools>=75.2.0 +types-setuptools From ed164d1bfab8a59d411aadab7d56d1ec116f572a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 26 Aug 2025 22:13:45 +1000 Subject: [PATCH 2014/2374] pre-commit fixes --- setup.py | 2 +- src/_imaging.c | 3 ++- src/libImaging/Filter.c | 6 ++++-- src/libImaging/Pack.c | 6 ++++-- src/libImaging/Resample.c | 6 ++++-- src/libImaging/Unpack.c | 14 +++++++++++--- 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index dcc07eaf691..b9f5cfe0610 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ import sys import warnings from collections.abc import Iterator -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from pybind11.setup_helpers import ParallelCompile from setuptools import Extension, setup diff --git a/src/_imaging.c b/src/_imaging.c index 7823745f0d6..8412124c191 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1677,7 +1677,8 @@ _putdata(ImagingObject *self, PyObject *args) { int bigendian = 0; if (image->type == IMAGING_TYPE_SPECIAL) { // I;16* - if (image->mode == IMAGING_MODE_I_16B + if ( + image->mode == IMAGING_MODE_I_16B #ifdef WORDS_BIGENDIAN || image->mode == IMAGING_MODE_I_16N #endif diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 48f21080906..cefb8fcdc36 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -155,7 +155,8 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { } else { int bigendian = 0; if (im->type == IMAGING_TYPE_SPECIAL) { - if (im->mode == IMAGING_MODE_I_16B + if ( + im->mode == IMAGING_MODE_I_16B #ifdef WORDS_BIGENDIAN || im->mode == IMAGING_MODE_I_16N #endif @@ -308,7 +309,8 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { } else { int bigendian = 0; if (im->type == IMAGING_TYPE_SPECIAL) { - if (im->mode == IMAGING_MODE_I_16B + if ( + im->mode == IMAGING_MODE_I_16B #ifdef WORDS_BIGENDIAN || im->mode == IMAGING_MODE_I_16N #endif diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index 0a97c4872f9..4afeb15b7f6 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -648,8 +648,10 @@ static struct { {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16B, 16, copy2}, {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16L, 16, copy2}, {IMAGING_MODE_I_16N, IMAGING_RAWMODE_I_16N, 16, copy2}, - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, packI16N_I16 - }, // LibTiff native->image endian. + {IMAGING_MODE_I_16, + IMAGING_RAWMODE_I_16N, + 16, + packI16N_I16}, // LibTiff native->image endian. {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, packI16N_I16}, {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, packI16N_I16B} }; diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index 3ab43a8955a..cbd18d0c116 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -470,7 +470,8 @@ ImagingResampleHorizontal_16bpc( double *k; int bigendian = 0; - if (imIn->mode == IMAGING_MODE_I_16N + if ( + imIn->mode == IMAGING_MODE_I_16N #ifdef WORDS_BIGENDIAN || imIn->mode == IMAGING_MODE_I_16B #endif @@ -509,7 +510,8 @@ ImagingResampleVertical_16bpc( double *k; int bigendian = 0; - if (imIn->mode == IMAGING_MODE_I_16N + if ( + imIn->mode == IMAGING_MODE_I_16N #ifdef WORDS_BIGENDIAN || imIn->mode == IMAGING_MODE_I_16B #endif diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 075ec5b950b..27ac7c46730 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1833,13 +1833,21 @@ static struct { {IMAGING_MODE_I_16N, IMAGING_RAWMODE_I_16N, 16, copy2}, {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, unpackI16B_I16}, - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16}, // LibTiff native->image endian. - {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16}, // LibTiff native->image endian. + {IMAGING_MODE_I_16, + IMAGING_RAWMODE_I_16N, + 16, + unpackI16N_I16}, // LibTiff native->image endian. + { + IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16 + }, // LibTiff native->image endian. {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16B}, {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16R, 16, unpackI16R_I16}, - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_12, 12, unpackI12_I16} // 12 bit Tiffs stored in 16bits. + {IMAGING_MODE_I_16, + IMAGING_RAWMODE_I_12, + 12, + unpackI12_I16} // 12 bit Tiffs stored in 16bits. }; ImagingShuffler From 178b3a70ccafd2fb81438cad2ebe8bb2a16ef67d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 27 Aug 2025 06:58:51 +1000 Subject: [PATCH 2015/2374] Updated formatting --- src/libImaging/Pack.c | 6 ++---- src/libImaging/Unpack.c | 19 ++++++------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index 4afeb15b7f6..fdf5a72aa9e 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -648,10 +648,8 @@ static struct { {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16B, 16, copy2}, {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16L, 16, copy2}, {IMAGING_MODE_I_16N, IMAGING_RAWMODE_I_16N, 16, copy2}, - {IMAGING_MODE_I_16, - IMAGING_RAWMODE_I_16N, - 16, - packI16N_I16}, // LibTiff native->image endian. + // LibTiff native->image endian. + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, packI16N_I16}, {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, packI16N_I16}, {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, packI16N_I16B} }; diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 27ac7c46730..ab5f2c158fa 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1833,21 +1833,14 @@ static struct { {IMAGING_MODE_I_16N, IMAGING_RAWMODE_I_16N, 16, copy2}, {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, unpackI16B_I16}, - {IMAGING_MODE_I_16, - IMAGING_RAWMODE_I_16N, - 16, - unpackI16N_I16}, // LibTiff native->image endian. - { - IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16 - }, // LibTiff native->image endian. - {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16B}, - {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16R, 16, unpackI16R_I16}, - {IMAGING_MODE_I_16, - IMAGING_RAWMODE_I_12, - 12, - unpackI12_I16} // 12 bit Tiffs stored in 16bits. + // LibTiff native->image endian. + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16}, + {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16}, + + // 12 bit Tiffs stored in 16bits. + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_12, 12, unpackI12_I16} }; ImagingShuffler From 84e89bf5c3c798ac55e726e3624c4bca5bacc90f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 27 Aug 2025 07:07:13 +1000 Subject: [PATCH 2016/2374] Restored unpacker --- src/libImaging/Unpack.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index ab5f2c158fa..203bcac2ca9 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1833,6 +1833,7 @@ static struct { {IMAGING_MODE_I_16N, IMAGING_RAWMODE_I_16N, 16, copy2}, {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, unpackI16B_I16}, + {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16B}, {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16R, 16, unpackI16R_I16}, // LibTiff native->image endian. From a59ce257e9ccc966d0a4a2b57a1ef2f05134f9b7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 28 Aug 2025 19:37:26 +1000 Subject: [PATCH 2017/2374] Install zstd for libtiff on Linux --- .github/workflows/wheels-dependencies.sh | 10 ++++++++ wheels/dependency_licenses/ZSTD.txt | 30 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 wheels/dependency_licenses/ZSTD.txt diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index c79cd2f17d1..72934a9b983 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -99,6 +99,7 @@ LIBPNG_VERSION=1.6.50 JPEGTURBO_VERSION=3.1.1 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 +ZSTD_VERSION=1.5.7 TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 ZLIB_VERSION=1.3.1 @@ -254,6 +255,14 @@ function build_libavif { 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 { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then @@ -285,6 +294,7 @@ function build { --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ --disable-webp --disable-libdeflate --disable-zstd else + build_zstd build_tiff fi diff --git a/wheels/dependency_licenses/ZSTD.txt b/wheels/dependency_licenses/ZSTD.txt new file mode 100644 index 00000000000..75800288cc2 --- /dev/null +++ b/wheels/dependency_licenses/ZSTD.txt @@ -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. From 05a601031142ecf0ca21c521a6c312c66c4e48b6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Aug 2025 07:35:18 +1000 Subject: [PATCH 2018/2374] Fixed loading rotated PCD images --- Tests/test_file_pcd.py | 7 +++++++ src/PIL/PcdImagePlugin.py | 9 +++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 9bf1a75f0f6..a2d07ff5180 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -6,6 +6,8 @@ from PIL import Image +from .helper import assert_image_equal + def test_load_raw() -> None: with Image.open("Tests/images/hopper.pcd") as im: @@ -30,3 +32,8 @@ def test_rotated(orientation: int) -> None: f = BytesIO(data) with Image.open(f) as im: assert im.size == (512, 768) + + with Image.open("Tests/images/hopper.pcd") as expected: + assert_image_equal( + im, expected.rotate(90 if orientation == 1 else -90, expand=True) + ) diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 7f9ab525c68..00864a4bf90 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -47,12 +47,17 @@ def _open(self) -> None: self._mode = "RGB" 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, 768, 512), 96 * 2048)] + + def load_prepare(self) -> None: + if self._im is None and self.tile_post_rotate: + self.im = Image.core.new(self.mode, (768, 512)) + ImageFile.ImageFile.load_prepare(self) def load_end(self) -> None: if self.tile_post_rotate: # Handle rotated PCDs - self.im = self.im.rotate(self.tile_post_rotate) + self.im = self.rotate(self.tile_post_rotate, expand=True).im # From c6915f717f3b9bb694421f4d711808ba2c464d40 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Aug 2025 07:43:51 +1000 Subject: [PATCH 2019/2374] rotate() will use "angle % 360" --- Tests/test_file_pcd.py | 2 +- src/PIL/PcdImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index a2d07ff5180..15dd7f116db 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -35,5 +35,5 @@ def test_rotated(orientation: int) -> None: with Image.open("Tests/images/hopper.pcd") as expected: assert_image_equal( - im, expected.rotate(90 if orientation == 1 else -90, expand=True) + im, expected.rotate(90 if orientation == 1 else 270, expand=True) ) diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 00864a4bf90..296f3775b0d 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -43,7 +43,7 @@ def _open(self) -> None: if orientation == 1: self.tile_post_rotate = 90 elif orientation == 3: - self.tile_post_rotate = -90 + self.tile_post_rotate = 270 self._mode = "RGB" self._size = (512, 768) if orientation in (1, 3) else (768, 512) From c7a268e5a5d026d17374309a0fde23cbcc0f8bf0 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 1 Sep 2025 08:23:30 +1000 Subject: [PATCH 2020/2374] ImageMorph operations must have length 1 (#9102) --- Tests/test_imagemorph.py | 14 ++++++++------ docs/releasenotes/12.0.0.rst | 7 +++++++ src/PIL/ImageMorph.py | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 515e29cead9..ca192a809c4 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -7,7 +7,7 @@ from PIL import Image, ImageMorph, _imagingmorph -from .helper import assert_image_equal_tofile, hopper +from .helper import assert_image_equal_tofile, hopper, timeout_unless_slower_valgrind def string_to_img(image_string: str) -> Image.Image: @@ -266,16 +266,18 @@ def test_unknown_pattern() -> None: ImageMorph.LutBuilder(op_name="unknown") -def test_pattern_syntax_error() -> None: +@pytest.mark.parametrize( + "pattern", ("a pattern with a syntax error", "4:(" + "X" * 30000) +) +@timeout_unless_slower_valgrind(1) +def test_pattern_syntax_error(pattern: str) -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") - new_patterns = ["a pattern with a syntax error"] + new_patterns = [pattern] lb.add_patterns(new_patterns) # Act / Assert - with pytest.raises( - Exception, match='Syntax error in pattern "a pattern with a syntax error"' - ): + with pytest.raises(Exception, match='Syntax error in pattern "'): lb.build_lut() diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index e21c243ea55..41edea31844 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -150,3 +150,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`). 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. diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index f0a066b5bd8..bd70aff7b48 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -150,7 +150,7 @@ def build_lut(self) -> bytearray: # Parse and create symmetries of the patterns strings for p in self.patterns: - m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", "")) + m = re.search(r"(\w):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", "")) if not m: msg = 'Syntax error in pattern "' + p + '"' raise Exception(msg) From 31eee6e5f706cd0a41ac26c45694adef1eca72a3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:57:54 +1000 Subject: [PATCH 2021/2374] [pre-commit.ci] pre-commit autoupdate (#9180) --- .pre-commit-config.yaml | 10 +++++----- src/libImaging/Palette.c | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2be509d54de..23bda1ec76b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.7 + rev: v0.12.11 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.8 + rev: v21.1.0 hooks: - id: clang-format types: [c] @@ -36,7 +36,7 @@ repos: - id: rst-backticks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable @@ -51,14 +51,14 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.2 + rev: 0.33.3 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.11.0 + rev: v1.12.1 hooks: - id: zizmor diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c index 78916bca52b..da1d8050476 100644 --- a/src/libImaging/Palette.c +++ b/src/libImaging/Palette.c @@ -148,7 +148,7 @@ ImagingPaletteDelete(ImagingPalette palette) { #define BOX 8 -#define BOXVOLUME BOX *BOX *BOX +#define BOXVOLUME BOX * BOX * BOX void ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) { From 57a5f76e6d78f280fa7cd666bff32fe3452f140b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Sep 2025 21:09:07 +1000 Subject: [PATCH 2022/2374] Removed unused split --- src/PIL/ImageFont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a2bf9ccf92b..446160c2f22 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -129,7 +129,7 @@ def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: if file.readline() != b"PILfont\n": msg = "Not a PILfont file" raise SyntaxError(msg) - file.readline().split(b";") + file.readline() self.info = [] # FIXME: should be a dictionary while True: s = file.readline() From 485d9884cf7a3cd2ceedc91df9c8625454b6d8f5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Sep 2025 21:24:57 +1000 Subject: [PATCH 2023/2374] Limit length of read operation --- Tests/test_imagefont.py | 5 +++++ src/PIL/ImageFont.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 4565d35bab7..08034ad0de5 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -492,6 +492,11 @@ def test_stroke_mask() -> None: 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: with tempfile.NamedTemporaryFile(delete=False) as tmp: pass diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 446160c2f22..df2f00882eb 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -126,7 +126,7 @@ def _load_pilfont(self, filename: str) -> None: def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: # read PILfont header - if file.readline() != b"PILfont\n": + if file.read(8) != b"PILfont\n": msg = "Not a PILfont file" raise SyntaxError(msg) file.readline() From caacd38e1be189ed5a9d9ba892e595cbdfaa551b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Sep 2025 21:32:13 +1000 Subject: [PATCH 2024/2374] Raise mode error before reading --- Tests/test_imagefontpil.py | 8 ++++++++ src/PIL/ImageFont.py | 10 +++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 3eb98d3797d..8c1cb3f5860 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -30,6 +30,14 @@ def test_default_font(font: ImageFont.ImageFont) -> None: 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: original_core = ImageFont.core if features.check_module("freetype2"): diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index df2f00882eb..92eb763a51e 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -125,6 +125,11 @@ def _load_pilfont(self, filename: str) -> None: image.close() 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 if file.read(8) != b"PILfont\n": msg = "Not a PILfont file" @@ -140,11 +145,6 @@ def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: # read PILfont metrics data = file.read(256 * 20) - # check image - if image.mode not in ("1", "L"): - msg = "invalid font image mode" - raise TypeError(msg) - image.load() self.font = Image.core.font(image.im, data) From 0e22b0ca6c9577fcd5be0013ce6d10e0ee28999a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Sep 2025 18:33:52 +1000 Subject: [PATCH 2025/2374] Removed unused code --- Tests/test_font_crash.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index b82340ef70b..fb5026ee0ef 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -2,7 +2,7 @@ import pytest -from PIL import Image, ImageDraw, ImageFont +from PIL import ImageFont from .helper import skip_unless_feature @@ -12,10 +12,6 @@ def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: # from fuzzers.fuzz_font font.getbbox("ABC") font.getmask("test text") - with Image.new(mode="RGBA", size=(200, 200)) as im: - draw = ImageDraw.Draw(im) - draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) - draw.text((10, 10), "Test Text", font=font, fill="#000") @skip_unless_feature("freetype2") def test_segfault(self) -> None: From 72c067af2969517fde1979a4749c5076be96894a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Sep 2025 19:23:26 +1000 Subject: [PATCH 2026/2374] Check all reserved bytes in header --- Tests/images/crash-5762152299364352.fli | Bin 8731 -> 8731 bytes ...39147ce93e20eb14088fe238e541443ffd64b3.fli | Bin 200 -> 200 bytes ...f0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli | Bin 159 -> 159 bytes src/PIL/FliImagePlugin.py | 7 ++++++- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/images/crash-5762152299364352.fli b/Tests/images/crash-5762152299364352.fli index 944fe0b56c73b016c7599beb5b8e47cd33f0432f..d7588eea88f4a37d000e6c12949a13e219298092 100644 GIT binary patch delta 21 dcmbR3GTUW>)?^7rwTS^jlNA`{Ha5&w1OQM22K@j4 delta 28 kcmbR3GTUW>)@CbaM#jksjBb CO)#eb delta 86 zcmWm2u?;{#07l{8MIq5BbeD+Q1_~Rf%wPsBBT(B1!)Qe$?w<3SmwZQbM03?bgYhQ` nYbFW3g#Foq(fNm1&IqpHm^-(gUO4dtaIkM>y-n1w(q-sA4znvm diff --git a/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli b/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli index 77a94b87a3ade935e707f3d89c9fbff801a1e976..abe642e6a9d665941b34501c6c0879370cac87b0 100644 GIT binary patch literal 159 zcmccrk72RkdM*Y=0S1Bp3^3sGi1Yo=yXXHu{?G9L4a5Hi1}=tgFgXJB|Nmcs<{*qB Zq#UU9*GH!Ykh1?k0HS$81PJ_}4FLDPAGiPj delta 86 zcmbQwIG=HXme2qH9RHdAy#bQ51sE6@{xkgf52QdqTJHb None: # HEAD 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" raise SyntaxError(msg) From e73b5ff4cd0c2ba4587c49bf310bb1421b47e725 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Sep 2025 19:35:01 +1000 Subject: [PATCH 2027/2374] Do not unnecessarily update __offset --- src/PIL/FliImagePlugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index ccb8a5953d6..679a9edd9ac 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -77,8 +77,7 @@ def _open(self) -> None: if i16(s, 4) == 0xF100: # prefix chunk; ignore it - self.__offset = self.__offset + i32(s) - self.fp.seek(self.__offset) + self.fp.seek(self.__offset + i32(s)) s = self.fp.read(16) if i16(s, 4) == 0xF1FA: From caede14465b664c542eff9365afb128d0b19a729 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Sep 2025 21:46:54 +1000 Subject: [PATCH 2028/2374] Revert "Removed unused code" This reverts commit 0e22b0ca6c9577fcd5be0013ce6d10e0ee28999a. --- Tests/test_font_crash.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index fb5026ee0ef..b82340ef70b 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -2,7 +2,7 @@ import pytest -from PIL import ImageFont +from PIL import Image, ImageDraw, ImageFont from .helper import skip_unless_feature @@ -12,6 +12,10 @@ def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: # from fuzzers.fuzz_font font.getbbox("ABC") font.getmask("test text") + with Image.new(mode="RGBA", size=(200, 200)) as im: + draw = ImageDraw.Draw(im) + draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) + draw.text((10, 10), "Test Text", font=font, fill="#000") @skip_unless_feature("freetype2") def test_segfault(self) -> None: From abf088fae57ff5fb8476652531c84755fd7d2bdd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Sep 2025 21:52:27 +1000 Subject: [PATCH 2029/2374] Updated comment --- Tests/test_font_crash.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index b82340ef70b..54bd2d183ca 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -9,7 +9,8 @@ class TestFontCrash: 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.getmask("test text") with Image.new(mode="RGBA", size=(200, 200)) as im: From 877707379bda7923de612a4ed4116fd1ec3b6017 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Sep 2025 22:38:37 +1000 Subject: [PATCH 2030/2374] Deprecate Image._show --- Tests/test_image.py | 8 ++++++++ docs/deprecations.rst | 8 ++++++++ docs/releasenotes/12.0.0.rst | 6 ++++++ src/PIL/Image.py | 5 ++++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index be7ca6a6f11..eb3882ddcea 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -19,6 +19,7 @@ ImageDraw, ImageFile, ImagePalette, + ImageShow, UnidentifiedImageError, features, ) @@ -1047,6 +1048,13 @@ def test_get_child_images(self) -> None: with pytest.warns(DeprecationWarning, match="Image.Image.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))) def test_zero_tobytes(self, size: tuple[int, int]) -> None: im = Image.new("RGB", size) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 3f95cf7f545..e31d3c31c91 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -61,6 +61,14 @@ ImageCms.ImageCmsProfile.product_name and .product_info ``.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. +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 ---------------- diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index 41edea31844..12bf760e28a 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -116,6 +116,12 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 354118a87ae..5a457803b62 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2632,7 +2632,9 @@ def show(self, title: str | None = None) -> None: :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, ...]: """ @@ -3797,6 +3799,7 @@ def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: def _show(image: Image, **options: Any) -> None: from . import ImageShow + deprecate("Image._show", 13, "ImageShow.show") ImageShow.show(image, **options) From f0bbab94a6da39b0366d0f55c9f033c6ab335d28 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Sep 2025 07:23:15 +1000 Subject: [PATCH 2031/2374] Updated libjpeg-turbo to 3.1.2 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index c79cd2f17d1..b4309e8d976 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -96,7 +96,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=11.3.3 LIBPNG_VERSION=1.6.50 -JPEGTURBO_VERSION=3.1.1 +JPEGTURBO_VERSION=3.1.2 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 TIFF_VERSION=4.7.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5633519ddb9..7539cff82f1 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -117,7 +117,7 @@ def cmd_msbuild( "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", "HARFBUZZ": "11.3.3", - "JPEGTURBO": "3.1.1", + "JPEGTURBO": "3.1.2", "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.4.0", From e0da1a62ec120cba1ae32a38880dd7c749051bda Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Sep 2025 08:10:31 +1000 Subject: [PATCH 2032/2374] Use walrus operator --- src/PIL/WalImageFile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index 87e32878b19..5494f62e892 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -49,8 +49,7 @@ def _open(self) -> None: # strings are null-terminated self.info["name"] = header[:32].split(b"\0", 1)[0] - next_name = header[56 : 56 + 32].split(b"\0", 1)[0] - if next_name: + if next_name := header[56 : 56 + 32].split(b"\0", 1)[0]: self.info["next_name"] = next_name def load(self) -> Image.core.PixelAccess | None: From cfca02a75970cc2816ce341eae5099e1465c6ac9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Sep 2025 08:27:52 +1000 Subject: [PATCH 2033/2374] Improved WAL test coverage --- Tests/test_file_wal.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index b15d79d61e0..549d47054e7 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -1,5 +1,7 @@ from __future__ import annotations +from io import BytesIO + from PIL import WalImageFile from .helper import assert_image_equal_tofile @@ -13,12 +15,22 @@ def test_open() -> None: assert im.format_description == "Quake2 Texture" assert im.mode == "P" assert im.size == (128, 128) + assert "next_name" not in im.info assert isinstance(im, WalImageFile.WalImageFile) 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: with WalImageFile.open(TEST_FILE) as im: px = im.load() From 73490e10ad7dd7821aed94ee088cef82659a9fa1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Sep 2025 21:00:13 +1000 Subject: [PATCH 2034/2374] Mention Pillow 11.3.0 behaviour --- docs/deprecations.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 8f7800ba5f9..a3c2c55db99 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -35,11 +35,12 @@ Image.fromarray mode parameter .. deprecated:: 11.3.0 -Using the ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` to change data types -has been deprecated. 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. +Using the ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` was deprecated in +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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 5de27c6258f9c4c7a3686d6e2ae9ce07c4ec1138 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Sep 2025 21:09:00 +1000 Subject: [PATCH 2035/2374] Split versionadded info --- docs/reference/ImageGrab.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index f6a2ec5bc03..5c3a73fada1 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -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 ``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. 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. - .. 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, or None if the clipboard does not contain image data or filenames. From 54d329f98f214bbaf6ee23df0aec91da7f03f035 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 4 Sep 2025 23:26:47 +1000 Subject: [PATCH 2036/2374] Updated harfbuzz to 11.4.5 (#9150) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index cbd8534aa4c..1fa63409626 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -94,7 +94,7 @@ ARCHIVE_SDIR=pillow-depends-main # annotations have a source code patch that is required for some platforms. If # you change those versions, ensure the patch is also updated. FREETYPE_VERSION=2.13.3 -HARFBUZZ_VERSION=11.3.3 +HARFBUZZ_VERSION=11.4.5 LIBPNG_VERSION=1.6.50 JPEGTURBO_VERSION=3.1.2 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 4ba68380142..ba69878bcfd 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ def cmd_msbuild( "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "11.3.3", + "HARFBUZZ": "11.4.5", "JPEGTURBO": "3.1.2", "LCMS2": "2.17", "LIBAVIF": "1.3.0", From 476b122ae45f1f6efd48f07f609114b4987c74c0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 Sep 2025 20:00:04 +1000 Subject: [PATCH 2037/2374] Simplified code --- src/PIL/CurImagePlugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index b817dbc87b8..868ff50b535 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -17,7 +17,7 @@ # from __future__ import annotations -from . import BmpImagePlugin, Image, ImageFile +from . import BmpImagePlugin, Image from ._binary import i16le as i16 from ._binary import i32le as i32 @@ -63,8 +63,7 @@ def _open(self) -> None: # patch up the bitmap height self._size = self.size[0], self.size[1] // 2 - d, e, o, a = self.tile[0] - self.tile[0] = ImageFile._Tile(d, (0, 0) + self.size, o, a) + self.tile = [self.tile[0]._replace(extents=(0, 0) + self.size)] # From a52979785756bd2cd68aa3910945dce5ed96138f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 Sep 2025 20:04:50 +1000 Subject: [PATCH 2038/2374] Assert fp is not None --- src/PIL/FliImagePlugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 679a9edd9ac..b9cc8ad6c01 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -48,6 +48,7 @@ class FliImageFile(ImageFile.ImageFile): def _open(self) -> None: # HEAD + assert self.fp is not None s = self.fp.read(128) if not (_accept(s) and s[20:22] == b"\x00\x00"): msg = "not an FLI/FLC file" @@ -110,6 +111,7 @@ def _palette(self, palette: list[tuple[int, int, int]], shift: int) -> None: # load palette i = 0 + assert self.fp is not None for e in range(i16(self.fp.read(2))): s = self.fp.read(2) i = i + s[0] From bf18e5fe8bf837ba6756bef0384eae82504c7883 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 Sep 2025 20:03:31 +1000 Subject: [PATCH 2039/2374] Assert fp is not None --- Tests/test_file_cur.py | 1 + src/PIL/CurImagePlugin.py | 1 + 2 files changed, 2 insertions(+) diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index dbf1b866d7f..ff82e2983eb 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -26,6 +26,7 @@ def test_invalid_file() -> None: no_cursors_file = "Tests/images/no_cursors.cur" cur = CurImagePlugin.CurImageFile(TEST_FILE) + assert cur.fp is not None cur.fp.close() with open(no_cursors_file, "rb") as cur.fp: with pytest.raises(TypeError): diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index 868ff50b535..9c188e08446 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -38,6 +38,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): format_description = "Windows Cursor" def _open(self) -> None: + assert self.fp is not None offset = self.fp.tell() # check magic From 067569790ba47e4149114cb3cd5df8561c8c0b52 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 Sep 2025 20:11:02 +1000 Subject: [PATCH 2040/2374] Test largest cursor --- Tests/test_file_cur.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index ff82e2983eb..4b3e3afcb43 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -1,8 +1,13 @@ from __future__ import annotations +from io import BytesIO + import pytest from PIL import CurImagePlugin, Image +from PIL._binary import o8 +from PIL._binary import o16le as o16 +from PIL._binary import o32le as o32 TEST_FILE = "Tests/images/deerstalker.cur" @@ -17,6 +22,24 @@ def test_sanity() -> None: assert im.getpixel((16, 16)) == (84, 87, 86, 255) +def test_largest_cursor() -> None: + magic = b"\x00\x00\x02\x00" + sizes = ((1, 1), (8, 8), (4, 4)) + data = magic + o16(len(sizes)) + for w, h in sizes: + image_offset = 6 + len(sizes) * 16 if (w, h) == max(sizes) else 0 + data += o8(w) + o8(h) + o8(0) * 10 + o32(image_offset) + data += ( + o32(12) # header size + + o16(8) # width + + o16(16) # height + + o16(0) # planes + + o16(1) # bits + ) + with Image.open(BytesIO(data)) as im: + assert im.size == (8, 8) + + def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" From d4ed512bec3258d38a9debd62cdd08fe86f4c27c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 Sep 2025 23:14:52 +1000 Subject: [PATCH 2041/2374] Use monkeypatch --- Tests/test_imageshow.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 7a2f5876757..8d6731acc91 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -59,15 +59,12 @@ def test_show(mode: str) -> None: assert ImageShow.show(im) -def test_show_without_viewers() -> None: - viewers = ImageShow._viewers - ImageShow._viewers = [] +def test_show_without_viewers(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageShow, "_viewers", []) with hopper() as im: assert not ImageShow.show(im) - ImageShow._viewers = viewers - @pytest.mark.parametrize( "viewer", From 2bf482230d61d785e17d74bd69dcb2fa2a71b1d1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 Sep 2025 23:43:47 +1000 Subject: [PATCH 2042/2374] Test unsupported BMP bitfields layout --- Tests/test_file_bmp.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 746b2e18061..c1c430aa5b5 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -6,6 +6,8 @@ import pytest from PIL import BmpImagePlugin, Image, _binary +from PIL._binary import o16le as o16 +from PIL._binary import o32le as o32 from .helper import ( assert_image_equal, @@ -114,7 +116,7 @@ def test_save_float_dpi(tmp_path: Path) -> None: def test_load_dib() -> None: - # test for #1293, Imagegrab returning Unsupported Bitfields Format + # test for #1293, ImageGrab returning Unsupported Bitfields Format with Image.open("Tests/images/clipboard.dib") as im: assert im.format == "DIB" assert im.get_format_mimetype() == "image/bmp" @@ -219,6 +221,18 @@ def test_rle8_eof(file_name: str, length: int) -> None: im.load() +def test_unsupported_bmp_bitfields_layout() -> None: + fp = io.BytesIO( + o32(40) # header size + + b"\x00" * 10 + + o16(1) # bits + + o32(3) # BITFIELDS compression + + b"\x00" * 32 + ) + with pytest.raises(OSError, match="Unsupported BMP bitfields layout"): + Image.open(fp) + + def test_offset() -> None: # This image has been hexedited # to exclude the palette size from the pixel data offset From 4469ee0fc0206326cd6cf016d31ce204342e35db Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Sep 2025 12:25:56 +1000 Subject: [PATCH 2043/2374] Test saving P4 images --- Tests/test_file_ppm.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 68f2f946855..ca5347f0fa9 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -92,6 +92,13 @@ def test_16bit_pgm() -> None: assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.tiff") +def test_p4_save(tmp_path: Path) -> None: + with Image.open("Tests/images/hopper_1bit.pbm") as im: + filename = tmp_path / "temp.pbm" + im.save(filename) + assert_image_equal_tofile(im, filename) + + def test_16bit_pgm_write(tmp_path: Path) -> None: with Image.open("Tests/images/16_bit_binary.pgm") as im: filename = tmp_path / "temp.pgm" From 7d379842c12f33c9cf972ee7fda1e16d207fbbe6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Sep 2025 12:28:20 +1000 Subject: [PATCH 2044/2374] Test saving unsupported mode --- Tests/test_file_ppm.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index ca5347f0fa9..598e9a445b6 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -141,6 +141,12 @@ def test_pfm_big_endian(tmp_path: Path) -> None: assert_image_equal_tofile(im, filename) +def test_save_unsupported_mode(tmp_path: Path) -> None: + im = hopper("P") + with pytest.raises(OSError, match="cannot write mode P as PPM"): + im.save(tmp_path / "out.ppm") + + @pytest.mark.parametrize( "data", [ From b90fe802ced1318a9b1b76dc78e53c458d75340b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Sep 2025 12:49:10 +1000 Subject: [PATCH 2045/2374] Test transparency --- Tests/test_file_gd.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index 806532c17ec..8a49fd4fa53 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -1,5 +1,7 @@ from __future__ import annotations +from io import BytesIO + import pytest from PIL import GdImageFile, UnidentifiedImageError @@ -16,6 +18,14 @@ def test_sanity() -> None: assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.jpg", 14) +def test_transparency() -> None: + with open(TEST_GD_FILE, "rb") as fp: + data = bytearray(fp.read()) + data[7:11] = b"\x00\x00\x00\x05" + with GdImageFile.open(BytesIO(data)) as im: + assert im.info["transparency"] == 5 + + def test_bad_mode() -> None: with pytest.raises(ValueError): GdImageFile.open(TEST_GD_FILE, "bad mode") From a58fc562f08ece7763824fea1e5ee02ed3000024 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 13:55:35 +1000 Subject: [PATCH 2046/2374] Update github-actions (#9194) --- .github/workflows/docs.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/stale.yml | 2 +- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 2 +- .github/workflows/wheels.yml | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 761dc112520..cf917407c6c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -37,7 +37,7 @@ jobs: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" cache: pip diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9827ef1cd7e..2addbaf674d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -33,7 +33,7 @@ jobs: lint-pre-commit- - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" cache: pip diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 61ccf58e2ea..1b0c3c654e7 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -22,7 +22,7 @@ jobs: steps: - name: "Check issues" - uses: actions/stale@v9 + uses: actions/stale@v10 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "Awaiting OP Action" diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index d55a8e5f51f..e12a5b1f7d1 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -67,7 +67,7 @@ jobs: # sets env: pythonLocation - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b17d08892a1..8504e5c1e2e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,7 +70,7 @@ jobs: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 24e78f965c5..81a68813526 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -111,7 +111,7 @@ jobs: persist-credentials: false submodules: true - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.x" @@ -164,7 +164,7 @@ jobs: repository: python-pillow/test-images path: Tests\test-images - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.x" @@ -239,7 +239,7 @@ jobs: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" From 2d8244c45adeab9fecdf7e1fa3adc9af175a67d8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 Sep 2025 23:39:04 +1000 Subject: [PATCH 2047/2374] Added GitHub profile link --- docs/releasenotes/12.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index b166f51b365..0a03b982fc7 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -171,4 +171,4 @@ 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. +strings. Reported by Jang Choi (https://github.com/uko3211). From 4b8bcb6f379e8073519b3afff745156542f78258 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 9 Sep 2025 00:04:01 +1000 Subject: [PATCH 2048/2374] Use link Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/releasenotes/12.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index 0a03b982fc7..de9d6dffd3b 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -171,4 +171,4 @@ 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). +strings. Reported by `Jang Choi `__. From 3a580e0f79bfb5bd491f24e604960e4823c4f58f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Sep 2025 21:04:16 +1000 Subject: [PATCH 2049/2374] Use _ensure_mutable --- src/PIL/Image.py | 4 +--- src/PIL/ImageDraw.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b17fd131d2c..95bf2da3f2f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2070,9 +2070,7 @@ def putpixel( :param value: The pixel value. """ - if self.readonly: - self._copy() - self.load() + self._ensure_mutable() if ( self.mode in ("P", "PA") diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index ed46899b455..1384f11696e 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -76,9 +76,7 @@ def __init__(self, im: Image.Image, mode: str | None = None) -> None: must be the same as the image mode. If omitted, the mode defaults to the mode of the image. """ - im.load() - if im.readonly: - im._copy() # make it writeable + im._ensure_mutable() blend = 0 if mode is None: mode = im.mode From 410fb60f65f21d6b1ee968b3d9d278d0ef97e355 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Sep 2025 22:01:07 +1000 Subject: [PATCH 2050/2374] Added alpha channel examples --- docs/reference/ImageDraw.rst | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 4a2223a40c5..6768a04c600 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -57,6 +57,43 @@ Color names See :ref:`color-names` for the color names supported by Pillow. +Alpha channel +^^^^^^^^^^^^^ + +By default, when drawing onto an existing image, the image's pixel values are simply +replaced by the new color:: + + im = Image.new("RGBA", (1, 1), (255, 0, 0)) + d = ImageDraw.Draw(im) + d.rectangle((0, 0, 1, 1), (0, 255, 0, 127)) + assert im.getpixel((0, 0)) == (0, 255, 0, 127) + + # Alpha channel values have no effect when drawing with RGB mode + im = Image.new("RGB", (1, 1), (255, 0, 0)) + d = ImageDraw.Draw(im) + d.rectangle((0, 0, 1, 1), (0, 255, 0, 127)) + assert im.getpixel((0, 0)) == (0, 255, 0) + +If you would like to combine translucent color with an RGB image, then initialize the +ImageDraw instance with the RGBA mode:: + + from PIL import Image, ImageDraw + im = Image.new("RGB", (1, 1), (255, 0, 0)) + d = ImageDraw.Draw(im, "RGBA") + d.rectangle((0, 0, 1, 1), (0, 255, 0, 127)) + assert im.getpixel((0, 0)) == (128, 127, 0) + +If you would like to combine translucent color with an RGBA image underneath, you will +need to combine multiple images:: + + from PIL import Image, ImageDraw + im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) + im2 = Image.new("RGBA", (1, 1)) + d = ImageDraw.Draw(im2) + d.rectangle((0, 0, 1, 1), (0, 255, 0, 127)) + im.paste(im2.convert("RGB"), mask=im2) + assert im.getpixel((0, 0)) == (128, 127, 0, 255) + Fonts ^^^^^ From 5df7f98a591e494cd2b0516c3f49d37eb8ee2dd9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Sep 2025 13:16:12 +1000 Subject: [PATCH 2051/2374] Updated Ghostscript to 10.6.0 --- .github/workflows/test-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e12a5b1f7d1..f6a7dd46b04 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -97,8 +97,8 @@ jobs: choco install nasm --no-progress echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - choco install ghostscript --version=10.5.1 --no-progress - echo "C:\Program Files\gs\gs10.05.1\bin" >> $env:GITHUB_PATH + choco install ghostscript --version=10.6.0 --no-progress + echo "C:\Program Files\gs\gs10.06.0\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images From d70cba37627586b243a9b3aeac7899f5389d3ba8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:55:50 +1000 Subject: [PATCH 2052/2374] Update dependency mypy to v1.18.1 (#9207) --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index bd95638008d..68d69c18333 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.17.1 +mypy==1.18.1 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 53302c2281a9576acabf5815f3eda1f48f253cf0 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 16 Sep 2025 19:43:03 +1000 Subject: [PATCH 2053/2374] Split versionadded info Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/reference/ImageGrab.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 25afc99266c..00d7f8e3c45 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -44,7 +44,8 @@ or the clipboard to a PIL image memory. :param window: Capture a single window. On Windows, this is a HWND. On macOS, it uses windowid. - .. versionadded:: 11.2.1 (Windows), 12.0.0 (macOS) + .. versionadded:: 11.2.1 Windows support + .. versionadded:: 12.0.0 macOS support :return: An image .. py:function:: grabclipboard() From ca3528f46eacb005d3875410655b6ae9dc91c45c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 16 Sep 2025 21:43:24 +1000 Subject: [PATCH 2054/2374] Document that macOS window value is a CGWindowID --- docs/reference/ImageGrab.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index c6671ca7107..e7dd41de1b7 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -44,7 +44,8 @@ or the clipboard to a PIL image memory. .. versionadded:: 7.1.0 :param window: - Capture a single window. On Windows, this is a HWND. On macOS, it uses windowid. + Capture a single window. On Windows, this is a HWND. On macOS, this is a + CGWindowID. .. versionadded:: 11.2.1 Windows support .. versionadded:: 12.0.0 macOS support From c8b4a24e75d894a2fa7233c4acaf75ed061d08f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Sep 2025 19:51:50 +1000 Subject: [PATCH 2055/2374] Updated macOS tested Pillow versions --- docs/installation/platform-support.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 3c5e4cd519f..186d9b96da5 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -75,6 +75,8 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | 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 | | +----------------------------+------------------+ | | | 3.8 | 10.4.0 | | From 9e4256e8aa9d7adad4271069732c48d129d5b38f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 22:22:30 +1000 Subject: [PATCH 2056/2374] Update dependency mypy to v1.18.2 (#9213) --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 68d69c18333..447856433b4 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.18.1 +mypy==1.18.2 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 92e671d7970b8f96c50424c0c47efd0a1c95bc51 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Sep 2025 20:15:20 +1000 Subject: [PATCH 2057/2374] Updated tests for FreeType 2.14.1 --- Tests/images/colr_bungee.png | Bin 4545 -> 4350 bytes Tests/images/colr_bungee_mask.png | Bin 2789 -> 2534 bytes Tests/images/colr_bungee_older.png | Bin 0 -> 4545 bytes Tests/test_imagedraw.py | 8 ++++++-- Tests/test_imagefont.py | 10 +++++++--- Tests/test_imagefontctl.py | 2 +- 6 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 Tests/images/colr_bungee_older.png diff --git a/Tests/images/colr_bungee.png b/Tests/images/colr_bungee.png index b10a60be057c4654ebcf36de28870e6bd6ee8010..9ec6b11823b429c1b7280b1539ce3bb2c4511e37 100644 GIT binary patch literal 4350 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 literal 2789 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 diff --git a/Tests/images/colr_bungee_older.png b/Tests/images/colr_bungee_older.png new file mode 100644 index 0000000000000000000000000000000000000000..b10a60be057c4654ebcf36de28870e6bd6ee8010 GIT binary patch literal 4545 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 None: def draw_text() -> None: 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) @@ -1513,7 +1515,9 @@ def draw_textbbox() -> None: def draw_multiline_text() -> None: 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) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 4565d35bab7..4b8a61eb3c1 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -19,6 +19,7 @@ assert_image_equal, assert_image_equal_tofile, assert_image_similar_tofile, + has_feature_version, is_win32, skip_unless_feature, skip_unless_feature_version, @@ -549,7 +550,7 @@ def test_default_font() -> None: draw.text((10, 60), txt, font=larger_default_font) # 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")) @@ -1055,7 +1056,10 @@ def test_colr(layout_engine: ImageFont.Layout) -> None: 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") @@ -1071,7 +1075,7 @@ def test_colr_mask(layout_engine: ImageFont.Layout) -> None: 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: diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 95af3fda8c4..633f6756b0c 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -183,7 +183,7 @@ def test_x_max_and_y_offset() -> None: draw.text((0, 0), "لح", font=ttf, fill=500) 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: From 6916a73b579df0f78ee9734b83f58c2acbb8b17e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Sep 2025 20:16:50 +1000 Subject: [PATCH 2058/2374] Build FreeType 2.14.1 on macOS 13, instead of using 2.14.0 from brew --- .github/workflows/macos-install.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 94e3d5d085e..8060e0850e6 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -4,11 +4,19 @@ set -e if [[ "$ImageOS" == "macos13" ]]; then 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 brew install \ aom \ dav1d \ - freetype \ ghostscript \ jpeg-turbo \ libimagequant \ From 04177eb6ba6af0aaea8d959e99d4cff7cd22c798 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Sep 2025 20:17:10 +1000 Subject: [PATCH 2059/2374] Updated FreeType to 2.14.1 on Windows --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index ba69878bcfd..5c638829e18 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -114,7 +114,7 @@ def cmd_msbuild( V = { "BROTLI": "1.1.0", - "FREETYPE": "2.13.3", + "FREETYPE": "2.14.1", "FRIBIDI": "1.0.16", "HARFBUZZ": "11.4.5", "JPEGTURBO": "3.1.2", From d64f56f53bde0f262250068471d3713c9d6ed3e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Sep 2025 07:38:17 +1000 Subject: [PATCH 2060/2374] Updated openjpeg to 2.5.4 --- .github/workflows/wheels-dependencies.sh | 2 +- depends/install_openjpeg.sh | 2 +- docs/installation/building-from-source.rst | 2 +- winbuild/build_prepare.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 1fa63409626..f400994d7f0 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -97,7 +97,7 @@ FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=11.4.5 LIBPNG_VERSION=1.6.50 JPEGTURBO_VERSION=3.1.2 -OPENJPEG_VERSION=2.5.3 +OPENJPEG_VERSION=2.5.4 XZ_VERSION=5.8.1 ZSTD_VERSION=1.5.7 TIFF_VERSION=4.7.0 diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index 1f8d781931b..bc7c7c634ed 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -1,7 +1,7 @@ #!/bin/bash # 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 diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index fc7ef7646c5..656d54325d4 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -58,7 +58,7 @@ Many of Pillow's features require external libraries: * **openjpeg** provides JPEG 2000 functionality. * 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 with Debian Jessie. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5c638829e18..30f7a123c8a 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -123,7 +123,7 @@ def cmd_msbuild( "LIBIMAGEQUANT": "4.4.0", "LIBPNG": "1.6.50", "LIBWEBP": "1.6.0", - "OPENJPEG": "2.5.3", + "OPENJPEG": "2.5.4", "TIFF": "4.7.0", "XZ": "5.8.1", "ZLIBNG": "2.2.5", From 222933df542d9a0c956bad2b8a4429ed7c973145 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 Sep 2025 21:48:01 +1000 Subject: [PATCH 2061/2374] Seek past BeginBinary data when parsing metadata --- Tests/test_file_eps.py | 8 ++++++++ src/PIL/EpsImagePlugin.py | 3 +++ 2 files changed, 11 insertions(+) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index d94de728709..b50915f28c3 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -197,6 +197,14 @@ def test_load_long_binary_data(prefix: bytes) -> None: assert img.format == "EPS" +def test_begin_binary() -> None: + with open("Tests/images/eps/binary_preview_map.eps", "rb") as fp: + data = bytearray(fp.read()) + data[76875 : 76875 + 11] = b"%" * 11 + with Image.open(io.BytesIO(data)) as img: + assert img.size == (399, 480) + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 5e2ddad99e9..69f3062b4d4 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -354,6 +354,9 @@ def read_comment(s: str) -> bool: read_comment(s) elif bytes_mv[:9] == b"%%Trailer": trailer_reached = True + elif bytes_mv[:14] == b"%%BeginBinary:": + bytecount = int(byte_arr[14:bytes_read]) + self.fp.seek(bytecount, os.SEEK_CUR) bytes_read = 0 # A "BoundingBox" is always required, From 9ba1029d515c5113bd0b1ea4f99fb5d3f1a9659b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Sep 2025 22:28:30 +1000 Subject: [PATCH 2062/2374] Clear C image when MPO frame image size changes --- Tests/test_file_mpo.py | 2 ++ src/PIL/JpegImagePlugin.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 9262e6ca781..ba05bbe4365 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -108,9 +108,11 @@ def test_frame_size() -> None: # in the SOF marker of the second frame with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: assert im.size == (640, 480) + im.load() im.seek(1) assert im.size == (680, 480) + im.load() im.seek(0) assert im.size == (640, 480) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 0d110035e1a..755ca648e55 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -193,6 +193,8 @@ def SOF(self: JpegImageFile, marker: int) -> None: n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) self._size = i16(s, 3), i16(s, 1) + if self._im is not None and self.size != self.im.size: + self._im = None self.bits = s[0] if self.bits != 8: From ce8d05484b71737a352eb6a52332cb7856b597c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Sep 2025 22:31:15 +1000 Subject: [PATCH 2063/2374] Use naturally created image --- Tests/images/frame_size.mpo | Bin 0 -> 14574 bytes Tests/images/sugarshack_frame_size.mpo | Bin 120198 -> 0 bytes Tests/test_file_mpo.py | 10 ++++------ 3 files changed, 4 insertions(+), 6 deletions(-) create mode 100644 Tests/images/frame_size.mpo delete mode 100644 Tests/images/sugarshack_frame_size.mpo diff --git a/Tests/images/frame_size.mpo b/Tests/images/frame_size.mpo new file mode 100644 index 0000000000000000000000000000000000000000..ee5c6cdf7a926901d490becc6892cc18dfc075f5 GIT binary patch literal 14574 zcmdsdc|6qL_x~L`6^TiC+oHlu_9eT}*s_foWGUJAeMzz;NlheM3!?0Up<+tO(qd_m zvPTOe2`OZmZ1cOL^6vL}e7=vz@Av!jcgJhy`MUR_hAWK*!WVpEk^4h>7rW2f)-6pa1}DU<-l^FhIltuQJ5eb-WGYTZnCd5&k0J2O#(X z^Ew9LG2)*M{2U?in+8fi$gT6TLb^_b{5s}^cmScXA@dtT7cl*j!vGL^|6&H7fBQ=G z`cn@(hD9eZ(AN!kS$GEcyNSptidX~(h?qJ>(3=e5MJHPSl~a*cmX-%VURGX7O;%M+ zc0Z(6lT%hxRQPY)droKlv4i39>FXQ#`8m;lZ8|6Z3p1Po;1U3I+n8kK73425{;`jt z^Aghr{(R}z_(3{`rAyrF^$;^MT-vsQIZ_e7@TN=Y39T@r37&q!g z)0j3eDvkBGZg4iz*ni`#5OZws%BS(J*TZpUR7;DZ_r;PAv2NO>bsd-#_sfH{)W2}r zzxc)<8|7qWZy&Mb$#G^%>O~j;|GJWJ zteF*o@Sl1}DY9O_2iB{BU4Paevm+2&r=jcM0rqUj3;=sKco~8C2K{k>-jEXur2f!w zodVK-FdQ|R4Xg=dH+T!-i2fOeP_TakGXa$i>G4yPbj9BlQ1b`n4oLI#0E=ph^t3y?b!@k-`RAdHbm}vR69|8V1pF` zPD0K&h{8Y^;ur#l(1tb^z`5~(yg}>y(U2w*UWXATh=WkaVc-UrjTht%ho4AjLj>eb z1kQoJULm2fvfVJqFfp^RvRS$NI(dXxg**9%x!Hz#`MHI-IQhC+xdjHecy2}T0I=YM z0|!5BQ_p%1cQqR~pP;yujEW|GEkU zz6cT-th*ZkjzBE3&SW9N6trJS)*q*D=5lJDaz^UN~tKTC`c(PsUDQq zm6gNDVw54rPaS{|&w{`o`YyM15rCHf7%vY1z`s*YNmfBlUv9sYj?#W*DCD55lS+pN`=y{RiovS^7iE0r*I@fTJu9 zR{Zj$+g40|-*dt`=KmqH5^;8N@ev6P_JVnrh>Me-Tdl>M1)>j|H4kwreGQn0q|IlDd16`5Bwss<{pJDX)ZMBn2NT7~|g$Z54pIDc)fu3e|WTf!+uyo6Z9#nZ_Z;;#_zbN;K|<1g&z8%FP! zUg(x?cJsG_2?vbr00@ACaO;Oa2kr|U+zB|s7C_;DpjTX{*Mp8C5K;z1J`d=4LLrY1 ztf5PAgV+c*U1Y9%Rn*eCLALtb9v@VXm3K-no0hlcTU<=IPcwR#u`w(}9U#kSMSr;zR4>a13 zy>MZr1F(9DMqAE@L9hTWp?3f@`O^kr(BN4wB0M}IQX(QEQkqh*Xqsx$i|G;5^>XBQ z30yDn^pd_&5PUm!>=D~@KuqkwHBT$gYya1U_8zWvCR!8VW&`>_g%KeN7`PFP+z8rJ zL_VA~X8H+iV?TpG9+{nE6Fe*+7#SIu7@3)w;M#^U10D&OxS6-@lG9<~!8);u2JyjX@7XIZA&F8{Qr@qks-|}kqi@J#T_oDGZqvIib` z5DZLAj7+R_dk_rA>Be$1G4GON*`|YKbqeAUm5*cN)lI#5`w_dCg5@;QIrt67_T7rZ zduHe+ty}beX3**XCyO==+OUT<05&rs;Dj-9!+h{6k7Y=l<vGwsCKF?y70wS6Mr?A4uNZjNG$#UQn4}zJL5c3hi}HM#uYAQ)8Eu zwjZ(~Iblkyk#}fdW;R%a9oJSiqqc2M%K{z8*x^smw7W^@^F?4idj0Qil#|izO}tOsO9LtR6|XcJAO#Rh zUO*wlc_rqZ?s0~mNY`xk=1YsQ;}-naV@ICFeq76R=sfT|WpP|0pdKTuO?yl0rwua{ zdWMDus>#Ushe$iQ1~|J(y9D^jL^uV)C?YEZG$Pa@`~v;lLY+h+{CxdG)FL!R*U8i% zrdP{|ima1_`e=&6U?ZXz5bOpck*u_=s1^s@>>9zY?rN3?^?$R&J5ABwJsv-PT>7|z zbU?6&jGU^fs*J3>jJ&)Qq>u_Z;UDT0A>|(;_DjegIR~MGhaoT2E5KibF4xI9AS_f< zRFvM)M){?uQ=qHYMo)Cx=uartZ42~@aPxI_3U%`GcMn*%^1tyL9<(%o6sAsr|6R(z z?Y~5<8~aNK4SL^dI$;rBzFtnjCye}@Jlyan0^PL2g8kLt=&G&jq88{G5E`HcPrtHi zPGJ#hp02?jutPtmKn0RI>n7+^lf2n!SRVX!c>Fs~Pu--UI(u+afdJ4^t{G)kcjF#0l6N{9U)nDy@qWsTcjzfT zKCADJ@WZknQ}4X*qm%IeTuy#rwk@2ckW=o^L+nF5p?} z>=O~d$9gv_zQ?=2O6;Cjxsh&kzx0Dwg&FF?HdBL#9Hx{4amA_b070T2rRc*+{+NmV1pm)O{h)Sr zCQGcDHClfpr|V6!X>CufT}?r%ZXfN<+JSfh=VFlM#j4dkBpJR;2sArEJQH)3=#pG* zKfUX0cbu(sB2xH@0sL4dXrd0p*K3HhU<_}^nFG#DS&ZUrLD4@!?RycFs}roE)$iH% z-t3oD=FrWBj$AyBaO}b@V$>z(O+xCpyF%T*O0teuv@%5zg!?tVEM;vEXRv-+88;;# z%NR$P)=NGz3nE+>Q@WA5LZ)@D=`c7KvhuFpA1hM#EMi2;S4F1UXyB(?njaIsh~otB za@jRIr8euCaTpeOjpI;bgova$;u(WMq-dpDcb$6I>B!DIV>^gQd0ELSAwI24homH{ z?E=nC9;W; zjB7WU?U=${Jbk%DkXZ_e3N7JICFJXKqB2gglp4+zvPtx#6dpD2G06+Rj^V{x8dzd6 z8fZ?8YH$h3?xI*y{7x*l$<$}P{zEv*Q1{cM3j|(k49Ks$y0grBAtj3 z>1glxsSYI1WONyMwp= zrAk2v&XZ`7q7SZi>v-HRXMcD3-3f*pnh}LWoJyf&yK(A_IeWYGBTGa&Ava_5wB#dBZ@cg= z!dd))JX+c6X9!-QrprqrpK#60s~d|kDPTx$RQBq|-OVy_&bi7`VtMs*%?*9YOjcoI zTJPF{Zp1Sp%9W9Kg(D(~ySGAg3K@fF4t;Rzt$%9nd^=B4X))HUqh12!qE-$f2$vEV zQ_a}ehH$-%-S|e2$cTgt`~`N!nVkaod|B3ZtfI?wMcuiPeJ7A9B3O&<3>V)PWT_U7 z_ZA)uc}}>(+L?(RJXKt5Z#(#nbl6n-=h|2Is@5AmoLYy%wG-?eIU?fLrpogklS)oU z9gLY{_MSTmdIneBw-*VqlehanGJD;@6Mg6xCi!xW4yW}^*5BxRDWxqVgjqMSB9+&TZ zgd#Rw66-Xf{o~~tzANAHVreW&r{F{UqPFp^2S@sM4lY^#v+3rFlG?yaz>EqbSdDlf zQHcV6C2n&(U{ zf@V=3zeNj*Yl-a;3ZR#vV|qbpB5HT(nEe?CBEXa7dI+Pcd^Aw%zpw zW%8%mGuw_zv$3&Y?`rSLXm7F7oLMq=_l}EK%Vxbp7=RLVz(FFb`C&%deFqZP7$jY( zlZsEjkyG-V@Y$4E^A~olKRMb12-ZdCl}oibyr|jEt!tWHv&AXr9{7aC84Q~VZ>`Cl zt{Oc=1Dk{m1WurTT4;%WX3(P=f}J8~8$g+R=68eR9QCn`j>WwT84IN;gx5*kj?9Z8`1h64#fAMC%h=O!#3v z7(%ATP5ZBpuNgKyQmi{1N;ELmHP_Mq7@5@CY~>VBfC0ip^sY#FxigEP#B2xI+IQ?` zsbdNj`%S1g`AK)DYiz1fs^zz%X9$UrJzi{IP}$sBaam>RR+c5DN`m{*MzcMq^qDmy zdrFsP?Bs}(cHAt(L3Z5wtL2swDoC+MwzKxm)rF=d$eed_yaOowxOJkAHLH##lcKMZ z$dL2(_;G!%q8(b964;C0%16l9j4@Iyj14w?=4RIzZk`ma-(ry_Uys3g-V+$=mf;d` zFPOFekg_|)E+VD>&FC_}59Z?%USGz16z^*N3X~-!XH`7ANXCvJxsNsx;w~+9yJ?Qs z67`!oQg-|hE(u6H@79m7IsG`xe=5-ixyf;mYlPvZTEeACk&1=h3dHi` zMi@yBcYZe$TXkN`WPCq9mJnnmFsS;t4VAqk5z%bpkrcbN_~&gJh`>F)y=*DGZ_XEc z=4*E$(3&na+`0Ui*wFEvP$bP7dE@hq0kT!RM-hfxk5U%EyB#P57kk|0S&I(;u<5o+ zxOQLy)y=0oH2e52Y5ovfqBLrj8gH6XPRLu^Mv^qxhf4Z@>Y{-Lxx>}e%T0L?(R&{1 zxwG#oKLb(Ulkuv9FaE^6yym^ECjVtqt!xi3*&5|X_9yq~0N~p=KG8%YcoW1N~(GoSFSLw~B z-OCSuG&P=>t_^f&$L-?%K6T;SEZ)Pakp^-zwwO`p)6%CF6D!VMTNrHoIc_XdnaE)5 zc=t$bObYoO4Mb-7k7>4x=LLTZC2K?zYmao(fav0C)pJs-SE+2%`F3-m3*|3k40o^k zm~pXCFNT~WxoXFeLaAOOlKPe{dQ|>*x2S?M;ud0UU3 zrAI~L?EG)mPCqP@DE`?yv{I&ximT!kI2L0CV|5Jq1=rga+}Z`NI}h(J8c{`pMNMpk zPYlz*8Iz9>R4Suaz^w1>Id~A zky>3^at{jR+fM8)kl!Dff}S)wJ7uWz#Y@g;b_u_ww-IJ3hW_8&w{1P#i#B`Oz2|(% z&yLAMV@ihvrUsQ%GTSD6^gmGiZm@_UzvS{|*_IqO(wI3)0~D&*t=Cg`Q?AD4M59Mz zx4tu3G||g(uO)tywn=V9UeUPYa#`t`SX!V_Sy7Gv$MLH{o#`hdafe(;Oq7BTUJq|m zijO=l=2XL7R*7r3)3?%)^3qe_PO`4y5iDPz4dc&D5(jzR$+QW#KY@y5XTR8E$$=Qkf1w=tE8-wi7W68V|> zW~ffiwalBoK}j|h5jz5Ub_q{$Eankdw+@!%+q7kMEREzvj*L1bal8z{W!N4)t__>9wD^YIARv_+GqSiVW}jzB#- ztEzX=E=pPY*%E)db7`K88u+KK&NuQ4^#%%yha@IX5c&wJ#ycM(=b`XM<(EiNAMik$=sAM4<-9X@X=@pd1#b$2ku}<-u<>K)7xH z9ba&xD8JAGQ7+k8HIO5#^i6w9dJH+>8zwTb*(?0R&xkMNS@&DVtFt~8`yPw5cDUYF zW~MJUZe^~~*(^qN1KE~f7F<$#hB5_J)^O7?oJN?O)_fbM#@dsIeDv)b`wghb6! zuwU!@Dk*9eqJPDv)$ZDpJH0Z9TagxKUN!ZSb(d4`3xt+Fz+x35agyweB+D$*S_XPh zJSc6yC&3N*0@rG6Cpq3r5;w7$jLENW9ghFn>qi40kY5_NyZ=zy4%5fcBPM4(kk4M} zPrQs-%e=m8(gJI&);)*!XSE~cXdbdeyTn`KiCsv4#&|+c%C}F}tJz!f>&2dMYIP>Q zvw8o4iAO5Qk9!2!x?=75;q5)HY~+ktWo>5pmWFxW&ZZ!2wFkjZI-*&(z2CBW^D%sA zi*F!G-e}U+Mn2{T)U|s}PdYN|mAKEks`#OvQ$cphB^rHoD^f|y)d6+OkLx~O(9tg* zjD3I}H9hp5930{ijyjV_@-VeH`AgH_t;{&$qqZVq+!#_chrgzTiNF8CH|M)kJ0w2W zGd@Y<(v+J(f0sEss@0TapA=dVKrr7lcx#g*_oQc3%iyF#qke0VdT{r_Pn}q-+!*h3 zIIcHK-%)P)?_n?_!$_b@pYUq?@3?!%faIrBsBIy*%_CXYIrTNsW-sAMB#LUce{%cC zI~%VVm&M5Plm(wr(}QdC<=NSuzPD(E|PrR~+rHF9#^7-7sfF~;%rUIWTnc8eVe-mQmE5nI?Q zuTZYPnzFVy$?=o$<8x{XDI|T_+T?U;ZShp`8>=s+Z*Mo1u&@%sOlw^fCvt*OPdB5D z*)@HF-^Ym~-$fkP3Pv~vM#*JIXZR3~Hy@O7AB^4-WkZ^7HVyiEAc_z(Ot8=y*HJ-o z!PgO-D9xuhue!?xpL*1bo0>x`wcFe2K2XO$Hf7xQQ5+=?gA^6&Q4WT+%NQ48^xxqOj7>(!I!2ecl-N}jWmrmzI(87p zp9k68WT-lb4(|KT|4lyzWam4V#cjOWSO z0vsu-g~fb3DEY-_2Ro}}b|X9Pzp#Pc@Rw>z&Ntt`^0o+;^bQP0n|(ZMw}^EqTU`ECj5AgS{k(t)iDJu+|h+3fW1s z9&Ojtvh;Osr<^>cVU&ssP`u056e#(TH~mGm?-D=titX;_Fu~YwA-O7HsYXDF4-Q;2 zG(<8ZIlqP+zLMPTnoEZ6@0VU;f@QjE882N3kwQIt%nITNo$;krHygNfe1f9knBn~f zYUY1fedlucn%r)YUF0rQ^D@q?IE~plB#uBJz!Lx=TGL+8+pZe1lcaK$vDtfIb!kdN zd#xTlCq9gSU7S-eaJyk&rt%5aB%3FZbzF*l3O&@4d+&>HHt!EA-o||JaF=JXYE$!p zkD-0q5v}29xg|;?^n(W~|4|kGP;m?F;H{yA7}jHKx5N=LSq3`qjUne`9^=Jr&#fsP zr}%vD5m)+=OnqX5mU~pTO|ID@WFTKEu3aRm>yV$)PF2MWthp}pA&g=V2AS;UB>&^5 zL2%>6bDmtX@<`uwm{02d58EE-1+_m{EJ@^-u!@%GH6JCr%=UHWv05lsu`d)*jxl0u z#co$+hp}_cqO)oz@q5-Xb(Xq0PvS$d7&*990#$j*qW4@Zaj!=Rx32q`$y{~5OS*dE zYGud4J^ty)#l|Q4`fpwf8-LQ0IwWAx*OFDvD1$uz5cj26+n7M`QC2BVq?F&E!jGxg zJ-T+EQq~7OowmQdKkDp>W+`6$xs&*^AiK-`(yZj@UDF|Jx-%o4d zPDN%fZ`b^gIhwb}%qH_nk(q+w`L zJ|TE@BJVERuI&faraP9oab$9h!6%J+#$chSb-8EA=Akw+PO{zO5dLElM|^|;9&fpg zS>%{kT)QEgrlcp4r?W_aKT8Xc4x7Iq^u{Su>iHf!Dp436k4GH)8L5U?9r#&7$$2>t zk8srPSgS@d^Wruo1!3WAo>68&?KJDI3?IGIUzQk$Wf6;`XL1_F%RMHcyfv1tp(UKC z$nI(tv2jQI3LiKkyy$bDdMcLU5wbQPp}F|6G8{)X#a{YkwO9X3b{k&F2*o3o4wbi<6OT#i1`N}LL)wa?MRlah=y*&47QGWRw z3{u36k+-hdQ1Tf}wY)p;joWj7dMQYg$Guz$LanaYy zp9!8_>HLH^gt5uL>E5nCgkv2=xB6Flrz8- zmhATvg`NnAmpcEM211r>u18G4Ly(Kia?3-@`Nornd4$NiW|^iPCf!Gv+d(g;;m;?I%sEk*pTj0dY>Pcx;^0B(BMYmg z7%yEJnw`4rE||V#lNB+trmGNb8c{#AI<6EiWf78DmNJkZP>1ypl&rzhpOd#*TAhou zuCOBc2R>cyyB8(*WP;cn9ZNF2?U}ebK=o)&<7-Txbg=#w-c?$BtF82HwP~?}3xSaJ zG5l85)iFPJf4lus`cHY9W1;I(|3=kL@OXpa{j}WUrq)Zo_A?>+&~(PITfhR>Y?ahZ zH4T(JHy~CC-VeBz>|`8K#K5hPW zy^8TWkh#$XQoQ$J+Sd`K#9e!d zhFfqh&_ELxdOjgh1Yc^{sC>><>l!IHTJ$kvQ0{Fmb$e7*62l$$nOKFVrnP8_GuBom zk=@lVn!2LhuVds|ca}bD-y?|H?%OTDwY)u~&zHR{qj#CSB^Ej@b(tpt6=lJnLU|Wd zhHw@`lpk&LdN4(LQb8+1p*~j+X@sNTW4>VCPC- zEYU+Y@3ciY5y2F6h<;2|gOApYzxbVa>u9~Qn|$Qv%n_Sr`zo|~e>8gJx}~m1Q^gFN zw_?>D%@_mYglxru^5A~sF>!yxxzgi5rcB*Cc%MU;r9RKJ>-=IxWJVwvDM6e#NKzNt zT~*ewJ!c#~>AQbaZbV?yGwlym68BY`-3(d%PnT*N7mwxO4<_Li>Fe8=6BYDI@>#Qe z%D3LkMurDId-lAJwCSS(_TlDROZZs6LWV8m*&{zb`;pB`1PJ=Rp{1VV7S-~9g1d^Upj*-; zW1eV}k3qSkeq`BsQnE(8NITqbeWRTogR5Of#VEZrV4A)<@ncg}{o&bZ@501WWK6Oy z{RxOZJP6Xyl&cc?8DU5j3Q7OC3$iq4FPiw~oV@t3qxZ`Gb6e2+>dT)s?0cT)J-}BK z^%Kn~4vm1E_)y&8A-qy*)Qa;R`>xJwrvBX%&?RZjJDjK_x4(gQ-u}_-`!nnJ=I#PMdU(NykX+O&73M#EY$R{F-L*BKFu#uWC=mLJXtj*EBc4-Zh?-l7`cB#OaiiL|=N@A$H?sBEB8OFOwzQ3~X z$+YBsb5iu};o67wz%_PDRPzjj#Xxo?`uSn$S&Yg&L+;1$oB8k`i_YY988wxnw@sZ7 z!L$GL=0`Qr4~hq(bQvAA_-8HdM{R-(%OMRm6BZ}urKi-v$kH>YVe&X*Y%X_3ZM_WzbxA<~p zPlvi`giMm$!b0m}@`;+!J7{Ix4=ZJj9_6xc@Hx%;ir3^+A$pQF1uNJqQ{NNj8d(Qh z7H7ml+p0)1Z1>(UJdGH16rm#cvEFkU|tgqGx_a5RNf5%dU>q8zxXct8AtZG8{46 z2X!MV{S=GW>n~yTiwdYz_Rk?7(>L(GOrX0cy{1x6K@|BFlgGHjr-xbV2fOu0WDniA z(uu~o^J7)5yuCwy@bSfqKEka6aU{_@d>eY)JW-l6S^LGnLyuXIka2cM#}|vd}5YFE+0;E1wZKW?kQ~IWF4gZG(_53sFd&e@Uz*jt{es zQ&Vej8{dd352(Ytm37zjvI;=2YOF&1Qc@1&EE#h%Z*Kb(ZwHqjYiIq)a6)N1&dA_^ zPygEPcLv)pTjU4?Iqo`I8ywQ#BhewISr$!6ezK#qwDjty8EkG2-s3c4%JyFJ;crir z?BK!0xmcIg4tljiYrD#%D}Gjqrhm_J$4%nRTSi9A}%fF~5W;Mnap{ z|Fr3B+!QBzu7eZY-6aq#5MYoQT!T9#0}L9H5Zv9}gC=MK3{=i|Bds3TmaO6lt0x13jf0%<0)ST6!`}x zd%`s+r2lP`;0i$dhdluB0RTYJ(sA_)aP)HU1_1trm_(HgpDX|X_5ZT_$5lv3L`V!E zA|xy(EhH)}B*OFrge9d#K>z?wIskzAWQ!&wA|jNI`mcV0(&=db#yT+IKW%_0r~vrO zf8eGk#&#wDsw*rcR08}jo=>AJLHQR?l&BKae_`Nu3HrY<3ei)a|64}?Tb3xq|LjTc zsn;mtf3kW0gs1;*oc3roy6M{xkn? zjQV8!cO0M@fTW(L>A(E@7ojp^?oVuCpOF^LF>000<3)c^A1 zQzZO%+@R$DcifG8!02E>% zDlzcS8XyjUgNccSiGhQKg@ucQgG)d~M1YS^Ku=CVOvTE`&c?#X!py-dDagSs#?8zk zq#!H?l988}XBSWgE6J)$%E`+Bad2@7@CoRMi0EXvSh!^VkJF!iVA<1v(SiR+|K$FZ zM?pnH$H2tG#=(6GuYcZqI{)+bf82a})_|y}C}^nY=x9$*2r%OBNJS$?Ct(&=z#!GP z#bog%6N!eEVzDYBddUrDzO#wi`Nd%4P*74))39@Ja&hyBiAzXIfuxm`RaDi~!5W4} z#t;)zGjn?fM<-_&S2zEFz@Xre(6HFJ*YOE&5|iK=nOWI6xq10z$6Mt#?*XVo)`J-qI%BD1QGNr2_hN%w|WtdHeBNTUC z_MW-Hpf3kHQ;m*5*xqt5YfdEVDu`l-Y z98$EaYlvZymFG3T&sXj;W$A_a5Q*&Z+@<3GC7a&GtNES<{bU)abN$vX%+G(?DL;KB z+E-?*`v861D(C%CdmB$Z&wIM4t09Z)$MVo*;f|iz*)P8R&`Lw zXNFrtnY@DeYlCG*Rkx-0YSjmO!DdDvXX629y_GI$LU1G@l1CXnC*6&1E7{!cYo9r9H zx34F`a*y`(B9TaNJ=P!|ju18V$jGV2i@+`v@T&Sy$@U+Bp}ud7Oba)?1&`=l6_+bp z7FnE}i0^IH>if~=Hk`u3Fyh5*ne4b6XqHU~bC%A6Ja;xBU{gP3^QGSv(sVkV*bS9jYClue^acscy z<5$cuOPewbU>_iKJ8Uv3i%w-1A+wP!#~U%b_ftkZ#{kYLA{5 zp1QH?oYg>v9~~>bhofsgQMLJ6kY(IM%Z1$7q}g*-CQ|O?BD-y7t0q)7_b^F4U)7G1 zGm8_t$Amif&cmoQEPVs!_NX;8`Z-<8L1=jIUD#B{kJkY7WML|ljOj3VxypXq@EuVf zU+C!^MkUy44?VAl%!Xn=U{Lg2$*GB3ZF~Xwee<0^?YPG(ZJ^$u>m>=g+|}~%gH!#+ zn%Rn32jy;f#%P7mp~$mMy9Vh$0RQ$dB)!0}rUf-Z>m6y%=y@Q3^n~yyjsPdNs0VB& zgo#k6{6?J6CQO9**(%UZXJ>t``J+}WW~#z*esS!2jLI?))cc#rPesA!aR$@5>SVD6T3c45R(rfDn^xF#|wj^q1Rsgjq(fp=Y>2oi74v(LhN8%DBUvz zs~m>AJif>Us65r(=SnM?4~JgoK<=kh!@7fkD-BTyDX#0s#OzT;vzg(aA)3Hc{xLASY_S+hhK+X=ZME8G;l(Z(0Ag(>F2k>{D^VffklW+k;{WaK(N15u6qX8tW6 zwi69=n6z-2q7P{pGILXff1Jy@*A*L~#-bhpO^t2V4Ju(nPCkSORSkDHt`n%s3Fq4( zAap~V*igERW9^Ry0_*$X8M?m9vo_6LEJuR#1oFsgQZZ73ljnHyNZ~Nc)%@w{+1t>U zG7Sn57-krfb$f!;tbV`W=3?{&-ZpYv%y7o)BVh_AR7FM)do{gx^4m+wfuM?p;dg@7 z^BxXX{H0k1`z{5wr=7u=w7R9gLYs8K@$Q})7Tgz=u{!E;CK6X@KBn1%OKi-;FLI!< z8Pj?{ND#H}%#ORNiqMfmV?rxTn16Y=(6Z;f`OGImRgAH#Fg$Yom;&MFc{r3~igOgC zvxZQE0=%l(qFZk`52-Qn+#DTi=Du3$(iJ`HYjm!XHnnbuYd#n3aQJJW*N81}SiY~6 zN-~mn!Liw$>fNCQ*tBIZiPb=T-v>BIsM~!Gp6mltC(>q{vEh;Vk)e*;1^^Q!d`Xx z7MFhj5h4rj8j~QNisR}dFEW8spMFLn zYwvd*5-R+!^&O%o>dP?vx7{sBDGnEJhdQv#OEjrxxB2XI@v z(Fpt^vhKe=xLizn^>F@J{b1x8Fg?t`aWx|Ebc^m6c618XfgmPEO^bhDT_0s%nS}sC zn?(aax3@K`27QONu~GhOr{K0P1DXY8?95mokVn{G;eWZga~O_{34v}a#-N5Oc^sA) zlRGP6PPG$3^(?jbYD;1;{p9>0=?Ts!=PuKPu~(%yqL%%d8y&v_UFcv9&1T8-xobKx zX%6@2u~!L^oPmFgrhSHtMx-x@ZCn_2{LI_s&Ha!wfARiRlGLo|eGNu)(@Xujji!-{ zcV48k?%Cc)`CjE>dPytP=fS)V6<8IwocXH7XYWRd7;W|T*XZL3+l2qxFZcEVkmF`s z`w$RfmXgh#IW74O&kMrW91N+9Thcq8H5zn;IzD$S+Gl;d(0L|p!XaouNHHDvqUd@n zU$xbt)%iXto61gz&^oh#&cZ`eroG*f09!AFZJ$uz+)vu7tU&+=n%>N?b<{pvWs$*k z%gpzEKPo}i(}n;OY0e_E?0{8ITIhfp1sbWG7HBX@>gUCHRamTR3HBo~t*&RJ}e_OK9WIPISx(4%a6}zeLzPL!j-y_RP zRCZ6zJTEI6#V?CqT8tJ}-(Gxqzw8TvR&o0fL1T+~I6;Y>+W~inuqnbfXi-Gv8OJ94 z<_ey|P!b{0L4`j6P{jnZ`!aK}WcyVRYk%Yw#WT(>=pR7g{Ppus4$8dHT+Q*OGs>Enr%pnnl=xqm2bdmJ#^|K$aD^4l&);0HVacL7l?uJz9r8gsi zQ~Qxsb=(prvSrW|lise$G`|`ChNT&v3rUH#D>2ifpKaeG+__c5MBY~7n=a_=*EOka z{G}q}egp(R;7yx|keB<8Kv3xi#0WJo12CDKYQ{rYYN;nO#2i^oa1`5_ibc3bhy%R< zv{XmGQFL!gtJ;NA_jz%}+(bU4?&Dn>t}tQyG7k~&0j_^6Rt*+uCi}2{A=6tvjS_zQ z2Vl&r^YWv*hc9fiv>c;hv@+7hemi42pWougBz5co7AvOM4Z^i5>4$#nzLZrpCbe9* zafqiAfhp9!6py=}38K=m%}LvqZJ8Q6)%ojGait-+G=GB5N%lMlu!HlqOU*zjLWNRH z+j<~b6C~(`v)aqk%G>jHat{mOKYW^Q!FXC~@%~n`MaC(pqwPkY;S&<|0qAT(qi&W+7u<8+LC>NB3x*2*LYMAGvns4WCSjrOV(&;B>Mxr!;eRfPTSuKl$9^hrG7FNkA*^##9&@aN^Rk*wtQ z+BecE_Uj4XYSa5(2e7T>N6aUoVehYUSn&`VK5aK>F~8^`1htRXizVXO_`Q!+rO!~c zEOG3R2PCrg`;Upup^U{)zOu>ixP|1&k5gn6qm#Hpp4@`#<8p5j+&RnTyvLcM#bZ@3R5&+;E&XK#*d7*+n~%05lf+Mlc>$rqI7#=?(pxqI(cg9N#qz* zkI`v%FtL_9Q_Egq$X7QS*{>G|BP1%f2}##hKLjmNMOvlI@(%SSRDGZ#0lx~uW zSSPw}0w1x}Tk<>tkvxQ>849r}kDWH5ziKO9b9XPFwl*?+xr}S{r#)wVsjrT0360uJ zSi5a1LyPA!=AR)Ep3$>0&{UEDs|XWKxZ<5Aycm5itFZm`wg988t^1Kx5)vRnP~Nt) zCinQ;hWbIb&w@A9F~aURP;b1ECRa;A2z+!X(=v4M%?c_pZK22Koc@BB2p!I&hnNZq0(Z2PTy3qmLf=C{q>Z&9l{OQJ>2N(d^?qF zGH5VbH6oUAX^)%Xy z)-!IXgsKEw`1sgTwX@RwLGq$}j$^(t+MszYW8?}C#3j;q;1yXEfo+vVS<4uWJhj4A zEAo0bOO_q6{bp6+$Mchtz3Q-2rk}|>iFC@Vx!1z0*cg#{mT$jgEyT}xT=#Gq=MV0( z(iJ=S77FG{O4mLUGW^B%82rt;w)|&?&Vq@9NL^QSrhn|*Yzq`Vld3+`-qY3{YJjaA zPqDg>8Wq_?xMR3jWvc=p(*yqJDfIWJ?<0(XLkZ3l2G0J^`*^mL*1m>3m|)lbwC|T_ zA(nU@m@d8cd=~X*)sEpvM3UB6DkLR}*fDW`kGvFpW85E`k=^&o^QFzh<%H8rWP;&N zO62b1Z)SggmcH1lJgOS|={EN{zFXSx&-HXQvt(OHosiqQ*soeJ2YXJ;Q%!TC|3Qe1 z81@|SXXVc>pjcZ^^}khj)c60PLcjmF8VW#p|4-$W=}9eo5>^2QOelY=uP4s`R1crd z|Hz**006Ux|0qL4{C90&Gupp&RRFLV9f0#>@we{yrWxbk`hEJ}wf_9i^tZwa+(wD| z9||kRlaPsyhJo=UXJTSt;Sk~AU}NKu5fI`LQIb(nQIb(m(9p9p($KQfQBW}PF|l%R za`SLgGx7`Za|y9?adQDNF)?wla7b}*NV#YzXt@55)Bmrq{(ntjeW*$TaFw7W0qScs zrY-VF;Yfy3zQn55nxGCn*Jwi{xNk+gDvOOM%v5xPeXzJcDu;JHxRD}8{Dya2_{PLMr>b;6^xzNRjs4s&GmHYk@y2v7>L@CDf0LJ?4Q84<-Q6kj zrt6#+ecY4UFL=WcG8HdRn@%MC2991l%QgB{zWSb2uSfvxQe&n#b{RtQ&N+ZCKD(e% zW9hlJ&!Cu0F7&Xg$bLxDJUhQFhHBi6aD3=c+^x&)45EgI7?dQP46XM$4PIP?n=AMn zqe96UM-hhL&B4Mt^{Kr4oj!H{)SDYUqSVfsjE|LT3YA~DamvN*P&gIe71gH}ok&|^ zHnVUr<8*)?7B6!4@U3ScAa+4Y95Os|hQZcKW2MTG0>3Z@#11b={2bJDm%;ZkW|`wd z_h5Kq;7Ga4C2!zvQp$~r`D3oc%s5zqn^}e?>>GP*jE=k12}ffYyDC=qWo&8|sYdz) zT?K{B3hQE>ph3o0SGgM#QAOtVwc^hG{C4`I<%_IU3V{z%Dn=C(#_Am}$;yad|N6KnLTS+ssVkOPx1YZaFRM<~6@X z7n|b_FrB!~-Hx6$Y*8O>c$v%lj1=?4HCS&bLWvh^M=u800$Y;npJ7d8n>br~(YLIA zs5b%2_H$v`4ytgRorx0|u17KwH-_L&d{lw&it~Qn2Cnv?@Jnx=_N7~zrU~rX;EW1^ z8_(ybiuj?4&L3JKZ5`O`)4geO1Af0*5!YD^PA+dTC-$I5BmC2bGr7`uZMB}k-m-7I z?XRABmcwj)Xm%j4SUVeUU+K(Ca5NAVFw^&Rk33xlKaKwk+1_ zLvyEggV6`aPjQQm=L@^ut@)}hnSaejzWCCdqRp2=W0ChF=#X!_#v}ji7kiz8XQ1Eb zGuR|i(qe|bXmK}mUc4%1Uz;wv$@sePrV+NgQvFRwbOUd)u2Ln_)<00o+)7~JP z^ZSy#z?C|cg%+BhNJ)9IoQG&f$%DRiQ!j9dd_7~Os-O?%7*W+7Rsl%3YUGX<#>>^& z50~IP{^;ZLBg|94TsP1v+nvH5X2~UEH+U{HGG%<%!0OR5eh07C~=Vh%Bnq8g=b=K@D?fI)GRAPX0@$-;4wPn$9l1JlG;{h@2cU7$X$#^7<|4 zwj=OTpU3r5!9!8;6B`2YI9O%qO8T)jud8U`L)$hliACLBrQ@5uz}3iaNFU?%6$>Zg z!R)h5)9vqWppCvXzG7W~Q6rIf&Ciu$zH|jTx&;tBjLjNgeKTL&i;$MzNhgqV-PKEJ zg=18-zswO@9dCFJOs6BYnDq%4ahSCr2}zYX z*ZJZMS0fgnwSOB{FeDsmO*d1;LzUO=i|@Hu`qHZ z?yd4^Az&>Uwcn`~5Y-Smu{}sfZh4&_=+sU|dy@LNGug2!pB!HO{qm!koI=xH??-`o z`SCWe$DDM;0P6kX_j*ct{lcb=x#@)j%^ba$hd%%atRz!nXHvrM!&?7uuJD@G0W*s+ zJG9z$h+h~5e4|H4bJANv96d_bxss-_Bk7Fj9A1(d2GM~iHS3(g(EIZ}HeE!0J0f@q zK>oi%JOavVH3pJHt+sqnapxZwRz&XHnxinl6CB-yO41g3ZSSPKihA6N>RXOFCUklf z|H4I)$oj%U;q8`m1!7*Ex<>shW?7LzjW;NuaYSF&I?~zRlKBFulFf(-Vy$rZvImO8Q*s`CFBlsxGrj zRt__DOoj!Flm@p?E0ByKuhe0Al zIMFlW#WUdU8w^L{hZJ^dta5JWjiZANK|5){9&T0Ln%0kHmcuR0S(ZVZo#O2M*XDh6 zI)pjDr0Nda2F_Z!#(W%X!1rnZXQ>)z+fPi_=W4B<=?h4*C{KsF+Ty^S^w*Xg{*puvnKEgr-*nk-zLB4IfiALN1&; zwmO4Hb+Nx~HiblZygwtOy@AHbGG}wUckxH2HvgeugMWq zPn2ahd7GK#I7~l9o}tm}HTb%5+>c^v?A$<9ty5h#X+-r*;B_uG$OI5(R~sq%bl7___?H{01tD|GbC6c%d&mh z=QoXVJTCkztH)7Rxq6|Ml|!C*W64=gHifFXzKb=Ma6>t6F>Dv3WIy8Gbvbthr}a9N zS|gbV+svS6=C^+UxU0wCSE#VIU^(``_}s(9mh=j5It>RzDjUmd3$TE+ znANpSD7{FlNHkOD6v*;rc}tdX|Z588<2kM!_OiF}znYE-;^l+dQ5FJWnSVn5mo4rPoy(83ny3v|AI z@iCamU1AWQzc~7ky6a>C_6rgW+uvE81A&GbrIBO}F#YOvqlXcnx=uUd-c+^ID$KaRdM$ATr=*u zpP&DPM~HKOj=4@_t{s}zim_Kz{%YauSFOba&)x8qKY*6?&|;6t?mXIynkG&CB?ewB z+Z1zdy_enLa|I?eVP<(8CdI2@4v%kVx?!W)`^mv?NT(FcB)SN#<26z06!Y^XY1ODF zH0g5mF|D!b4&S~CSi&p0Y(cW|k~w%pilxCn=-t@dauj_r<#SWLUM+c@N}w>{O44qg zJ|$fd1%yY`1!~z8cQovexUD6HIRw<~dkCU(lpP2heN`b}wKMAOGG)yi?J%}Hl5;Qa zTJmI{W8f-UNnm@xnHBn2=1FH6HagI#!m88gk>JC2Q2%L8+<6}2TER>b+&Ohn@<({X;S>!@|@<0+em~*z{1CHbaTR=VTt4`K!$`Roq(7 z4V1-NHmwX+N}+KfAmpsN$MjLmoiJZ_GK67);&2{WHa|1n8iK?&+t0H%md)6FXKCJu zTwATZX3_7o3>I4~$lT5JGE2s0oCHp3G=`N7Km({JINcLjDiNfm20`zu-t%r@iY?ia z7sA1luG3lrMm*41=%Obcf-j2u8Zb;>S1%)s>5o2j0Olq1TkIz$%x#g>cozfJR&1 z?#v2QCIS-k42*5X_=*!BPde9y)@ig?Aqwt|+Rtd}EKcGE`|F9qj_6EeY5|MiM~G16 z)GG~r=aNLd99FJUE!vS>GG@AsPOi-q({J|O6EVU{7%~@MY?7#xEyF$W=MM73vBoX$ z)>4u4?v(9BYbC+=Y_t~hj2do`H@uCLcHp+6o{_!PB2NxBY8!>^*>#%f7Ha;=QL=!@ zjoMW>wudL;N75^EEng2=5Sp(W_gy?I?@y<)g|qV1pZw)ssxoin6zbdFkQv@my(eZM z#GIaEPpLv95=nkdF1u*IoJxO4uA@3P!8SRpsxahla8ToxVW-)u+N?GrS4Y1#G;tL@ zh%eVG1LL#NP-4tM+?Eb=Vt%jH7!F<>2sYW6Fl|#MpjK1Luef3U1rZl(<@n}9yMNm9 zxG;Mv@oJSq@o`7%K?LS%yl5;t4s$vQ6Pwyae*9_8Mjqs|-Q z$m50~q;AA!Lu9WF^!a86^|2n8_vaViQ?oc9Ph$4C#QNiWy$_2{_PsZ?fj}KQcQf0t z-EHX$!aR^kL?L0kor;~W!$KZCbbVS1g@v{0(+K7f^)Li0N1k~%3r*-W|UMl$C zS*$>b&R*`L4)TWgb=?luyTC>`6bq?_00x;zvLKn@Is1+lQDt8)L)fV$`>%4{PA898 znwR&xbx*(TI%$2g3A20JdNB^(!s@3)*)^9E#0nV1iJgB~9CZt7r})X$`}use zwq|v|yGP&PcXiF1p@>ko{&1SnQ=g+??$*hatHQ8_;ju9yC#&9icBEN08ok+FjB8S zMwIho|Ml=T;UYY#XSuL5Sh_on+75QaV8w1 z%Ja>lfPr|1V;482cZc*U!#Y6Yxxg7v#37soQL zF%G7}28vzDH<>f2zRGbGd@MzC7A-ncrF}52I2t2Pne>Gno2~AKo$1&T$58D%bF>^d zlTd72WyHvFpPAd2WGeA!^&ij?r*<@I8|ptAz!4b~+rg5Sb6w`x?S9ETqovVDWPQXC zG?>Pn(jouaa16B_Xnj3j^M|dwdkb3lU#_dW-UhrD2^PzF2~Tg5x3X1@9GkgRbypZ& zu)JI?)=Z+6uP)uh16-a%0%3OLS?g^}2C<{RJ(B9L9`XU~rov?T?>QrIE@f725@ z1E{IQOm&qF{jc|nf8i~}sTCF2eV4x0Eqz*{V=D`8=(nle`gqwN1bS4`ZH0*b$4a-0!DM_ z<-D_8ZBPG!r>2N4KeQ2+6hMhjrktv9ZsG}aYjRM<%TF5ph2R7=GL53!eG4ZD(^TYU zL`x4HP-R=+aLe`Xd|*@wq)uO#Mq^QYekcAVBuXyjVT=?3#=t%`p+}oJw3dpYJuN%< z(ETRI$M&=DZ}Ea{&G$K1U$)c4ju@g0;3hpoZE*Y4n7j4vf(E9dz6$_ zhfYIz2d~+~j8%WNle&tQ{ET2nF0{IR7lS^3J~eF;Sog~L7{aom7oQhJixGPI)@_0V zG{WvrX}ENQuSk7!T#X%IXsgtF{6g&KlFIMVc`Nodd+IUXntR4v-UBPr)0%vA(t?#unsFBtxP1b4icqRJ4teKZrD6A2;op^94 z3m7$GBPIUUFCEOs;Mw(tvP6HRK6aYrmPb2`r3c9LhRG70wK8z-58w+wV_cc$gi2hU zh_Zth=O(gJcU6X2!_!hb@<*?0r6RxIQVIjH@6vq!9^bbz$_5zbBghi4A*#_p7vRW~ zSZ}(N43_kma66%;Uz;7|{I(X8dp97q^5K)f%eK?v zTMB!Kh`?U$C~S6X+U4^Cy*9PD`nlnbxLF8HZ>pJjJGPU=cEz57D( zY^^D~jh65OG93}yyt1i`GJd!Q;Ys-!W&pijXan=Q1_?edF(w1 zhnz^v#5zmvr4Y<%Chj8((YydVHdFo#df?k05y7Giz%9*t77;X z`WbX)m*Zggr)IGtmP->3wAI5=&iybigDO8ME(+ID`-B}uYiq0`Ml73VvJy?(c~={^ zEGIKqTkT!-uSt!^(8c_vx5z17e4-aB~w7!c~ByXHziY6=nSJh+D6IL}*`2jbV~s}D0ubwnG#B(f@-Hf#rY>pwgIc=P0={*} z4CUts+GDNuOSMe~eY5K7{3Llb!raXnKXp@|BhOxZvQYLRuX++|qxgNKm^*`4idi<`ktFrH^m~6!tctV%Bak)BYnc>CIROROJ^py;y1#= zR&_g=uMEw9aXD^W-FWFgdjf*H<$B$)6c1l8Ew20(`=h%6LiT`_6&ZaN%fRl_wiU~t{k}w zBmId~1dOoN^6U#&tN5TM5WuK4v1gD=pXeSj#ghNSqrb|;TWb!YjI2c)Sd`^Ny`S&0 z+N`V|#jp5A0sZr+mv_C5t2t;}TcC z`Nx5jzN#W^HN%VIR+>f`9*c2bLK!7lFY93R^|6HTpW8>E1~)L`s*;Xhl* zpg?7F6mo9HFV2o^5xn>Ko>H42wJthfQphD0h(@4(Q`#dYrm4tete-lt0^K$~$58eZ zd7$v>;F04>EmPs9V?9U72-;DNZb9Iwo1ZJ9X3QTpYEp+wrX6w0AWVF8HHlp>9R2ZX zuV6zp^{a@<3|AI`Lj(PqAe&Y$SAh7#%iPHju@?20_mZjAi|}B4kLS#M)a7iF)~-xu zZ)~C0BNO~YUQ$}veZTsu@WFg#?3O8=UximQl6d&p^!be73ccsb&3SG6Ho%{A2blVZ zX8h5P$(aH6PoxAD!F3ie_WAsS_nm_mZYTrsYA-ajB-zr~@YfA<@%#%7*IUI(Mt3^c za2%I4L)%0dn|QqK=rT2F%t$7LREJlF>rAFvndtC#o0Mkiv(Vy_Q0erbBf78QpTX@UFtFR}Z za}l~-e_xV257kGruc zMYsAzoW9NKU9aD835+C~5xh{DXo9f}PG2cSOv|07T&CtlMAVM9k0YjN3OgPf&^>8V z2I|SSGeDtdUIE=$;7H3i7`{m?>(&*Oo)hGk z2L1N&v;pmwDFb1Pj8KlGUHbqh{vMndd~b?^4{AXhsV!e9-f#Y-0nz&m|F-@PI@)Yf z!GlnAh|upbNrcwI+e&4u`zdJ}5Nkt}#g@fB+DbGcqP}Y zz`BN#ujpO#dRh63G-Hh%59bGd?&Z>DRaRU94B^F+;o-k6`|<%2CmBPzj;I~d{!+_U zl+?43TCUjLVN+=bmV{dX90>Zvb9OaDS-RR?zwC4Z=yvNg>u?40z!!vtMwhV9cXhKu z7k(7YejVr2Wrn`g50?gQ_ImyM41@8*V6VY1jSKD2>&4{iDZozmta%otA9|1j@5nD+ zwZ4Y#cO(iUx;f%ON-N}rgrWi~OxFVv2a4O}KA|<{!ZJOk7LqwakrjdUi&D8`MJxI+ z^RU-VN#P#~%DHJf_Ho+rVC$wjonP0YrZiYCXo;42*@u@q?5F2mw>mU;|2hjxnl$ir zN=G(WB9WU7Y*JqVJRDFbc`e{)cXSh(mQFJaZ^pYFP37hKf1FWD^?|$@OtR`^I}C(P8^QBIBFU zt;?|b_fR^dWzkH7)eRSED{8UKydX21&2A+#u~=~3TPdC5*PD{&k4tG0KHAhgbRI=evr4`9xFM@Zva z9M@ye+pr}xBCM~WP4Nm@5ie<++9?dzOZJ-yo=IQ7dlTID<7tJq<4`C=?pde1t152z z96}^QLe``|Ou36Z(H-ify2b#SfXsLu%m|EGDr;&1ftY9}qOL9zl-&)winHELVl(&y z4fXq5J8Y;>(r;D?@h|yag85I~E4n>7;7-T%ig75Vv46G%jumY5bY9$2v`|8Jz;m zCPIKsJ?PF=#od+$JWd?`-$%i>hk#ZfayxVi-vCw%x33s`sON zpnlgZ`q&b+fZuO0KrI~a;iq_8DkskA&qFKhH^Iu+4c9|GvhaUF40Ey8`wj zw6yQBWGk-~_UoJpq3q|b`WbQN1a{D84uD}izpJ)#GqCQ~WaB^2`dlr&1W|dE*pgu> zb}XKXoataFIw8M`0wyw=59O6^J|ymE+I{|JI9HtcdciF2ZGxQJt;5BIUYC1-=SdL! zX@^+_mHyHvs@hMAbF<@9>2X2#ek!VU7ubNpz^b_Pveli!eIclydi?|BEnkixB7Pbq zC#*YMU}0~)JG#1p*W%y4)#$ixDLX?Jg*AP{*=RUt=oWClSyh~{3*x&uL=?+PyjntC zY?$)n#O{Vcn{+j**~NI??`J0~X7x4X2$wUsZfd#m0*8+$>=AT#)6iz_u_(ZCQpz4B zrcVay`ysNk#Ga9IMMRWEpprkXCmRzj#4j!JT4pTM>;HDm)f$_$|wPX37wH z_^#B|w3n?5YDmFC5OS>lKK*lCyPTV2nj19mM=5wcj~~n`_1M4LwDR%Bg9OxQhvZpO z3I7aS@0lbdCzYau!IJXGJ#hq9o<4}!96V2BT%g?<{$~?7Mwb#f!GUF)@lg_B`$C3} z0hh1xV#Xa5VI%5Tw5)a^uYUIgyVfRW8pt^3vR4O#=WeI8i zD;mMTF_{*d*Y;psL|sETMKQ-h;{X4-OR?QM4rY`&4iJiW>6T-dpDD& zI+gy{zytr+125peree@rIqB$<*w$7sMcoEm@y3Cfyus+96Z+ZnqqqaiYEu*4Roqs$ z9Cz#4VsLTVwREXfcS!G-5}WOGpP^YJ4wRRwiG0KZRXhh&c%Ww*NKMTP zgRF&_qq``NQ7@M%Oq4tsAwk^1Iz_1Nb7D%Wr`nR*f7d*Q*0`R;6$Ky(Fii!;uu zcl)Jxh^T^mbAfpNEk=SZC+bBklF8gh;_?jM!BTI;#PoQ_>8*oq)b}ar)p2WWe?|? z>ggZ^UJ%|lR!fP`x}*4z4pZ_>bu~Ja*!;<*FhU2DqV!(~zmFBhKCge+1k{_ef@FFy zYCO^xoTP%(4&k`3uquknI!%>NaOoThZyULVJW+<9qlYgdJj)6TgnEtl?&tkSf%?Tf z9+dM5&FSF*Tc+T?sZL zmKl7rVmnt`(i6hP^y9~r3f!B+I~lV@)5siF!$h`6O_;Rjb?QTnIfsz+1i~H zTHaxDO>OzTT8_7iU)(+?%Z#)3RG#9pW&vk_e0V*H5^zL5{!42%Z$CWuAQao6|Lu;P zt1G+hbUnd6BYF6R&Lz24Io7PA=fYHC_9~f^5 z@BL-XyYvw=BwtRTx3w04buF4mx2-C7N;NvG-kNV-^rHXMo1lEJJn^dh!O#OsYF1e; zIrsSQ7u7wUOX{s%>7&oW#zQ#TY6WyE2Cn6Z{s0mgs3nU@+>xnN-mDRmdcVfyct02K zoNtuBEDvKqPVR0tw0DwfTmf`J+dzFyvfc$0M9`7Tev)Pc%J@uc>TbwJrK++*%9;+h zmODAj)$YEaQZll%YVSPIxY>q-?<>Ye?ypG*J3KW? z?cXyX0X-ZWX+^=(Ds(S9@PMpqiW|K6 z5p{bq*jH8Fzj)!JYlC*-1u#!k#nC|sDkl`@_SsLhxdBa|)u0jw9PFzKy7!!gkw&{! z$YkDvJkvcvS$>R++RSWQFI{Znqr;)FgFvusm4AnnD)XB&R}I*csnQ`-3hzz<0?D9y^QZT=6xxCXAsWP(f+vUT~z5tD)Tw2Rh-?C!Rx*_@h+ik<5AS)!wd+Q0Z-s7fYlOf zqO!NmAkSX)&t9t9CtV6|+LbM45~+@UtJB)8>7_=aZtA0IxXG+}PwMQcnz~XSe?Vo&zV|xplZ=xqeCGnoU`gCQ7ryU}a_- zkLguryl0QfDsVg0Nh|6#pF+7X)(z*T#m}(Oa)G(Ijgfn9j&7(fxWmS(J3dPDi&rv!>|}ZgX>TWhgk6; zCbW%_p5XTaIMf{Eo~EsNqDTgEaw|mA&!V8om*`)HJUUz}kCm6EF^> z{LlW_p z6_46xyvoU%J{`8Twvt!9DdwIPPimUqQZ^U2Rxw5zX-^H<&DGHJoiK- zD#Lqq`E$*7niq~dEumN~zLH*Rg#e5%e)X+7e9xL>b2OS;j??@_sA|{O21_WVhJW>I zzl?gHdh?xs;qIk#sa{PLq^q|s$BurL6O)#RaZ*b1E9gEFv(_vi`#s|=+;V>LanlsK z$A<=w<2y*MXWS)($c&v1J!QtG=${49;Tc2lfF_4y2;CD)~Z*Ys^KHR_$T3C{cFf z2iCEhyEKlv7y$BAGZq;*0|WD|4Hg+zb&-&SQPhv?TRA5i9g(zb*!@oA8kjsPev2Le~*6PJ=Z}UobIqE$tH+Dtb5jBKss$7U|ZEj^_ zkRB-6fIftZ>hzz69x&9|e2YZ4w>zc;i;_b8DqtA$=`F*PQ@Fme7SYUy%ye4 zXozC04?NJ3jv_qKxxeC}BgbuX82MT;lUaIP&c;|<7;*CbYOLEhlCrtb+WCLjtf~i^ zk&wsUxiXAFB!Wq#_prAbs!72VED{+RM*#E9OYDS-h9{0fthn~8R_N@RL~p=Rupv7z7LfRl2!Xwt~y3pC@QgB~#b2 zs#i_+hDMYE%UGqUPI_3s{h^gcgl!;TgH&!<$a9m|rD+so<)w;@-AysjIO*1%x{0y3 zHtJPK+)g#NO(SzLestNOLX*A`+Je(@oist=iumus1@4Ux03{qL@G z#X}ZW{iG=%L*K3`sI;{dt#Zb@cX4$DcG1r1YDg?SezkML8g`hMa!D&~bX=}@2Bo<< zTcG1;vRQR`biGbiwSCZr-V_{kuG>%e-KP;87usa4Gbuvxjrr}^S41ZWuXu>koSl}U z{2kLY`@ax~J9zf%_^{!*awp+d-zLc^_JoYhcT&Wx$LC?~r7Ex_= zF1;x!DBR+;R=m@%C$wEcYhvSb#|*o2eR1no{6D2fs@fSA_7frCiC^ZTbtO3R7Ya(l zH718nm8>9+N4!IUA{>EHUt3-zPxhB-rjjzCIly0fo@Y-(B^bAMU(>V;Ht_IEZDuIt zRNpGP;|ISr&Ra^*+e!&6GD4N;ed-!*UPVet>`!Srg+&T{!Ol-gvhkUNc`^X!v8dMM zq?sEdf~s%`>r#+aIua^d4pN2Mg+C}Np0ryi1Qg^STByq5ZshYv9EZ#)nG@5kK_$ey zzEA@C5z><`O-bBSG%ku-AoB`#oF!Ri3cv!OMc7>$Uy-8)#}vLi>-Z;l3kugXI-S9x_b({ zZ3%YIK)lf9bDb_$PFe^KWg&&B>ozd??c4(tI7K z*$X*fnmC7;WsIq2?s9t9X(q{Tl6cH=458{hX^(3ITNn&US*FE4WZ3(l@+*;^dE~egGZ7#^FSSC{);5;sMWt#9c$*wzRUG^K z)2^X0lO!M#265|7_G5bctlhY{TmJwJ*+~Os;~QOj)+{l*NU|$z+T%Xd-kPJ&n&oz9 zTLe%G3-8`8=@Wxotg#d)3>impQzY+kH6C3Ah!9H<3XZjV8{sl4@%*VXIPYSs$Ttn7 zwoWrv?dK87u#>bkK4)!9MY$meMoA&6mzXT&ZBPKVN4!Rtw0W2kVRZ|CP{2Hqt5Nz zah&?p3BvR1!KY?O?ZA!=TDNRPuxE-Q)$9Zp5UT*pP6cL593-ESiS{(!`VH-=1gN3F z3HA1;BuliM5NW5-9-9{7iF*u#&1-3FRZc#Jk5M&p4QCRt&)&~9&Pno(}?KXzkH6J(GE z+~TD$krrZb=elE>+RFAhlI}wYXY&MrD(&xG?}9!W-T0QsUoGrb)`aaCUCe!cl`>k` zs%jVJk?4A7!H*2VvP|I3Vg@$t(Kb^UG8Swcq7AC6SULl){;s_ z3o_)^4yW*g!ME~xhfFr_*EaGLqs-D3r412-bMPC(tYtQrx`n&DZrmI(2l1xA!5dqR zziijIOItF1F|J+gLi*iQ=CQUL+dc zi8KYi!UoG0Pvr$V%P)$lEG6@8#(YDv!8x5z>@men`nAF3{k)L|aSS1-E z4b5`9Rd=g?l||x~7r`4qY;#na=t_%QleA444vq^qr9pKRmnZihZz<%s}`LbsDK6ZhsL__66;Wqo${vtZ?72P!K%lhVlF2%MeR*yF|k1P`TM zeA_^UAP&Uin&EdlDoHI2t!gMPVaZsby3TzvE1H@Tj5lNIC|5;6x4BuKNTpd}C1hSj zby{8EzK3xEWCP|}wvBwkM0gpR0#qNoIs;EsmPJ+_8-vXevfSmwQ*YizrId2TXhSlc zrwfC@syA>-b0c80AG)-j=Pk6eDgO0eA~S*cRMB1l31uL3BhrPb9}dDw@ook-`EgOa zm;jDQ=B+EDuc>!Wx{^zw=C(%ODm#V@=0y35^`zB?uGtev%-jMGu&Zk9G3EiBbDFDz zT}rdURz(B?tb6vQkfpf@q<$2WMK(kvB;@t?q2vXzz&*26%-?cYN`tmg7&m`PY>Kgs zyGPC5movE5i&B#-DZt0nRjW+yY;niuR-z3J)C!<0(Cw^IkOn$pvTfW>%~(?ViOw@m ziyLx4&0e-2E|(;ZWZbH!CpCXdB*`KZ$Q%l#exh->`?MniRPytPJ4nAQ(6{c&r=LR&5?fA&&aQ^@riq}@Nxnip)mgD$j8%g}>sJE~?BJw&& zGK{P}j%$GUmEcbe>y~MItSpwvXiTN8FpVL`06cT_rF&|@Jr5r7y`PCZ9F`Y88otyt zy)xu%!{SbVhamK=y))tuh%}uQCD!d>xj?{q*D;*2^&P4EWhnBs^Dt3yZNu(68$D-7 z)VwnKh2E2=+D&rVDRCCjAntc`6~t(_T78watv!~T4ENBMWw)3DlEv7e{Qm$dn?&|RO zteY6~-;-L)4=Dct5Ahr%o|7{5{{RE(XHSCpVU4A5Jmgm!RG&jw7B)9pJ@kutYVZ;; z2^=@j)+#c-s8UM)>nz;6s*nRZlg0*V&_#4!IYphrr?Q$$PK82O(X&q4-s#I2E$4X8 zbzI}?Pto+74Np^w8C@Y}ZMh?8r=vP84kpcA-8tPuA|{+ zh^;Q~Z+tr~oN(W1Y*8fGKY1Tx^{$F>ahG&w0%BTTX~pGEJ#0g z7Z~}6zAMnLJT;?TOw-S&+Zjp8lVHw$b5$s+iqcyOE>!hTaMN`SCJEO}v%8K-hDbzh z*k+{-x4SBH}1<#ZKCe^ zEr$|HM|Sr1qisu!dEY8$nmn&^<;W`-;S}~2BnU~s9czkmyFFMzQ;BV)NY2o&#WG_g zvXIY+i277n*yEP7xm)b^N)McmI0HMgkyyHdTR9@yHjf9XJQ_)MJ1NDzdKuD3#ws+L zOKBD3IFWg)v8zW_XLlB%YOaKD$ILwq46hT7hcS{jDDP0)Gj`=^n%2`SHccD1&Q5BR z+{mlWe=e0c?1v2vF(BbNEmPFHcyPPIf#)3J9$ zhB*hdD{FH!?oS%JG2C*b@kP-ng^0?pS_btYig=uW4l`FIe3kiz0PRZ6vR0dsFe=$^ z^?tQIyQ?tT!yj6;CN@W^kZp?|SxsX|M_f?Q;;urVyPgje7GiLso$M(}%ybb*&%Xo( zt(_R4om`)th6pq%Uviw4iyD~0W9&_H7q;yzhht+sN$*L%qJz@I1+YwHhi znR%^!GV5+3VhZEs2DP_ix%M{@B(}|IE&{(QeHyTsGCQMyO7V)?J6W8yVbrd;Jw<3} z`m^Xdt)8g@mYUi3WBbd{kIIIQWbLXxv+%aPW#T;=4M$IrUMUA4AG&*kTJ=H-C<8P5 zNuj+k-l9PpzQ`E9OrPOebO|u3T8^1%sH%&&L+$&gBoX+E6R^)c@mGSica;`_CcNN$8lR?w9Z3x+4TPxcpAIltc_p6*767wQ5Qq=-!x;~|( zL#G=k=F`;&?%jA?bQR-Y5wsf%&x=|vk)n?xR~?$EJGMH{;!E*H+b!_G-%>pC8Y*QS$@nJ*uF& zAN~c!)SH5Exy$>&ADKKa8sV#(Fnz@ zQN80kYFV}LHotvwX?LjW`lYqBCwjyHI}c%w^+(5^7S}uzB)%^2#-Rk$Al-Kz+T(cj z81Gc(zaeCR~l}gEyz*ktSsdQI(Dy1(EJnPvEq$7{{UFm zV!hK9OG_MKl({3nJu6RZ8AUp1x>l!tX4^e8Pto-KK1Pzt(kNtZtfP}yemC*XkKrp; zHssWf}e41JmE@T$^v2 zPc~n?duKSTYEMfYm}+)zY?B}^SmW4K*77+`te_6q>s-pr+N|5r5M4--ox);YINA*| z<4|dkBB4{zwt9*f=u+laWn>;$`527)8jeNIIOd@3OHB&W#Z_JsPrV7WJ6LVBvRKX} zjQqLzm$9y;CG(+46Ckhzk4k=LEqQXG<;-eb$!}cLZz8$eykvK(IJqpjh@6l{Y0$>^ z8@#YcUZ)f`$y?YXh?d&ue=3vg;#R`A-Pw8?wP0)#{RkaGga2QBG}462YMXaE9}MQDo2b>4` z_VuA^NcFRXlIJ}0k4g&sfGFaxZOe8NSHQ+HDl2wRw5sjKLofLF5s^HO}8Zm2-oTF;4o}arlU5l&M|c#4~|V2VsuAYND0GDO~K9 z24Gu1FReu)C}s;MAa|~HaBXag#zsTCG?tK?jY2VjjP5l{La8r!=pA?OC#E=~{#(b6 z$2Do4n;V*S>~~SA-TTGsQff=)Mdu=b!-g2^T13XLy&IV=j>45$Sw3LZXAQMI1K<~m zVDRR*f23c^VzIp|BnA2`oaB3CSJKKuC}kjmG6<^%tSuA>$MHUo9$a`Hea#C`$8BiSa$&r)djXk*3ESlHN*26@XGg z=f5;0uX1I3Ssp{B3!5`>t6RL0StM*#Q2y~j>5lbdz`ihj66vM!R8RVo8F`*_wZIw(jfUlYJ?v?vtY zKsN0hx%?|BQJYsfZ5c(l=vS9U`vThP8it)Myv%n-B}{H2y5A4Qq+e+o)z+aQH^Ks9 zPTeXgIVWK&SlIY4s@OsB_g?s^jqsb6MeS#qy=ZgNvv+UOe7TWMnLCkFwL@2xs<=8clD>Pfp?^RFKKZok#G#qjqL!5a}J-NJqB ze{^T1c@DR$-QQj`_g7}`P?+Xfh~N|0dQz(;7}*g@E~J`eDB)7&iOU|Pxipf_u!amW z>)0CLPWzqdTSGQWiDr=xJM(~|wqdcB8$}XHwnuJB=M-(QRB1-tOz9$6=U^Lf9sOv* zJvw)(I~T%*65E!+nFD=lRVc*yax3Gm4HAuv)b+5EMhfsc)kcVx+6Gvh3{r`scPF>8 zSXrgKTowDMd(;zZaz!u;84-@79Yt1$9FnOnR%_eM9n=BiY_2&Onm zCiiwL=RNtR!gkz5>M%j1mG%u$u<7U9$s2sWhOywXcy4z-03C;Vr+esC-HMvA#pV=t z2PUecvoKIOK9x7rdf30DD~7oZ_Xx&4>ZY9v#i`_tw~E z-4$ewR5s!Y)cQjCaZ52=Za7YQinVraOS!NkBFHg0W6dO>DT}dTiqbN+=IwVP)2B-( zd026~kUNU$Br50#KPwEHyX&AmRf-qZ;@&_(B^#kNO_@M%n;5NAy1D1cS&&@9w{bL5 zNK|7Wj-d9%eLwIoTANt#1(?0MTZ_A5(7S&Y0pmY~G}$iJJqn7VO#NE!drN)sSiR8C z%&Nc;J!&lrRD)8sX`xn#1}f3FVh=RLW{FOVyk3l`b0^ImPP4+E7qjsut7&;8raeB+5RYu9<(d6Y zwRwNW`^Y5twQr>9*03d<7Nc^+pazkS266PRSwb;RS1wYON2#xI@b*;FENrgaLvwJ! zplIUBQ^ytWx_^dklf%~9ecG&!u_{X7V7UUdr*yA<%$iqm=q9n^Js-mGM{glw%D7k| zUCr&qP}A1?#R(p&%X6m2kGk3!lm{anTaJg)wt`TzYUOIRInRrFt=;c|^(_H`vevIb zf)E=bRXlD}^!%$EMeufu;B6~SiUqnl)xElTscL>_a0XcRKb2=CeG?jPI@@y-#agzx zdvvE(f*EwXfMbG5q5?>UIb)79_*Pz%Z>HF2_p78R)CZK{+_4-Tr;K8`RNQ2)byB30 z*H(=0gt}#KCxJ-K7EPmQ{z6bty(7NF65uQ-<@h`S5ilIiyr27`^)MpqH9BiR9yO<>~^wE5M?1_ZgP87fGyxuQG? z51APRH|a#x^+!v6ivC%(xl(`C9trfV81(BY7>O92-u-Hpt5cG!Z7cUC(Clqsf>t1q zuYeCcRb4@mWt74W(s<+AuH8;qXsku1xbxX&X2~36RfZd{8YA~W%9@Ml2`dON<{?4d?1cq5z&RdMB&KaD4F zZtmn4F~_xV<#F1b0g6Sy`IvR8Q4+CUT3}H@5^rr|thZl0|+-`up zTclpUAk9Ss%XHER;JAD)M#z}|07~1~&ft~y6Z<;<08wqJrD_c6z&~evAJVn7UjX>8 zR3GSi70;EQy(DFSoeGRpz2LJ-(q`R{!T$gq29cjlNYM1ljMf&T;2#s{nunczrO45N zozV@#ioB3|8%8jl?#fb5@yEV+UNfFUDFjwdqP}A*U=<_~-j$1L=B#fRr9@WgEu1uuan=3P6e~FGW>s#@{Q6Ygx%%EqA z-Dq?{+BzD{ghR0N^8R&SP*6-5m4hf{AgLJyccrETH$p=Z@`hj2J!_NHEp=PPp3*BXC3^YF`*%}YlO*~Y zIz9Zt*X%PL+Jq~QnCEcy_p8v)r~#QFLO&{P4Lg{6uzgbG!+RDWjOA5EcN+7(bHLsu z*0nfqKhex_NtBI-K3>^9&1F(4+d`o(S932_*RMP+X{hO!_9iK(Bg~fE71+P*kwQ7av!(fQDia#2mXR_2 zE3MMJWv1$uSF^$;g{^+_+#y4pWMehDNg+n=?U)}9bURz+y4LkE=js#Ow1Jt43ctbt z`eMBhvVqG0KC~;9DL11yzZ65I>eCDDRH3rd3lzg)neo>noL4hqd`qS1S6a@GZ62oA zPFW(KbIOtS0Oy`NRL)$g_PAZz$mBd*;{7LH(5GA7Cs4Q4Y?ow7B!)A;)UT#%%5?p0 zVA7|te=-T!mfVDZeX43nCams>r|x^ta-Ei`s9D+GSz6q@HkX4F?BKZc&!sZdzMjG0 z0ZNP<_a4=Qn~_}ap#Y7wJyXh$dlwlExP}MVjzxFA7?kNz$ESFsQJ+zQcLLw-QyD(b zkFRRl4eUz4Lqo=%C-9?K+<0EfCz|eJHs0V68`#&6-6*ux=8QO#?CdkR^{S|&AB3pg z$6kk6+P$QDYB?pi`B$l~Zu;(P`GYe75XsmpIm+6aQc3DmX4-H70+8GmOhyUqkyx6_ z&5yT;y5?5eGlT0|R#x^*-!@&BoRdk}=r<<56k3w%H3bxdA8oZydlJFHWjx?g+dBKK^h8-&QXbjQX$kC2>R>y}ldxe@C=8;i( z8&6u=RZ-_lO^(6lJ3xqx8f~6qb}V-SdybWhXsUGFtjSDwSkA!l`hi&2>Aks4jo*&7 zn$@0!Iey8TELbN8p4?Gkb0w79mn9G0Us|~Nl=M1ijCXMf7nMD!vAWwxqCndi_4KEH zhE*fZv^7ylT*Tv_$kbEWIKga<+?)*1_OZa?+OZsV>XIulJAv*onoGNgt%%w)A?ST7 z)LYpJb|~NKQL6=clNbcx^G&q7OSsvsT*?9D1tivrlS=GqHuf~W%`-t8C9T7p^R_0* z{4rKz(zQ#6QEj7NO5TeJ6r&Djp>7R5S+gI2ykDxSl+f;N$4}mE8U1Nu_*dhnn8p2+ z6&Lu7gPL-OE7ww8wrtRUf_^i(F`-$g$A?+_=J*vdy4$gN{# zvL&>Ru0PrT0K;vuuI{epan$t9ZrXem@J+J@8d!D%1haLka%@VYPUz??yeHvZA}=m0 z8yJ*-e;?L~Zu}voe9u0UZ~p)TN=gmA3A-|G{w8=EPs)u_$&aK!9M?H}@%zG7+m_lp zskZ+Bay$cF5yI4ajxm(ok}+<-Xx%YWHu^o|pZ16!*0Hp|+7`y=P%&#db3<(*z&sp% zYW}F6m(;1#l{oV=xJf(-<0~Q|mdTz$j#vg6uR8H(!6`{I_;N=bp-NNw5fD;T{+ zxM`6Y^))0Ck#c^u(A$z{GC9E{f%(?nXH_V)jRwI$df;`ccSjSz58;Y^4Q+h}I_BQt z7;}u$g3D$$vE!W5uvpZ!lgzhFH_kg!>XxlEKWvFh5HT(~k9yLZv$HX+8^-1Yk&to+ zUey%vtnjpKxzDF>deq}qJtM+@7j^9mN4C>tw)A9OtJ0i9B$NP9>K24RwbAmP!Dp-CzTHIVh)7m=1oRQFz^s1FL6?>8IJq~MJ z_=#<*Yb&YQ-A`q88U4{xaHHv6w}$>WX-{Zj)m}k59lM!+_q}AAld>93Y>vM~@&19T zOTH=NBm2d%it6CkG}+fNUP8csv?`r9(0s{mc;CeziM|%nH5n|H)(gAKld?&P2g@F{ zz*%_4)5DJxKMv-3uC8Bg)6V;mRP)E7>sUHTNObDCU0XzYJOry*mAJJjFvAey$)d4{J3ak99j`ex`TXAD?_8$%3+dZ@q z0$tAFPoSa{xtBbOihlw;SH>Fdr{eu|Uh1Ya5M@+CHs|Z>T=_VNU`DHhyFYJ zTO@uSx+vXz#t192{{V$Y<14iR^thuPetG`@`qfUCSeQzrAon#iPlEn3`PTLH*poje z&c`2KE2FUZ52(ne`%A=NWbhz^p!`i#A^gbZomgGj8aAJ^?v#@WFE!mt;|Du%sw-MQ zfgTYuhq(J}=h3CWKai~~*TU>`^I6>++JC~22H5TL^k`g;^8D4%Sol{#umDvr5kLrplmDahTY2^9$)-aL&(*FQTBL?w;^uVV;&QZ$YpZ{pC$pW|1I?ofdw7UYlM=DGbx;t!0k z)QL6AnDf`m;-}hCj>_hd!lk<~t~D)Q>;68qOWBWo(s!)(ws~{*iCw|xB$G?)@{W-< zj8f?UiaQW73 zCAXSHb?6Y|n)D9`d}G)AGM+`yd)NsW^B<5a(xF@0`;{5TQiAxY=+5f*$6gij{j$q^ z;>P+Bk&)EbEpes8b8+R^T&pYeV0+gkTrVWDUqpbWp7Ni+DbE$75ZD0xYSyUkwV7{nh>kj9sZQCE1_uX&O(ML-W4zpv{o0T& zZGaqQX>|+So&aM3m!}!6jV^VD=}Y|9&+@wadetXl8{HUQDURDo)dW9jjgnRK5QgXp z=xZIW;Q;>iKL!=ix#!L;7p$%mcHumD^v-KkGE9X;@s{)?b)>Y=99@k2xCC$f1pfdK z9qK6URlZ0c^BjAyMD&m?TKTS#J*38nFdcNx-D#V`_0tU5t1x zi47K4+cfcyv#*|1P5f+oj@0hA3XTs%RdTue*;*DrK-5l+-H%-y)O3$M~c_SUdStH5)YpRN}xaxC3s~zpFv{0}X+C8Kvu~<}p zD$$a8?c!-}7T!2eWt%v_tt^?9+uZZ3-x6vbCGiN-w2RAlVb0`{2Ik;o_pbB9w%V<< z4!X6}bK?OvrVc&MYGUq7jMTT#)p_Df@0K;ks{vFe*L0h9D>kEOgmyBDwHEY6q~69Q z_rz}sSp(+lHq1v+8Qom{@5b*2TPZ7daWV9cEY`lleNjrB+qH~)?~nQnFSgfBzqsRr z!iwf~FWLi9kh1AoHM$YrcPAf}SF(a$^o`+#l(%O)sC;PgocuvGm9Xpb!dv;8=QR%= z_@eq#B>LUpJ$A%G59Dfou}T^!+|$@owx(^?yQ|-f-Rf7a2=6KWl|i0o1j#ElM^<9T z^{n10?HvtLj8=%Q@JAw%CLcbu$gSav8x#-1v3Qz~Mkgz)4%(r(8Sl*ssykw`Yd8`$o`mW>PVpD$;us%E)Hb zE%FvKxS!Ih7vIV6M0X|aW;Bp-lS)FjCm)qv>`nR=CALu}^!ZC3aZiTk^2OEdCn>Ok zGwWRts`R+I6Gi2akOG|NwOcu9B7uJM;1TIuOL`zBU=DISRf~A!F<_}ap7h!t#mH@@#tMP#YEL1OGrNy! zMDEUG#Fj99qA4A7BLX_sGg%-5fs@-6QmFQ0L``D5a1J@`#WdM%Y^1C^}P520maYI`S&)vao?loG~4J=+@($ z6_+Vn%#utOIU=O{Lx+uV^7q9T)P%d1!{lwwGBKLZxJQZC4V?Ehxw;FNTOw#>!5|#5 z;<9gU^|(lAUlEc96as3aB^FmLw>B-zs~7r2q1^O1$LCCD8;;x>B>IV6vQ}&+2Tq=~idyCg*_`3}5Pd1Ajxn~ZhT=e5BLw39k(WFbtPn?M8I%IQFL9!rL5%-U+PK31M zp!}2tg5}$;D?cEDBL{Hg;8TeCZd$dKRuHcmvHEnW7VZx$0{ADO2dy^mv5z}W$rMuP z$t$l#&Or32H9{t~W0mt47+m$LQPA6x@wuNC@)UGl;;ky6ws3H&d(zQ84C%(x)b_sw z{8aFFg>-vcNbMKxOB#tKSV1L#=bG4?_KCAcCfdT{c=N&91$r>1xo>kx(5oq3EePB8 zjFM&xr}%Z}BkvcJZYz=T*TnsA#gGeq!%wwgwc&}gyV#nVl7v?Jna+2s~6!l_Xr#v3E(Yg<(D_l&OG`Tili9g4)M9)BTG8vMH0(u@=zES1i7ZBtpg zX8T^Ldn|9+r*G(Lq?YQ#=Vol2ozBhA;%iA_+t}@bj9K`N2(HxnG@@~l&o-JyB2DJwR@xYV~l<@ zC2hg3aj}1RQ!&dykgG{P^Srq-8JJ zb5w3;!=T0%nq7-ap!JY827fA%a>s$6dMrm>O98TjaUU-fgJii`3CCV>DXj#>9yM!3DxfIV+w;VP4GdD`ocq#aen1b+Juc7sq;f;+Stg>dGAt=^(AvnZ&1P++Y;{KeX3Yw2-$3XN$FE(a~hE49B$p!(KL(G75LN$*WN z?s`#&Atk-pP9$IkI*OgHRj?N%W33i4bbafgiV@fip_umTT9%qyl03xnko$6Kx!hHe ztm=Cx>RXhHQ{_Z9ratiFH3*eJCw@8SajCu8%#xGlV^a1jcMfHZM?;fQzNu@~u46u& z)`~6eZlkTzA#HNaatw^be{@tY;tPI}#Ul~lr3-s8tJq=j2>u&cKNC<(uUxk&5-1(L zX}y7YksV_EkV7K-ZKg?ocBCmnHoR31)S|Dv&!R7J7 z{&b-RIa~^xbrsm)gswpAPFTqp?^MMR!XTU8~cjNg|X{#Wa#*TNQ~` zVU;w^faC#9s~MH$fE?qF)l8$sz&|LaoyonVb`DEMR1!h0jS9|1wN*?4$0sA2#CT<+ zxzNL8UQ|G&wHy9k6R*)=X%_^Xr@^M9m`}Z&YL4c(YQUD;bJ4Ja;vjJ@w7TeEXL<>H)<$M#9k$ ze7ng#52>UqGwo5RC$6Mr)jACQDrjZ-g-LpDL#gd)5RsA=s-acSq3ce?KIpXrOEWBR zZX}#-2dJ*D!YPt7_LkVApl?cxSmdnJ+(|UMX~RndgQq;?8p+gbF74G!EEx~p1GP_E zn8v3@U(j@UZysRXv;=~UNYU$JKFGFZIPo^hJWw7Q-ln5Jm=b;T?4tqR;^(#s@`B=Q%2 zo2s`3z5VKlQf*j3yyLw;%Wb@MschDiT$wyeD>ulXk73PTw}S3vV-35yj=c1&)RoSu zzD1#<4wtzDToze+?H#Fnl*Y)%B=fk{>$^CrRdOncN=T~UZ9~ATcFrd<#z?_jfzp<& z&F)e~5i1OB!RHlJ-rMAm{{WpWPRuk@)M2@V{{YKH=~>rT3WV+CdetO%N@*gSUT_B9 zz3Tpwv+7fMagB;Uim9y(9Zz=WPKg$q4r3vu92Q~LsXgSZrLt_cO(}4^c3R<9lf}$+=bMUAo)SXE5HU00j9f@kiJMK ziVPrj6v3{egne)*5a5yrT5jUIu;U**fBLDxI|0Q^mBnkJ{#YGHGMrsAo_YuN6X)cCsm)QyGV)Q`F}~ZbM{xRcXj7 zPB0Btz|MorbpvAX4;9&1+^jGwF^zZ}lUc`o38N=wSH1B~jkCX-cBQ)n9V-g&#}MxS z07rqPC-|7+v_{(*7d|i5A0pT+Kdoe3>Y9z_1;x~@?VJi}t7s){$S#)=jxu`HV*7eh zT7n4XJ;(-?r@`xrZs4ARtj7b9LAmE{MJri}*$5;(YD;-v&rE@W)iOw=@1itySi&;n z89d|CvgG^T)RQUfIvHe%!^GuWbRxENtxiez!*uU1M+eYSdYMisqfbWP4bz1RZO(I( zT2tFK<7BszM`6feP?9+%9ZL51_ZGLyZ0e|ojyu+flX)I{BPMtaP}*-n+p0PJOKB-H%`9iv&{ZpGV)M`Mv!6jyz3t7S zx!Y+Pu)`5RM&{3cm1f^hwwmfVi@NSS?yE^OM3X{lIyAFP-c*}Z91Na9e z>b$n&&{XqWsHI_=pkby?szySNmChh!1D@0*jO4Y@WU>;c2Lh{^2pra%5z1M%4=LLc zm&Ye)&0bqugFGl>HrToa3KLWNNP1V}t8WJqnC^GG$dN z{{T40PHR1)3JyT})Hzw6jvfvujG*j4N`);VYmM$V0nXD+Xv(dmZf-%P+1%_h+aLjQ zr#-67o=vo72zJJUoDL0A)bpn%jGKwKLgSO@NvL9S2I4pZje3>J@1a&<=Sa*{1MN^4 zPI;uBr*u`4M2Qc#=T8tQ=}o;$)R0BTPATF(^e3%`hX~GUMT~{*^{ReBvlJl5J!xHd zAJerf4J(gTz{lxGR}`;d?1k7tzyg%0-OW3SC{Un|1sg{vwHF&2qmGh> zPG_SSM}!!?T8o{ITXt~`Q(^1lHru`ib-j6M+M8w&YnF{H3;Nb&zC3}AMH@O zjJEBvXvC4f%6lGY3}aHH<}3WjMTRaTL(r+{eLi9xralXrYY z=JTmQ_$LUxl&__5saU`&S_(X<#39>UffjJ^dV7uJF?U1=gcKY zF`s^FRkydXS4qf`q&eG;g0Drn#ZtSPvMK9cXoVUV#6W$))Kv>R!Lg&bVnZLid7|3r zR3NHTZ>lA|00m?q@z7OuKQA<2Lt0SNQB)3sml$rCH5NBY$dE776yP}P)3p&UI+7}$ zGfr{Oy&Xl$I03~xecTGSaIl!~ON^RV4Tk}bttL6oT14(B&q`0uor)KwGwz-R2@9a( zkPidtKp!tEGfikD7#neq#*+Y1)Hz3jO7Yf&Cmfv6;ww9Y4AL(d6-!};RJ0;mEen?R^2su)#BxPzwv{9>0%pv; z(-jx!g)J4zvaZrMboHoNM{$}HT}ge%tZSc2mvm%=+5!G`4(;emcPU&|cD9?Mg@#YfkF8@}#91;riaD&#r|^{mkIxZcAWZG zM+L)L+AO8^I{-l*wS`r76@BQd79!W~E?zj@44C6M>st3LF&@pUF&}rasgh{sl{Eso z!z9I{xVKWI{q^G&TJK6&vGzrFJx=3Ey9vtM6&BgU06|hc>VGeJ8;9dVmG>H#R#u+W zM&Pk58=8VNP~`MpwP~6uIU?PmDTx%3^sQErS&^k+5xeK1q00IbYns!=8^J0WG9*lA zt}6~r9!O#T07Z6~{{VHksJmJb&1b1SmY%lplGyWHWQG~X<4w8>$im`6fc9Q#$5Fyb zvZOJh#;FRBgSqQkXzo--qXQhZChknOC|%tTG)4n#9qHy&DjRlv38i#RnACKdW{y+) zwjVF1YMsPGZ`tm6{v~1BtJLYG7j$O`FxYNJeJaGWi2Ses=ZYV_vDH>Q$kAp2IqE6a z#oe-_o|vH-IQxawZ?eQzT<#}zLo+SXG0qDP4ti5KW$|ccTQrEymq$GCPjOW)ZN}zk zhDV^M(CVc=SqzM}+-~BPFkh+7E1mXM(3EgbJX4fmvz{nTTT!LdkPP%4Dd?lUNS1@s z+oeAQl1~&RW>aE-DcH%Xy~%7w8OABV3Ir<=K&7yQ98t+W)NVVAjyltE?^Up_C@1Sj zYDfe*z$3LH^1yLTiuM(|Vw7@6wKVhu?mwu-FaeqdhY|--ny+z|LPa$7GLk5sGw;@; zbLcANsfSV*T#WvepL&Oqy(u{f>~0&qy9UCb4&AC`xoJ?4ibg$osEWSq=UZH_alipr zAB9yX2L#oq+&P5|Ndb8#pE8)nO7q^6&=V_87~A~B5^A-!RQgi6*!~khN+ZTkCZD`* z>q18@`-zZyk|}!}QoFf(j*2Lp2f3SMYt%1OTJTzE^Ay=E4T%Pt|$qK9zUn z+{RH$Q=gUxf@vdlD#&niOx{RP#fTj;YPU5_Su|UMRzfyNRF6v7w!M;a&n5=NS^(&^YijM;)KSuC0JY`NXR3cXNr+6u8pCM;{y$q=QJj+ zXr&g7`$tHV_oLt)) z1I$+Esp-WpQAikLnpyw_Aq1YZ*5Vz2I8&Nv$Q;pog~FEy2NY}r4&mQ4qpv(u-4L}L zeGk@@bmJ5xRge;RIqBAgJ^ImWg@L+ss`n&?;~A=ou`((Ud-G90P%75PF0I^&^PYMN zr8ygipfs5>yDqB-Vdgdmb5>*75j#NZ)}q>(bg=0hXNsja%;#w8YV}5GTwo(gbgpJwF=gE$*))WjA-_WM&&;MF;S!aZ*G-c^Y?`8tRB-^E{?J zGY?whbsZw=#`P`A6h<4i!7G-+@m~!=~$ODv6O&GaB$nHd`wp7lDOzlmgHa) z&0ex~ON0aNXC|p4Dn5pyJR$HQj05T`GFxYeyTt0i_9m9*3EOdPYAB|g0LN!5< zbDU$+noQBf7IIwik`HWE8RjnKHsfjPDA-oea!HP-fz4TlUHtz54l|l8W|pKcd*+e? z6;OCN>s2IRxh^(?PTkEMY%*ft6)lA$r9`+Vlfa~n{hzosBE7M^DC!bu)$1 zv1@3i2X?wT2P60badwDJ6@ux(nziGka# zNGY-|JvgN}BQ-2qin#-W%`X|q{A!k=EfAn*ImJ0~>p;@5xfGc+hCNBgYD0oSrmTbm z+K`h&+;#wPIiyl~#RN;R-lH_`NFKBaTytP`^rahqm1ttHJORk2sl_F6+)z2^rB%8+ zSDu-nHl2v5^eaizij1B(t!>ogT9HUdQ(5<0h{!!DCvnQ@7fI|uAa=z|8Qj2n)=1}l z$t6;U2dzNa&Pg46R)i$(K{i8CyMebO)|IY}lO>l8XiDS04N!$kNHdOUEe@+bR9$5| zeo@Y86rP8z4V<=jyA|9HIn4tMoq5k{OG2%7FDDoxqzi=kxc;=8vC#x=YuOeso|qt3 zD@^bm%1@~Sy-U|qX0H=g()LT0+qVeAk_IUzj4HDdL0&=4Qf%{Di%_ZOB8| z)=4WBIjMWc6xxQME^yPhWF#E=Rc|m0mVdvVlv*DH2<>Y3-CxxVugSRz{DRh}2u+P0hZXtKtVH-i@ zsphIHQKXyD=rs9b^0x$Gf#0oXtU(>jtNi zSKI+3idhwAPn)poRii3Uab{Z0BtBxqKvzFDM>N>@IOpqB?PIMyG$v9r?M_pZj@2DU zeF>90Y2c5>nrEpW!RykPKH?1ar!IS*XdzruKfE~} zv?pFFu7>v>$Bbtc8en&(gQ%yTI(=#x*!g_+r({g+Ym}UW+wrDWy$*Cn*%`p9cLR=` z)S_jE09&e4t4YbCLjHIu`3sCkRAtM>OYSe>JLA?#T3N`6L8T+uoUOPD8w7~F>L zq|(S_ISQefxF^t5*vDpO-)Yv@3%W@dNQ0stwaT)TcNjZC;F`4~wv5_VVxh?9rH)2J zl_Va-Q}s7qW!P@#01esWxXG)YW&lJ;@_EYuX+ zSlgYT_9CTPFGD->Jq`Of3))1Flyw}M$&pw|AR(Iv8TG448c~wJFj<|p%7hR>G?O?v z#xvRPZ@Z~?L*Y4RY*YMH2`vb=@rjh(6kj=*U=71348`V*sU1h zI2(s2tt(i~$rFGF1Eo{cp?9~DVo#fb2vwW;XBNvonK zd~`JNC{fq7HDYG2jQI}cEZP468bC>=hZ04>saK{bda>Tjl}{XXsYeu;uW?l7rCb5; z?@4Y8aY4>bX~O_#ll^M+8yrs*gXzz`7f(SFcFrj6+uo2dK~c$|oDq&i1N0w}M^nWi zH(}pHQRlJb3Sii|BzB}EZpWnoaS6e~@j&Ag$&(z-h8d+&2;-#*0(yre9`w~++3G5` zDYPi(BhsO{<#{yBr1U8^X`p0Qk7J(W7>rd*jsX2?64a*2-<}N#oxG4LnaSosB%d%C z%{@-@ihyDeK{&-Pn8O1U?V+qyiywN@PJVKy0Rx7mV089l0GgP~ofRp9Vy{4>GigE4PxR5hV=^k-Xl4YkIX z8p$Te%8W5J%<6h`MQ+a&%PNfY^rKsw%5E;kAZ)J$3XT;_`CTDP{hy^O!_B4O{=wxc9kh)jjJ#^ctB zCWPg6%E%W=ou!T9bl~Ki)w}yhq_;NnLZ>4+1E;-2Tey^(Icc(hCMb z`{oFJxvRd1SGQIiZR3ybO}W05;cf;PKh5t+G9^)yZf&7Ngk!MpOB1^P06u%uaZ6Gz znV^?aAXY#NAH9)PV8=2-4qol$^2To=rH+-1&{1cc*o*G@hh$ z&$j_sZamPpp%j+qifEMwBD}Xo<%?~{9CJ{=C$4E*Q_!OxSfntXaZ#}y2RWqoE?W>v z$B)W@dK#+{6=CQ`>UeSp&p%2{6p7-&=aKDC=A|nD@Nt||#xOm5(?|yciUVN#RU|7k zNuHG9j@@Wh7`^Zh6bfMV7e17%#BoWXEeD~<=dB^ZIi;{%aKRNA;B(f2%VB!0H@;6a z&|5*+rjj~PiCTwz(t}s9G&!3f1KN>JJJg66^O}ZsC4CRQG=(LhNsd9{lRH+lkOf^`%zYdVVyU)Gj!=AYk)O zScy3~2i}HS)t0SS_cI0ug(vZ>)F*H_sOojc;Vh7G^O}pwQh2LO=4#AV8$rP1)~iV_ z$`U2&I@3^=)<9AypFP}U*GFr8X!4?5c@hS|Dyh)_qFY#1{ZN{GHik2^$$?}%VMwaI(aR~cmBRKD z4p77M5s>1jh+L)_fzM+}Xhq%2nq8bxJdL@S^&r%jSdLItN~xhf>UvXAn2hsU+d{9*?mSA%pL&uvDn|3g3c)1$8yZX^?f~tRfxT8e9FLKKe(Pj>A^-NfCsDnn&!5kG0T62JFQRkec66uR3f zJx1!Oc>`~%8uBrG;Z$}CI#r~8o%rUWT?&1Hut!ckYLvu+bN5eb9;HP#SCoQsM@kd- zpD7>%PUT5*jE*;oQh`{XKq^rb?e}e^$NQ#>YIMS)<+&7*G)=NaD#xB`HW)k(MJ}d^ z-%}O+d(r_zXX!ya@zRS0hXaM&+&-07C=bji zz@S?(X+fX>^vwu)&T&P68-bHi&yOUY`J{x_jHtj=Z}(1XbCoWPia;<dx2Cq9^@?#f9$MA`;>3Xxwe*OA3Gu2d6Dy=I`r zFR4S2D-}-AeT_;-TrA>E9_ca9T9CzsIW(_h2YZ#JWl#owV^t;V>T5*wCQDJ7%K_4@ z*(_3As>?BfK?isg?WTbjq3Kdw#|EDn^B!HGZ8)s&v8Fw7W00+%giFc|}JhyE1Qxfu6>tvn8Uqu!#`-_$+-n zr%$<=9oOaT4Hg86BttL`-ztuxppNxyQ{YHj#^ zj26;)psN<(Rco0xU6vpRan$yt)|V;nMSm=*4&ui>2+d`gT#OfDJrNk!cY z1*Nc&m_?O5V2sondc|*a;gNi&1V~Zefo2tes!MzAn(CX*GQU_9%eKlglMX)YEZYK~`7Q7*NUPs3?q2 zemYcxAmFLV^%XuxMTvI+a7{bR$9|rb4FV)b$fs{T=;}7f17vmSN*Hvfb_2t7rQ9j4 zN_!6kH&IR)^rXZTjsps4sJjS*98%+))3`C+y{U{3<48zdrySEZeA%jQ!*2T$0m;QT zy-2!>X+3BGdB!Oe0p6=i06!peQOfGkmBw?@k|I*Jg+04i(mpo&)479IAaS3iR+EM4 zR%Y}xk|b<^NcvGcYNRMQ;o%H2VsYGLOB|hLzZL;+2-=N+}Q$dJ|0`(-TRE z6rO7Rs_C*qR_$j=KfH$^(amC9TUs80Y_X=3674RbFf-n=W_42RMgZp?)r{L}<`R|N znMNsMncaer=Zuk7Z0yz5h?pvW6XPXmr)ZhB>}y*?Y__bnVZArF#!szMk{g>-`^F5X z`@JeG*fyU+Ic}B}Qv3HY#sygtEZw;54_a0Nc1L(f0Q|#@RzoGkZWY16^r}xw3szZ*^K&)Gt0Re^scQv0Y z1#nB^rRr%0Y-kF%7L0tug=);zB6;;yNWjP(6WXbJyOiZ}!rPy-OtMTQkc{!$6_bFZ zbGtO+)7VR5DWuwJoY%Wf-lX=bQM(e{k{h7QouD6X^=)-fs-cjbymh6>Q+(PMhOZMY z)h?$UhHCT#GD5=Vd2n(KB&8k6bnH*Hhz*gqq2{Yd`_OrzCT%x>8Fhh)n z6;?Am!^WQo{1K|#Pjv(l>DNFO^KA~ppP;Xh^+}Dr{l&1K?GQ&JEOM~HWgQJ+HD->w zQ|9=X$fiNnQ2j+nWP!#_vM;zb4l4IF=64(gsUIrH4_>t8js|Q8VaIySr?~l~mZFPg zVU(MXVM=XVZ~DQX%Btvm_F9e>leqbIx6so4mgHx2dHNjFOH!3Y#1T!mn3!kUg$@VI zj0Fu8q>!b8oDo4`fzC5apf)D%*`s&^CzG0`sBgI-ZYeX*G>WkG?Z*@^2Pg70gdy*M z0Hg{D;;1iS%DL@GM<*V&7Su_JkF6qg)Z;Rc#E@yVSiKuPgR0QArAd0_UYlmGmDm9Fdeb$7&HuqqiR@%~g>uPWC+>+=;w349>-JnKQY zKegL3+pfpY8;&Tww?LIj#on zsylV59F>ugej+ytQB*M>HYyGmlUm2jEui(Bzse3*-mD^%*MZuef{7;6<+nX64sy~+ z+pqvUXNnr?ii)|=cw96vG6D*do@uvhjcYx&8kswqUeVB*fR5aWFgFqbu3}|tX&yE? z+IT+Hpk|fIttz=HNzGOANqnA_vUVtYf};ZgY8|BJsXar* zd59qcZR_5NGg!OoZr^4b3wTtynSfvb&lN?r8@z$L_{pbyzdFK>zrK&XLOxTOYI-_h^Zrqs0*um%pbao#V{6%kT94mSIv}}y^tLKv3 z&C*gjyKjzK(L|2o7#NYmDH%1;=>Gs57t1m08hnnYp^WoXs=lUjqiu_l{6+BIq~)c! zyt52*f~Pgj>Yg3=W8!PSwCXzK;(TCj%3yvJDv`dTcmfy~v{NgT2PEv(99-$NQ#~#bD`=CPf~`m^UQGn`<{1 zj#(r6jML7Y9zjcyk8wrX=stN|KEQLu#C02K^M=Chaq0~Vy9ZSwHnW{jmyZBbRtbRS zCg-OV-*jIxlF&yErwpt;IHs-u`M9J?dI2ExJW>J)>rAm9BoI29uOP}EC}NhyW6<$Y z6O|lQ+~y%{o}5)#PC*q(^b(1GW(Pc)k`w{5c*SRORfkC9j8j#K%`GkotBC&qFVdxg z-P~n?RmW39P3%6GEojh%zLNv^IjlRd7VdTEH&D^N6`?hVgU|i(ZH0e^rVSd&Ce%AR z4aTL#+#Z?{V-b_-P=F2_6s>JcoyV{a+fP3ASgzENV>M_XncwNe9|ft|$V_uw{hGDH zt4WYC&1GYHj>}V8+ADYY9B!1JIluz0K8%+TI@(A}dK2=4^{IR4a8z2|j)ueRfGy0+ zyB~COijGG}kddCbCW|(Sx@;|PAlTSqeT7J}4TaDX?NXy*+S?VH;Z&WglixKQh$I<@ z=3e>jMcuS6_uRvEVxDRB!Ob=T?9Us0-rm)A(AF`!Bt?x6&5}JTd9IO@0Z73d(^0;x zl^YP7hYUzz#UxY2(h+eIx}N^^Piq<^x*M8O!lkwq2s;XcQMXv`R$xqmKvxUeu#}QU zPU_@s7U)qJ9iZpySso>|Wu6HTk1AEc&tfX+Sj}BNQzRm&!0YQsa5KooXlT`u41fXe zQTGR?YSrviW7NN+NU`24%5b4upTd#mWr3m#fMnmhG&N&Qi<))(w_1g{m2h+A{G&W# znJgbX{AX*CnsU@?7u1dj{QIaj%%Y)jh-CKliv_;!J8DGOU zy1P7{TrzAyjty70(zPG7Y1Z>wG$T-Out@zIH9AjYMQdtpU3fQKi^gNaaa%_%@0aFh zb_Ow?rm4ZC>g|81Tg7E5lE9UmuRAf;n~y?Zl!`XDbIBaHDkl)B!IY^hpRG-AsYt=3 z3=YtKW?T$bE-$HEtDyDNiMR7YBObgO)V9_og4j3jHXMAQqxpy(%QgCJY_IxT1!?;phGRDg((DEjQXLai$~I&kqnZ?Pki*MQgT`Xaus|z z5O0%GZhJ7L{=mB&M|%?v!KtRYmg*P=x1S0@NyWh(A1x4m<$OCs=qddk<)RlJg2xP z+zLF_;O#^8QAA>a**gyN!CW=EWcA{j#sYF}!1@|FUG9nIaTCh6Zi>V4s9Mh1^1frP zJN2b*qzb}?Yu6e3*XxIBm5h?cF zLP$KzI|o)h>3AHR)@+1`9e{k1){$5ea1J{PM3NSHIc{;sN@IDgkY#edg!H0Jtr4f7 z!zyY3GXjik^IG)EuGK&iPo&w4f@YzB4^N99PV`?0(66uJhM z7{}%VJkk`%e__~D>7b)yywH)Q@PJJ6$Ur8rq?Rja))-FqUJs>bX2&$-?1C{ovSvQN z%C#bjOJvBE^PKQ_%@Vclb5!g@YkJc(zi7sD!Rl%)M&UvT;^dwW6q_>T?pLskMHArU zlg(U)=Ll6B<~?dw&@;2P6C zaQNHnS7{ z!56wTs{U-#7L9<&#}%)t>jp#NINB0hPIP?aayb703b^~5hq8>iokpwTU_LCvtt3A$ z*>*DLoRQL-;13X5!LE4Gua$?}l(OJtb4kw9Z4~LLXpD^x^G%1~{;{G-v|mXpnD-7& zNyaJ-Z&lK7d;_Lw*4Bucgc}T)9FRKGjGWe`a;RCNuZz~gYh6Fdv|CtE29PgtRIV+f zlE-uh_gDmR+NN3y+B%-|GU0(lG6G0Fs@>L!acOHC%n4Z+a*#Nud!hLmHbhGl*y@1h zp9p`mmEK2w4GCXCE%hX{Gl1ks`@qN~_Ni>{?%F1aA`>ckC|={6EI?|U9s4Tm;Mq&X4xXf!1M)b64PN>B1hD0rcfn<3?AEQ zC67myKY41PkM2`aYpAv6+^+h5k10^Ta$t69YiSlPpv8FWk5g8XTMwI3DKu@^@^93U z{o_e-q}_}dXN8BRXje=wuW}h}uj5ioVVAZlGA_2{!2bZ1H)XKz)EY@4Po5S8`if#* zt0~NG2&;;3P~BWrxmNo68hDXU8@Z<0D^YyK2LlG2TXLKVQ()GYLKBiYW`-C%=RUNP z3jrh!3H<0gzlE{uPTGUlLmdfY>q{d1v@~JO*sU@)Gmey~89?CuaZ_R>i6IWz*rbnY zaf$QE$Kyjv#cpiqFcsAx1cl!BoVM3ITcY_YBtDT84^_tn@Jhwty$Wad0J;H5)YU!QYxN= zUtJF4ONI~Z0@lJo5+N!ZuS(+W)X1gFC?xl+d2DS-HFnJyL&G85eo_Y_t4Sr%)po3p zWA+60Uk`>BC!94q=$S_wOqZmmjZt10N+ zX`xYI`T6y#ayr!2v>Awhi+0}FH5*&mZuzH`8@Cjq>?J8*LrX%95?2W!aKL9GpRUgF zENLGhk2IZ|6usrCjIqrrX(T+3{dF$?+(yM@f=v4gmfG4~ptKK;+*Zlya-5?*OOS8D4IA<8QzE|`QTgqP z&ASdQ50>6{Aeh*m+*P}15ddjH-Z}bIw)Q4-va&3^al39gX2)Yiydl_&fC6IxccLpp zILu@)fo~*aX9p^3MMqK@i3c1U)ivyMYoB_ zz+y-oR?efPKA8lzlPO7JP^Ar1j+Y!GWoBe9lC9t^Pg)WYBo`|NJ&k&I#Qg!Ze*x%M zk0P`)#F>0!1gJDh39F$evEq6H<@FHV&lN71nQ1}igHwANStoJ!YCp7OPx@!ls@O9f zW638VfHk3ly2Un z^6ED7EW-?>{JEu>Sn2NQ*yF8RO5ojv(^f$N%!j9Q?NHj!JN&OJFv;spJ2D(nVIr^Z)Gejz5g+xTm+RJu&2}X>a-IFf)ti9J8OY+92BP4G zk~|FZYLdIS`I2@!9S`A0iSIW_s4QA!dv{`P7!P`i%i*58beGK)#1h8?5gdf$ek-NX zit2Hz{grc(3jTDkD*3X+#T#JAC)%|1Zwz>k#1Y)Sm8VH8R@^%gg<9uSTb8WRNp&N* zv71W*EkY!T$-&%kDI-V^Ik(&D7#!3|a=lP1bS>!@0rlH-2$%rlZDDwu_kJV00CL0{ z&r_xr(64G$E+S@K;Z92W)~)uWC6&Z5Dx)qkR01ibeHk-XWm_w`Ev@B-Fqm>!lh&%u zdhKntd5aFi6;{51qAl9#`kteyTiP3Uj(3mDPcUuVzM`*qQ$f}B#nu|m2_;J%!e1z7 zBnqWTtr?S&zq)o>b+X0aojOF_nc*(`y>VW30A0g*A#zE@LsnKtT{&|`=+$&bKvG6* z4gsv|lRE0qxDDlS4mwoDy^d(B%ictKX$yaMpW^2}m2Ivt1{5<6Y9yWX6xTDb$Gd4r zW;w@TDa|tg0=7A)D5E}GXo=^Q9((Zs@OVD!wHjHp;HcglF_nqN0QaRYBU(!1-0wz39YzQhrQtE;-7p*iyJ~M^ zYgnPETg)#ZyNnWu`(GV_tqc7$-FPSF8MgW*-CmF6sfqme~it z$y&!@)J`%|G5#v}LrBs*7o>QW(k~|J;@&vr3-?Y`AL?txbS4jXa=2i5JgFF{apse^ zLZc~4Jl&sp(ZXrTA^g2d`JNubDynX_pzq7A81v4OyCpB^s7j?)uch6nnok*F1033?1si{`!#@Zsf32sArxzU4l$2As^rhOi4_BgFHvmX1CXjqat)c*hr zarskiQyy^R@9~q-`XXFqnD~N=)agsadnia&;uvy|w=y8==tzii>d3R%!T0z|v}WsT?T{tWaakDghvUYtwvxd1c_g zhj+HRQZz3z;1wLP?th(U4rx72C1kF7_MPIb15mT_JV~b8>Hx*^LQ`?gVoC6H_`c$O zLqoG)4{55o5wQE-hN?4+uNF&|tCw>+FNGS8hY8dq)}WRs4nl3gYV$u8}@Wtt|N-t7cw#z2X6!l-LUZ9p?#<|p;Sp0A2Esarl9R{w`XH>;pc($ z&3D6^-TtpTneC)_)m5+*4)wDig`?3v9qBr)j0tgS@+7mDKwoJ*^IBdqg`>F>c4+pG z80ngQ$HiX+-Zh**WoubdM%VxfRPE2>T(*U&=zbCLH^fV5rnR)OF^52;0l23%O8loI zi<3OJLe&>k@OGaBY)Q6=qm$CO6a*Jum}Aztsw+JYN|lsSL;liWBLrme&03mV%Bh2c z&T~<8XH{9<6`(N-*+)k!$j21O040L*G1iN-HtaoP2nk*XdX80RU`Bd$q-T9@FPGfycXLfZlSXe+3e z-)Qp#Lgf^F>a_k;@)mfCupfUNXtP0kYKipYvC5=?f_h_`wW%;&tXC=OPkK%3s3&tK zd8UyH1PgeA-4M3RZ8+tJ7PCMM2-s2K? z&Iu%eoS#a$7Np`O3^y_098;7`)il|qZEzI>NrFJ;m+k8;VZlW?z!e0_ouoy1BFMX! zI0|!FuohiD0;d2u$n~bQTAD>AWNRTIgANa^GAokyE_2-el~zb?U6yU6SAjPS{GzRb zs#SB0j-Ird6jvztvR%UCik2z<%*h}eZo_lhr_{MEE^p~k$2FFr8x7e_!|<#6Ma)0& zto2<#Z~8$}3H)e^YeA-7@g>VY!hZ+G*&++5PJWCk*N@mq;wj|nS~TzNRPWTNMJYC6 zPlfnL;&3=0ZG@lJy+7ls?)V4c$up3q-2VXMmC+p{D%P?k_~3K!CqQ)>!o&Xnpo(|J zJtoIb_$ym<^Y_H)BxOGxRa4>>A zR~YQT)>DE{L$aK?ktvKXidV7QkF8jLlV2eJ0C$SY=7g7j@$x?kk5^CtCjg!)tCE(a z7X$tzgp-8>pL(z1;0}>E$mNd}OjjlF=v>Y@Bp$Tv0PlhPRJ9V^lI?dShpLsT(SX`a zY)(!{q9s2v4J02vVhCm{#wpe-w*LSh#exl6vw9O3dzPgDULYeSyVbj8%JJK+W}U&e z@R4c}jf0R$teb(3GuxVRKt6o7n-cWgdeNr<`fcIr*u`3HF|_PscwiB$fKEPB)~V}c z$EZpN-QQYUV<|pZvungDof^^9{J017s`_G&_(rBU=O>zk^kUt`pKILse$^h@4T>PQ z8@7SLu1{J9?&1Fcv2EkGwu>T0+qZEQ(CyIR_oQ6#rld5;m&y_B+w-<@lUSb@{7qq~ zX!f?(E#;h?F*wP^Dsgwzxhtz2cfm+B%dZeE=BFYy3NY^>TgU?Rrd zy^pnJ2Bjv-aZyUlPh1BeU`p!q&)R7(aV7w!S&lA-j215$AaPz0NrTv4WIQ*6AsG&6PB76l)q)j-hF$ zv1l%f8Np2QYHK}f#kX1)kphdS-nxpi%QaDr7&FwY|v-hQ>Lo8{G$CX|y$Md6($eJ4kn-sy*%xwm4t z(~~TNoPukUQ?d@~&@?L#^Y0%=^hrZh{i&axl{Ph6g8en9T}6y&;-ZV&`P*mtV!cPQXWz*1`H)}6YVULUx5uUTaI zhTacK(${#i1bx-!v*k&Il8eyhncK_NwJ*(o43_B>`OAzJ z_o|oLQoL%bKhC_2iXzl^FmiC&7`HIJw7y)U2OYXotz+Jb$DcvzS0{CINW1D?b}Jc7 zXV#&&+9N^LbHz&7Nv7;rjmcuTIHpX2ErNlX`#Mg*p5xr>Rg*?8}`*p9uU|gkx>YB>uJO-x|~(+0R5`akVY+kN0b6ZDTsoOWqyv znOVPQy%B~uiVTzf=&f&y$>x5}kQMD|0sjEUD$&R0V0g94e#{yq$&JZr86TB$l6)f% z;LR7t7B^%+*%Eo;Vbm1_;XJyvIyn(oWllHPMBj<#p5}(2B3G#s;Bq2lunhYt6Bg zmgwEq#?_jNgUC=%Zjo`*}9jAT3@yK5fJLQ$>O;ic`r?) z!XPn>?ssGNtEH=}9(^|Lc4D1U9Tq#goklk{hm-+w4S7DNX{r1{@euyahF`NuFUy{h zuYaXS!ijI9J#Ri9M6MZ6wh04%b%bL(9_&HG$m#WTuHsvHDO;_XjJ*7Yc_w4E;E36)9`W7-J%R8pyEwJlBO9)6~7g>x>KVR3H; zoRb-(FS*A|)1sSIh6ITsi6S4sM--(u(C6fedyxtCc&=V}U1FE9LHB-?*Spu|VROnwPF(J#POUOk}@To z8L$97Pe3X1_Z6(PEX{cvmsG$(z}wu_Z#=IeiD|iqBkDwxJN06D;-(6A<$)iCOI?LH zsG9s%Vwc&=YLb&~74Y){Yzn+s1mM>BwG6fkR~{mo$5=iu1=mok4E}yCC}+ z*x%iA$@);8)`doP*n&}pVA)q4K@}P<)Bq2ZbgM&-_auyx#{!mL-bO&<(xt6Iw8yS~ z*KEf*3PJrU+YCo31Y;Q<^fM%`SDoAYEhHQqfOA_rl=*1KBn+t(a;xfVXJj_qk*Qq- z$=rH+RhEseEiNNvIU^aRWVH>;`eo19E#QstRff`k3hX=|;rnfG!Tuz=zJty%LgqXk zgaca9jBjysIK{H+qR;y+Ln4A?g6((*o*Y(ow=teroMV_!{*+aUy~?E9jmpJ@p9y?r z({P?1KRWdP0E{cY_G;3I>LRy4`01@7wuW`KL%uD5-?NU6%rl5B&VS${wf-k4f7!Y` zaz4QcANREu=Br~A_epUeS7FG;YT%MdinP7i#w=yq5~o3vSr?uNy4F4*Uid#somH*Wu$Dp83aKcxk#U{dZ)9QX-W1jJ z{{Rrh;M;iCeO6z(bA`q_*2jy!9AEr4@YEMq*DEc>;Rsbn3e>qGlaW5s^5tP~@UO%= zK7%ia?p`S@|pBf$!jFG zQmYGyfk#j>K^03!8?=)ic~W|eR!%Qk)oK>IxqANq z9&}I8bj?|`E4?|6ADQd{wNz#bU0B*Fp56up` za`~8=K>q-V>R^w&bM&bEP6zg^yq=9oZOh@ioxe61^zBc$ z;;&=tS?q2kxcf{9T``}S;2MJE?qgsskhE>t2Nl=XL(i17*ysFXq}*#-LRwflMu^*+ z%d#|JaaSPmcZetPIJXxzrZEl(By(8%Rd$eZ$E8Hw(y`S}DI#d4c*rP-$J|w< zylmm2!S>>#Wua1q_809@mih9&)bO!Qj)y+=I@;{P!^nEuZTqUffz2n`Uz4zaYEo=V z%vk2tBNOcvNL^tXPBOY)V zBA#rei~y`2F3t}GRWxYZT8t_|BQ!1*a90_mkqPxAOl}N6I+aT}Do+@s%Z|iSM&H@y zB2Nab%H&IF`F$#u!&kYYEhn~j)Co@}X;cqTI#+3~=n>lZmq@V}E#>L}Ww1DIa2;6ZB4F4>T;j6ug~yTRgXLaIR15K_E`aZ z8?HhLJ8xth{xxx06NvsRD1OZ!FN-{D9_|Ie!uoP*zlB%I{i*duAH{0t^%Z=*%-yxI z4~cUJ?MtPezHC??+#1~dtr5TB4X}}U3veHhBL4iD*Cg z?Q{PCvcs!i?_qoJ;E7NVSzC3uc(@=pYjR|nC$OMy3{iD1; zVWMhUK+_>hRJ?7D5CA&Um%AGx>9S0r4yQE;l7=8-Y7GFh^J-Sq$9Qju5vyfJ>!2SSgu8q08H+ZYmXEdKsPvJ=Wdf~03w-Lg7U}CxLM6_TQ zaktXC?v65r#ofFRlA|Q{1Cv~}<0SWcj9a7cxK!-DWpEtLk~TVGSw(N8EVg`Q&2cz%f z$07PXR_i5MZaJ*1h_^HEQ(WzKOf_iU=OOX@K)9GSW-2`$C)CmYjo zG&;GQN@s1PBNQ|^k>qE((4W{X*kZpdPQ&wi&DOXKEgF>zKJ9g+d+V? z7g8gE+?#M2fm71K!{*>JyvU2WjQMDwd}uCtv!!Je^W&ts5sRWfFRDEiNKH@(jLo1&<~!fq{9Po|M@}*UM%>T|FP-sxMS! zqLE19>*OT-m@u@SPR5=E?ug_z0NHkp@ZYvrzC7!5lqcql2>RDZc(iUvqDVrDW zY6$iiB<^t_q6I7K{P90k9J%FTb+>jtmMo$DM8fL@G`y`Cmtx#r9U%J({l)c|v94~}@2B$M=(X8z!ARN)s<^BSMp29vd;GW;@ zdCRZiyb|^$AQO75vDR5anEe)!I3T0)2VfD;hThb=$+dOErn^aRk#TR^hM>4xHqZfn zx0(J%EiSi0b6}#6opP@f1PIV&6=qzry!wsd2h|9O3$#cE8~mpizM4w}ocrX|oz0{_ zN>$e3{ic3zF}>Hb;3tjBNMOl-$}T^%)ZMiRJUig_6TKb_oHKr%;E;wDUah^3+CSGr zoo)8b0=0Xce>QB+kEGnfjf;DSZ4b=gY@IC!lo3a^M z94K9qBuE={nJ9l_{JxoM_F$&C`Z2^$O4$>F9+Lqz5Ug_{2`wn5~`@ zoH)D;iO;4CqFX8pUj(YDk%VL`5D=thT`d%xHX|h~TiMkW8lbAW2+~toNGt$_CV&{z znI(kgqO0ZoMl5Kp7rsE-8Y1caZlmujT3PiU#Xh+BXnu0aTWGdbjHxwb?|NHxz6N@yQxbY6LMRoOUcSuK9I>qrfU z@PZd;xW&GO3lX;Hp&r`Y+#OiW-8jF1`lQ^JL6EN$I9r=rTobA4priL-&mh}4Yp)6h z7D|9m77Hq_f2dkeSpP{|0;P@q?WW>6W@4TbnfjHq)q1fB^X;g&^sAV);3VNdAbdB| z4xx+@aDs99ZG{ZaypHsm=4{u~B#|Yur7c;6H^Kb$aY2WDCjDfB;liu5F(ZKZdwRcq zix;!$ZmIgWh{lqIT?Y!KSD%htZjh<0J677*tPO`fC4FKY7nGsiH&2=1Gx{enO~pMH z4z~T}cP;>*9}CU2a%fzSdB1o*`Ec3^@Htm#NO-PeRibyj#T<+;pwqDR-RDHk8S2+i|fS zlHBoGHU3)BZpZT^RfDcmu4cDLH- zVcT;>+IQ|d+{$KCe}cBWEU;Cf){U^3SH7-|k`nNJ&nKXm2TgC(Z_8>m#LZmqdTY`U=5kuHcd-eA^G-5DfH zFm$7 z+-hcHeN5dL89>^MU-!e1kEU<6wP*ms>ZW(Wcs-8%o2`$FUT4-MGRUMXdSdkPqg5l+ zzPBmGT=rRu?QfPJv__v$8_7r?Caj8Dl4BpSUsCOAO;heIqI-yuZU za?|B={OS1{cb3TSScNUPhx^X;cvL961Hi9WMo;#i6m!FON$2O9rq$J4QyBc9L~#c$ z34&)bi2{FAvK|PpX2JhJw%dA7(`~|OA$x3>H*AIrHG?Be5b7p43AEwuo{CUy4LFMl zXyM`vpP;Lnpz!TSzE2EEKm7wxKL7rB?hJmdN`ctUHTn38Ta&DjzCWSEZeXDl@(1Ah zB4-RHBKn91oH=`OCY+qMtwz<{N<}&6;JPz$bLXy893Wbt>(Qd-=h=m zr|L=J>s{>*A&*FymuO**jCxVW7ga^Pw_7{Z^720bt3jwC?!5#g3W~*;#B9pBfag!y z$_P2v22U^C)~#*d(DLNQy-EXlLnaFrX4)7mS^_W^Sw689>x;}xl)*40?V>bD@ehFAq;UIS zZ6Aj6eEqrY9i!0M3xCd1nA?U)cQBn_Pl;0Dpvf$1Zm~G~Y?MLrFv}~$V7_I7{em!- z2a!FgUD%_$adAqs#T@?kz?a{@ons~gZst3cR=%sbkm8;Y`1b}cP>XInFM(be6Ha%S zW)k}5#+rkJI@nBAJ&jPd~S#HQvHzl6dd3z<7BM((RuvzMImG(DlNd8p!RifP-+s(Q$%hJF;%tBqRX*r4H1H7kj*M>)N@wvKyDV)3nN&)St zi4SWgW^42YuiVqMVQXZ*8Wj5!4YeMJTX!d&gvcUziA)E~y!U0r>zEm0=~Y|fbqJ$G zjT?+JiXLvgu;n(~oAUB&u^)*QOgU3#a?$)|y7)>$`1asy`L}Xzv8T04lO6Hg84k*O z8R_J*R}t)hzf_vL$Z6KAy`bX|n3>FFu*oS7KYsCD^z@j|RqGIHF(=L~F%>=*Cqjv+ zH=obLUEwqeJ=R1VH&wTgBPm87Qk+&~_0^OB9kYD0_w*aL_nLG6jlfAqXwKkny{}BADCI$(Cu6aN@;)yaidx6 zn8H5RAaF>uzmFd6JUJ=zBn_9fQ4*Drq`wgq-s0(xM>rK}TXbxmcyQ@r{~Gm#*uE9_ zV)89$&P`X48VSAWau`A9{Ui_TT>j(i!XGCDI#CBfvWR!vA<-lhLYxI zTYT>czGyXa;12`hCE1qu3|q<=)@oDeU29>nZNrzJ#C);fD28hJL^PS*4P5XSzLh+x8JbubA~@*>)SsG$ zg?~YHxVXDKCaRQx>7i$A1 zQK`D$X<`qc&RO1U92Sz)6T8O@;hBY|uGI!6zeZeWfe}j%j%dz_O$AVU2ov{pnCwiO z%>DofvnU?hwh@04F&1FzJDw9VdKx0s#>syeOV00Wth%zXl~^rNFpR9Feq^%Ef4VUW zq~8rR=VL4L3L)pitdWeO|;~9lM?ir>S5($u(8eXTCoBu<@ zC$HGnvdKn)R+FL}uEt+<3k(cnErdB^G}|m^*48HS0ji2r)i=AnE4plcqo`U`0)V9q zAp!Ve>-}j*oF(Hp!A9UwP7Jrz+gKj|t1GA)41j9H%N3HYD&gYSM}X$dnVCtIDK})| z`T4Z3O@5KOu1H75qe;Qq@#7f+0$(<{%E5Y_P`*jtdpqxKL(wN-d5PB7-)S!vr{2t}Au&|$=j3kLD&ywq#IZo=p_q^B; z3+(xsGjDD`@p2hP!f{QK{c(x;{G3v99f?%<_n?L_OwBnX^0zu~SI(^cks({HUM7RR zj-H}wmv0o0Ql(81vvriM&OuU}upR!6+W{4p1VUfk+T;Un+Se3Tj$-0vbk+DtBj4$O zq{;)IKiffV#P6E3o zy#Wp0VA@K$7XR=G*T}>H0{1Z8ft8%Zt`O<_8U@V$Z>-nnvkGO)qc9O(9pG?1U#L+K zf#2j%f+C_CI6L?&kv{rCigu^%6V=`&5yG85X78$GuANE35JuP`V?m0}Dw{@h+Iky5 zo;_IzYtm48ff0aT4aijTI!uzrL&snz4GX6?Zzj{di$_d{l;nngDklPi1yB(|PR5X6 zAg`i3>n9Iamp~|>PY6N)6joVvdek{wop3u%Ca`)IPhSanE63sP)t2US8h&BtfZ>wC(2*KC=n+U{51o$)vYO`^`7pe#J!w5y zD26P{^H~`2GSlzlXXxZT_TWcNz89`dM1h_F8Uun|GL&`om9|0-o2mISN9L(j^)e#K z>puWBeYkVYRwejV`%lpv$*WD8)_To7`=+fe&MbY>Ka7-?MW$Y1+fi4TuD8z>ab8CG zMjj@3K$2*I9Trk?nY!<@*&Mf%foa0`=_p*MaENJEKPFtihrC-nG({Hf(Hy9992<(g zkKfQ;)$G~0Mo+k@JZf{j6Xq>Qz~QNYI|E|tMtdZ*kU^Oqu*|p4k1BhxW@=t&!c-H2 zo4qvMEYtEeJ%Nf%5^mxnKDM*wdNg;7fInidk4NSyXI6U$oWNR;P)KJG;0VS}r1`H~ z$lvZGSYeRxpJD*v1$cdV961Ca!Gic$`}MN&(#&j*+G+~(4Z>7Gk42bQOp3UiE$2Zw z{h(sk4`lp(hgX$yl;!c6SN;U~Of6;?&fhp2aPI3|3Gp^=23TsmiIO(C_T-5o#Xj)u z`7XLMPJHyuULpt!A6}(N4&tRmh>`p1f|fyv83&c}q`{WZ%4zMOl3U!lfw633;7TO< zDIFql-ym&^GzjD5m*sr7zB{GU+ii2y?s1ClPzJ>Ure>BlweM79H+s<#Lo8u{GuTPm zja63yf|^pDrUDAThS}0Ay0T|KhsPN!3j*#N9n#L75@2m)@;FozhW6qiVIG1n^1X?i4+{p;)-0fr&+Sm`T7}@#pu=-#*f>+xZ zyQ#{G&a)xNhOgu13--k7g1h;ttWnZzv=d}*hIM{5w}i}MgOk)Ha2-{$(sgjF#(Z9r zJloCINe=&O4E}8({$rtvo;(3i?V5FanREXh0+U*7Q%c~GE`=E+l!Cni`}nO zb6&=57_bor*zd%DuWyMyU*i*H)?=B?{sZ{XZS7vl6%PJXmu_pq)=)HcTjdh=jneiU ze{4S8A^=xoQ9Jhl`VDr5}!9BapQ9E1r!1?V#<>Lk?zef;-gjhq$_s} zrwo}(=%0gl7TZCj8{gGhrnGb$_NwM)drt4oa>-{{iq}~!CfsH;74R1Vb_M&-*#iA6 zN;Ea_q0uE0Y!Q+1Vt+xj6MHh*z9zzfFd@J3()| zGrQ^oj7)J{cGDK#S(SyxGL2^mQdOq-^0oJPSk1zlK`kN_`mc6kGt!VT0NR58>H_K9 zP-5;bN?s?v+Lh%+Jo|=}@Rh(eYA;2kD_IRX))MSSmK`8pK00ig*_bi#PTJtlqn%~c zhFH(cw>DQ}ymG?5irj}x7Xq8aIpA)r{Uw934kNvC1JS3oYx_-Vz(XhJC&3OoE^%Z> zxe$Xbnto5SkNu7uKEAu;6mb{BYURcW1YbxhFQYo)n^pZ+D_y%b6wN< zfEjioVV@pLPv$Kd-eK&TjP%ewp|RNG3?7yVNm|pn~)(&BC#Q{xcaF#jySqqh*@k zt)W#$aTY=O+jR85y?}xnd?#$arp{oh=?gkztxJ&b>)1_<44}Sx>t}6EznJnwhF=$( z=eJSZr!furW*&l%Jcc(y%04MhKJ<;y3bvr6dNJekaiIFC<-?Bm$G zb~`nuo6P%%ekNmFK&+5i)NjhP(tB35Tx z=WONnsmWhm#8sl&<4Kx`ry4Wxz!-6`4dA~m^#8XbY>P%f`e0j~1i+QxGN@airV8<# z8;7a)qm%9UtN&H3#PluUxsoIkQu2`EjJHb{SO8&e*7$aN7&bzyJUfa@9`g>?y*+u;h4&arBb8^lwEVitP51m~8FX9f zQ$fUPOpj^>(D!@nR^N-d^nY7lsMN{!~xK3bPs@vu`3HUp_;P>LzALme%Nn?lK)zTB1r z_opl5R)yK7cJQm2kz8M=?}#Vd2t3M+t3(R5br*DNZFz?)WCW5 z7quXM8;KOR>&@Dn`C^-<>E=dHm6rgw$f(pciGLK9O@k~uRt&9$SEh?6p&ZV9vdc)i zTG^mIYOqX`eh_76NnG#>%C%(=&R8!*Dsy|SUmjGkn5HQPuX$@Ns9 zyLtMhBz_A1vei1X-_wpEvQ2A70&`6^Hyjj2%wY2zvvDT7(4wF;*b6|fG&y%>PTSy@ zj+oyQj!FW3_)wMC#gEJ}lI2tF)fY$~tRK-g7D(}Vv9m>K#*`tmY4_MN0;&05JgBYf zO4-*(GRjY66{LLDM;hWv1 zi=V8ggx$=5aoHcp zc?ZYhMyAFF@NlkHW;|8Iwi*%yD$Hx(&8UcZJt4qySY$N zY`fZ;K@N&763h2?UTjLS-auep%gf)ci&Bw3hVD0O9~Zc4S=04Ob1*e^Y*>)mgsX8= zshcV$SJp>C9Bd;vi~<2?8KRiZA%-WtY!)8@VRdwH^JLZr7WTK5&!V@DZCvfGDX zd3R%Uip2hx)fWCBgf6vurnus44B55q9p-A`ML_SE`=@T*cuf1G%1UHbxaU=9ZyVrH zHj32d@KOlYONdq814hy{$wy;mV7z?2Y$l&)*!J*(+%VHa@I+T%m{~qf&JEfP`_M{1 z+e6~fC`&YHzZD!&%NW+!ZX_M&zvc5qYw@z8g!wfeXn}V;$aY6tvk<0`-(Wu(5yCbBhbT@u)2CzMXNddNR*tVsC0b%#jzBH;yx#vB#X$?b6D*xgbU zwUW^mz+X__7+Z5Uos+st%9K}Irth%d^qCw6`D6o{{xQY+_sQ1(l!8Q0jtQ|^ep^S? z%eF!dGs!h^;%1uy8N3ASsbHMrQ#dVM}lRRp%~s?bgf(c(lv$Up>+0w*HWU?q7rOiOT#y#%p-EFk0;u)v zz_mxZ{T|je%CsnZ3t2Z<@m5QAOlSv{6K4VXWqiX`DM+|2KXiaENA6b>oKV!mFVHs5 z)IC$mTiZ!cBX6^LFqC(cTyC4lEq-Jx*2?j}I-LJ{xQnsF7oY~ON=gbuQu<%Ys%Mx0 z;hD6(iHSzj)N?N&BsTZ1THMnXf{YN8fp9iBVJ$9a8es5zF%Gu#)pRBnvj|P4uE}3M zUqjQ6J-kXT5jf#z|?90OdY5 zoYnE)kxzJZa4wr<^Z7uaWal1CWnvrO5SX~|QAgtg`$gG{bdl?w*%)G(bp2#M-@wMl-nvNt8iZNU} zk%9&!?n%-k*Dz8@^o^C5(hahi(>~x(sm+=20(uM52Vr8c%uP*pH7{}-6`SfzNEIew zj|dPLwQz2Ba?(4EgZj|=JLVk3ViQNttk!U4l#a~Wa#k{O7=DXAQ&r_Zi>#wD9l{VD zV?;JivVhU2(*3kQ#79wIRv)Xm?+&fIlmMKg)l%aib)>tStZl1qA>-004joe1Jd&K!T|c;GYi!+TUnoFdYU# z0Du9nA;3Q%1mQpFYzSHa)ZhLG9|ghoxB9T)?IH;FztLD=x*QVo-|IwL1EBv_9{}hE z03hWRtsUIV9n2g7fPYYA9G}&|H2?tVf2;SmH7hGSD<^=Rm5r01m4ly^oeWH{@$hr- z0RRX|0011gE;K7UJ8KfuKiY-hPlEm@t(Xk)_c{=ePynFc-{@K}$0h~;IF^l-wE*JZ z_yk{N0pvgUgbXTx`UefMRRHr38WIiM=6}l3f665!`rlhp2Dci7{&#Lf!1VNAXZcI4 z|Caf9V0VDjzSnoBmC{ zU>Sig`mg^nz$^a0YRf81NRj=eL}VJ~E@bS?>;M2P1k}ID5v&RSt~Zd`|E@QXMgOWd zkP!crX(i-8<&L%Z*ZINa|I!1n*8TvX!vnkk1yB$l0g&hrQ0NeU)&OAu1UNW&I9LRD zcz8qv1Vj`(G!$fH6k==~bUX@DDoS!v@{iOEJS^07oOB<_Sw-16`2>W7gs7N5%ZPuH z<`EPUfIvV*L_tO&LPH}Gpe3gj_-~g#0}w^v3xb;c&tG{K|w-8!N5R+M+k)fUz!Sy4ukQLO%xVW#Tbs<8H+tQxe%U0tf3cM zb>@na!^9=z0|E{%9zFpTH4QBtJtr484=*3T_-6@8DQOv5wJ+)#np)aAre@|AmR8m_ zu5Rugo?hNQp<&?>kx|hxz?9Uq^o-1`?4sfl5IAL8MP*}Cb4zPmdq-zq|G?nT@W|-c z?A-jq;?nZU>h{j=-u}Vi(ecUk&F|a0`-jJ;XLK;jkkHUj&~SgT3<2r+*Ttek!+d0e z#Sm42Gj_%#XAg$Q5=$;@=>0&!p?ZaF;xdDPL&>>Kb^RCDe`ET;#&gL3i0Oaw{7;tu ztOAgrz^aW7g$@t~-0S=`#UtlIk8t1YO2Nm!s~)Yx*`6WGXXb~&JfxcmrF(!YYdh?X*E!i){_&u|j|RJfvQyw1oR=SN$=6Z@-I;dK zk#3>UZn7^q$mkkQ%pfP9v@%7!IQXPQ-%l9fV@YJu*u|n!C!G7KR#dOyqc<;H!K*Mr zUf;d0_09?%YI~4cTwu=3M7geb@waRia$3sIOqB8ytzte%&%t@2!OM$WLnRZQP;AWg zyC1(MpnjkGb*eK8j3-63@woTEW!0BP$7*_tJfNU*D)ha<14n7(1r%3+&i4D#A}7Wo zs1i^&r{;sKU9E&lRmLkeWplOy+zBHLbE7u$yr)i;$HXc4a@DWnZY12~?@vLDszbH< zc=k7AX?{VdOLe@YHLA<<_t^|$va_2dhI|xbdpNfgEex9?OeJ5blHqgQ1~2{ z?(9tTxmuO~{g&DVE8kJJ#3)1>X{I=VPi1&_vp%(j3By0v{*SB=4~4TwRMPK9sziOCTBj-auH~wuSy->5c#qQ6PoPcUl!uyf+Tj ztk1y7sRK+Byw;1n4&ixqYu$aCvU2j+V7w&QU9#k*&3vl$6yDVQENKef^SLKAdHHAN zrn_8+e$fWos;2?l$w8u~d3rx#4-}nQQ_Njb7NUWX>E;`oiUPC{yMk9rfb(>2< z2)l)R7@??#!mB~Hm6~~#DqW7HxpCKsoX9D{#50ZWy>ThY*?MuE*Kk-erqY7v2YTj{bSz1^Z zr^%cUE5R)}LgtHRag29SDQ-##ZJ3JnTXR6fv2FRjr*YG>9hl81Fb{rLD$;H+J+K(l7fqJVrX0tf`zJ~g1L&xo$w41gO$03?@OAU4}8uG5* zz-Fyg?gknF354)$txA2Hrucr66p>vDHq-nYe?XYJs|%HV+zQ`mkT6NwS51@l#}G%v zoOC=QC;G0THMv6s6xiLx*ku9*l8!n*l{wBe4IJm@C4b1#peX;5Pj7H#Dh8rer>hy! zEO`gd+g~a09r~vsrhQR;&Qg>$bhCP0yKWiiu_D<%f72oYiZVTVU~_?*>HT&9nxt-d zX_FV}5la{L%VQ69&J@XA6t*zUao}f$Y0w?%80I<+YeWB z)JK60o8w#dLS8YW{=VP6oXPQa&5iKP>_w+0mg>g%PICn1wp*&5N68jmONQ=WWQiMD z57~vb!3YV@orh9bqa72du$uym=n6f{Nkn}IIK4(5oflK?coSos6iG_2JDKQ5lkxl<-eSdXH?jXP6!GcxQzh zBX|9W(A?kzG7~RZmM9jR_B|VlfGU;>+rsc`r7iYc3ma+kkSwesD7)1ATW7{V00mi) z=%Bk0$2D_S;jxQ0OoDD(+%ORW#7!{XC~*X;;;RQ=&FsET)+x0&^c-$nQ|l}&mWR?* zMfJG$DqV=IYiOE>aN&Ke$Hj$*D6=k8JRE+r#19zlhGB!;K>8@*OSp>+K|cIq*Q}#q z4h9BrGWIx>qN4S$PU=w)C)T0t3nbQ1V*`5!HNAu%Lg{f3ryy$;&zh@aZ+PvJ`$@VY z$qOwY$z0fZpL`cTXBcU;#88X=w#*6OdvEb}0M&QKJkPwg#rIdC43O@h-_;b|aaNJK z-TSj>wk|gF)}haFNc=8+>(VX6KjE9=JZ1qU_V)# zfY9I|_>)0JNl6*skm)Th&be;k3P!8OPnB4WS4x}EV6S4h;5CXI*9o2a>s(M8XrdqP zeJ108iE&H3omN>aoDzt_Z<^TMlHKS*xI7v=tTWiDH)`*eJnkbWi$R4%NRzWmwi`U@ zdv{Q8AQub%LX#)0?_v^`k^3!xh zmPiIE`@Nd>n%vip<|pNmLze?O-2)ngjl>pxl`t4Xx3BeBTd1Y{2RWKz^h2vf%UADJ zJQFA>f#lx;tj^8MU$0}j?6(ZjWxwvsGNXXQ(3!Z0pV?rW_bixl%16!MLG45k8bC0< z6?!lf5$o_>^@@v~soZauAIVGjJL!u&zPoVRVH_4ZcGKDmQj}*_|Cz zrw11~w%rMK4px#Z8r~|yHJ{=q(-+yJCrdJ}e&X-)d33HL6NA_~C*CfW89yD)^T+Gz zu?tZW>TA8EA3;U zRi*(YNH2G905DF5y^AUmZnAFqJG^~_@b#9hLEy(q&&S-KUHK%ZOTZ753tWKyckj`C zs7GeFV{IOvmWkJI_G6?Y0TzOf+5PEkxCv$xS@!+2yD8OtWav%A58bZdzt##rv0W+l-dfly1877p)sviY18S4;e8c*D@DKh zuE4Oo4;4CAX3ZYvQL=gks$b2>67Y7*K$$ zO=tGBA&%jt*BuZxUWY(~0sMj|d)0MPbvHurK6?`Uk0Dlmh$H%VjKfSyx+8jHg5YTt zSKp7@63jElB51_{S8l>;G0mTK)pOX}V-vi1_$UuGUTOUHSbUw0Sh*&LV=sl#ov5BRKxIqWXmeUjv+gL*rihGAQt?0Yn zy}N1o)WmN<^^Wg+vBuSCn%jl01Py!znFyQ%oR_|NfgQElqPj@~x1)O2|S(hZkc} z_Lx#08YSt}Tao*rh)=3rkL!=s%@t0*_|J{)lD=% zKoJX8HV5%-b|5EUB_NBj#}Xux>kkn@%0YzimFfr6vsapq+tFPnxIQnqb9&I0uzv_9 zi!UR)dsZ)LWvnbKAB9ewds@(AF?V?b>WGrJOf>y0Os$te*mF&O{Z652r2R1qXXav{ z5B($C7+W&B=^90;e~@E2b6z@=P?3VIr;CHbO28cbI6)Vu@kZMRlpW_h92%Nsb=s9L z44m}6^^%AvUH*RntDs6+LpJzv3Gz6HCnC2Y-%wcx3h2!40sM=?@9BaP@KW6hV3#_C zUu*WU67}ZDxr;{>e5B{psHcj}SNV~*a%o-7v&ypk0$oB=r8`dk?=6dW>$@E$_g}i} zfqZ9++s^Mb4J}c7{uct8*wx4P(&Z^ggLoTU%_5Z2#n3?jA~!yw*Ka*2K^D4$#Wtg^ z+*OuXR=WO`6!1TlgFN<1Q6;uzDBwzMBU{_(US{l}YpF;Xzgb$s@j?+g!Bg-ZG$uEb z#LdLKpMOkPv1hVhg82|Jm#&c{U_PEQ58yol)OxXzF-uO1?peZ?DxS?y8CmSohb@_YRz0k)EOi2ppE(7OkcyqpXnS+l=Pi;4Y?F zcqD7xf7+eHdcbJi@AAg>a&Ur`CffT0xH}D$#NgLA3cA+OaEirKRDGF01#%EKD%;J{ z6vam)$jiogD+-iYeiJ_I7C%uk&JOaj)rsH%DhRd~GB4{E5aN9Ffh5-YYOa3<>{#7TwO(8uq8Sx{5Xbv`&?NiZq-aTu4)X`lFNh;D z<>>BjJA{p+JW}=!{Xsa_yY~ClZ^Ky&v-%I^1F+(feLSzq`TZfT&&peQKTZU+_J<;O z@YPt#CLDMmI;~a&US8(5ukxC_*Pu`G8GvI;P@=@NKQi|Pqv}9p@^T)tjUo8Rb<)>!wfzb=)?U>k zLYG(qPniCI&um>eKQ+GiP3I2)*H!0c1u<`*!4cUjyf1l7o5q`Pq+gSbN(Uh9}_%kQ?a+t5yVQyP{9peu`7bp!7rbM?F zewf1<_Pf3vq^?C2Z=P!wWRVUhZ)i#&i)|*+ZaC7~%gbvao{Dt)+JTg!LucaR(O|e| z9W#Kli8jsjk+ru>vfPZ_2>6Q)kJ9XVn8>3ly^}fx`~rp3X%CJXXT2Flh;*zNQh+eI z!$P7hKDv0+ zJj7e26f0JB6srM?f6cbUcCy2S=D21#0bV;zCcDtw8uc0O4>M|6-<>55#?A zutLg;{s~IoK~k!nIcewd6NSc{QWOQm^uUpsJmrk%oG;5I~ zw{})L>+>_s`Nf1*z@~CdtIlc2 z?08{}OM1^IlYg`r^t;>1N8@_VOzK82Hjj4DnmgQ|pDeOpo@}}vs1`dck|~G~-}JX^NLBDO_!F{TYoSLI(Dg`QeoTf5?o#GNQ!5r$ztI1~ znKd_j;>2&Z+iI5evf7f7Xs1GS?)JSF$O*Lf-w*jpvNmf7%rCj$BktZ{H}MMI$Jxeu zh#Mk=?pCy$2|y*Y=PH0F*&(mHnlxGoEBO-v65k!dl2{dwmX{bOW7E0KCY^h2tq8Vbv77_iqdn~;X}vFl}b5(L188 z+456WJG$?Knj;-N^5{Fd!qii2UE*^=gY%gfd+dAcaVdJ{X$u!$cNiiX=Lqf!?AYr$ zQ}em@8}b=!#$CInyUk%sW_9H(S~1W>O&=tT9%D7q7qU0ZWVVv-#2>6ui|^s^WM1X> z9kOIC1$wGLzfBYSem5VPm;{m0Tp~=;Wsgf{?XRW|5rVQHo}~Ryx(Oa)8N6EZ;U%i1 zGSiZ!4W5Wkwkw-HXlwT*9O&9Uy7}q`({@Dl@i6lpYoTvymz4ApCF_MgDDXa|Q2A>N zvBcOrnkCb0>NVyT1b%p4)X4C5E#N`U79}TqYn1h1Ft66$43suoF$RT8&#vR0oK3x`7h)=aVEZ4#6p1zpgI)|Dd z>sFsA-8h!?@1EF;Tv09crWC>GdWY?TRpzkGR6OLRS=(6ioKFrHc;2-rf(qW*%XoLX zQ9~-*NoPXs)qCsIKvJ$<)AKO~)&Hm8jA^@S!9z*5D1qe?NC5D@u7*M>DK#vQE)7o! z!PwnC#rxy(3vrt+{Fq*btdo5(o)3}j~ia4hrP z2Q$v1D(j9(uJFrLqfwCaX4!8ziPK{>>VY+}s(K3EVdrB}65~-`lIBY{*(DcgGqj*g z2jATqEmiIM-bj+vw9ahFQA}}!ri$Irb1%Vv6 zSQ^~t>b6DJBJH{{6rTk-@1?WB%=is9*-Dw0qOKL76eHZ5Rqs7E@w_Kx7P&bZI5Ct- z$z=uTvQT2ra%dkS(pYCP*->kycZfk_RV0>;k=(c^719>;If9O9p%os5hYKrhv(!~5 z*D-<_CZc+Y`}ZKFbiw(A!0_tbdxg~qxnTp{x@^vp#mJ<>=17IByDQ9FPi}0Xw+TvD zM)08V(e$Q7)d_}oumhaRN`=Dru=e&fv7y*1>j%uY4Alri?|icSK)ofp9opiCZ`r(f z-Jj#Z#s}FUkRWFCmtVi}iCp#d7jAh1(1olB7eowesFqzWoOte)ygFiuS^ZBJ zg>X-t#J6I5=wDpM33fQ*4r};bWR~rjR@SZGSsTEp#2R=r{rY4#uisu(nA4n2kc7 zG1I*39oY9#sZ_Qhk+EayOdCs00u7XjfrI^LW1P-&w;fl4}O5ODgN-Jv?s-(9$w%B=} zC2b6wnd8|_12Kbzlzr&ihnm#ec|~MF#AnAC`x>`Xy^?T|<-LkwlDq!LZ&nvvYUS4Z z8XT|Wr9}ra_b~p6bw^cb{~-cGMFc4ss+J#pdO^dTjlN@YZfZ6ga)wu>4G-Hu3TB~5 zm~HEgt^@{|@Jqyw?N=9JZ%j1Tn8cqf zJWJw3n=-8(n<5h$JdJoB7;+wzOPpVsRvK-63;JQ22KA!*(-D4Lk5;!alrKihK@L-@ znGwpUzkHjwA;2Ph045b&+*KtPC>QhDshC8WxobXsvp5e`3Xh^pQMZK+f5Dggi5Mi7 z0AmwU&CzDf#U3Bbxv}CfhUop}lG4b83g;VN*+ z+C>kX5Wa(%f4LKkI-<9|o_xfiD#dAuK#9`+(wyxt<9PCxEp5Ho;5s*}|2Tl!@ti?7c1>H}h8s?T2JV&ap>2oB{K6iwQCKD6+X4pg zXc)%b`(qn_+Sg2wG`f5~Wze%{t>u^bVxSTu-A$2WN>6IG2+43)(*r_|Tpenzg(hBk=vyR;T7v`+#FDj`i|?h+w_o!T}D|u`%JBoH-pGGiS5%ae>=bEu$r`=v$ZG}sN&@XBw zZOc)!OxF7ckuZV4SyPkrtJcV_BrI{XQEdz4*66KpNE?*ldy0#lfR1zd)TP{)p%j5z z8;1k6Rd18-_cSyosX9v%@JT;PbEG7P49kwflT2%{-?d2cY(NTQ@u?)p9Q4LI(st-K z8(glNA$;fX&32v|wv}KPPQw6YRGzh@BdLpu>UTOmqcBM_#o~3^NNhBJO5{9Gtj8G; z+g%rzpPQ-0bvF5QAtiFBgtZxAxE9+LmAM3t^?SsRCAI$fB3Snh0IZ$2>S&vOhckWz zN|Glb&wkZS*+UR9k?mQ@6|xZ>L$h@>u}>_IA(^rQ;X$butVbj+PAT3WVEV*TMoN@9 z#&Nj%QBB%cVK%lqZEIUlWrxdX-Qxrwdg7(IQz0wzzeDd@&D=?O4Yyz=9rNC-Fbk(; zMo44)>o$={X0sY35JA@nALiIe-Mt}9N$IHb75*z(6x zdsRhrCAN*L?K0lfL8#cqWLVoSr?B;_xBBg^(KtULj0N;lSGJ9bzHN++LQ8XOJh$B7 z0#>uw=SX=_oQK8*MY@Br_SF@+Aw>WbPys~%6i@+002ELGRd20D#7q^oF;X`QY1j?} zP}9wnW;rJFRGvsZE6*?Qp|iPc+%$k5LXMu*bG?kCB$?Ofo+d$J%ciR!VapL(R~DBS zQLW2L&&Mn6N);MbDsZ~GIO!ucy0Xia8P8tT(D-uUFIstG0x7e#T=X>CVy3jQ)!11i zf=I7#5w|bhTnglTQ$C}i>9gM1T_anA7D;l>642riK!Di}7sHC}t zW7x8d$Gv9a#_Cxax3dOKsxyxDTGr#s1$ZOe)pR0G?6Jg%N_?mDsANp9azHejG)qf` zn2&1iIe$+|mOuthIHBAxpsO+MPv3AyOjOIrIf)|E?dO#hfKi@FHQhYA zW}l(JAS!0Y9d~EzT2sGMIYl&b`mU=IUV(08*jH#!O-tdu9j~vPNF7y1;>B~tU&Qoc zwU(ztsY5QBVVN!bwT}!OkG+b@@eSkZ5=PPx%uhReQd<>wb0X^EH{WW?ccIF$tfy>B z!B2G==mr}>59-o9?{c14rnKLVcMI7x3kE| z*@}-#x)JLT00<|7am5QqamwhmsU7;Hk+3_Ji5c{!NhC6|jfHlc8g5q_-%yqYicArO zJdD)^nGu-+k^$-}3VJd(QsJ9xZR<=jp^PEqgGw(_C2^sO##DAW6P%r@7A%%PNxxc8>C+?LLZOw_HTd&_Cq zD#*Vlr%L8wDH_~GFy|zx9+hofXiRT=lqO<${_g;8@VKAu2Cgs2q>g5rbydIxuF5W>+aUK z*_9QdZkRvLynn_v5=E#3ZllVO{J?f5v~J`^Hzx3<*Ohwqj1&Md!S7zFr}@!o@;hL} zs7YdLHDv5txy5+Atdj?}3aZf(bI0N;=YpY2yBL-3Jf)0h2r@~kmYRfhx!xv$6{VEd z6V19t0HI@ydsJGsmv4WgNfO9~oj3)7=BYN(GHYghv*MjLeKS{h%?ASFR?4?=qi)0dl8JL=+d6YRK`%92#` z6k%9%nxG-Lk=-^w%g4*pFo2Fo8Sl+W zJf2`(x#O?3CdJakwrHM9VEn_A#XoSsVaUf6wH4ALk-;4s@HF!_QDJx58vp}O(3|V2 zuVbV%*U+qSw2TKN4u-v-!=4+C!$+RuQFZerL$n?ZZ%#c>uV!Q1-B{|Ekm=WcU^2($ zs8OAyR}Gr(w6Dud3Tc{hbuC zE*(Gs`qp$rq7KHc!rNj=Bn-J7dx}K)U5savF@sCcYgcjlu71UBB0nHwVI69&T^%MX zwYr0vYfzQmh5O5-n)O(=9H=?RT64tLHxRT#49o$f!4c{gQvIXLARjE_IqE8`PK_($ z1GyA1yKH{)P7461jw-p0Vhtm71EH+a*Fv1S5f?11!FmkwRaws5j`epLqocByXrxsj zj-+*}(`}08MeUkPdy=H34N0P0I5`~CX4sPn4hd7!)~j$<+*uh}ixP3$J*pEnSYRGF zrn?E~hRDO5b57Fa2I0XZ0<>ST5^baI+AE|2iU25}0+7%JS#FdtP{WgeYCG7))uf9e zoT$iQR2wpPGLV?z# zwYrYv6A=YRLOa%OHq^VKNhL`C0M;F=>Uvip;(rqTor+oh@DA!}#dj2)wlKBLV%^}k zlK%h+41fSZUYNoW*e(6Srnh8%1q+W5+sfjDftFC6pYf9D-?c zb4tRDn$?!|e~ec5Q;4-ocJh?&M2wMx?_6G?+s zb=YvgiqZ1DiC0>p2|{_TkOJeTD;ey(+sAOqu%192YAk8Z)t0TIjuC{&ZloHLZKyC< zf_cYkK@vDJ-*^o6BCO^(hwpk+Noq+1*Bh}67z)ReOa7^XwtTf1$f0&Kmg8i)M_rL{ z0qcsPGnJS!f><^(d8U^x<;CAas_>VLZnRs^Iy-RkAC$C%BE3r2#Sm&y#d)Y*#*s$5 zjC=dnT{)(#jHP?n=CxgZ#XMfRg{{nDJZFQzHIL%I7e}nzNg0sr1LR=5Q72_)%^}$> z&U8A&o?;#C?kXR&F>TGy*R^uAn<=GxvRLK?!b!kk#wueNLU|ODu};>xT2xgemc}{b zsHWT}nQi{CgN{2?x2RvCr)4+WJU=Gy^^h=JR%yT|2C3>M4#~5oThgIPnPl|`r?oW6 zv1aj{*&}6lA?Q2PBOhysg>RV$dZ_4I+Q_%B1s2vaj1AH4I2h)s=+Zd7LL8mE<25(d zz~!J^NM({oKPfGonyTT!+`l){saXqK5|%aqCHNS>#er7|%6HXiKpBX-QzY2hyE595Wt!Q@x2# zu?&(i0I&sp0jH?Sg1tM^yD6X&`WsD(K1#`guwb5zwt;L*e-xwp_w4TI4SmgElfu-I$$DFy`a5`3Oy0bm) zx0ixhaDD4bCmXXZd8~~Vm`P)ECIEfdz^$JR1dhYaY=#*GR&v_LiR*Jl?IOIhaVrrC z9YuNEx-pMYWZLF8J5Z6uOnVpRa9%gkFCn{oNTq9lgK+@xwdZdmNc)+KY;H5!vZ*$f zgMv-AZ)uQRU&P;Jx+ti5W7yY2qg>xWCWc6!?m)QS4hi}S&Jyl9Byrw1g|GDldpl;1 zV5B@)=j&Xhp_bhF?GX{F!6b3`Rm#_}v})x$+k~EGU4t3OYOd;;h{-iEb=M;RTPrE6$fd+J&`PMAqDcPZ#GS=6)G zud;MDDdj*Wbp~V@3!V)*^;>v>hwSR1xnr7fN+c;hh7PB3XE;M{hjSmBJ*ze`1eXe! z0C(fHIoqHo)t7EQ$P|=OxO1AK_ZHKV88ATL*FD%p>tePDC(GmxL8|gd#D4LMdsDsl z2cseq;gl+Y>T%MRdEa2pGJSn1=tHx2BDP{AT(0E@HLfkg$>xj*&p5?9+^bxg&O1Ou zPFRfOk6P$-jXOw~Gf8aZNZV8wI2Ei@T}I@R)Xvr|trFr0;xO zijB_bg7w?do`#Cs8afPR?e5SL02_$)swgtY833O2HWq2>EZ;71$)&Vq7a4E6k4g}l z)VpsQ&XSyi8uG)vRkZ>Oi5ojmu2j{lUgLIaLfenDX!~F0^56N@eLP1M#^f0xPZbYF zD6WGZ)<#(zFf4zDt|gnge&_(9a8G*8)HQIv#W|z^ljlySf@!O6 z5!{72OrDh>m<|Gr3a0x4mb#63Ks;dmDHcX|M&ocf6kLX{x%agd)B!~R6i@+8UhWoi-aYKXI8&YOa4U4V(VoolkBA=@ZZEH8w$UPu;`^;T zbSK)K@VmzG%XR&qt&nCXFv-{tO1jaEER)pAjY&CkOJmq{>7l!ZNdW>9_`vV)T^^WV zkjT-n*f3iiD-M@8>T487k8S}^N~wD_swsv^Qa4~ioN_94WgboOF3qfT*~X6EW6VE! zE`Dn9E11o_ppxS#54H)SufmfugGx@t-w4fb0?#$mxJ}qZCmf$z^h+&BXje&i(6ozk zq9}>%$Dyux!&79EcSgEsKk$)j(=D19t|Nozaz0(h*0_&|9t6}hdmDQTso`5`ro>a@ z0JaIits@S06LyQ{c0988PFp2-$pS<;VozVCV@bL|SwSG20a?#SsOhE3nkTo>?-@e} zRR@x-)Kz(O>&1|rnFG?6jg>gulG{!W>*r176YbKppFp@%`@5Lr9YG6<$=%4Lv?pyP zgb@T}ifsJMPIFgnw7C4mYm1a&-=1m8ZLV07lvbvd?VKV6`!s-{?Vhz&9IQnE=e0ss z(T0~Q-c7w39aVV9Em*eD$s=w_m2i92#aI=$G<-p3!%MPvpUjn;4o9VRUk+~Vn?s#$ zoTTXI2+e|4wd9vl?5&|=Rn)C6E<;*K%eR8Kz%`e4xMg73-O$!^m9#Up^&o}@Af7UM zd(?5qwp_})Tc88drLnis5|s=J_CKX(7}hp@{otKlIhPw|9YPLF&W0F_B%91E-oSnn+scm5M zS+c+?_4TCPkSB8PndEP{jxgMzCnv3Bp%NxB&uWD3h-%ESroI;wNcwaZRU!0b2&m6u6-z# z>?0~8&*#NPy$jK;Oq-@zWGtf{h^Vd=Kms$nHBnbN-mc@~F^D{{xF@R<>E5)D zW;bqy%`PD{ueT!$)~np3KpS!Be>zdJb_TkllFR$AT=uBLat=?@t?0-eWKG5iZjXQ?cQ zFdecERCYQRxdyRrL^*|{7;sNRS`tBL^35tncJyZEmZc}9&oA+>h9uQ>DKE5LOd{hA zZJrO(yobixX0bKMy1dfliptpWwEgUNs+Yd3%;RV)El7MkH2q>Z1fRRQJSy}63g~t1 zTu)&H7K)-fDHcI1Hg_`i0=eT0y?PqE^4WDgci_gLHBS#udni$LG`Wxg(;X|a*7SP~ zZpE!NJ9M4~?8Bk;9)`49Y$-)G%Jn{K_^I%VQ`04FLrR&htXCt+KYR-E6t%oYK3qg? zI3SO^ipsQ-)sBTeMQfI=-sgK8kV|Kd)o0H!M$FBc!cy#N%BJQyitdl)&PlENZD|%H zg&>JzQH-8VG}2m`&iw~nNo)XU(d1_2p2D+O!@QMH3F(@K(|1C@kpQnk@)NqeZ9 z#Tr!c*;~jXF)V!J@T=bobVIL8aTL(5_=#f7agZxmrc!BaTJbbiHjdG0*78RTb$XZrL4vmxPo|QVw=0wkroE~Zrlj<@>EgvL~{ zjmv*ZPUBCx9rVFL1QN%A?NuU+&cM$-Ni}vU+UBO5v0Oir=&_XKkq*%WrFZpi7OPV&@Zpb4~es6=fEFKT?Z zJe+r^T5NVyf|9YNZ*Z*|5%Rxv_o(1~m!vZ1fAJbOJf}4YSe4`(nH9eg+N;eY0f0$v z)FcW+;hS*J;A+C)u6++mrE-;+o==-D;3VYg=hW~Zjo^Sxmmp7xIWzV>sB=x ze2b}9kV(p&qz0l(m0gWNy-HY@&n&?0VMP9NE>Vgh9FS^v(9X`xx1h8_^c0gWRH<5| z)}?Qb$0bquT3nW6PUydDwkyPPMh^f};7=xNzy<~g6kKT}tYS@XAYv9gr>LZ0NWokj zQ%x{KBaN0|`;HG^dbJ{9cPQu3R5pSuQLwCT518zxnaIRoifnx3h* z3kg#p;~-M4Rn3{uq06z7U5PI2S%$Ecd6<|9MQ4VTEi>bt1Eu+ z07DA&Z-#z4e-6zprm*jE6!-}v{{YKBZ>3x+C!xhwpR<+w9VN!0sM~mk3!!|AXDfr{ z!g2DG-o0Ai#p!iyE$zZH0k<8EHtz0dp=EUUJj2J*>biB@cacmZE65S_{A#pc6klG` zUrp5Q;dvNs+`Mz|M>^_CjFLR^IhNB@o;hN|mLzRB;;c+y2O|GvW`1yG9%;Bg-wzMH>LVz)VdR5C?qEXV_LjW>K>L@AgV#TbiA>Q-2L&w&vLwRp2BFc)Y zPB&(U9W)_bXk3_E`AOxz18@j6T4-4jNF0BA)`)4iUfPq{3z*TGNj^c)dQ`E<_V;n! z+NmxNIOdvmcQmprU+S~!c6SlaG8p=*w^3Gp8(QDoc)lntBna&j5CagW1HEa_cVrQ# z?w$VtiL^WYR?=9W);2JV?zbHca+X>R^V&L2k-G-qlgJe^eJoN+$=w|N)Nw;IAWjGK zsuIY{kbr?%S)yem(Oy~0LQ!0m>&-IVpo`{YV0~(v5?9z;=+VSwNyClDHC`EFw>T$p zKAkAPa+hF0?=wn^f;*bNE@FVI%RUQrSkGE%np3BJSv`f+SJS$zPIiyIAG{gG*F5)#Sft{9(s-c+MbHj$iaRH-cw zExf#)E{l_$+~9N+l1|sbTO~GdR30md>U314$gWSJ{_9zRlhgxSnyVxloUyh&c&6Ug zIhEV7RLZeDZgN!eD`xRyihJ%s4G3U;@kcG#*7iBIx!iC!1sz6d)ktW4V|?5swPMcds3>V7E>cu$#W{ z>(-o|(41O#xx1_g5#U%eA#w9LJk{MROjecNY@N6vbJnq)*2SlFsjFwGEsf*J42C9% z?u}FUO)rUWul!XVq?bXSP~a;yTq-!|XC=<8dLM-J%^OD3P1db>1lKY0z~C_UrZToOWw%MCbv3;(_ojw!bOFxBaUcGHITPr~E~^(CyIPr6n4~>_%YHTl|~87u#rJ*YgLEM}xsWkazj7cHiz+=rhvu7m)%9j(5 zB(y}aCxhupX71#n$tM6B&9~6v=J&46NUaQASwZWHl3P+^^P_O;y}QtxdW7vcXj{|m zZ?z@~W2fBQ9DmC+;8$g${3r2NsRK0dTj}b0dB-h}Tvp1R)ym~bUT0~cd?3*59x>uQ zS?{L=VkIIq`eAuB-^mAsJR52yHoAR+0hMiv82%@k)-+eCjY^T-XpbN9m&J`|QNEH7 z4>-26Q-LPjm-<%|e)k%ith$`iO&}RNnB(%Uk7%H|QO>U^U!k3Us9M0DRk|URaBZDy?qiKZU!B$C&7=3&gQ#WQy)4jDn$t>{j24 z*e0g3Du1m-A-#C2ZCLZ0ac<^ytb*p&c!?%dF>cjm zy4;{DoMcwo+`|fph9BkO{U}ShSJvpry1A9)IbuarEE}rfznwU%XhfaQ&>AbyG73UK z87BZx2bp|E_^YS*aT?Q2+jprGjiCIg?rY}n6ZnTw*Yy^=)ovbFCm{=E5PAV#gdnI> z=6kcxsa4gf%C<)`*R;uz_H?OS?omh+AB z4UuDm(AOm8bE$i}mgbTO2bXLC!TE{rRqiZ4*%^`%k_RG`Q%dX<`J1_gGzAXF7$DVW zM+9Rz2O_mnRt34y*f!`86=3-Eu8YI4G#Z>2QmjTe+axf_toh93tt+FjxxBNz^E}2m zAdREau&sW@XLAyyAnrK98L4t@=q_t+Xljzc3?e9#BdH^$QivHx(Ci(X`!r!55|}kb)QhYl`@PqCL-zHLWrUz=d-) zuqTbA@ml*Q6q^(!89SLj6k@wi6KVF>BrVOTIVw3htlcZe8sCNGxwg|S?O02OEi(MT z`qPw>V(FoQJ&oSEJ;tGMiqpv3q-e*=NvVVfOWVJ1+Y<$1$5HQCUzxczpw^DaZmF(a zY64iVqeyM`5~!n+YE3`G`iy!ccaLShNn;F{IVin(A4(r8S``~DBxOS@m1HMiLP7O4 zW& zh4j#t85xn4%My6V?MVzV+c|yxK^*s|Bpoh@fr?pw)s)W!ib)`bJd~B2C!wP6psl8) zva>#u<(SR^Ay2I>m8i6Z9waS0u1-dHsgrFN6kc6A>hd5+^0D8hX|mWPc{cvIChEAsikCYfGGSnw!wG4ZARGu26IWuEQGJBR=uF zA9l08x(%eFO{8hJ8pf|6YkH!aJD=8IodOKY`-9 zp-nY*j8nF#?5(YB?Nep65kkI^pi%l!brVk$MzXX)h6^WO!nP#Hx$y0|vX(nHYkQZ+ z`cK|`=dMm`$i6daT6U9VYYc*G?Mh?2&$*Df9^ey!_M%N9YX|)lKv#NM-n&wDu z@DIz3)LLcJO{_*2<{`QEt4iAuP7YE#dz()wH@F2@XE^U!L=z{W&IN9@)sr~c@}Xpb z;2Du{Nx(RyL>^l)907u8aZFLv>U=xk-w|pMCB~O!G*Lb_$(^8&TI=mT3Tjuk9)7W` zssq+IkR1Lra<;5$xgd0Qp96d&8!VTRTwTgJ%#xe~>@&r6x-WsWEf-LV_QKT1bsB70 z3zByFa%*_W#yq#s$;$lzw zU>sn66%IEXWy_(6-v+gfE(cu;UQ!t3M>Kp7#8wsO!VehDboWN*PkGw_{XaUK_RFPj_evTiQh;FF`JMpU%Ah096|Pi+v`Wd2==rtn%bj8-jO8YsCT@oGOYg?Ho*L8+h%D`8k* z)TMoi_(|kdA1ND;O3j@V$?^;kIRck0h0u;UofLowCydm?J3>ym9Y$)M+7xel6s}%W zm9Rf{I&o3SamaN41#j z_pF<-E$pt4!p^zns(Gw*RBM-%!?}uEd3?K=+&TN9yVaXp30;)PyA$56yQ7Z9hLR{3 zY-Efcxu|suD~orDrw$^JWeM&nTE=(odmq5>0Kuur7PEaUS2jt?Lc=(3TKXjsQ9`1H zQP2U3?TTqxlgh7jI*MRJd2o-?uWKB*PWvNU$K!I546 z0FT^JWoeQqVThyG7=+G3efv z%WC#=!*tH>(y8diu~OGUxszsFN1U8;YR;8IJmRffZP}JcR%XLvHJ>%OneyO<>56S!HztZT zfD9kI+|ctYPmkgC?MbU_O@>MXxn=EBY>reT8(4G2Q@dITJ2UBszAQ?r-)zyPh_756 z)^@A$-%PcMlG@GWy6lADX1$oJ9^`*iKbIt5NAUWpm|Z4@%peC1a5%C#e~R zTXVK}${$1Ct1}Y$0~JxUmCLn@77)tw?tqM`=O(vg$jRG>Ba@D`&s%cSx*utlR&IcC zlUDE*1F<~xtU$`XxB@pPgPd_v_%a6l-OFUVH#w_{vbm)NW_m4*;O@7$IavY0Jp)#5 zpcdX=Bn-=sn32VA6q?Y>5Uk44Ok-A4mdMA=&MGn;k&rhBsO?p;mF$nAehYY|HO&Cn zN)<0GFpQvME7*$kYS34{p+x``Py<_VIT*)!sTQFVIZ0LFe@O;7rj^5D;2?~G2=x_< z;!g}|9wW7q9ahdMOQAt7FzP*%)xH^OejnCMx*n>w`Y8kVa=9+e>GY~+ z;~uYLsKhx$!&9W84WW2?MJxn zRs3J@-@>|9hYil1E#gRG-mDyuezbcSrOnW95r1)RXGh^*4O&=OL#kcgeWLV*idact zN$tmapHTQ;JY+VI&2<1$U7c|2?TY4fY_A*Caol-5>Q6MIj*3)NHns%%#GcYqGdOt(W#lO} zc4v8^F^BTmM=YEXO4oWD$@{iq>7E?C)n9%jx!N!?dHgG>)BFTs(q-0sK=WE@kV&>! zoE_wMKDAMU=TR``gcYRESMb-v?*!cVWZdZXk=;dbh2yw?nf-_-rD1q#J3ky;UHETZ z)1qj!Rc4lb z%&LgT)~ejtu!WiKkYsW*kx9D~R;EL$-mk-k{iWz>D|TGWDH_?H%Jh6fm;_Bg5~85LgOHU(}2nnn&<6^JTIs6P3mi*7%7Shwj^OKt8s0~Wy_ja{)HypVy9l#@>7ZJ|=e zT#1HBUH$u2_!d51LC;w#7qUpU@8rOLTH~&6gj<-C zwYlBg*QY1Sta;H*%b3Qz)fwMxO^|w%S@oHP;YMZ<7lh0UiW3#?60CB)k>)A>?-Q&6EO1B5soqq8cAqn ze#ENpCItC^OpcX_s$1;3EUIwBCytbdEmSa%<|D2vL8nb&;xgrQ(zDQ>!uoFz_`<_h*CDsG)2$Tw5xasz_F`*OT<~U( z;y)JJ>XP|y9^JT&ffC?%#b;KUdM%2Jgr1Dd@i)UAD(}S_Rm3H2ZB@Q_mw@DUt=(I| zx_+rBOKaKSw?VR4V|=UQpdzN$)Tl02@2$@ik4)EY^}FpeO}j>qOurinCny2yUbUup zj^-6>ZCWq1UMzT7Ex|rv?Vn2D+-}KOgr{b{;}2i3(seyP`a4SrFLj-{l3REABXjTS zE0@u}FKRv?)PBd}YdJ3%{vsOn#lRO1 z`dZmZ#oolO!eYseXu z)B%;pt!C(U8a}Dvn~N(ub1t;Rx049m<$43hKyh6Z=tet|>NO8{&PqQL^Ayky4fSIOLs4Xhm~; z&i?@D?tH$vs;@DaeCh{$6-ilY7v|L&<&`FoDBzk@X<9Zs z?WbpJl_6QY)mCr4OmLFI!HD4T#V4^g1z2K5W+1T?(K@mD4&mPvC)9c+Zlb!2mD*0+ zbUuce_6Lpk`Pt+1s!yvWl`r)biLwUbdau1=+)E|L-eZjByDBm$r7wAqpeo!0&1gj# z+0PZrEsmw8#G42n{`91j`kG5p*2J;M&xHw+JqHH5T{3v$iFd-m@!5E&tD*%Y^ksv4 z461lz?0Bs7iU*mQ%c!XLk&8)4ltUOK`G;xXR8vTR7>~`Krxeth8;fe_mNbqib|)v+ zqA*DLDyxp%(^9_VuQDkXNRK$rv8dW|3lL*9XdvW+f53) zmys(lA1~`zo*MC1qv0(M{_je)wriJhwkb&*cfhWQPCT}j#$1t%xud!2Uk|)NsQA{- z8FjxV&Ra&@>W2lJ{42fG+se@H+Q5dB%3&E(oVger{c9)Wv@LDSc~`=%GH(WHdac%` z#$8WQxY+=dZE57^r?L zk-vZ=Y2&y<=j8l}@Aa=GxbXGuwySpf>Q88@#LFzGaLw#`Rx{D+R98b5<|wXFj@Mgq z*p0|Pol9e6wvukVxoe~gh+~nm0X^$Dwx_1WEj31_y`r^;gl)A}llSsWRHtq^73{bF z0A_y$cynCTWERp}!mMLN`G_8$=e=zTHg{n4Pn5{F`jd;DHr+1mRYcD! zox>v|X(N$aHyVg(^0E&JZu<>oprM{!9THD=1 zyQ8-<0hT>0mGF%EFN9^c`(5FKQL~)EC};UtXBo$(YYKH9`w=Wn8g5A*@Y;o%*o(%w zg>6XP=9d9+^{*ZAkH-isby+mc2Tzjyh$^>AM#XPh&qVbHKF5S>n*ODz>eF4{-AQwF z#Qf4WM0xDKg0dk=3%s*7`5U?IT-9fN4%|Fkbz`TQVO`F{9+gRMqDS- zZS`gQpS1nRQ{_?1WD3vaj$Ml%Q&A&KDvq`sxg;v_ikXqJ2*IUeqhS=EK)tAaAz3{;B*g6UHv1{tKA zLVob^7nkNl?Z;773{s4dw1My1ixq7yHbrJ!5<#eAERz612oE?NX{3p_F@82ykmM1b zoK=@b7y~AoMrrprd>4F@N%g6si;k7NuFj5C(2_W|zE675iAezCio#mxQd$cj+6c!_ zN`>>sy*TVUXj`&vq~K$Bde+k}405rTETC~&OI8gn%irktuEkKMJNnaQwAr+)fB@$; z3A@)kf}M6<24?&*M#_NKIPIEvF=4#zx-Rtt4n>x7?1ZDJd`&hqIYnnc{8 z_ylLMJX1>Hduq)HT^$FO!IW{4O}cp_w|9m=Hb62E9A=MDI053n7k_MALcSOCEuzQW zlm`84JH#It?L0fG$78N&I<2!vqYvfpT%Orniq*mv_gN5fYV91Si|5n4O|EFoWjqn- z^GvhJ1ZR)|&RllQr`J<`fx9T-t z6W&3m3xX}DXkk=x3y^Sgjo&w9UoCDxzdO*dE8ZYQ{&L$ca3-zvq^82l?(S`N+V zYbpCZmCVls>y~!+^USw0S|!usRoZ^&1NK@dx5Yg>d6d_hC)aW zP&3rkd^)BnB|B(v&8I^yq_@|%a@^`9iFdq&o;~XAr4;nmq7-ij@@z%NON?pr@ zRBo(8tuC#1Z3VUVsdCy}18jEdCJ{%ts#kie$sgJ6`BB42M{2oExaw(5YT6)(Y*CS% z(n~(nAxJJd3gm3;ZFDI~8%*jLuHtylt!wGl=E^sGhm?*-_)&cca?-+FB=VM$LL-ch zgRN349^I?FzHm6=hVN?~6k1XDOs#iN`vQnzAk5>RYUktCE|G!X`3{w=X{jr+D^;`5 z-|+sZW@fjzW@mGiWBjXqZRM4KR6BdtbBjil>MLEBqql|^a~K(}4+~#7=pqvqn)v2Uk0tLM66_yxV`f&q`+}4yWfC6s>buzJyWRNX`%Nlk-wbXu-)`dje_fEp{0Ni@T8L zAFWQdj~V&M+v!fq+KW_JWz1nyfIHL^Z;ilVOL8Qh=N}YV`qb)&JpNUqeGa9fCi3tH z=~m%fMhN73R(9$%hPsyAIL$#SVMgkA=rVgiFgjq>gDxB@j12arC8;*h(6_ln1ZTZW zZqSV3kEKJ0Q#y+8-6LrN4i_2iO_37d4d8G%soSXv=!SFwln#R#&0UTNwg*6Y9<`jL z(P~9&$;zSI)DG33D|x|V1{%y2>G%%IRGY$d85wg@%ZLJS6wbO2YwD0dO zwCkv&WXV)R&$dSbx}AH+vS~gLhQmbE)*H09+a!~9SI0r`Q3=NFJ&jxxW6a*C5AcpH zH^cVntX;FuegiNCau}Yc>t6NXyWIk2mssa{ncN`&_8F;!9_2a4OO$Zw;KNdhV*-z9V1_Y zNc9(QHLh+`s<1h+FG3|PmrZ+bS!PA zghm;dMUx)4k-7Mop_Di17WveNk3( z^AWXu54CjH?P;es+Aymz;~C9m7`tj(T*oVUdoA>*c2Xh8QQn~v58(#_vy`rM#yqj} z31>I~c*k0N5KStETyxx-ixr~O(6-ZK3?`ZPd4D$1Qrz4|PUU6i_;Z6&kvH~|vMS2{ zVs0k~rqX$==3=+VH#b6YMfx4kjiF&0(j%4M4Nge zwu~8#bqEYXoxpS=sk~=*QV8Twk<}&K+0r!wAo6Y)2OIJMFc!xRC(PRwh5vwp1r=Q4a0sM{0E&jEFSNLyrFdXMgakcD^6+CaZKI zve5Mwml-ACWgnTP?5(RQ!AD6ml(diK0F1Y-IpW&cAcLMO8@rp@`k}-Sc{My)+mbO! zuw8)@=gV}dRiRKk(4(K??@;v;(?e3%?1y1RMkl!KM7CDaOvSebKAov+7jh)s^dXwY zHACe)0y`RoqiB_T`r@fc%SJ?`KYmnq_oSGzt|Q=@iv_Q#2bj*gUm59BTL(p9xSv5z zQP7uU$RsYL?$6Ss(bLVoc^Q6KhZ*&&M$}cE&Fdlamtnx^S7iCIo&{SwIuz$BZ@BB_ z;-i`|P^yR3GrQygT-fDod!CcOce|jFCL#QG7|zrjGReuG%Yg`C?Jk zy-jkuAH)eX>$!!@*0(k+<;EBdi&@QbE1AW)PWxE&%Wo9uch+&s4b`?KT&N5)R&=pp zIc8FhGxtp`&CNTAt=`f|z>QF;`A0bPZ zX&3w@;+;QB)Zp-X#b^RGc`ty0!Bh6at43<5@Q1vfjrNRM;Nyg~5? zTD;UQ?2r_+gcLqr13C5jR+Ktth&7R=-D;Nzu|zkdWbmM41dm#$Ei^%AE!i)M{9~iZ zrClbkp*8FYnBHK3<%g#^uL0Jz6MZz&+D2m2xDY_Yl@-TYq0qZY87iAsqYmb#ftTkb zat9vfv(!&xKK=~T2wmO1Azx0bG$VtLJM>ouY?Nu-e(v!2wfi7%M>;;m+Th7WXpf3FpX zw0fIUN*a$boM#+;YA@aA_l5S3pj2+ODb%@aLvJqMvcn=aG7F9kU$)Y|(yChILf(Y; zsJlo}cle2KET*@KVGb~QxI7xHEVeOrk!0z@iluuM6qC9d(C#MzK&p+W%=arN$+rY& zJt|`pwl$Jkkip@rdwus45j*bO0LBGaxw3~)j@Yy)p%|0KI2E#QbB1zKY<2C*#;TBj z*z3(tW`;->SpYy!T1jbf!K>Mli-nFsfB*-kbDCStLNWr)m0O=}i{&lP<->HpM zp0_t(_+#UX_=BA)3w}QAvoG{Dqhs(x#WFZ+o4XhWIZ(y0e=4}sjrF<4?DsE6`#$P( zTnTRcLkjwc;wH9jKWBXuznQ4&mzNFw(ZbfM(fC2nOCzJO_#@!kSb{yAvty?Btyq2( z_&OvCGMPq(?Pqa}s04QAv+jhEw#lPp=c(vw;?pJ0E!eeZ zapa%}k?UH}Oh^t_f;i1YqJmc%(xG$M`_);VMhsJx2a0RV$t5LW!YN%LJ8 z0AQSRO4nnKhJ56JyG}o)NegYkB%Q*kt01|hS(vd@WH-Mg`VvoIV)cx^8^9vHN9jfJ^+wwWac@(xAHZAR!Rkv$4 z!rY+U6P}0KxjTWfXNms$5(pl(v?ZazU0PZd!(ia!HGbA1ARc5(&q90EIoniXmoYWg zn;(yE{ignucXqvJQ6i12v6Gt&5b5%{(&* z;+jC94up-rO4+yZCy1>gNbGfKqaLmERYI-4EsXhjyp0==A9%`3C$#%|7~OW6InU)> zAK332d1t)6mP?oFz~Z!>SiNGbH3;)_JxvP>tBp1M%LfYG95*|v{CfVNCJv=n4Zp7$ z`c_`j>g=gT&gkoO&x-o~l-ppiw~-%)aM&iVN%6B=w|vj1qejCYD}qIAs#B7?2OTVq zLg&V^N+%Zf>?gQw&fb+Io;>j!`e&7QsY@T0!D8};K3_vo?UHsx^0xIVw}~$0wl-dT zl3D%YjqBXid9GDsKok<(;C8M$lICWLGFz3*o@$WfvBkVKMsnjjZx2M`i0?1@=U6*glI#ql}Y~ya@o~PQA zozS1X@Q#w<5Q5M0R7P}n`CBRLS-Xx&qIZ`A0lxuOEp5%>x&iY6$vjfnsK)B((~c&$ ziZy5LjCxbl64^!s4%F@_EpEu}9okMo>Chf3i%Bc63UH&ZH12B{^Js|#gCO!prA0N| zigpA99x+L7Z^*4?IK*QajF2(TO+c#P@a21BHA_WfhH1BSH0_#Ail8@2+3?1Xb+6t@ zV`+dQAj$J|&!t>fHi2i$%zKuB@O#1UXA|mkUEExi8`n4h_pYl;_-o)hDFL_9{PzR? zS_0;`jVn9c*XwhPc_Q>RZhSA`+Y4E4rSSctENWQph*uq@9Vb-PTKh=wouvAC0diGU zzav|6+`@R8i)X3m)_xSwrG`1B&}^ZVfF%rSO8Zu&ri-Roj9TgTv46V|6%8Qgl$F_W zV2OV0NIr@wvQGpf_mamL{{U%#rk7wSU3iB_wK%xdEYSKHmawmWCU`>7H<_+mGcQ$4 zvHn$}r8m$=F{pfW@Vq%&CA+(je|9~f{wBHW@7e_fZT2k&VtNLWRRXm3DST2oBZiwf z`Tjikibnm_g@jR`R%T)PRyFskRwW$IxA&z{;eDCbGM!_@g3P~4ztV_TZ&pb^@y;-8S zL{+nmSY@-vYHijxj8`Ju$+Xc~jt>T|TFAwB6Ow7T+?Uw9ZY3?dc0lc#l5DvGSc3uU z%|ePYl3St_w6*gYiN<-^=~GQKX@CxJF~@2exYWCpT0nSTTA3x-M&ZDrqF1`EG48T9 zudXQx%KFn<5zTg0x&6-8JnrWe8cJjZSL^hvYU<^tnJjS`=ZfAwyMZU#x~Z_Nse^247fRJ(8 zkT#yhsO6RtvPQ*SdJ$YcsckaqHw@FenHzDTJ1aCxf|Jyy$uDVKemSQ{HS9wRaL3S| z)kRPf*{{SBd-M(>GtuAB@y7KNaN$3Hj=W<;icg)oaCXHG-(Y=Ymsy9)- z2$ev`LJeanE1c4Wo~+2dHxU#oh65&~7tQ3UAo-c+jw<5NxjBZNQb^giAqq#zPkM$c zvoordBPThbwu@AB(4OMtK>WsqNybU2pqDc8eo`|<^d+_M^({ek8JTf_I(yPw10sBz zcLSbHLRt{jnMGU6k+KKOdWx|m=@q$l8`*n%RVnMKr!C!zHoEMOGTRFf3C|_D$K_gG zBOQqyJt&(^=uKGiy~e9;IUJt&rI?^>zXP=-!CkEhVv!^q=O^^4ml8U&kd1{rQEP`Q zv0UA=ppAh^3H#luznD@@+qX3W-hWZN(VlDPG& zA_ze3lhhMb*$Q6Gj+8?TOu!6#4r;V_2;(76JLGkxsvDE3sxq}7GAUJqFj6uz&{G4M zup1+e$EmFnR=Jd}otb{Z*j!)P+qAw~OyOP0#s{uz=x9pV`E_o{{XgGi2W-9>*DW?FB59t#RspI7x_H~18y@`;nSW#HlZjwl5tbM#L8AhyIUw@ zEM7?X<8E_8x*#D?6WE$8mg0=MkxTp2gU5a=kk;z8 zN#p5Q-%z@4#5{qWz~+#!C#GuKLu%R*q?yA8+k@9NwIs34YPfET`I?OmD)BQNl~|}^ zPTcY;yUG}hbnBX08AWy;BvpUjVV9>Pr4F%1-eJJ*Jt?~~<0iByJ1p~%N!mMhsn>2W zF~{px^i~F}5jNSf3CBvXcw&1}7bc8k*5*KEM;QYpaZ77&h4UtCC#4Qr3)(AK6(TaT zq5wd^J?b;_%&J@FZUssvI=je{L^iS&eprr!sHT?O20_ogQu8);PeC6ja56o`SazN( zXLpPk{moX51^G^X;$gV<$*9zm%_d}}b}I6Dt5E^cgpRstC38-~M2bShbI=1<=5-J9 zo?f(z%O# z?57*eB9QVB4>+lJ?G1*(1GQ7J*G8R|hHTdpK17aLbJG<(4y>Sz4`OJJ)-J6y^%i+N z!MArG%BrohGmPZt+}5y{A+0rKsjF$GdCXc>2^x-1b5~W-5n{Gbu@Aa&$)xKxtYuPC zld@xEB$6a-=dNm4W)Z~77XAIWT)iuzGwJS6~J;HxF zg|(YB=_4@q6r}7HcF=+K+fe@iD$2*$aZw$AmJ0#)$)W60L~?60oNbA5>7JDv>bI;9 z2&33h7Y?XaSJZ*8Zlpgnq;|lj%Pju@C=)kKQ*B>DG)~B4ZzJY*C+kfSmKb1hOK`pP z5I{4Xpm9%FPI={hJt%I&Z=tIRB}gq2?;;~>`6qFGu)x;7qR z_2(2frHS2WG%du3JNJDm%zMLOzEhl3N%kd0CMDxWi@6`?>CIUc(@AStND|08Q z4aAJ`l6wx_>VQ))03D#7O=++t(6QusNyBdU6&RIOhnh(l>*-BvsO()U-4hEk_%hT09K4(W)Z?*xY?9ZW~5+!nRMQI5l>*$D^{jBHN;}v6`fjGBTy{6^*kn-XwBAgPO>>@s?SZC>tYy zb57S342!=J>NhM;m?IzTj%zipW@j$)voY#TCas}zvsM%B4UQeL^`sXr0X}1&UTLKp zTEywlk{Q|95Kaed)k~|1M9f5tGwD*S-HVDEml6SkBa`cz)3MZUjl^R)PT&fe#ygWJ z#hYy#+{S|#e#WRxr{2PV5)VB^X4%6QqRh*S$)j@=pr+pEy=up4b}huBCz3W?<&Ofb z*%sBcDl8)+K_h2Bbkmx6k+9jv`!#LfVK>e=>M)VAHE>THinOjs86%}+)9O@{L*d6e zpK6{>K3kM^+En0orEBP5*HRKCaD0wPrHTH|>e5EWQ~X_pX#${=)OXrG({d5UC1Jts zQR*^1vOp>1I_WON{zef3>2FlR`{Qbua0W%cx%d-r+I2mO#72c=o`DxRe?dkT>8|a5;@=xr6$LA zI9rp>c&CU*<3ywb)(jEBsXUB~f%(*2Ic^CM1fHJMzDdqaG=!qis|W`*A%$=-C{Iwc zLM$UWBLbb|j1VcdEyqT1z*5crv3ba=YQ_zrq~jGVKpf|%UTHmrv7>1C1c?YZ^{b#e zkCC!{eJeLDOsCwZc@%-cBoo+FBO@cztvI`o-h_rA(UQ5x^QakBKvlqg)HVsER-RT= z+xgT!UgPuUZ){Ms)DzQDBN2(P2>^68a@tSy*ipJM!K;?KiAlDO*2$uYQ;dK+)RJmZ z+U1MHzMz_Lea}J3wPOzR#aey4^Dg3gZCcB?@z%w0ZEw8)010EoYs#LcP3gIfdE(7d z=yKvUC!!DQS$DS+%E!%>m}esMe4vSkqV=fS$yWNq9ES`5yHfI~1F>%Fe?b>g|T8_ECWx zYV1F~O~&SOcTF2FsI}FYc;CyDZy*l!tEJs}_A1u_gD)L9tmPXm4o4J<_i?j=^Fkin za!Be$Xjtmv=F@5;Y)_Q0YgUQWHTHKk) z&PKq&ClwU(oPY-z#WmcU+(6t^h(Y9Mif=;HZzPMlWGQ)a$i##}lY!Q{I4muuiIhX~ zgUQWApHy#BT$>tJ@j`$j1q!1X#b#bi8Qm0Q4*t}R6x-B-J+ec$c7LU3gYy)89CgQf zXr-$ri_8&~M;>4IaZubuBtjpN)MA{ZZAv$mg;5%XAQPI5MZ*om1MgDXx#&FxTj@}| z+wLYLW2P$QzlN>kK46%%VDd3qIcRWHt;;5nsWUB{F~w>7lpT2MS$C4OtficuDs6Xj z9o@!mP=v4`o(3yEc#E97vtxmpw9c$NTGfmY4CM3pRLKa&NU4*#(QAq!QHqsdQh3EA z$IETUtUw+B%{)h+@Tx^_hb%z<0QISSupi!(n27=o0X=EK06f&#s|d&m1a+qb$j9+h zxiVH_Y#mJmVEnYk)`5YFcFc7=deeF$5-JRq^%qnuCv>|zJRy;6ZJ785= zCd`1bC9}b+ASKR;@o-b?OJxaUA(fWW_%u_J!)k(k$T;hrqL|~p@DT!KIlDanbV?a z_Nj3nx#p6n?Xk@&jx2NOcEL$kln^_b$J#ezJm(c^cQl0gosB&v5H3L=oQ!AEnW?E4 z@&VHTQnlIB6jXB%TyvU-Ki%Zfc4qWC4K~;%m}1#xq7fggX4{-rDe@_b5xHr zX4RaMTrPgjL?nIV(wp{ccom)kRI$&NO+uG5GHTj0FIBm9e<>to2dz_ygn2KM#cHC9 zj)kZ%W0Y+|xHW8xbPllaWwJK2k!KZ$ZsU*r>&{Z9}A4jX7>6l5N0cfH?dqW4w+90?OHT z`p7a(OHHi;7jEXotbSx-Qwl*V51Zbtf-S&}bB6oe)@iX+yLu7aU5KUJkGzgHR%^=5 zBf9P6^0ihnf_EcV5pR9`07(@>-tAAW|a7<4RnzD zI`F!!MTQ5IHqp>$>qwR}o-xn+My1H;ok*heRxm=y%=_35R30-~x4NCpy~{*UBz=^6 zde)rRRy@TOIlB^>MZ`+$o}JB6lYC}(sbZ3cw^@D3_v z9P?83G(*4{>G)HC&om@8uH%CC6#bwMY2AsfM<^KJQi0A*Pg{U6=9B<)nl1|B0fWUn zat%JBVk(TBigp(r^GeNwmB#fQu|VfFO@h>^8NnF!>q{Wy;~A@aoK~=aocdFN!0Ev> z(jDwrf;N6gI2C3n;eoR6mOaSnS;}c_9K`cF0Jvg(Dko+D5lZ(*G`Ap{;hnaYz{em| zEgUQ`jCUVuadtve=srb<$tq!7V+Yo<9@Z6&vh1Yt2Vqss)d{3#$(23$si2xib%}E9 z-4?XbtFofHmuIz}0lCuz_6DmuobtG-PePkr2+EPi6zJj`vIpZs5_jC3#DRtzE9+U; ztMiSc>rFFSZ6W%24Y&|;D`w6_izJMfd2ih1YUpkWWEVL46JU%v50dS2rz=Sw`yQ zH|7<5&u-HIc-g@f2ljeiCT!#}k=0}ws}putDN6~rkgJ{Eha6OplelB1Dz2Db`ja{{ z2J-&(XEY@NR^<1keNAc2MH1Vf5rLnVrYmPmpJZ)r!ze~n8Z;(O`Ur-??s^bOyMV|e(xpIg$^0r?HfUTB4>d66p2$pi7{SLB@H5hc z#c|m7r6Y~rQRoo2n zYbfexYivU)k9HBb#}xy)0~yGx*ok*5Zc-S7jQZ3L;2saaCANxcGw$5BL;_7&VoXPeH;0>eEsRHiGCGD~$N)x~Hy*_GJf`_oK~ zl>ify+LIqOv_#B&yb^ftnx}b>l&R}T=%;3cv~!jV)Yag?bvP&1oRFr{97x9|qw7a8 zlhk-Ta4JhnC%Bg$ba5#>?>M7QrjU)zdwms_P|@7SAydc|kFBYW{{U>r%G*E(v8ao? zXG9uEpvfLXG3nZ)DGAQli!taIty*l#rSG6!X|ursPvuClF!{F*m0lNU+&q0pBCDF^ zMYg@-X1IA}-yzF% zFKm&Hl?^0t&YNE%wbj%N<|$P%pS(JXtW_k>lOQXe{2EEH_;y5dOkfzm-P=9tqqvhK zfJvsFqUKkys}Oym2?QQNr^zIdsC5J~kTNN@YSE<&x7yZBvgC7virGmbSe98BmII)v z+==a>N^A00F^#L9nC7E1Jc;r))$g83rE3{ON2y+WXL6)`{{T~0k|>ox5dbO6$hk|Q zPU_?{2;vK~+OwxmCS_>2Bd?_=wxPDfW_M>Lx@U}4h-MKsK|J*omWNFyq^v^fGRM@^ zXMw?^cd6ErO2jtv2(Q9+f6|`c*ezxQ7L~^v@K%xcsRu>{1$e=8z9sYRE2BEz=)#(_D~16w^5#tePlY z&H(0>p_n!TKVG7%nQY0rPg2YV>NAav&jcERcwHE?j=bWhL{5leyAnN%Ez@uY9yQz7b-i75q!F84BY@fT0tL}{{Scfi0ehn z)yt8Ph!vQz$9jTbyNT*?LMfuNI6SW-){+?tNC^WXf>o|dGl>8vo_bYV_1hd=bI(d> z>8BP$Z6?5YqA6TT9T81X;pz7R(?dKs1t$5T8}Ry=0+{`s!vT#qjb%^ z26tJ41M-f!tvjhwcQIV5$fwmu6_+jEg*#a1HJu?L#MUlkUZqLSK9vTCaxLX~B7@Gm z{v7rdqFM`@@zm;m-!`FWw%Uv^;!X}9EHhXdzJ+satt3$?g-_oD98}uIN_v>^K_s*J zam3Fd{uSo0>H2KfH{uER+X2+Dr@7JXBf4itsQ6_huqk12zF3BKde$|B_ZAlNK{RZV zmg;*7T-sR}C(Rdfw6_7DsXirT`OO_^`nnQIHt;aif2fyb_C8Qoh z3v@nc;1X(E)aG_iLZ0IZ0#zz%<-4l<+2r;#-l!XIV(67vU=hC**=H*oG@05v0aAq1 zDTx7c@twfch~ymPQ@zmnSzt^OGb0=*s?*&0s&+9Pew5I1Us93-yLWZ;rd&wOMtgLs zljbc(?1(N-d*-E$I321`<9N$L)C!{vKqsd)E38<_je2$!Ehg?aQdiu}cn!>(LJW@h z#cKyC07&?3V75C@(B|&d%7Rx{`A!M_X>T0)9Z4sxQn9MEtYyEaW6mlmrY^;gCatve zIxL&GfwR`6M!>0XMyU^01k|AVnmQ8n9E1W+Ij4sCxExTCOSvF^!>v4Y$?rzD6IVmJ zV}NJ`=BrB$A>f14r8jmP>Dr)%$Ag9K^rRFdS3p)780Mdvwyq|DGtDm|m4(zZySKF` zhfMp~bX&c{{1O9W$3Bz_uW5p2< zUS?6ByJEa$bld*`w?zZEZdnjxZCFiGDWr&AAQQJLanstSR!kIAT-``@JIOSqYs+;E z@_d9IfK~*$yw}VAkcwZOt~(04dg^UToz=<~?HEjONfkMqYzhYh^{QpKHj9>X#}t1t zJJ|3APjNrl;W#JEZb7WtzQz)To}?DnFCsUewTS3(PG(Z4aQS_!NUmMVRzk%jZ88;q zoOWSacb~h`URe2d;1Nn)!{U0Bb>c8@u9$no^6FA#vKByO~AZ z`=xt}bBv9kw{uixwR?6AkmKBOR;`s>qS7|&Rm=zm19r>)(EwN8jK`dfZoCP&p z!m4mzkELjpiQ8gC5yYgXOA*aic|-DgQGH5Dy~Z@S9A_Ss!SfPvR@&UOtcNUPJkfqO zz^5drtlPUcp&q4MGxGp{IaI1FyUNXvgF`%!SaQZWTvr(h^HUT5?I*nQ<`Sik~MQm2Dk% z(?Z3(-ciU<$I#X2t`_8kmNA3rQq}cGJxX`fP?o`)YREer8hb$ygK&_5M?+Hip5{wL zRJHR3+#exjEz4t!R`#G~iq=L#Hm^@=xh2qTrp%XPJOIjm@CPQWI|*aQ;fjkkX*k(k z6{YzXXkVBLgO=-7i09O;Dlw5r832xIb1IDO#^L(ZHDjmRD8}M>>{I|Sderbr&%jU* zN^Z#JbLY^v3`Bk4MNG+<_6`B1)X8pSU0lfwtL27Kj2f93;QNlXqp{yjEyOL(GffZk zan_+dOK`d$G=ZWafX7PB83vn1a9p|yGEGVV{Ayb^ACgBr)QiB)3FuQCrlkbcI}q6Y z**Nb?Gny2viX5jcz|B9C#}xM^js%Kaob=5gPeH?vU!@>El!k|A7|lHN>BUmiLq>Cs z)Dy=enp*=DXQ`&YOw&Uu5_s=W9G1a2t9C~mwPlNG5-9_14OWh!BQU~%D>>7gBEgg|cJnp4^>$r^b#Xh9eT9KE+SFV!qbwd8o5xT-wrOR=$O#o-|>|8P01<#C8(REOJCcDvTBNscEC8 zocUI#F?zDwxLFjTHChQ?83Tu5&p175)~F|GYKo{xC1S7Lf#S6-Qt~O3tg7(ifYh3>OKnXVo#u#(afj)Sf~i4l_YUiO z33O49n0Ku2VpO?NOIFdNUUcLEhg21f_G^&XXk25j6{KZ-+0#aJG*?khtZ$(`x1St@xX$jiERvGPD4}}~T2fbfGdVRRNoyt@NK6uO^sDmD zk8aU0+(!pJDLEq;t2>#q0)vlEl^d4Xf!EYi=yka#605NTo_*>+GX{g5D}Zpvlf^|M zVnS0a4KX{*LdCtSwpu_?8owGv0_9Y*W0GoCriv=pL)vnA3rHvZ=F^UxlS^YMD?1=* z?bUZo0Y3cIA2)VL?^AZptTY zl`RJ2qajXuikuQh7|jf5dbTOqB>GcJK{QOKDkyB^cBRxF!@2EF&U$b?>7k3CmY%0H z*m92gnl}N`q-;WX%}6=}RS^sg&UpY+_heJ_4aVo3^Ti!{^rg@?D6^d7xTYQ3z0Fa> zsH#BgPA$i;y%JgtvTIzp0I<$@?N*F)M*jePcH@u@N^4>!`;oEz-zO%bV*7K0ifqi0 zLj}p=n=*|2_;iB=+$0D=yFl#)sWT@O8Kku6>5RVrKQQOgUd8CJpXLn#nW%oqST zsD5DC=7!MJwH>@D>yt{vxZ{CTnRdRT5N9NH6=}wF;K%q!2BumwN-EMRTDBw`vd67j zW(mgbb7Elvp(&S#<_X^0(j?N@B()8=J}T0F9zxj4lQHL3;VbUI#_ zVLj3`OktKUn`Z3ht?T~)YFQ$CeZ@x%2YSnz`c*jC2ljYdW&dv5O+v%3kM&3+iNEt2E(`R6ctrlNAVA5=vJ8sI;TC})Op&vQV zYTC88D{74_xqNk`+b5kk>JZB4Ljz&Q3`*;OxNRdFxL0 z4UpuM7%V_J#w%J&cS~oOXA8~;aZcI^Yq3F}bbeqNTysoxatH9_QZ7!#d7`+HOi;wk zI_?7i)N$#r{{Sx&2%8z&IW<( z$Oh0$W1jU5SaElDDzt}x9X)EfcHf-wPR5QF)MUY(GuPUi1BFa6!5z&O=xs_`*m;l= zRZd9jifQMbaYc>W7~uA*Tn z_o`YUCvnQyr>V)Q+#3Oqc_yUBMt@2Z7d3%1>56d5=e}y0NsE)~(vTi`qQLePf<;Il zibzR~$f20yr%GtM>7dYkGfrd1PAEjTa+)dHJNwn^TSz6_<}-qN_7qL&q06UI!P4!W zPmt$xaZ$~r$ru}@0CdJGd7pD0Ze7aLTRb@!+yKcVu&Wks-dNtdKq)EbbkWvETyN#DB$|<#q&WoV(y1<`R$7v%Z1bL!$%w`~)O9)8M4j3EsxcqU zk&u0jZLt=)()dy_d9O^txWXNn^h)!7azv)wRn zn%+_0lr^P1EZ(UzPi-EAkX^+f3Jx36t)K@O=QR~6xVtjc(^o_K0qIg9J^EBNp=sS} zII%q`%ag%0($Gl~E;EkxAR$lmq?V&%t9CdG!uE*4i{oZMJk<{_&L*Bl2H7;^Hluwn{ufHdsVGgrY?z`?l(OP|-S_jn}Xv=tNbRb;SZEI3SZn(nb0S zD&e@Oc9wh-fm0W7Oc)e=cJ-$S44K6Z>NKt~qG%;5`RSZh$kkg!cHm$dFVyI!?94*B z0OyL49H%%HO`6U(Vl~4MKm!MwftiT!nn_s`C=n1(S_j@4I2q*nRiZ!F>P!WWg>yFgm>^RGmzh_}2 zvH4uqa!1|*qElC~l%(9563-?u#D*MF$2@V!Rh>f)YDU=j z!o9-IPs~OJMK@4)Sk$VPT=D?TH0)0{niLXmFoH%ur8Y@+D;)GR0xK(?h zAYy=(Ey<rJ~3+@*gpgDG}g6W+Q7NfzHEhEAfHw>F16 zZt}g$`6|!z9OIzoq||N55z0v4jigocSy`E=#6N+O_)AW>)wKOZF05i* z?HsFz{OdVF?(Ei`M;#9wQ*R+=*>H zNMw>WJ9i-muOgz2V#!#{Xgi$Nv5Q-Vwk<&rDY#0%hk99BFoi(m`&4ek*HV<@E5{#7 zr6?Ka8TF@TOVyEX$67{^V?C;(ox#}^-ST)Ob5kUnea#LfZa04R&N1ywD0h;2Q5d9O zE`yr`lhTEem2!vjs=bF>Eh^CEjTLnsz0OqCWX+>F>3zk7rMF#lU zi6aLX^`za7iOaAw0Z_2X#Wcmrmf$xW8Z~IoTM;xQw;0<@WdU$h=hmq{hHaNLRuNt# zjh>(i?{%lxkZ$2kOqQdmX0CI00jHS9fFoY{=B17zn?fE3y-m(ZE1ey-nr1|K0)@s@ z4uY&Y#)_fl22Pn%(z0nV)fl>(P9`l3Z6f-CRpYl0x>be4^*e?s**(KfQp6u1`Hth) zCanXXym67a}fM4h%YQFbxxIJaV> z_>OV)qg9E8s}(;m!;fkdQJc}&#FB568ml`rk(OM3CbUSn#`=|Gjmv!F0;DWKArDHs zl=Wm}Fze1vY4J%cWXgqx!^;lT`BN@3H8jiX$u8nXVEd0b;N#Y>Y8sQ<-R&4E7dQvK zXDB4>WhlFl9d1@pz7^T%PI;#dP7r^2Ya3^{TF==vvowqpR^{C;{>s-;hVWU&&k@`M zbT!>rd{4TxxkgiH=euCPMpWsa*cg9y^Q!X@8{LE(B~AQ;|p@6AfHN;w2nsG-V|xW06}vxN4iEV%@-As)h=mCVwz0p&L)EKsq) zPt2GfUX^xZ)f0W6PHv4NpQT1DZHs)DC4b%&uCHObjUqB-eSAkN>(3M3l#GXb4CZ9n;_aKl% zjer5}dsIitakP*~=}PR@>ti1z5K7)q!>k z_3291E0)B!7a%qh4T09Hy~b3Bk9K(A)g`6aa_ew{*4A?VT#_qw+C_90(Vwsk0$Xkd zPg<6`lH}8~MpO~BKu6{xzD`%E6*Q3fvy=Vh+ln`LA+Be0lARHw!-lj4sDv&eiUU?VN5()h#ZFl&xexO}Dw!rf9UtWVn_!-y)#wIR11 zB1ZZSmh2iEndA2~#_;rEkC&-&jyEl6+F=<PrfyZxpU+k?}W8x)|y#W;D z)wD0#c@fEPsKh{P37B^)gGdbO*lT%IbvasA6L%PIBUTDa0)149mfl$1mTco8LMOGfBrCDQcj-;PMLW^3E$6**_ij=)|P+U>4uR8<+1P$))4DRkWXmEEO zd~gfF-8Bpz+=k#zaF^gtaEIW*LLTqFTkqYv@0?TT{IP7fzx z@kW-hGvyv-Y;f4M67ZZryj=` z)X#wkm$-&fqL$EuIAmF~6$V%gL+;*+Rf-q)RLNBM7HmW4HAja4@4}MVJ}Fd7oJgK3 zO&r#9dJ+wk$S@E(yZS8@TQ+m(Hw+7w=DV51N=?i@RyDHEFMz(vR8IbU(s&cWC<=`Y z(@uL=dq$L^i_@7j%4PKM&;#du*aff&WNoCV5-gz&{lwCt2pMXWyr@tPLWa zQ{uFYb2@WZY0$80hW^0>p3?FFXJHhU?Q97qDqv?TW6WhZGY>{21{h0e^q;omGG zDZPn`m-kD{{SE5NS=!J?2Ej)o5krqrk0DoiO{~XmDa@tEw2TDqj9VD?{x%MvHNP#c zbeb-W6eL!Kl9pfa3NSc?fbJF8p-yXkzV>C@2#sCgKY&q?EfU!vuSblR^RWC@uBKw+ zLs88-cAbHmm9}dRKKVlWwv*h{T+b`d_hfN@h&ydkO6S(@3vZBYF&A>l z53)S9?-bpKgC?JFU!=Du;Y8eR z!DuP*ECT~!jxYUXyz%~}tEFusd<_CE@p3&IDv$-R;*5}1#vrTB8rG1UJ2XN`zf5ly zGxzHr+@MwZ;27N=w?Tch5}VMoCS8hl41auf>YykIT`I|G8vOXG8blLRwLa)TT11-~ zQtb9wcF(U<;o@in*NXX@! zUNIxz`k9Tc0h|PXTkkem%DK~2Sfx+0-b@*%AVX5DwY#@GxZqR6G~@~EFR#D)qayc8 z-nJ>yv78myIHy#w3%sSulwQEPAsILkMl25;ttig6Y8JRd(24n!f@I0@w|@vCcUCYm z+(yCKjWp& zNGabmPs;1}Sw9Sz(U9Kc-F{VLr~Pw~ZHIRz%N&0iIz0PkKp9iPAooc!khcMp*g8}N z`*C&{*RY-_QPiRGO6SN!Z=+rs4<0Y4X_@*lH)~RoFr1VHVr)jWt#Y&pBzL15)DtygTI%`nK98f*o)Kx6x1{XP}+x zC>(J!GHeerQl6;~H`_ev_ecB8L>sI&Ns}S27<=lD_!}7G*ssr1If#|7!5fJ8dG#TU5L+N|C2e&Z=VRWvi4-?Yz}EN>6-$s%aADVZH?;F zN)aa)yR?xNoo;`!qMIucZ^oZi+8VbB^-x=q-PB3+(6a7}m}SB;=b$L3ag`F~-=oq) zOe~nI9*>6g&olGRz4j4?eCB`rY@(=Xm|I_%7s3gz>wz+!tqlHbYVLy~)2^9n(Ryrt z8b&o|*i9E=C-*x{7APyzHA-Sd5K?;C;eAlm9sMGiONHW0>P<*@ymF{+p4NohNHqqB zs(le2J~B)Z(dinYXAc^~biNT%UD$OEEYxs&P@me!{d!dwZRXxwqD|7zIF^1Uyr_CA z<=gZuye)Sx zt55~mPld2Y=J5r~6v<@BxO@fp;<>9l=Fv0UF0t_ zeWeyEBE4<=skd?!#DbOuqB0);J?YLpKua0!QY!S>ArD4&v^Y?4!v_T_b{mBV%z{WS zD?@2t@fu2fGza+a>!)2x73zkUCLdJ(!hX*3jQmOSw zhc43%*l|5~Yf5BLSrCkq+b8*XRyfq*2Q*vzXK5G5=zJ;?7?cQyv6cBydtcAHtcsl* z#O3wZH^nwPK-dzjIGq~6Qjg(kM)$nSKC^TxhywKrDg_iKKzis;SFod-L4fLY1U6dv z6yaS}KG$f(Uq)R#-&Hvg_bjNjRshMwK{AfhHx1pisDq9YvqL%bB_mQ-dNvI z&~8DV=KY;!ynTIcM@(<4L@ez2;sq>z4|H7@tLP71`$ z#(Nh`O#19msq!r?TB;Ie`KfFP?_(>Fo`O85F^@Rt0iV%>1p+rFeKD3t_s_?PKEQ;4x<$lqjFb7?sJne|55|+4-ye+ zYX=m%day$s=iEN5{Fn@lB+aHtq+#HyV$LWo1-}bba(9=#|8RJ-Ui}{+VP5?N9Icco zt-F&E4c=j21%hUp(0^HXGzs@zr8al|*!dBb+Dnk>$E8dgWuvEex9SHkt)omOd_6=@ zpWAHAjD#d&HD?(`#%1}s!W-TJm2Wduknrf}QT3TtO{a(i@bO146JrC@4K+TK@q=$l z1Gni*n^evK&&w4msjT|?NxnVsFWMO8BAIB&Lw*Y_%O-_YaAY}L<>TKgOhliUN!`|= ztn1mobnxwdSPzTD)ZkUFa-k2lkV30|X+XttX1~~~Vf4iQEpzwu@{e%DoNd><)a(rz zk%C$z-UlCa*+%=*CLi)MREew&n<6+@*P`VS$CtD~{s=reWPE=x+&PYA2FK#h^?IHD zo~=&_yYs_A0(P4({Xh-SwTY*_x3qxiCmr4nYKHbeccrSEak%Pw%(_nKB6T|T?I8}E zKkug_ej$0Rp*lFpn}ki4Cw^rTVPEWW+Z>u*;p=t5bvUYgFV!xwF*?q?g^lQ^<-!$# z(#rl)QB1>N+)SW@!A6~+A&J6w(E0i$LI|U<3^xzX#glx_6X3z3)_L|Gs|qZ#w?aG;EN9`+31MTZf777t^?d!QH}soS#WHR=C!A z{{E42T;Qcps%LVzk-6q!se=%MA?Lc*! zcj%Xod(YF~#wGq#wUTwGH?IRT^==US;kttt()QevC_U}`O8k%_12sk%_qO^0IS>X> z73oq4OU5V#6uA(W0ymO42Wzi|wBNgfi#liqi4ha#sgYAMIe2XwCULYG zd2^ab^X0U=S5l_JZ-(Hi*J4A7)51jx{4tsubwc8_a>u;POTT{G6+EcfEQoVN&xHR` zqM4o>Z|kr^xanTp_NEsB1^#CG!?%Npgu8m8ROVs)?0SW$cJnW69pZ4c-^X?21gF8rliJeP57e@;adq@&@2E%H*r{jku_pN^Je<=tC0KLd+% zoD0RJ)3VBO*D=9!%LZ-TNP97)43;p`^xZU?X$HSXM0EeOr!n8+;Gwf=Xc>;i)qUr! z-2~!^MJ?&8do+6G*?IKhX)F*4?-$ZY1q?riOTlq?UH)rpy#4vO&=a^zM+cW)w z*bjk@PmFo>xMy1N za-3Uu+H58IWqpdCA1YW9*7DRS)4z>IH#sCJ@EPB|iZNqiF>1xZ_0mEeYfTP zOQZ;B>=POvkX{rdX%2cxwME$>iqlfYBF^_=1E1OlW9VVe6_$r`mfR zrgZCAaaNy_aGhWQgUlDGo~N3$Gv!nx=%>mGjSW(DL7a14ZshcQmJ`t5pHgtudvpeQ zqV4~xi8Non9dmaLK$`~cS)PmH*Qg-J1}P6VD=rXMl+}T+>lnySb8aeUTH5Ie3>r;_ zz_xEQH4S#dV(t{#47I!)52l}5W*wE$s`<7;Pt^sgj<8N#e~`Pd)><_|H_gV;{KveX z7F^Sb#i_P7Y-AzEPbQfoZkE+ zJKFDfj}uL6w1X8V9zSQvK6hYlL`zeYI#jh>iAH3XZ;ZOQd;nRh6xV79E#Vk7*p`eF zNjTNUS(sv+@?kn6WKQWTfAN&>lEqBCu)LHZr^s0PlFBvFfHchPhRA79g5S74hFcS8 zvG5Nt@DK2D(;g~T#)7|T1KlswD0?3@d|g&M;6iL~LOf@b}qL-t#9@$qIw7L(x<5M%3IBXNX;1&e#!)>hr%xd{p& zy#Cm6)+8^m*y6;Ct5?wC2aS99K{>gctcR4YM45+vlLjfb6-39`lxbAttz86FhB4`a zMAgt4vY6bW^Z`xMd|J^E-Ox>Tq$`#do2KFb!kRnPKL*1jWAr;=un|L+UXxOFLps%= z^fC;|b#sTRf~pAUWBL98L`pEn-{>Q=QmiXUb!LK7%w!osjj~D+Yu@&7LtDPE;9nCn zmXRry8%NDhl+}3XQO!ZR^O;fc)%E8OwbEBsZ>$qi5>JOKe_unp~hukS%#|>#v zYF3OAcVScyGh2euGy!ipjqARKSJC5PjfaxL<79#O-nlZ6|RHKx#j7%~4pH!|f6kH4%z@Y(J z!MOv_eoc8?X}=EkVTvK-%1wr`K@!U@jRri}S#+(c2V*gcpz=Yc(nLGbMHQeS%@VVs4B;Wo((E z?O=aCBRSLY_v+v78f0~)BU)Qp2B}8K4bF%XqdO(e{LK+~T{Ca=D$Y#O5T6Q5Q?Z4x zm@&A!s;1}-+&l+`OPW#hVnlPy>vP*7m5%eq^ZTngBqT%Cq1^=rC$i zMU^*Uh^i5$Hu2g1+gTKNTw1f=kM&1nK`Xg?eM19an96~Fz+j$5$! zck5=XbfQDK&Fla&Y-X3Gf3cy4myu=$^+Xdl1>2Va;px+oj;2G}HM+`OKA}&sfuY<4 zK`?_^?v2Thtu8V452r3=JoQ<8h%r(=Q$ku=>S|KQW2c)!xAXgL3kZKurjjmW%6=1% zKa_DzZ|Ah9)6H+SsM>GS(-(ObUf(@EdM{B)?q3Dp)yiIqhXsgZtBJ?dIvqr(@h}s{ zk#7Y#eBdee(@0Sl?DChNt@cE{Dp~(c!N5YNQ(070<)?b_d!gk~XMf zW@uWSCJNO8yZ%-lR4R9qsysZ0Qu-}A3vC`$PFp66urt6PgK!wX^7(lbwH0;H*0sc! zdAWz9j^feI^+{FBFdexo=@Y4*!CLf?TxyNxl&dFO?_NF5Eu2aQtwp#YpF(f8<~=!c zB7rzLKar=WQ;+PrvcPNV32k(vPnz0auhjsTc77q=-WDdtkHNls-jg!*L{i6TNGlG-_l`K6Ehiq%I4(^p9~{Fanp-;1k%5|B=|+DQ$Y}{+ads4^X1|?T!Iz zO1+|Rn$%j7RBxZmoZne6tE90}YbmQm!I_lgTT<7SqJ#&=I9!($n?FdM4_LJWd49+< z6#RG{mV)WG;!teJWG5C+0=$$vd2f%ws~P1!wqrw_wswcTjJzc3>Jq zQ8i$V)cFOd(`ISj#It9A~KuCNw5$71H4m^JkSQb z;<0UQjGH!v>`Km94J$QKlvjUA6vD_Z4iK2*WffWk`d6Si&k&-;ATxPd=p*bzVMD%tl}MJi#_S|~;c7Cz~% zFkfvEDU~t#576XwevOl24_|8EO+Jg@HA~J&6JU&XH+xV(Ozaklz!Kjq83ZEFJ&6ms-m$3kO6ZyU4&}Vdd=|>Kq-0ya zGP0MY#cpr3PGvwr!ZlTSZ_`3;iCcMJqVgk9)FJjEt89Iv>zlrQLm!6z++rzHiw%>1 z)ktQaZu2Yt%t~=E`Ti70++QrKQumcV#bFP6KV#mAL{gVYGut3nW3ostM)gL#TI0K+ zN-|7LqmHxO{NCwQ{SV;mfT-k<6<%P3{3oEJ3w*-}oeS+Q6Um68+q8_onNJ?AVj_dq z{c`Ad)WOiTl}@x6Ftu36v(Wwn90cu4cBUhLAl%X#QxPTtaoF+47f|$Qij4^(@Qb)B zp0O7UHEM8-Ad_0f*uaXO$+b=TgH`WsZZ*0iNl&If%AUqp>C}(nC=fD9&ym?DYdGP2 zhRO%iPEkB}H>Mb#BY%k%5Swp1FU0V+Ab}jjT{2rmTRtXW)}5=29cZRElW=2T5>_fp z@{-#KC#nYt?Bcy2*hYr00b)Y;ZQIr%c})GTgzKD}**|COnwU&Db(u+GVu+9fO&Vi< zdUqFZ+5vxJ2I%TBkYTD{C1ngOfYI5@*7#rQ*f>kCT$&fe}VkCGlW8( z?mZH7fkU4+>?&yY_2xn6TJAcx2}p^is&%_ZESVtZ+Py#=Lp$0-c>G&K4q!8^sqG&h zK6Ti#iU=~#pDLsLTaR>0NMBOJreU2eh-$Pk@Z*y>%EDztmgxDs4x(tP0%3UI%bdC+ zZJKsgMcGkH=2JqE?K|qG;Qqb4-6r!Bh;h{3j|3vIv7#|w&K}BzO4i8snUi1GiYk}# z{nza5q%)+uFm#iLY1c#D%kZlnP5T0h^2+QZ6=9<-UY2%6$*q{v+eZU&>=$m=z_Xa9 z9Fr1lRVC@~57o-1jr?!5yBzNlr-~n-UV$@#Z(>F{f$8mnCkA5!6piL!B(c6 zgrV1U@Q*sl*>NzL61(HBsegd&zxRc_>zPaE-JE@lc*lG#+(yk?gm7y-g-#QDIUc;9 z>Lm2cyO`#YrF*I$&$Bj)dyzy%!G+vF&5)x~^TfkfY-Uk*P&-tO@0rBE%Dd8PrP*Q9 z@K=X$FQ8u~%LR4L*{}+$@w+>AddcSxiBISZifwDPh6&xF-?mqBoD?bB^^w zNy#1zhKjbY>H`xVSmHQ3peY}i?KuyUw|^t52-H+dntv%U7Wmi+X?w*#HK?+*WmC3c z;vP0QIBahaGBO}xODIblQGaU!Oq8B?VZdM2u3pL{ODA|*kYYAO*K&|Eae?z$SD+Rn z%E@?T{RjAKpZMvLA-?e;tp35`cmQI6@d?B~ew07B;7Eb9BLuipr2r5U~p0krw zHRMBPNAR9tUwR2jy@H)DM`JXZpBDU}B?r<^IM{lt@gDob(bY^S?J~}3egU31(`z7I z@WaqJ05%qw>(|T5Ia`{zao?4+<+CO|I%5MNG#s>le*&3LwgipK1P*y-TV)O11Z=e2 zrns1BJAG+s0}8zEs`gt!r-g79lNqB=b6UCU&a_^Y3ZUg2y3s^2pUgxf_*Vg`~ z6WeY9M>xo}Pp&{j=saDck~%+GM0JkBztJ~`kFT%2Vr~PJ)iT`b`>gNF_Wh>QHkzR7LCq>wDb~bCoMGdY)4$vtS^%aq;om;ohbAsD@B9 z)-I))M-rokG2q;d=MzJehJ`z&E9!U9s8Pq|me>mR2~lf&onEzy&ppw+<(ZDvjra$U zyv)+PkwvlkbHt>jJ71~Nrl0c`8H?D84myLEh!#7s_t2Y{;A#KbGmRP@Ij`m(4#pO@ zJuGZhY+GCD6Nu*;Jyg;EO3XvD2JA?1Kj=!yc7D=-Cf}doL+S7&rQmo4Zq^rf1x{Z zcxVdvwK633>*y_BQf#~Y{vi;)FRbdzw-JFSVpu8oxxD9ZCbAgv6{%t|LRZq~K!dVp z4ysV2t2p%BzRS8NY>29ub=k!zUZ;mh{;H{jE2Il(cwevi4}d%rPkGW(^%uxM_M!Nf zknk7WRQ}%4ane*X?tg%QCDv#9Br!a>+{FWbJ&bc`_iYURAG_OOnR4UVh?zhaaI=R9i=wvvYw`KDXz zO}Wj(Q4$_&U(`CCOL8k#$~(>}ZfzvEYcIzAOod|%edt)EB>=28PT#bi=G}qya_>1; zpFqW5nE9|$N6463%GVHnww;b#Fgg{qTkGFG!h&3tP;z)0TB z5|Y@XkR8;|-Fvz;yXbtTScWXtVK&^!33sXGvEZ5nssM%x{SDd_7WaWRA~Nt3QsfJ0 zhXR`|etYCCvz|qWjfGSpS{_Ed*+C+d^L@_!=+_%T6W-H-WEm!+U7KCyTIN{|3>t1W zgo&kJ5YtDO9c4s2m9EipmJdmcQ&gNd=rhW$<_!_J=o4#c1sfa;%^oYXHHDb@Mbn_a zoaS{ueBM@CFc%9qQ}U$#h;C%dcs5KV&@n6>vl#f^hi^MB<7(63M==2!+zYpf=dSo$ zji6Qv?-#Ta|71jET~p2%bZo_<&KT`OJcnQN*Ww=^ga^e4G*%BXhE`P3F83oBzrWva zVAIj2ZadL*4V0B@v@HUvln-!+5LbdK%8iO`8Ri!Qr<6PHDV;P_CE^&57YiI#?qBM$ z446SUQ&Hq#(tr!YC7#L6CiNJG+`2&TX^o=WQa4Ox?G;C0^Tn{$m8PT%Z-3yqfcSt9 zUMjKG&V>AHvt;rMb>{T-5NneBg&=)u`mL(M38L@BRS&)p-V5EfH@FCa>)m0^w_^UX2wj=e| zk~%PWmL{<7K++vTbYnWaFY|Zk%0I6fVHX_k2R6+S6C2kXJ9!8iOQ}dpcPcq)OdSmE z%W-H01xD2BJtP?VIJ2DBzFS>KQz8$%v)X2C(3_9tM~b0uy)5#v9gVM^f4%q=Xug@h zLuQYzD>vshVbRVI_0oP`v3XOzIaJ>xc+XUH-gAfRh^ky>IjH zs6z1)ffr@Ew=D>KGI^i4(x&>hEnwg6lsu3s=fiE?h#!U(JuYOK%EVEud^+skWRAO^ zCwbJOXc+(6P5rRBd`NA!{9*t#_xd)YyHj={Zl{Ja8FwK7i@dK+{1RME^4wzYz}W4u zejMkx^}K%STnD){WyoE%`v>^xko-4gogxyu?22{0=pTTbx5Yg}`PwSgd?WN0&zJpx z896I?7#`ZEOt>X#4x|J&aCyWRBxF6AjJl4xU5OLn$@Kczv#=o@i-M$1u3uZ zK}ESFTk%@HTgCIeF5^MXE;<=aN;`FTyrX{U%@#sOKzIXg&H_^q zd9LMB*pwycO00o|S)or0z6b+im?0)m&p>cL^bQj}512CbM_vhZ;+GgC!pac8P}(rr z6#>VOmhVGxvDED&+_(BA+@lvd7=O`9Ks zbU~JGi84=U4u|{hnenZBl4wVtI>~%KF4R=yVf@~|uKNe{kNmvOfCBg4`?13(b+>?- zeRBE-@ts^m!aRi6pTBadQ9!0OuF#VB*L3mJN_`avDk4;7Jb}XA)uZx}QY+PAUKgci z{v{?9L}p(LVU%c56a(&pe}J$+Mm;9EWHq8mlf+)-MY$tDU4n$;jlQ6`RNmkzCfvAp z2|@A-ZR&%%&%5unB3NH6ww>VxL_+dHV5&3i9HD6*wz^ySk*L-3o+5)q%RlJ1e=bh` z>8o-%yNh?BB?$X_CqVe*QAWaB(IQe~Wh;3(t=!gV_4>2*S=sg=ReZMI6El42O8x-+ z$HF}?juQ$7E=LN|V2dL>#I1kD$Zt*X_M$n}abj^ko2kAi`SR};Km95 zC538tkhg82m;Eat4GQykuEX~Z;U2cfuU=J~fGjv#@A?%(S>qa(yrpnVlY}SqEjPDn$Qe6Ey`b*k*ak~G{kRI!~MI!7xv~01h z%T)TBm$3Rwn>I#m2VMYAbpq7XHtq0)x^e=xYZ%1Jw6DU{a99(;c@{s~lpzpm8XVl; zMrE!`3M#9WG?n38on?Ohv@TS%cRPI za&g6LcDw+-tNd^VOV&7Oe1aQ&xFCf(s1N%HOf+hD&uIm{KkI5L&GKcP-Q`4{k~M0Y z3ov)S9P1`r2sk1Xk&#iwtYI`SYg-VME>141ddMFnAXp77*~uk8X80~#v;k-I*QcV% zh5Q)$JDNw-<~VSeEUsu$&RL;`LD8Py(zQ(8<>yg1vrnYTB1}Z^bLlAm%A&P9uws>f zCTB5!$YI<`@`YElW`G}&aLJA(HxE-M|iH9Lfz+iXMlb()%==-s4NI&J_;7uE}zzt^FKBH|a~?t9LT zdd9%_ZS|Sm8^E!rO<@^lDaWSD6bua%_r>Qhs^7ys~(!rO$i(Y;6$q{1_=uhY)+)@~4Wps=j) zVRUqenW)nmugE2-Ed8yKx$1zsDGn3=VP{Fbfnz1Y7ZXrE>AT#z5Di%4kns29)mU)G zU=@a5p5hpWYmy)iQ^q(xv zLNupP)(TuORs=9z(WbuC*n$UfK%Bv)vvw=7%~iKiAQQe}csAR6NLU z?U%&_C{uQ{r?VuvBr%C!p_NssOM3sGc&e=j>9Gj4px0)GWkS6C2!w1@A0^WcASX><>5}T2Ku}OR(dZf znC<9RCniL5o~P*TlW_zZOxsox5EWrZX;6;*3-dGz>+`5sCgHfH9R>UTY}rB}`LTup zi=TnK4czlckVRV8Ig$O*Q1_*5UT*0GtF>*%FgCx)qFi}{?Dk*_R0J1b<#$k5o+8_) zax}4Bxeh|`;;eWpcpRT2zw74tN>yiVb8`?tU@IuDR>JJZs+iKjFxNysXURE4Fg`s| zG`Cu?(5afd*4m;x;&Twmre1zST^8;s31P|JHO+=0DP|8qa}S_2ZFPEi8(h{p0z0ML zY4@ZB-(tL8ITxI_LFXde6)HGxv&kE#XVzwWwh8&tYO z|1{Ebhv~#Q7hWdnw7Jfi4x>1Yq6lsnh5rNe*W&1{H*!QB#Qg&h+6PWLGEQzDidTXC zrZTxD%MFxYF2_C~W|w6@BXNy8K@c3d$37B&`&17m9&+~2)yZ{ZZGw>7|IMrqCGncTxvL!j-Q#5_c;o|I_UK8W?{bJScN+;7r* zk~>NrSt6X~R9v5rk(t9jJFLFoU3Nbivu&zqT-*J=6q#mk5k+*cn@j6D{xIrp=dELk z6u`mut*fn(dws~gU57RGG}I*ArL3g#eG34<0R$_a3=H8foQz`?y&ty>^w->DDWbG` z)*}AcOT|T6_?T$Wr0^~|jiVnRUs!-_+1MOCKInoXcrq45oEOv-RT{!HL?2B{9#^o$ zRQ1Ap8CY9Rcm5h`$=a85I7TS0z6o_gDs1_@eNO$_f2Ql_S)Ebng*;&i@I$#V#eKo} z`Mc;>0mWHWl6yPn@V+ie0*y*h=#_lmk0>sE5~TUvh1&s&4iM%9y7fb?=2iX+wQYa< zVWf4!I(tS77R(1ehSXBH^^PxIkFRZ%<(1Cv>V-eGuf^5K;8#>Y3D=Mo&Z#kbZ?2|n z^=2W831uqt6#U0Gj({s~vkLV_KNODR%U3QDnDt5QK z%~6$KhD1F28&_Agih`&*#jusbm#A*Sv=nXY-w(RF&k`H4(-96tMigm>m({zM9p1cd z4#^~WD4h{%A@`@rgULpLc%y-1Y;x(fQfeFBNr_VxRSJW^(y{x+WVviF_7bC5=OOj@ zt}6V9yyC=<=BcB@np%aS=k6(A7Rp=<*v=RSP4=&BKTtqiMVZwaufZR*ZODRq->W|R%_rL&iIV(|$?%hkk@h>z{y=RJ&MN0s2~j)MUFTSTHGhr zEEOIp=U1=y)zlS@k`F}1w|~FYii!5w8*~@2EJWfTwe4~J)v-booe1j4Jv&aBk~^2wtxydw>+B55K|y;Lp`4W z>Y!_89_Ej*L-XXcJJN#$6Q{7QIf_p+>|4RbuTvRr7+z!Wp?%~-k|LvC0J7aixKJS^%s+{u7 z+=BZj&3G|mJw;W;k(q^V3O$ow!~V4INTMD!a@^Iyd1$Q!vuBL48zWQpx!Kg}pHY|I z(khL{Zlv}Vw6!bg&AZpt8^C+NOV9KX=mAn2;P2>i>t7WOh%pLi`OX0yN9}u~0;`in z0xf`1d+5DU#D?YhEqOYdE^&;^d=uS!TtJXz2`d2i6P)b-9F}C0z1ky8rMM*sS8KyP zRJ0Yk=bTFZFmfBw7hN*+>bmpH=rwf?9C|3rf{E)Bf*QT$d3`T+jpkeo4*R;_Dqbz4 zroU9c%zUJGOv5{6+F{vc(v_wH)S&cM8qvQeI~9tcGtC;Hm=h>KzbHWZDzL1~)V`aL zW7MuEC;>y>?>La|?+^V;P4MNBUySj$gzq_47+K%63i0fO6LVEy(F7vM_!JQ#Q&*j_ z13{57swj8oM$>1%A#^2+O{6a2{AjM2UCKq^mvB_Kkww)&Q`WW%dlz7lB^YMInx*k~ zFJ@uQ7pbL7#Vp%N?!!h*<^eBB#tzXCVXB5JqTVcG)5joPX0^7m_dN*tQ`u%0Or#-5 zhjM|Aa8Q+Z#zXJ6hyQ?H?4_4Nr~MR1-j2=7x!gW8%}OO8Zl}nXXb-b0oO=MXdxrYB zPUmPL#+Z>WSUmMC?Zq+WWna?{c^l1I=no55*Q6a#23?4UiEp(DE6P8rOT4{?|95~x zXs#Rm6I`hY_9aY^8+P?ArH_h?JX<5T$PoQ|%qAjOHWr4gt;&HeoH|ltIUQhGym=JW z!ol@bvRCIHfcWYkAUe|7=BFZ;pI&_vc*F{U|0^KE?>)@miLESKKp9 zNeSfgsvJh5vQO~(c-_~bcen`ZGZHV1aPwcM4FCp=fNw!4|JlH_32xRcANO=XD~!O{ z$2(jd?pyQsweGx3pF7q~Sj*D95d4GZnjPE5#+0+X>}?r*zL_JY^*f=jZN_WyY;>{Z zb0RPZKIn^3n==HPEAC2BaVO3|mm%t9Adr_{a#T$Q(Pl~q!2vhu9J&EVr)Tam#KI;W^Ho;#o5bWS?Zq2P7;8M1*J^glM*|81x$ zUp749t99`v9>s~nqv6%(Q6k(@NI||Ycyue+fqKyma=DzX*+|gG@DD$WXon&w2cs!` z@_6rq%eOqQK?@HLU&2Zi{=XQ~|7TMF)fGAX{X^O|GGq+^_336&wMZ4efKGep#AgLk z=h@EXcBaG#j`I4eWKHGbaGVqz&X($apIh(1=l7#9wRcybcF9_zFi?|h)@)sBJk@1l zozN$bA;>+AEJ_sg{jRx#hB^DPxn?G7kEyWwEBL zlcS$np>0U(pdB}Ly?5}NsH%T)8jDkK9;fa(L(=N+RwKOI-#TZqa(Ss;`iOoNP6 zV(kxYI+cQ`0&^y;-g`w1yQ@!n*k|l5)G|I)zNv3MA zw;w@Avyap~H)*(_XMcP_*={j06WX)u?bm`|tS_a6Md`T;0@1gC-8DfU(c(5ymqdXBz};xKxY2J*Qv6R-*NP6mz=h3`EQnR+E;gZsED3 z*}){(Y$X0X8nb@c!}&=p{g@o}azc!_Q5Sm2+e5m46LTDgEzbk{RXB4b)NlgLmf7=F zfaKi4M)}U+Ai)M){PUjw@f!Hwmc)Pe=j9JB(ZYrlpPh8Y)<=b5W$2(-A)P;uJ2@`_ z;`De60>GEL^P0qHJeu~gW6z?|ABC&)3`gG=O;52ifjR6MnUb`n$tu2DQ-3%sLWjJ- zyL>b;P|ghlqHPoH)?J;sKeUC+uvps6j2BjqM;1i}=5^PCDYSfSaH9K;wE3=vb|xw+ zoIi{w1=DVNW*Y4cNHzwV+kEK10^X zX%*e5{>u7l37ZHTj5bB^zO{NT*`+Bl;Ia|_yHqBeS4WWkXJD;itWQsWWf{D9F!YN@28t}VifDR z{V`51Sz9l@EE+vv1r@Wv|JuPEA6Q zqIs(3^;0f5L{U*z({;L5a0HBv$=I^WFfyTmLQ|8@ONP+G8cu3Fk zxPYW-((!f%G|@@}fP*a!?#ch-F8_CJ^gn(MGp(_K*eKxHtFC9zi|_dp+oxSJ2PU@i zDnmD4T5HJ7yY8>wB|CHZc*raGFM=7M|0i>G;h@Dg4-H8FffK- zxzvfDa~Z@yYFVPy89Hv3)eoHnIZg_){D%BQUeNVBkbIG<#$x8MqVR7kUv6#$Ztge7 zeO3i$M5DRcI8Bt*U#WD#XJ%_nW~QuAA}808)w^qNx8~Xm=xd)bTUH{bu#l9ZHysj@ zeiW=B+>deUK$`qTU8-~Pp`?z@D?|N{+Pk7y{A;(r=BM=U z;aXNgIaely#SeL7JMS}a(nN0zHDTTOwgM$}VpTgn=T5|Wg|=n_Mt8MTOV(GW(Y#Ry zdL<1ZG#$<_5#;G}*0i=Bi6)m}Zn2E zxuag8N(!Wv%(l(&NnSCdiGpn?w{b2vQ7En!&VAl*;h0W$FI~cc$XQVTjL%7mstIx3 zvPN8Wk?p)#pS9F8UJx~>qS_ku6e3&dvhnaoy`4iUsX%n}khiAbVd`L2hyN^_a9dtP zr_$XZzK{Rs>Hb5OlY?Ezf^uTg2l#g#`KUgT&u(~G5_%GQdBsbzLhBbp8j;LGl1j#i z=CS}hjl)kZEorhe`5Z}n!P7i&&i^x({Ev01M)Tp#=RM%c)Q?gk zm?ydc>8s;OmZSy;H#=xX>pzCKcR-&-Hb#l3au{0^06e7&Dd<>vTzmh5QwZvR>J&+?Py z8`Q#|759cNo`aEydZ;^qzD|^3Yb<7RMbNBOAxc->HWU8=lz?&xz`5pTl(R)R1QKB( zQ11V}+MqG-{XA>u2!9c&BiZRjfFQQM-jLIr{onJfV}tEqQq1(#Qz@HI_qH<8c=B!w zO+>;HZm7FCx+Y!9-j+uN@4n8+O_3!lCB(_|0G|Fculv8{+raNOuH?{As0uGmhA+$q z^@APD{m1N?*-A5&WWc>8ChG`Xzfv$aQ4KyGHShioS-uz+tWfcw{%t8o;#1TUj|(>h z7_uLyE}*>~MqzaNR-LS&l>)go$Gr*>T2L|QH5rnmnjT-QWKokI|Lxe>0;UR16!n^W z$J_IFd5t>ZMQe&U6S2k;s13597W^POv^uR9nw+9=TY?)r_JnfzsLmupaV%3yKmyly zx0Bm}V}&y!QeS{ZVPM5pY(`}bpK)6djnrHnCpE*H^AM)l7D^ z*s1Gx@8dh}jmo1@GS2({^fQPv4An!I2l=F7930 zZ@A4WZQT=3)s-iAALJI);PL#Y9P-@b?&(7d-M1#)_PM2L8yNJAr|BlkJmZ(4YMWD~ zFK(Cj3DsO3ArpJlJ9Q#w(ShH=3)?&%Rm@pG<@n^jLN8z5Q@3XssxIA=pp)|M`|1m) z1zjBs#FvW-l}mY9a~gWeJYfSa4$@fck}UP`z>*s$*Dg*|Tt1OO7C5FO@?cRq%X3p* zZKkSeYp)z);3=q7x%%4rrB1%mEV09y&+mj8cc*ndFI`r#@7|}BruSMqnV-GL-ZDLG z(!DdaT?*lnC!Tk`UaXgzJa0**sm6;sg)91|8IL<7nC?8kwAWF_K&I#W(Y!rpF7Mj$ y`1P$7zJ)4hg6^ct`UVN>+#5FHi( diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index ba05bbe4365..ddb5897366b 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -104,18 +104,16 @@ def test_exif(test_file: str) -> None: def test_frame_size() -> None: - # This image has been hexedited to contain a different size - # in the SOF marker of the second frame - with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: - assert im.size == (640, 480) + with Image.open("Tests/images/frame_size.mpo") as im: + assert im.size == (56, 70) im.load() im.seek(1) - assert im.size == (680, 480) + assert im.size == (349, 434) im.load() im.seek(0) - assert im.size == (640, 480) + assert im.size == (56, 70) def test_ignore_frame_size() -> None: From 913a6d8390b57dde5f1dcfbd4a0fa8887d992e8a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Sep 2025 09:12:01 +1000 Subject: [PATCH 2064/2374] Updated harfbuzz to 11.5.0 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 1fa63409626..3d016b5e259 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -94,7 +94,7 @@ ARCHIVE_SDIR=pillow-depends-main # annotations have a source code patch that is required for some platforms. If # you change those versions, ensure the patch is also updated. FREETYPE_VERSION=2.13.3 -HARFBUZZ_VERSION=11.4.5 +HARFBUZZ_VERSION=11.5.0 LIBPNG_VERSION=1.6.50 JPEGTURBO_VERSION=3.1.2 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5c638829e18..d21b549b6b0 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ def cmd_msbuild( "BROTLI": "1.1.0", "FREETYPE": "2.14.1", "FRIBIDI": "1.0.16", - "HARFBUZZ": "11.4.5", + "HARFBUZZ": "11.5.0", "JPEGTURBO": "3.1.2", "LCMS2": "2.17", "LIBAVIF": "1.3.0", From d42e537efeb1bd11cd9df1db1c7d7a6dc529d9e2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 12:17:40 +1000 Subject: [PATCH 2065/2374] Update dependency cibuildwheel to v3.2.0 (#9219) --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index d87d7956f75..8ec7262c090 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.1.4 +cibuildwheel==3.2.0 From 2c438830736a5cb17cd9aea8c8aacb67a11dd86c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Sep 2025 21:01:16 +1000 Subject: [PATCH 2066/2374] Updated libtiff to 4.7.1 --- .github/workflows/wheels-dependencies.sh | 2 +- docs/installation/building-from-source.rst | 2 +- winbuild/build_prepare.py | 8 +------- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index cbeee8f9dc9..8a398576389 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -100,7 +100,7 @@ JPEGTURBO_VERSION=3.1.2 OPENJPEG_VERSION=2.5.4 XZ_VERSION=5.8.1 ZSTD_VERSION=1.5.7 -TIFF_VERSION=4.7.0 +TIFF_VERSION=4.7.1 LCMS2_VERSION=2.17 ZLIB_NG_VERSION=2.2.5 LIBWEBP_VERSION=1.6.0 diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 656d54325d4..6080d29afa3 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -44,7 +44,7 @@ Many of Pillow's features require external libraries: * **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 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b28aa8caa8b..e00f6185d0c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -124,7 +124,7 @@ def cmd_msbuild( "LIBPNG": "1.6.50", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.4", - "TIFF": "4.7.0", + "TIFF": "4.7.1", "XZ": "5.8.1", "ZLIBNG": "2.2.5", } @@ -228,12 +228,6 @@ def cmd_msbuild( # link against libwebp.lib "#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": [ *cmds_cmake( From 637f25dc2c7f1593bc7fca0ce3654feaff752b59 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Sep 2025 21:01:33 +1000 Subject: [PATCH 2067/2374] Revert "Allow cmake<4 when building libtiff" This reverts commit 81412212016a70eb160460e26dc552a0f8a8c153. --- winbuild/build_prepare.py | 1 - 1 file changed, 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e00f6185d0c..76f05bdcb19 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -235,7 +235,6 @@ def cmd_msbuild( "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DWebP_LIBRARY=libwebp", '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', - "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", ) ], "headers": [r"libtiff\tiff*.h"], From e2a8e217dad1445caff35092695264eefbbf8ae7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Sep 2025 23:18:47 +1000 Subject: [PATCH 2068/2374] Removed _expand() --- Tests/test_image.py | 27 --------------------------- src/PIL/Image.py | 6 ------ 2 files changed, 33 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index eb3882ddcea..17864436527 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -284,33 +284,6 @@ def test_comparison_with_other_type(self) -> None: assert item is not None 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: # Assert assert hopper("RGB").getbands() == ("R", "G", "B") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b17fd131d2c..708e8589910 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1336,12 +1336,6 @@ def draft( """ 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: """ Filters this image using the given filter. For a list of From a953d86b4db252d614312e2684c0c1f459d6a796 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:11:53 +1000 Subject: [PATCH 2069/2374] Python 3.9 wheels are no longer needed (#9214) --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 81a68813526..68a446f7922 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -54,7 +54,7 @@ jobs: platform: macos os: macos-13 cibw_arch: x86_64 - build: "cp3{9,10,11}*" + build: "cp3{10,11}*" macosx_deployment_target: "10.10" - name: "macOS 10.13 x86_64" platform: macos From 0bcfd3b55c350269c7275067bc10bc095d0df27c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Oct 2025 21:35:26 +1000 Subject: [PATCH 2070/2374] Updated Python version --- winbuild/README.md | 2 +- winbuild/build.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/README.md b/winbuild/README.md index 62345af60bc..db71f094e0e 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -16,7 +16,7 @@ For more extensive info, see the [Windows build instructions](build.rst). Here's an example script to build on Windows: ``` -set PYTHON=C:\Python39\bin +set PYTHON=C:\Python310\bin cd /D C:\Pillow\winbuild %PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends build\build_dep_all.cmd diff --git a/winbuild/build.rst b/winbuild/build.rst index aa4677ad595..23b26c42252 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -115,7 +115,7 @@ Example Here's an example script to build on Windows:: - set PYTHON=C:\Python39\bin + set PYTHON=C:\Python310\bin cd /D C:\Pillow\winbuild %PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends build\build_dep_all.cmd From 7cb518031ad64d9566f8d20d51c5da784023d49f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Oct 2025 22:21:16 +1000 Subject: [PATCH 2071/2374] Updated FreeType to 2.14.1 on macOS and Linux --- .github/workflows/wheels-dependencies.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index cbeee8f9dc9..bc490a38a26 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -93,7 +93,11 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds. Version numbers with "Patched" # annotations have a source code patch that is required for some platforms. If # you change those versions, ensure the patch is also updated. -FREETYPE_VERSION=2.13.3 +if [[ -n "$IOS_SDK" ]]; then + FREETYPE_VERSION=2.13.3 +else + FREETYPE_VERSION=2.14.1 +fi HARFBUZZ_VERSION=11.5.0 LIBPNG_VERSION=1.6.50 JPEGTURBO_VERSION=3.1.2 @@ -314,6 +318,10 @@ function build { if [[ -n "$IS_MACOS" ]]; then # 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 else build_freetype From 0c0ff7c38f91182b30b979a81423efd84deb3805 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 3 Oct 2025 20:27:42 +1000 Subject: [PATCH 2072/2374] Removed use of sudo from libavif and raqm install scripts --- .ci/install.sh | 4 ++-- depends/install_libavif.sh | 2 +- depends/install_raqm.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 2178c664626..52b8214170c 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -51,10 +51,10 @@ pushd depends && ./install_webp.sh && popd pushd depends && ./install_imagequant.sh && popd # raqm -pushd depends && ./install_raqm.sh && popd +pushd depends && sudo ./install_raqm.sh && popd # libavif -pushd depends && ./install_libavif.sh && popd +pushd depends && sudo ./install_libavif.sh && popd # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh index 26af8a36ce7..50ba0175567 100755 --- a/depends/install_libavif.sh +++ b/depends/install_libavif.sh @@ -59,6 +59,6 @@ cmake \ "${LIBAVIF_CMAKE_FLAGS[@]}" \ . -sudo make install +make install popd diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index b5a05100ba2..33bb2d0a77f 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -8,6 +8,6 @@ archive=libraqm-0.10.3 pushd $archive -meson build --prefix=/usr && sudo ninja -C build install +meson build --prefix=/usr && ninja -C build install popd From b3d1836907796e6d6f3c590c9a1860f6ecb04246 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 4 Oct 2025 19:49:09 +1000 Subject: [PATCH 2073/2374] Update harfbuzz to 12.1.0 (#9218) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 68c2eea30e6..69c867b4dba 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -98,7 +98,7 @@ if [[ -n "$IOS_SDK" ]]; then else FREETYPE_VERSION=2.14.1 fi -HARFBUZZ_VERSION=11.5.0 +HARFBUZZ_VERSION=12.1.0 LIBPNG_VERSION=1.6.50 JPEGTURBO_VERSION=3.1.2 OPENJPEG_VERSION=2.5.4 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 76f05bdcb19..186a80cca5e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ def cmd_msbuild( "BROTLI": "1.1.0", "FREETYPE": "2.14.1", "FRIBIDI": "1.0.16", - "HARFBUZZ": "11.5.0", + "HARFBUZZ": "12.1.0", "JPEGTURBO": "3.1.2", "LCMS2": "2.17", "LIBAVIF": "1.3.0", From 09e571780ec33df260c0dccfb7efdf59cdea0d8d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:29:41 +0000 Subject: [PATCH 2074/2374] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.11 → v0.13.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.11...v0.13.3) - [github.com/psf/black-pre-commit-mirror: 25.1.0 → 25.9.0](https://github.com/psf/black-pre-commit-mirror/compare/25.1.0...25.9.0) - [github.com/pre-commit/mirrors-clang-format: v21.1.0 → v21.1.2](https://github.com/pre-commit/mirrors-clang-format/compare/v21.1.0...v21.1.2) - [github.com/python-jsonschema/check-jsonschema: 0.33.3 → 0.34.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.33.3...0.34.0) - [github.com/zizmorcore/zizmor-pre-commit: v1.12.1 → v1.14.2](https://github.com/zizmorcore/zizmor-pre-commit/compare/v1.12.1...v1.14.2) - [github.com/tox-dev/pyproject-fmt: v2.6.0 → v2.7.0](https://github.com/tox-dev/pyproject-fmt/compare/v2.6.0...v2.7.0) --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23bda1ec76b..ab0153687d2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.11 + rev: v0.13.3 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 25.1.0 + rev: 25.9.0 hooks: - id: black @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v21.1.0 + rev: v21.1.2 hooks: - id: clang-format types: [c] @@ -51,14 +51,14 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.3 + rev: 0.34.0 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.12.1 + rev: v1.14.2 hooks: - id: zizmor @@ -68,7 +68,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.6.0 + rev: v2.7.0 hooks: - id: pyproject-fmt From 7259685ba4d05a77ae802920bf54c02a84b6db79 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 7 Oct 2025 09:05:53 +1100 Subject: [PATCH 2075/2374] Build Python 3.14 on macOS 10.15 --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 68a446f7922..f1c851bc7c8 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -60,13 +60,13 @@ jobs: platform: macos os: macos-13 cibw_arch: x86_64 - build: "cp3{12,13,14}*" + build: "cp3{12,13}*" macosx_deployment_target: "10.13" - name: "macOS 10.15 x86_64" platform: macos os: macos-13 cibw_arch: x86_64 - build: "pp3*" + build: "{cp314,pp3}*" macosx_deployment_target: "10.15" - name: "macOS arm64" platform: macos From 6d19b8adeff16674e62bd1e0aed95f29ff1932fb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Jul 2025 21:58:41 +1000 Subject: [PATCH 2076/2374] Do not allow negative offset with memory mapping --- Tests/test_imagefile.py | 5 +++++ src/PIL/ImageFile.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index d4dfb1b6d59..7dfb3abf986 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -164,6 +164,11 @@ def test_negative_stride(self) -> None: with pytest.raises(OSError): p.close() + def test_negative_offset(self) -> None: + with Image.open("Tests/images/raw_negative_stride.bin") as im: + with pytest.raises(ValueError, match="Tile offset cannot be negative"): + im.load() + def test_no_format(self) -> None: buf = BytesIO(b"\x00" * 255) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index e33b846d416..a1d98bd5103 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -313,6 +313,9 @@ def load(self) -> Image.core.PixelAccess | None: and args[0] == self.mode and args[0] in Image._MAPMODES ): + if offset < 0: + msg = "Tile offset cannot be negative" + raise ValueError(msg) try: # use mmap, if possible import mmap From 1d4cda65cf31d012690c1637ed1046a5de1448b7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 12 Sep 2025 16:27:38 +1000 Subject: [PATCH 2077/2374] Cast to UINT32 before shifting bits --- src/libImaging/SgiRleDecode.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index e604689908c..a562f582cb0 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -22,7 +22,8 @@ static void read4B(UINT32 *dest, UINT8 *buf) { - *dest = (UINT32)((buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]); + *dest = ((UINT32)buf[0] << 24) | ((UINT32)buf[1] << 16) | ((UINT32)buf[2] << 8) | + buf[3]; } /* From a2ef220b320b82cc6a42ef65fba4dbddd360107a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Oct 2025 21:01:42 +1100 Subject: [PATCH 2078/2374] Cast before additional shifting --- src/libImaging/Access.c | 10 ++++++---- src/libImaging/BcnEncode.c | 10 +++++----- src/libImaging/FliDecode.c | 6 ++++-- src/libImaging/GetBBox.c | 4 ++-- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 3db52377e80..65c832cbeff 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -64,7 +64,7 @@ static void get_pixel_16L(Imaging im, int x, int y, void *color) { UINT8 *in = (UINT8 *)&im->image[y][x + x]; #ifdef WORDS_BIGENDIAN - UINT16 out = in[0] + (in[1] << 8); + UINT16 out = in[0] + ((UINT16)in[1] << 8); memcpy(color, &out, sizeof(out)); #else memcpy(color, in, sizeof(UINT16)); @@ -77,7 +77,7 @@ get_pixel_16B(Imaging im, int x, int y, void *color) { #ifdef WORDS_BIGENDIAN memcpy(color, in, sizeof(UINT16)); #else - UINT16 out = in[1] + (in[0] << 8); + UINT16 out = in[1] + ((UINT16)in[0] << 8); memcpy(color, &out, sizeof(out)); #endif } @@ -91,7 +91,8 @@ static void get_pixel_32L(Imaging im, int x, int y, void *color) { UINT8 *in = (UINT8 *)&im->image[y][x * 4]; #ifdef WORDS_BIGENDIAN - INT32 out = in[0] + (in[1] << 8) + (in[2] << 16) + (in[3] << 24); + INT32 out = + in[0] + ((INT32)in[1] << 8) + ((INT32)in[2] << 16) + ((INT32)in[3] << 24); memcpy(color, &out, sizeof(out)); #else memcpy(color, in, sizeof(INT32)); @@ -104,7 +105,8 @@ get_pixel_32B(Imaging im, int x, int y, void *color) { #ifdef WORDS_BIGENDIAN memcpy(color, in, sizeof(INT32)); #else - INT32 out = in[3] + (in[2] << 8) + (in[1] << 16) + (in[0] << 24); + INT32 out = + in[3] + ((INT32)in[2] << 8) + ((INT32)in[1] << 16) + ((INT32)in[0] << 24); memcpy(color, &out, sizeof(out)); #endif } diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c index 7a5072ddee6..861ae1c261c 100644 --- a/src/libImaging/BcnEncode.c +++ b/src/libImaging/BcnEncode.c @@ -36,10 +36,9 @@ decode_565(UINT16 x) { static UINT16 encode_565(rgba item) { - UINT8 r, g, b; - r = item.color[0] >> (8 - 5); - g = item.color[1] >> (8 - 6); - b = item.color[2] >> (8 - 5); + UINT16 r = item.color[0] >> (8 - 5); + UINT8 g = item.color[1] >> (8 - 6); + UINT8 b = item.color[2] >> (8 - 5); return (r << (5 + 6)) | (g << 5) | b; } @@ -157,7 +156,8 @@ encode_bc1_color(Imaging im, ImagingCodecState state, UINT8 *dst, int separate_a static void encode_bc2_block(Imaging im, ImagingCodecState state, UINT8 *dst) { int i, j; - UINT8 block[16], current_alpha; + UINT8 block[16]; + UINT32 current_alpha; for (i = 0; i < 4; i++) { for (j = 0; j < 4; j++) { int x = state->x + i * im->pixelsize; diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index 130ecb7f75d..44994823e5d 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -16,9 +16,11 @@ #include "Imaging.h" -#define I16(ptr) ((ptr)[0] + ((ptr)[1] << 8)) +#define I16(ptr) ((ptr)[0] + ((int)(ptr)[1] << 8)) -#define I32(ptr) ((ptr)[0] + ((ptr)[1] << 8) + ((ptr)[2] << 16) + ((ptr)[3] << 24)) +#define I32(ptr) \ + ((ptr)[0] + ((INT32)(ptr)[1] << 8) + ((INT32)(ptr)[2] << 16) + \ + ((INT32)(ptr)[3] << 24)) #define ERR_IF_DATA_OOB(offset) \ if ((data + (offset)) > ptr + bytes) { \ diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index d430893ddb2..e50bd7140b9 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -212,7 +212,7 @@ ImagingGetExtrema(Imaging im, void *extrema) { UINT16 v; UINT8 *pixel = *im->image8; #ifdef WORDS_BIGENDIAN - v = pixel[0] + (pixel[1] << 8); + v = pixel[0] + ((UINT16)pixel[1] << 8); #else memcpy(&v, pixel, sizeof(v)); #endif @@ -221,7 +221,7 @@ ImagingGetExtrema(Imaging im, void *extrema) { for (x = 0; x < im->xsize; x++) { pixel = (UINT8 *)im->image[y] + x * sizeof(v); #ifdef WORDS_BIGENDIAN - v = pixel[0] + (pixel[1] << 8); + v = pixel[0] + ((UINT16)pixel[1] << 8); #else memcpy(&v, pixel, sizeof(v)); #endif From 2b4c7c011eef28401828f979f1005ad80bbf8709 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 10 Oct 2025 11:55:45 +0100 Subject: [PATCH 2079/2374] Typing import suggestion Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b9f5cfe0610..3a72a0742c1 100644 --- a/setup.py +++ b/setup.py @@ -16,12 +16,12 @@ import sys import warnings from collections.abc import Iterator -from typing import TYPE_CHECKING from pybind11.setup_helpers import ParallelCompile from setuptools import Extension, setup from setuptools.command.build_ext import build_ext +TYPE_CHECKING = False if TYPE_CHECKING: from setuptools import _BuildInfo From bd6e70fccdf14f9a2d0ff4201c4c82b3cb84572f Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 10 Oct 2025 12:31:15 +0100 Subject: [PATCH 2080/2374] Check against mode 1 instead of input mode for Chops.c --- src/libImaging/Chops.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libImaging/Chops.c b/src/libImaging/Chops.c index 331f2dfe64c..3ce8a0903e0 100644 --- a/src/libImaging/Chops.c +++ b/src/libImaging/Chops.c @@ -64,7 +64,8 @@ create(Imaging im1, Imaging im2, const ModeID mode) { int xsize, ysize; if (!im1 || !im2 || im1->type != IMAGING_TYPE_UINT8 || - (mode != IMAGING_MODE_UNKNOWN && (im1->mode != mode || im2->mode != mode))) { + (mode != IMAGING_MODE_UNKNOWN && + (im1->mode != IMAGING_MODE_1 || im2->mode != IMAGING_MODE_1))) { return (Imaging)ImagingError_ModeError(); } if (im1->type != im2->type || im1->bands != im2->bands) { From 5d3086b01ff356c123bd2d9ea929a0bee030c08c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Oct 2025 22:44:09 +1100 Subject: [PATCH 2081/2374] Removed unused access for I;32L and I;32B --- src/libImaging/Access.c | 45 ++--------------------------------------- 1 file changed, 2 insertions(+), 43 deletions(-) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 65c832cbeff..00aaaa405c6 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -12,8 +12,8 @@ #include "Imaging.h" /* use make_hash.py from the pillow-scripts repository to calculate these values */ -#define ACCESS_TABLE_SIZE 35 -#define ACCESS_TABLE_HASH 8940 +#define ACCESS_TABLE_SIZE 23 +#define ACCESS_TABLE_HASH 28677 static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE]; @@ -87,30 +87,6 @@ get_pixel_32(Imaging im, int x, int y, void *color) { memcpy(color, &im->image32[y][x], sizeof(INT32)); } -static void -get_pixel_32L(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image[y][x * 4]; -#ifdef WORDS_BIGENDIAN - INT32 out = - in[0] + ((INT32)in[1] << 8) + ((INT32)in[2] << 16) + ((INT32)in[3] << 24); - memcpy(color, &out, sizeof(out)); -#else - memcpy(color, in, sizeof(INT32)); -#endif -} - -static void -get_pixel_32B(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image[y][x * 4]; -#ifdef WORDS_BIGENDIAN - memcpy(color, in, sizeof(INT32)); -#else - INT32 out = - in[3] + ((INT32)in[2] << 8) + ((INT32)in[1] << 16) + ((INT32)in[0] << 24); - memcpy(color, &out, sizeof(out)); -#endif -} - /* store individual pixel */ static void @@ -131,21 +107,6 @@ put_pixel_16B(Imaging im, int x, int y, const void *color) { out[1] = in[0]; } -static void -put_pixel_32L(Imaging im, int x, int y, const void *color) { - memcpy(&im->image8[y][x * 4], color, 4); -} - -static void -put_pixel_32B(Imaging im, int x, int y, const void *color) { - const char *in = color; - UINT8 *out = (UINT8 *)&im->image8[y][x * 4]; - out[0] = in[3]; - out[1] = in[2]; - out[2] = in[1]; - out[3] = in[0]; -} - static void put_pixel_32(Imaging im, int x, int y, const void *color) { memcpy(&im->image32[y][x], color, sizeof(INT32)); @@ -174,8 +135,6 @@ ImagingAccessInit(void) { #else ADD("I;16N", get_pixel_16L, put_pixel_16L); #endif - ADD("I;32L", get_pixel_32L, put_pixel_32L); - ADD("I;32B", get_pixel_32B, put_pixel_32B); ADD("F", get_pixel_32, put_pixel_32); ADD("P", get_pixel_8, put_pixel_8); ADD("PA", get_pixel_32_2bands, put_pixel_32); From 324258ca7a1836e0fb42fa84038619a4f3f8abd8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 22 Jul 2025 22:59:15 +1000 Subject: [PATCH 2082/2374] Split parametrization --- Tests/test_arro3.py | 23 +++++++++-------------- Tests/test_nanoarrow.py | 23 +++++++++-------------- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/Tests/test_arro3.py b/Tests/test_arro3.py index a7c755fc2b6..92493d9b056 100644 --- a/Tests/test_arro3.py +++ b/Tests/test_arro3.py @@ -225,23 +225,18 @@ def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> Non @pytest.mark.parametrize( - "mode, data_tp, mask", + "mode, mask", ( - ("LA", UINT32, [0, 3]), - ("RGB", UINT32, [0, 1, 2]), - ("RGBA", UINT32, None), - ("CMYK", UINT32, None), - ("YCbCr", UINT32, [0, 1, 2]), - ("HSV", UINT32, [0, 1, 2]), - ("LA", INT32, [0, 3]), - ("RGB", INT32, [0, 1, 2]), - ("RGBA", INT32, None), - ("CMYK", INT32, None), - ("YCbCr", INT32, [0, 1, 2]), - ("HSV", INT32, [0, 1, 2]), + ("LA", [0, 3]), + ("RGB", [0, 1, 2]), + ("RGBA", None), + ("CMYK", None), + ("YCbCr", [0, 1, 2]), + ("HSV", [0, 1, 2]), ), ) -def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: +@pytest.mark.parametrize("data_tp", (UINT32, INT32)) +def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None: (dtype, elt, elts_per_pixel) = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] diff --git a/Tests/test_nanoarrow.py b/Tests/test_nanoarrow.py index b08333ae97b..3a839a01569 100644 --- a/Tests/test_nanoarrow.py +++ b/Tests/test_nanoarrow.py @@ -232,23 +232,18 @@ def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> Non @pytest.mark.parametrize( - "mode, data_tp, mask", + "mode, mask", ( - ("LA", UINT32, [0, 3]), - ("RGB", UINT32, [0, 1, 2]), - ("RGBA", UINT32, None), - ("CMYK", UINT32, None), - ("YCbCr", UINT32, [0, 1, 2]), - ("HSV", UINT32, [0, 1, 2]), - ("LA", INT32, [0, 3]), - ("RGB", INT32, [0, 1, 2]), - ("RGBA", INT32, None), - ("CMYK", INT32, None), - ("YCbCr", INT32, [0, 1, 2]), - ("HSV", INT32, [0, 1, 2]), + ("LA", [0, 3]), + ("RGB", [0, 1, 2]), + ("RGBA", None), + ("CMYK", None), + ("YCbCr", [0, 1, 2]), + ("HSV", [0, 1, 2]), ), ) -def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: +@pytest.mark.parametrize("data_tp", (UINT32, INT32)) +def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None: (dtype, elt, elts_per_pixel) = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] From 13e4e587e65fe652a1392244e746715f8da740d7 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 10 Oct 2025 15:34:11 +0100 Subject: [PATCH 2083/2374] added import-not-found ignores, removed call-overload ignores --- Tests/test_arro3.py | 17 ++++++++++------- Tests/test_nanoarrow.py | 16 ++++++++-------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/Tests/test_arro3.py b/Tests/test_arro3.py index 92493d9b056..a161a7a9667 100644 --- a/Tests/test_arro3.py +++ b/Tests/test_arro3.py @@ -16,7 +16,9 @@ TYPE_CHECKING = False if TYPE_CHECKING: - from arro3 import compute + from arro3 import compute # type: ignore [import-not-found] + + # type: ignore [import-not-found] from arro3.core import Array, DataType, Field, fixed_size_list_array else: arro3 = pytest.importorskip("arro3", reason="Arro3 not installed") @@ -106,7 +108,7 @@ def test_to_array(mode: str, dtype: DataType, mask: list[int] | None) -> None: img = img.crop((3, 0, 124, 127)) assert img.size == (121, 127) - arr = Array(img) # type: ignore[call-overload] + arr = Array(img) _test_img_equals_pyarray(img, arr, mask) assert arr.type == dtype @@ -123,8 +125,8 @@ def test_lifetime() -> None: img = hopper("L") - arr_1 = Array(img) # type: ignore[call-overload] - arr_2 = Array(img) # type: ignore[call-overload] + arr_1 = Array(img) + arr_2 = Array(img) del img @@ -141,8 +143,8 @@ def test_lifetime2() -> None: img = hopper("L") - arr_1 = Array(img) # type: ignore[call-overload] - arr_2 = Array(img) # type: ignore[call-overload] + arr_1 = Array(img) + arr_2 = Array(img) assert compute.sum(arr_1).as_py() > 0 del arr_1 @@ -261,8 +263,9 @@ def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) def test_image_metadata(mode: str, metadata: list[str]) -> None: img = hopper(mode) - arr = Array(img) # type: ignore[call-overload] + arr = Array(img) + assert arr.type.value_field assert arr.type.value_field.metadata assert arr.type.value_field.metadata[b"image"] diff --git a/Tests/test_nanoarrow.py b/Tests/test_nanoarrow.py index 3a839a01569..fe7505134af 100644 --- a/Tests/test_nanoarrow.py +++ b/Tests/test_nanoarrow.py @@ -16,7 +16,7 @@ TYPE_CHECKING = False if TYPE_CHECKING: - import nanoarrow + import nanoarrow # type: ignore [import-untyped] else: nanoarrow = pytest.importorskip("nanoarrow", reason="Nanoarrow not installed") @@ -105,7 +105,7 @@ def test_to_array(mode: str, dtype: nanoarrow, mask: list[int] | None) -> None: img = img.crop((3, 0, 124, 127)) assert img.size == (121, 127) - arr = nanoarrow.Array(img) # type: ignore[call-overload] + arr = nanoarrow.Array(img) _test_img_equals_pyarray(img, arr, mask) assert arr.schema.type == dtype.type assert arr.schema.nullable == dtype.nullable @@ -123,8 +123,8 @@ def test_lifetime() -> None: img = hopper("L") - arr_1 = nanoarrow.Array(img) # type: ignore[call-overload] - arr_2 = nanoarrow.Array(img) # type: ignore[call-overload] + arr_1 = nanoarrow.Array(img) + arr_2 = nanoarrow.Array(img) del img @@ -141,8 +141,8 @@ def test_lifetime2() -> None: img = hopper("L") - arr_1 = nanoarrow.Array(img) # type: ignore[call-overload] - arr_2 = nanoarrow.Array(img) # type: ignore[call-overload] + arr_1 = nanoarrow.Array(img) + arr_2 = nanoarrow.Array(img) assert sum(arr_1.iter_py()) > 0 del arr_1 @@ -270,7 +270,7 @@ def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) def test_image_nested_metadata(mode: str, metadata: list[str]) -> None: img = hopper(mode) - arr = nanoarrow.Array(img) # type: ignore[call-overload] + arr = nanoarrow.Array(img) assert arr.schema.value_type.metadata assert arr.schema.value_type.metadata[b"image"] @@ -294,7 +294,7 @@ def test_image_nested_metadata(mode: str, metadata: list[str]) -> None: def test_image_flat_metadata(mode: str, metadata: list[str]) -> None: img = hopper(mode) - arr = nanoarrow.Array(img) # type: ignore[call-overload] + arr = nanoarrow.Array(img) assert arr.schema.metadata assert arr.schema.metadata[b"image"] From b4fe17cecf0f5c6bce2e8c637a7e3c40601ec665 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 10 Oct 2025 15:39:47 +0100 Subject: [PATCH 2084/2374] More typey lint --- Tests/test_arro3.py | 9 ++++++--- Tests/test_nanoarrow.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Tests/test_arro3.py b/Tests/test_arro3.py index a161a7a9667..9c70daf5a95 100644 --- a/Tests/test_arro3.py +++ b/Tests/test_arro3.py @@ -17,9 +17,12 @@ TYPE_CHECKING = False if TYPE_CHECKING: from arro3 import compute # type: ignore [import-not-found] - - # type: ignore [import-not-found] - from arro3.core import Array, DataType, Field, fixed_size_list_array + from arro3.core import ( # type: ignore [import-not-found] + Array, + DataType, + Field, + fixed_size_list_array, + ) else: arro3 = pytest.importorskip("arro3", reason="Arro3 not installed") from arro3 import compute diff --git a/Tests/test_nanoarrow.py b/Tests/test_nanoarrow.py index fe7505134af..90293130e38 100644 --- a/Tests/test_nanoarrow.py +++ b/Tests/test_nanoarrow.py @@ -16,7 +16,7 @@ TYPE_CHECKING = False if TYPE_CHECKING: - import nanoarrow # type: ignore [import-untyped] + import nanoarrow # type: ignore [import-not-found] else: nanoarrow = pytest.importorskip("nanoarrow", reason="Nanoarrow not installed") From 76ab80f10b50dd9abc5ef4fc4a0e537dfb1f8505 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jun 2025 14:08:15 +1000 Subject: [PATCH 2085/2374] Assert getpixel returns tuple --- Tests/test_file_avif.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 505d2e59639..727191153a1 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -221,6 +221,7 @@ def test_file_pointer_could_be_reused(self) -> None: def test_background_from_gif(self, tmp_path: Path) -> None: with Image.open("Tests/images/chi.gif") as im: original_value = im.convert("RGB").getpixel((1, 1)) + assert isinstance(original_value, tuple) # Save as AVIF out_avif = tmp_path / "temp.avif" @@ -233,6 +234,7 @@ def test_background_from_gif(self, tmp_path: Path) -> None: with Image.open(out_gif) as reread: reread_value = reread.convert("RGB").getpixel((1, 1)) + assert isinstance(reread_value, tuple) difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)]) assert difference <= 6 From 755ebb8307f887a69a75c0fe024eaed963c411bf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jun 2025 14:19:27 +1000 Subject: [PATCH 2086/2374] Assert getcolors does not return None --- Tests/test_file_png.py | 24 ++++++++++++++++++------ Tests/test_file_tga.py | 8 ++++++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ce655235410..dc1077fedab 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -229,7 +229,9 @@ def test_load_transparent_p(self) -> None: assert_image(im, "RGBA", (162, 150)) # image has 124 unique alpha values - assert len(im.getchannel("A").getcolors()) == 124 + colors = im.getchannel("A").getcolors() + assert colors is not None + assert len(colors) == 124 def test_load_transparent_rgb(self) -> None: test_file = "Tests/images/rgb_trns.png" @@ -241,7 +243,9 @@ def test_load_transparent_rgb(self) -> None: assert_image(im, "RGBA", (64, 64)) # image has 876 transparent pixels - assert im.getchannel("A").getcolors()[0][0] == 876 + colors = im.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == 876 def test_save_p_transparent_palette(self, tmp_path: Path) -> None: in_file = "Tests/images/pil123p.png" @@ -262,7 +266,9 @@ def test_save_p_transparent_palette(self, tmp_path: Path) -> None: assert_image(im, "RGBA", (162, 150)) # image has 124 unique alpha values - assert len(im.getchannel("A").getcolors()) == 124 + colors = im.getchannel("A").getcolors() + assert colors is not None + assert len(colors) == 124 def test_save_p_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/p_trns_single.png" @@ -285,7 +291,9 @@ def test_save_p_single_transparency(self, tmp_path: Path) -> None: assert im.getpixel((31, 31)) == (0, 255, 52, 0) # image has 876 transparent pixels - assert im.getchannel("A").getcolors()[0][0] == 876 + colors = im.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == 876 def test_save_p_transparent_black(self, tmp_path: Path) -> None: # check if solid black image with full transparency @@ -313,7 +321,9 @@ def test_save_grayscale_transparency(self, tmp_path: Path) -> None: assert im.info["transparency"] == 255 im_rgba = im.convert("RGBA") - assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent + colors = im_rgba.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == num_transparent test_file = tmp_path / "temp.png" im.save(test_file) @@ -324,7 +334,9 @@ def test_save_grayscale_transparency(self, tmp_path: Path) -> None: assert_image_equal(im, test_im) test_im_rgba = test_im.convert("RGBA") - assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent + colors = test_im_rgba.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == num_transparent def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/caption_6_33_22.png" diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bd39de2e12e..bb8d3eefcc6 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -274,13 +274,17 @@ def test_save_l_transparency(tmp_path: Path) -> None: in_file = "Tests/images/la.tga" with Image.open(in_file) as im: assert im.mode == "LA" - assert im.getchannel("A").getcolors()[0][0] == num_transparent + colors = im.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == num_transparent out = tmp_path / "temp.tga" im.save(out) with Image.open(out) as test_im: assert test_im.mode == "LA" - assert test_im.getchannel("A").getcolors()[0][0] == num_transparent + colors = test_im.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == num_transparent assert_image_equal(im, test_im) From a66d0d1f05a7a07904961623037ffe4de11fa4f2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Oct 2025 14:48:13 +1100 Subject: [PATCH 2087/2374] Assert getpalette does not return None --- Tests/test_image_putpalette.py | 1 + Tests/test_imagesequence.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index f2c447f711c..661764b608a 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -62,6 +62,7 @@ def test_putpalette_with_alpha_values() -> None: expected = im.convert("RGBA") palette = im.getpalette() + assert palette is not None transparency = im.info.pop("transparency") palette_with_alpha_values = [] diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 7b9ac80bc5d..32da22e043f 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -76,9 +76,14 @@ def test_consecutive() -> None: def test_palette_mmap() -> None: # Using mmap in ImageFile can require to reload the palette. with Image.open("Tests/images/multipage-mmap.tiff") as im: - color1 = im.getpalette()[:3] + palette = im.getpalette() + assert palette is not None + color1 = palette[:3] im.seek(0) - color2 = im.getpalette()[:3] + + palette = im.getpalette() + assert palette is not None + color2 = palette[:3] assert color1 == color2 From 52413cf0dce42c5eab96209a0a271e125c6cce8c Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sat, 11 Oct 2025 08:25:07 +0100 Subject: [PATCH 2088/2374] Update Tests/test_arro3.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_arro3.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/test_arro3.py b/Tests/test_arro3.py index 9c70daf5a95..60955cfdb40 100644 --- a/Tests/test_arro3.py +++ b/Tests/test_arro3.py @@ -116,9 +116,6 @@ def test_to_array(mode: str, dtype: DataType, mask: list[int] | None) -> None: assert arr.type == dtype reloaded = Image.fromarrow(arr, mode, img.size) - - assert reloaded - assert_image_equal(img, reloaded) From fbdf607c7f85731cc66db42938f1cee580303023 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 11 Oct 2025 17:13:22 +0300 Subject: [PATCH 2089/2374] Wheels CI: Check number of expected dists (#9239) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> Co-authored-by: Andrew Murray --- .github/workflows/wheels.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index f1c851bc7c8..9de8a440fc4 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -39,6 +39,7 @@ concurrency: cancel-in-progress: true env: + EXPECTED_DISTS: 91 FORCE_COLOR: 1 jobs: @@ -250,9 +251,27 @@ jobs: name: dist-sdist path: dist/*.tar.gz + count-dists: + needs: [build-native-wheels, windows, sdist] + runs-on: ubuntu-latest + name: Count dists + steps: + - uses: actions/download-artifact@v5 + with: + pattern: dist-* + path: dist + merge-multiple: true + - name: "What did we get?" + run: | + ls -alR + echo "Number of dists, should be $EXPECTED_DISTS:" + files=$(ls dist 2>/dev/null | wc -l) + echo $files + [ "$files" -eq $EXPECTED_DISTS ] || exit 1 + scientific-python-nightly-wheels-publish: if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') - needs: [build-native-wheels, windows] + needs: count-dists runs-on: ubuntu-latest name: Upload wheels to scientific-python-nightly-wheels steps: @@ -269,7 +288,7 @@ jobs: pypi-publish: if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - needs: [build-native-wheels, windows, sdist] + needs: count-dists runs-on: ubuntu-latest name: Upload release to PyPI environment: From c874256132f67543e6928be9e97ad3c4bb806d3f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Oct 2025 07:08:52 +1100 Subject: [PATCH 2090/2374] Support saving variable length rational TIFF tags by default --- Tests/test_file_libtiff.py | 9 +++++++++ src/PIL/TiffImagePlugin.py | 10 +++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 18bcfaa2088..4908496cff3 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -367,6 +367,15 @@ def test_whitepoint_tag( assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[318] == pytest.approx((0.3127, 0.3289)) + # Save tag by default + out = tmp_path / "temp2.tif" + with Image.open("Tests/images/rdf.tif") as im: + im.save(out) + + with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) + assert reloaded.tag_v2[318] == pytest.approx((0.3127, 0.3289999)) + def test_xmlpacket_tag( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index c1741284b9f..de2ce066ebf 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -252,6 +252,7 @@ (II, 3, (1,), 1, (8,), ()): ("P", "P"), (MM, 3, (1,), 1, (8,), ()): ("P", "P"), (II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), + (MM, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), @@ -1177,6 +1178,7 @@ def _open(self) -> None: """Open the first image in a TIFF file""" # Header + assert self.fp is not None ifh = self.fp.read(8) if ifh[2] == 43: ifh += self.fp.read(8) @@ -1343,6 +1345,7 @@ def _load_libtiff(self) -> Image.core.PixelAccess | None: # To be nice on memory footprint, if there's a # file descriptor, use that instead of reading # into a string in python. + assert self.fp is not None try: fp = hasattr(self.fp, "fileno") and self.fp.fileno() # flush the file descriptor, prevents error on pypy 2.4+ @@ -1936,9 +1939,10 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: types[tag] = TiffTags.LONG8 elif tag in ifd.tagtype: types[tag] = ifd.tagtype[tag] - elif not (isinstance(value, (int, float, str, bytes))): - continue - else: + elif isinstance(value, (int, float, str, bytes)) or ( + isinstance(value, tuple) + and all(isinstance(v, (int, float, IFDRational)) for v in value) + ): type = TiffTags.lookup(tag).type if type: types[tag] = type From e36bf768c5ae17a64e86433f7fb2bd0d67fb1a91 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Oct 2025 15:58:22 +1100 Subject: [PATCH 2091/2374] Added four private SGI tags --- src/PIL/TiffTags.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 86adaa45857..761aa3f6b08 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -203,6 +203,11 @@ def lookup(tag: int, group: int | None = None) -> TagInfo: 531: ("YCbCrPositioning", SHORT, 1), 532: ("ReferenceBlackWhite", RATIONAL, 6), 700: ("XMP", BYTE, 0), + # Four private SGI tags + 32995: ("Matteing", SHORT, 1), + 32996: ("DataType", SHORT, 0), + 32997: ("ImageDepth", LONG, 1), + 32998: ("TileDepth", LONG, 1), 33432: ("Copyright", ASCII, 1), 33723: ("IptcNaaInfo", UNDEFINED, 1), 34377: ("PhotoshopInfo", BYTE, 0), From 1b2121c7a1c17d998af52687cdc13153e8f3622f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Oct 2025 09:35:05 +0000 Subject: [PATCH 2092/2374] Update dependency cibuildwheel to v3.2.1 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 8ec7262c090..56517374f5b 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.2.0 +cibuildwheel==3.2.1 From 416fb810742c597280d627323d0e558e8a7f713c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 12 Oct 2025 21:19:34 +1100 Subject: [PATCH 2093/2374] Removed shebang lines and executable flags (#9179) Co-authored-by: Andrew Murray --- Tests/createfontdatachunk.py | 1 - checks/32bit_segfault_check.py | 1 - checks/check_imaging_leaks.py | 1 - 3 files changed, 3 deletions(-) mode change 100755 => 100644 Tests/createfontdatachunk.py mode change 100755 => 100644 checks/32bit_segfault_check.py mode change 100755 => 100644 checks/check_imaging_leaks.py diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py old mode 100755 new mode 100644 index 41c76f87eac..0a3fdb809ed --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 from __future__ import annotations import base64 diff --git a/checks/32bit_segfault_check.py b/checks/32bit_segfault_check.py old mode 100755 new mode 100644 index 06ed2ed2f60..e277bc10af9 --- a/checks/32bit_segfault_check.py +++ b/checks/32bit_segfault_check.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 from __future__ import annotations import sys diff --git a/checks/check_imaging_leaks.py b/checks/check_imaging_leaks.py old mode 100755 new mode 100644 index e9f202f3d3b..65090b6b6ae --- a/checks/check_imaging_leaks.py +++ b/checks/check_imaging_leaks.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 from __future__ import annotations import sys From 48922449080e9d9fa7ddcb030bd7d0f242a6bd49 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 13 Oct 2025 00:31:32 +0300 Subject: [PATCH 2094/2374] Update 12.0.0 release notes (#9247) Co-authored-by: Andrew Murray --- docs/releasenotes/12.0.0.rst | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index de9d6dffd3b..fb5733944f8 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -1,19 +1,6 @@ 12.0.0 ------ -Security -======== - -TODO -^^^^ - -TODO - -:cve:`YYYY-XXXXX`: TODO -^^^^^^^^^^^^^^^^^^^^^^^ - -TODO - Backwards incompatible changes ============================== @@ -132,18 +119,10 @@ Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0. API changes =========== -TODO -^^^^ - -TODO - -API additions -============= - -TODO -^^^^ +Image.alpha_composite: LA images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +:py:meth:`~PIL.Image.alpha_composite` can now use LA images as well as RGBA. Other changes ============= From c60b36d0a738fcfe0fc16ada872564426b5b0748 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 13 Oct 2025 23:24:04 +1100 Subject: [PATCH 2095/2374] Run sdist when scheduled, but do not upload to scientific-python-nightly-wheels index (#9248) --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 9de8a440fc4..3017e36a7cb 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -232,7 +232,7 @@ jobs: path: winbuild\build\bin\fribidi* sdist: - if: github.event_name != 'schedule' + if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -277,7 +277,7 @@ jobs: steps: - uses: actions/download-artifact@v5 with: - pattern: dist-* + pattern: dist-*.whl path: dist merge-multiple: true - name: Upload wheels to scientific-python-nightly-wheels From 014f4212214025bdc417e168a7bb415bbdf0f669 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Oct 2025 08:48:22 +1100 Subject: [PATCH 2096/2374] Removed assert --- Tests/test_nanoarrow.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/test_nanoarrow.py b/Tests/test_nanoarrow.py index 90293130e38..69980e71909 100644 --- a/Tests/test_nanoarrow.py +++ b/Tests/test_nanoarrow.py @@ -111,9 +111,6 @@ def test_to_array(mode: str, dtype: nanoarrow, mask: list[int] | None) -> None: assert arr.schema.nullable == dtype.nullable reloaded = Image.fromarrow(arr, mode, img.size) - - assert reloaded - assert_image_equal(img, reloaded) From 55f3e63b2251c0ba06b238c8c6bb540ff1c22d46 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Oct 2025 18:25:56 +1100 Subject: [PATCH 2097/2374] Revert "Use macos-14 for iOS arm64 simulator (#9161)" This reverts commit c214ad8c8d40c785c8aca6226b5033085f24cb3d. --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3017e36a7cb..8f717a627c4 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -100,7 +100,7 @@ jobs: cibw_arch: arm64_iphoneos - name: "iOS arm64 simulator" platform: ios - os: macos-14 + os: macos-latest cibw_arch: arm64_iphonesimulator - name: "iOS x86_64 simulator" platform: ios From 2caa504991a2713ddee2a161e6b9f8416b7225ec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Oct 2025 18:57:26 +1100 Subject: [PATCH 2098/2374] ImagingHistogramInstance can use two bands --- src/libImaging/Imaging.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index bfe67d46213..5d85ea73e4d 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -148,7 +148,7 @@ struct ImagingAccessInstance { struct ImagingHistogramInstance { /* Format */ char mode[IMAGING_MODE_LENGTH]; /* Band names (of corresponding source image) */ - int bands; /* Number of bands (1, 3, or 4) */ + int bands; /* Number of bands (1, 2, 3, or 4) */ /* Data */ long *histogram; /* Histogram (bands*256 longs) */ From a59100005548a8dd3df6b201acfd112dcf19bb22 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Oct 2025 20:31:42 +1100 Subject: [PATCH 2099/2374] Removed BGR;24 and BGR;32 --- src/_imaging.c | 6 ------ src/libImaging/Mode.c | 3 --- src/libImaging/Mode.h | 3 --- 3 files changed, 12 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 8412124c191..999b8a30d5a 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -701,12 +701,6 @@ getink(PyObject *color, Imaging im, char *ink) { ink[1] = (UINT8)(v >> 8); ink[2] = ink[3] = 0; return ink; - } else if (im->mode == IMAGING_MODE_BGR_24) { - ink[0] = (UINT8)b; - ink[1] = (UINT8)g; - ink[2] = (UINT8)r; - ink[3] = 0; - return ink; } } } diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c index 8222c585b68..78ea5aa7026 100644 --- a/src/libImaging/Mode.c +++ b/src/libImaging/Mode.c @@ -19,7 +19,6 @@ const ModeData MODES[] = { [IMAGING_MODE_RGBa] = {"RGBa"}, [IMAGING_MODE_YCbCr] = {"YCbCr"}, [IMAGING_MODE_BGR_15] = {"BGR;15"}, [IMAGING_MODE_BGR_16] = {"BGR;16"}, - [IMAGING_MODE_BGR_24] = {"BGR;24"}, [IMAGING_MODE_I_16] = {"I;16"}, [IMAGING_MODE_I_16L] = {"I;16L"}, [IMAGING_MODE_I_16B] = {"I;16B"}, [IMAGING_MODE_I_16N] = {"I;16N"}, @@ -74,8 +73,6 @@ const RawModeData RAWMODES[] = { [IMAGING_RAWMODE_BGR_15] = {"BGR;15"}, [IMAGING_RAWMODE_BGR_16] = {"BGR;16"}, - [IMAGING_RAWMODE_BGR_24] = {"BGR;24"}, - [IMAGING_RAWMODE_BGR_32] = {"BGR;32"}, [IMAGING_RAWMODE_I_16] = {"I;16"}, [IMAGING_RAWMODE_I_16L] = {"I;16L"}, diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index a20ad0cb68d..b824becf65f 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -23,7 +23,6 @@ typedef enum { IMAGING_MODE_BGR_15, IMAGING_MODE_BGR_16, - IMAGING_MODE_BGR_24, IMAGING_MODE_I_16, IMAGING_MODE_I_16L, @@ -66,8 +65,6 @@ typedef enum { // BGR modes. IMAGING_RAWMODE_BGR_15, IMAGING_RAWMODE_BGR_16, - IMAGING_RAWMODE_BGR_24, - IMAGING_RAWMODE_BGR_32, // I;* modes. IMAGING_RAWMODE_I_16, From 55a4901bba47ebeed65476e8637cf47cfe07a995 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Oct 2025 20:34:03 +1100 Subject: [PATCH 2100/2374] Removed BGR;15 and BGR;16 modes --- src/_imaging.c | 18 ------------------ src/libImaging/Mode.c | 2 -- src/libImaging/Mode.h | 9 ++------- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 999b8a30d5a..9867fe5710c 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -684,24 +684,6 @@ getink(PyObject *color, Imaging im, char *ink) { } else if (!PyArg_ParseTuple(color, "iiL", &b, &g, &r)) { return NULL; } - if (im->mode == IMAGING_MODE_BGR_15) { - UINT16 v = ((((UINT16)r) << 7) & 0x7c00) + - ((((UINT16)g) << 2) & 0x03e0) + - ((((UINT16)b) >> 3) & 0x001f); - - ink[0] = (UINT8)v; - ink[1] = (UINT8)(v >> 8); - ink[2] = ink[3] = 0; - return ink; - } else if (im->mode == IMAGING_MODE_BGR_16) { - UINT16 v = ((((UINT16)r) << 8) & 0xf800) + - ((((UINT16)g) << 3) & 0x07e0) + - ((((UINT16)b) >> 3) & 0x001f); - ink[0] = (UINT8)v; - ink[1] = (UINT8)(v >> 8); - ink[2] = ink[3] = 0; - return ink; - } } } diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c index 78ea5aa7026..9a855817965 100644 --- a/src/libImaging/Mode.c +++ b/src/libImaging/Mode.c @@ -18,8 +18,6 @@ const ModeData MODES[] = { [IMAGING_MODE_RGBA] = {"RGBA"}, [IMAGING_MODE_RGBX] = {"RGBX"}, [IMAGING_MODE_RGBa] = {"RGBa"}, [IMAGING_MODE_YCbCr] = {"YCbCr"}, - [IMAGING_MODE_BGR_15] = {"BGR;15"}, [IMAGING_MODE_BGR_16] = {"BGR;16"}, - [IMAGING_MODE_I_16] = {"I;16"}, [IMAGING_MODE_I_16L] = {"I;16L"}, [IMAGING_MODE_I_16B] = {"I;16B"}, [IMAGING_MODE_I_16N] = {"I;16N"}, [IMAGING_MODE_I_32L] = {"I;32L"}, [IMAGING_MODE_I_32B] = {"I;32B"}, diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index b824becf65f..a3eb3d86da5 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -21,9 +21,6 @@ typedef enum { IMAGING_MODE_RGBa, IMAGING_MODE_YCbCr, - IMAGING_MODE_BGR_15, - IMAGING_MODE_BGR_16, - IMAGING_MODE_I_16, IMAGING_MODE_I_16L, IMAGING_MODE_I_16B, @@ -62,10 +59,6 @@ typedef enum { IMAGING_RAWMODE_RGBa, IMAGING_RAWMODE_YCbCr, - // BGR modes. - IMAGING_RAWMODE_BGR_15, - IMAGING_RAWMODE_BGR_16, - // I;* modes. IMAGING_RAWMODE_I_16, IMAGING_RAWMODE_I_16L, @@ -95,6 +88,8 @@ typedef enum { IMAGING_RAWMODE_BGRA_16L, IMAGING_RAWMODE_BGRX, IMAGING_RAWMODE_BGR_5, + IMAGING_RAWMODE_BGR_15, + IMAGING_RAWMODE_BGR_16, IMAGING_RAWMODE_BGRa, IMAGING_RAWMODE_BGXR, IMAGING_RAWMODE_B_16B, From 8de7e7763e0d7b117c25ae31ef2e19404bf35c17 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Oct 2025 21:47:56 +1100 Subject: [PATCH 2101/2374] Corrected scientific-python-nightly-wheels pattern --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3017e36a7cb..eef70f89417 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -277,7 +277,7 @@ jobs: steps: - uses: actions/download-artifact@v5 with: - pattern: dist-*.whl + pattern: dist-!(sdist)* path: dist merge-multiple: true - name: Upload wheels to scientific-python-nightly-wheels From 9cb36a91d026115734a5dd46f408983035e6c3c4 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 15 Oct 2025 18:53:49 +1100 Subject: [PATCH 2102/2374] Upgrade from macos-13 (#9212) Co-authored-by: Andrew Murray --- .github/workflows/macos-install.sh | 13 +------------ .github/workflows/test.yml | 2 +- .github/workflows/wheels-dependencies.sh | 6 +++++- .github/workflows/wheels.yml | 8 ++++---- docs/installation/platform-support.rst | 6 +++--- 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 8060e0850e6..b114d4a23f8 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,21 +2,10 @@ set -e -if [[ "$ImageOS" == "macos13" ]]; then - 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 brew install \ aom \ dav1d \ + freetype \ ghostscript \ jpeg-turbo \ libimagequant \ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8504e5c1e2e..b52000a2766 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,7 +57,7 @@ jobs: - { python-version: "3.14t", disable-gil: true } - { python-version: "3.13t", disable-gil: true } # Intel - - { os: "macos-13", python-version: "3.10" } + - { os: "macos-15-intel", python-version: "3.10" } exclude: - { os: "macos-latest", python-version: "3.10" } diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 69c867b4dba..7d6eb8681a4 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -271,7 +271,11 @@ function build { if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi - build_zlib_ng + if [[ -n "$IS_MACOS" ]]; then + CFLAGS="$CFLAGS -headerpad_max_install_names" build_zlib_ng + else + build_zlib_ng + fi build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [[ -n "$IS_MACOS" ]]; then diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index eef70f89417..6dc8db7e9c8 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -53,19 +53,19 @@ jobs: include: - name: "macOS 10.10 x86_64" platform: macos - os: macos-13 + os: macos-15-intel cibw_arch: x86_64 build: "cp3{10,11}*" macosx_deployment_target: "10.10" - name: "macOS 10.13 x86_64" platform: macos - os: macos-13 + os: macos-15-intel cibw_arch: x86_64 build: "cp3{12,13}*" macosx_deployment_target: "10.13" - name: "macOS 10.15 x86_64" platform: macos - os: macos-13 + os: macos-15-intel cibw_arch: x86_64 build: "{cp314,pp3}*" macosx_deployment_target: "10.15" @@ -104,7 +104,7 @@ jobs: cibw_arch: arm64_iphonesimulator - name: "iOS x86_64 simulator" platform: ios - os: macos-13 + os: macos-15-intel cibw_arch: x86_64_iphonesimulator steps: - uses: actions/checkout@v5 diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 186d9b96da5..7999504fbe7 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -39,9 +39,9 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 13 Ventura | 3.10 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14 | arm64 | +| macOS 15 Sequoia | 3.10 | x86-64 | +| +----------------------------+---------------------+ +| | 3.11, 3.12, 3.13, 3.14, | arm64 | | | PyPy3 | | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | From ef323ab7d71ab8ed02704e34e19d51091a976118 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Oct 2025 18:58:55 +1100 Subject: [PATCH 2103/2374] Install dependencies when type checking --- .ci/requirements-mypy.txt | 2 ++ Tests/test_arro3.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 447856433b4..6ca35d28642 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,6 @@ mypy==1.18.2 +arro3-compute +arro3-core IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython diff --git a/Tests/test_arro3.py b/Tests/test_arro3.py index 60955cfdb40..672eedc9ba4 100644 --- a/Tests/test_arro3.py +++ b/Tests/test_arro3.py @@ -16,8 +16,8 @@ TYPE_CHECKING = False if TYPE_CHECKING: - from arro3 import compute # type: ignore [import-not-found] - from arro3.core import ( # type: ignore [import-not-found] + from arro3 import compute + from arro3.core import ( Array, DataType, Field, From 78b0e06dbbe8f7cdeee1fd9234c13b6d4d997876 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Oct 2025 23:41:50 +1100 Subject: [PATCH 2104/2374] Shift bits before making value negative --- src/libImaging/BcnDecode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index 7b3d8f908e3..ac81ed6df3d 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -603,7 +603,7 @@ static void bc6_sign_extend(UINT16 *v, int prec) { int x = *v; if (x & (1 << (prec - 1))) { - x |= -1 << prec; + x |= -(1 << prec); } *v = (UINT16)x; } From 4889863139473270b69e5583007760401198cd4c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Oct 2025 19:38:25 +1100 Subject: [PATCH 2105/2374] Renamed ImageText class to Text --- Tests/test_imagetext.py | 24 ++++++++++++------------ docs/reference/ImageText.rst | 4 ++-- src/PIL/ImageDraw.py | 10 +++++----- src/PIL/ImageText.py | 22 +++++++++++----------- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Tests/test_imagetext.py b/Tests/test_imagetext.py index b58d048b5df..7db22989786 100644 --- a/Tests/test_imagetext.py +++ b/Tests/test_imagetext.py @@ -26,24 +26,24 @@ def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont: def test_get_length(font: ImageFont.FreeTypeFont) -> None: - assert ImageText.ImageText("A", font).get_length() == 12 - assert ImageText.ImageText("AB", font).get_length() == 24 - assert ImageText.ImageText("M", font).get_length() == 12 - assert ImageText.ImageText("y", font).get_length() == 12 - assert ImageText.ImageText("a", font).get_length() == 12 + assert ImageText.Text("A", font).get_length() == 12 + assert ImageText.Text("AB", font).get_length() == 24 + assert ImageText.Text("M", font).get_length() == 12 + assert ImageText.Text("y", font).get_length() == 12 + assert ImageText.Text("a", font).get_length() == 12 def test_get_bbox(font: ImageFont.FreeTypeFont) -> None: - assert ImageText.ImageText("A", font).get_bbox() == (0, 4, 12, 16) - assert ImageText.ImageText("AB", font).get_bbox() == (0, 4, 24, 16) - assert ImageText.ImageText("M", font).get_bbox() == (0, 4, 12, 16) - assert ImageText.ImageText("y", font).get_bbox() == (0, 7, 12, 20) - assert ImageText.ImageText("a", font).get_bbox() == (0, 7, 12, 16) + assert ImageText.Text("A", font).get_bbox() == (0, 4, 12, 16) + assert ImageText.Text("AB", font).get_bbox() == (0, 4, 24, 16) + assert ImageText.Text("M", font).get_bbox() == (0, 4, 12, 16) + assert ImageText.Text("y", font).get_bbox() == (0, 7, 12, 20) + assert ImageText.Text("a", font).get_bbox() == (0, 7, 12, 16) def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: font = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) - text = ImageText.ImageText("Hello World!", font) + text = ImageText.Text("Hello World!", font) text.embed_color() im = Image.new("RGB", (300, 64), "white") @@ -60,7 +60,7 @@ def test_stroke() -> None: im = Image.new("RGB", (120, 130)) draw = ImageDraw.Draw(im) font = ImageFont.truetype(FONT_PATH, 120) - text = ImageText.ImageText("A", font) + text = ImageText.Text("A", font) text.stroke(2, stroke_fill) # Act diff --git a/docs/reference/ImageText.rst b/docs/reference/ImageText.rst index fa55b4f306e..299561acec6 100644 --- a/docs/reference/ImageText.rst +++ b/docs/reference/ImageText.rst @@ -16,7 +16,7 @@ Example from PIL import Image, ImageDraw, ImageFont, ImageText font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 24) - text = ImageText.ImageText("Hello world", font) + text = ImageText.Text("Hello world", font) text.embed_color() text.stroke(2, "#0f0") @@ -30,5 +30,5 @@ Example Methods ------- -.. autoclass:: PIL.ImageText.ImageText +.. autoclass:: PIL.ImageText.Text :members: diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index f1b5dd4f3f4..0256efd62ad 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -540,7 +540,7 @@ def draw_corners(pieslice: bool) -> None: def text( self, xy: tuple[float, float], - text: AnyStr | ImageText.ImageText, + text: AnyStr | ImageText.Text, fill: _Ink | None = None, font: ( ImageFont.ImageFont @@ -561,12 +561,12 @@ def text( **kwargs: Any, ) -> None: """Draw text.""" - if isinstance(text, ImageText.ImageText): + if isinstance(text, ImageText.Text): imagetext = text else: if font is None: font = self._getfont(kwargs.get("font_size")) - imagetext = ImageText.ImageText( + imagetext = ImageText.Text( text, font, self.mode, spacing, direction, features, language ) if embedded_color: @@ -721,7 +721,7 @@ def textlength( """Get the length of a given string, in pixels with 1/64 precision.""" if font is None: font = self._getfont(font_size) - imagetext = ImageText.ImageText( + imagetext = ImageText.Text( text, font, self.mode, @@ -757,7 +757,7 @@ def textbbox( """Get the bounding box of a given string, in pixels.""" if font is None: font = self._getfont(font_size) - imagetext = ImageText.ImageText( + imagetext = ImageText.Text( text, font, self.mode, spacing, direction, features, language ) if embedded_color: diff --git a/src/PIL/ImageText.py b/src/PIL/ImageText.py index 9bb31a1c8c0..c74570e6939 100644 --- a/src/PIL/ImageText.py +++ b/src/PIL/ImageText.py @@ -4,7 +4,7 @@ from ._typing import _Ink -class ImageText: +class Text: def __init__( self, text: str | bytes, @@ -104,26 +104,26 @@ def get_length(self): For example, instead of:: - hello = ImageText.ImageText("Hello", font).get_length() - world = ImageText.ImageText("World", font).get_length() - helloworld = ImageText.ImageText("HelloWorld", font).get_length() + hello = ImageText.Text("Hello", font).get_length() + world = ImageText.Text("World", font).get_length() + helloworld = ImageText.Text("HelloWorld", font).get_length() assert hello + world == helloworld use:: hello = ( - ImageText.ImageText("HelloW", font).get_length() - - ImageText.ImageText("W", font).get_length() + ImageText.Text("HelloW", font).get_length() - + ImageText.Text("W", font).get_length() ) # adjusted for kerning - world = ImageText.ImageText("World", font).get_length() - helloworld = ImageText.ImageText("HelloWorld", font).get_length() + world = ImageText.Text("World", font).get_length() + helloworld = ImageText.Text("HelloWorld", font).get_length() assert hello + world == helloworld or disable kerning with (requires libraqm):: - hello = ImageText.ImageText("Hello", font, features=["-kern"]).get_length() - world = ImageText.ImageText("World", font, features=["-kern"]).get_length() - helloworld = ImageText.ImageText( + hello = ImageText.Text("Hello", font, features=["-kern"]).get_length() + world = ImageText.Text("World", font, features=["-kern"]).get_length() + helloworld = ImageText.Text( "HelloWorld", font, features=["-kern"] ).get_length() assert hello + world == helloworld From 95a85dc6693ca221643906214b0e1f4590986c0f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Oct 2025 19:36:10 +1100 Subject: [PATCH 2106/2374] Use snake case --- src/PIL/ImageDraw.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 0256efd62ad..a720ad40a1c 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -562,17 +562,17 @@ def text( ) -> None: """Draw text.""" if isinstance(text, ImageText.Text): - imagetext = text + image_text = text else: if font is None: font = self._getfont(kwargs.get("font_size")) - imagetext = ImageText.Text( + image_text = ImageText.Text( text, font, self.mode, spacing, direction, features, language ) if embedded_color: - imagetext.embed_color() + image_text.embed_color() if stroke_width: - imagetext.stroke(stroke_width, stroke_fill) + image_text.stroke(stroke_width, stroke_fill) def getink(fill: _Ink | None) -> int: ink, fill_ink = self._getink(fill) @@ -586,14 +586,14 @@ def getink(fill: _Ink | None) -> int: return stroke_ink = None - if imagetext.stroke_width: + if image_text.stroke_width: stroke_ink = ( - getink(imagetext.stroke_fill) - if imagetext.stroke_fill is not None + getink(image_text.stroke_fill) + if image_text.stroke_fill is not None else ink ) - for xy, anchor, line in imagetext._split(xy, anchor, align): + for xy, anchor, line in image_text._split(xy, anchor, align): def draw_text(ink: int, stroke_width: float = 0) -> None: mode = self.fontmode @@ -604,7 +604,7 @@ def draw_text(ink: int, stroke_width: float = 0) -> None: coord.append(int(xy[i])) start = (math.modf(xy[0])[0], math.modf(xy[1])[0]) try: - mask, offset = imagetext.font.getmask2( # type: ignore[union-attr,misc] + mask, offset = image_text.font.getmask2( # type: ignore[union-attr,misc] line, mode, direction=direction, @@ -621,7 +621,7 @@ def draw_text(ink: int, stroke_width: float = 0) -> None: coord = [coord[0] + offset[0], coord[1] + offset[1]] except AttributeError: try: - mask = imagetext.font.getmask( # type: ignore[misc] + mask = image_text.font.getmask( # type: ignore[misc] line, mode, direction, @@ -635,9 +635,9 @@ def draw_text(ink: int, stroke_width: float = 0) -> None: **kwargs, ) except TypeError: - mask = imagetext.font.getmask(line) + mask = image_text.font.getmask(line) if mode == "RGBA": - # imagetext.font.getmask2(mode="RGBA") + # image_text.font.getmask2(mode="RGBA") # returns color in RGB bands and mask in A # extract mask and set text alpha color, mask = mask, mask.getband(3) @@ -653,7 +653,7 @@ def draw_text(ink: int, stroke_width: float = 0) -> None: if stroke_ink is not None: # Draw stroked text - draw_text(stroke_ink, imagetext.stroke_width) + draw_text(stroke_ink, image_text.stroke_width) # Draw normal text if ink != stroke_ink: @@ -721,7 +721,7 @@ def textlength( """Get the length of a given string, in pixels with 1/64 precision.""" if font is None: font = self._getfont(font_size) - imagetext = ImageText.Text( + image_text = ImageText.Text( text, font, self.mode, @@ -730,8 +730,8 @@ def textlength( language=language, ) if embedded_color: - imagetext.embed_color() - return imagetext.get_length() + image_text.embed_color() + return image_text.get_length() def textbbox( self, @@ -757,14 +757,14 @@ def textbbox( """Get the bounding box of a given string, in pixels.""" if font is None: font = self._getfont(font_size) - imagetext = ImageText.Text( + image_text = ImageText.Text( text, font, self.mode, spacing, direction, features, language ) if embedded_color: - imagetext.embed_color() + image_text.embed_color() if stroke_width: - imagetext.stroke(stroke_width) - return imagetext.get_bbox(xy, anchor, align) + image_text.stroke(stroke_width) + return image_text.get_bbox(xy, anchor, align) def multiline_textbbox( self, From d5e1601b32ea43b45ce8f820e4b349e9b5e2dd6c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Oct 2025 20:02:12 +1100 Subject: [PATCH 2107/2374] Improved documentation --- docs/reference/ImageDraw.rst | 4 ++++ docs/reference/ImageText.rst | 33 ++++++++++++++++++++++++++++++--- docs/releasenotes/12.0.0.rst | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 6768a04c600..4c956759334 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -582,6 +582,8 @@ Methods hello_world = hello + world # kerning is disabled, no need to adjust assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) # True + .. seealso:: :py:meth:`PIL.ImageText.Text.get_length` + .. versionadded:: 8.0.0 :param text: Text to be measured. May not contain any newline characters. @@ -683,6 +685,8 @@ Methods 1/64 pixel precision. The bounding box includes extra margins for some fonts, e.g. italics or accents. + .. seealso:: :py:meth:`PIL.ImageText.Text.get_bbox` + .. versionadded:: 8.0.0 :param xy: The anchor coordinates of the text. diff --git a/docs/reference/ImageText.rst b/docs/reference/ImageText.rst index 299561acec6..8744ad36825 100644 --- a/docs/reference/ImageText.rst +++ b/docs/reference/ImageText.rst @@ -4,9 +4,9 @@ :py:mod:`~PIL.ImageText` module =============================== -The :py:mod:`~PIL.ImageText` module defines a class with the same name. Instances of -this class provide a way to use fonts with text strings or bytes. The result is a -simple API to apply styling to pieces of text and measure or draw them. +The :py:mod:`~PIL.ImageText` module defines a :py:class:`~PIL.ImageText.Text` class. +Instances of this class provide a way to use fonts with text strings or bytes. The +result is a simple API to apply styling to pieces of text and measure or draw them. Example ------- @@ -27,6 +27,33 @@ Example d = ImageDraw.Draw(im) d.text((0, 0), text, "#f00") +Comparison +---------- + +Without ``ImageText.Text``:: + + from PIL import Image, ImageDraw + im = Image.new(mode, size) + d = ImageDraw.Draw(im) + + d.textlength(text, font, direction, features, language, embedded_color) + d.multiline_textbbox(xy, text, font, anchor, spacing, align, direction, features, language, stroke_width, embedded_color) + d.text(xy, text, fill, font, anchor, spacing, align, direction, features, language, stroke_width, stroke_fill, embedded_color) + +With ``ImageText.Text``:: + + from PIL import ImageText + text = ImageText.Text(text, font, mode, spacing, direction, features, language) + text.embed_color() + text.stroke(stroke_width, stroke_fill) + + text.get_length() + text.get_bbox(xy, anchor, align) + + im = Image.new(mode, size) + d = ImageDraw.Draw(im) + d.text(xy, text, fill, anchor=anchor, align=align) + Methods ------- diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index fb5733944f8..4c00d8c4cc1 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -124,6 +124,39 @@ Image.alpha_composite: LA images :py:meth:`~PIL.Image.alpha_composite` can now use LA images as well as RGBA. +API additions +============= + +Added ImageText.Text +^^^^^^^^^^^^^^^^^^^^ + +:py:class:`PIL.ImageText.Text` has been added, as a simpler way to use fonts with text +strings or bytes. + +Without ``ImageText.Text``:: + + from PIL import Image, ImageDraw + im = Image.new(mode, size) + d = ImageDraw.Draw(im) + + d.textlength(text, font, direction, features, language, embedded_color) + d.multiline_textbbox(xy, text, font, anchor, spacing, align, direction, features, language, stroke_width, embedded_color) + d.text(xy, text, fill, font, anchor, spacing, align, direction, features, language, stroke_width, stroke_fill, embedded_color) + +With ``ImageText.Text``:: + + from PIL import ImageText + text = ImageText.Text(text, font, mode, spacing, direction, features, language) + text.embed_color() + text.stroke(stroke_width, stroke_fill) + + text.get_length() + text.get_bbox(xy, anchor, align) + + im = Image.new(mode, size) + d = ImageDraw.Draw(im) + d.text(xy, text, fill, anchor=anchor, align=align) + Other changes ============= From 3eecafd62c760cf2715f4bcc4c995ead35680e0e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Oct 2025 22:19:38 +1100 Subject: [PATCH 2108/2374] Fixed warning --- src/libImaging/Arrow.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index e353ab2e9c0..d2ed10f0a6e 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -149,7 +149,7 @@ assemble_metadata(const char *band_json) { } int -export_named_type(struct ArrowSchema *schema, char *format, char *name) { +export_named_type(struct ArrowSchema *schema, char *format, const char *name) { char *formatp; char *namep; size_t format_len = strlen(format) + 1; From 7d89946688a44e302b5480e8c02c37fe97369c6b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Oct 2025 22:21:51 +1100 Subject: [PATCH 2109/2374] Removed duplicate library --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 3a72a0742c1..032c1c6d263 100644 --- a/setup.py +++ b/setup.py @@ -1086,10 +1086,10 @@ def debug_build() -> bool: for src_file in _LIB_IMAGING: files.append(os.path.join("src/libImaging", src_file + ".c")) ext_modules = [ - Extension("PIL._imaging", files, libraries=["pil_imaging_mode"]), - Extension("PIL._imagingft", ["src/_imagingft.c"], libraries=["pil_imaging_mode"]), - Extension("PIL._imagingcms", ["src/_imagingcms.c"], libraries=["pil_imaging_mode"]), - Extension("PIL._webp", ["src/_webp.c"], libraries=["pil_imaging_mode"]), + Extension("PIL._imaging", files), + Extension("PIL._imagingft", ["src/_imagingft.c"]), + Extension("PIL._imagingcms", ["src/_imagingcms.c"]), + Extension("PIL._webp", ["src/_webp.c"]), Extension("PIL._avif", ["src/_avif.c"]), Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), Extension("PIL._imagingmath", ["src/_imagingmath.c"]), From 592b2f820aa1f75f8ae8bf4f30e1b4bc62023535 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 15 Oct 2025 19:00:54 +0300 Subject: [PATCH 2110/2374] Revert "Use macos-latest for iOS arm64 simulator" --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 21ea79553ef..6dc8db7e9c8 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -100,7 +100,7 @@ jobs: cibw_arch: arm64_iphoneos - name: "iOS arm64 simulator" platform: ios - os: macos-latest + os: macos-14 cibw_arch: arm64_iphonesimulator - name: "iOS x86_64 simulator" platform: ios From 693df7b42c666f88c719f9973be0ad71607328e0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:06:44 +0300 Subject: [PATCH 2111/2374] 12.0.0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 6a3c01f2607..79ce194c334 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "12.0.0.dev0" +__version__ = "12.0.0" From 3620d48459da4e8f30278b0abc8d6c3d51565447 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 15 Oct 2025 21:28:16 +0300 Subject: [PATCH 2112/2374] 12.1.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 79ce194c334..41cb17a3697 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "12.0.0" +__version__ = "12.1.0.dev0" From 933df2450d9b415eeed656525d4d69c347fa1c7e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Oct 2025 07:21:15 +1100 Subject: [PATCH 2113/2374] Reapply "Use macos-latest for iOS arm64 simulator" This reverts commit 592b2f820aa1f75f8ae8bf4f30e1b4bc62023535. --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6dc8db7e9c8..21ea79553ef 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -100,7 +100,7 @@ jobs: cibw_arch: arm64_iphoneos - name: "iOS arm64 simulator" platform: ios - os: macos-14 + os: macos-latest cibw_arch: arm64_iphonesimulator - name: "iOS x86_64 simulator" platform: ios From ae7d28eddbdc24bfadb13ed27f01ef8256b29480 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Oct 2025 12:03:13 +1100 Subject: [PATCH 2114/2374] Removed Fedora 41 --- .github/workflows/test-docker.yml | 1 - docs/installation/platform-support.rst | 2 -- 2 files changed, 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 30e5c494db9..581e1f52bf4 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -49,7 +49,6 @@ jobs: debian-12-bookworm-amd64, debian-13-trixie-x86, debian-13-trixie-amd64, - fedora-41-amd64, fedora-42-amd64, gentoo, ubuntu-22.04-jammy-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 7999504fbe7..471bc1eb30e 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -33,8 +33,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 13 Trixie | 3.13 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ -| Fedora 41 | 3.13 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 42 | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | From ae43b36030a3d8dca20bb908bf158dcadc2cf35f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Oct 2025 20:55:56 +1100 Subject: [PATCH 2115/2374] Simplified code now that I;16* modes are the only IMAGING_TYPE_SPECIAL --- src/_imaging.c | 31 +++++-------------------------- src/libImaging/Geometry.c | 9 +-------- 2 files changed, 6 insertions(+), 34 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 41af72568f2..f6be4a90124 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -543,12 +543,7 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) { case IMAGING_TYPE_FLOAT32: return PyFloat_FromDouble(pixel.f); case IMAGING_TYPE_SPECIAL: - if (im->bands == 1) { - return PyLong_FromLong(pixel.h); - } else { - return Py_BuildValue("BBB", pixel.b[0], pixel.b[1], pixel.b[2]); - } - break; + return PyLong_FromLong(pixel.h); } /* unknown type */ @@ -665,26 +660,10 @@ getink(PyObject *color, Imaging im, char *ink) { memcpy(ink, &ftmp, sizeof(ftmp)); return ink; case IMAGING_TYPE_SPECIAL: - if (isModeI16(im->mode)) { - ink[0] = (UINT8)r; - ink[1] = (UINT8)(r >> 8); - ink[2] = ink[3] = 0; - return ink; - } else { - if (rIsInt) { - b = (UINT8)(r >> 16); - g = (UINT8)(r >> 8); - r = (UINT8)r; - } else if (tupleSize != 3) { - PyErr_SetString( - PyExc_TypeError, - "color must be int, or tuple of one or three elements" - ); - return NULL; - } else if (!PyArg_ParseTuple(color, "iiL", &b, &g, &r)) { - return NULL; - } - } + ink[0] = (UINT8)r; + ink[1] = (UINT8)(r >> 8); + ink[2] = ink[3] = 0; + return ink; } PyErr_SetString(PyExc_ValueError, wrong_mode); diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index 80ecd7cb6b3..2186f95f8e5 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -714,14 +714,7 @@ getfilter(Imaging im, int filterid) { case IMAGING_TYPE_UINT8: return nearest_filter8; case IMAGING_TYPE_SPECIAL: - switch (im->pixelsize) { - case 1: - return nearest_filter8; - case 2: - return nearest_filter16; - case 4: - return nearest_filter32; - } + return nearest_filter16; } } else { return nearest_filter32; From e969fa7aeac1cfc46ef1e6a8e77699f71a9effe8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Oct 2025 06:14:02 +1100 Subject: [PATCH 2116/2374] Correct __getitem__ return type --- Tests/test_image_getdata.py | 2 +- src/PIL/_imaging.pyi | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index dd3d70b3450..c8b213d841b 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -15,7 +15,7 @@ def test_sanity() -> None: def test_mode() -> None: - def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]: + def getdata(mode: str) -> tuple[float | tuple[int, ...] | None, int, int]: im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) data = im.getdata() return data[0], len(data), len(list(data)) diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi index 998bc52eb8a..81028a5960a 100644 --- a/src/PIL/_imaging.pyi +++ b/src/PIL/_imaging.pyi @@ -1,7 +1,7 @@ from typing import Any class ImagingCore: - def __getitem__(self, index: int) -> float: ... + def __getitem__(self, index: int) -> float | tuple[int, ...] | None: ... def __getattr__(self, name: str) -> Any: ... class ImagingFont: From 03d48f4011d4c35099341ae76fc5bf1f34ea3e9e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Oct 2025 23:05:33 +1100 Subject: [PATCH 2117/2374] Updated macOS tested Pillow versions --- docs/installation/platform-support.rst | 196 +++++++++++++------------ 1 file changed, 99 insertions(+), 97 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 471bc1eb30e..e0c4a8eec3d 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -69,100 +69,102 @@ These platforms have been reported to work at the versions mentioned. Contributors please test Pillow on your platform then update this document and send a pull request. -+----------------------------------+----------------------------+------------------+--------------+ -| Operating system | | Tested Python | | Latest tested | | Tested | -| | | 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 | -| +----------------------------+------------------+ | -| | 3.8 | 10.4.0 | | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | -| +----------------------------+------------------+ | -| | 3.7 | 9.5.0 | | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | -| +----------------------------+------------------+--------------+ -| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 | -| +----------------------------+------------------+ | -| | 3.6 | 8.4.0 | | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 | -| +----------------------------+------------------+ | -| | 3.5 | 7.2.0 | | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 | -| +----------------------------+------------------+ | -| | 2.7 | 6.0.0 | | -| +----------------------------+------------------+ | -| | 3.4 | 5.4.1 | | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | -| +----------------------------+------------------+ | -| | 3.3 | 4.1.0 | | -+----------------------------------+----------------------------+------------------+--------------+ -| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Redhat Linux 6 | 2.6 | |x86 | -+----------------------------------+----------------------------+------------------+--------------+ -| CentOS 6.3 | 2.7, 3.3 | |x86 | -+----------------------------------+----------------------------+------------------+--------------+ -| CentOS 8 | 3.9 | 9.0.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | -| | | PyPy5.3.1, PyPy3 v2.4.0 | | | -| +----------------------------+------------------+--------------+ -| | 2.7 | 4.3.0 |x86-64 | -| +----------------------------+------------------+--------------+ -| | 2.7, 3.2 | 3.4.1 |ppc | -+----------------------------------+----------------------------+------------------+--------------+ -| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | -+----------------------------------+----------------------------+------------------+--------------+ -| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | -+----------------------------------+----------------------------+------------------+--------------+ -| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm | -| +----------------------------+------------------+ | -| | 2.7 | 6.2.2 | | -+----------------------------------+----------------------------+------------------+--------------+ -| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 23H2 | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 10 | 3.7 | 7.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ ++----------------------------------+-----------------------------+------------------+--------------+ +| Operating system | | Tested Python | | Latest tested | | Tested | +| | | versions | | Pillow version | | processors | ++==================================+=============================+==================+==============+ +| macOS 26 Tahoe | 3.10, 3.11, 3.12, 3.13, 3.14| 12.0.0 |arm | +| +-----------------------------+------------------+ | +| | 3.9 | 11.3.0 | | ++----------------------------------+-----------------------------+------------------+--------------+ +| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13 | 11.3.0 |arm | +| +-----------------------------+------------------+ | +| | 3.8 | 10.4.0 | | ++----------------------------------+-----------------------------+------------------+--------------+ +| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm | ++----------------------------------+-----------------------------+------------------+--------------+ +| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | +| +-----------------------------+------------------+ | +| | 3.7 | 9.5.0 | | ++----------------------------------+-----------------------------+------------------+--------------+ +| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | ++----------------------------------+-----------------------------+------------------+--------------+ +| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | +| +-----------------------------+------------------+--------------+ +| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 | +| +-----------------------------+------------------+ | +| | 3.6 | 8.4.0 | | ++----------------------------------+-----------------------------+------------------+--------------+ +| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 | +| +-----------------------------+------------------+ | +| | 3.5 | 7.2.0 | | ++----------------------------------+-----------------------------+------------------+--------------+ +| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 | +| +-----------------------------+------------------+ | +| | 2.7 | 6.0.0 | | +| +-----------------------------+------------------+ | +| | 3.4 | 5.4.1 | | ++----------------------------------+-----------------------------+------------------+--------------+ +| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | +| +-----------------------------+------------------+ | +| | 3.3 | 4.1.0 | | ++----------------------------------+-----------------------------+------------------+--------------+ +| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Redhat Linux 6 | 2.6 | |x86 | ++----------------------------------+-----------------------------+------------------+--------------+ +| CentOS 6.3 | 2.7, 3.3 | |x86 | ++----------------------------------+-----------------------------+------------------+--------------+ +| CentOS 8 | 3.9 | 9.0.0 |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | +| | | PyPy5.3.1, PyPy3 v2.4.0 | | | +| +-----------------------------+------------------+--------------+ +| | 2.7 | 4.3.0 |x86-64 | +| +-----------------------------+------------------+--------------+ +| | 2.7, 3.2 | 3.4.1 |ppc | ++----------------------------------+-----------------------------+------------------+--------------+ +| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | ++----------------------------------+-----------------------------+------------------+--------------+ +| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | ++----------------------------------+-----------------------------+------------------+--------------+ +| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm | +| +-----------------------------+------------------+ | +| | 2.7 | 6.2.2 | | ++----------------------------------+-----------------------------+------------------+--------------+ +| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Windows 11 23H2 | 3.9, 3.10, 3.11, 3.12, 3.13 | 11.0.0 |arm64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Windows 10 | 3.7 | 7.1.0 |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | ++----------------------------------+-----------------------------+------------------+--------------+ From 51e3fe45bf34fb4c344eaaaadf3434a079c5b6dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Oct 2025 19:17:38 +1100 Subject: [PATCH 2118/2374] Use different variables for Image and ImageFile instances --- Tests/test_file_mic.py | 4 ++-- Tests/test_file_mpo.py | 8 ++++---- Tests/test_file_sun.py | 4 ++-- Tests/test_file_tiff.py | 6 +++--- Tests/test_file_tiff_metadata.py | 6 +++--- Tests/test_image.py | 26 +++++++++++++------------- Tests/test_image_convert.py | 4 ++-- Tests/test_image_crop.py | 8 ++++---- Tests/test_image_quantize.py | 18 +++++++++--------- Tests/test_image_resize.py | 4 ++-- Tests/test_image_rotate.py | 16 ++++++++-------- Tests/test_image_thumbnail.py | 4 ++-- Tests/test_imagedraw.py | 4 ++-- Tests/test_imageops.py | 12 ++++++------ Tests/test_pickle.py | 10 +++++----- Tests/test_shell_injection.py | 10 ++++++---- 16 files changed, 73 insertions(+), 71 deletions(-) diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 9aeb306e439..0706af4c07d 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -22,10 +22,10 @@ def test_sanity() -> None: # Adjust for the gamma of 2.2 encoded into the file lut = ImagePalette.make_gamma_lut(1 / 2.2) - im = Image.merge("RGBA", [chan.point(lut) for chan in im.split()]) + im1 = Image.merge("RGBA", [chan.point(lut) for chan in im.split()]) im2 = hopper("RGBA") - assert_image_similar(im, im2, 10) + assert_image_similar(im1, im2, 10) def test_n_frames() -> None: diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index f947d141929..4db62bd6d0a 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -300,12 +300,12 @@ def test_save_all() -> None: im_reloaded.seek(1) assert_image_similar(im, im_reloaded, 30) - im = Image.new("RGB", (1, 1)) + im_rgb = Image.new("RGB", (1, 1)) for colors in (("#f00",), ("#f00", "#0f0")): append_images = [Image.new("RGB", (1, 1), color) for color in colors] - im_reloaded = roundtrip(im, save_all=True, append_images=append_images) + im_reloaded = roundtrip(im_rgb, save_all=True, append_images=append_images) - assert_image_equal(im, im_reloaded) + assert_image_equal(im_rgb, im_reloaded) assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile) assert im_reloaded.mpinfo is not None assert im_reloaded.mpinfo[45056] == b"0100" @@ -315,7 +315,7 @@ def test_save_all() -> None: assert_image_similar(im_reloaded, im_expected, 1) # Test that a single frame image will not be saved as an MPO - jpg = roundtrip(im, save_all=True) + jpg = roundtrip(im_rgb, save_all=True) assert "mp" not in jpg.info diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index c2f162cf9b4..78534e15411 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -84,8 +84,8 @@ def test_rgbx() -> None: with Image.open(io.BytesIO(data)) as im: r, g, b = im.split() - im = Image.merge("RGB", (b, g, r)) - assert_image_equal_tofile(im, os.path.join(EXTRA_DIR, "32bpp.png")) + im_rgb = Image.merge("RGB", (b, g, r)) + assert_image_equal_tofile(im_rgb, os.path.join(EXTRA_DIR, "32bpp.png")) @pytest.mark.skipif( diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index bd364377b74..556c886476e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -764,9 +764,9 @@ def test_tiff_save_all(self) -> None: # Test appending images mp = BytesIO() - im = Image.new("RGB", (100, 100), "#f00") + im_rgb = Image.new("RGB", (100, 100), "#f00") ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] - im.copy().save(mp, format="TIFF", save_all=True, append_images=ims) + im_rgb.copy().save(mp, format="TIFF", save_all=True, append_images=ims) mp.seek(0, os.SEEK_SET) with Image.open(mp) as reread: @@ -778,7 +778,7 @@ def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims mp = BytesIO() - im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims)) + im_rgb.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims)) mp.seek(0, os.SEEK_SET) with Image.open(mp) as reread: diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 36ad8cee93b..322ef5abcc5 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -175,13 +175,13 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: del info[278] # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT - im = im.resize((500, 500)) - info[TiffImagePlugin.IMAGEWIDTH] = im.width + im_resized = im.resize((500, 500)) + info[TiffImagePlugin.IMAGEWIDTH] = im_resized.width # STRIPBYTECOUNTS can be a SHORT or a LONG info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT - im.save(out, tiffinfo=info) + im_resized.save(out, tiffinfo=info) with Image.open(out) as reloaded: assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) diff --git a/Tests/test_image.py b/Tests/test_image.py index ac30f785c5e..88f55638ee9 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -613,8 +613,8 @@ def test_linear_gradient(self, mode: str) -> None: assert im.getpixel((0, 0)) == 0 assert im.getpixel((255, 255)) == 255 with Image.open(target_file) as target: - target = target.convert(mode) - assert_image_equal(im, target) + im_target = target.convert(mode) + assert_image_equal(im, im_target) def test_radial_gradient_wrong_mode(self) -> None: # Arrange @@ -638,8 +638,8 @@ def test_radial_gradient(self, mode: str) -> None: assert im.getpixel((0, 0)) == 255 assert im.getpixel((128, 128)) == 0 with Image.open(target_file) as target: - target = target.convert(mode) - assert_image_equal(im, target) + im_target = target.convert(mode) + assert_image_equal(im, im_target) def test_register_extensions(self) -> None: test_format = "a" @@ -663,20 +663,20 @@ def test_remap_palette(self) -> None: assert_image_equal(im, im.remap_palette(list(range(256)))) # Test identity transform with an RGBA palette - im = Image.new("P", (256, 1)) + im_p = Image.new("P", (256, 1)) for x in range(256): - im.putpixel((x, 0), x) - im.putpalette(list(range(256)) * 4, "RGBA") - im_remapped = im.remap_palette(list(range(256))) - assert_image_equal(im, im_remapped) - assert im.palette is not None + im_p.putpixel((x, 0), x) + im_p.putpalette(list(range(256)) * 4, "RGBA") + im_remapped = im_p.remap_palette(list(range(256))) + assert_image_equal(im_p, im_remapped) + assert im_p.palette is not None assert im_remapped.palette is not None - assert im.palette.palette == im_remapped.palette.palette + assert im_p.palette.palette == im_remapped.palette.palette # Test illegal image mode - with hopper() as im: + with hopper() as im_hopper: with pytest.raises(ValueError): - im.remap_palette([]) + im_hopper.remap_palette([]) def test_remap_palette_transparency(self) -> None: im = Image.new("P", (1, 2), (0, 0, 0)) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 8d0ef4b221d..547a6c2c678 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -80,8 +80,8 @@ def test_16bit() -> None: _test_float_conversion(im) for color in (65535, 65536): - im = Image.new("I", (1, 1), color) - im_i16 = im.convert("I;16") + im_i = Image.new("I", (1, 1), color) + im_i16 = im_i.convert("I;16") assert im_i16.getpixel((0, 0)) == 65535 diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 07fec2e64af..b90ce84bc02 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -78,13 +78,13 @@ def test_crop_crash() -> None: extents = (1, 1, 10, 10) # works prepatch with Image.open(test_img) as img: - img2 = img.crop(extents) - img2.load() + img1 = img.crop(extents) + img1.load() # fail prepatch with Image.open(test_img) as img: - img = img.crop(extents) - img.load() + img2 = img.crop(extents) + img2.load() def test_crop_zero() -> None: diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index e8b783ff316..8876285605a 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -58,8 +58,8 @@ def test_rgba_quantize() -> None: def test_quantize() -> None: with Image.open("Tests/images/caption_6_33_22.png") as image: - image = image.convert("RGB") - converted = image.quantize() + converted = image.convert("RGB") + converted = converted.quantize() assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 1) @@ -67,13 +67,13 @@ def test_quantize() -> None: def test_quantize_no_dither() -> None: image = hopper() with Image.open("Tests/images/caption_6_33_22.png") as palette: - palette = palette.convert("P") + palette_p = palette.convert("P") - converted = image.quantize(dither=Image.Dither.NONE, palette=palette) + converted = image.quantize(dither=Image.Dither.NONE, palette=palette_p) assert converted.mode == "P" assert converted.palette is not None - assert palette.palette is not None - assert converted.palette.palette == palette.palette.palette + assert palette_p.palette is not None + assert converted.palette.palette == palette_p.palette.palette def test_quantize_no_dither2() -> None: @@ -97,10 +97,10 @@ def test_quantize_no_dither2() -> None: def test_quantize_dither_diff() -> None: image = hopper() with Image.open("Tests/images/caption_6_33_22.png") as palette: - palette = palette.convert("P") + palette_p = palette.convert("P") - dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette) - nodither = image.quantize(dither=Image.Dither.NONE, palette=palette) + dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette_p) + nodither = image.quantize(dither=Image.Dither.NONE, palette=palette_p) assert dither.tobytes() != nodither.tobytes() diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 270500a44b4..323d31f51fd 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -314,8 +314,8 @@ def resize(mode: str, size: tuple[int, int] | list[int]) -> None: @skip_unless_feature("libtiff") def test_transposed(self) -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: - im = im.resize((64, 64)) - assert im.size == (64, 64) + im_resized = im.resize((64, 64)) + assert im_resized.size == (64, 64) @pytest.mark.parametrize( "mode", ("L", "RGB", "I", "I;16", "I;16L", "I;16B", "I;16N", "F") diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 252a15db742..c3ff52f5767 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -43,8 +43,8 @@ def test_angle(angle: int) -> None: with Image.open("Tests/images/test-card.png") as im: rotate(im, im.mode, angle) - im = hopper() - assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1)) + im_hopper = hopper() + assert_image_equal(im_hopper.rotate(angle), im_hopper.rotate(angle, expand=1)) @pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) @@ -76,9 +76,9 @@ def test_center_0() -> None: with Image.open("Tests/images/hopper_45.png") as target: target_origin = target.size[1] / 2 - target = target.crop((0, target_origin, 128, target_origin + 128)) + im_target = target.crop((0, target_origin, 128, target_origin + 128)) - assert_image_similar(im, target, 15) + assert_image_similar(im, im_target, 15) def test_center_14() -> None: @@ -87,22 +87,22 @@ def test_center_14() -> None: with Image.open("Tests/images/hopper_45.png") as target: target_origin = target.size[1] / 2 - 14 - target = target.crop((6, target_origin, 128 + 6, target_origin + 128)) + im_target = target.crop((6, target_origin, 128 + 6, target_origin + 128)) - assert_image_similar(im, target, 10) + assert_image_similar(im, im_target, 10) def test_translate() -> None: im = hopper() with Image.open("Tests/images/hopper_45.png") as target: target_origin = (target.size[1] / 2 - 64) - 5 - target = target.crop( + im_target = target.crop( (target_origin, target_origin, target_origin + 128, target_origin + 128) ) im = im.rotate(45, translate=(5, 5), resample=Image.Resampling.BICUBIC) - assert_image_similar(im, target, 1) + assert_image_similar(im, im_target, 1) def test_fastpath_center() -> None: diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 1181f6fcaca..2ae230f3df1 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -159,9 +159,9 @@ def test_reducing_gap_for_DCT_scaling() -> None: with Image.open("Tests/images/hopper.jpg") as ref: # thumbnail should call draft with reducing_gap scale ref.draft(None, (18 * 3, 18 * 3)) - ref = ref.resize((18, 18), Image.Resampling.BICUBIC) + im_ref = ref.resize((18, 18), Image.Resampling.BICUBIC) with Image.open("Tests/images/hopper.jpg") as im: im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0) - assert_image_similar(ref, im, 1.4) + assert_image_similar(im_ref, im, 1.4) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 790acee2a46..49765cd68d0 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -198,10 +198,10 @@ def test_bitmap() -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) with Image.open("Tests/images/pil123rgba.png") as small: - small = small.resize((50, 50), Image.Resampling.NEAREST) + small_resized = small.resize((50, 50), Image.Resampling.NEAREST) # Act - draw.bitmap((10, 10), small) + draw.bitmap((10, 10), small_resized) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png") diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 27ac6f308fc..63cd0e4d4a9 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -261,10 +261,10 @@ def test_colorize_2color() -> None: # Open test image (256px by 10px, black to white) with Image.open("Tests/images/bw_gradient.png") as im: - im = im.convert("L") + im_l = im.convert("L") # Create image with original 2-color functionality - im_test = ImageOps.colorize(im, "red", "green") + im_test = ImageOps.colorize(im_l, "red", "green") # Test output image (2-color) left = (0, 1) @@ -301,11 +301,11 @@ def test_colorize_2color_offset() -> None: # Open test image (256px by 10px, black to white) with Image.open("Tests/images/bw_gradient.png") as im: - im = im.convert("L") + im_l = im.convert("L") # Create image with original 2-color functionality with offsets im_test = ImageOps.colorize( - im, black="red", white="green", blackpoint=50, whitepoint=100 + im_l, black="red", white="green", blackpoint=50, whitepoint=100 ) # Test output image (2-color) with offsets @@ -343,11 +343,11 @@ def test_colorize_3color_offset() -> None: # Open test image (256px by 10px, black to white) with Image.open("Tests/images/bw_gradient.png") as im: - im = im.convert("L") + im_l = im.convert("L") # Create image with new three color functionality with offsets im_test = ImageOps.colorize( - im, + im_l, black="red", white="green", mid="blue", diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 54cef00ad38..fc76f81e945 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -90,18 +90,18 @@ def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: # Arrange filename = tmp_path / "temp.pkl" with Image.open("Tests/images/hopper.jpg") as im: - im = im.convert("PA") + im_pa = im.convert("PA") # Act / Assert for protocol in range(pickle.HIGHEST_PROTOCOL + 1): - im._mode = "LA" + im_pa._mode = "LA" with open(filename, "wb") as f: - pickle.dump(im, f, protocol) + pickle.dump(im_pa, f, protocol) with open(filename, "rb") as f: loaded_im = pickle.load(f) - im._mode = "PA" - assert im == loaded_im + im_pa._mode = "PA" + assert im_pa == loaded_im @skip_unless_feature("webp") diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 465517bb699..a7e95ed83c8 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -49,11 +49,13 @@ def test_load_djpeg_filename(self, tmp_path: Path) -> None: @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: - im = im.convert("RGB") - self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) + im_rgb = im.convert("RGB") + self.assert_save_filename_check( + tmp_path, im_rgb, GifImagePlugin._save_netpbm + ) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: - im = im.convert("L") - self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) + im_l = im.convert("L") + self.assert_save_filename_check(tmp_path, im_l, GifImagePlugin._save_netpbm) From 4b90888a7db8953f3f56bf166672661b93343f5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Oct 2025 19:38:29 +1100 Subject: [PATCH 2119/2374] Added type hints --- Tests/test_file_gbr.py | 2 +- Tests/test_file_iptc.py | 2 +- src/PIL/ImageText.py | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index b8851d82beb..d89ef0583ec 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -33,7 +33,7 @@ def test_multiple_load_operations() -> None: assert_image_equal_tofile(im, "Tests/images/gbr.png") -def create_gbr_image(info: dict[str, int] = {}, magic_number=b"") -> BytesIO: +def create_gbr_image(info: dict[str, int] = {}, magic_number: bytes = b"") -> BytesIO: return BytesIO( b"".join( _binary.o32be(i) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 5a8aaa3ef32..0376b99977b 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -12,7 +12,7 @@ def create_iptc_image(info: dict[str, int] = {}) -> BytesIO: - def field(tag, value): + def field(tag: tuple[int, int], value: bytes) -> bytes: return bytes((0x1C,) + tag + (0, len(value))) + value data = field((3, 60), bytes((info.get("layers", 1), info.get("component", 0)))) diff --git a/src/PIL/ImageText.py b/src/PIL/ImageText.py index c74570e6939..6c5a8a8a3ef 100644 --- a/src/PIL/ImageText.py +++ b/src/PIL/ImageText.py @@ -88,7 +88,7 @@ def _get_fontmode(self) -> str: else: return "L" - def get_length(self): + def get_length(self) -> float: """ Returns length (in pixels with 1/64 precision) of text. @@ -130,8 +130,7 @@ def get_length(self): :return: Either width for horizontal text, or height for vertical text. """ - split_character = "\n" if isinstance(self.text, str) else b"\n" - if split_character in self.text: + if "\n" in self.text if isinstance(self.text, str) else b"\n" in self.text: msg = "can't measure length of multiline text" raise ValueError(msg) return self.font.getlength( From e90bb1559cfd0ab18c2af653a1a8b1305bb0b2ca Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Oct 2025 21:00:29 +1100 Subject: [PATCH 2120/2374] Rearranged code --- src/PIL/ImageText.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageText.py b/src/PIL/ImageText.py index 6c5a8a8a3ef..8bfd8988443 100644 --- a/src/PIL/ImageText.py +++ b/src/PIL/ImageText.py @@ -130,7 +130,11 @@ def get_length(self) -> float: :return: Either width for horizontal text, or height for vertical text. """ - if "\n" in self.text if isinstance(self.text, str) else b"\n" in self.text: + if isinstance(self.text, str): + multiline = "\n" in self.text + else: + multiline = b"\n" in self.text + if multiline: msg = "can't measure length of multiline text" raise ValueError(msg) return self.font.getlength( From e1f4352ce9b3b92912f7a678666c9f5bca09101e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 21 Oct 2025 23:11:18 +1100 Subject: [PATCH 2121/2374] Fixed ZeroDivisionError --- Tests/images/zero_mask_totals.dds | Bin 0 -> 131 bytes Tests/test_file_dds.py | 5 +++++ src/PIL/DdsImagePlugin.py | 3 +++ 3 files changed, 8 insertions(+) create mode 100644 Tests/images/zero_mask_totals.dds diff --git a/Tests/images/zero_mask_totals.dds b/Tests/images/zero_mask_totals.dds new file mode 100644 index 0000000000000000000000000000000000000000..31e329e4f266b8c8407531b77e65fbd9c46b41a4 GIT binary patch literal 131 pcmZ>930A0KU|`@EU|?Vb(jd$X#N+@4pe6^XMhPg5LILf-0s!Yi0we$c literal 0 HcmV?d00001 diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 116dfa59cec..60d0c09bce1 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -380,6 +380,11 @@ def test_palette() -> None: assert_image_equal_tofile(im, "Tests/images/transparent.gif") +def test_zero_mask_totals() -> None: + with Image.open("Tests/images/zero_mask_totals.dds") as im: + im.load() + + def test_unsupported_header_size() -> None: with pytest.raises(OSError, match="Unsupported header size 0"): with Image.open(BytesIO(b"DDS " + b"\x00" * 4)): diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index f9ade18f9a1..37e16d52725 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -333,6 +333,7 @@ class DdsImageFile(ImageFile.ImageFile): format_description = "DirectDraw Surface" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(4)): msg = "not a DDS file" raise SyntaxError(msg) @@ -516,6 +517,8 @@ def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int # Remove the zero padding, and scale it to 8 bits data += o8( int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) + if mask_totals[i] + else 0 ) self.set_as_raw(data) return -1, 0 From 7d6f2ce90b688ef4dfd485cd5336ea52cdf48c77 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 21 Oct 2025 23:35:17 +1100 Subject: [PATCH 2122/2374] Removed BytesIO --- src/PIL/DdsImagePlugin.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index f9ade18f9a1..19a210275da 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -12,7 +12,6 @@ from __future__ import annotations -import io import struct import sys from enum import IntEnum, IntFlag @@ -340,21 +339,20 @@ def _open(self) -> None: if header_size != 124: msg = f"Unsupported header size {repr(header_size)}" raise OSError(msg) - header_bytes = self.fp.read(header_size - 4) - if len(header_bytes) != 120: - msg = f"Incomplete header: {len(header_bytes)} bytes" + header = self.fp.read(header_size - 4) + if len(header) != 120: + msg = f"Incomplete header: {len(header)} bytes" raise OSError(msg) - header = io.BytesIO(header_bytes) - flags, height, width = struct.unpack("<3I", header.read(12)) + flags, height, width = struct.unpack("<3I", header[:12]) self._size = (width, height) extents = (0, 0) + self.size - pitch, depth, mipmaps = struct.unpack("<3I", header.read(12)) - struct.unpack("<11I", header.read(44)) # reserved + pitch, depth, mipmaps = struct.unpack("<3I", header[12:24]) + struct.unpack("<11I", header[24:68]) # reserved # pixel format - pfsize, pfflags, fourcc, bitcount = struct.unpack("<4I", header.read(16)) + pfsize, pfflags, fourcc, bitcount = struct.unpack("<4I", header[68:84]) n = 0 rawmode = None if pfflags & DDPF.RGB: @@ -366,7 +364,7 @@ def _open(self) -> None: self._mode = "RGB" mask_count = 3 - masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4)) + masks = struct.unpack(f"<{mask_count}I", header[84 : 84 + mask_count * 4]) self.tile = [ImageFile._Tile("dds_rgb", extents, 0, (bitcount, masks))] return elif pfflags & DDPF.LUMINANCE: From b1e2f2e6528f92c0b763c86374ef782210a625fe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Oct 2025 20:08:22 +1100 Subject: [PATCH 2123/2374] Improved coverage --- Tests/test_imagetext.py | 11 +++++++++++ src/PIL/ImageText.py | 3 +-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagetext.py b/Tests/test_imagetext.py index 7db22989786..46afea0645a 100644 --- a/Tests/test_imagetext.py +++ b/Tests/test_imagetext.py @@ -32,6 +32,10 @@ def test_get_length(font: ImageFont.FreeTypeFont) -> None: assert ImageText.Text("y", font).get_length() == 12 assert ImageText.Text("a", font).get_length() == 12 + text = ImageText.Text("\n", font) + with pytest.raises(ValueError, match="can't measure length of multiline text"): + text.get_length() + def test_get_bbox(font: ImageFont.FreeTypeFont) -> None: assert ImageText.Text("A", font).get_bbox() == (0, 4, 12, 16) @@ -45,6 +49,7 @@ def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: font = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) text = ImageText.Text("Hello World!", font) text.embed_color() + assert text.get_length() == 288 im = Image.new("RGB", (300, 64), "white") draw = ImageDraw.Draw(im) @@ -52,6 +57,12 @@ def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1) + text = ImageText.Text("", mode="1") + with pytest.raises( + ValueError, match="Embedded color supported only in RGB and RGBA modes" + ): + text.embed_color() + @skip_unless_feature("freetype2") def test_stroke() -> None: diff --git a/src/PIL/ImageText.py b/src/PIL/ImageText.py index 8bfd8988443..e6ccd824332 100644 --- a/src/PIL/ImageText.py +++ b/src/PIL/ImageText.py @@ -316,6 +316,5 @@ def get_bbox( max(bbox[3], bbox_line[3]), ) - if bbox is None: - return xy[0], xy[1], xy[0], xy[1] + assert bbox is not None return bbox From 208bbe95f9ccaf5659d5b246e18ce76bcefeea84 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Oct 2025 22:22:00 +1100 Subject: [PATCH 2124/2374] Remove I;32L and I;32B modes --- src/libImaging/Mode.c | 1 - src/libImaging/Mode.h | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c index 7521f4cdae4..d6ee261317b 100644 --- a/src/libImaging/Mode.c +++ b/src/libImaging/Mode.c @@ -20,7 +20,6 @@ const ModeData MODES[] = { [IMAGING_MODE_I_16] = {"I;16"}, [IMAGING_MODE_I_16L] = {"I;16L"}, [IMAGING_MODE_I_16B] = {"I;16B"}, [IMAGING_MODE_I_16N] = {"I;16N"}, - [IMAGING_MODE_I_32L] = {"I;32L"}, [IMAGING_MODE_I_32B] = {"I;32B"}, }; const ModeID diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index a3eb3d86da5..8cb96c984c4 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -25,8 +25,6 @@ typedef enum { IMAGING_MODE_I_16L, IMAGING_MODE_I_16B, IMAGING_MODE_I_16N, - IMAGING_MODE_I_32L, - IMAGING_MODE_I_32B, } ModeID; typedef struct { @@ -64,8 +62,6 @@ typedef enum { IMAGING_RAWMODE_I_16L, IMAGING_RAWMODE_I_16B, IMAGING_RAWMODE_I_16N, - IMAGING_RAWMODE_I_32L, - IMAGING_RAWMODE_I_32B, // Rawmodes IMAGING_RAWMODE_1_8, @@ -106,6 +102,8 @@ typedef enum { IMAGING_RAWMODE_C_I, IMAGING_RAWMODE_Cb, IMAGING_RAWMODE_Cr, + IMAGING_RAWMODE_I_32B, + IMAGING_RAWMODE_I_32L, IMAGING_RAWMODE_F_16, IMAGING_RAWMODE_F_16B, IMAGING_RAWMODE_F_16BS, From 109ee1569ddc3156229dfc4b683d252409afe51f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Oct 2025 22:24:15 +1100 Subject: [PATCH 2125/2374] Removed I;32L rawmode --- src/libImaging/Mode.c | 1 - src/libImaging/Mode.h | 1 - 2 files changed, 2 deletions(-) diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c index d6ee261317b..2e459c48fb4 100644 --- a/src/libImaging/Mode.c +++ b/src/libImaging/Mode.c @@ -75,7 +75,6 @@ const RawModeData RAWMODES[] = { [IMAGING_RAWMODE_I_16L] = {"I;16L"}, [IMAGING_RAWMODE_I_16B] = {"I;16B"}, [IMAGING_RAWMODE_I_16N] = {"I;16N"}, - [IMAGING_RAWMODE_I_32L] = {"I;32L"}, [IMAGING_RAWMODE_I_32B] = {"I;32B"}, [IMAGING_RAWMODE_1_8] = {"1;8"}, diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h index 8cb96c984c4..39c0eb91994 100644 --- a/src/libImaging/Mode.h +++ b/src/libImaging/Mode.h @@ -103,7 +103,6 @@ typedef enum { IMAGING_RAWMODE_Cb, IMAGING_RAWMODE_Cr, IMAGING_RAWMODE_I_32B, - IMAGING_RAWMODE_I_32L, IMAGING_RAWMODE_F_16, IMAGING_RAWMODE_F_16B, IMAGING_RAWMODE_F_16BS, From b04d8792f5779b24c3c723dd368a4d43d7609276 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Oct 2025 08:53:00 +1100 Subject: [PATCH 2126/2374] Support writing InkNames --- Tests/test_file_libtiff.py | 12 ++++++++++++ src/PIL/TiffTags.py | 1 - src/encode.c | 11 ++++++----- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 4908496cff3..e53832db3b8 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -355,6 +355,18 @@ def test_subifd(self, tmp_path: Path) -> None: # Should not segfault im.save(outfile) + def test_inknames_tag( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) + + out = tmp_path / "temp.tif" + hopper("L").save(out, tiffinfo={333: "name\x00"}) + + with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) + assert reloaded.tag_v2[333] in ("name", "name\x00") + def test_whitepoint_tag( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 761aa3f6b08..613a3b7def7 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -558,7 +558,6 @@ def _populate() -> None: LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff LIBTIFF_CORE.remove(323) # Tiled images -LIBTIFF_CORE.remove(333) # Ink Names either # Note to advanced users: There may be combinations of these # parameters and values that when added properly, will work and diff --git a/src/encode.c b/src/encode.c index b1d0181e0b7..f0e204bc6eb 100644 --- a/src/encode.c +++ b/src/encode.c @@ -668,10 +668,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { int key_int, status, is_core_tag, is_var_length, num_core_tags, i; TIFFDataType type = TIFF_NOTYPE; // This list also exists in TiffTags.py - const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, - 277, 278, 280, 281, 340, 341, 282, 283, 284, - 286, 287, 296, 297, 320, 321, 338, 32995, 32998, - 32996, 339, 32997, 330, 531, 530, 65537, 301, 532}; + const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, 277, + 278, 280, 281, 282, 283, 284, 286, 287, 296, 297, + 301, 320, 321, 330, 333, 338, 339, 340, 341, 530, + 531, 532, 32995, 32996, 32997, 32998, 65537}; Py_ssize_t tags_size; PyObject *item; @@ -821,7 +821,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { } } - if (type == TIFF_BYTE || type == TIFF_UNDEFINED) { + if (type == TIFF_BYTE || type == TIFF_UNDEFINED || + key_int == TIFFTAG_INKNAMES) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, From ddd4f007209a3af7f8976322c13c97f205cc4f06 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Oct 2025 20:03:14 +1100 Subject: [PATCH 2127/2374] Support writing IFD tag types --- Tests/test_file_libtiff.py | 14 ++++++++++++++ src/encode.c | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index e53832db3b8..38e4111a1d6 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -355,6 +355,20 @@ def test_subifd(self, tmp_path: Path) -> None: # Should not segfault im.save(outfile) + def test_ifd(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) + + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[37000] = 100 + ifd.tagtype[37000] = TiffTags.IFD + + out = tmp_path / "temp.tif" + im = Image.new("L", (1, 1)) + im.save(out, tiffinfo=ifd) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[37000] == 100 + def test_inknames_tag( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: diff --git a/src/encode.c b/src/encode.c index f0e204bc6eb..2e9ef843d74 100644 --- a/src/encode.c +++ b/src/encode.c @@ -974,7 +974,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (UINT16)PyLong_AsLong(value) ); - } else if (type == TIFF_LONG) { + } else if (type == TIFF_LONG || type == TIFF_IFD) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value) ); From 82cdaa456c88139a2d8d6d23cac8b9dacf4dc75f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Oct 2025 03:55:45 +1100 Subject: [PATCH 2128/2374] Support writing SIGNED_RATIONAL tag types --- Tests/test_file_libtiff.py | 7 +++++-- src/encode.c | 7 ++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 38e4111a1d6..7cb3ea8e40b 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -355,12 +355,15 @@ def test_subifd(self, tmp_path: Path) -> None: # Should not segfault im.save(outfile) - def test_ifd(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + @pytest.mark.parametrize("tagtype", (TiffTags.SIGNED_RATIONAL, TiffTags.IFD)) + def test_tag_type( + self, tagtype: int, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd[37000] = 100 - ifd.tagtype[37000] = TiffTags.IFD + ifd.tagtype[37000] = tagtype out = tmp_path / "temp.tif" im = Image.new("L", (1, 1)) diff --git a/src/encode.c b/src/encode.c index 2e9ef843d74..513309c8d7d 100644 --- a/src/encode.c +++ b/src/encode.c @@ -990,10 +990,6 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (FLOAT32)PyFloat_AsDouble(value) ); - } else if (type == TIFF_DOUBLE) { - status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value) - ); } else if (type == TIFF_SBYTE) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (INT8)PyLong_AsLong(value) @@ -1002,7 +998,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, PyBytes_AsString(value) ); - } else if (type == TIFF_RATIONAL) { + } else if (type == TIFF_DOUBLE || type == TIFF_SRATIONAL || + type == TIFF_RATIONAL) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value) ); From 29c5ffe7451bc981925a9c4f8cb294d69e086d32 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 02:48:10 +0000 Subject: [PATCH 2129/2374] Update github-actions --- .github/workflows/cifuzz.yml | 4 ++-- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 2 +- .github/workflows/wheels.yml | 14 +++++++------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 0456bbaba3c..6a86b8aeb3b 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -44,13 +44,13 @@ jobs: language: python dry-run: false - name: Upload New Crash - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: failure() && steps.build.outcome == 'success' with: name: artifacts path: ./out/artifacts - name: Upload Legacy Crash - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: steps.run.outcome == 'success' with: name: crash diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index f6a7dd46b04..02d4da999bf 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -216,7 +216,7 @@ jobs: shell: bash - name: Upload errors - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: failure() with: name: errors diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b52000a2766..ef7b34b8d10 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -140,7 +140,7 @@ jobs: mkdir -p Tests/errors - name: Upload errors - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: failure() with: name: errors diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6dc8db7e9c8..4bc48bec975 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -134,7 +134,7 @@ jobs: CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: dist-${{ matrix.name }} path: ./wheelhouse/*.whl @@ -220,13 +220,13 @@ jobs: shell: cmd - name: Upload wheels - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: dist-windows-${{ matrix.cibw_arch }} path: ./wheelhouse/*.whl - name: Upload fribidi.dll - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: fribidi-windows-${{ matrix.cibw_arch }} path: winbuild\build\bin\fribidi* @@ -246,7 +246,7 @@ jobs: - run: make sdist - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: dist-sdist path: dist/*.tar.gz @@ -256,7 +256,7 @@ jobs: runs-on: ubuntu-latest name: Count dists steps: - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: pattern: dist-* path: dist @@ -275,7 +275,7 @@ jobs: runs-on: ubuntu-latest name: Upload wheels to scientific-python-nightly-wheels steps: - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: pattern: dist-!(sdist)* path: dist @@ -297,7 +297,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: pattern: dist-* path: dist From dfd24ba6150ea3803099d445ce1a486fb7e87c13 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Oct 2025 22:03:39 +1100 Subject: [PATCH 2130/2374] Read all non-zero transparency from mode 1 images in the same way --- Tests/test_file_png.py | 9 +++++++++ src/PIL/PngImagePlugin.py | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index dc1077fedab..9875fe09676 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -338,6 +338,15 @@ def test_save_grayscale_transparency(self, tmp_path: Path) -> None: assert colors is not None assert colors[0][0] == num_transparent + def test_save_1_transparency(self, tmp_path: Path) -> None: + out = tmp_path / "temp.png" + + im = Image.new("1", (1, 1), 1) + im.save(out, transparency=1) + + with Image.open(out) as reloaded: + assert reloaded.info["transparency"] == 255 + def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/caption_6_33_22.png" with Image.open(in_file) as im: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index d0f22f812e5..11a48e55c39 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -509,7 +509,9 @@ def chunk_tRNS(self, pos: int, length: int) -> bytes: # otherwise, we have a byte string with one alpha value # for each palette entry self.im_info["transparency"] = s - elif self.im_mode in ("1", "L", "I;16"): + elif self.im_mode == "1": + self.im_info["transparency"] = 255 if i16(s) else 0 + elif self.im_mode in ("L", "I;16"): self.im_info["transparency"] = i16(s) elif self.im_mode == "RGB": self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4) From 1a27f958d7d51e9fef68493d68a29c423e5f6e38 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Oct 2025 18:19:05 +1100 Subject: [PATCH 2131/2374] Updated brotli to 1.2.0 --- .github/workflows/wheels-dependencies.sh | 9 ++--- .pre-commit-config.yaml | 6 ++-- MANIFEST.in | 1 - patches/README.md | 14 -------- patches/iOS/brotli-1.1.0.tar.gz.patch | 46 ------------------------ winbuild/build_prepare.py | 2 +- 6 files changed, 7 insertions(+), 71 deletions(-) delete mode 100644 patches/README.md delete mode 100644 patches/iOS/brotli-1.1.0.tar.gz.patch diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 7d6eb8681a4..cdc1faf15ac 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -32,7 +32,6 @@ if [[ "$CIBW_PLATFORM" == "ios" ]]; then # or `build/deps/iphonesimulator` WORKDIR=$(pwd)/build/$IOS_SDK BUILD_PREFIX=$(pwd)/build/deps/$IOS_SDK - PATCH_DIR=$(pwd)/patches/iOS # GNU tooling insists on using aarch64 rather than arm64 if [[ $PLAT == "arm64" ]]; then @@ -90,9 +89,7 @@ fi ARCHIVE_SDIR=pillow-depends-main -# Package versions for fresh source builds. Version numbers with "Patched" -# annotations have a source code patch that is required for some platforms. If -# you change those versions, ensure the patch is also updated. +# Package versions for fresh source builds. if [[ -n "$IOS_SDK" ]]; then FREETYPE_VERSION=2.13.3 else @@ -110,7 +107,7 @@ ZLIB_NG_VERSION=2.2.5 LIBWEBP_VERSION=1.6.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 -BROTLI_VERSION=1.1.0 # Patched; next release won't need patching. See patch file. +BROTLI_VERSION=1.2.0 LIBAVIF_VERSION=1.3.0 function build_pkg_config { @@ -168,7 +165,7 @@ function build_brotli { if [ -e brotli-stamp ]; then return; fi local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ - && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \ + && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib -DCMAKE_MACOSX_BUNDLE=OFF $HOST_CMAKE_FLAGS . \ && make -j4 install) touch brotli-stamp } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ab0153687d2..b9f7e599b89 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: rev: v1.5.5 hooks: - id: remove-tabs - exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$) + exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format rev: v21.1.2 @@ -46,9 +46,9 @@ repos: - id: check-yaml args: [--allow-multiple-documents] - id: end-of-file-fixer - exclude: ^Tests/images/|\.patch$ + exclude: ^Tests/images/ - id: trailing-whitespace - exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$ + exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.34.0 diff --git a/MANIFEST.in b/MANIFEST.in index 6623f227d60..d4623a4a8e5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,7 +15,6 @@ include tox.ini graft Tests graft Tests/images graft checks -graft patches graft src graft depends graft winbuild diff --git a/patches/README.md b/patches/README.md deleted file mode 100644 index ff4a8f0994f..00000000000 --- a/patches/README.md +++ /dev/null @@ -1,14 +0,0 @@ -Although we try to use official sources for dependencies, sometimes the official -sources don't support a platform (especially mobile platforms), or there's a bug -fix/feature that is required to support Pillow's usage. - -This folder contains patches that must be applied to official sources, organized -by the platforms that need those patches. - -Each patch is against the root of the unpacked official tarball, and is named by -appending `.patch` to the end of the tarball that is to be patched. This -includes the full version number; so if the version is bumped, the patch will -at a minimum require a filename change. - -Wherever possible, these patches should be contributed upstream, in the hope that -future Pillow versions won't need to maintain these patches. diff --git a/patches/iOS/brotli-1.1.0.tar.gz.patch b/patches/iOS/brotli-1.1.0.tar.gz.patch deleted file mode 100644 index f165a9ac12f..00000000000 --- a/patches/iOS/brotli-1.1.0.tar.gz.patch +++ /dev/null @@ -1,46 +0,0 @@ -# Brotli 1.1.0 doesn't have explicit support for iOS as a CMAKE_SYSTEM_NAME. -# That release was from 2023; there have been subsequent changes that allow -# Brotli to build on iOS without any patches, as long as -DBROTLI_BUILD_TOOLS=NO -# is specified on the command line. -# -diff -ru brotli-1.1.0-orig/CMakeLists.txt brotli-1.1.0/CMakeLists.txt ---- brotli-1.1.0-orig/CMakeLists.txt 2023-08-29 19:00:29 -+++ brotli-1.1.0/CMakeLists.txt 2024-11-07 10:46:26 -@@ -114,6 +114,8 @@ - add_definitions(-DOS_MACOSX) - set(CMAKE_MACOS_RPATH TRUE) - set(CMAKE_INSTALL_NAME_DIR "${CMAKE_INSTALL_PREFIX}/lib") -+elseif(${CMAKE_SYSTEM_NAME} MATCHES "iOS") -+ add_definitions(-DOS_IOS) - endif() - - if(BROTLI_EMSCRIPTEN) -@@ -174,10 +176,12 @@ - - # Installation - if(NOT BROTLI_BUNDLED_MODE) -- install( -- TARGETS brotli -- RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" -- ) -+ if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "iOS") -+ install( -+ TARGETS brotli -+ RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" -+ ) -+ endif() - - install( - TARGETS ${BROTLI_LIBRARIES_CORE} -diff -ru brotli-1.1.0-orig/c/common/platform.h brotli-1.1.0/c/common/platform.h ---- brotli-1.1.0-orig/c/common/platform.h 2023-08-29 19:00:29 -+++ brotli-1.1.0/c/common/platform.h 2024-11-07 10:47:28 -@@ -33,7 +33,7 @@ - #include - #elif defined(OS_FREEBSD) - #include --#elif defined(OS_MACOSX) -+#elif defined(OS_MACOSX) || defined(OS_IOS) - #include - /* Let's try and follow the Linux convention */ - #define BROTLI_X_BYTE_ORDER BYTE_ORDER diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 186a80cca5e..1277e404fca 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ def cmd_msbuild( } V = { - "BROTLI": "1.1.0", + "BROTLI": "1.2.0", "FREETYPE": "2.14.1", "FRIBIDI": "1.0.16", "HARFBUZZ": "12.1.0", From b3d9bd9950d9806ef904566896228d37df824821 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Nov 2025 23:07:15 +1100 Subject: [PATCH 2132/2374] Test ImageFont.ImageFont, in case freetype2 is not supported --- Tests/test_imagetext.py | 79 +++++++++++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/Tests/test_imagetext.py b/Tests/test_imagetext.py index 46afea0645a..2b424629dfd 100644 --- a/Tests/test_imagetext.py +++ b/Tests/test_imagetext.py @@ -2,7 +2,7 @@ import pytest -from PIL import Image, ImageDraw, ImageFont, ImageText +from PIL import Image, ImageDraw, ImageFont, ImageText, features from .helper import assert_image_similar_tofile, skip_unless_feature @@ -20,42 +20,69 @@ def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout: return request.param -@pytest.fixture(scope="module") -def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont: - return ImageFont.truetype(FONT_PATH, 20, layout_engine=layout_engine) - - -def test_get_length(font: ImageFont.FreeTypeFont) -> None: - assert ImageText.Text("A", font).get_length() == 12 - assert ImageText.Text("AB", font).get_length() == 24 - assert ImageText.Text("M", font).get_length() == 12 - assert ImageText.Text("y", font).get_length() == 12 - assert ImageText.Text("a", font).get_length() == 12 +@pytest.fixture( + scope="module", + params=[ + None, + pytest.param(ImageFont.Layout.BASIC, marks=skip_unless_feature("freetype2")), + pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), + ], +) +def font( + request: pytest.FixtureRequest, +) -> ImageFont.ImageFont | ImageFont.FreeTypeFont: + layout_engine = request.param + if layout_engine is None: + return ImageFont.load_default_imagefont() + else: + return ImageFont.truetype(FONT_PATH, 20, layout_engine=layout_engine) + + +def test_get_length(font: ImageFont.ImageFont | ImageFont.FreeTypeFont) -> None: + factor = 1 if isinstance(font, ImageFont.ImageFont) else 2 + assert ImageText.Text("A", font).get_length() == 6 * factor + assert ImageText.Text("AB", font).get_length() == 12 * factor + assert ImageText.Text("M", font).get_length() == 6 * factor + assert ImageText.Text("y", font).get_length() == 6 * factor + assert ImageText.Text("a", font).get_length() == 6 * factor text = ImageText.Text("\n", font) with pytest.raises(ValueError, match="can't measure length of multiline text"): text.get_length() -def test_get_bbox(font: ImageFont.FreeTypeFont) -> None: - assert ImageText.Text("A", font).get_bbox() == (0, 4, 12, 16) - assert ImageText.Text("AB", font).get_bbox() == (0, 4, 24, 16) - assert ImageText.Text("M", font).get_bbox() == (0, 4, 12, 16) - assert ImageText.Text("y", font).get_bbox() == (0, 7, 12, 20) - assert ImageText.Text("a", font).get_bbox() == (0, 7, 12, 16) +@pytest.mark.parametrize( + "text, expected", + ( + ("A", (0, 4, 12, 16)), + ("AB", (0, 4, 24, 16)), + ("M", (0, 4, 12, 16)), + ("y", (0, 7, 12, 20)), + ("a", (0, 7, 12, 16)), + ), +) +def test_get_bbox( + font: ImageFont.ImageFont | ImageFont.FreeTypeFont, + text: str, + expected: tuple[int, int, int, int], +) -> None: + if isinstance(font, ImageFont.ImageFont): + expected = (0, 0, expected[2] // 2, 11) + assert ImageText.Text(text, font).get_bbox() == expected def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: - font = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) - text = ImageText.Text("Hello World!", font) - text.embed_color() - assert text.get_length() == 288 + if features.check_module("freetype2"): + font = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) + text = ImageText.Text("Hello World!", font) + text.embed_color() + assert text.get_length() == 288 - im = Image.new("RGB", (300, 64), "white") - draw = ImageDraw.Draw(im) - draw.text((10, 10), text, "#fa6") + im = Image.new("RGB", (300, 64), "white") + draw = ImageDraw.Draw(im) + draw.text((10, 10), text, "#fa6") - assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1) + assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1) text = ImageText.Text("", mode="1") with pytest.raises( From 85d783fb52cd93584da76018baec888cbf680f6d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:20:17 +0000 Subject: [PATCH 2133/2374] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.3 → v0.14.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.3...v0.14.3) - [github.com/python-jsonschema/check-jsonschema: 0.34.0 → 0.34.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.34.0...0.34.1) - [github.com/zizmorcore/zizmor-pre-commit: v1.14.2 → v1.16.2](https://github.com/zizmorcore/zizmor-pre-commit/compare/v1.14.2...v1.16.2) - [github.com/sphinx-contrib/sphinx-lint: v1.0.0 → v1.0.1](https://github.com/sphinx-contrib/sphinx-lint/compare/v1.0.0...v1.0.1) - [github.com/tox-dev/pyproject-fmt: v2.7.0 → v2.11.0](https://github.com/tox-dev/pyproject-fmt/compare/v2.7.0...v2.11.0) - [github.com/tox-dev/tox-ini-fmt: 1.6.0 → 1.7.0](https://github.com/tox-dev/tox-ini-fmt/compare/1.6.0...1.7.0) --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ab0153687d2..beef225fd77 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.3 + rev: v0.14.3 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] @@ -51,24 +51,24 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.34.0 + rev: 0.34.1 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.14.2 + rev: v1.16.2 hooks: - id: zizmor - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v1.0.0 + rev: v1.0.1 hooks: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.7.0 + rev: v2.11.0 hooks: - id: pyproject-fmt @@ -79,7 +79,7 @@ repos: additional_dependencies: [trove-classifiers>=2024.10.12] - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.6.0 + rev: 1.7.0 hooks: - id: tox-ini-fmt From 666dd5247819ceec8b772758f2d0d2d1e9ec0572 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:56:21 +0200 Subject: [PATCH 2134/2374] Drop removed rule --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0006ccd1297..f4514925d1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -189,7 +189,6 @@ lint.ignore = [ "PT012", # pytest-raises-with-multiple-statements "PT017", # pytest-assert-in-except "PYI034", # flake8-pyi: typing.Self added in Python 3.11 - "UP038", # pyupgrade: deprecated rule ] lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [ "I002", From e44ce2f00e6c1fd8fffa30b812aa522e5c16fbdb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Nov 2025 19:09:49 +1100 Subject: [PATCH 2135/2374] Updated harfbuzz to 12.2.0 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 7d6eb8681a4..226fcdb6af7 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -98,7 +98,7 @@ if [[ -n "$IOS_SDK" ]]; then else FREETYPE_VERSION=2.14.1 fi -HARFBUZZ_VERSION=12.1.0 +HARFBUZZ_VERSION=12.2.0 LIBPNG_VERSION=1.6.50 JPEGTURBO_VERSION=3.1.2 OPENJPEG_VERSION=2.5.4 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 186a80cca5e..2401dd4ecd6 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ def cmd_msbuild( "BROTLI": "1.1.0", "FREETYPE": "2.14.1", "FRIBIDI": "1.0.16", - "HARFBUZZ": "12.1.0", + "HARFBUZZ": "12.2.0", "JPEGTURBO": "3.1.2", "LCMS2": "2.17", "LIBAVIF": "1.3.0", From 18c7f87fe3180e494c9397d63f917aa9f05d870c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Nov 2025 23:21:46 +1100 Subject: [PATCH 2136/2374] Added Fedora 43 --- .github/workflows/test-docker.yml | 1 + docs/installation/platform-support.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 581e1f52bf4..213062ee2b8 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -50,6 +50,7 @@ jobs: debian-13-trixie-x86, debian-13-trixie-amd64, fedora-42-amd64, + fedora-43-amd64, gentoo, ubuntu-22.04-jammy-amd64, ubuntu-24.04-noble-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index e0c4a8eec3d..17e38719a93 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -35,6 +35,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 42 | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 43 | 3.14 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 15 Sequoia | 3.10 | x86-64 | From 921a470506bdf7cc82d78d6ff4915b6aa5420b7b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 6 Nov 2025 18:24:10 +1100 Subject: [PATCH 2137/2374] Simplified code --- src/_imaging.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index f6be4a90124..d2a195887fa 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2459,7 +2459,6 @@ _merge(PyObject *self, PyObject *args) { static PyObject * _split(ImagingObject *self) { - int fails = 0; Py_ssize_t i; PyObject *list; PyObject *imaging_object; @@ -2473,14 +2472,12 @@ _split(ImagingObject *self) { for (i = 0; i < self->image->bands; i++) { imaging_object = PyImagingNew(bands[i]); if (!imaging_object) { - fails += 1; + Py_DECREF(list); + list = NULL; + break; } PyTuple_SET_ITEM(list, i, imaging_object); } - if (fails) { - Py_DECREF(list); - list = NULL; - } return list; } From 142c1320b23fa645dd115e8b979407518e3cd696 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 14 Nov 2025 20:08:49 +1100 Subject: [PATCH 2138/2374] Apply encoder options when saving multiple PNG frames --- Tests/test_file_apng.py | 20 ++++++++++++++++++++ src/PIL/PngImagePlugin.py | 25 ++++++++++++++----------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 12204b5b78c..d918a24a799 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -1,5 +1,6 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path import pytest @@ -718,6 +719,25 @@ def test_apng_save_size(tmp_path: Path) -> None: assert reloaded.size == (200, 200) +def test_compress_level() -> None: + compress_level_sizes = {} + for compress_level in (0, 9): + out = BytesIO() + + im = Image.new("L", (100, 100)) + im.save( + out, + "PNG", + save_all=True, + append_images=[Image.new("L", (200, 200))], + compress_level=compress_level, + ) + + compress_level_sizes[compress_level] = len(out.getvalue()) + + assert compress_level_sizes[0] > compress_level_sizes[9] + + def test_seek_after_close() -> None: im = Image.open("Tests/images/apng/delay.png") im.seek(1) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index d0f22f812e5..b89c10da3c1 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1152,6 +1152,15 @@ def write(self, data: bytes) -> None: self.seq_num += 1 +def _apply_encoderinfo(im: Image.Image, encoderinfo: dict[str, Any]) -> None: + im.encoderconfig = ( + encoderinfo.get("optimize", False), + encoderinfo.get("compress_level", -1), + encoderinfo.get("compress_type", -1), + encoderinfo.get("dictionary", b""), + ) + + class _Frame(NamedTuple): im: Image.Image bbox: tuple[int, int, int, int] | None @@ -1245,10 +1254,10 @@ def _write_multiple_frames( # default image IDAT (if it exists) if default_image: - if im.mode != mode: - im = im.convert(mode) + default_im = im if im.mode == mode else im.convert(mode) + _apply_encoderinfo(default_im, im.encoderinfo) ImageFile._save( - im, + default_im, cast(IO[bytes], _idat(fp, chunk)), [ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)], ) @@ -1282,6 +1291,7 @@ def _write_multiple_frames( ) seq_num += 1 # frame data + _apply_encoderinfo(im_frame, im.encoderinfo) if frame == 0 and not default_image: # first frame must be in IDAT chunks for backwards compatibility ImageFile._save( @@ -1357,14 +1367,6 @@ def _save( bits = 4 outmode += f";{bits}" - # encoder options - im.encoderconfig = ( - im.encoderinfo.get("optimize", False), - im.encoderinfo.get("compress_level", -1), - im.encoderinfo.get("compress_type", -1), - im.encoderinfo.get("dictionary", b""), - ) - # get the corresponding PNG mode try: rawmode, bit_depth, color_type = _OUTMODES[outmode] @@ -1494,6 +1496,7 @@ def _save( im, fp, chunk, mode, rawmode, default_image, append_images ) if single_im: + _apply_encoderinfo(single_im, im.encoderinfo) ImageFile._save( single_im, cast(IO[bytes], _idat(fp, chunk)), From bc1237ef3d22f7407fbeb3de19c1c867ed5dfd0a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 14 Nov 2025 21:22:58 +1100 Subject: [PATCH 2139/2374] Update dependency cibuildwheel to v3.3.0 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 56517374f5b..485866de67c 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.2.1 +cibuildwheel==3.3.0 From 6107b9e82d93b29ca86a4261eec832de48ee45f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Nov 2025 07:41:59 +1100 Subject: [PATCH 2140/2374] Update libimagequant to 4.4.1 --- depends/install_imagequant.sh | 2 +- docs/installation/building-from-source.rst | 2 +- winbuild/build_prepare.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 357214f1fd6..de63abdecc7 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -2,7 +2,7 @@ # install libimagequant archive_name=libimagequant -archive_version=4.4.0 +archive_version=4.4.1 archive=$archive_name-$archive_version diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 6080d29afa3..4349f980498 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -64,7 +64,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.4.0** + * Pillow has been tested with libimagequant **2.6-4.4.1** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 186a80cca5e..6cbc5c1e113 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -120,7 +120,7 @@ def cmd_msbuild( "JPEGTURBO": "3.1.2", "LCMS2": "2.17", "LIBAVIF": "1.3.0", - "LIBIMAGEQUANT": "4.4.0", + "LIBIMAGEQUANT": "4.4.1", "LIBPNG": "1.6.50", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.4", From 88247a9ef38d8eba9610393ddfe34616e108257d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:31:27 +1100 Subject: [PATCH 2141/2374] Updated version --- docs/reference/ImageGrab.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index e7dd41de1b7..4138667850b 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -48,7 +48,7 @@ or the clipboard to a PIL image memory. CGWindowID. .. versionadded:: 11.2.1 Windows support - .. versionadded:: 12.0.0 macOS support + .. versionadded:: 12.1.0 macOS support :return: An image .. py:function:: grabclipboard() From cce73b1e892a14d8d48146ac6ad3500be4d40d44 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Nov 2025 21:52:21 +1100 Subject: [PATCH 2142/2374] Close image on ImageFont exception --- src/PIL/ImageFont.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 92eb763a51e..2e8ace98dda 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -127,11 +127,15 @@ def _load_pilfont(self, filename: str) -> None: def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: # check image if image.mode not in ("1", "L"): + image.close() + msg = "invalid font image mode" raise TypeError(msg) # read PILfont header if file.read(8) != b"PILfont\n": + image.close() + msg = "Not a PILfont file" raise SyntaxError(msg) file.readline() From 7055937eb15a66209ebf8f5e275e58cbd56ca629 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Nov 2025 17:47:09 +1100 Subject: [PATCH 2143/2374] Updated Ubuntu version --- docs/installation/building-from-source.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 6080d29afa3..40e2a1938ae 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -116,7 +116,7 @@ Many of Pillow's features require external libraries: .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. - Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: + Prerequisites for **Ubuntu 16.04 LTS - 24.04 LTS** are installed with:: sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ From 6a9960e8c1960879fe3fb1c3afc143d0899213f5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 25 Nov 2025 23:40:34 +1100 Subject: [PATCH 2144/2374] Only update Python palette if rawmode was different to the mode --- src/PIL/Image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9d50812eb3e..dc51860a040 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -897,7 +897,9 @@ def load(self) -> core.PixelAccess | None: else: self.im.putpalettealphas(self.info["transparency"]) self.palette.mode = "RGBA" - else: + elif self.palette.mode != mode: + # If the palette rawmode is different to the mode, + # then update the Python palette data self.palette.palette = self.im.getpalette( self.palette.mode, self.palette.mode ) From d06c8b3591ed7eee91f2bca4711369e59cfc8645 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 27 Nov 2025 13:12:42 +1100 Subject: [PATCH 2145/2374] Test drawing a new color onto a dirty palette --- Tests/test_imagedraw.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 790acee2a46..1e0dedef3cf 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -68,10 +68,20 @@ def test_sanity() -> None: draw.rectangle(list(range(4))) -def test_valueerror() -> None: +def test_new_color() -> None: with Image.open("Tests/images/chi.gif") as im: draw = ImageDraw.Draw(im) + assert len(im.palette.colors) == 249 + + # Test drawing a new color onto the palette draw.line((0, 0), fill=(0, 0, 0)) + assert len(im.palette.colors) == 250 + assert im.palette.dirty + + # Test drawing another new color, now that the palette is dirty + draw.point((0, 0), fill=(1, 0, 0)) + assert len(im.palette.colors) == 251 + assert im.convert("RGB").getpixel((0, 0)) == (1, 0, 0) def test_mode_mismatch() -> None: From 8814d42fd96ea8c86e5aa3bc4970066ade5de489 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Nov 2025 14:24:43 +1100 Subject: [PATCH 2146/2374] Update zlib-ng to 2.3.1, except on manylinux2014 aarch64 --- .github/workflows/wheels-dependencies.sh | 21 ++++++++++----------- winbuild/build_prepare.py | 6 +++--- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 226fcdb6af7..194c51a944f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -106,7 +106,11 @@ XZ_VERSION=5.8.1 ZSTD_VERSION=1.5.7 TIFF_VERSION=4.7.1 LCMS2_VERSION=2.17 -ZLIB_NG_VERSION=2.2.5 +if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "aarch64" ]]; then + ZLIB_NG_VERSION=2.2.5 +else + ZLIB_NG_VERSION=2.3.1 +fi LIBWEBP_VERSION=1.6.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 @@ -149,18 +153,13 @@ function build_zlib_ng { ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS unset HOST_CONFIGURE_FLAGS - build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat + if [[ "$ZLIB_NG_VERSION" == 2.2.5 ]]; then + build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat + else + build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --installnamedir=$BUILD_PREFIX/lib --zlib-compat + fi HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS - - if [[ -n "$IS_MACOS" ]] && [[ -z "$IOS_SDK" ]]; then - # Ensure that on macOS, the library name is an absolute path, not an - # @rpath, so that delocate picks up the right library (and doesn't need - # DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an - # option to control the install_name. This isn't needed on iOS, as iOS - # only builds the static library. - install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib - fi touch zlib-stamp } diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 2401dd4ecd6..65d0af48170 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -126,7 +126,7 @@ def cmd_msbuild( "OPENJPEG": "2.5.4", "TIFF": "4.7.1", "XZ": "5.8.1", - "ZLIBNG": "2.2.5", + "ZLIBNG": "2.3.1", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) @@ -167,12 +167,12 @@ def cmd_msbuild( "license": "LICENSE.md", "patch": { r"CMakeLists.txt": { - "set_target_properties(zlib PROPERTIES OUTPUT_NAME zlibstatic${{SUFFIX}})": "set_target_properties(zlib PROPERTIES OUTPUT_NAME zlib)", # noqa: E501 + "set_target_properties(zlib-ng PROPERTIES OUTPUT_NAME zlibstatic${{SUFFIX}})": "set_target_properties(zlib-ng PROPERTIES OUTPUT_NAME zlib)", # noqa: E501 }, }, "build": [ *cmds_cmake( - "zlib", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DZLIB_COMPAT:BOOL=ON" + "zlib-ng", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DZLIB_COMPAT:BOOL=ON" ), ], "headers": [r"z*.h"], From 37da2ba381ecb47fb7a06af88fe6ed9dc64349f7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Nov 2025 17:22:44 +1100 Subject: [PATCH 2147/2374] Corrected allocating new color to RGBA palette --- Tests/test_imagepalette.py | 6 ++++++ src/PIL/ImagePalette.py | 9 +++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 782022f5171..6ad21502f9f 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -49,6 +49,12 @@ def test_getcolor() -> None: palette.getcolor("unknown") # type: ignore[arg-type] +def test_getcolor_rgba() -> None: + palette = ImagePalette.ImagePalette("RGBA", (1, 2, 3, 4)) + palette.getcolor((5, 6, 7, 8)) + assert palette.palette == b"\x01\x02\x03\x04\x05\x06\x07\x08" + + def test_getcolor_rgba_color_rgb_palette() -> None: palette = ImagePalette.ImagePalette("RGB") diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 103697117b9..eae7aea8fc3 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -118,7 +118,7 @@ def _new_color_index( ) -> int: if not isinstance(self.palette, bytearray): self._palette = bytearray(self.palette) - index = len(self.palette) // 3 + index = len(self.palette) // len(self.mode) special_colors: tuple[int | tuple[int, ...] | None, ...] = () if image: special_colors = ( @@ -168,11 +168,12 @@ def getcolor( index = self._new_color_index(image, e) assert isinstance(self._palette, bytearray) self.colors[color] = index - if index * 3 < len(self.palette): + mode_len = len(self.mode) + if index * mode_len < len(self.palette): self._palette = ( - self._palette[: index * 3] + self._palette[: index * mode_len] + bytes(color) - + self._palette[index * 3 + 3 :] + + self._palette[index * mode_len + mode_len :] ) else: self._palette += bytes(color) From 65c32ecca4019984862aa9caa916fb2196e1cb2d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 30 Nov 2025 21:55:59 +0200 Subject: [PATCH 2148/2374] retina -> Retina --- src/PIL/ImageGrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index b82a2ff3a47..4228078b11b 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -54,7 +54,7 @@ def grab( os.unlink(filepath) if bbox: if window: - # Determine if the window was in retina mode or not + # Determine if the window was in Retina mode or not # by capturing it without the shadow, # and checking how different the width is fh, filepath = tempfile.mkstemp(".png") From 370da461cf5e1226376d7ea591265a84dc5a5b06 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:02:09 +1100 Subject: [PATCH 2149/2374] Updated libpng to 1.6.51 (#9305) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index e033b69a97e..07ea75a75c5 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -96,7 +96,7 @@ else FREETYPE_VERSION=2.14.1 fi HARFBUZZ_VERSION=12.2.0 -LIBPNG_VERSION=1.6.50 +LIBPNG_VERSION=1.6.51 JPEGTURBO_VERSION=3.1.2 OPENJPEG_VERSION=2.5.4 XZ_VERSION=5.8.1 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 99421ebe25d..cd2ef13c11f 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -121,7 +121,7 @@ def cmd_msbuild( "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.4.1", - "LIBPNG": "1.6.50", + "LIBPNG": "1.6.51", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.4", "TIFF": "4.7.1", From ce3e08575164756e5dcbcc07d49105cd9e8d5c55 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:26:21 +0000 Subject: [PATCH 2150/2374] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.3 → v0.14.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.3...v0.14.7) - [github.com/psf/black-pre-commit-mirror: 25.9.0 → 25.11.0](https://github.com/psf/black-pre-commit-mirror/compare/25.9.0...25.11.0) - [github.com/PyCQA/bandit: 1.8.6 → 1.9.2](https://github.com/PyCQA/bandit/compare/1.8.6...1.9.2) - [github.com/pre-commit/mirrors-clang-format: v21.1.2 → v21.1.6](https://github.com/pre-commit/mirrors-clang-format/compare/v21.1.2...v21.1.6) - [github.com/python-jsonschema/check-jsonschema: 0.34.1 → 0.35.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.34.1...0.35.0) - [github.com/zizmorcore/zizmor-pre-commit: v1.16.2 → v1.18.0](https://github.com/zizmorcore/zizmor-pre-commit/compare/v1.16.2...v1.18.0) - [github.com/sphinx-contrib/sphinx-lint: v1.0.1 → v1.0.2](https://github.com/sphinx-contrib/sphinx-lint/compare/v1.0.1...v1.0.2) - [github.com/tox-dev/pyproject-fmt: v2.11.0 → v2.11.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.11.0...v2.11.1) --- .pre-commit-config.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 564206ce170..8477729e636 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,17 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.3 + rev: v0.14.7 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 25.9.0 + rev: 25.11.0 hooks: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.8.6 + rev: 1.9.2 hooks: - id: bandit args: [--severity-level=high] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v21.1.2 + rev: v21.1.6 hooks: - id: clang-format types: [c] @@ -51,24 +51,24 @@ repos: exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.34.1 + rev: 0.35.0 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.16.2 + rev: v1.18.0 hooks: - id: zizmor - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v1.0.1 + rev: v1.0.2 hooks: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.11.0 + rev: v2.11.1 hooks: - id: pyproject-fmt From 9342e209b2176bde761b321a74846857257ea78c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Dec 2025 10:21:55 +1100 Subject: [PATCH 2151/2374] Disable https://docs.zizmor.sh/audits/#obfuscation --- .github/zizmor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/zizmor.yml b/.github/zizmor.yml index b567097811a..e60c79441ca 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,6 +1,8 @@ # Configuration for the zizmor static analysis tool, run via pre-commit in CI # https://docs.zizmor.sh/configuration/ rules: + obfuscation: + disable: true unpinned-uses: config: policies: From 47c6aae0cae6b5c4e956d4e56da7f189575ada9a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Dec 2025 10:40:43 +1100 Subject: [PATCH 2152/2374] Fixed testing good P mode BMP images --- Tests/test_bmp_reference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 82cab39c613..3cd0fbb2d2e 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -72,7 +72,7 @@ def test_good() -> None: "pal8-0.bmp": "pal8.png", "pal8rle.bmp": "pal8.png", "pal8topdown.bmp": "pal8.png", - "pal8nonsquare.bmp": "pal8nonsquare-v.png", + "pal8nonsquare.bmp": "pal8nonsquare-e.png", "pal8os2.bmp": "pal8.png", "pal8os2sp.bmp": "pal8.png", "pal8os2v2.bmp": "pal8.png", @@ -103,7 +103,7 @@ def get_compare(f: str) -> str: # with paletized image, since the palette might # be differently ordered for an equivalent image. im = im.convert("RGBA") - compare = im.convert("RGBA") + compare = compare.convert("RGBA") assert_image_similar(im, compare, 5) except Exception as msg: From b3d9ba8e886147201acbd94c5c5f2cc4b3513311 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Dec 2025 10:42:14 +1100 Subject: [PATCH 2153/2374] Removed unused files --- Tests/images/bmp/html/bkgd.png | Bin 126 -> 0 bytes Tests/images/bmp/html/bmpsuite.html | 578 ---------------------- Tests/images/bmp/html/fakealpha.png | Bin 1181 -> 0 bytes Tests/images/bmp/html/pal1p1.png | Bin 124 -> 0 bytes Tests/images/bmp/html/pal2.png | Bin 961 -> 0 bytes Tests/images/bmp/html/pal4rletrns-0.png | Bin 1441 -> 0 bytes Tests/images/bmp/html/pal4rletrns-b.png | Bin 1362 -> 0 bytes Tests/images/bmp/html/pal4rletrns.png | Bin 1465 -> 0 bytes Tests/images/bmp/html/pal8nonsquare-v.png | Bin 11576 -> 0 bytes Tests/images/bmp/html/pal8rletrns-0.png | Bin 3776 -> 0 bytes Tests/images/bmp/html/pal8rletrns-b.png | Bin 3715 -> 0 bytes Tests/images/bmp/html/pal8rletrns.png | Bin 3793 -> 0 bytes Tests/images/bmp/html/rgb16-231.png | Bin 2643 -> 0 bytes Tests/images/bmp/html/rgb24.jpg | Bin 2319 -> 0 bytes Tests/images/bmp/html/rgba16-4444.png | Bin 1093 -> 0 bytes Tests/images/bmp/html/rgba32.png | Bin 1229 -> 0 bytes 16 files changed, 578 deletions(-) delete mode 100644 Tests/images/bmp/html/bkgd.png delete mode 100644 Tests/images/bmp/html/bmpsuite.html delete mode 100644 Tests/images/bmp/html/fakealpha.png delete mode 100644 Tests/images/bmp/html/pal1p1.png delete mode 100644 Tests/images/bmp/html/pal2.png delete mode 100644 Tests/images/bmp/html/pal4rletrns-0.png delete mode 100644 Tests/images/bmp/html/pal4rletrns-b.png delete mode 100644 Tests/images/bmp/html/pal4rletrns.png delete mode 100644 Tests/images/bmp/html/pal8nonsquare-v.png delete mode 100644 Tests/images/bmp/html/pal8rletrns-0.png delete mode 100644 Tests/images/bmp/html/pal8rletrns-b.png delete mode 100644 Tests/images/bmp/html/pal8rletrns.png delete mode 100644 Tests/images/bmp/html/rgb16-231.png delete mode 100644 Tests/images/bmp/html/rgb24.jpg delete mode 100644 Tests/images/bmp/html/rgba16-4444.png delete mode 100644 Tests/images/bmp/html/rgba32.png diff --git a/Tests/images/bmp/html/bkgd.png b/Tests/images/bmp/html/bkgd.png deleted file mode 100644 index d66ca9d65263950295e774210a32056350c15901..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!D3?x-;bCrM;V{wqX6T`Z5GB1G~wg8_H*9GU# zPoMr@$gd<0D8gCb5n0T@z%2~Ij105pNB{)|JzX3_IIbuEIDeo)G*SF7KSSMq{*SLd S3ttCHGI+ZBxvX - - - - -BMP Suite Image List - - - - - - - -