From 30015f6236ee3165e06b78ff7dced956e21e24d5 Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 16:16:25 +0100 Subject: [PATCH 01/68] 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 6db7cc4ec..c8d834f91 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -582,22 +582,13 @@ class FreeTypeFont: _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 + Image._decompression_bomb_check(size) + return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - return im - - offset = self.font.render( + return self.font.render( text, fill, mode, @@ -610,8 +601,6 @@ class FreeTypeFont: 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 68c66ac2c..6e24fcf95 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 02/68] 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 0dde82bd7..fec482f43 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 643fce830..eafafd583 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 @@ FLAGS = { "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 @@ class ImageCmsTransform(Image.ImagePointHandler): 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 03/68] 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 a42dc555f..0f9c75756 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 9b9b5e7b2..9b6be40b1 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 2bf299dd3..1fc245c9a 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 04/68] 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 9b6be40b1..4ef5ac774 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 eafafd583..9a7afe81f 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 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 05/68] 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 4ef5ac774..22ed516ce 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 06/68] 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 b6944e234..bdbf1373d 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 000000000..127880182 --- /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 5d6affa94..82c75e373 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 1bba9aad2..c56da5458 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2666,6 +2666,10 @@ class Image: 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 84c81f184..4f79500e6 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -20,12 +20,14 @@ from . import Image 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 @@ class Transform(Image.ImageTransformHandler): 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 85c552934af5ed378b034560ace23107bb4ec497 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 3 Jan 2024 17:45:41 +0200 Subject: [PATCH 07/68] 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 32ac6f65e..7244315ac 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 eb27b4bf7..3bb6856f6 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 115c2e9be..cdd51e2bb 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 86cd5b5fa..a0ef1c3f1 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 b7e112f43..05f78704b 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 8f8250809..000000000 --- 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 e11bd2faa..6982676f5 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 97f4f8dcd..62f3627de 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 872ac0ea6..da351ce2c 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 4f577fe9c..053d55c3c 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 08/68] 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 85d9eba1c..6ebf9a4c0 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 09/68] 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 6ebf9a4c0..4de599b81 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 10/68] 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 4de599b81..d7a93c70c 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 11/68] 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 86cd5b5fa..94c2d4d70 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 12/68] 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 d7a93c70c..8a9f81dfd 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 13/68] 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 8a9f81dfd..e2bc78b98 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 14/68] 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 e2bc78b98..50e47f198 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 15/68] 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 50e47f198..77c1489df 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 16/68] 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 77c1489df..be8f65244 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 17/68] 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 0f5dea9c5..4c5a7f9ee 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 85d9eba1c..36e98aa55 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 a8e4ebaa6..cd3b559e7 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 8e3757ca8..440e64d98 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -105,7 +105,7 @@ SF_PROJECTS = "https://sourceforge.net/projects" 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 @@ if __name__ == "__main__": ( "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 18/68] 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 440e64d98..1615abbdb 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -175,22 +175,15 @@ DEPS = { "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 @@ DEPS = { }, 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 @@ DEPS = { *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 19/68] 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 1615abbdb..84284ae10 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -174,6 +174,11 @@ DEPS = { "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 @@ DEPS = { 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 20/68] 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 84284ae10..df33ea493 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -176,6 +176,7 @@ DEPS = { "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 @@ DEPS = { "#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 21/68] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 85036f642..ac961a680 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 22/68] 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 c479c384a..8b48e83ad 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 b4bf6c8df..5a578dba5 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -403,7 +403,7 @@ class TestCoreResampleCoefficients: 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 f23245405..e37301df7 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -105,7 +105,7 @@ encode_loop: 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 98eaac28d..7cdba9022 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 23/68] 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 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 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 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 Date: Sat, 6 Jan 2024 14:33:11 +0100 Subject: [PATCH 24/68] 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 9a7afe81f..62b010f45 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 25/68] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ac961a680..ebf731c56 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 26/68] Removed PPM loop to read header tokens --- src/PIL/PpmImagePlugin.py | 49 ++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 25dbfa5b0..82314214a 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -105,40 +105,31 @@ class PpmImageFile(ImageFile.ImageFile): 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 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 - 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" + 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._size = xsize, ysize - self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)] + 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 27/68] 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 15939ef64..578a0a296 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -16,6 +16,8 @@ class TestImageTransform: 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 127880182..5b0a5ce49 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 4f79500e6..6aa82dadd 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 28/68] 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 e4ed93880..823f12492 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -378,7 +378,7 @@ class PngStream(ChunkStream): } 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 29/68] 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 fec482f43..9575b026d 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 62b010f45..827755bf6 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -20,8 +20,10 @@ import operator 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 @@ except ImportError as ex: _imagingcms = DeferredError.new(ex) -DESCRIPTION = """ +_DESCRIPTION = """ pyCMS a Python / PIL interface to the littleCMS ICC Color Management System @@ -95,7 +97,22 @@ pyCMS """ -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 @@ class Flags(IntFlag): _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 30/68] Added identity tests for Transform classes --- Tests/test_image_transform.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 578a0a296..f5d5ab704 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -10,20 +10,25 @@ from .helper import assert_image_equal, assert_image_similar, hopper class TestImageTransform: def test_sanity(self): - im = Image.new("L", (100, 100)) + im = hopper() - 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) + 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 31/68] 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 0f9c75756..7602f8b4e 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 22ed516ce..c4484cbe2 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 000000000..ddcb38aa1 --- /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 02da702a7..6400218f4 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 d8034853c..e86f8082b 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 32/68] 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 6982676f5..6ca870166 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 33/68] 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 60d951636..8069d4b08 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 34/68] 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 7602f8b4e..4c9abe195 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 ddcb38aa1..8dfe34d95 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 827755bf6..3e40105e4 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 35/68] 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 6be1278eb..d43e21e14 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -115,7 +115,7 @@ class PpmImageFile(ImageFile.ImageFile): 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 36/68] 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 efdb880de..d8e259b1c 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 37/68] 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 02a6a3af7..569ccb769 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 000000000..34afbe4b8 --- /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 d8034853c..e86f8082b 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 38/68] 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 8069d4b08..47d425f8b 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 39/68] 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 47d425f8b..ea5ab41ec 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 40/68] 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 34afbe4b8..391068769 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 41/68] 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 83e028718..9d37dcde0 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -311,7 +311,6 @@ class PpmDecoder(ImageFile.PyDecoder): 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 42/68] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ebf731c56..30bbaec3a 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 43/68] 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 464ab77ea..06965ead3 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 a3f238119..705ca0415 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 fde2faae3..16075ce95 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 44/68] 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 4c9abe195..205fcb9ab 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 8ce6f4b9c..548f95df9 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 45/68] 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 ea5ab41ec..340332258 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 46/68] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 30bbaec3a..c267ca472 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 659098c6acc46ff353ba2a136805b463fb5cc97b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 22:05:26 +1100 Subject: [PATCH 47/68] 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 da2537b21..54a4bcaec 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 c56da5458..0fbbe5861 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -873,7 +873,7 @@ class Image: 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 @@ class Image: """ 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 b77f4bce5..949fa45bb 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 @@ from . import Image, _imagingmath 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 @@ class _Operand: 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 @@ class _Operand: 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 @@ for k, v in list(globals().items()): 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 000000000..b0235555d --- /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 48/68] 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 0fbbe5861..ac13c6c0c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1305,7 +1305,7 @@ class Image: """ 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 49/68] 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 949fa45bb..bc3318c04 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 6f144d45b98d6c331da9fcd437542e224793b893 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Wed, 10 Jan 2024 16:03:42 -0500 Subject: [PATCH 50/68] 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 340332258..e2c4569ce 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 51/68] 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 9575b026d..810394e6f 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 52/68] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c267ca472..887319dab 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 53/68] 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 54a4bcaec..8acfc0420 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 282e7d2a5..534c6291a 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 @@ class LutBuilder: 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 @@ class LutBuilder: 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 @@ class LutBuilder: 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 @@ class LutBuilder: 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 @@ class LutBuilder: 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 @@ class MorphOp: 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 @@ class MorphOp: 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 @@ class MorphOp: 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 @@ class MorphOp: 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 @@ class MorphOp: 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 @@ class MorphOp: 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 000000000..b0235555d --- /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 54/68] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 887319dab..62ae2a68b 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 55/68] 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 8acfc0420..789df6f5e 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 ac13c6c0c..5ab27c359 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -242,7 +242,7 @@ MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", " _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 @@ class Image: 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 fad3e0980..d90545e92 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 @@ class Viewer: # 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 @@ class Viewer: 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 @@ class DisplayViewer(UnixViewer): 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 @@ class GmDisplayViewer(UnixViewer): 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 @@ class XVViewer(UnixViewer): 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 @@ if sys.platform not in ("win32", "darwin"): # unixoids 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 d89d017e4..fb6746ce7 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 56/68] 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 7dce2d60f..e69890bab 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -15,7 +15,7 @@ import math 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 ac13c6c0c..b2520d57c 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 c97b5c6f7a221ed534d93e943992e44c2a7ec5fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 14 Jan 2024 22:29:56 +1100 Subject: [PATCH 57/68] 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 d90545e92..c03122c11 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 58/68] 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 c1f89af46..e32f254de 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 0923979af..72c3c03c5 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -514,7 +514,7 @@ class Parser: # -------------------------------------------------------------------- -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 fbcfa309d..2b6cecc61 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -192,7 +192,7 @@ class ImagePalette: # 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 9a85c0d15..27972236c 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -22,8 +22,8 @@ import struct 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 a0515b302..1cd5c4a9d 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 @@ class PcdImageFile(ImageFile.ImageFile): 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 98ecefd05..3e0968a83 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -37,7 +37,7 @@ from ._binary import o16le as o16 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 @@ SAVE = { } -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 af866feb3..887b6568b 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -27,7 +27,7 @@ from ._binary import i16le as i16 # 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 11ce3dfef..4e098474a 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -21,7 +21,7 @@ from . import Image, ImageFile, ImagePalette 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 @@ class SunImageFile(ImageFile.ImageFile): # 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 47ba1c548..c84adaca2 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -33,7 +33,7 @@ for r in range(8): ) -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 566acbfe5..0291e2858 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 @@ xbm_head = re.compile( ) -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 @@ class XbmImageFile(ImageFile.ImageFile): 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 59/68] 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 e32f254de..3f35bf50e 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 60/68] 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 7469c592d..abb3fb762 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 61/68] 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 3f35bf50e..b7bb514ac 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 72c3c03c5..b79f2707b 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -713,7 +713,7 @@ class PyDecoder(PyCodec): 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 77dac65b6..bb7e466a7 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -35,7 +35,7 @@ from ._binary import o16le as o16 # 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 @@ Image.register_decoder("MSP", MspDecoder) # 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 62/68] [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 b79f2707b..40353da67 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -713,7 +713,7 @@ class PyDecoder(PyCodec): 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 63/68] 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 b7bb514ac..ec1cff896 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -710,7 +710,7 @@ class Image: 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 @@ class 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 @@ class Image: ] 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 @@ class Image: _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 f9a10f610..ccf661ff1 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -24,13 +24,14 @@ from __future__ import annotations 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 @@ class SgiImageFile(ImageFile.ImageFile): ] -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 64/68] 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 bc3318c04..a7652f237 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 54c96df9d6e370d16960ee8a2aee932b41f213f3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jan 2024 08:03:09 +1100 Subject: [PATCH 65/68] 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 65c7484f7..584932d2c 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 @@ class TgaImageFile(ImageFile.ImageFile): 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 @@ SAVE = { } -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 66/68] 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 ec1cff896..553f36703 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 40353da67..5ba5a6f82 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -616,6 +616,8 @@ class PyCodecState: 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 9d37dcde0..3e45ba95c 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 @@ MODES = { } -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 @@ class PpmImageFile(ImageFile.ImageFile): 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 @@ class PpmImageFile(ImageFile.ImageFile): 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 @@ class PpmImageFile(ImageFile.ImageFile): 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 @@ class PpmImageFile(ImageFile.ImageFile): 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 @@ class PpmPlainDecoder(ImageFile.PyDecoder): 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 @@ class PpmPlainDecoder(ImageFile.PyDecoder): 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 @@ class PpmPlainDecoder(ImageFile.PyDecoder): 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 @@ class PpmPlainDecoder(ImageFile.PyDecoder): 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 @@ class PpmPlainDecoder(ImageFile.PyDecoder): 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 @@ class PpmPlainDecoder(ImageFile.PyDecoder): 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 @@ class PpmDecoder(ImageFile.PyDecoder): # -------------------------------------------------------------------- -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 67/68] 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 d84876eb6..7bb4736af 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -27,6 +27,8 @@ """ 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 @@ class GdImageFile(ImageFile.ImageFile): ] -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 68/68] 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 f4e598ca3..b9e9243e5 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 @@ from ._binary import i8 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 @@ class BitStream: 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)