From 8e96748aeab2e081b1ca6a50ee5ac0c25cc90513 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 1 Apr 2024 09:35:56 -0500 Subject: [PATCH 001/195] correct property names in method documentation --- src/PIL/ImageCms.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 3a45572a1..162db628c 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -704,12 +704,12 @@ def applyTransform( """ (pyCMS) Applies a transform to a given image. - If ``im.mode != transform.inMode``, a :exc:`PyCMSError` is raised. + If ``im.mode != transform.input_mode``, a :exc:`PyCMSError` is raised. - If ``inPlace`` is ``True`` and ``transform.inMode != transform.outMode``, a + If ``inPlace`` is ``True`` and ``transform.input_mode != transform.output_mode``, a :exc:`PyCMSError` is raised. - If ``im.mode``, ``transform.inMode`` or ``transform.outMode`` is not + If ``im.mode``, ``transform.input_mode`` or ``transform.output_mode`` is not supported by pyCMSdll or the profiles you used for the transform, a :exc:`PyCMSError` is raised. @@ -723,10 +723,10 @@ def applyTransform( If you want to modify im in-place instead of receiving a new image as the return value, set ``inPlace`` to ``True``. This can only be done if - ``transform.inMode`` and ``transform.outMode`` are the same, because we can't - change the mode in-place (the buffer sizes for some modes are + ``transform.input_mode`` and ``transform.output_mode`` are the same, because we + can't change the mode in-place (the buffer sizes for some modes are different). The default behavior is to return a new :py:class:`~PIL.Image.Image` - object of the same dimensions in mode ``transform.outMode``. + object of the same dimensions in mode ``transform.output_mode``. :param im: An :py:class:`~PIL.Image.Image` object, and im.mode must be the same as the ``inMode`` supported by the transform. From 16ce3da0a4befe5c905da205962c18c2b1fe0ed9 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 1 Apr 2024 09:45:52 -0500 Subject: [PATCH 002/195] remove unused mode properties from CmsTransformObject/PIL.ImageCms.core.CmsTransform --- src/_imagingcms.c | 29 +++++------------------------ 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 84b8a7e71..b0ef2469c 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -181,8 +181,7 @@ cms_profile_dealloc(CmsProfileObject *self) { /* a transform represents the mapping between two profiles */ typedef struct { - PyObject_HEAD char mode_in[8]; - char mode_out[8]; + PyObject_HEAD cmsHTRANSFORM transform; } CmsTransformObject; @@ -191,7 +190,7 @@ static PyTypeObject CmsTransform_Type; #define CmsTransform_Check(op) (Py_TYPE(op) == &CmsTransform_Type) static PyObject * -cms_transform_new(cmsHTRANSFORM transform, char *mode_in, char *mode_out) { +cms_transform_new(cmsHTRANSFORM transform) { CmsTransformObject *self; self = PyObject_New(CmsTransformObject, &CmsTransform_Type); @@ -201,9 +200,6 @@ cms_transform_new(cmsHTRANSFORM transform, char *mode_in, char *mode_out) { self->transform = transform; - strncpy(self->mode_in, mode_in, 8); - strncpy(self->mode_out, mode_out, 8); - return (PyObject *)self; } @@ -476,7 +472,7 @@ buildTransform(PyObject *self, PyObject *args) { return NULL; } - return cms_transform_new(transform, sInMode, sOutMode); + return cms_transform_new(transform); } static PyObject * @@ -523,7 +519,7 @@ buildProofTransform(PyObject *self, PyObject *args) { return NULL; } - return cms_transform_new(transform, sInMode, sOutMode); + return cms_transform_new(transform); } static PyObject * @@ -1456,21 +1452,6 @@ static struct PyMethodDef cms_transform_methods[] = { {"apply", (PyCFunction)cms_transform_apply, 1}, {NULL, NULL} /* sentinel */ }; -static PyObject * -cms_transform_getattr_inputMode(CmsTransformObject *self, void *closure) { - return PyUnicode_FromString(self->mode_in); -} - -static PyObject * -cms_transform_getattr_outputMode(CmsTransformObject *self, void *closure) { - return PyUnicode_FromString(self->mode_out); -} - -static struct PyGetSetDef cms_transform_getsetters[] = { - {"inputMode", (getter)cms_transform_getattr_inputMode}, - {"outputMode", (getter)cms_transform_getattr_outputMode}, - {NULL}}; - static PyTypeObject CmsTransform_Type = { PyVarObject_HEAD_INIT(NULL, 0) "PIL.ImageCms.core.CmsTransform", /*tp_name*/ sizeof(CmsTransformObject), /*tp_basicsize*/ @@ -1501,7 +1482,7 @@ static PyTypeObject CmsTransform_Type = { 0, /*tp_iternext*/ cms_transform_methods, /*tp_methods*/ 0, /*tp_members*/ - cms_transform_getsetters, /*tp_getset*/ + 0, /*tp_getset*/ }; static int From 7a9b57ce0896e2f95126cba22bbca184d1a677f0 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 1 Apr 2024 10:16:23 -0500 Subject: [PATCH 003/195] remove mode properties from CmsTransform interface --- src/PIL/_imagingcms.pyi | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi index 036521b0e..f704047be 100644 --- a/src/PIL/_imagingcms.pyi +++ b/src/PIL/_imagingcms.pyi @@ -108,10 +108,6 @@ class CmsProfile: def is_intent_supported(self, intent: int, direction: int, /) -> int: ... class CmsTransform: - @property - def inputMode(self) -> str: ... - @property - def outputMode(self) -> str: ... def apply(self, id_in: int, id_out: int) -> int: ... def profile_open(profile: str, /) -> CmsProfile: ... From b37279a29cf29a66f86a99de6ce46d6226999c75 Mon Sep 17 00:00:00 2001 From: jbjd Date: Mon, 1 Apr 2024 20:09:02 -0500 Subject: [PATCH 004/195] fix parameter name in ImageMath docs --- docs/reference/ImageMath.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 703b2f5b9..2535db711 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -31,7 +31,7 @@ Example: Using the :py:mod:`~PIL.ImageMath` module b=im2 ) -.. py:function:: lambda_eval(expression, environment) +.. py:function:: lambda_eval(expression, options) Returns the result of an image function. @@ -44,7 +44,7 @@ Example: Using the :py:mod:`~PIL.ImageMath` module :return: An image, an integer value, a floating point value, or a pixel tuple, depending on the expression. -.. py:function:: unsafe_eval(expression, environment) +.. py:function:: unsafe_eval(expression, options) Evaluates an image expression. From 75454646f4bd3eb21e4585702286e8e8841cdc89 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Apr 2024 15:34:46 +1100 Subject: [PATCH 005/195] Moved code onto single line --- src/_imagingcms.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index b0ef2469c..fd487d9da 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -181,8 +181,7 @@ cms_profile_dealloc(CmsProfileObject *self) { /* a transform represents the mapping between two profiles */ typedef struct { - PyObject_HEAD - cmsHTRANSFORM transform; + PyObject_HEAD cmsHTRANSFORM transform; } CmsTransformObject; static PyTypeObject CmsTransform_Type; From 38e81126508974b506d6d61914f5efd303958760 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Apr 2024 17:58:49 +1100 Subject: [PATCH 006/195] Updated macOS tested Pillow versions --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 59fc312ab..bc60eeed4 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -81,7 +81,7 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+============================+==================+==============+ -| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm | +| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.3.0 |arm | +----------------------------------+----------------------------+------------------+--------------+ | macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | | +----------------------------+------------------+ | From 4915f19b1370c17036cb3f4c3cbe12bbf23bc43c Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Tue, 2 Apr 2024 17:45:06 +0200 Subject: [PATCH 007/195] fromarray: add type hints --- 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 baef0aa11..0dfccd9b0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3069,7 +3069,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): return frombytes(mode, size, data, decoder_name, args) -def fromarray(obj, mode=None): +def fromarray(obj: "numpy.typing.ArrayLike", mode: Optional[str] = None) -> Image: """ Creates an image memory from an object exporting the array interface (using the buffer protocol):: From 8e47a6f2c82b696f6b96bb8fff350a937ea195b9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:46:25 +0000 Subject: [PATCH 008/195] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- 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 0dfccd9b0..a2723756f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3069,7 +3069,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): return frombytes(mode, size, data, decoder_name, args) -def fromarray(obj: "numpy.typing.ArrayLike", mode: Optional[str] = None) -> Image: +def fromarray(obj: numpy.typing.ArrayLike, mode: Optional[str] = None) -> Image: """ Creates an image memory from an object exporting the array interface (using the buffer protocol):: From 37ed8c337de3617bb0b17b721c0cef08e502dc3b Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Tue, 2 Apr 2024 17:53:11 +0200 Subject: [PATCH 009/195] Try type comment --- src/PIL/Image.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a2723756f..c932f1932 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,7 +41,7 @@ import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum from types import ModuleType -from typing import IO, TYPE_CHECKING, Any +from typing import IO, Optional, TYPE_CHECKING, Any # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -3069,7 +3069,10 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): return frombytes(mode, size, data, decoder_name, args) -def fromarray(obj: numpy.typing.ArrayLike, mode: Optional[str] = None) -> Image: +def fromarray( + obj, # type: numpy.typing.ArrayLike + mode: Optional[str] = None +) -> Image: """ Creates an image memory from an object exporting the array interface (using the buffer protocol):: From 8a63980e393e0ffc9fc9ae4337d97b5bbfb95524 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:53:39 +0000 Subject: [PATCH 010/195] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- 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 c932f1932..a4043c620 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,7 +41,7 @@ import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum from types import ModuleType -from typing import IO, Optional, TYPE_CHECKING, Any +from typing import IO, TYPE_CHECKING, Any, Optional # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -3071,7 +3071,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): def fromarray( obj, # type: numpy.typing.ArrayLike - mode: Optional[str] = None + mode: Optional[str] = None, ) -> Image: """ Creates an image memory from an object exporting the array interface From 5d19151cd31951754dc284db7434c4e6e7fbc240 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Tue, 2 Apr 2024 17:56:52 +0200 Subject: [PATCH 011/195] Python 3.10+ --- 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 a4043c620..245fadd6b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,7 +41,7 @@ import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, Optional +from typing import IO, TYPE_CHECKING, Any # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -3071,7 +3071,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): def fromarray( obj, # type: numpy.typing.ArrayLike - mode: Optional[str] = None, + mode: str | None = None, ) -> Image: """ Creates an image memory from an object exporting the array interface From 8c57cd56a5107fe8957f8a753dbe15f3da33e5d9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Apr 2024 12:00:02 +1100 Subject: [PATCH 012/195] QoiImagePlugin uses PyDecoder --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index f3f0499f0..c171705c2 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1488,7 +1488,7 @@ QOI .. versionadded:: 9.5.0 -Pillow identifies and reads images in Quite OK Image format. +Pillow reads images in Quite OK Image format, using a Python decoder. SUN ^^^ From a6793bba5e4a3c0e102359545507828966960cc1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Apr 2024 12:19:45 +1100 Subject: [PATCH 013/195] Updated pattern for skipping builds based on file changes --- .appveyor.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 6f35a0190..57a8fa5a0 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,9 +1,9 @@ skip_commits: files: - - ".github/**" + - ".github/**/*" - ".gitmodules" - - "docs/**" - - "wheels/**" + - "docs/**/*" + - "wheels/**/*" version: '{build}' clone_folder: c:\pillow From e85a84baa7ebee1b458cabbeb23b8d497c735175 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Apr 2024 20:00:40 +1100 Subject: [PATCH 014/195] Added SupportsArrayInterface --- Tests/test_image_array.py | 10 ++++++++++ docs/reference/Image.rst | 2 ++ src/PIL/Image.py | 22 +++++++++++++++------- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index cf85ee4fa..342bd8654 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -91,6 +91,16 @@ def test_fromarray() -> None: Image.fromarray(wrapped) +def test_fromarray_strides_without_tobytes() -> None: + class Wrapper: + def __init__(self, arr_params: dict[str, Any]) -> None: + self.__array_interface__ = arr_params + + with pytest.raises(ValueError): + wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) + Image.fromarray(wrapped, "L") + + def test_fromarray_palette() -> None: # Arrange i = im.convert("L") diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 4281b182c..0d9b4d93d 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -78,6 +78,8 @@ Constructing images ^^^^^^^^^^^^^^^^^^^ .. autofunction:: new +.. autoclass:: SupportsArrayInterface + :show-inheritance: .. autofunction:: fromarray .. autofunction:: frombytes .. autofunction:: frombuffer diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 245fadd6b..7e68ee3cb 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,7 +41,7 @@ import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum from types import ModuleType -from typing import IO, TYPE_CHECKING, Any +from typing import IO, TYPE_CHECKING, Any, Protocol # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -3013,7 +3013,7 @@ def frombytes(mode, size, data, decoder_name="raw", *args) -> Image: return im -def frombuffer(mode, size, data, decoder_name="raw", *args): +def frombuffer(mode, size, data, decoder_name="raw", *args) -> Image: """ Creates an image memory referencing pixel data in a byte buffer. @@ -3069,10 +3069,15 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): return frombytes(mode, size, data, decoder_name, args) -def fromarray( - obj, # type: numpy.typing.ArrayLike - mode: str | None = None, -) -> Image: +class SupportsArrayInterface(Protocol): + """ + An object that has an ``__array_interface__`` dictionary. + """ + + __array_interface__: dict[str, Any] + + +def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: """ Creates an image memory from an object exporting the array interface (using the buffer protocol):: @@ -3151,8 +3156,11 @@ def fromarray( if strides is not None: if hasattr(obj, "tobytes"): obj = obj.tobytes() - else: + elif hasattr(obj, "tostring"): obj = obj.tostring() + else: + msg = "'strides' requires either tobytes() or tostring()" + raise ValueError(msg) return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) From 0cc5de4e0949b8e5ef810465cde7332bde825c72 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Apr 2024 12:17:02 +1100 Subject: [PATCH 015/195] Link to https://pypi.org/project/qoi --- docs/handbook/image-file-formats.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index c171705c2..bff056b75 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1488,7 +1488,9 @@ QOI .. versionadded:: 9.5.0 -Pillow reads images in Quite OK Image format, using a Python decoder. +Pillow reads images in Quite OK Image format, using a Python decoder. If you wish to +write code specifically for this format, https://pypi.org/project/qoi is an alternative +library that uses C to decode the image, and interfaces with NumPy. SUN ^^^ From 8c14a394c1d3ca9c06f356ebfbf2acaa634da25e Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Apr 2024 19:54:07 +0200 Subject: [PATCH 016/195] add type hints for Image.open and Image.init --- src/PIL/Image.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index baef0aa11..09ddc770f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,7 +41,7 @@ import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum from types import ModuleType -from typing import IO, TYPE_CHECKING, Any +from typing import IO, TYPE_CHECKING, Any, Literal, cast # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -55,7 +55,7 @@ from . import ( _plugins, ) from ._binary import i32le, o32be, o32le -from ._typing import TypeGuard +from ._typing import StrOrBytesPath, TypeGuard from ._util import DeferredError, is_path ElementTree: ModuleType | None @@ -357,7 +357,7 @@ def preinit() -> None: _initialized = 1 -def init(): +def init() -> bool: """ Explicitly initializes the Python Imaging Library. This function loads all available file format drivers. @@ -368,7 +368,7 @@ def init(): global _initialized if _initialized >= 2: - return 0 + return False parent_name = __name__.rpartition(".")[0] for plugin in _plugins: @@ -380,7 +380,8 @@ def init(): if OPEN or SAVE: _initialized = 2 - return 1 + return True + return False # -------------------------------------------------------------------- @@ -3222,7 +3223,11 @@ def _decompression_bomb_check(size: tuple[int, int]) -> None: ) -def open(fp, mode="r", formats=None) -> Image: +def open( + fp: StrOrBytesPath | IO[bytes], + mode: Literal["r"] = "r", + formats: list[str] | tuple[str, ...] | None = None, +) -> ImageFile.ImageFile: """ Opens and identifies the given image file. @@ -3253,10 +3258,10 @@ def open(fp, mode="r", formats=None) -> Image: """ if mode != "r": - msg = f"bad mode {repr(mode)}" + msg = f"bad mode {repr(mode)}" # type: ignore[unreachable] raise ValueError(msg) elif isinstance(fp, io.StringIO): - msg = ( + msg = ( # type: ignore[unreachable] "StringIO cannot be used to open an image. " "Binary data must be used instead." ) @@ -3265,7 +3270,7 @@ def open(fp, mode="r", formats=None) -> Image: if formats is None: formats = ID elif not isinstance(formats, (list, tuple)): - msg = "formats must be a list or tuple" + msg = "formats must be a list or tuple" # type: ignore[unreachable] raise TypeError(msg) exclusive_fp = False @@ -3276,6 +3281,8 @@ def open(fp, mode="r", formats=None) -> Image: if filename: fp = builtins.open(filename, "rb") exclusive_fp = True + else: + fp = cast(IO[bytes], fp) try: fp.seek(0) @@ -3287,9 +3294,14 @@ def open(fp, mode="r", formats=None) -> Image: preinit() - accept_warnings = [] + accept_warnings: list[str | bytes] = [] - def _open_core(fp, filename, prefix, formats): + def _open_core( + fp: IO[bytes], + filename: str | bytes, + prefix: bytes, + formats: list[str] | tuple[str, ...], + ) -> ImageFile.ImageFile | None: for i in formats: i = i.upper() if i not in OPEN: @@ -3298,7 +3310,7 @@ def open(fp, mode="r", formats=None) -> Image: factory, accept = OPEN[i] result = not accept or accept(prefix) if type(result) in [str, bytes]: - accept_warnings.append(result) + accept_warnings.append(result) # type: ignore[arg-type] elif result: fp.seek(0) im = factory(fp, filename) @@ -3318,7 +3330,7 @@ def open(fp, mode="r", formats=None) -> Image: im = _open_core(fp, filename, prefix, formats) if im is None and formats is ID: - checked_formats = formats.copy() + checked_formats = ID.copy() if init(): im = _open_core( fp, @@ -3334,7 +3346,7 @@ def open(fp, mode="r", formats=None) -> Image: if exclusive_fp: fp.close() for message in accept_warnings: - warnings.warn(message) + warnings.warn(message) # type: ignore[arg-type] msg = "cannot identify image file %r" % (filename if filename else fp) raise UnidentifiedImageError(msg) From 2a2588d5df13c329d442b613960bc0ba7e606543 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 5 Apr 2024 05:09:11 +1100 Subject: [PATCH 017/195] Use extlink Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index bff056b75..fbef08bce 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1489,7 +1489,7 @@ QOI .. versionadded:: 9.5.0 Pillow reads images in Quite OK Image format, using a Python decoder. If you wish to -write code specifically for this format, https://pypi.org/project/qoi is an alternative +write code specifically for this format, :pypi:`qoi` is an alternative library that uses C to decode the image, and interfaces with NumPy. SUN From 0702f704fa147ea583eb61ca01280c9e59665264 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 5 Apr 2024 05:16:41 +1100 Subject: [PATCH 018/195] Remove commas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- docs/handbook/image-file-formats.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index fbef08bce..06716255b 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1488,9 +1488,9 @@ QOI .. versionadded:: 9.5.0 -Pillow reads images in Quite OK Image format, using a Python decoder. If you wish to +Pillow reads images in Quite OK Image format using a Python decoder. If you wish to write code specifically for this format, :pypi:`qoi` is an alternative -library that uses C to decode the image, and interfaces with NumPy. +library that uses C to decode the image and interfaces with NumPy. SUN ^^^ From 059b8e91717c04e2827a937c1c5bfd7d5d947548 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 Apr 2024 06:52:36 +1100 Subject: [PATCH 019/195] Updated line formatting --- docs/handbook/image-file-formats.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 06716255b..1ec972149 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1489,8 +1489,8 @@ QOI .. versionadded:: 9.5.0 Pillow reads images in Quite OK Image format using a Python decoder. If you wish to -write code specifically for this format, :pypi:`qoi` is an alternative -library that uses C to decode the image and interfaces with NumPy. +write code specifically for this format, :pypi:`qoi` is an alternative library that +uses C to decode the image and interfaces with NumPy. SUN ^^^ From 819e1b9dd2bfeb6455524053b6d76b65d77bd66c Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Apr 2024 23:38:50 +0200 Subject: [PATCH 020/195] add type hints for Image.save --- src/PIL/Image.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 09ddc770f..8bae2974c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2374,7 +2374,9 @@ class Image: (w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor ) - def save(self, fp, format=None, **params) -> None: + def save( + self, fp: StrOrBytesPath | IO[bytes], format: str | None = None, **params: Any + ) -> None: """ Saves this image under the given filename. If no format is specified, the format to use is determined from the filename @@ -2455,6 +2457,8 @@ class Image: fp = builtins.open(filename, "r+b") else: fp = builtins.open(filename, "w+b") + else: + fp = cast(IO[bytes], fp) try: save_handler(self, fp, filename) From c61a4810289e6283e2d20cc5d4941f4c57b19a64 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Apr 2024 13:40:39 +1100 Subject: [PATCH 021/195] Support reading CMYK JPEG2000 images --- Tests/test_file_jpeg2k.py | 10 ++++++++++ src/PIL/Jpeg2KImagePlugin.py | 4 ++++ src/libImaging/Jpeg2KDecode.c | 1 + 3 files changed, 15 insertions(+) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 81f75cc72..a7cae563a 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -289,6 +289,16 @@ def test_rgba(ext: str) -> None: assert im.mode == "RGBA" +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +@skip_unless_feature_version("jpg_2000", "2.5.1") +def test_cmyk() -> None: + with Image.open(f"{EXTRA_DIR}/issue205.jp2") as im: + assert im.mode == "CMYK" + assert im.getpixel((0, 0)) == (185, 134, 0, 0) + + @pytest.mark.parametrize("ext", (".j2k", ".jp2")) def test_16bit_monochrome_has_correct_mode(ext: str) -> None: with Image.open("Tests/images/16bit.cropped" + ext) as im: diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index be000c351..51b29bca2 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -176,6 +176,10 @@ def _parse_jp2_header(fp): mode = "RGB" elif nc == 4: mode = "RGBA" + elif tbox == b"colr" and nc == 4: + meth, _, _, enumcs = header.read_fields(">BBBI") + if meth == 1 and enumcs == 12: + mode = "CMYK" elif tbox == b"pclr" and mode in ("L", "LA"): ne, npc = header.read_fields(">HB") bitdepths = header.read_fields(">" + ("B" * npc)) diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index 13f363422..78a09bb83 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -632,6 +632,7 @@ static const struct j2k_decode_unpacker j2k_unpackers[] = { {"RGBA", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, {"RGBA", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgba_rgba}, {"RGBA", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycca_rgba}, + {"CMYK", OPJ_CLRSPC_CMYK, 4, 1, j2ku_srgba_rgba}, }; /* -------------------------------------------------------------------- */ From 1c2a323a90fabf8cedb4229c8864aac8d6a24ed5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Apr 2024 15:18:43 +1100 Subject: [PATCH 022/195] Corrected variable name --- 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 162db628c..4af1b79e2 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -728,8 +728,8 @@ def applyTransform( different). The default behavior is to return a new :py:class:`~PIL.Image.Image` object of the same dimensions in mode ``transform.output_mode``. - :param im: An :py:class:`~PIL.Image.Image` object, and im.mode must be the same - as the ``inMode`` supported by the transform. + :param im: An :py:class:`~PIL.Image.Image` object, and ``im.mode`` must be the same + as the ``input_mode`` supported by the transform. :param transform: A valid CmsTransform class object :param inPlace: Bool. If ``True``, ``im`` is modified in place and ``None`` is returned, if ``False``, a new :py:class:`~PIL.Image.Image` object with the From 7eee479ce5f52e0aec8e08f9380ec528bda84c72 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Apr 2024 14:28:33 +1100 Subject: [PATCH 023/195] Corrected indentation --- src/_imagingcms.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index fd487d9da..f18d55a57 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -390,7 +390,7 @@ _buildTransform( Py_END_ALLOW_THREADS - if (!hTransform) { + if (!hTransform) { PyErr_SetString(PyExc_ValueError, "cannot build transform"); } @@ -424,7 +424,7 @@ _buildProofTransform( Py_END_ALLOW_THREADS - if (!hTransform) { + if (!hTransform) { PyErr_SetString(PyExc_ValueError, "cannot build proof transform"); } From 6a255de24fadfb2daf52f1fa86849611078127ab Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 6 Apr 2024 11:14:38 -0500 Subject: [PATCH 024/195] Rename test_roundtrip() to test_hopper() This test isn't actually roundtripping anything. --- Tests/test_image_getdata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index ac27400be..9181b2409 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -14,7 +14,7 @@ def test_sanity() -> None: assert data[0] == (20, 20, 70) -def test_roundtrip() -> None: +def test_hopper() -> None: def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]: im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) data = im.getdata() From 48b270590c61abf77547dc17b1ee5303ef6f4283 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Apr 2024 13:58:53 +1100 Subject: [PATCH 025/195] accept returns bool or str --- src/PIL/BlpImagePlugin.py | 2 +- src/PIL/BmpImagePlugin.py | 2 +- src/PIL/BufrStubImagePlugin.py | 2 +- src/PIL/CurImagePlugin.py | 2 +- src/PIL/DcxImagePlugin.py | 2 +- src/PIL/DdsImagePlugin.py | 2 +- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/FliImagePlugin.py | 2 +- src/PIL/FpxImagePlugin.py | 2 +- src/PIL/FtexImagePlugin.py | 2 +- src/PIL/GbrImagePlugin.py | 2 +- src/PIL/GifImagePlugin.py | 2 +- src/PIL/GribStubImagePlugin.py | 2 +- src/PIL/Hdf5StubImagePlugin.py | 2 +- src/PIL/IcnsImagePlugin.py | 2 +- src/PIL/IcoImagePlugin.py | 2 +- src/PIL/Image.py | 12 ++++++------ src/PIL/Jpeg2KImagePlugin.py | 2 +- src/PIL/JpegImagePlugin.py | 2 +- src/PIL/MicImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 2 +- src/PIL/PsdImagePlugin.py | 2 +- src/PIL/QoiImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 2 +- src/PIL/WebPImagePlugin.py | 3 ++- src/PIL/WmfImagePlugin.py | 2 +- src/PIL/XpmImagePlugin.py | 2 +- 27 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 95e807781..8d351ce91 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -241,7 +241,7 @@ class BLPFormatError(NotImplementedError): pass -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] in (b"BLP1", b"BLP2") diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 9947f439b..6643ac39b 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -48,7 +48,7 @@ BIT2MODE = { } -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:2] == b"BM" diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 60f3ec25b..1cbd50d19 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -29,7 +29,7 @@ def register_handler(handler): # Image adapter -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC" diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index 5fb2b0193..b8790e209 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -25,7 +25,7 @@ from ._binary import i32le as i32 # -------------------------------------------------------------------- -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"\0\0\2\0" diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index f7344df44..b24c16329 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -29,7 +29,7 @@ from .PcxImagePlugin import PcxImageFile MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return len(prefix) >= 4 and i32(prefix) == MAGIC diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 93c8e341d..3032e4aec 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -562,7 +562,7 @@ def _save(im, fp, filename): ) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"DDS " diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 523ffcbf7..ec6705742 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -195,7 +195,7 @@ class PSFile: return b"".join(s).decode("latin-1") -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index f9e4c731c..7a233d015 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -27,7 +27,7 @@ from ._binary import o8 # decoder -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return ( len(prefix) >= 6 and i16(prefix, 4) in [0xAF11, 0xAF12] diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 75680a94e..cfaf86239 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -41,7 +41,7 @@ MODES = { # -------------------------------------------------------------------- -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:8] == olefile.MAGIC diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index b4488e6ee..a746959a3 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -107,7 +107,7 @@ class FtexImageFile(ImageFile.ImageFile): pass -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == MAGIC diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 6722fa2b1..62197e36c 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -29,7 +29,7 @@ from . import Image, ImageFile from ._binary import i32be as i32 -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return len(prefix) >= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 6b415d238..93be7fefb 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -60,7 +60,7 @@ LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST # Identify/read GIF files -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:6] in [b"GIF87a", b"GIF89a"] diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index f8106800c..a80fe0a23 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -29,7 +29,7 @@ def register_handler(handler): # Image adapter -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"GRIB" and prefix[7] == 1 diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index 65409e269..f50e6bf16 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -29,7 +29,7 @@ def register_handler(handler): # Image adapter -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:8] == b"\x89HDF\r\n\x1a\n" diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index d877b4ecb..c2c950863 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -374,7 +374,7 @@ def _save(im, fp, filename): fp.flush() -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == MAGIC diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index d66fbc287..82b190eb8 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -114,7 +114,7 @@ def _save(im, fp, filename): fp.seek(current) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == _MAGIC diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 8bae2974c..3ae901060 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -223,7 +223,7 @@ OPEN: dict[ str, tuple[ Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], - Callable[[bytes], bool] | None, + Callable[[bytes], bool | str] | None, ], ] = {} MIME: dict[str, str] = {} @@ -3298,7 +3298,7 @@ def open( preinit() - accept_warnings: list[str | bytes] = [] + accept_warnings: list[str] = [] def _open_core( fp: IO[bytes], @@ -3313,8 +3313,8 @@ def open( try: factory, accept = OPEN[i] result = not accept or accept(prefix) - if type(result) in [str, bytes]: - accept_warnings.append(result) # type: ignore[arg-type] + if isinstance(result, str): + accept_warnings.append(result) elif result: fp.seek(0) im = factory(fp, filename) @@ -3350,7 +3350,7 @@ def open( if exclusive_fp: fp.close() for message in accept_warnings: - warnings.warn(message) # type: ignore[arg-type] + warnings.warn(message) msg = "cannot identify image file %r" % (filename if filename else fp) raise UnidentifiedImageError(msg) @@ -3464,7 +3464,7 @@ def merge(mode, bands): def register_open( id, factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], - accept: Callable[[bytes], bool] | None = None, + accept: Callable[[bytes], bool | str] | None = None, ) -> None: """ Register an image file plugin. This function should not be used diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index be000c351..f28de5c4d 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -313,7 +313,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): return ImageFile.ImageFile.load(self) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return ( prefix[:4] == b"\xff\x4f\xff\x51" or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 81b8749a3..e3c0083e9 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -344,7 +344,7 @@ MARKER = { } -def _accept(prefix): +def _accept(prefix: bytes) -> bool: # Magic number was taken from https://en.wikipedia.org/wiki/JPEG return prefix[:3] == b"\xFF\xD8\xFF" diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index f4529d9ae..96de386a8 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -25,7 +25,7 @@ from . import Image, TiffImagePlugin # -------------------------------------------------------------------- -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:8] == olefile.MAGIC diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index d922bacfb..8b81e54ea 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -689,7 +689,7 @@ class PngStream(ChunkStream): # PNG reader -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:8] == _MAGIC diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index d29bcf997..b15918313 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -44,7 +44,7 @@ MODES = { # read PSD images -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"8BPS" diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index f8aa720c1..2875b8d75 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -13,7 +13,7 @@ from . import Image, ImageFile from ._binary import i32be as i32 -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"qoif" diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 8bfcd2907..10ac9ea3a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -277,7 +277,7 @@ PREFIXES = [ ] -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] in PREFIXES diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index c07abcaf9..9c8d53336 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -23,7 +23,7 @@ _VP8_MODES_BY_IDENTIFIER = { } -def _accept(prefix): +def _accept(prefix: bytes) -> bool | str: is_riff_file_format = prefix[:4] == b"RIFF" is_webp_file = prefix[8:12] == b"WEBP" is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER @@ -34,6 +34,7 @@ def _accept(prefix): "image file could not be identified because WEBP support not installed" ) return True + return False class WebPImageFile(ImageFile.ImageFile): diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index b5b8c69b1..7f045ec7d 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -65,7 +65,7 @@ if hasattr(Image.core, "drawwmf"): # Read WMF file -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return ( prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or prefix[:4] == b"\x01\x00\x00\x00" ) diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 3125f8d52..a638547af 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -24,7 +24,7 @@ from ._binary import o8 xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)') -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:9] == b"/* XPM */" From 1635e7a5714243cfe762b1dfaf3709bdf8a25118 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 6 Apr 2024 15:18:16 -0500 Subject: [PATCH 026/195] Update Tests/test_image_getdata.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_image_getdata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index 9181b2409..dd3d70b34 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -14,7 +14,7 @@ def test_sanity() -> None: assert data[0] == (20, 20, 70) -def test_hopper() -> None: +def test_mode() -> None: def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]: im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) data = im.getdata() From 05d231460600f485dd954cca88f73967b4fdd790 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 6 Apr 2024 15:52:17 -0500 Subject: [PATCH 027/195] Make ModeDescriptor a NamedTuple --- src/PIL/ImageMode.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 0b31f6081..b0c846553 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -16,24 +16,17 @@ from __future__ import annotations import sys from functools import lru_cache +from typing import NamedTuple -class ModeDescriptor: +class ModeDescriptor(NamedTuple): """Wrapper for mode strings.""" - def __init__( - self, - mode: str, - bands: tuple[str, ...], - basemode: str, - basetype: str, - typestr: str, - ) -> None: - self.mode = mode - self.bands = bands - self.basemode = basemode - self.basetype = basetype - self.typestr = typestr + mode: str + bands: tuple[str, ...] + basemode: str + basetype: str + typestr: str def __str__(self) -> str: return self.mode From a25a1aef059911d29765406c37b1a92797bac3e3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Apr 2024 08:38:43 +1000 Subject: [PATCH 028/195] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9fae613a5..e39031aea 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ Changelog (Pillow) ================== +10.4.0 (unreleased) +------------------- + +- Support reading CMYK JPEG2000 images #7947 + [radarhere] + 10.3.0 (2024-04-01) ------------------- From bcb2db6a871189160ce619ad581411e30d9b0b63 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sun, 7 Apr 2024 00:44:53 +0200 Subject: [PATCH 029/195] ImageStat: use functools.cached_property and add type hints --- docs/reference/ImageStat.rst | 67 ++---------------------- src/PIL/ImageStat.py | 98 ++++++++++++++++++++++++------------ 2 files changed, 69 insertions(+), 96 deletions(-) diff --git a/docs/reference/ImageStat.rst b/docs/reference/ImageStat.rst index f61d12313..f69466382 100644 --- a/docs/reference/ImageStat.rst +++ b/docs/reference/ImageStat.rst @@ -7,67 +7,6 @@ The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or for a region of an image. -.. py:class:: Stat(image_or_list, mask=None) - - Calculate statistics for the given image. If a mask is included, - only the regions covered by that mask are included in the - statistics. You can also pass in a previously calculated histogram. - - :param image: A PIL image, or a precalculated histogram. - - .. note:: - - For a PIL image, calculations rely on the - :py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are - grouped into 256 bins, even if the image has more than 8 bits per - channel. So ``I`` and ``F`` mode images have a maximum ``mean``, - ``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum - of more than 255. - - :param mask: An optional mask. - - .. py:attribute:: extrema - - Min/max values for each band in the image. - - .. note:: - - This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and - simply returns the low and high bins used. This is correct for - images with 8 bits per channel, but fails for other modes such as - ``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to - return per-band extrema for the image. This is more correct and - efficient because, for non-8-bit modes, the histogram method uses - :py:meth:`~PIL.Image.Image.getextrema` to determine the bins used. - - .. py:attribute:: count - - Total number of pixels for each band in the image. - - .. py:attribute:: sum - - Sum of all pixels for each band in the image. - - .. py:attribute:: sum2 - - Squared sum of all pixels for each band in the image. - - .. py:attribute:: mean - - Average (arithmetic mean) pixel level for each band in the image. - - .. py:attribute:: median - - Median pixel level for each band in the image. - - .. py:attribute:: rms - - RMS (root-mean-square) for each band in the image. - - .. py:attribute:: var - - Variance for each band in the image. - - .. py:attribute:: stddev - - Standard deviation for each band in the image. +.. autoclass:: Stat + :members: + :special-members: __init__ diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index 13864e59c..07ea76e6f 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -23,35 +23,61 @@ from __future__ import annotations import math +from functools import cached_property + +from . import Image class Stat: - def __init__(self, image_or_list, mask=None): - try: + def __init__( + self, image_or_list: Image.Image | list[int], mask: Image.Image | None = None + ) -> None: + """ + Calculate statistics for the given image. If a mask is included, + only the regions covered by that mask are included in the + statistics. You can also pass in a previously calculated histogram. + + :param image: A PIL image, or a precalculated histogram. + + .. note:: + + For a PIL image, calculations rely on the + :py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are + grouped into 256 bins, even if the image has more than 8 bits per + channel. So ``I`` and ``F`` mode images have a maximum ``mean``, + ``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum + of more than 255. + + :param mask: An optional mask. + """ + if isinstance(image_or_list, Image.Image): if mask: self.h = image_or_list.histogram(mask) else: self.h = image_or_list.histogram() - except AttributeError: - self.h = image_or_list # assume it to be a histogram list + else: + self.h = image_or_list if not isinstance(self.h, list): - msg = "first argument must be image or list" + msg = "first argument must be image or list" # type: ignore[unreachable] raise TypeError(msg) self.bands = list(range(len(self.h) // 256)) - def __getattr__(self, id): - """Calculate missing attribute""" - if id[:4] == "_get": - raise AttributeError(id) - # calculate missing attribute - v = getattr(self, "_get" + id)() - setattr(self, id, v) - return v + @cached_property + def extrema(self) -> list[tuple[int, int]]: + """ + Min/max values for each band in the image. - def _getextrema(self): - """Get min/max values for each band in the image""" + .. note:: + This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and + simply returns the low and high bins used. This is correct for + images with 8 bits per channel, but fails for other modes such as + ``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to + return per-band extrema for the image. This is more correct and + efficient because, for non-8-bit modes, the histogram method uses + :py:meth:`~PIL.Image.Image.getextrema` to determine the bins used. + """ - def minmax(histogram): + def minmax(histogram: list[int]) -> tuple[int, int]: res_min, res_max = 255, 0 for i in range(256): if histogram[i]: @@ -65,12 +91,14 @@ class Stat: return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)] - def _getcount(self): - """Get total number of pixels in each layer""" + @cached_property + def count(self) -> list[int]: + """Total number of pixels for each band in the image.""" return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)] - def _getsum(self): - """Get sum of all pixels in each layer""" + @cached_property + def sum(self) -> list[float]: + """Sum of all pixels for each band in the image.""" v = [] for i in range(0, len(self.h), 256): @@ -80,8 +108,9 @@ class Stat: v.append(layer_sum) return v - def _getsum2(self): - """Get squared sum of all pixels in each layer""" + @cached_property + def sum2(self) -> list[float]: + """Squared sum of all pixels for each band in the image.""" v = [] for i in range(0, len(self.h), 256): @@ -91,12 +120,14 @@ class Stat: v.append(sum2) return v - def _getmean(self): - """Get average pixel level for each layer""" + @cached_property + def mean(self) -> list[float]: + """Average (arithmetic mean) pixel level for each band in the image.""" return [self.sum[i] / self.count[i] for i in self.bands] - def _getmedian(self): - """Get median pixel level for each layer""" + @cached_property + def median(self) -> list[int]: + """Median pixel level for each band in the image.""" v = [] for i in self.bands: @@ -110,19 +141,22 @@ class Stat: v.append(j) return v - def _getrms(self): - """Get RMS for each layer""" + @cached_property + def rms(self) -> list[float]: + """RMS (root-mean-square) for each band in the image.""" return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands] - def _getvar(self): - """Get variance for each layer""" + @cached_property + def var(self) -> list[float]: + """Variance for each band in the image.""" return [ (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] for i in self.bands ] - def _getstddev(self): - """Get standard deviation for each layer""" + @cached_property + def stddev(self) -> list[float]: + """Standard deviation for each band in the image.""" return [math.sqrt(self.var[i]) for i in self.bands] From 76fb002dd4d7bac29407c532dc119ed8ba07dd10 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 7 Apr 2024 16:14:41 +1000 Subject: [PATCH 030/195] Removed outdated comment --- src/PIL/ImageMode.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 0b31f6081..f6524f44b 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -42,7 +42,6 @@ class ModeDescriptor: @lru_cache def getmode(mode: str) -> ModeDescriptor: """Gets a mode descriptor for the given mode.""" - # initialize mode cache endian = "<" if sys.byteorder == "little" else ">" modes = { From 2f3281dcda446c0a5b4121ee8e17feb312192c1e Mon Sep 17 00:00:00 2001 From: Jonah Jeleniewski Date: Mon, 8 Apr 2024 21:14:19 +1000 Subject: [PATCH 031/195] Add support for bitmaps with header size 52 Size 52 is the undocumented `BITMAPV2INFOHEADER`. It adds the RGB bit masks. The format is known to be supported by: - Adobe Photoshop - Popular web browsers --- Tests/images/bmp/q/rgb32h52.bmp | Bin 0 -> 32578 bytes Tests/test_bmp_reference.py | 1 + src/PIL/BmpImagePlugin.py | 15 +++++++++------ 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 Tests/images/bmp/q/rgb32h52.bmp diff --git a/Tests/images/bmp/q/rgb32h52.bmp b/Tests/images/bmp/q/rgb32h52.bmp new file mode 100644 index 0000000000000000000000000000000000000000..db6e4538ef84f48baf706603bd919223d372444b GIT binary patch literal 32578 zcmcKDKWtlPg7E8eaa9nwDhON_9DoHEV8Ja|2)Hab0D;Q^SZDziT0n&s5U|=A&5V*L ziQ>q%Y|DRSTef9ewq;wkWm~poTS=5eNi-AAF83Q21eS#g7A%GZfnh;lS+G#SLIn%m zhndm0`MeYFot^o3&d-b_i|={g$5wRs$NI_0PtFD3U4G)%|402d|6lzdK|J_BUH9Md zfB#>9sQ>04FNeWl(BHqighD|O4hO-HeiQ_M^EW~8w|^T1LqkFE;~xjXPu%d|{aq0J z{oe<{Km0=w{Pd?m@b0@oaO_wRoIV`{mo5dtjT=F5?_Ll*c@hM#UkAaLUk1Uq-v+^N ze;Wk<_HRK@tp>sGeisD4|9uer`@aXl{(cY~92ozP2VnS+;lqaik>UTw@PBLgA;bUJ z@INv9-x>bz4gU{@|Eb~MHT*HdpEmp@!{0FcJ;OgS{A02){mAw=w!gIv*?w&MiS6%fe{cH-+fQxp+K$;y+b-E|*zVb$ z*k0Sdw0&#)t?l1zRom}uzqkFnZQpiaqwtUa82sa;{z*U8&orf<>lgZ^ruB~A)q9%J z`}#m1YE~cVm_F8=j_ZU@YF?*wT4%JNvw}aset`V|`vLX?><8EnupeMQz<8EnupeMQz<8EnupeMQzx52fUZ-?gXSATRIww;Pu@A8iu@A8iu@A8iu@A8iu@A8iu@A8iu@A8i zu@A8iu@A8iu@A8iu@A8iu@A8iu@A8iu@A8iu@A8ivG4zla{$KoH}6Lw_96Bm_96Bm z_96Bm_96Bm_96Bm_96Bm_96Bm_96Bm_96Bm_96Bm_96Bm_96Bm_96Bm_96D6!2XBv zec=g**oWAM*oWAM*oWAM*oWAM*oWAM*oWAM*oWAM*oWAM*oWAM*oWAM*oWAM*oWAM z*oWAM*oWAM*#FZ%1^@I@{Y+E(xqhKvYFh8;UA?Cny{`}Sp=R}wj_G5~>9|hlq~>)> zr*%dPI;(R!FH;}1_JiyP*$=WGWIxD$ko_S0LH2{}2iXs@A7nqsevthj`$6`D><8Ho zvL9qW$bOLhAp1e~gX{;{53(O*KgfQN{h%!f4YD6(KgfQN{UG~6_JiyP*$=WGWIxD$ zko_S0LH2{}2iXs@A7nqsevthj`$6`D><8HovL9qW$bOLhAp1e~gMq!j01kouAp1e~ zgX{;{53(O*KgfQN{UG~6_JiyP*$=WGWIxD$ko_S0LH2{}2iXs@A7nqsevthj`$6`D z><8HovbUZ;{h6lpbNxcU)U@8wyLwMEdS4&tL(S?V9n;5}({Y{9NzLn&PV0;obXMnd zUKeEQVfJD6VfJD6VfJD6VfJD6VfJD6VfJCi8fG76A7&qBA7&qBA7&qBA7&qBA7&qB zA7&qBA7&qBA7&qBA7*b<_P*wPH@^Sl{!N&Dn0=Uin0=Uin0=Uin0?rKgxQDLhuMeO zhuMeOhuMeOhuMeOhuMeOhuMeOhuMeOhuMeOhXZ?m0sb()Z#aGwW*=rBW*=rBW*=rB zW*=rBwq{}WVfJD6VfJD6VfJD6VfJD6VfJD6VfJD6VfJD6VfJD6VfJD6Kl@qmvnl;t zztAr=t#|aU-qVcU*9ZDgv-(KK^s(l2Tqkr=^E#!|I->=h)j6Hl1ue?dBkUvAKEgi2 zKEgi2KEgi2KEgi2KEgi2KEgi2KEgi2KEgi2KEgi2KEgi2KEgi2KEgi2KEgi2KEgi2 zKEgg?3qlcVAMs}|!al-2!al-2!al-2!al-2!al-2!al-2!al-2!al-2!al-2!al-2 z!al-2!al-2!al-2!al-264?6-;1K*NjIfWekFbxhkFbxhkFbxhkFbxhkFbxhkFbxh zkFbxhkFbxhkFbxhkFbxhkFbxhkFbxhkFbxhkFcMb3Z{OpU+9;b);oGv?`cNw>jQnL zS$(8q`dD*1t`j<`d7aW}oza5M>YUE&f);gArXFP{>2V;^H5V;^H5 zV;^H5V;^H5V;^H5V;^H5V;^H5V;^H5V;^H5V;^H5V;^H5V;^H5V;^H5V;^Jxi(dr4 z_@$=xj^5RKn$i3EKp$#WAL*Dr)|`&(gidN+r*v9pw4k#(r}Mg?MP1Y-U6!fG*~i(( z*~i((*~i((*~i((*~i((*~i((*~i((*~i((*~i((*~i((*~i((*~i((*~i((*~i(( z*~i((*~e``D9%34KF&VQKF&VQKF&VQKF&VQKF&VQKF&VQKF&VQKF&VQKF&VQKF&VQ zKF&VQKF&VQKF&VQKF&VQJ|5Wn3*Zpg$Jxi($Jxi($Jxi($Jxi($Jxi($Jxi($Jxi( z$Jxi($Jxi($Jxi($Jxi($Jxi($Jxi($Jxi($Jzh#m%%Tm^^V@vdz#Vv`amCQRv+n@ zKGvL$>x52fUZ-?gXSATRI;ZoxphaEOC0*8%Og+Iq!9Kx0!9Kx0!9Kx0!9Kx0!9Kx0 z!9Kx0!9Kx0!9Kx0!9Kx0!9Kx0!9Kx0!9Kx0!9Kx0!9Kx0!9Kx0VGBYD_6hb0_6hb0 z_6hb0_6hb0_6hb0_6hb0_6hb0_6hb0_6hb0_6hb0_6hb0_6hb0_6hb0_6hb0_6hch zz}{Z~hrm9;KEXc0KEXc0KEXc0KEXc0KEXc0KEXc0KEXc0KEXc0KEXc0KEXc0KEXc0 zKEXc0KEXc0KEXc0etJ5Xen;=>JbwVdKuTwg$Gg{DDozr<; z(4sEtk}hjWS7ho*_DS|h_DS|h_DS|h_DS|h_DS|h_DS|h_DS|h_DS|h_DS|h_DS|h z_DS|h_DS|h_DS|h_DS|h_DS|h_DNe1O0rL~PqI(4PqI(4PqI(4PqI(4PqI(4PqI(4 zPqI(4PqI(4PqI(4PqI(4PqI(4PqI(4PqI(4PqI(4PX_k>0yqTrN%l$hN%l$hN%l$h zN%l$hN%l$hN%l$hN%l$hN%l$hN%l$hN%l$hN%l$hN%l$hN%l$hN%l$hN%rr&6TI`T z-qVcU*9ZDgv-(KK^s(l2Tqkr=^E#!|I->=h)j6Hl1ug2LF6pwCbVXNX>M8aq_9^x$ z_9^x$_9^x$_9^x$_9^x$_9^x$_9^x$_9^x$_9^x$_9^x$_9^x$_9^x$_9^x$_9^x$ z_9^x$TM$aIPq9z2Pq9z2Pq9z2Pq9z2Pq9z2Pq9z2Pq9z2Pq9z2Pq9z2Pq9z2Pq9z2 zPq9z2Pq9z2Pq9z2Pq9x0_WlAm1okQRDfTJ$DfTJ$DfTJ$DfTJ$DfTJ$DfTJ$DfTJ$ zDfTJ$DfTJ$DfTJ$DfTJ$DfTJ$DfTJ$DfTJ$*7Du=G^6+Rfj-o%KGHFLtT`Rm37you zPU*DHXhCOnPUm$&i@K;wx~wH#(N$fOsSmLqVn4)wi2V@zA@)P;hu9CXA7Vemeu({$ z;~io@#D0kV5c?taL+ppx53wI&Kg52B{Sf;h_CxH4*blKEVn4**DD2&b=zKc-qapS~ z?1$L*$9IqVPlnhJu^(bT#D0kVkTn@%Kg52B{Sf;h_CxH4*blKEVn4)wi2V@zA@)P; zhu9CXA7Vcg*!v6M5ZDi~A7VemzCXSP6n;F!eu(`L`yuv2?1!w|5c?taL+ppx53wI& zKg52B{Sf;h_CxH4*blKEVn4)wi2V@zA@=XR7rZy4_w|83)T}VDB%0 zLtvj~pJtzCpJtzCpJtzCpJtzCpJtzCpJtzCpJtzCpJtzCpJtzCpJtzCpJtzCpJtzC zpJtzCpJtzCKQj}|ysr=Rp=R}wj_G5~>9|hlq~>)>r*%dPI;(R!uM1k#MP1TmE$ND` z>YA2yU8bI4pJAV2pJAV2pJAV2pJAV2pJAV2pJAV2pJAV2pJAV2pJAV2pJAV2pJAV2 zpJAV2pJAV2pJAV2pJAV2pRom@4EqfG4EqfG4EqfG4EqfG4EqfG4EqfG4EqfG4EqfG z4EqfG4EqfG4EqfG4EqfG4EqfG4EqfG4EqfGOknRXfJ0!PVV_~2VV_~2VV_~2VV_~2 zVV_~2VV_~2VV_~2VV_~2VV_~2VV_~2VV_~2VV_~2VV_~2VV_~2VgLU7!TTTRL(S?V z9n;5}({Y{9NzLn&PV0;obXMndUKg~ei@K!CTGAC=)io{ax^BqSv+T3%v+T3%v+T3% zv+T3%v+T3%v+T3%v+T3%v+T3%v+T3%v+T3%v+T3%v+T3%v+T3%v+T3%v+T3%v+T3B zAe3dFWuIlAWuIlAWuIlAWuIlAWuIlAWuIlAWuIlAWuIlAWuIlAWuIlAWuIlAWuIlA zWuIlAWuIlAWuFb~{RMCc?6d5%?6d5%?6d5%?6d5%?6d5%?6d5%?6d5%?6d5%?6d5% z?6d5%?6d5%?6d5%?6d5%?6d5%?6d4Y_#pV;L(S?V9n;5}({Y{9NzLn&PV0;obXMnd zUKg~ei@K!CTGAC=)io{ax^C#EOg+aw$3DkC$3DkC$3DkC$3DkC$3DkC$3DkC$3DkC z$3DkC$3DkC$3DkC$3DkC$3DkC$3DkC$3DkC$3DkCXA43(_Br-B_Br-B_Br-B_Br-B z_Br-B_Br-B_Br-B_Br-B_Br-B_Br-B_Br-B_Br-B_Br-B_Br-B_Br;sz}{Z~hrm9^ zKF2=CKF2=CKF2=CKF2=CKF2=CKF2=CKF2=CKF2=CKF2=CKF2=CKF2=CKF2=CKF2=C zKF2=C{=*N04`=m}j_G5~>9|hlq~>)>r*%dPI;(R!uM1k#MP1TmE$ND`>YA2yT{m=7 zD>C&w`#k$R`#k$R`#k$R`#k$R`#k$R`#k$R`#k$R`#k$R`#k$R`#k$R`#k$R`#k$R z`#k$R`#k$R`#k$R`@Ag(<=N-i=h^4k=h^4k=h^4k=h^4k=h^4k=h^4k=h^4k=h^4k z=h^4k=h^4k=h^4k=h^4k=h^4k=h^4k=L36x0UQGRJo`NRJo`NRJo`NRJo`NRJo`NR zJo`NRJo`NRJo`NRJo`NRJo`NRJo`NRJo`NRJo`NRJo`NRJp0+%VD=*&)5n_Aah=df z&Fhp->x>q3R_AnH7qqC0x}?im(iL6RH7)D9Zs?{~bW5f_%zl{tF#BQl!|aFI53?U; zKg@oZ{V@As_QULl*$=ZHWbwVdKuTwg$Gg{DD zozr<;(4sEtk}hjWS9Dd^w5;p8p_^LKE!~!>kFXzMKf->5{RsOJ_9N^^*pILuVL!rt zg#8Hn5%weON7#?BA7MYjeuVu9`w{jd>_^y-7~`*cd3S{U2>TKCBkV`mkFX!{oT(qU z{BDH(2>TKCBkV`mj|BGq0tBHE_9N^^*pIOPft#@a!B0onkFfv2-@|@{{RsOJ_9N^^ z*pILuVL!rtg#8G6>v-&A&FQ#K=%nU#N~d*33p%TFII3CQTC(kM;-Gh`%(6z>_^#;vL9tX z%6^pnDEm?Nqd&Apf9d;AkFp_^#;vL9tX%KqbzgOBHQTqkr=^E#!| zI->=h)j6Hl1ug2LF6pwCbVXNnP0PBj8@j0#-O_E{(W)H$82d5yW9-M+kFg(PKgNEH z{TTZ(_G9eF*pIOvV?V}zjQtq8m&VwS zu^(eU_8;~~;~f3o-;J>!V?V}zjQtqkLCoz*#=*99%=qAuyO zmUKl|bxq5x>q3R_AnH7qqC0x}?im(iL6RH7)D9Zs?{~bW68&N2|K4douM2_7m(U z*iW#ZU_Zfrg8c;h3HB50C)iK0pI|@1euDi3`w8|F>?hbyu%BQ*!G41M1pA4;Pycdc z{)e~zd%qstM*sTg7H&+ipI|@n7W)bI6YMA0Pq3e0KM~mb3lM}R*iW#ZU_bE|`w8|F z>?hu0Kf!*2{RI07_7m(U*iW#ZU_Zfrg8hU!oH!AjIH`G^(rKO1g3juk&g+5}by1ge zSxdU2tGcFTUDplW)QWEDw(e+EcXdzqW$KgcC)rQ3pJYGDev?hezvY%u> z$$pakB>PGBlk6wiPqLq6KgoWQ{UrNI_LE0OM{Y-N|97w7yqz19>?hezzQul${UrNI z_LJ-<*-v_o^mhvogeKWfvY%u>`4;<0_LJ-<-(o+>ev?hezvY%u>$$pak zB>R&mgOl?*rPDg21)bG7o!12|>Y^^`vX*p3S9MLxx~?0#sTJMQZQaqT?&_ZIYfYv; z#eRzY6#FUmQ|zbMPqCk3KgE8E{S^Bt_EYSq*iW&aVn4-xiv1M(DfUzBr`S)ipJLyC zUf6e+H_g2L-Lu9*Q_k_Goa0S7$D4AFH{~2}$~oSYbG#|%cvD^*@x9BNw{v5P{S^DD zx7bgypJG47ev17R`zgMi#D=hIHHpL&b^6#FUmQ|zbMPqCk3 zKgE8E{S^Bt_EYTV=Y#oEI;}HW&{>_+d0o(=F6xplYe`phRoAqv>$;(vTG1`t)*Y?t zuI}l+*7QK8KFxlb{WSY&_S5XA*-x{dW`)T&m z?5EjJv!7<)e~!R+mp9G4{oOMNLeuQ0*-x{dW`)T&mo;%?-r`b=lpJqSJe)=u;{pU_kv!8y8{WSY&_S5XA*-x{d zW~6+>yB1+ zSNC*ZYkHuEGW8kuGwf&B&#<3iKf`{8{S5mV_A~5f*w3(^VL!uuhW!lt8TK>mXV}lM zpJ6}4eun)F`~Kra-(CKM{dbQhg=W~#u%BT+!+wVS4Eq`OGwf&B&%DKchW!ltnYY-_ zu%BT+!+wVS4Eq_6qdBq}_A~5f*w3(^d5eAjdGh_o+GpNkKf`{8{S5mV_A~5f*w3(^ zVL!uuhW!kCYk2yM7IaqUbY2&$+~}rdD)Iw{=IWx~qG-uQff; zLp_qI&$6FoKg)iW{Ve-g_Ot9~+0U|{Wk1V)mi;XIS@yGzdzSqy`&st0>}T1}vY%x? z%YK%9|GvZTE`P%QyL%Qxv+QTt&$6FoKg)iW{Ve-g_Ot)R*Zf)bv+QTzVn54%mi;XI zS@yH+XWggj$Y$BkvY%x?%YODP_WjpH^q*HT`xg6I_Ot9~+0U|{Wk1V)mi;XIS@yH+ zXW5@Q6P#JlS)J2)UC^Q~>XI&NNmq1L*R-tbx}lp|(JkH99j)rF?&-eP^gs{wNb557 zIrekx=h)A&pJPAAevbVd`#JV=?C03ev7ci<$9|6e9Q!%;bL{8X&#|9lKgWKK{T%!L zdE|GOH|>w!|L0u4c^l4gFFB9Bac_?O+*|DD*w3+_V?W1!&TAq6c$s5A$9|6e9Q(Pq z*!N%m(ti$3|9-fm&wulnj^6+0UY}nG7A|N}7j;ROwWKS$s%u)-b=}ZSt>~6+>yB1+ zSNC*ZYkHuEdZcwdmgAdeKhJ)i{XF}5_VeuL+0V0|XFtz=p8Y)gdG_<{=h@G*pJzYM zexCh2`+4^B?C06fv+sXT@!jPwU86TY-}gtSegAXG_c%9vA9L@?{9Ekj+0V0|XFuYnavO%L=?kF>7GdLqZSz!aH`w&1?*1@;T<7uYYbUwDiC z0{aE_3tj{L$IH=i9KF53eu4eMTkQL=)TDvR`Ds$bOOi zBKt-5i|iNKFS1``zsP=({UZBC_KWNn*)OtRWWUIM@n}we>Du%^|LC@kEwW!^zsP=( z{UZCtx7aVTU-bI-KVIHEmZSGCvR{0Q{UZDRb0GVVqa1zCo456synS?={p%~|gY#E) zP0PBj8@j0#-O_E{(W>t1p6+W+5A;xvw64c`qNm!BA(q%Lv0q}p#D0nW68k0gOYE1} zFR@=@zr=ot{Sx~n_Dh#81xxIg*e|hPV!y|M{-{$IO1{_Kx2F=Ug8hL;w2d^R8Y9 zE?m>HuIq+wYDKqnTX(doySk_QTGInP)FZ9yv7YFuHuOx!S!Tb?ewqC;`(^ga?3dXu zvtMSv%zl~uGW%ur%j}ogFSB1}zs!D_{WAMy_RH*-*)OwSKAOwX{EpuK?_9rm8<&>Z zFSB1}zx>1YM`OMD`G4;Fm)S3~?>|2JUw!V;$N&Fczj^=YkHuEdZcwdmamD6Pqm?EdM?Mc!hVJQ3i}oIE9_U;udrWXzrucn{R;aP z_ABgH*sri(VZXwDh5ZWq74|FaSJ=IxdKM?)*l@m8GUtvJV9agMj* z9B;)r-imX)73X*>|HV1p3i}oID{r^|PvfryhyU~79ORoH?;rP@?^|KN!oL4l_z%r% z#W~)JbG#Mjcq`8FR-EIlILBLYj<@0*Z^b#@igUb`@BJPBQ)4Y(3@%>R4c*j=Zt1q} zXjON0PxrN^2YRSSTGwMe(Nk^cnV#!~jJe8wmHjIFRragwSJ|(!UuD0_ewF{r>ZvR`Gt%6^soD*ILTtL#_Vud-idzxqRKWL;PL9}TUtUuD0_ewF{r>Z zx^HLoE%)uLvR`Gt`os2b8Xl}Vzh4dP{RId@Z#l;A9(O+a_$vEV_N#BPUuD0_ewF{r>ZvR`Gt%6^r-^}BR*+ixD@bsxK-n_AH=-PRqg>aOnTzSi_W5A{gvdaNgU zstrBUbG^`}jJw8ujr|(?HTG-l*VwPIUt_<>hH*;OAklevSPa`!)7!?AO?@v0r1q#(wQB_G|3d*suL*`#0Bg zf6e**8vC`t-d_NR;2iHy9p{^$yT*Qv{n}gX*VwPIUt_<-~($9kft+R!sS*9&dxr3|soex3a~ z`*rr~?AO_^vtMVw&VHT!I{S6@>+ILrud`ogzs`Q0{W|+~_Ur7|*{`!-XTSbKbF;4N z{f~y$*{`!-XTQ#Vo&7rdb@uD**Z+&J`RnZ0*{}a+`#0_Weck!}I{S6@>w&$$01kou zx^uiYkM+&ZUuVD0e*G=>>+ILrud`ogzs`Q0{W|+~_Ur7|*{}Z>U-NG+1xqWsrQ5or zRo#{EZI|wAO%L=?kF>7G@_p~pQ*G#(p6i7+^-`~7t{dz(*l)1kV86kBgZ&2k4fY%C zH`s5m-(bJNeuMo6`wjLR>^InNu-{<6!G44N2Kx>68*i@X(cAys>o+}aY_Q*8zwxK- zkH-1^hV%Ok_8aUs*lz^({sIJ{4fY%CH=N@g{r;nI`qxMA+hD)Je&a3nN5A*ay*?W6 z=<`;t1Xph9w(e+EcXdzqwWbGps7G4YV?EJRZRnYv>xDM;Qm^z{#@S@Q$$pdlCi_kH zo9s8)Z?fNHzsY`+{U-ZO_M7ZC*>AGnWWULNll><9P4=7YH`#Bp-#nVjn{zvQ|DSVx zbQ|CIxUtE8ll|s@wm-V<-)%a--(-(XFvExHv4V%+w8a5Z?oTKzs-J| z{Wkk;_S@{YzrTim?e)2|?R(*E_S@{Y|HJ-h%zxYV{QPb9+w8a5Z?oTKzs-I-u=f`r z2yL_9X1~pToBcNXZO_j?x}BrzZT8#jxBoZTAC1w!zP%hQ-_fe>%KaD1_qC=6dZu>z?04Aju-{?7!+wYT4*MPUJM4GZ@37xt zzr%iq{SNyb_B-r%*zd64VZXzEhy4!wo$t@-ule7ncigYH!+wYT4*Q+|)&93TUYoJQ zeuw=I`yKW>?04Aju-^&n{RId@JM4GZ@37xtzr%iq{f^gWeE*nt*zd64`JVmvKmOPF z@7vdd>vy!OySk_QTGInP)FZ9yv7YFuHuOx-^+KC^saJZfEq$U-^_d*&F8f{fyX<$_ z@3P-zzsr7?{Vw}m_PgwN+3&L7WxvaQm;EmLUG}@|ciHc<-(|ncewY33Ut>MKe;e=a zdTqw8`}23%@3P-zzsr92KkUEV^}3v0_PgwN+3&L7WxvaQm;EmL-N4>ofFQKXewY0& z`(5_C?04DkvfuT(oTKq~+3&L7{oi1J%jyB1+SNC*ZYkHuEdZcwd))PI|hMwuU zUT9M<^-8a`rBC#!KGU|$sldL#zQDe~zQDe~zQDe~zQDe~zQDe~zQDe~zQDe~zQDe~ zzQDe~zQDe~zQDe~zTh=k1+U2}cuiKpYqAPnlU4AVtb(uq1zQj*IL9kE$16C;D>%n1 zIL9kE$16C;D>%n1IKMA=O;*8cvI<_4RbXFWUtnKgU$906_64uWDtJv+!E3S#>YnavO%L=?kF>7GdZMS=&@(;P3vKG9Ug@>A^oc&zXWG{1GW8<+BKsoy zBKsoyBKsoyBKsoyBKsoyBKsoyBKsoyBKsoyBKsoyBKsoyBKsoyBKsoyBKsoyBKsoy zBKsoyqAds&*%#Rt*%#Rt*%#Rt*%#Rt*%#Rt*%#Rt*%#Rt*%#Rt*%#Rt*%#Rt*%#Rt z*%#Rt*%#Rt*%#Rt*%#Rt1ABh~90L0y`y%@y`y%@y`y%@y`y%@y`y%@y`y%@y`y%@y z`y%@y`y%@y`y%@y`y%@y`y%@y`y%@y`y%@y`xWssebg)WwWbGps7G4Y zW4Zr&<*7FGOwaW~n|i5NdaW&eqEGdiw)MHbkg1p0m)Musm)Musm)Musm)Musm)Mus zm)Musm)Musm)Musm)Musm)Musm)Musm)Musm)Musm)Musm)Musmux|(#JO>?`am z>?`am>?`am>?`am>?`am>?`am>?`am>?`am>?`am>?`am>?`am>?`am>?`am>?`am z>?`amwjfktUtwQiUtwQiUtwQiUtwQiUtwQiUtwQiUtwQiUtwQiUtwQiUtwQiUtwQi zUtwQiUtwQiUtwQiUtwPf?EM9B2<$8DE9@)mE9@)mE9@)mE9@)mE9@)mE9@)mE9@)m zE9@)mE9@)mE9@)mE9@)mE9@)mE9@)mE9@)m@7xLQtm>}r>Au$VKo9ju>w2swda4aQ z({sJhre5lmUTaIA=u>^BZGEmU^rd$6l}vq){T};0_IvF2*zd95W536KkNqC|J@$L- z_t@{T-($bWevkbg`#tu1?DyF3vEO6A$9|9f9{WA^d+hhv@3G%wzh?_Vd+hhv@3G%w zzsG)${T};0_IvF2*zd95W536KkNqC|J@$L-_t@{T-($bWevkbg`#tu1?DyF3vEO6A z$9|9fUSRJpfJ0!v$9|9f9{WA^d+hhv@3G%wzsG)${T};0_IvF2*zd95W536KkNqC| zJ@$L-_t@{T-($bWevkbg`#tu1>{mUKc~|#zUu$}xhkB%SJ(kBPSD$J_&-7d`w5gYR zrPtchC;C*MXQ(kt_Eq*(_Eq*(_Eq*(_Eq*(_Eq*(_Eq*(_Eq*( z_Eq*(_Eq*(_Eq*(_Eq*(_Eq*(_Eq*(_Eq*(_Eq*i!rsqT@b8R=e^g~(WnX3Q-(~x8 zm3@_cm3@_cm3@_cm3@_cm3@_cm3@_cm3@_cm3@_cm3@_ce|$f+-~($9kft+R!sS*9&dxrC#Z^w)BZU)o0q)=lVilYDZt`Yki|#nR=aloqe5s zoqe5soqe5soqe5soqe5soqe5soqe5soqe5s-5l!d>+I|7>+I|7>+I|7>+I|7>+I|7 z>+I|7>$V_NXJ2PuXJ2PuXJ2PuXJ2PuXJ2PuXJ2PuXJ2PuXJ2PuXJ5BYb@p}kb@p}k zb@p}kb@p}kb@p}kb@p}k^}ya=0EfW7&c4pR&c4pR&c4pR&c4pR&c4pR&c4pR&c4pR z&c1Ff>+I|7>+I|7>+I|7>+I|7>+I|7>+I|7>+J8}5ALt&fgb9S*7aCV^i&&qrssO0 zO}*4Bz1EgK(Wm-M+xlEz=u7SBD}AkRw5wmq)En#@>>KPG>>KPG>>KPG>>KPG>>KPG z>>KPG>>KPG>>KPG>>KPG>>KPG>>KPG>>KPG>>KPG>>KPG>>KPGwjk7C-(cTh-(cTh z-(cTh-(cTh-(cTh-(cTh-(cTh-(cTh-(cTh-(cTh-(cTh-(cTh-(cTh-(cTh-(cSe z?EM9B2<#i|8|)kG8|)kG8|)kG8|)kG8|)kG8|)kG8|)kG8|)kG8|)kG8|)kG8|)kG z8|)kG8|)kG8|)kG*F1aWfgb9S*7aDPhqCrm8+xYadZA6d)GNK#mOjy^`b^vUTwmx* z?dU6gt#7oeU+LE}^(Ol!`zHG)`zHG)`zHG)`zHG)`zHG)`zHG)`zHG)`zHG)`zHG) z`zHG)`zHG)`zHG)`zHG)`zHG)`zHIQEeJK)H`zDYH`zDYH`zDYH`zDYH`zDYH`zDY zH`zDYH`zDYH`zDYH`zDYH`zDYH`zDYH`zDYH`zDYH`zA>dw&5O0{bTWCi^D)Ci^D) zCi^D)Ci^D)Ci^D)Ci^D)Ci^D)Ci^D)Ci^D)Ci^D)Ci^D)Ci^D)Ci^D)Ci^D)2M>Y= z5A{gvdaNgUstrBUbG^`}Uh0)zYfGQ#Q+=jweXcL`rFQg{zScL|)vxqxeJfLMv2U?& zv2U?&v2U?&v2U?&v2U?&v2U?&v2U?&v2U?&v2U?&v2U?&v2U?&v2U?&v2U?&v2U?& zv2U?&v2WReP>X$weT#jIeT#jIeT#jIeT#jIeT#jIeT#jIeT#jIeT#jIeT#jIeT#jI zeT#jIeT#jIeT#jIeT#jIeJil{7r-H~Z?SK&Z?SK&Z?SK&Z?SK&Z?SK&Z?SK&Z?SK& zZ?SK&Z?SK&Z?SK&Z?SK&Z?SK&Z?SK&Z?SK&Z?S*)FnIV#>w2swda4aQ({sJhre5lm zUTaIA=u>^BZGEmU^rd$6mA=+D+SRZ0YkjMNOufy%&A!dP&A!dP&A!dP&A!dP&A!dP z&A!dP&A!dP&A!dP&A!dP&A!dP&A!dP&A!dP&A!dP&A!dP&A!dPZ3{we_HFiU_HFiU z_HFiU_HFiU_HFiU_HFiU_HFiU_HFiU_HFiU_HFiU_HFiU_HFiU_HFiU_HFiU_HFj< zz}{Z~hrqtgzRkYPzRkYPzRkYPzRkYPzRkYPzRkYPzRkYPzRkYPzRkYPzRkYPzRkYP zzRkYPzRkYPzRkYP{?Vi0(YhY%iJodh&-7d`w5gYRrPtchC;C*MX^tl`>^tl`>^tl` z>^tl`>^tl`>^tl`>^tl`>^tl`>^tl`>^tl`>^tl`>^tl`>^tl`>^tl`>^tn&J!A2) zp6IDI^i0q7LYsQ2S9+~2eWFkGnYQ)0zR;K2(O3Ff-)L9A(y#Tc3i^%yS*E_vexLn5 z`+fHN?DyI4v)^aG&wii%KKp(4`|S7G@3Y@$zt4W3{XYAB_WSJj+3&O8XTQ&WpZz}j zefIn8_u22W_h0P&tVBOIEc~N=_WSJj+530af4t9rpZz}jefIn8_u22W-)FziexLn5 z`+fHN?DyI4v)^aG&wii1`xAb*&wii%KKuUoek$qVeqiq}fJ0!v&wii%K70Sp`p`c6 zefIn8_u22W-)FziexLn5`+fHN?DyI4v)^aG&wii%KKp(4?oX)hv)^aG&%QtYKKp(4 zj~@q*pXjMJ^i0q7LYsQ2S9+~2eWFkGnYQ)0zR;K2(O3Ff-)L9A(y#Tc3i^%yS^pwa z@3QZ*@3QZ*@3QZ*@3QZ*@3QZ*@3QZ*@3QZ*@3QZ*@3QZ*@3QZ*@3QZ*@3QZ*@3QZ* z@3QZ*@0xp;eV2XL7KFO&yX?E{yX?E{yX?E{yX?E{yX?E{yX?E{yX?E{yX?E{yX?E{ zyX?E{yX?E{yX?E{yX?E{yVkzTzRSKF*!v6M5ZHIwciDH@ciDH@ciDH@ciDH@ciDH@ zciDH@ciDH@ciDH@ciDH@ciDH@ciDH@ciDH@ciDH@ciDH@TeBxmwV`Kvt{2+WOTE%- zZRrzzs?W5o&-I1A)Q-N=*ZM}g`jvjIZ`FT%`8WD!{fqjKIhy(b`vdj|><`!jb z!2W>!0s8~?2kZ~nAFw}Qf585L{Q>&}_6O#1!2W>!0s8~?2kZ~nAFw}Qf585L{Q>&} z_I_pW=cD>rf#Dw=us>jbz~0YR`0)Yz1NH~(57-~DKVW~r{($`f`vdj|><`!N_ z2kZ~nAFw}Q?>@xe9k4%Of56_?obSf>(})dk0UQGR1NH~(57-~DKVW~r{($`f`vdj| z><`!jb!2W>!0s8~%dBFaF{Q>&}_6O|Uhgdyef585Ly{|dnjeo%Y>C@ophMwuU zUT9M<^-8a`rBC#!KGU{7*BAOyJNimr>l^LrSNgTSRYAYeKkHvq)W6Eqd+dAcd+dAc zd+dAcd+dAcd+dAcd+dAcd+dAcd+dAcd+dAcd+dAcd+dAcd+dAcd+dAcd+dAcd+dAG zzQ?|23qn2iJ@!5JJ@!5JJ@!5JJ@!5JJ@!5JJ@!5JJ@!5JJ@!5JJ@!5JJ@!5JJ@!5J zJ@!5JJ@!5JJ@!3o-}7g$7ufp?;1JmN*!S4?*!S4?*!S4?*!S4?*!S4?*!S4?*!S4? z*!S4?*!S4?*!S4?*!S4?*!S4?*!S4?*!TRY@3G(52sWPSxn5{fFZD{VwWUw=sXo)T zKGzreQak!eU+Wv~>R0-;zEwfL(Ld{7RMfxff5_Ai*&nh$WPiy1ko_V1L-vR457{5G zKV;wk`K|q*@zVcyVEg}8O#d}ShwS_JP4>Tj9kM@Uf5`rj{UQ59_J`~b*&nh$WPiy1 z53hCd%y&N%IOOMphJSR({*e74dp}R*$A|0>*&nh$WPiy1ko_V1L-vR457{5GKV*N% z{*e74`$P7J><`&{+~9YI><`%=viE(4zjOVsJNEtpI0W{G><`%=vOi>h$o`Q1A^Su2 zhwKm8AF@AWf5`rj{UQ59_J`~b*&nh$WPiy1ki7>Ds)y_k*&nj^O~$@2INy!`>{;;a zxn5{fFZD{VwWUw=sXo)TKGzreQak!eU+Wv~>R0-;zEwfL(Ld{7RMfxff9SXR{{Say B3Bv#Y literal 0 HcmV?d00001 diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 0ad496135..5398664b8 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -44,6 +44,7 @@ def test_questionable() -> None: "pal8os2sp.bmp", "pal8rletrns.bmp", "rgb32bf-xbgr.bmp", + "rgb32h52.bmp", ] for f in get_files("q"): try: diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 6643ac39b..dd8c06f4d 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -53,7 +53,7 @@ def _accept(prefix: bytes) -> bool: def _dib_accept(prefix): - return i32(prefix) in [12, 40, 64, 108, 124] + return i32(prefix) in [12, 40, 52, 64, 108, 124] # ============================================================================= @@ -95,7 +95,7 @@ class BmpImageFile(ImageFile.ImageFile): # --------------------------------------------- Windows Bitmap v2 to v5 # v3, OS/2 v2, v4, v5 - elif file_info["header_size"] in (40, 64, 108, 124): + elif file_info["header_size"] in (40, 52, 64, 108, 124): file_info["y_flip"] = header_data[7] == 0xFF file_info["direction"] = 1 if file_info["y_flip"] else -1 file_info["width"] = i32(header_data, 0) @@ -117,10 +117,13 @@ class BmpImageFile(ImageFile.ImageFile): file_info["palette_padding"] = 4 self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) if file_info["compression"] == self.BITFIELDS: - if len(header_data) >= 52: - for idx, mask in enumerate( - ["r_mask", "g_mask", "b_mask", "a_mask"] - ): + if len(header_data) >= 48: + masks = ["r_mask", "g_mask", "b_mask"] + if len(header_data) >= 52: + masks.append("a_mask") + else: + file_info["a_mask"] = 0x0 + for idx, mask in enumerate(masks): file_info[mask] = i32(header_data, 36 + idx * 4) else: # 40 byte headers only have the three components in the From a1a2202ebec15c6ba4c77e0eaf94bb698ab81530 Mon Sep 17 00:00:00 2001 From: Jonah Jeleniewski Date: Mon, 8 Apr 2024 21:16:39 +1000 Subject: [PATCH 032/195] Add support for bitmaps with header size 56 Size 56 is the undocumented `BITMAPV3INFOHEADER`. It adds the alpha bit mask. The format is known to be supported by: - Windows (MS paint, etc.) - Adobe Photoshop - Popular web browsers --- Tests/images/bmp/q/rgba32h56.bmp | Bin 0 -> 32582 bytes Tests/test_bmp_reference.py | 1 + src/PIL/BmpImagePlugin.py | 4 ++-- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 Tests/images/bmp/q/rgba32h56.bmp diff --git a/Tests/images/bmp/q/rgba32h56.bmp b/Tests/images/bmp/q/rgba32h56.bmp new file mode 100644 index 0000000000000000000000000000000000000000..343baa3300f331aaeabeb40d8fcd2f232393e815 GIT binary patch literal 32582 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 None: "pal8rletrns.bmp", "rgb32bf-xbgr.bmp", "rgb32h52.bmp", + "rgba32h56.bmp", ] for f in get_files("q"): try: diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index dd8c06f4d..82a9c47db 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -53,7 +53,7 @@ def _accept(prefix: bytes) -> bool: def _dib_accept(prefix): - return i32(prefix) in [12, 40, 52, 64, 108, 124] + return i32(prefix) in [12, 40, 52, 56, 64, 108, 124] # ============================================================================= @@ -95,7 +95,7 @@ class BmpImageFile(ImageFile.ImageFile): # --------------------------------------------- Windows Bitmap v2 to v5 # v3, OS/2 v2, v4, v5 - elif file_info["header_size"] in (40, 52, 64, 108, 124): + elif file_info["header_size"] in (40, 52, 56, 64, 108, 124): file_info["y_flip"] = header_data[7] == 0xFF file_info["direction"] = 1 if file_info["y_flip"] else -1 file_info["width"] = i32(header_data, 0) From 9e5b9fb18fdb2fe957346424432163cdb5fe3b00 Mon Sep 17 00:00:00 2001 From: Jonah Jeleniewski Date: Mon, 8 Apr 2024 21:33:24 +1000 Subject: [PATCH 033/195] Improve comments around bitmap info headers --- src/PIL/BmpImagePlugin.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 82a9c47db..ddc6b3fcc 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -83,8 +83,9 @@ class BmpImageFile(ImageFile.ImageFile): # read the rest of the bmp header, without its size header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4) - # -------------------------------------------------- IBM OS/2 Bitmap v1 + # ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1 # ----- This format has different offsets because of width/height types + # 12: BITMAPCOREHEADER/OS21XBITMAPHEADER if file_info["header_size"] == 12: file_info["width"] = i16(header_data, 0) file_info["height"] = i16(header_data, 2) @@ -93,8 +94,13 @@ class BmpImageFile(ImageFile.ImageFile): file_info["compression"] = self.RAW file_info["palette_padding"] = 3 - # --------------------------------------------- Windows Bitmap v2 to v5 - # v3, OS/2 v2, v4, v5 + # --------------------------------------------- Windows Bitmap v3 to v5 + # 40: BITMAPINFOHEADER + # 52: BITMAPV2HEADER + # 56: BITMAPV3HEADER + # 64: BITMAPCOREHEADER2/OS22XBITMAPHEADER + # 108: BITMAPV4HEADER + # 124: BITMAPV5HEADER elif file_info["header_size"] in (40, 52, 56, 64, 108, 124): file_info["y_flip"] = header_data[7] == 0xFF file_info["direction"] = 1 if file_info["y_flip"] else -1 From 98ae91a65deb6ab0790c042ca1f0ebc478f88df0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 Apr 2024 23:58:03 +1000 Subject: [PATCH 034/195] Added BGXR and BGAR unpackers --- src/PIL/BmpImagePlugin.py | 4 ++++ src/libImaging/Unpack.c | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index ddc6b3fcc..fa337f6eb 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -184,9 +184,11 @@ class BmpImageFile(ImageFile.ImageFile): 32: [ (0xFF0000, 0xFF00, 0xFF, 0x0), (0xFF000000, 0xFF0000, 0xFF00, 0x0), + (0xFF000000, 0xFF00, 0xFF, 0x0), (0xFF000000, 0xFF0000, 0xFF00, 0xFF), (0xFF, 0xFF00, 0xFF0000, 0xFF000000), (0xFF0000, 0xFF00, 0xFF, 0xFF000000), + (0xFF000000, 0xFF00, 0xFF, 0xFF0000), (0x0, 0x0, 0x0, 0x0), ], 24: [(0xFF0000, 0xFF00, 0xFF)], @@ -195,9 +197,11 @@ class BmpImageFile(ImageFile.ImageFile): MASK_MODES = { (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX", (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR", + (32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR", (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR", (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA", + (32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR", (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", (24, (0xFF0000, 0xFF00, 0xFF)): "BGR", (16, (0xF800, 0x7E0, 0x1F)): "BGR;16", diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 6c7d52f58..a84dc0a6f 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -790,6 +790,17 @@ ImagingUnpackBGRX(UINT8 *_out, const UINT8 *in, int pixels) { } } +static void +ImagingUnpackBGXR(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[3], in[1], in[0], 255); + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + static void ImagingUnpackXRGB(UINT8 *_out, const UINT8 *in, int pixels) { int i; @@ -1090,6 +1101,17 @@ unpackBGRA16B(UINT8 *_out, const UINT8 *in, int pixels) { } } +static void +unpackBGAR(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[3], in[1], in[0], in[2]); + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + /* Unpack to "CMYK" image */ static void @@ -1584,6 +1606,7 @@ static struct { {"RGB", "RGBA;L", 32, unpackRGBAL}, {"RGB", "RGBA;15", 16, ImagingUnpackRGBA15}, {"RGB", "BGRX", 32, ImagingUnpackBGRX}, + {"RGB", "BGXR", 32, ImagingUnpackBGXR}, {"RGB", "XRGB", 32, ImagingUnpackXRGB}, {"RGB", "XBGR", 32, ImagingUnpackXBGR}, {"RGB", "YCC;P", 24, ImagingUnpackYCC}, @@ -1624,6 +1647,7 @@ static struct { {"RGBA", "BGRA", 32, unpackBGRA}, {"RGBA", "BGRA;16L", 64, unpackBGRA16L}, {"RGBA", "BGRA;16B", 64, unpackBGRA16B}, + {"RGBA", "BGAR", 32, unpackBGAR}, {"RGBA", "ARGB", 32, unpackARGB}, {"RGBA", "ABGR", 32, unpackABGR}, {"RGBA", "YCCA;P", 32, ImagingUnpackYCCA}, From 40504bb49007974658d19e32aa62cc93b9160fd8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 8 Apr 2024 22:15:51 +0300 Subject: [PATCH 035/195] No more eggs --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index ad0a1adab..5547b2e3c 100644 --- a/Makefile +++ b/Makefile @@ -78,7 +78,6 @@ release-test: python3 selftest.py python3 -m pytest Tests python3 -m pip install . - -rm dist/*.egg -rmdir dist python3 -m pytest -qq python3 -m check_manifest From bf463c25dfa75ef715470e65206eedde087323d2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 8 Apr 2024 22:48:08 +0300 Subject: [PATCH 036/195] Inline the 'Source and Binary Distributions' step --- RELEASING.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index b013d8288..9e6ec5dd4 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -20,8 +20,10 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. git tag 5.2.0 git push --tags ``` -* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) -* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) +* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) + has passed, including the "Upload release to PyPI" job. This will have been triggered + by the new tag. +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases). * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: ```bash @@ -50,7 +52,9 @@ Released as needed for security, installation or critical bug fixes. ```bash make sdist ``` -* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) +* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) + has passed, including the "Upload release to PyPI" job. This will have been triggered + by the new tag. * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: ```bash git push @@ -72,18 +76,14 @@ Released as needed privately to individual vendors for critical security-related git tag 2.5.3 git push origin --tags ``` -* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) +* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) + has passed, including the "Upload release to PyPI" job. This will have been triggered + by the new tag. * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: ```bash git push origin 2.5.x ``` -## Source and Binary Distributions - -* [ ] 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. - ## Publicize Release * [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321 From 3a92d4af0162d80bc13ae15efbc2e47483870d3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Apr 2024 07:34:52 +1000 Subject: [PATCH 037/195] Replace ImageMath.eval with ImageMath.lambda_eval --- selftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/selftest.py b/selftest.py index ed5252c44..661abcddb 100755 --- a/selftest.py +++ b/selftest.py @@ -139,7 +139,9 @@ def testimage() -> None: In 1.1.6, you can use the ImageMath module to do image calculations. - >>> im = ImageMath.eval("float(im + 20)", im=im.convert("L")) + >>> im = ImageMath.lambda_eval( \ + lambda args: args["float"](args["im"] + 20), im=im.convert("L") \ + ) >>> im.mode, im.size ('F', (128, 128)) From 4b4cdbd40c865cd932329f1ace42f0d42ce1cb71 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Apr 2024 19:01:34 +1000 Subject: [PATCH 038/195] Added image to supported list --- Tests/test_bmp_reference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index a5a4283e3..7f8487921 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -44,6 +44,7 @@ def test_questionable() -> None: "pal8os2sp.bmp", "pal8rletrns.bmp", "rgb32bf-xbgr.bmp", + "rgba32.bmp", "rgb32h52.bmp", "rgba32h56.bmp", ] From e2a57263c72cd0e55822b78aaf1faa246a8d6895 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Apr 2024 19:17:20 +1000 Subject: [PATCH 039/195] Reduced duplicate code --- src/PIL/BmpImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index fa337f6eb..9ce0fed88 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -123,8 +123,8 @@ class BmpImageFile(ImageFile.ImageFile): file_info["palette_padding"] = 4 self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) if file_info["compression"] == self.BITFIELDS: + masks = ["r_mask", "g_mask", "b_mask"] if len(header_data) >= 48: - masks = ["r_mask", "g_mask", "b_mask"] if len(header_data) >= 52: masks.append("a_mask") else: @@ -141,7 +141,7 @@ class BmpImageFile(ImageFile.ImageFile): # location, but it is listed as a reserved component, # and it is not generally an alpha channel file_info["a_mask"] = 0x0 - for mask in ["r_mask", "g_mask", "b_mask"]: + for mask in masks: file_info[mask] = i32(read(4)) file_info["rgb_mask"] = ( file_info["r_mask"], From 94fe670c0bc9ad88c8d7d55491cb1c55f7ed14dc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Apr 2024 21:50:17 +1000 Subject: [PATCH 040/195] Test DIB header size --- Tests/test_file_bmp.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 1eaff0c7d..c7c9b24e7 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -5,7 +5,7 @@ from pathlib import Path import pytest -from PIL import BmpImagePlugin, Image +from PIL import BmpImagePlugin, Image, _binary from .helper import ( assert_image_equal, @@ -128,6 +128,29 @@ def test_load_dib() -> None: assert_image_equal_tofile(im, "Tests/images/clipboard_target.png") +@pytest.mark.parametrize( + "header_size, path", + ( + (12, "g/pal8os2.bmp"), + (40, "g/pal1.bmp"), + (52, "q/rgb32h52.bmp"), + (56, "q/rgba32h56.bmp"), + (64, "q/pal8os2v2.bmp"), + (108, "g/pal8v4.bmp"), + (124, "g/pal8v5.bmp"), + ), +) +def test_dib_header_size(header_size, path): + image_path = "Tests/images/bmp/" + path + with open(image_path, "rb") as fp: + data = fp.read()[14:] + assert _binary.i32le(data) == header_size + + dib = io.BytesIO(data) + with Image.open(dib) as im: + im.load() + + def test_save_dib(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.dib") From 82b7b8a9ee1c474f1f78132d4de61e80c2f434a8 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 11 Apr 2024 00:29:31 -0500 Subject: [PATCH 041/195] Fix some comments --- src/libImaging/Pack.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index d47344245..f3b714215 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -339,7 +339,7 @@ ImagingPackXBGR(UINT8 *out, const UINT8 *in, int pixels) { void ImagingPackBGRA(UINT8 *out, const UINT8 *in, int pixels) { int i; - /* BGRX, reversed bytes with right padding */ + /* BGRA, reversed bytes with right alpha */ for (i = 0; i < pixels; i++) { out[0] = in[B]; out[1] = in[G]; @@ -353,7 +353,7 @@ ImagingPackBGRA(UINT8 *out, const UINT8 *in, int pixels) { void ImagingPackABGR(UINT8 *out, const UINT8 *in, int pixels) { int i; - /* XBGR, reversed bytes with left padding */ + /* ABGR, reversed bytes with left alpha */ for (i = 0; i < pixels; i++) { out[0] = in[A]; out[1] = in[B]; From c1f6abbd3746d8332b6f9aae69f1fd262a246257 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 11 Apr 2024 14:07:07 -0500 Subject: [PATCH 042/195] Fix test error message grammar --- Tests/test_image_resample.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index dbe193808..9b3bdf330 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -284,7 +284,7 @@ class TestCoreResampleAlphaCorrect: used_colors = {px[x, y][0] for x in range(i.size[0])} assert 256 == len(used_colors), ( "All colors should be present in resized image. " - f"Only {len(used_colors)} on {y} line." + f"Only {len(used_colors)} on line {y}." ) @pytest.mark.xfail(reason="Current implementation isn't precise enough") From 7b9a276c7f2dcba699bb2d9c7530f70bb499fa00 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Apr 2024 13:47:52 +1000 Subject: [PATCH 043/195] Updated libwebp to 1.4.0 --- .github/workflows/wheels-dependencies.sh | 2 +- depends/install_webp.sh | 2 +- winbuild/build_prepare.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0d45d5a20..0cf5c58ab 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -33,7 +33,7 @@ if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then else ZLIB_VERSION=1.2.8 fi -LIBWEBP_VERSION=1.3.2 +LIBWEBP_VERSION=1.4.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.16.1 BROTLI_VERSION=1.1.0 diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 6f867ab37..c47fb35f1 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.3.2 +archive=libwebp-1.4.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0d6da7754..7ff645fc9 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -117,7 +117,7 @@ V = { "JPEGTURBO": "3.0.2", "LCMS2": "2.16", "LIBPNG": "1.6.43", - "LIBWEBP": "1.3.2", + "LIBWEBP": "1.4.0", "OPENJPEG": "2.5.2", "TIFF": "4.6.0", "XZ": "5.4.5", From 77e2c38aea9d88adb3e3f6a9dd8bc56f85b2a075 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Apr 2024 15:54:42 +1000 Subject: [PATCH 044/195] dist directory is no longer created --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 5547b2e3c..477d92609 100644 --- a/Makefile +++ b/Makefile @@ -78,7 +78,6 @@ release-test: python3 selftest.py python3 -m pytest Tests python3 -m pip install . - -rmdir dist python3 -m pytest -qq python3 -m check_manifest python3 -m pyroma . From e58cccfc2322ccba038af77d48a269681867a277 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Apr 2024 16:28:29 +1000 Subject: [PATCH 045/195] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e39031aea..196f8ed20 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.4.0 (unreleased) ------------------- +- Add support for reading BITMAPV2INFOHEADER and BITMAPV3INFOHEADER #7956 + [Cirras, radarhere] + - Support reading CMYK JPEG2000 images #7947 [radarhere] From 71029803e74c2148567a1a2dda7f8e0e3c49d27c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 14 Apr 2024 21:57:29 +1000 Subject: [PATCH 046/195] Corrected check for libtiff feature --- Tests/test_tiff_ifdrational.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index f6adae3e6..7cbc1a266 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -3,10 +3,12 @@ from __future__ import annotations from fractions import Fraction from pathlib import Path -from PIL import Image, TiffImagePlugin, features +import pytest + +from PIL import Image, TiffImagePlugin from PIL.TiffImagePlugin import IFDRational -from .helper import hopper +from .helper import hopper, skip_unless_feature def _test_equal(num, denom, target) -> None: @@ -52,18 +54,17 @@ def test_nonetype() -> None: assert xres and yres -def test_ifd_rational_save(tmp_path: Path) -> None: - methods = [True] - if features.check("libtiff"): - methods.append(False) +@pytest.mark.parametrize( + "libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False) +) +def test_ifd_rational_save(tmp_path: Path, libtiff: bool) -> None: + im = hopper() + out = str(tmp_path / "temp.tiff") + res = IFDRational(301, 1) - for libtiff in methods: - TiffImagePlugin.WRITE_LIBTIFF = libtiff + TiffImagePlugin.WRITE_LIBTIFF = libtiff + im.save(out, dpi=(res, res), compression="raw") + TiffImagePlugin.WRITE_LIBTIFF = False - im = hopper() - out = str(tmp_path / "temp.tiff") - res = IFDRational(301, 1) - im.save(out, dpi=(res, res), compression="raw") - - with Image.open(out) as reloaded: - assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) + with Image.open(out) as reloaded: + assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) From 3e1df0afeb13041b132939b28d53860093e5f3d6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 14 Apr 2024 22:28:28 +1000 Subject: [PATCH 047/195] Removed CentOS 7 --- .github/workflows/test-docker.yml | 1 - docs/installation/platform-support.rst | 2 -- 2 files changed, 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index f40286fe4..4b8041f9d 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -43,7 +43,6 @@ jobs: amazon-2-amd64, amazon-2023-amd64, arch, - centos-7-amd64, centos-stream-8-amd64, centos-stream-9-amd64, debian-11-bullseye-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index bc60eeed4..5b52105d2 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -25,8 +25,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Arch | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| CentOS 7 | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | CentOS Stream 8 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 3.9 | x86-64 | From 25b46523246a7908959615ccab547f3b9345dd62 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 14 Apr 2024 22:45:14 +1000 Subject: [PATCH 048/195] Removed CentOS Stream 8 --- .github/workflows/test-docker.yml | 1 - docs/installation/platform-support.rst | 2 -- 2 files changed, 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 4b8041f9d..70426d7b5 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -43,7 +43,6 @@ jobs: amazon-2-amd64, amazon-2023-amd64, arch, - centos-stream-8-amd64, centos-stream-9-amd64, debian-11-bullseye-amd64, debian-12-bookworm-x86, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 5b52105d2..af205a4e8 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -25,8 +25,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Arch | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| CentOS Stream 8 | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86-64 | From d431c97ba3b19d40d4090763ca25499536287141 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Apr 2024 19:28:52 +1000 Subject: [PATCH 049/195] Deprecate BGR;15, BGR;16 and BGR;24 --- Tests/helper.py | 7 ++++++- Tests/test_image.py | 30 +++++++++++++++++++++++++----- Tests/test_image_access.py | 6 +++++- Tests/test_image_putdata.py | 3 ++- Tests/test_lib_pack.py | 13 ++++++++----- docs/deprecations.rst | 7 +++++++ docs/handbook/concepts.rst | 3 --- src/PIL/Image.py | 7 +++++++ src/PIL/ImageMode.py | 4 ++++ 9 files changed, 64 insertions(+), 16 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index c1399e89b..213d99427 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -273,7 +273,12 @@ def _cached_hopper(mode: str) -> Image.Image: im = hopper("L") else: im = hopper() - return im.convert(mode) + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + im = im.convert(mode) + else: + im = im.convert(mode) + return im def djpeg_available() -> bool: diff --git a/Tests/test_image.py b/Tests/test_image.py index 941ec40d9..ed80be503 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -66,7 +66,11 @@ image_mode_names = [name for name, _ in image_modes] class TestImage: @pytest.mark.parametrize("mode", image_mode_names) def test_image_modes_success(self, mode: str) -> None: - Image.new(mode, (1, 1)) + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + Image.new(mode, (1, 1)) + else: + Image.new(mode, (1, 1)) @pytest.mark.parametrize("mode", ("", "bad", "very very long")) def test_image_modes_fail(self, mode: str) -> None: @@ -1050,7 +1054,11 @@ class TestImageBytes: im = hopper(mode) source_bytes = im.tobytes() - reloaded = Image.frombytes(mode, im.size, source_bytes) + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + reloaded = Image.frombytes(mode, im.size, source_bytes) + else: + reloaded = Image.frombytes(mode, im.size, source_bytes) assert reloaded.tobytes() == source_bytes @pytest.mark.parametrize("mode", image_mode_names) @@ -1058,17 +1066,29 @@ class TestImageBytes: im = hopper(mode) source_bytes = im.tobytes() - reloaded = Image.new(mode, im.size) + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + reloaded = Image.new(mode, im.size) + else: + reloaded = Image.new(mode, im.size) reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes @pytest.mark.parametrize(("mode", "pixelsize"), image_modes) def test_getdata_putdata(self, mode: str, pixelsize: int) -> None: - im = Image.new(mode, (2, 2)) + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + im = Image.new(mode, (2, 2)) + else: + im = Image.new(mode, (2, 2)) source_bytes = bytes(range(im.width * im.height * pixelsize)) im.frombytes(source_bytes) - reloaded = Image.new(mode, im.size) + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + reloaded = Image.new(mode, im.size) + else: + reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 8c42da57a..8bb90710a 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -229,7 +229,11 @@ class TestImageGetPixel(AccessTest): ), ) def test_basic(self, mode: str) -> None: - self.check(mode) + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + self.check(mode) + else: + self.check(mode) def test_list(self) -> None: im = hopper() diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 73145faac..dad26ef14 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -81,7 +81,8 @@ def test_mode_F() -> None: @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) def test_mode_BGR(mode: str) -> None: data = [(16, 32, 49), (32, 32, 98)] - im = Image.new(mode, (1, 2)) + with pytest.warns(DeprecationWarning): + im = Image.new(mode, (1, 2)) im.putdata(data) assert list(im.getdata()) == data diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 6a0e704b8..f80c5b78c 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -359,11 +359,14 @@ class TestLibUnpack: ) def test_BGR(self) -> None: - self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8)) - self.assert_unpack( - "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) - ) - self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + with pytest.warns(DeprecationWarning): + self.assert_unpack( + "BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8) + ) + self.assert_unpack( + "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) + ) + self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) def test_RGBA(self) -> None: self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index c3d1ba4f0..da4e9e597 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -100,6 +100,13 @@ ImageMath eval() ``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or :py:meth:`~PIL.ImageMath.unsafe_eval` instead. +BGR;15, BGR 16 and BGR;24 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 + +The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated. + Removed features ---------------- diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index e0975a121..5094dbf3f 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -59,9 +59,6 @@ Pillow also provides limited support for a few additional modes, including: * ``I;16L`` (16-bit little endian unsigned integer pixels) * ``I;16B`` (16-bit big endian unsigned integer pixels) * ``I;16N`` (16-bit native endian unsigned integer pixels) - * ``BGR;15`` (15-bit reversed true colour) - * ``BGR;16`` (16-bit reversed true colour) - * ``BGR;24`` (24-bit reversed true colour) Premultiplied alpha is where the values for each other channel have been multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)`` diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3ae901060..26be42779 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -55,6 +55,7 @@ from . import ( _plugins, ) from ._binary import i32le, o32be, o32le +from ._deprecate import deprecate from ._typing import StrOrBytesPath, TypeGuard from ._util import DeferredError, is_path @@ -939,6 +940,9 @@ class Image: :returns: An :py:class:`~PIL.Image.Image` object. """ + if mode in ("BGR;15", "BGR;16", "BGR;24"): + deprecate(mode, 12) + self.load() has_transparency = "transparency" in self.info @@ -2956,6 +2960,9 @@ def new( :returns: An :py:class:`~PIL.Image.Image` object. """ + if mode in ("BGR;15", "BGR;16", "BGR;24"): + deprecate(mode, 12) + _check_size(size) if color is None: diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 5e05c5f43..7bd2afcf2 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -18,6 +18,8 @@ import sys from functools import lru_cache from typing import NamedTuple +from ._deprecate import deprecate + class ModeDescriptor(NamedTuple): """Wrapper for mode strings.""" @@ -63,6 +65,8 @@ def getmode(mode: str) -> ModeDescriptor: "PA": ("RGB", "L", ("P", "A"), "|u1"), } if mode in modes: + if mode in ("BGR;15", "BGR;16", "BGR;24"): + deprecate(mode, 12) base_mode, base_type, bands, type_str = modes[mode] return ModeDescriptor(mode, bands, base_mode, base_type, type_str) From 66d32a7dffe1b859bbcc33a43a6a8ea4d1de7c4d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Apr 2024 07:03:56 +1000 Subject: [PATCH 050/195] Updated installation links --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 823ea76d0..b4c6d2987 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ The core image library is designed for fast access to data stored in a few basic ## More Information - [Documentation](https://pillow.readthedocs.io/) - - [Installation](https://pillow.readthedocs.io/en/latest/installation.html) + - [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html) - [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html) - [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md) - [Issues](https://github.com/python-pillow/Pillow/issues) diff --git a/setup.py b/setup.py index ac401dde7..7d8e1c1ee 100644 --- a/setup.py +++ b/setup.py @@ -1018,7 +1018,7 @@ The headers or library files could not be found for {str(err)}, a required dependency when compiling Pillow from source. Please see the install instructions at: - https://pillow.readthedocs.io/en/latest/installation.html + https://pillow.readthedocs.io/en/latest/installation/basic-installation.html """ sys.stderr.write(msg) From 1af66df732f1842494d4eb8470f81c4c1e94b5b9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Apr 2024 07:13:40 +1000 Subject: [PATCH 051/195] Updated xcb-proto to 1.17.0 --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0d45d5a20..2d5e174ce 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -70,7 +70,7 @@ function build { fi build_new_zlib - build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto + build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib From 712aa994f27aba19209e33be382f1a8c85ade82f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Apr 2024 07:14:04 +1000 Subject: [PATCH 052/195] Updated libxcb to 1.17.0 --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2d5e174ce..e140665fe 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -35,7 +35,7 @@ else fi LIBWEBP_VERSION=1.3.2 BZIP2_VERSION=1.0.8 -LIBXCB_VERSION=1.16.1 +LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then From c655dc0c6b6257ffecb7fa8464ac51d19347e651 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Tue, 16 Apr 2024 17:53:48 +0200 Subject: [PATCH 053/195] Use a property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/PIL/Image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b64133cbc..c65cf3850 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3079,7 +3079,9 @@ class SupportsArrayInterface(Protocol): An object that has an ``__array_interface__`` dictionary. """ - __array_interface__: dict[str, Any] + @property + def __array_interface__(self) -> dict[str, Any]: + raise NotImplementedError() def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: From 8b6253861766dbb0ac0aa896be1f1845e1e9a00f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Tue, 16 Apr 2024 18:27:48 +0200 Subject: [PATCH 054/195] ImageStat: simplify call to Image.histogram(mask) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/ImageStat.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index 07ea76e6f..2cb841eef 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -51,10 +51,7 @@ class Stat: :param mask: An optional mask. """ if isinstance(image_or_list, Image.Image): - if mask: - self.h = image_or_list.histogram(mask) - else: - self.h = image_or_list.histogram() + self.h = image_or_list.histogram(mask) else: self.h = image_or_list if not isinstance(self.h, list): From 2e73bed053a14e28ac4e8dd4f93db02a58630140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Tue, 16 Apr 2024 18:34:48 +0200 Subject: [PATCH 055/195] ImageStat: simplify if block Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageStat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index 2cb841eef..8bc504526 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -52,9 +52,9 @@ class Stat: """ if isinstance(image_or_list, Image.Image): self.h = image_or_list.histogram(mask) - else: + elif isinstance(image_or_list, list): self.h = image_or_list - if not isinstance(self.h, list): + else: msg = "first argument must be image or list" # type: ignore[unreachable] raise TypeError(msg) self.bands = list(range(len(self.h) // 256)) From cd179541b11277caed1168f0a2d7f0870eed6545 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Apr 2024 11:47:35 +1000 Subject: [PATCH 056/195] Removed nitpick_ignore by updating Sphinx to 7.3 --- docs/conf.py | 9 ++------- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 483535f96..8b879d1e4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ import PIL # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "2.4" +needs_sphinx = "7.3" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -121,12 +121,7 @@ nitpicky = True # generating warnings in “nitpicky mode”. Note that type should include the domain name # if present. Example entries would be ('py:func', 'int') or # ('envvar', 'LD_LIBRARY_PATH'). -nitpick_ignore = [ - # Sphinx does not understand typing.Literal[-1] - # Will be fixed in a future version. - # https://github.com/sphinx-doc/sphinx/pull/11904 - ("py:obj", "typing.Literal[-1, 1]"), -] +# nitpick_ignore = [] # -- Options for HTML output ---------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 3ce082fb9..328088e05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ dynamic = [ docs = [ "furo", "olefile", - "sphinx>=2.4", + "sphinx>=7.3", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", From 03835ce6f52cd51d232469025a66fe197f6efed8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Apr 2024 14:51:12 +1000 Subject: [PATCH 057/195] Corrected UnixViewer command --- 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 4e505f2ee..f60b1e11e 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -199,7 +199,7 @@ class UnixViewer(Viewer): def get_command(self, file: str, **options: Any) -> str: command = self.get_command_ex(file, **options)[0] - return f"({command} {quote(file)}" + return f"{command} {quote(file)}" class XDGViewer(UnixViewer): From a64f4cf68587b4adb375bb70933fcf3e3283b749 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:22:22 +0300 Subject: [PATCH 058/195] Remove sphinx-removed-in, now Sphinx 7.3.0 adds versionremoved --- docs/Makefile | 2 +- docs/conf.py | 1 - pyproject.toml | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 3b4deb9bf..6495e5866 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -46,7 +46,7 @@ clean: -rm -rf $(BUILDDIR)/* install-sphinx: - $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-removed-in sphinxext-opengraph + $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinxext-opengraph .PHONY: html html: diff --git a/docs/conf.py b/docs/conf.py index 8b879d1e4..392cf317e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,7 +34,6 @@ extensions = [ "sphinx.ext.viewcode", "sphinx_copybutton", "sphinx_inline_tabs", - "sphinx_removed_in", "sphinxext.opengraph", ] diff --git a/pyproject.toml b/pyproject.toml index 328088e05..20e87ad32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,6 @@ docs = [ "sphinx>=7.3", "sphinx-copybutton", "sphinx-inline-tabs", - "sphinx-removed-in", "sphinxext-opengraph", ] fpx = [ From 2c0b2dceba4f72694221f8a1acb2efb16e761047 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 Apr 2024 08:33:37 +1000 Subject: [PATCH 059/195] Updated nasm to 2.16.03 --- .appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 57a8fa5a0..dfa548548 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -32,10 +32,10 @@ install: - curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - 7z x pillow-test-images.zip -oc:\ - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images -- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip +- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip - 7z x nasm-win64.zip -oc:\ - choco install ghostscript --version=10.3.0 -- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH% +- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.00.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ From 28f436c94d21c445230c50bd5d16e898e12febd5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 Apr 2024 17:57:40 +1000 Subject: [PATCH 060/195] Use monkeypatch to set READ_LIBTIFF and WRITE_LIBTIFF --- Tests/test_file_libtiff.py | 100 +++++++++++++++------------------ Tests/test_imagesequence.py | 5 +- Tests/test_tiff_ifdrational.py | 7 ++- 3 files changed, 51 insertions(+), 61 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 6c32b5ad4..4db0bfed5 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -185,7 +185,9 @@ class TestFileLibTiff(LibTiffTestCase): assert field in reloaded, f"{field} not in metadata" @pytest.mark.valgrind_known_error(reason="Known invalid metadata") - def test_additional_metadata(self, tmp_path: Path) -> None: + def test_additional_metadata( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: # these should not crash. Seriously dummy data, most of it doesn't make # any sense, so we're running up against limits where we're asking # libtiff to do stupid things. @@ -236,12 +238,10 @@ class TestFileLibTiff(LibTiffTestCase): del new_ifd[338] out = str(tmp_path / "temp.tif") - TiffImagePlugin.WRITE_LIBTIFF = True + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out, tiffinfo=new_ifd) - TiffImagePlugin.WRITE_LIBTIFF = False - def test_custom_metadata(self, tmp_path: Path) -> None: class Tc(NamedTuple): value: Any @@ -343,24 +343,24 @@ class TestFileLibTiff(LibTiffTestCase): # Should not segfault im.save(outfile) - def test_xmlpacket_tag(self, tmp_path: Path) -> None: - TiffImagePlugin.WRITE_LIBTIFF = True + def test_xmlpacket_tag( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) out = str(tmp_path / "temp.tif") hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) - TiffImagePlugin.WRITE_LIBTIFF = False with Image.open(out) as reloaded: if 700 in reloaded.tag_v2: assert reloaded.tag_v2[700] == b"xmlpacket tag" - def test_int_dpi(self, tmp_path: Path) -> None: + def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: # issue #1765 im = hopper("RGB") out = str(tmp_path / "temp.tif") - TiffImagePlugin.WRITE_LIBTIFF = True + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out, dpi=(72, 72)) - TiffImagePlugin.WRITE_LIBTIFF = False with Image.open(out) as reloaded: assert reloaded.info["dpi"] == (72.0, 72.0) @@ -422,13 +422,13 @@ class TestFileLibTiff(LibTiffTestCase): assert "temp.tif" == reread.tag_v2[269] assert "temp.tif" == reread.tag[269][0] - def test_12bit_rawmode(self) -> None: + def test_12bit_rawmode(self, monkeypatch: pytest.MonkeyPatch) -> None: """Are we generating the same interpretation of the image as Imagemagick is?""" - TiffImagePlugin.READ_LIBTIFF = True + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/12bit.cropped.tif") as im: im.load() - TiffImagePlugin.READ_LIBTIFF = False + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", False) # to make the target -- # convert 12bit.cropped.tif -depth 16 tmp.tif # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif @@ -514,12 +514,13 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, out) @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) - def test_palette_save(self, im: Image.Image, tmp_path: Path) -> None: + def test_palette_save( + self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: out = str(tmp_path / "temp.tif") - TiffImagePlugin.WRITE_LIBTIFF = True + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out) - TiffImagePlugin.WRITE_LIBTIFF = False with Image.open(out) as reloaded: # colormap/palette tag @@ -548,9 +549,9 @@ class TestFileLibTiff(LibTiffTestCase): with pytest.raises(OSError): os.close(fn) - def test_multipage(self) -> None: + def test_multipage(self, monkeypatch: pytest.MonkeyPatch) -> None: # issue #862 - TiffImagePlugin.READ_LIBTIFF = True + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/multipage.tiff") as im: # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue @@ -569,11 +570,9 @@ class TestFileLibTiff(LibTiffTestCase): assert im.size == (20, 20) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) - TiffImagePlugin.READ_LIBTIFF = False - - def test_multipage_nframes(self) -> None: + def test_multipage_nframes(self, monkeypatch: pytest.MonkeyPatch) -> None: # issue #862 - TiffImagePlugin.READ_LIBTIFF = True + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/multipage.tiff") as im: frames = im.n_frames assert frames == 3 @@ -582,10 +581,8 @@ class TestFileLibTiff(LibTiffTestCase): # Should not raise ValueError: I/O operation on closed file im.load() - TiffImagePlugin.READ_LIBTIFF = False - - def test_multipage_seek_backwards(self) -> None: - TiffImagePlugin.READ_LIBTIFF = True + def test_multipage_seek_backwards(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/multipage.tiff") as im: im.seek(1) im.load() @@ -593,24 +590,21 @@ class TestFileLibTiff(LibTiffTestCase): im.seek(0) assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) - TiffImagePlugin.READ_LIBTIFF = False - - def test__next(self) -> None: - TiffImagePlugin.READ_LIBTIFF = True + def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/hopper.tif") as im: assert not im.tag.next im.load() assert not im.tag.next - def test_4bit(self) -> None: + def test_4bit(self, monkeypatch: pytest.MonkeyPatch) -> None: # Arrange test_file = "Tests/images/hopper_gray_4bpp.tif" original = hopper("L") # Act - TiffImagePlugin.READ_LIBTIFF = True + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open(test_file) as im: - TiffImagePlugin.READ_LIBTIFF = False # Assert assert im.size == (128, 128) @@ -650,12 +644,12 @@ class TestFileLibTiff(LibTiffTestCase): assert im2.mode == "L" assert_image_equal(im, im2) - def test_save_bytesio(self) -> None: + def test_save_bytesio(self, monkeypatch: pytest.MonkeyPatch) -> None: # PR 1011 # Test TIFF saving to io.BytesIO() object. - TiffImagePlugin.WRITE_LIBTIFF = True - TiffImagePlugin.READ_LIBTIFF = True + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) # Generate test image pilim = hopper() @@ -672,9 +666,6 @@ class TestFileLibTiff(LibTiffTestCase): save_bytesio("packbits") save_bytesio("tiff_lzw") - TiffImagePlugin.WRITE_LIBTIFF = False - TiffImagePlugin.READ_LIBTIFF = False - def test_save_ycbcr(self, tmp_path: Path) -> None: im = hopper("YCbCr") outfile = str(tmp_path / "temp.tif") @@ -694,15 +685,16 @@ class TestFileLibTiff(LibTiffTestCase): if Image.core.libtiff_support_custom_tags: assert reloaded.tag_v2[34665] == 125456 - def test_crashing_metadata(self, tmp_path: Path) -> None: + def test_crashing_metadata( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: # issue 1597 with Image.open("Tests/images/rdf.tif") as im: out = str(tmp_path / "temp.tif") - TiffImagePlugin.WRITE_LIBTIFF = True + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) # this shouldn't crash im.save(out, format="TIFF") - TiffImagePlugin.WRITE_LIBTIFF = False def test_page_number_x_0(self, tmp_path: Path) -> None: # Issue 973 @@ -733,20 +725,19 @@ class TestFileLibTiff(LibTiffTestCase): # Should not raise PermissionError. os.remove(tmpfile) - def test_read_icc(self) -> None: + def test_read_icc(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/hopper.iccprofile.tif") as img: icc = img.info.get("icc_profile") assert icc is not None - TiffImagePlugin.READ_LIBTIFF = True + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/hopper.iccprofile.tif") as img: icc_libtiff = img.info.get("icc_profile") assert icc_libtiff is not None - TiffImagePlugin.READ_LIBTIFF = False assert icc == icc_libtiff - def test_write_icc(self, tmp_path: Path) -> None: + def test_write_icc(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: def check_write(libtiff: bool) -> None: - TiffImagePlugin.WRITE_LIBTIFF = libtiff + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff) with Image.open("Tests/images/hopper.iccprofile.tif") as img: icc_profile = img.info["icc_profile"] @@ -756,10 +747,9 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as reloaded: assert icc_profile == reloaded.info["icc_profile"] - libtiffs = [] + libtiffs = [False] if Image.core.libtiff_support_custom_tags: libtiffs.append(True) - libtiffs.append(False) for libtiff in libtiffs: check_write(libtiff) @@ -840,12 +830,13 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB") - def test_sampleformat_write(self, tmp_path: Path) -> None: + def test_sampleformat_write( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: im = Image.new("F", (1, 1)) out = str(tmp_path / "temp.tif") - TiffImagePlugin.WRITE_LIBTIFF = True + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out) - TiffImagePlugin.WRITE_LIBTIFF = False with Image.open(out) as reloaded: assert reloaded.mode == "F" @@ -1091,15 +1082,14 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as im: im.load() - def test_realloc_overflow(self) -> None: - TiffImagePlugin.READ_LIBTIFF = True + def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: with pytest.raises(OSError) as e: im.load() # Assert that the error code is IMAGING_CODEC_MEMORY assert str(e.value) == "-9" - TiffImagePlugin.READ_LIBTIFF = False @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 7f3a3d141..18070377f 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -60,10 +60,9 @@ def test_tiff() -> None: @skip_unless_feature("libtiff") -def test_libtiff() -> None: - TiffImagePlugin.READ_LIBTIFF = True +def test_libtiff(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) _test_multipage_tiff() - TiffImagePlugin.READ_LIBTIFF = False def test_consecutive() -> None: diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 7cbc1a266..ae80b98b8 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -57,14 +57,15 @@ def test_nonetype() -> None: @pytest.mark.parametrize( "libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False) ) -def test_ifd_rational_save(tmp_path: Path, libtiff: bool) -> None: +def test_ifd_rational_save( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool +) -> None: im = hopper() out = str(tmp_path / "temp.tiff") res = IFDRational(301, 1) - TiffImagePlugin.WRITE_LIBTIFF = libtiff + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff) im.save(out, dpi=(res, res), compression="raw") - TiffImagePlugin.WRITE_LIBTIFF = False with Image.open(out) as reloaded: assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) From 533f78e0a255c77357e2694419d6b0f5ea6bcd05 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 19 Apr 2024 07:47:14 +1000 Subject: [PATCH 061/195] Parametrize test --- Tests/test_file_libtiff.py | 39 ++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 4db0bfed5..71f1b6f1d 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -735,24 +735,31 @@ class TestFileLibTiff(LibTiffTestCase): assert icc_libtiff is not None assert icc == icc_libtiff - def test_write_icc(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: - def check_write(libtiff: bool) -> None: - monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff) + @pytest.mark.parametrize( + "libtiff", + ( + pytest.param( + True, + marks=pytest.mark.skipif( + not Image.core.libtiff_support_custom_tags, + reason="Custom tags not supported by older libtiff", + ), + ), + False, + ), + ) + def test_write_icc( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool + ) -> None: + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff) - with Image.open("Tests/images/hopper.iccprofile.tif") as img: - icc_profile = img.info["icc_profile"] + with Image.open("Tests/images/hopper.iccprofile.tif") as img: + icc_profile = img.info["icc_profile"] - out = str(tmp_path / "temp.tif") - img.save(out, icc_profile=icc_profile) - with Image.open(out) as reloaded: - assert icc_profile == reloaded.info["icc_profile"] - - libtiffs = [False] - if Image.core.libtiff_support_custom_tags: - libtiffs.append(True) - - for libtiff in libtiffs: - check_write(libtiff) + out = str(tmp_path / "temp.tif") + img.save(out, icc_profile=icc_profile) + with Image.open(out) as reloaded: + assert icc_profile == reloaded.info["icc_profile"] def test_multipage_compression(self) -> None: with Image.open("Tests/images/compression.tif") as im: From 11ac0c1703cb9f46f9de9ac692f008255222d7b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 19 Apr 2024 17:15:10 +1000 Subject: [PATCH 062/195] Combine tests through parametrization --- Tests/test_imagesequence.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 18070377f..9b37435eb 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -47,7 +47,11 @@ def test_iterator_min_frame() -> None: assert i[index] == next(i) -def _test_multipage_tiff() -> None: +@pytest.mark.parametrize( + "libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False) +) +def test_multipage_tiff(monkeypatch: pytest.MonkeyPatch, libtiff: bool) -> None: + monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", libtiff) with Image.open("Tests/images/multipage.tiff") as im: for index, frame in enumerate(ImageSequence.Iterator(im)): frame.load() @@ -55,16 +59,6 @@ def _test_multipage_tiff() -> None: frame.convert("RGB") -def test_tiff() -> None: - _test_multipage_tiff() - - -@skip_unless_feature("libtiff") -def test_libtiff(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) - _test_multipage_tiff() - - def test_consecutive() -> None: with Image.open("Tests/images/multipage.tiff") as im: first_frame = None From 139245a3db00cf4cc6de4ed726eba4561dcd5cec Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 27 Mar 2024 10:29:19 -0500 Subject: [PATCH 063/195] use namedtuple for image mode info --- Tests/test_image.py | 67 ++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 941ec40d9..779785c53 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -8,7 +8,7 @@ import sys import tempfile import warnings from pathlib import Path -from typing import IO +from typing import IO, NamedTuple import pytest @@ -33,34 +33,39 @@ from .helper import ( skip_unless_feature, ) -# name, pixel size + +class ImageModeInfo(NamedTuple): + name: str + pixel_size: int + + image_modes = ( - ("1", 1), - ("L", 1), - ("LA", 4), - ("La", 4), - ("P", 1), - ("PA", 4), - ("F", 4), - ("I", 4), - ("I;16", 2), - ("I;16L", 2), - ("I;16B", 2), - ("I;16N", 2), - ("RGB", 4), - ("RGBA", 4), - ("RGBa", 4), - ("RGBX", 4), - ("BGR;15", 2), - ("BGR;16", 2), - ("BGR;24", 3), - ("CMYK", 4), - ("YCbCr", 4), - ("HSV", 4), - ("LAB", 4), + ImageModeInfo("1", 1), + ImageModeInfo("L", 1), + ImageModeInfo("LA", 4), + ImageModeInfo("La", 4), + ImageModeInfo("P", 1), + ImageModeInfo("PA", 4), + ImageModeInfo("F", 4), + ImageModeInfo("I", 4), + ImageModeInfo("I;16", 2), + ImageModeInfo("I;16L", 2), + ImageModeInfo("I;16B", 2), + ImageModeInfo("I;16N", 2), + ImageModeInfo("RGB", 4), + ImageModeInfo("RGBA", 4), + ImageModeInfo("RGBa", 4), + ImageModeInfo("RGBX", 4), + ImageModeInfo("BGR;15", 2), + ImageModeInfo("BGR;16", 2), + ImageModeInfo("BGR;24", 3), + ImageModeInfo("CMYK", 4), + ImageModeInfo("YCbCr", 4), + ImageModeInfo("HSV", 4), + ImageModeInfo("LAB", 4), ) -image_mode_names = [name for name, _ in image_modes] +image_mode_names = [mode.name for mode in image_modes] class TestImage: @@ -1062,13 +1067,13 @@ class TestImageBytes: reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize(("mode", "pixelsize"), image_modes) - def test_getdata_putdata(self, mode: str, pixelsize: int) -> None: - im = Image.new(mode, (2, 2)) - source_bytes = bytes(range(im.width * im.height * pixelsize)) + @pytest.mark.parametrize("mode", image_modes) + def test_getdata_putdata(self, mode: ImageModeInfo) -> None: + im = Image.new(mode.name, (2, 2)) + source_bytes = bytes(range(im.width * im.height * mode.pixel_size)) im.frombytes(source_bytes) - reloaded = Image.new(mode, im.size) + reloaded = Image.new(mode.name, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) From 5a4b771fb00389a43dbf32a4c13d73c9b39e3f5d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 2 Jan 2023 16:55:31 -0600 Subject: [PATCH 064/195] move image mode info variables to helper.py --- Tests/helper.py | 36 +++++++++++++++++++++++++++++++++++- Tests/test_image.py | 39 ++++----------------------------------- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index c1399e89b..32ea99fdd 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -13,7 +13,7 @@ import sysconfig import tempfile from functools import lru_cache from io import BytesIO -from typing import Any, Callable, Sequence +from typing import Any, Callable, NamedTuple, Sequence import pytest from packaging.version import parse as parse_version @@ -29,6 +29,40 @@ elif "GITHUB_ACTIONS" in os.environ: uploader = "github_actions" +class ImageModeInfo(NamedTuple): + name: str + pixel_size: int + + +image_modes = ( + ImageModeInfo("1", 1), + ImageModeInfo("L", 1), + ImageModeInfo("LA", 4), + ImageModeInfo("La", 4), + ImageModeInfo("P", 1), + ImageModeInfo("PA", 4), + ImageModeInfo("F", 4), + ImageModeInfo("I", 4), + ImageModeInfo("I;16", 2), + ImageModeInfo("I;16L", 2), + ImageModeInfo("I;16B", 2), + ImageModeInfo("I;16N", 2), + ImageModeInfo("RGB", 4), + ImageModeInfo("RGBA", 4), + ImageModeInfo("RGBa", 4), + ImageModeInfo("RGBX", 4), + ImageModeInfo("BGR;15", 2), + ImageModeInfo("BGR;16", 2), + ImageModeInfo("BGR;24", 3), + ImageModeInfo("CMYK", 4), + ImageModeInfo("YCbCr", 4), + ImageModeInfo("HSV", 4), + ImageModeInfo("LAB", 4), +) + +image_mode_names = [mode.name for mode in image_modes] + + def upload(a: Image.Image, b: Image.Image) -> str | None: if uploader == "show": # local img.show for errors. diff --git a/Tests/test_image.py b/Tests/test_image.py index 779785c53..5654307f1 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -8,7 +8,7 @@ import sys import tempfile import warnings from pathlib import Path -from typing import IO, NamedTuple +from typing import IO import pytest @@ -23,51 +23,20 @@ from PIL import ( ) from .helper import ( + ImageModeInfo, assert_image_equal, assert_image_equal_tofile, assert_image_similar_tofile, assert_not_all_same, hopper, + image_mode_names, + image_modes, is_win32, mark_if_feature_version, skip_unless_feature, ) -class ImageModeInfo(NamedTuple): - name: str - pixel_size: int - - -image_modes = ( - ImageModeInfo("1", 1), - ImageModeInfo("L", 1), - ImageModeInfo("LA", 4), - ImageModeInfo("La", 4), - ImageModeInfo("P", 1), - ImageModeInfo("PA", 4), - ImageModeInfo("F", 4), - ImageModeInfo("I", 4), - ImageModeInfo("I;16", 2), - ImageModeInfo("I;16L", 2), - ImageModeInfo("I;16B", 2), - ImageModeInfo("I;16N", 2), - ImageModeInfo("RGB", 4), - ImageModeInfo("RGBA", 4), - ImageModeInfo("RGBa", 4), - ImageModeInfo("RGBX", 4), - ImageModeInfo("BGR;15", 2), - ImageModeInfo("BGR;16", 2), - ImageModeInfo("BGR;24", 3), - ImageModeInfo("CMYK", 4), - ImageModeInfo("YCbCr", 4), - ImageModeInfo("HSV", 4), - ImageModeInfo("LAB", 4), -) - -image_mode_names = [mode.name for mode in image_modes] - - class TestImage: @pytest.mark.parametrize("mode", image_mode_names) def test_image_modes_success(self, mode: str) -> None: From 0fed6a5fbcbe40aff7540693a438d18d825c839e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 31 Mar 2024 23:29:01 -0500 Subject: [PATCH 065/195] use common image mode list for TestImageGetPixel tests --- Tests/test_image_access.py | 38 ++++++++------------------------------ 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 8c42da57a..96afc87a4 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -10,7 +10,7 @@ import pytest from PIL import Image -from .helper import assert_image_equal, hopper, is_win32 +from .helper import assert_image_equal, hopper, image_mode_names, is_win32 # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -138,8 +138,8 @@ class TestImageGetPixel(AccessTest): if bands == 1: return 1 if mode in ("BGR;15", "BGR;16"): - # These modes have less than 8 bits per band - # So (1, 2, 3) cannot be roundtripped + # These modes have less than 8 bits per band, + # so (1, 2, 3) cannot be roundtripped. return (16, 32, 49) return tuple(range(1, bands + 1)) @@ -168,16 +168,15 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # Check 0 + # check 0x0 image with None initial color im = Image.new(mode, (0, 0), None) assert im.load() is not None - error = ValueError if self._need_cffi_access else IndexError with pytest.raises(error): im.putpixel((0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) - # Check 0 negative index + # check negative index with pytest.raises(error): im.putpixel((-1, -1), expected_color) with pytest.raises(error): @@ -198,36 +197,15 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # Check 0 + # check 0x0 image with initial color im = Image.new(mode, (0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) - # Check 0 negative index + # check negative index with pytest.raises(error): im.getpixel((-1, -1)) - @pytest.mark.parametrize( - "mode", - ( - "1", - "L", - "LA", - "I", - "I;16", - "I;16B", - "F", - "P", - "PA", - "BGR;15", - "BGR;16", - "BGR;24", - "RGB", - "RGBA", - "RGBX", - "CMYK", - "YCbCr", - ), - ) + @pytest.mark.parametrize("mode", image_mode_names) def test_basic(self, mode: str) -> None: self.check(mode) From da7198c98740e9607dac0bf97ae073cf21510634 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 30 Mar 2024 14:32:45 -0500 Subject: [PATCH 066/195] fix ImagingAccess for I;16N on big-endian --- src/libImaging/Access.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 091c84e18..04618df09 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -81,12 +81,6 @@ get_pixel_16B(Imaging im, int x, int y, void *color) { #endif } -static void -get_pixel_16(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image[y][x + x]; - memcpy(color, in, sizeof(UINT16)); -} - static void get_pixel_BGR15(Imaging im, int x, int y, void *color) { UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; @@ -207,7 +201,11 @@ ImagingAccessInit() { ADD("I;16", get_pixel_16L, put_pixel_16L); ADD("I;16L", get_pixel_16L, put_pixel_16L); ADD("I;16B", get_pixel_16B, put_pixel_16B); - ADD("I;16N", get_pixel_16, put_pixel_16L); +#ifdef WORDS_BIGENDIAN + ADD("I;16N", get_pixel_16B, put_pixel_16B); +#else + ADD("I;16N", get_pixel_16L, put_pixel_16L); +#endif ADD("I;32L", get_pixel_32L, put_pixel_32L); ADD("I;32B", get_pixel_32B, put_pixel_32B); ADD("F", get_pixel_32, put_pixel_32); From 5dabc6cf14c78d397d2b31d81d6a7fe9e10dbd3b Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 30 Mar 2024 15:10:04 -0500 Subject: [PATCH 067/195] fix I;16N lib pack test --- Tests/test_lib_pack.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 6a0e704b8..f34ff7d02 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -216,7 +216,10 @@ class TestLibPack: ) def test_I16(self) -> None: - self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) + if sys.byteorder == "little": + self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) + else: + self.assert_pack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) def test_F_float(self) -> None: self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) From fe79ae5653e42e5b5c42ff5428eeef36e4bbe7a6 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 6 Apr 2024 10:48:38 -0500 Subject: [PATCH 068/195] get pixel size by counting bytes in 1x1 image --- Tests/helper.py | 57 ++++++++++++++++++++------------------------- Tests/test_image.py | 19 +++++++++------ 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 32ea99fdd..f0bb8af00 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -13,7 +13,7 @@ import sysconfig import tempfile from functools import lru_cache from io import BytesIO -from typing import Any, Callable, NamedTuple, Sequence +from typing import Any, Callable, Sequence import pytest from packaging.version import parse as parse_version @@ -29,39 +29,32 @@ elif "GITHUB_ACTIONS" in os.environ: uploader = "github_actions" -class ImageModeInfo(NamedTuple): - name: str - pixel_size: int - - -image_modes = ( - ImageModeInfo("1", 1), - ImageModeInfo("L", 1), - ImageModeInfo("LA", 4), - ImageModeInfo("La", 4), - ImageModeInfo("P", 1), - ImageModeInfo("PA", 4), - ImageModeInfo("F", 4), - ImageModeInfo("I", 4), - ImageModeInfo("I;16", 2), - ImageModeInfo("I;16L", 2), - ImageModeInfo("I;16B", 2), - ImageModeInfo("I;16N", 2), - ImageModeInfo("RGB", 4), - ImageModeInfo("RGBA", 4), - ImageModeInfo("RGBa", 4), - ImageModeInfo("RGBX", 4), - ImageModeInfo("BGR;15", 2), - ImageModeInfo("BGR;16", 2), - ImageModeInfo("BGR;24", 3), - ImageModeInfo("CMYK", 4), - ImageModeInfo("YCbCr", 4), - ImageModeInfo("HSV", 4), - ImageModeInfo("LAB", 4), +image_mode_names = ( + "1", + "L", + "LA", + "La", + "P", + "PA", + "F", + "I", + "I;16", + "I;16L", + "I;16B", + "I;16N", + "RGB", + "RGBA", + "RGBa", + "RGBX", + "BGR;15", + "BGR;16", + "BGR;24", + "CMYK", + "YCbCr", + "HSV", + "LAB", ) -image_mode_names = [mode.name for mode in image_modes] - def upload(a: Image.Image, b: Image.Image) -> str | None: if uploader == "show": diff --git a/Tests/test_image.py b/Tests/test_image.py index 5654307f1..8b20de0a9 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -23,14 +23,12 @@ from PIL import ( ) from .helper import ( - ImageModeInfo, assert_image_equal, assert_image_equal_tofile, assert_image_similar_tofile, assert_not_all_same, hopper, image_mode_names, - image_modes, is_win32, mark_if_feature_version, skip_unless_feature, @@ -1036,13 +1034,20 @@ class TestImageBytes: reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", image_modes) - def test_getdata_putdata(self, mode: ImageModeInfo) -> None: - im = Image.new(mode.name, (2, 2)) - source_bytes = bytes(range(im.width * im.height * mode.pixel_size)) + @pytest.mark.parametrize("mode", image_mode_names) + def test_getdata_putdata(self, mode: str) -> None: + # create an image with 1 pixel to get its pixel size + im = Image.new(mode, (1, 1)) + pixel_size = len(im.tobytes()) + + # create a new image with incrementing byte values + im = Image.new(mode, (2, 2)) + source_bytes = bytes(range(im.width * im.height * pixel_size)) im.frombytes(source_bytes) - reloaded = Image.new(mode.name, im.size) + # copy the data from the previous image to a new image + # and check that they are the same + reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) From 5573ec74901945c58e700b2becb34ac800607bbe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Apr 2024 22:54:47 +1000 Subject: [PATCH 069/195] use hopper() for test_getdata_putdata() --- Tests/test_image.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 8b20de0a9..090b80f87 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1036,17 +1036,7 @@ class TestImageBytes: @pytest.mark.parametrize("mode", image_mode_names) def test_getdata_putdata(self, mode: str) -> None: - # create an image with 1 pixel to get its pixel size - im = Image.new(mode, (1, 1)) - pixel_size = len(im.tobytes()) - - # create a new image with incrementing byte values - im = Image.new(mode, (2, 2)) - source_bytes = bytes(range(im.width * im.height * pixel_size)) - im.frombytes(source_bytes) - - # copy the data from the previous image to a new image - # and check that they are the same + im = hopper(mode) reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) From 5c960d6abc25aa94044a0831743e061383d3f486 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Apr 2024 23:01:51 +1000 Subject: [PATCH 070/195] rename "image_mode_names" to "modes" --- Tests/helper.py | 2 +- Tests/test_image.py | 10 +++++----- Tests/test_image_access.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index f0bb8af00..5d206f644 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -29,7 +29,7 @@ elif "GITHUB_ACTIONS" in os.environ: uploader = "github_actions" -image_mode_names = ( +modes = ( "1", "L", "LA", diff --git a/Tests/test_image.py b/Tests/test_image.py index 090b80f87..9985f2346 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -28,15 +28,15 @@ from .helper import ( assert_image_similar_tofile, assert_not_all_same, hopper, - image_mode_names, is_win32, mark_if_feature_version, + modes, skip_unless_feature, ) class TestImage: - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_image_modes_success(self, mode: str) -> None: Image.new(mode, (1, 1)) @@ -1017,7 +1017,7 @@ class TestImage: class TestImageBytes: - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_roundtrip_bytes_constructor(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() @@ -1025,7 +1025,7 @@ class TestImageBytes: reloaded = Image.frombytes(mode, im.size, source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_roundtrip_bytes_method(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() @@ -1034,7 +1034,7 @@ class TestImageBytes: reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_getdata_putdata(self, mode: str) -> None: im = hopper(mode) reloaded = Image.new(mode, im.size) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 96afc87a4..50afb2a23 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -10,7 +10,7 @@ import pytest from PIL import Image -from .helper import assert_image_equal, hopper, image_mode_names, is_win32 +from .helper import assert_image_equal, hopper, is_win32, modes # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -205,7 +205,7 @@ class TestImageGetPixel(AccessTest): with pytest.raises(error): im.getpixel((-1, -1)) - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_basic(self, mode: str) -> None: self.check(mode) From 98510570e6a54398c7975f1128b3de91fd8aad1f Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 20 Apr 2024 09:19:20 -0500 Subject: [PATCH 071/195] ignore BGR;15/16 test failure on big-endian --- Tests/test_image.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/test_image.py b/Tests/test_image.py index 9985f2346..4a056df40 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -28,6 +28,7 @@ from .helper import ( assert_image_similar_tofile, assert_not_all_same, hopper, + is_big_endian, is_win32, mark_if_feature_version, modes, @@ -1036,6 +1037,8 @@ class TestImageBytes: @pytest.mark.parametrize("mode", modes) def test_getdata_putdata(self, mode: str) -> None: + if is_big_endian and mode in ("BGR;15", "BGR;16"): + pytest.xfail(f"Known failure of {mode} on big-endian") im = hopper(mode) reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) From bb2411dd01197d0a393dbcfdccfdd487a8c6d7be Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Apr 2024 08:11:45 +1000 Subject: [PATCH 072/195] Support reading P mode TIFF images with padding --- src/PIL/TiffImagePlugin.py | 1 + src/libImaging/Unpack.c | 1 + 2 files changed, 2 insertions(+) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 10ac9ea3a..106f09c2d 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -244,6 +244,7 @@ OPEN_INFO = { (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), (II, 3, (1,), 1, (8,), ()): ("P", "P"), (MM, 3, (1,), 1, (8,), ()): ("P", "P"), + (II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index a84dc0a6f..e351aa2f1 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1582,6 +1582,7 @@ static struct { {"P", "P", 8, copy1}, {"P", "P;R", 8, unpackLR}, {"P", "L", 8, copy1}, + {"P", "PX", 16, unpackL16B}, /* palette w. alpha */ {"PA", "PA", 16, unpackLA}, From d5c1ff4b43b0b17d2379939cf941b06e3b0c3464 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Apr 2024 22:22:25 +1000 Subject: [PATCH 073/195] Removed type hint ignores --- 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 4af1b79e2..5f5c5df54 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -838,8 +838,8 @@ def getProfileName(profile: _CmsProfileCompatible) -> str: if not (model or manufacturer): return (profile.profile.profile_description or "") + "\n" - if not manufacturer or len(model) > 30: # type: ignore[arg-type] - return model + "\n" # type: ignore[operator] + if not manufacturer or (model and len(model) > 30): + return f"{model}\n" return f"{model} - {manufacturer}\n" except (AttributeError, OSError, TypeError, ValueError) as v: From 4171435db45822f6ebf696e36a7edfacdb1e6756 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Apr 2024 18:25:03 +1000 Subject: [PATCH 074/195] Added more modes --- src/PIL/Image.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c65cf3850..8efaf8b78 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -248,7 +248,28 @@ def _conv_type_shape(im): return shape, m.typestr -MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"] +MODES = [ + "1", + "CMYK", + "F", + "HSV", + "I", + "I;16", + "I;16B", + "I;16L", + "I;16N", + "L", + "LA", + "La", + "LAB", + "P", + "PA", + "RGB", + "RGBA", + "RGBa", + "RGBX", + "YCbCr", +] # raw modes that may be memory mapped. NOTE: if you change this, you # may have to modify the stride calculation in map.c too! From 745eb23a87b40707af9474ad732fc6d13b94628f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Apr 2024 22:22:40 +1000 Subject: [PATCH 075/195] Use LAB hopper file if conversion is not supported --- Tests/helper.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/helper.py b/Tests/helper.py index c1399e89b..680825d4b 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -273,7 +273,14 @@ def _cached_hopper(mode: str) -> Image.Image: im = hopper("L") else: im = hopper() - return im.convert(mode) + try: + im = im.convert(mode) + except ImportError: + if mode == "LAB": + im = Image.open("Tests/images/hopper.Lab.tif") + else: + raise + return im def djpeg_available() -> bool: From f690b7f6915ed0cfee0b1b4246741d17aa2e70b1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Apr 2024 13:39:35 +1000 Subject: [PATCH 076/195] Added MPEG accept function --- Tests/test_file_mpeg.py | 39 ++++++++++++++++++++++++++++++++++++++ src/PIL/MpegImagePlugin.py | 6 +++++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 Tests/test_file_mpeg.py diff --git a/Tests/test_file_mpeg.py b/Tests/test_file_mpeg.py new file mode 100644 index 000000000..468aef8a9 --- /dev/null +++ b/Tests/test_file_mpeg.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from io import BytesIO + +import pytest + +from PIL import Image, MpegImagePlugin + + +def test_identify() -> None: + # Arrange + b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01") + + # Act + with Image.open(b) as im: + # Assert + assert im.format == "MPEG" + + assert im.mode == "RGB" + assert im.size == (16, 1) + + +def test_invalid_file() -> None: + # Arrange + invalid_file = "Tests/images/flower.jpg" + + # Act / Assert + with pytest.raises(SyntaxError): + MpegImagePlugin.MpegImageFile(invalid_file) + + +def test_load() -> None: + # Arrange + b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01") + + with Image.open(b) as im: + # Act / Assert: cannot load + with pytest.raises(OSError): + im.load() diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index 1565612f8..ad4d3e937 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -53,6 +53,10 @@ class BitStream: return v +def _accept(prefix: bytes) -> bool: + return prefix[:4] == b"\x00\x00\x01\xb3" + + ## # Image plugin for MPEG streams. This plugin can identify a stream, # but it cannot read it. @@ -77,7 +81,7 @@ class MpegImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # Registry stuff -Image.register_open(MpegImageFile.format, MpegImageFile) +Image.register_open(MpegImageFile.format, MpegImageFile, _accept) Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"]) From 023d017da00f8adb8de86719509145c405369ac8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Apr 2024 18:26:20 +1000 Subject: [PATCH 077/195] Deprecate libtiff < 4 --- docs/deprecations.rst | 8 ++++++++ src/PIL/TiffImagePlugin.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index c3d1ba4f0..91bc150b3 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -100,6 +100,14 @@ ImageMath eval() ``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or :py:meth:`~PIL.ImageMath.unsafe_eval` instead. +Support for libtiff earlier than 4 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 + +Support for libtiff earlier than 4 has been deprecated. Upgrade to a newer version of +libtiff instead. + Removed features ---------------- diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 10ac9ea3a..13bcf5d86 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -56,6 +56,7 @@ from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 +from ._deprecate import deprecate from .TiffTags import TYPES logger = logging.getLogger(__name__) @@ -276,6 +277,9 @@ PREFIXES = [ b"II\x2B\x00", # BigTIFF with little-endian byte order ] +if not getattr(Image.core, "libtiff_support_custom_tags", True): + deprecate("Support for libtiff earlier than 4", 12) + def _accept(prefix: bytes) -> bool: return prefix[:4] in PREFIXES From c7bb152ed94002a83d648d2a1b9703c2bf27e8e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Apr 2024 18:30:00 +1000 Subject: [PATCH 078/195] support_custom_tags attribute is not present if libtiff is not supported --- Tests/test_file_libtiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 71f1b6f1d..e1867cffb 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -741,7 +741,7 @@ class TestFileLibTiff(LibTiffTestCase): pytest.param( True, marks=pytest.mark.skipif( - not Image.core.libtiff_support_custom_tags, + not getattr(Image.core, "libtiff_support_custom_tags", False), reason="Custom tags not supported by older libtiff", ), ), From 0df8796e1910619741a36bb1a2d334bdb941e440 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Apr 2024 18:45:41 +1000 Subject: [PATCH 079/195] Parametrized test --- Tests/test_file_libtiff.py | 93 ++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index e1867cffb..11883ad24 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -242,7 +242,24 @@ class TestFileLibTiff(LibTiffTestCase): im.save(out, tiffinfo=new_ifd) - def test_custom_metadata(self, tmp_path: Path) -> None: + @pytest.mark.parametrize( + "libtiff", + ( + pytest.param( + True, + marks=pytest.mark.skipif( + not getattr(Image.core, "libtiff_support_custom_tags", False), + reason="Custom tags not supported by older libtiff", + ), + ), + False, + ), + ) + def test_custom_metadata( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool + ) -> None: + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff) + class Tc(NamedTuple): value: Any type: int @@ -281,53 +298,43 @@ class TestFileLibTiff(LibTiffTestCase): ) } - libtiffs = [False] - if Image.core.libtiff_support_custom_tags: - libtiffs.append(True) + def check_tags( + tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] + ) -> None: + im = hopper() - for libtiff in libtiffs: - TiffImagePlugin.WRITE_LIBTIFF = libtiff + out = str(tmp_path / "temp.tif") + im.save(out, tiffinfo=tiffinfo) - def check_tags( - tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] - ) -> None: - im = hopper() + with Image.open(out) as reloaded: + for tag, value in tiffinfo.items(): + reloaded_value = reloaded.tag_v2[tag] + if ( + isinstance(reloaded_value, TiffImagePlugin.IFDRational) + and libtiff + ): + # libtiff does not support real RATIONALS + assert round(abs(float(reloaded_value) - float(value)), 7) == 0 + continue - out = str(tmp_path / "temp.tif") - im.save(out, tiffinfo=tiffinfo) + assert reloaded_value == value - with Image.open(out) as reloaded: - for tag, value in tiffinfo.items(): - reloaded_value = reloaded.tag_v2[tag] - if ( - isinstance(reloaded_value, TiffImagePlugin.IFDRational) - and libtiff - ): - # libtiff does not support real RATIONALS - assert ( - round(abs(float(reloaded_value) - float(value)), 7) == 0 - ) - continue + # Test with types + ifd = TiffImagePlugin.ImageFileDirectory_v2() + for tag, tagdata in custom.items(): + ifd[tag] = tagdata.value + ifd.tagtype[tag] = tagdata.type + check_tags(ifd) - assert reloaded_value == value - - # Test with types - ifd = TiffImagePlugin.ImageFileDirectory_v2() - for tag, tagdata in custom.items(): - ifd[tag] = tagdata.value - ifd.tagtype[tag] = tagdata.type - check_tags(ifd) - - # Test without types. This only works for some types, int for example are - # always encoded as LONG and not SIGNED_LONG. - check_tags( - { - tag: tagdata.value - for tag, tagdata in custom.items() - if tagdata.supported_by_default - } - ) - TiffImagePlugin.WRITE_LIBTIFF = False + # Test without types. This only works for some types, int for example are + # always encoded as LONG and not SIGNED_LONG. + check_tags( + { + tag: tagdata.value + for tag, tagdata in custom.items() + if tagdata.supported_by_default + } + ) def test_osubfiletype(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") From e144e418791eb205765b74da793487a038675984 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 22 Apr 2024 19:14:23 +1000 Subject: [PATCH 080/195] Updated wording Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/deprecations.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 91bc150b3..7882ec5ee 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -100,13 +100,13 @@ ImageMath eval() ``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or :py:meth:`~PIL.ImageMath.unsafe_eval` instead. -Support for libtiff earlier than 4 +Support for LibTIFF earlier than 4 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 10.4.0 -Support for libtiff earlier than 4 has been deprecated. Upgrade to a newer version of -libtiff instead. +Support for LibTIFF earlier than version 4 has been deprecated. +Upgrade to a newer version of LibTIFF instead. Removed features ---------------- From 2e1d2b2029eefe7255c1c11f81a53f862a7861e1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Apr 2024 19:15:38 +1000 Subject: [PATCH 081/195] Updated deprecation message --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 13bcf5d86..82ac47647 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -278,7 +278,7 @@ PREFIXES = [ ] if not getattr(Image.core, "libtiff_support_custom_tags", True): - deprecate("Support for libtiff earlier than 4", 12) + deprecate("Support for LibTIFF earlier than version 4", 12) def _accept(prefix: bytes) -> bool: From 5a0a288dd048097d2ffa62fb2d5a24cf5727bbb9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Apr 2024 19:16:55 +1000 Subject: [PATCH 082/195] Added release notes --- docs/releasenotes/10.4.0.rst | 54 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 55 insertions(+) create mode 100644 docs/releasenotes/10.4.0.rst diff --git a/docs/releasenotes/10.4.0.rst b/docs/releasenotes/10.4.0.rst new file mode 100644 index 000000000..0c2926732 --- /dev/null +++ b/docs/releasenotes/10.4.0.rst @@ -0,0 +1,54 @@ +10.4.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +Support for LibTIFF earlier than 4 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support for LibTIFF earlier than version 4 has been deprecated. +Upgrade to a newer version of LibTIFF instead. + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 089d44b90..6ee5fb6c8 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.4.0 10.3.0 10.2.0 10.1.0 From d4a4b59ee39dcf2e8dddafebfecccaaf709ec8bb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:43:48 +0300 Subject: [PATCH 083/195] Sphinx extension to add dates to release notes Co-authored-by: Jason R. Coombs --- docs/conf.py | 1 + docs/dater.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 docs/dater.py diff --git a/docs/conf.py b/docs/conf.py index 392cf317e..f12b30e65 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,6 +28,7 @@ needs_sphinx = "7.3" # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "dater", "sphinx.ext.autodoc", "sphinx.ext.extlinks", "sphinx.ext.intersphinx", diff --git a/docs/dater.py b/docs/dater.py new file mode 100644 index 000000000..d9e583547 --- /dev/null +++ b/docs/dater.py @@ -0,0 +1,52 @@ +""" +Sphinx extension to add timestamps to release notes based on Git versions. + +Based on https://github.com/jaraco/rst.linker, with thanks to Jason R. Coombs. +""" + +from __future__ import annotations + +import datetime as dt +import os +import re +import subprocess +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from sphinx.application import Sphinx + +DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+") +VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") + + +def get_date_for(git_version: str) -> dt.datetime | None: + cmd = ["git", "log", "-1", "--format=%ai", git_version] + try: + with open(os.devnull, "w", encoding="utf-8") as devnull: + out = subprocess.check_output( + cmd, stderr=devnull, text=True, encoding="utf-8" + ) + ts = out.strip() + return dt.datetime.fromisoformat(ts) + except subprocess.CalledProcessError: + return None + + +def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None: + if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])): + old_title = m.group(1) + + if tag_datetime := get_date_for(old_title): + new_title = f"{old_title} ({tag_datetime:%Y-%m-%d})" + else: + new_title = f"{old_title} (unreleased)" + + new_underline = "-" * len(new_title) + + result = source[0].replace(m.group(0), f"{new_title}\n{new_underline}\n", 1) + source[0] = result + + +def setup(app: Sphinx) -> dict[str, bool]: + app.connect("source-read", add_date) + return {"parallel_read_safe": True} From 35003343386f3d2d40f179e0c18f96e0b58ce102 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:58:44 +0300 Subject: [PATCH 084/195] Fetch tags on Read the Docs --- .readthedocs.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 0c8f935d5..b83ba05b1 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -6,6 +6,10 @@ build: os: ubuntu-22.04 tools: python: "3" + jobs: + post_checkout: + - git remote add upstream https://github.com/python-pillow/Pillow.git # For forks + - git fetch upstream --tags python: install: From 7f6ad116d1213948b318423cfd58990ec1e85c07 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Apr 2024 08:02:42 +1000 Subject: [PATCH 085/195] Update CHANGES.rst [ci skip] --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 196f8ed20..85dc0b43c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,18 @@ Changelog (Pillow) 10.4.0 (unreleased) ------------------- +- Support reading P mode TIFF images with padding #7996 + [radarhere] + +- Deprecate support for libtiff < 4 #7998 + [radarhere, hugovk] + +- Corrected ImageShow UnixViewer command #7987 + [radarhere] + +- Use functools.cached_property in ImageStat #7952 + [nulano, hugovk, radarhere] + - Add support for reading BITMAPV2INFOHEADER and BITMAPV3INFOHEADER #7956 [Cirras, radarhere] From 4a4eb0f3eeb76f59c9c890bf74c91a910e98a3eb Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 23 Apr 2024 01:08:42 -0500 Subject: [PATCH 086/195] remove semicolon after function definition --- src/_imaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index 520e50793..9b521f552 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3737,7 +3737,7 @@ _getattr_unsafe_ptrs(ImagingObject *self, void *closure) { self->image->image32, "image", self->image->image); -}; +} static struct PyGetSetDef getsetters[] = { {"mode", (getter)_getattr_mode}, From b9307f08d14e499e75edf574ff15c9b5c1f62a7d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 23 Apr 2024 12:02:25 -0500 Subject: [PATCH 087/195] remove unused variable --- src/display.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/display.c b/src/display.c index ef2ff3754..6b66ddafb 100644 --- a/src/display.c +++ b/src/display.c @@ -427,7 +427,6 @@ error: PyObject * PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { - int clip; HANDLE handle = NULL; int size; void *data; From eee53ba6647b75bbecc56e98a6a99d7c2360d1ca Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 23 Apr 2024 13:06:22 -0500 Subject: [PATCH 088/195] extract band count check --- src/libImaging/Matrix.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index 182eb62a7..ec7f4d93e 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -24,11 +24,11 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { ImagingSectionCookie cookie; /* Assume there's enough data in the buffer */ - if (!im) { + if (!im || im->bands != 3) { return (Imaging)ImagingError_ModeError(); } - if (strcmp(mode, "L") == 0 && im->bands == 3) { + if (strcmp(mode, "L") == 0) { imOut = ImagingNewDirty("L", im->xsize, im->ysize); if (!imOut) { return NULL; @@ -47,7 +47,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { } ImagingSectionLeave(&cookie); - } else if (strlen(mode) == 3 && im->bands == 3) { + } else if (strlen(mode) == 3) { imOut = ImagingNewDirty(mode, im->xsize, im->ysize); if (!imOut) { return NULL; From 46b85e6ab4eb1a5f1781acca2fb085826b328c3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Apr 2024 11:02:56 +1000 Subject: [PATCH 089/195] Simplified code --- src/libImaging/Convert.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 7e60a960c..64840d08c 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -254,9 +254,8 @@ static void rgb2i16l(UINT8 *out_, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++, in += 4) { - UINT8 v = CLIP16(L24(in) >> 16); - *out_++ = v; - *out_++ = v >> 8; + *out_++ = L24(in) >> 16; + *out_++ = 0; } } @@ -264,9 +263,8 @@ static void rgb2i16b(UINT8 *out_, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++, in += 4) { - UINT8 v = CLIP16(L24(in) >> 16); - *out_++ = v >> 8; - *out_++ = v; + *out_++ = 0; + *out_++ = L24(in) >> 16; } } From 03627d92a739d31269de0af61714b45db04069de Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:23:44 +0300 Subject: [PATCH 090/195] GitHub Actions: Python 3.8 and 3.9 are on macos-13 but not macos-14 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 643273e58..4573fde90 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,9 +57,9 @@ jobs: - python-version: "3.10" PYTHONOPTIMIZE: 2 # M1 only available for 3.10+ - - os: "macos-latest" + - os: "macos-13" python-version: "3.9" - - os: "macos-latest" + - os: "macos-13" python-version: "3.8" exclude: - os: "macos-14" From 76c17a10f0563f2343c7dada04af217093d3ef67 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:24:23 +0300 Subject: [PATCH 091/195] GitHub Actions: macos-13 is Intel but macos-latest will be M1 --- .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 36bb54050..b2fbd3140 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -97,7 +97,7 @@ jobs: matrix: include: - name: "macOS x86_64" - os: macos-latest + os: macos-13 cibw_arch: x86_64 macosx_deployment_target: "10.10" - name: "macOS arm64" From ccf1efb3efb371818455289b8c04aa21f18d4c4d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Apr 2024 23:06:06 +1000 Subject: [PATCH 092/195] Use subprocess.DEVNULL --- docs/dater.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/dater.py b/docs/dater.py index d9e583547..04855956b 100644 --- a/docs/dater.py +++ b/docs/dater.py @@ -7,7 +7,6 @@ Based on https://github.com/jaraco/rst.linker, with thanks to Jason R. Coombs. from __future__ import annotations import datetime as dt -import os import re import subprocess from typing import TYPE_CHECKING @@ -22,11 +21,10 @@ VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") def get_date_for(git_version: str) -> dt.datetime | None: cmd = ["git", "log", "-1", "--format=%ai", git_version] try: - with open(os.devnull, "w", encoding="utf-8") as devnull: - out = subprocess.check_output( - cmd, stderr=devnull, text=True, encoding="utf-8" - ) - ts = out.strip() + out = subprocess.check_output( + cmd, stderr=subprocess.DEVNULL, text=True, encoding="utf-8" + ) + ts = out.strip() return dt.datetime.fromisoformat(ts) except subprocess.CalledProcessError: return None From 4af831e70c41441dbe0cdebf9e2cbd46ae71eb92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Apr 2024 23:45:25 +1000 Subject: [PATCH 093/195] Accept '.zlib-ng' suffix to zlib version --- Tests/test_features.py | 2 ++ Tests/test_file_png.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index 3a528a7c8..2d402ca91 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -37,6 +37,8 @@ def test_version() -> None: else: assert function(name) == version if name != "PIL": + if name == "zlib" and version is not None: + version = version.replace(".zlib-ng", "") assert version is None or re.search(r"\d+(\.\d+)*$", version) for module in features.modules: diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 30fb14c44..19462dcb5 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -85,7 +85,9 @@ class TestFilePng: def test_sanity(self, tmp_path: Path) -> None: # internal version number - assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib")) + assert re.search( + r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", features.version_codec("zlib") + ) test_file = str(tmp_path / "temp.png") From 03bcf03567ae934feaa737ee9128c19136cb7803 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Apr 2024 08:41:15 +1000 Subject: [PATCH 094/195] Removed Fedora 38 and added Fedora 40 --- .github/workflows/test-docker.yml | 2 +- docs/installation/platform-support.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 70426d7b5..8f4a4d090 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -47,8 +47,8 @@ jobs: debian-11-bullseye-amd64, debian-12-bookworm-x86, debian-12-bookworm-amd64, - fedora-38-amd64, fedora-39-amd64, + fedora-40-amd64, gentoo, ubuntu-20.04-focal-amd64, ubuntu-22.04-jammy-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index af205a4e8..c08a53a43 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -31,10 +31,10 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ -| Fedora 38 | 3.11 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 39 | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 40 | 3.12 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 12 Monterey | 3.8, 3.9 | x86-64 | From 02db41119018f313df060c254a22f44a95057e15 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Apr 2024 09:14:48 +1000 Subject: [PATCH 095/195] Added release notes --- docs/releasenotes/10.4.0.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/releasenotes/10.4.0.rst b/docs/releasenotes/10.4.0.rst index 0c2926732..3150bf4e0 100644 --- a/docs/releasenotes/10.4.0.rst +++ b/docs/releasenotes/10.4.0.rst @@ -23,6 +23,11 @@ TODO Deprecations ============ +BGR;15, BGR 16 and BGR;24 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated. + Support for LibTIFF earlier than 4 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 35ffbdc9cde567ae629ae01ddce3116a35f3483d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 01:38:43 +0000 Subject: [PATCH 096/195] Update dependency mypy to v1.10.0 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 6b0535fc1..a0dcb92d2 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1 +1 @@ -mypy==1.9.0 +mypy==1.10.0 From 5faebadd56ab3ec66139d123fcee54d3ff89ad30 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Apr 2024 11:06:04 +1000 Subject: [PATCH 097/195] BGR;16 does not fail on big-endian --- Tests/test_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 4a056df40..8e7e40c05 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1037,8 +1037,8 @@ class TestImageBytes: @pytest.mark.parametrize("mode", modes) def test_getdata_putdata(self, mode: str) -> None: - if is_big_endian and mode in ("BGR;15", "BGR;16"): - pytest.xfail(f"Known failure of {mode} on big-endian") + if is_big_endian and mode == "BGR;15": + pytest.xfail("Known failure of BGR;15 on big-endian") im = hopper(mode) reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) From bc35bf0c9e2b3c0a5702a21daa02d5982932f8d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Apr 2024 13:10:45 +1000 Subject: [PATCH 098/195] Use split instead of datetime --- docs/dater.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/dater.py b/docs/dater.py index 04855956b..f9fb0c1da 100644 --- a/docs/dater.py +++ b/docs/dater.py @@ -6,7 +6,6 @@ Based on https://github.com/jaraco/rst.linker, with thanks to Jason R. Coombs. from __future__ import annotations -import datetime as dt import re import subprocess from typing import TYPE_CHECKING @@ -18,24 +17,23 @@ DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+") VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") -def get_date_for(git_version: str) -> dt.datetime | None: +def get_date_for(git_version: str) -> str | None: cmd = ["git", "log", "-1", "--format=%ai", git_version] try: out = subprocess.check_output( cmd, stderr=subprocess.DEVNULL, text=True, encoding="utf-8" ) - ts = out.strip() - return dt.datetime.fromisoformat(ts) except subprocess.CalledProcessError: return None + return out.split()[0] def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None: if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])): old_title = m.group(1) - if tag_datetime := get_date_for(old_title): - new_title = f"{old_title} ({tag_datetime:%Y-%m-%d})" + if tag_date := get_date_for(old_title): + new_title = f"{old_title} ({tag_date})" else: new_title = f"{old_title} (unreleased)" From bbd5a87e6046bd0c0eb9ad105102a0d019277eb5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Apr 2024 16:16:33 +1000 Subject: [PATCH 099/195] Combined conditions --- src/_imagingcms.c | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index f18d55a57..63d78f84d 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -213,34 +213,37 @@ cms_transform_dealloc(CmsTransformObject *self) { static cmsUInt32Number findLCMStype(char *PILmode) { - if (strcmp(PILmode, "RGB") == 0) { + if ( + strcmp(PILmode, "RGB") == 0 || + strcmp(PILmode, "RGBA") == 0 || + strcmp(PILmode, "RGBX") == 0 + ) { return TYPE_RGBA_8; - } else if (strcmp(PILmode, "RGBA") == 0) { - return TYPE_RGBA_8; - } else if (strcmp(PILmode, "RGBX") == 0) { - return TYPE_RGBA_8; - } else if (strcmp(PILmode, "RGBA;16B") == 0) { + } + if (strcmp(PILmode, "RGBA;16B") == 0) { return TYPE_RGBA_16; - } else if (strcmp(PILmode, "CMYK") == 0) { + } + if (strcmp(PILmode, "CMYK") == 0) { return TYPE_CMYK_8; - } else if (strcmp(PILmode, "L") == 0) { - return TYPE_GRAY_8; - } else if (strcmp(PILmode, "L;16") == 0) { + } + if (strcmp(PILmode, "L;16") == 0) { return TYPE_GRAY_16; - } else if (strcmp(PILmode, "L;16B") == 0) { + } + if (strcmp(PILmode, "L;16B") == 0) { return TYPE_GRAY_16_SE; - } else if (strcmp(PILmode, "YCCA") == 0) { + } + if ( + strcmp(PILmode, "YCCA") == 0 || + strcmp(PILmode, "YCC") == 0 + ) { return TYPE_YCbCr_8; - } else if (strcmp(PILmode, "YCC") == 0) { - return TYPE_YCbCr_8; - } else if (strcmp(PILmode, "LAB") == 0) { + } + if (strcmp(PILmode, "LAB") == 0) { // LabX equivalent like ALab, but not reversed -- no #define in lcms2 return (COLORSPACE_SH(PT_LabV2) | CHANNELS_SH(3) | BYTES_SH(1) | EXTRA_SH(1)); } - else { - /* take a wild guess... */ - return TYPE_GRAY_8; - } + /* presume "L" by default */ + return TYPE_GRAY_8; } #define Cms_Min(a, b) ((a) < (b) ? (a) : (b)) From 1b1c825f7ba5ffe6b62fbffdad4d094f13e254e8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 25 Apr 2024 14:39:51 +0300 Subject: [PATCH 100/195] Add ClangFormat to pre-commit --- .pre-commit-config.yaml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 51625eb4c..f81f03da1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + rev: v0.4.1 hooks: - id: ruff args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.3.0 + rev: 24.4.1 hooks: - id: black @@ -23,13 +23,20 @@ repos: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v18.1.4 + hooks: + - id: clang-format + types: [c] + exclude: ^src/thirdparty/ + - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: - id: rst-backticks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable @@ -43,7 +50,7 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.28.1 + rev: 0.28.2 hooks: - id: check-github-workflows - id: check-readthedocs @@ -55,7 +62,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 1.7.0 + rev: 1.8.0 hooks: - id: pyproject-fmt From 617e7295a80f2067f4be2861c43fc9e2345455e5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 12:51:20 +0000 Subject: [PATCH 101/195] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/Tk/tkImaging.c | 19 +- src/_imaging.c | 54 +++--- src/_imagingcms.c | 16 +- src/_imagingft.c | 127 +++++++++----- src/encode.c | 10 +- src/libImaging/Access.c | 10 +- src/libImaging/BcnDecode.c | 24 +-- src/libImaging/BoxBlur.c | 14 +- src/libImaging/Convert.c | 26 +-- src/libImaging/Draw.c | 66 +++++-- src/libImaging/Filter.c | 9 +- src/libImaging/FliDecode.c | 5 +- src/libImaging/Geometry.c | 16 +- src/libImaging/GetBBox.c | 10 +- src/libImaging/Gif.h | 8 +- src/libImaging/GifEncode.c | 318 ++++++++++++++++++---------------- src/libImaging/Imaging.h | 20 ++- src/libImaging/ImagingUtils.h | 2 +- src/libImaging/Jpeg2KDecode.c | 10 +- src/libImaging/Jpeg2KEncode.c | 6 +- src/libImaging/JpegEncode.c | 12 +- src/libImaging/Paste.c | 9 +- src/libImaging/Point.c | 2 +- src/libImaging/Quant.c | 63 +++---- src/libImaging/QuantOctree.c | 4 +- src/libImaging/Reduce.c | 2 +- src/libImaging/Resample.c | 2 +- src/libImaging/SgiRleDecode.c | 9 +- src/libImaging/Storage.c | 5 +- src/libImaging/TiffDecode.c | 136 +++++++++------ src/libImaging/TiffDecode.h | 2 +- src/libImaging/Unpack.c | 49 +++--- src/path.c | 36 ++-- 33 files changed, 608 insertions(+), 493 deletions(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index bd3cafe95..ef1c00a94 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -128,14 +128,7 @@ PyImagingPhotoPut( block.pixelPtr = (unsigned char *)im->block; TK_PHOTO_PUT_BLOCK( - interp, - photo, - &block, - 0, - 0, - block.width, - block.height, - TK_PHOTO_COMPOSITE_SET); + interp, photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET); return TCL_OK; } @@ -287,7 +280,7 @@ load_tkinter_funcs(void) { * Return 0 for success, non-zero for failure. */ - HMODULE* hMods = NULL; + HMODULE *hMods = NULL; HANDLE hProcess; DWORD cbNeeded; unsigned int i; @@ -313,7 +306,7 @@ load_tkinter_funcs(void) { #endif return 1; } - if (!(hMods = (HMODULE*) malloc(cbNeeded))) { + if (!(hMods = (HMODULE *)malloc(cbNeeded))) { PyErr_NoMemory(); return 1; } @@ -345,7 +338,7 @@ load_tkinter_funcs(void) { } else if (found_tk == 0) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines"); } - return (int) ((found_tcl != 1) || (found_tk != 1)); + return (int)((found_tcl != 1) || (found_tk != 1)); } #else /* not Windows */ @@ -400,8 +393,8 @@ _func_loader(void *lib) { return 1; } return ( - (TK_PHOTO_PUT_BLOCK = - (Tk_PhotoPutBlock_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL); + (TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)_dfunc(lib, "Tk_PhotoPutBlock")) == + NULL); } int diff --git a/src/_imaging.c b/src/_imaging.c index 9b521f552..c565c21bb 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -110,7 +110,7 @@ #define B16(p, i) ((((int)p[(i)]) << 8) + p[(i) + 1]) #define L16(p, i) ((((int)p[(i) + 1]) << 8) + p[(i)]) -#define S16(v) ((v) < 32768 ? (v) : ((v)-65536)) +#define S16(v) ((v) < 32768 ? (v) : ((v) - 65536)) /* -------------------------------------------------------------------- */ /* OBJECT ADMINISTRATION */ @@ -533,7 +533,9 @@ getink(PyObject *color, Imaging im, char *ink) { /* unsigned integer, single layer */ if (rIsInt != 1) { if (tupleSize != 1) { - PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple"); + PyErr_SetString( + PyExc_TypeError, + "color must be int or single-element tuple"); return NULL; } else if (!PyArg_ParseTuple(color, "L", &r)) { return NULL; @@ -552,7 +554,9 @@ getink(PyObject *color, Imaging im, char *ink) { a = 255; if (im->bands == 2) { if (tupleSize != 1 && tupleSize != 2) { - PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or two elements"); + PyErr_SetString( + PyExc_TypeError, + "color must be int, or tuple of one or two elements"); return NULL; } else if (!PyArg_ParseTuple(color, "L|i", &r, &a)) { return NULL; @@ -560,7 +564,10 @@ getink(PyObject *color, Imaging im, char *ink) { g = b = r; } else { if (tupleSize != 3 && tupleSize != 4) { - PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one, three or four elements"); + PyErr_SetString( + PyExc_TypeError, + "color must be int, or tuple of one, three or four " + "elements"); return NULL; } else if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) { return NULL; @@ -599,7 +606,9 @@ getink(PyObject *color, Imaging im, char *ink) { g = (UINT8)(r >> 8); r = (UINT8)r; } else if (tupleSize != 3) { - PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or three elements"); + PyErr_SetString( + PyExc_TypeError, + "color must be int, or tuple of one or three elements"); return NULL; } else if (!PyArg_ParseTuple(color, "iiL", &b, &g, &r)) { return NULL; @@ -1537,14 +1546,14 @@ _putdata(ImagingObject *self, PyObject *args) { return NULL; } -#define set_value_to_item(seq, i) \ -op = PySequence_Fast_GET_ITEM(seq, i); \ -if (PySequence_Check(op)) { \ - PyErr_SetString(PyExc_TypeError, "sequence must be flattened"); \ - return NULL; \ -} else { \ - value = PyFloat_AsDouble(op); \ -} +#define set_value_to_item(seq, i) \ + op = PySequence_Fast_GET_ITEM(seq, i); \ + if (PySequence_Check(op)) { \ + PyErr_SetString(PyExc_TypeError, "sequence must be flattened"); \ + return NULL; \ + } else { \ + value = PyFloat_AsDouble(op); \ + } if (image->image8) { if (PyBytes_Check(data)) { unsigned char *p; @@ -1596,8 +1605,10 @@ if (PySequence_Check(op)) { \ value = value * scale + offset; } if (image->type == IMAGING_TYPE_SPECIAL) { - image->image8[y][x * 2 + (bigendian ? 1 : 0)] = CLIP8((int)value % 256); - image->image8[y][x * 2 + (bigendian ? 0 : 1)] = CLIP8((int)value >> 8); + image->image8[y][x * 2 + (bigendian ? 1 : 0)] = + CLIP8((int)value % 256); + image->image8[y][x * 2 + (bigendian ? 0 : 1)] = + CLIP8((int)value >> 8); } else { image->image8[y][x] = (UINT8)CLIP8(value); } @@ -1639,8 +1650,7 @@ if (PySequence_Check(op)) { \ for (i = x = y = 0; i < n; i++) { double value; set_value_to_item(seq, i); - IMAGING_PIXEL_INT32(image, x, y) = - (INT32)(value * scale + offset); + IMAGING_PIXEL_INT32(image, x, y) = (INT32)(value * scale + offset); if (++x >= (int)image->xsize) { x = 0, y++; } @@ -2785,8 +2795,8 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { glyph = &self->glyphs[text[i]]; if (i == 0 || text[i] != text[i - 1]) { ImagingDelete(bitmap); - bitmap = - ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); + bitmap = ImagingCrop( + self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); if (!bitmap) { goto failed; } @@ -3315,7 +3325,8 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { free(xy); - if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) < 0) { + if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) < + 0) { free(ixy); return NULL; } @@ -4411,7 +4422,8 @@ setup_module(PyObject *m) { PyModule_AddObject(m, "HAVE_XCB", have_xcb); PyObject *pillow_version = PyUnicode_FromString(version); - PyDict_SetItemString(d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None); + PyDict_SetItemString( + d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None); Py_XDECREF(pillow_version); return 0; diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 63d78f84d..ba8c81005 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -213,11 +213,8 @@ cms_transform_dealloc(CmsTransformObject *self) { static cmsUInt32Number findLCMStype(char *PILmode) { - if ( - strcmp(PILmode, "RGB") == 0 || - strcmp(PILmode, "RGBA") == 0 || - strcmp(PILmode, "RGBX") == 0 - ) { + if (strcmp(PILmode, "RGB") == 0 || strcmp(PILmode, "RGBA") == 0 || + strcmp(PILmode, "RGBX") == 0) { return TYPE_RGBA_8; } if (strcmp(PILmode, "RGBA;16B") == 0) { @@ -232,10 +229,7 @@ findLCMStype(char *PILmode) { if (strcmp(PILmode, "L;16B") == 0) { return TYPE_GRAY_16_SE; } - if ( - strcmp(PILmode, "YCCA") == 0 || - strcmp(PILmode, "YCC") == 0 - ) { + if (strcmp(PILmode, "YCCA") == 0 || strcmp(PILmode, "YCC") == 0) { return TYPE_YCbCr_8; } if (strcmp(PILmode, "LAB") == 0) { @@ -393,7 +387,7 @@ _buildTransform( Py_END_ALLOW_THREADS - if (!hTransform) { + if (!hTransform) { PyErr_SetString(PyExc_ValueError, "cannot build transform"); } @@ -427,7 +421,7 @@ _buildProofTransform( Py_END_ALLOW_THREADS - if (!hTransform) { + if (!hTransform) { PyErr_SetString(PyExc_ValueError, "cannot build proof transform"); } diff --git a/src/_imagingft.c b/src/_imagingft.c index 6e24fcf95..e83ddfec1 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -47,17 +47,17 @@ ; #ifdef HAVE_RAQM -# ifdef HAVE_RAQM_SYSTEM -# include -# else -# include "thirdparty/raqm/raqm.h" -# ifdef HAVE_FRIBIDI_SYSTEM -# include -# else -# include "thirdparty/fribidi-shim/fribidi.h" -# include -# endif -# endif +#ifdef HAVE_RAQM_SYSTEM +#include +#else +#include "thirdparty/raqm/raqm.h" +#ifdef HAVE_FRIBIDI_SYSTEM +#include +#else +#include "thirdparty/fribidi-shim/fribidi.h" +#include +#endif +#endif #endif static int have_raqm = 0; @@ -490,8 +490,7 @@ text_layout( size_t count; #ifdef HAVE_RAQM if (have_raqm && self->layout_engine == LAYOUT_RAQM) { - count = text_layout_raqm( - string, self, dir, features, lang, glyph_info); + count = text_layout_raqm(string, self, dir, features, lang, glyph_info); } else #endif { @@ -550,7 +549,17 @@ font_getlength(FontObject *self, PyObject *args) { } static int -bounding_box_and_anchors(FT_Face face, const char *anchor, int horizontal_dir, GlyphInfo *glyph_info, size_t count, int load_flags, int *width, int *height, int *x_offset, int *y_offset) { +bounding_box_and_anchors( + FT_Face face, + const char *anchor, + int horizontal_dir, + GlyphInfo *glyph_info, + size_t count, + int load_flags, + int *width, + int *height, + int *x_offset, + int *y_offset) { int position; /* pen position along primary axis, in 26.6 precision */ int advanced; /* pen position along primary axis, in pixels */ int px, py; /* position of current glyph, in pixels */ @@ -558,8 +567,8 @@ bounding_box_and_anchors(FT_Face face, const char *anchor, int horizontal_dir, G int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ int error; FT_Glyph glyph; - FT_BBox bbox; /* glyph bounding box */ - size_t i; /* glyph_info index */ + FT_BBox bbox; /* glyph bounding box */ + size_t i; /* glyph_info index */ /* * text bounds are given by: * - bounding boxes of individual glyphs @@ -654,8 +663,7 @@ bounding_box_and_anchors(FT_Face face, const char *anchor, int horizontal_dir, G break; case 'm': // middle (ascender + descender) / 2 y_anchor = PIXEL( - (face->size->metrics.ascender + - face->size->metrics.descender) / + (face->size->metrics.ascender + face->size->metrics.descender) / 2); break; case 's': // horizontal baseline @@ -719,7 +727,7 @@ bad_anchor: static PyObject * font_getsize(FontObject *self, PyObject *args) { int width, height, x_offset, y_offset; - int load_flags; /* FreeType load_flags parameter */ + int load_flags; /* FreeType load_flags parameter */ int error; GlyphInfo *glyph_info = NULL; /* computed text layout */ size_t count; /* glyph_info length */ @@ -758,7 +766,17 @@ font_getsize(FontObject *self, PyObject *args) { load_flags |= FT_LOAD_COLOR; } - error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset); + error = bounding_box_and_anchors( + self->face, + anchor, + horizontal_dir, + glyph_info, + count, + load_flags, + &width, + &height, + &x_offset, + &y_offset); if (glyph_info) { PyMem_Free(glyph_info); glyph_info = NULL; @@ -767,12 +785,7 @@ font_getsize(FontObject *self, PyObject *args) { return NULL; } - return Py_BuildValue( - "(ii)(ii)", - width, - height, - x_offset, - y_offset); + return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset); } static PyObject * @@ -869,7 +882,17 @@ font_render(FontObject *self, PyObject *args) { horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset); + error = bounding_box_and_anchors( + self->face, + anchor, + horizontal_dir, + glyph_info, + count, + load_flags, + &width, + &height, + &x_offset, + &y_offset); if (error) { PyMem_Del(glyph_info); return NULL; @@ -1066,17 +1089,26 @@ font_render(FontObject *self, PyObject *args) { /* paste only if source has data */ if (src_alpha > 0) { /* unpremultiply BGRa */ - int src_red = CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha); - int src_green = CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha); - int src_blue = CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha); + int src_red = + CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha); + int src_green = + CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha); + int src_blue = + CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha); /* blend required if target has data */ if (target[k * 4 + 3] > 0) { /* blend RGBA colors */ - target[k * 4 + 0] = BLEND(src_alpha, target[k * 4 + 0], src_red, tmp); - target[k * 4 + 1] = BLEND(src_alpha, target[k * 4 + 1], src_green, tmp); - target[k * 4 + 2] = BLEND(src_alpha, target[k * 4 + 2], src_blue, tmp); - target[k * 4 + 3] = CLIP8(src_alpha + MULDIV255(target[k * 4 + 3], (255 - src_alpha), tmp)); + target[k * 4 + 0] = + BLEND(src_alpha, target[k * 4 + 0], src_red, tmp); + target[k * 4 + 1] = + BLEND(src_alpha, target[k * 4 + 1], src_green, tmp); + target[k * 4 + 2] = + BLEND(src_alpha, target[k * 4 + 2], src_blue, tmp); + target[k * 4 + 3] = CLIP8( + src_alpha + + MULDIV255( + target[k * 4 + 3], (255 - src_alpha), tmp)); } else { /* paste unpremultiplied RGBA values */ target[k * 4 + 0] = src_red; @@ -1093,10 +1125,16 @@ font_render(FontObject *self, PyObject *args) { unsigned int src_alpha = source[k] * convert_scale; if (src_alpha > 0) { if (target[k * 4 + 3] > 0) { - target[k * 4 + 0] = BLEND(src_alpha, target[k * 4 + 0], ink[0], tmp); - target[k * 4 + 1] = BLEND(src_alpha, target[k * 4 + 1], ink[1], tmp); - target[k * 4 + 2] = BLEND(src_alpha, target[k * 4 + 2], ink[2], tmp); - target[k * 4 + 3] = CLIP8(src_alpha + MULDIV255(target[k * 4 + 3], (255 - src_alpha), tmp)); + target[k * 4 + 0] = BLEND( + src_alpha, target[k * 4 + 0], ink[0], tmp); + target[k * 4 + 1] = BLEND( + src_alpha, target[k * 4 + 1], ink[1], tmp); + target[k * 4 + 2] = BLEND( + src_alpha, target[k * 4 + 2], ink[2], tmp); + target[k * 4 + 3] = CLIP8( + src_alpha + + MULDIV255( + target[k * 4 + 3], (255 - src_alpha), tmp)); } else { target[k * 4 + 0] = ink[0]; target[k * 4 + 1] = ink[1]; @@ -1109,7 +1147,13 @@ font_render(FontObject *self, PyObject *args) { for (k = x0; k < x1; k++) { unsigned int src_alpha = source[k] * convert_scale; if (src_alpha > 0) { - target[k] = target[k] > 0 ? CLIP8(src_alpha + MULDIV255(target[k], (255 - src_alpha), tmp)) : src_alpha; + target[k] = + target[k] > 0 + ? CLIP8( + src_alpha + + MULDIV255( + target[k], (255 - src_alpha), tmp)) + : src_alpha; } } } @@ -1249,7 +1293,8 @@ font_getvaraxes(FontObject *self) { if (name.name_id == axis.strid) { axis_name = Py_BuildValue("y#", name.string, name.string_len); - PyDict_SetItemString(list_axis, "name", axis_name ? axis_name : Py_None); + PyDict_SetItemString( + list_axis, "name", axis_name ? axis_name : Py_None); Py_XDECREF(axis_name); break; } @@ -1299,7 +1344,7 @@ font_setvaraxes(FontObject *self, PyObject *args) { } num_coords = PyObject_Length(axes); - coords = (FT_Fixed*)malloc(num_coords * sizeof(FT_Fixed)); + coords = (FT_Fixed *)malloc(num_coords * sizeof(FT_Fixed)); if (coords == NULL) { return PyErr_NoMemory(); } diff --git a/src/encode.c b/src/encode.c index c7dd51015..442b5d04f 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1163,8 +1163,10 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { ((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype; ((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi; ((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi; - ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_blocks = restart_marker_blocks; - ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_rows = restart_marker_rows; + ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_blocks = + restart_marker_blocks; + ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_rows = + restart_marker_rows; ((JPEGENCODERSTATE *)encoder->state.context)->comment = comment; ((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size; ((JPEGENCODERSTATE *)encoder->state.context)->extra = extra; @@ -1333,9 +1335,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { if (comment && comment_size > 0) { /* Size is stored as as an uint16, subtract 4 bytes for the header */ if (comment_size >= 65532) { - PyErr_SetString( - PyExc_ValueError, - "JPEG 2000 comment is too long"); + PyErr_SetString(PyExc_ValueError, "JPEG 2000 comment is too long"); Py_DECREF(encoder); return NULL; } diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 091c84e18..97d21034a 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -191,11 +191,11 @@ put_pixel_32(Imaging im, int x, int y, const void *color) { void ImagingAccessInit() { -#define ADD(mode_, get_pixel_, put_pixel_) \ - { \ - ImagingAccess access = add_item(mode_); \ - access->get_pixel = get_pixel_; \ - access->put_pixel = put_pixel_; \ +#define ADD(mode_, get_pixel_, put_pixel_) \ + { \ + ImagingAccess access = add_item(mode_); \ + access->get_pixel = get_pixel_; \ + access->put_pixel = put_pixel_; \ } /* populate access table */ diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index 5e4296eeb..72f478d8d 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -83,7 +83,6 @@ decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) { g1 = p[1].g; b1 = p[1].b; - /* NOTE: BC2 and BC3 reuse BC1 color blocks but always act like c0 > c1 */ if (col.c0 > col.c1 || separate_alpha) { p[2].r = (2 * r0 + 1 * r1) / 3; @@ -354,8 +353,7 @@ decode_bc7_block(rgba *col, const UINT8 *src) { } return; } - while (!(mode & (1 << bit++))) - ; + while (!(mode & (1 << bit++))); mode = bit - 1; info = &bc7_modes[mode]; /* color selection bits: {subset}{endpoint} */ @@ -546,7 +544,7 @@ static const UINT8 bc6_bit_packings[][75] = { 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 10, 112, 113, 114, 115, 64, 65, 66, 67, 26, 176, 160, 161, 162, 163, 80, 81, 82, 83, 42, 177, 128, 129, 130, 131, - 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, + 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 10, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, @@ -684,7 +682,7 @@ bc6_clamp(float value) { } else if (value > 1.0f) { return 255; } else { - return (UINT8) (value * 255.0f); + return (UINT8)(value * 255.0f); } } @@ -826,7 +824,13 @@ put_block(Imaging im, ImagingCodecState state, const char *col, int sz, int C) { static int decode_bcn( - Imaging im, ImagingCodecState state, const UINT8 *src, int bytes, int N, int C, char *pixel_format) { + Imaging im, + ImagingCodecState state, + const UINT8 *src, + int bytes, + int N, + int C, + char *pixel_format) { int ymax = state->ysize + state->yoff; const UINT8 *ptr = src; switch (N) { @@ -849,8 +853,7 @@ decode_bcn( DECODE_LOOP(2, 16, rgba); DECODE_LOOP(3, 16, rgba); DECODE_LOOP(4, 8, lum); - case 5: - { + case 5: { int sign = strcmp(pixel_format, "BC5S") == 0 ? 1 : 0; while (bytes >= 16) { rgba col[16]; @@ -865,8 +868,7 @@ decode_bcn( } break; } - case 6: - { + case 6: { int sign = strcmp(pixel_format, "BC6HS") == 0 ? 1 : 0; while (bytes >= 16) { rgba col[16]; @@ -880,7 +882,7 @@ decode_bcn( } break; } - DECODE_LOOP(7, 16, rgba); + DECODE_LOOP(7, 16, rgba); #undef DECODE_LOOP } return (int)(ptr - src); diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index adf425d0d..4ea9c7717 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -313,12 +313,12 @@ _gaussian_blur_radius(float radius, int passes) { } Imaging -ImagingGaussianBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int passes) { +ImagingGaussianBlur( + Imaging imOut, Imaging imIn, float xradius, float yradius, int passes) { return ImagingBoxBlur( - imOut, - imIn, - _gaussian_blur_radius(xradius, passes), - _gaussian_blur_radius(yradius, passes), - passes - ); + imOut, + imIn, + _gaussian_blur_radius(xradius, passes), + _gaussian_blur_radius(yradius, passes), + passes); } diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 64840d08c..fcb5f7ad9 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -518,8 +518,8 @@ rgba2rgb_(UINT8 *out, const UINT8 *in, int xsize) { /* * Conversion of RGB + single transparent color either to - * RGBA or LA, where any pixel matching the color will have the alpha channel set to 0, or - * RGBa or La, where any pixel matching the color will have all channels set to 0 + * RGBA or LA, where any pixel matching the color will have the alpha channel set to 0, + * or RGBa or La, where any pixel matching the color will have all channels set to 0 */ static void @@ -1676,7 +1676,8 @@ convert( return (Imaging)ImagingError_ValueError("conversion not supported"); #else static char buf[100]; - snprintf(buf, 100, "conversion from %.10s to %.10s not supported", imIn->mode, mode); + snprintf( + buf, 100, "conversion from %.10s to %.10s not supported", imIn->mode, mode); return (Imaging)ImagingError_ValueError(buf); #endif } @@ -1720,25 +1721,24 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { return (Imaging)ImagingError_ModeError(); } - if (strcmp(imIn->mode, "RGB") == 0 && (strcmp(mode, "RGBA") == 0 || strcmp(mode, "RGBa") == 0)) { + if (strcmp(imIn->mode, "RGB") == 0 && + (strcmp(mode, "RGBA") == 0 || strcmp(mode, "RGBa") == 0)) { convert = rgb2rgba; if (strcmp(mode, "RGBa") == 0) { premultiplied = 1; } - } else if (strcmp(imIn->mode, "RGB") == 0 && (strcmp(mode, "LA") == 0 || strcmp(mode, "La") == 0)) { + } else if ( + strcmp(imIn->mode, "RGB") == 0 && + (strcmp(mode, "LA") == 0 || strcmp(mode, "La") == 0)) { convert = rgb2la; source_transparency = 1; if (strcmp(mode, "La") == 0) { premultiplied = 1; } - } else if ((strcmp(imIn->mode, "1") == 0 || - strcmp(imIn->mode, "I") == 0 || - strcmp(imIn->mode, "I;16") == 0 || - strcmp(imIn->mode, "L") == 0 - ) && ( - strcmp(mode, "RGBA") == 0 || - strcmp(mode, "LA") == 0 - )) { + } else if ( + (strcmp(imIn->mode, "1") == 0 || strcmp(imIn->mode, "I") == 0 || + strcmp(imIn->mode, "I;16") == 0 || strcmp(imIn->mode, "L") == 0) && + (strcmp(mode, "RGBA") == 0 || strcmp(mode, "LA") == 0)) { if (strcmp(imIn->mode, "1") == 0) { convert = bit2rgb; } else if (strcmp(imIn->mode, "I") == 0) { diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 0ccf22d58..133696dd8 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -48,7 +48,7 @@ * This guarantees that ROUND_UP|DOWN(f) == -ROUND_UP|DOWN(-f) */ #define ROUND_UP(f) ((int)((f) >= 0.0 ? floor((f) + 0.5F) : -floor(fabs(f) + 0.5F))) -#define ROUND_DOWN(f) ((int)((f) >= 0.0 ? ceil((f)-0.5F) : -ceil(fabs(f) - 0.5F))) +#define ROUND_DOWN(f) ((int)((f) >= 0.0 ? ceil((f) - 0.5F) : -ceil(fabs(f) - 0.5F))) /* -------------------------------------------------------------------- */ /* Primitives */ @@ -439,7 +439,14 @@ draw_horizontal_lines( * Filled polygon draw function using scan line algorithm. */ static inline int -polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, int hasAlpha) { +polygon_generic( + Imaging im, + int n, + Edge *e, + int ink, + int eofill, + hline_handler hline, + int hasAlpha) { Edge **edge_table; float *xx; int edge_count = 0; @@ -499,7 +506,7 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h // Needed to draw consistent polygons xx[j] = xx[j - 1]; j++; - } else if (current->dx != 0 && roundf(xx[j-1]) == xx[j-1]) { + } else if (current->dx != 0 && roundf(xx[j - 1]) == xx[j - 1]) { // Connect discontiguous corners for (k = 0; k < i; k++) { Edge *other_edge = edge_table[k]; @@ -510,23 +517,38 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h // Check if the two edges join to make a corner if (((ymin == current->ymin && ymin == other_edge->ymin) || (ymin == current->ymax && ymin == other_edge->ymax)) && - xx[j-1] == (ymin - other_edge->y0) * other_edge->dx + other_edge->x0) { + xx[j - 1] == (ymin - other_edge->y0) * other_edge->dx + + other_edge->x0) { // Determine points from the edges on the next row // Or if this is the last row, check the previous row int offset = ymin == ymax ? -1 : 1; - adjacent_line_x = (ymin + offset - current->y0) * current->dx + current->x0; - adjacent_line_x_other_edge = (ymin + offset - other_edge->y0) * other_edge->dx + other_edge->x0; + adjacent_line_x = + (ymin + offset - current->y0) * current->dx + + current->x0; + adjacent_line_x_other_edge = + (ymin + offset - other_edge->y0) * other_edge->dx + + other_edge->x0; if (ymin == current->ymax) { if (current->dx > 0) { - xx[k] = fmax(adjacent_line_x, adjacent_line_x_other_edge) + 1; + xx[k] = fmax( + adjacent_line_x, + adjacent_line_x_other_edge) + + 1; } else { - xx[k] = fmin(adjacent_line_x, adjacent_line_x_other_edge) - 1; + xx[k] = fmin( + adjacent_line_x, + adjacent_line_x_other_edge) - + 1; } } else { if (current->dx > 0) { - xx[k] = fmin(adjacent_line_x, adjacent_line_x_other_edge); + xx[k] = fmin( + adjacent_line_x, adjacent_line_x_other_edge); } else { - xx[k] = fmax(adjacent_line_x, adjacent_line_x_other_edge) + 1; + xx[k] = fmax( + adjacent_line_x, + adjacent_line_x_other_edge) + + 1; } } break; @@ -552,7 +574,8 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h int x_start = ROUND_UP(xx[i - 1]); if (x_pos > x_start) { - // Line would be partway through x_pos, so increase the starting point + // Line would be partway through x_pos, so increase the starting + // point x_start = x_pos; if (x_end < x_start) { // Line would now end before it started @@ -776,7 +799,8 @@ ImagingDrawRectangle( } int -ImagingDrawPolygon(Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op) { +ImagingDrawPolygon( + Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op) { int i, n, x0, y0, x1, y1; DRAW *draw; INT32 ink; @@ -803,7 +827,7 @@ ImagingDrawPolygon(Imaging im, int count, int *xy, const void *ink_, int fill, i if (y0 == y1 && i != 0 && y0 == xy[i * 2 - 1]) { // This is a horizontal line, // that immediately follows another horizontal line - Edge *last_e = &e[n-1]; + Edge *last_e = &e[n - 1]; if (x1 > x0 && x0 > xy[i * 2 - 2]) { // They are both increasing in x last_e->xmax = x1; @@ -826,14 +850,24 @@ ImagingDrawPolygon(Imaging im, int count, int *xy, const void *ink_, int fill, i /* Outline */ if (width == 1) { for (i = 0; i < count - 1; i++) { - draw->line(im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink); + draw->line( + im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink); } draw->line(im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink); } else { for (i = 0; i < count - 1; i++) { - ImagingDrawWideLine(im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink_, width, op); + ImagingDrawWideLine( + im, + xy[i * 2], + xy[i * 2 + 1], + xy[i * 2 + 2], + xy[i * 2 + 3], + ink_, + width, + op); } - ImagingDrawWideLine(im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op); + ImagingDrawWideLine( + im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op); } } diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 4dcd368ca..85de77fcb 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -106,7 +106,7 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin) { void ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { -#define KERNEL1x3(in0, x, kernel, d) \ +#define KERNEL1x3(in0, x, kernel, d) \ (_i2f(in0[x - d]) * (kernel)[0] + _i2f(in0[x]) * (kernel)[1] + \ _i2f(in0[x + d]) * (kernel)[2]) @@ -224,10 +224,9 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { void ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { -#define KERNEL1x5(in0, x, kernel, d) \ - (_i2f(in0[x - d - d]) * (kernel)[0] + \ - _i2f(in0[x - d]) * (kernel)[1] + _i2f(in0[x]) * (kernel)[2] + \ - _i2f(in0[x + d]) * (kernel)[3] + \ +#define KERNEL1x5(in0, x, kernel, d) \ + (_i2f(in0[x - d - d]) * (kernel)[0] + _i2f(in0[x - d]) * (kernel)[1] + \ + _i2f(in0[x]) * (kernel)[2] + _i2f(in0[x + d]) * (kernel)[3] + \ _i2f(in0[x + d + d]) * (kernel)[4]) int x = 0, y = 0; diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index d6e4ea0ff..debe7ddd8 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -232,7 +232,8 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt /* Note, have to check Data + size, not just ptr + size) */ if (data + (state->xsize * state->ysize) > ptr + bytes) { /* not enough data for frame */ - /* UNDONE Unclear that we're actually going to leave the buffer at the right place. */ + /* UNDONE Unclear that we're actually going to leave the buffer at + * the right place. */ return ptr - buf; /* bytes consumed */ } for (y = 0; y < state->ysize; y++) { @@ -251,7 +252,7 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt return -1; } advance = I32(ptr); - if (advance == 0 ) { + if (advance == 0) { // If there's no advance, we're in an infinite loop state->errcode = IMAGING_CODEC_BROKEN; return -1; diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index 0c5915792..cf3bc9979 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -565,13 +565,13 @@ bilinear_filter32RGB(void *out, Imaging im, double xin, double yin) { #undef BILINEAR_HEAD #undef BILINEAR_BODY -#define BICUBIC(v, v1, v2, v3, v4, d) \ - { \ - double p1 = v2; \ - double p2 = -v1 + v3; \ - double p3 = 2 * (v1 - v2) + v3 - v4; \ - double p4 = -v1 + v2 - v3 + v4; \ - v = p1 + (d) * (p2 + (d) * (p3 + (d)*p4)); \ +#define BICUBIC(v, v1, v2, v3, v4, d) \ + { \ + double p1 = v2; \ + double p2 = -v1 + v3; \ + double p3 = 2 * (v1 - v2) + v3 - v4; \ + double p4 = -v1 + v2 - v3 + v4; \ + v = p1 + (d) * (p2 + (d) * (p3 + (d) * p4)); \ } #define BICUBIC_HEAD(type) \ @@ -966,7 +966,7 @@ affine_fixed( ysize = (int)imIn->ysize; /* use 16.16 fixed point arithmetics */ -#define FIX(v) FLOOR((v)*65536.0 + 0.5) +#define FIX(v) FLOOR((v) * 65536.0 + 0.5) a0 = FIX(a[0]); a1 = FIX(a[1]); diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 86c687ca0..bd2a2778c 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -58,11 +58,11 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { INT32 mask = 0xffffffff; if (im->bands == 3) { ((UINT8 *)&mask)[3] = 0; - } else if (alpha_only && ( - strcmp(im->mode, "RGBa") == 0 || strcmp(im->mode, "RGBA") == 0 || - strcmp(im->mode, "La") == 0 || strcmp(im->mode, "LA") == 0 || - strcmp(im->mode, "PA") == 0 - )) { + } else if ( + alpha_only && + (strcmp(im->mode, "RGBa") == 0 || strcmp(im->mode, "RGBA") == 0 || + strcmp(im->mode, "La") == 0 || strcmp(im->mode, "LA") == 0 || + strcmp(im->mode, "PA") == 0)) { #ifdef WORDS_BIGENDIAN mask = 0x000000ff; #else diff --git a/src/libImaging/Gif.h b/src/libImaging/Gif.h index 5d7e2bdaa..8edfbc2ed 100644 --- a/src/libImaging/Gif.h +++ b/src/libImaging/Gif.h @@ -9,10 +9,10 @@ /* Max size for a LZW code word. */ -#define GIFBITS 12 +#define GIFBITS 12 -#define GIFTABLE (1<next_code = st->end_code + 1; st->max_code = 2 * st->clear_code - 1; st->code_width = st->bits + 1; memset(st->codes, 0, sizeof(st->codes)); } -static void glzwe_init(GIFENCODERSTATE *st) { +static void +glzwe_init(GIFENCODERSTATE *st) { st->clear_code = 1 << st->bits; st->end_code = st->clear_code + 1; glzwe_reset(st); @@ -64,156 +72,157 @@ static void glzwe_init(GIFENCODERSTATE *st) { st->code_buffer = 0; } -static int glzwe(GIFENCODERSTATE *st, const UINT8 *in_ptr, UINT8 *out_ptr, - UINT32 *in_avail, UINT32 *out_avail, - UINT32 end_of_data) { +static int +glzwe( + GIFENCODERSTATE *st, + const UINT8 *in_ptr, + UINT8 *out_ptr, + UINT32 *in_avail, + UINT32 *out_avail, + UINT32 end_of_data) { switch (st->entry_state) { - - case LZW_TRY_IN1: -get_first_byte: - if (!*in_avail) { - if (end_of_data) { - goto end_of_data; + case LZW_TRY_IN1: + get_first_byte: + if (!*in_avail) { + if (end_of_data) { + goto end_of_data; + } + st->entry_state = LZW_TRY_IN1; + return GLZW_NO_INPUT_AVAIL; } - st->entry_state = LZW_TRY_IN1; - return GLZW_NO_INPUT_AVAIL; - } - st->head = *in_ptr++; - (*in_avail)--; + st->head = *in_ptr++; + (*in_avail)--; - case LZW_TRY_IN2: -encode_loop: - if (!*in_avail) { - if (end_of_data) { - st->code = st->head; - st->put_state = PUT_LAST_HEAD; - goto put_code; + case LZW_TRY_IN2: + encode_loop: + if (!*in_avail) { + if (end_of_data) { + st->code = st->head; + st->put_state = PUT_LAST_HEAD; + goto put_code; + } + st->entry_state = LZW_TRY_IN2; + return GLZW_NO_INPUT_AVAIL; } - st->entry_state = LZW_TRY_IN2; - return GLZW_NO_INPUT_AVAIL; - } - st->tail = *in_ptr++; - (*in_avail)--; + st->tail = *in_ptr++; + (*in_avail)--; - /* Knuth TAOCP vol 3 sec. 6.4 algorithm D. */ - /* Hash found experimentally to be pretty good. */ - /* This works ONLY with TABLE_SIZE a power of 2. */ - st->probe = ((st->head ^ (st->tail << 6)) * 31) & (TABLE_SIZE - 1); - while (st->codes[st->probe]) { - if ((st->codes[st->probe] & 0xFFFFF) == - ((st->head << 8) | st->tail)) { - st->head = st->codes[st->probe] >> 20; - goto encode_loop; - } else { - /* 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; + /* Knuth TAOCP vol 3 sec. 6.4 algorithm D. */ + /* Hash found experimentally to be pretty good. */ + /* This works ONLY with TABLE_SIZE a power of 2. */ + st->probe = ((st->head ^ (st->tail << 6)) * 31) & (TABLE_SIZE - 1); + while (st->codes[st->probe]) { + if ((st->codes[st->probe] & 0xFFFFF) == ((st->head << 8) | st->tail)) { + st->head = st->codes[st->probe] >> 20; + goto encode_loop; + } else { + /* 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; + } } } - } - /* Key not found, probe is at empty slot. */ - st->code = st->head; - st->put_state = PUT_HEAD; - goto put_code; -insert_code_or_clear: /* jump here after put_code */ - if (st->next_code < CODE_LIMIT) { - st->codes[st->probe] = (st->next_code << 20) | - (st->head << 8) | st->tail; - if (st->next_code > st->max_code) { - st->max_code = st->max_code * 2 + 1; - st->code_width++; - } - st->next_code++; - } else { - st->code = st->clear_code; - st->put_state = PUT_CLEAR; + /* Key not found, probe is at empty slot. */ + st->code = st->head; + st->put_state = PUT_HEAD; goto put_code; -reset_after_clear: /* jump here after put_code */ - glzwe_reset(st); - } - st->head = st->tail; - goto encode_loop; - - case LZW_INITIAL: - glzwe_reset(st); - st->code = st->clear_code; - st->put_state = PUT_INIT_CLEAR; -put_code: - st->code_bits_left = st->code_width; -check_buf_bits: - if (!st->buf_bits_left) { /* out buffer full */ - - case LZW_TRY_OUT1: - if (!*out_avail) { - st->entry_state = LZW_TRY_OUT1; - return GLZW_NO_OUTPUT_AVAIL; + insert_code_or_clear: /* jump here after put_code */ + if (st->next_code < CODE_LIMIT) { + st->codes[st->probe] = + (st->next_code << 20) | (st->head << 8) | st->tail; + if (st->next_code > st->max_code) { + st->max_code = st->max_code * 2 + 1; + st->code_width++; + } + st->next_code++; + } else { + st->code = st->clear_code; + st->put_state = PUT_CLEAR; + goto put_code; + reset_after_clear: /* jump here after put_code */ + glzwe_reset(st); } - *out_ptr++ = st->code_buffer; - (*out_avail)--; - st->code_buffer = 0; - st->buf_bits_left = 8; - } - /* code bits to pack */ - UINT32 n = st->buf_bits_left < st->code_bits_left - ? st->buf_bits_left : st->code_bits_left; - st->code_buffer |= - (st->code & ((1 << n) - 1)) << (8 - st->buf_bits_left); - st->code >>= n; - st->buf_bits_left -= n; - st->code_bits_left -= n; - if (st->code_bits_left) { - goto check_buf_bits; - } - switch (st->put_state) { - case PUT_INIT_CLEAR: - goto get_first_byte; - case PUT_HEAD: - goto insert_code_or_clear; - case PUT_CLEAR: - goto reset_after_clear; - case PUT_LAST_HEAD: - goto end_of_data; - case PUT_END: - goto flush_code_buffer; + st->head = st->tail; + goto encode_loop; + + case LZW_INITIAL: + glzwe_reset(st); + st->code = st->clear_code; + st->put_state = PUT_INIT_CLEAR; + put_code: + st->code_bits_left = st->code_width; + check_buf_bits: + if (!st->buf_bits_left) { /* out buffer full */ + + case LZW_TRY_OUT1: + if (!*out_avail) { + st->entry_state = LZW_TRY_OUT1; + return GLZW_NO_OUTPUT_AVAIL; + } + *out_ptr++ = st->code_buffer; + (*out_avail)--; + st->code_buffer = 0; + st->buf_bits_left = 8; + } + /* code bits to pack */ + UINT32 n = st->buf_bits_left < st->code_bits_left ? st->buf_bits_left + : st->code_bits_left; + st->code_buffer |= (st->code & ((1 << n) - 1)) << (8 - st->buf_bits_left); + st->code >>= n; + st->buf_bits_left -= n; + st->code_bits_left -= n; + if (st->code_bits_left) { + goto check_buf_bits; + } + switch (st->put_state) { + case PUT_INIT_CLEAR: + goto get_first_byte; + case PUT_HEAD: + goto insert_code_or_clear; + case PUT_CLEAR: + goto reset_after_clear; + case PUT_LAST_HEAD: + goto end_of_data; + case PUT_END: + goto flush_code_buffer; + default: + return GLZW_INTERNAL_ERROR; + } + + end_of_data: + st->code = st->end_code; + st->put_state = PUT_END; + goto put_code; + flush_code_buffer: /* jump here after put_code */ + if (st->buf_bits_left < 8) { + case LZW_TRY_OUT2: + if (!*out_avail) { + st->entry_state = LZW_TRY_OUT2; + return GLZW_NO_OUTPUT_AVAIL; + } + *out_ptr++ = st->code_buffer; + (*out_avail)--; + } + st->entry_state = LZW_FINISHED; + return GLZW_OK; + + case LZW_FINISHED: + return GLZW_OK; + default: return GLZW_INTERNAL_ERROR; - } - -end_of_data: - st->code = st->end_code; - st->put_state = PUT_END; - goto put_code; -flush_code_buffer: /* jump here after put_code */ - if (st->buf_bits_left < 8) { - - case LZW_TRY_OUT2: - if (!*out_avail) { - st->entry_state = LZW_TRY_OUT2; - return GLZW_NO_OUTPUT_AVAIL; - } - *out_ptr++ = st->code_buffer; - (*out_avail)--; - } - st->entry_state = LZW_FINISHED; - return GLZW_OK; - - case LZW_FINISHED: - return GLZW_OK; - - default: - return GLZW_INTERNAL_ERROR; } } /* -END- GIF LZW encoder. */ int -ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) { - UINT8* ptr; - UINT8* sub_block_ptr; - UINT8* sub_block_limit; - UINT8* buf_limit; - GIFENCODERSTATE *context = (GIFENCODERSTATE*) state->context; +ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + UINT8 *ptr; + UINT8 *sub_block_ptr; + UINT8 *sub_block_limit; + UINT8 *buf_limit; + GIFENCODERSTATE *context = (GIFENCODERSTATE *)state->context; int r; UINT32 in_avail, in_used; @@ -278,9 +287,9 @@ ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) { return ptr - buf; } sub_block_ptr = ptr; - sub_block_limit = sub_block_ptr + - (256 < buf_limit - sub_block_ptr ? - 256 : buf_limit - sub_block_ptr); + sub_block_limit = + sub_block_ptr + + (256 < buf_limit - sub_block_ptr ? 256 : buf_limit - sub_block_ptr); *ptr++ = 0; } @@ -301,9 +310,9 @@ ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) { /* get another line of data */ state->shuffle( state->buffer, - (UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->xsize - ); + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->xsize); state->x = 0; /* step forward, according to the interlace settings */ @@ -331,10 +340,15 @@ ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) { } } - in_avail = state->xsize - state->x; /* bytes left in line */ + in_avail = state->xsize - state->x; /* bytes left in line */ out_avail = sub_block_limit - ptr; /* bytes left in sub-block */ - r = glzwe(context, &state->buffer[state->x], ptr, &in_avail, - &out_avail, state->state == FINISH); + r = glzwe( + context, + &state->buffer[state->x], + ptr, + &in_avail, + &out_avail, + state->state == FINISH); out_used = sub_block_limit - ptr - out_avail; *sub_block_ptr += out_used; ptr += out_used; diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index afcd2229b..1f2c03e93 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -108,15 +108,15 @@ struct ImagingMemoryInstance { #define IMAGING_PIXEL_1(im, x, y) ((im)->image8[(y)][(x)]) #define IMAGING_PIXEL_L(im, x, y) ((im)->image8[(y)][(x)]) -#define IMAGING_PIXEL_LA(im, x, y) ((im)->image[(y)][(x)*4]) +#define IMAGING_PIXEL_LA(im, x, y) ((im)->image[(y)][(x) * 4]) #define IMAGING_PIXEL_P(im, x, y) ((im)->image8[(y)][(x)]) -#define IMAGING_PIXEL_PA(im, x, y) ((im)->image[(y)][(x)*4]) +#define IMAGING_PIXEL_PA(im, x, y) ((im)->image[(y)][(x) * 4]) #define IMAGING_PIXEL_I(im, x, y) ((im)->image32[(y)][(x)]) #define IMAGING_PIXEL_F(im, x, y) (((FLOAT32 *)(im)->image32[y])[x]) -#define IMAGING_PIXEL_RGB(im, x, y) ((im)->image[(y)][(x)*4]) -#define IMAGING_PIXEL_RGBA(im, x, y) ((im)->image[(y)][(x)*4]) -#define IMAGING_PIXEL_CMYK(im, x, y) ((im)->image[(y)][(x)*4]) -#define IMAGING_PIXEL_YCbCr(im, x, y) ((im)->image[(y)][(x)*4]) +#define IMAGING_PIXEL_RGB(im, x, y) ((im)->image[(y)][(x) * 4]) +#define IMAGING_PIXEL_RGBA(im, x, y) ((im)->image[(y)][(x) * 4]) +#define IMAGING_PIXEL_CMYK(im, x, y) ((im)->image[(y)][(x) * 4]) +#define IMAGING_PIXEL_YCbCr(im, x, y) ((im)->image[(y)][(x) * 4]) #define IMAGING_PIXEL_UINT8(im, x, y) ((im)->image8[(y)][(x)]) #define IMAGING_PIXEL_INT32(im, x, y) ((im)->image32[(y)][(x)]) @@ -161,7 +161,7 @@ typedef struct ImagingMemoryArena { int stats_reallocated_blocks; /* Number of blocks which were actually reallocated after retrieving */ int stats_freed_blocks; /* Number of freed blocks */ -} * ImagingMemoryArena; +} *ImagingMemoryArena; /* Objects */ /* ------- */ @@ -309,7 +309,8 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn); extern Imaging ImagingFlipTopBottom(Imaging imOut, Imaging imIn); extern Imaging -ImagingGaussianBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int passes); +ImagingGaussianBlur( + Imaging imOut, Imaging imIn, float xradius, float yradius, int passes); extern Imaging ImagingGetBand(Imaging im, int band); extern Imaging @@ -487,7 +488,8 @@ ImagingDrawPieslice( extern int ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op); extern int -ImagingDrawPolygon(Imaging im, int points, int *xy, const void *ink, int fill, int width, int op); +ImagingDrawPolygon( + Imaging im, int points, int *xy, const void *ink, int fill, int width, int op); extern int ImagingDrawRectangle( Imaging im, diff --git a/src/libImaging/ImagingUtils.h b/src/libImaging/ImagingUtils.h index 0c0c1eda9..714458ad0 100644 --- a/src/libImaging/ImagingUtils.h +++ b/src/libImaging/ImagingUtils.h @@ -21,7 +21,7 @@ #define DIV255(a, tmp) (tmp = (a) + 128, SHIFTFORDIV255(tmp)) -#define BLEND(mask, in1, in2, tmp1) DIV255(in1 *(255 - mask) + in2 * mask, tmp1) +#define BLEND(mask, in1, in2, tmp1) DIV255(in1 * (255 - mask) + in2 * mask, tmp1) #define PREBLEND(mask, in1, in2, tmp1) (MULDIV255(in1, (255 - mask), tmp1) + in2) diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index 78a09bb83..dd066c10b 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -183,9 +183,9 @@ j2ku_gray_i( UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; for (x = 0; x < w; ++x) { UINT16 pixel = j2ku_shift(offset + *data++, shift); - #ifdef WORDS_BIGENDIAN - pixel = (pixel >> 8) | (pixel << 8); - #endif +#ifdef WORDS_BIGENDIAN + pixel = (pixel >> 8) | (pixel << 8); +#endif *row++ = pixel; } } @@ -778,7 +778,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { color_space = OPJ_CLRSPC_SYCC; break; } - break; + break; } } @@ -864,7 +864,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { a, and then a malicious file could have a smaller tile_bytes */ - for (n=0; n < tile_info.nb_comps; n++) { + for (n = 0; n < tile_info.nb_comps; n++) { // see csize /acsize calcs int csize = (image->comps[n].prec + 7) >> 3; csize = (csize == 3) ? 4 : csize; diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 3295373fd..7f1aeaddb 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -383,8 +383,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { float *pq; if (len > 0) { - if ((size_t)len > - sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0])) { + if ((size_t)len > sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0])) { len = sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0]); } @@ -464,7 +463,8 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { } if (!context->num_resolutions) { - while (tile_width < (1U << (params.numresolution - 1U)) || tile_height < (1U << (params.numresolution - 1U))) { + while (tile_width < (1U << (params.numresolution - 1U)) || + tile_height < (1U << (params.numresolution - 1U))) { params.numresolution -= 1; } } diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 00f3d5f74..bcbe65aa4 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -145,8 +145,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { case JCS_EXT_RGBX: #endif switch (context->subsampling) { - case -1: /* Default */ - case 0: /* No subsampling */ + case -1: /* Default */ + case 0: /* No subsampling */ break; default: /* Would subsample the green and blue @@ -305,7 +305,11 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { case 4: if (context->comment) { - jpeg_write_marker(&context->cinfo, JPEG_COM, (unsigned char *)context->comment, context->comment_size); + jpeg_write_marker( + &context->cinfo, + JPEG_COM, + (unsigned char *)context->comment, + context->comment_size); } state->state++; @@ -342,7 +346,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { } jpeg_finish_compress(&context->cinfo); -cleanup: + cleanup: /* Clean up */ if (context->comment) { free(context->comment); diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index 6684b11ef..a018225b2 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -432,11 +432,10 @@ fill_mask_L( } } else { - int alpha_channel = strcmp(imOut->mode, "RGBa") == 0 || - strcmp(imOut->mode, "RGBA") == 0 || - strcmp(imOut->mode, "La") == 0 || - strcmp(imOut->mode, "LA") == 0 || - strcmp(imOut->mode, "PA") == 0; + int alpha_channel = + strcmp(imOut->mode, "RGBa") == 0 || strcmp(imOut->mode, "RGBA") == 0 || + strcmp(imOut->mode, "La") == 0 || strcmp(imOut->mode, "LA") == 0 || + strcmp(imOut->mode, "PA") == 0; for (y = 0; y < ysize; y++) { UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx * pixelsize; UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index 8883578cb..dd06f3940 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -134,7 +134,7 @@ ImagingPoint(Imaging imIn, const char *mode, const void *table) { ImagingSectionCookie cookie; Imaging imOut; im_point_context context; - void (*point)(Imaging imIn, Imaging imOut, im_point_context * context); + void (*point)(Imaging imIn, Imaging imOut, im_point_context *context); if (!imIn) { return (Imaging)ImagingError_ModeError(); diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index 2582830c4..cdc614536 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -260,8 +260,7 @@ mergesort_pixels(PixelList *head, int i) { return head; } for (c = t = head; c && t; - c = c->next[i], t = (t->next[i]) ? t->next[i]->next[i] : NULL) - ; + c = c->next[i], t = (t->next[i]) ? t->next[i]->next[i] : NULL); if (c) { if (c->prev[i]) { c->prev[i]->next[i] = NULL; @@ -354,12 +353,10 @@ splitlists( for (_i = 0; _i < 3; _i++) { for (_nextCount[_i] = 0, _nextTest = h[_i]; _nextTest && _nextTest->next[_i]; - _nextTest = _nextTest->next[_i], _nextCount[_i]++) - ; + _nextTest = _nextTest->next[_i], _nextCount[_i]++); for (_prevCount[_i] = 0, _prevTest = t[_i]; _prevTest && _prevTest->prev[_i]; - _prevTest = _prevTest->prev[_i], _prevCount[_i]++) - ; + _prevTest = _prevTest->prev[_i], _prevCount[_i]++); if (_nextTest != t[_i]) { printf("next-list of axis %d does not end at tail\n", _i); exit(1); @@ -368,10 +365,8 @@ splitlists( printf("prev-list of axis %d does not end at head\n", _i); exit(1); } - for (; _nextTest && _nextTest->prev[_i]; _nextTest = _nextTest->prev[_i]) - ; - for (; _prevTest && _prevTest->next[_i]; _prevTest = _prevTest->next[_i]) - ; + for (; _nextTest && _nextTest->prev[_i]; _nextTest = _nextTest->prev[_i]); + for (; _prevTest && _prevTest->next[_i]; _prevTest = _prevTest->next[_i]); if (_nextTest != h[_i]) { printf("next-list of axis %d does not loop back to head\n", _i); exit(1); @@ -548,22 +543,18 @@ split(BoxNode *node) { for (_i = 0; _i < 3; _i++) { for (_nextCount[_i] = 0, _nextTest = node->head[_i]; _nextTest && _nextTest->next[_i]; - _nextTest = _nextTest->next[_i], _nextCount[_i]++) - ; + _nextTest = _nextTest->next[_i], _nextCount[_i]++); for (_prevCount[_i] = 0, _prevTest = node->tail[_i]; _prevTest && _prevTest->prev[_i]; - _prevTest = _prevTest->prev[_i], _prevCount[_i]++) - ; + _prevTest = _prevTest->prev[_i], _prevCount[_i]++); if (_nextTest != node->tail[_i]) { printf("next-list of axis %d does not end at tail\n", _i); } if (_prevTest != node->head[_i]) { printf("prev-list of axis %d does not end at head\n", _i); } - for (; _nextTest && _nextTest->prev[_i]; _nextTest = _nextTest->prev[_i]) - ; - for (; _prevTest && _prevTest->next[_i]; _prevTest = _prevTest->next[_i]) - ; + for (; _nextTest && _nextTest->prev[_i]; _nextTest = _nextTest->prev[_i]); + for (; _prevTest && _prevTest->next[_i]; _prevTest = _prevTest->next[_i]); if (_nextTest != node->head[_i]) { printf("next-list of axis %d does not loop back to head\n", _i); } @@ -668,8 +659,7 @@ median_cut(PixelList *hl[3], uint32_t imPixelCount, int nPixels) { return NULL; } for (i = 0; i < 3; i++) { - for (tl[i] = hl[i]; tl[i] && tl[i]->next[i]; tl[i] = tl[i]->next[i]) - ; + for (tl[i] = hl[i]; tl[i] && tl[i]->next[i]; tl[i] = tl[i]->next[i]); root->head[i] = hl[i]; root->tail[i] = tl[i]; } @@ -832,16 +822,9 @@ build_distance_tables( } for (i = 0; i < nEntries; i++) { for (j = 0; j < nEntries; j++) { - dwi[j] = (DistanceWithIndex){ - &(avgDist[i * nEntries + j]), - j - }; + dwi[j] = (DistanceWithIndex){&(avgDist[i * nEntries + j]), j}; } - qsort( - dwi, - nEntries, - sizeof(DistanceWithIndex), - _distance_index_cmp); + qsort(dwi, nEntries, sizeof(DistanceWithIndex), _distance_index_cmp); for (j = 0; j < nEntries; j++) { avgDistSortKey[i * nEntries + j] = dwi[j].distance; } @@ -1213,7 +1196,7 @@ k_means( compute_palette_from_quantized_pixels( pixelData, nPixels, paletteData, nPaletteEntries, avg, count, qp); if (!build_distance_tables( - avgDist, avgDistSortKey, paletteData, nPaletteEntries)) { + avgDist, avgDistSortKey, paletteData, nPaletteEntries)) { goto error_3; } built = 1; @@ -1452,15 +1435,17 @@ quantize( hashtable_insert(h2, pixelData[i], bestmatch); } if (qp[i] != bestmatch) { - printf ("discrepancy in matching algorithms pixel %d [%d %d] %f %f\n", - i,qp[i],bestmatch, - sqrt((double)(_SQR(pixelData[i].c.r-p[qp[i]].c.r)+ - _SQR(pixelData[i].c.g-p[qp[i]].c.g)+ - _SQR(pixelData[i].c.b-p[qp[i]].c.b))), - sqrt((double)(_SQR(pixelData[i].c.r-p[bestmatch].c.r)+ - _SQR(pixelData[i].c.g-p[bestmatch].c.g)+ - _SQR(pixelData[i].c.b-p[bestmatch].c.b))) - ); + printf( + "discrepancy in matching algorithms pixel %d [%d %d] %f %f\n", + i, + qp[i], + bestmatch, + sqrt((double)(_SQR(pixelData[i].c.r - p[qp[i]].c.r) + + _SQR(pixelData[i].c.g - p[qp[i]].c.g) + + _SQR(pixelData[i].c.b - p[qp[i]].c.b))), + sqrt((double)(_SQR(pixelData[i].c.r - p[bestmatch].c.r) + + _SQR(pixelData[i].c.g - p[bestmatch].c.g) + + _SQR(pixelData[i].c.b - p[bestmatch].c.b)))); } } hashtable_free(h2); diff --git a/src/libImaging/QuantOctree.c b/src/libImaging/QuantOctree.c index 5e79bce35..1331a30ad 100644 --- a/src/libImaging/QuantOctree.c +++ b/src/libImaging/QuantOctree.c @@ -38,7 +38,7 @@ typedef struct _ColorBucket { uint64_t g; uint64_t b; uint64_t a; -} * ColorBucket; +} *ColorBucket; typedef struct _ColorCube { unsigned int rBits, gBits, bBits, aBits; @@ -47,7 +47,7 @@ typedef struct _ColorCube { unsigned long size; ColorBucket buckets; -} * ColorCube; +} *ColorCube; #define MAX(a, b) (a) > (b) ? (a) : (b) diff --git a/src/libImaging/Reduce.c b/src/libImaging/Reduce.c index 60928d2bc..61566f0c5 100644 --- a/src/libImaging/Reduce.c +++ b/src/libImaging/Reduce.c @@ -2,7 +2,7 @@ #include -#define ROUND_UP(f) ((int)((f) >= 0.0 ? (f) + 0.5F : (f)-0.5F)) +#define ROUND_UP(f) ((int)((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F)) UINT32 division_UINT32(int divider, int result_bits) { diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index cf79d8a4e..59c27b3f4 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -2,7 +2,7 @@ #include -#define ROUND_UP(f) ((int)((f) >= 0.0 ? (f) + 0.5F : (f)-0.5F)) +#define ROUND_UP(f) ((int)((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F)) struct filter { double (*filter)(double x); diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index 4eef44ba5..89dedb525 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -113,7 +113,8 @@ expandrow(UINT8 *dest, UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer } static int -expandrow2(UINT8 *dest, const UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer) { +expandrow2( + UINT8 *dest, const UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer) { UINT8 pixel, count; int x = 0; @@ -197,7 +198,6 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t return -1; } - /* decoder initialization */ state->count = 0; state->y = 0; @@ -252,7 +252,7 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t c->rlelength, im->bands, im->xsize, - &ptr[c->bufsize-1]); + &ptr[c->bufsize - 1]); } else { status = expandrow2( &state->buffer[c->channo * 2], @@ -260,7 +260,7 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t c->rlelength, im->bands, im->xsize, - &ptr[c->bufsize-1]); + &ptr[c->bufsize - 1]); } if (status == -1) { state->errcode = IMAGING_CODEC_OVERRUN; @@ -268,7 +268,6 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t } else if (status == 1) { goto sgi_finish_decode; } - } /* store decompressed data in image */ diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index b1b03c515..b27195a35 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -418,9 +418,8 @@ ImagingAllocateArray(Imaging im, int dirty, int block_size) { } im->blocks[current_block] = block; /* Bulletproof code from libc _int_memalign */ - aligned_ptr = (char *)( - ((size_t) (block.ptr + arena->alignment - 1)) & - -((Py_ssize_t) arena->alignment)); + aligned_ptr = (char *)(((size_t)(block.ptr + arena->alignment - 1)) & + -((Py_ssize_t)arena->alignment)); } im->image[y] = aligned_ptr + aligned_linesize * line_in_block; diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index e3b81590e..858de9332 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -60,7 +60,11 @@ _tiffReadProc(thandle_t hdata, tdata_t buf, tsize_t size) { dump_state(state); if (state->loc > state->eof) { - TIFFError("_tiffReadProc", "Invalid Read at loc %" PRIu64 ", eof: %" PRIu64, state->loc, state->eof); + TIFFError( + "_tiffReadProc", + "Invalid Read at loc %" PRIu64 ", eof: %" PRIu64, + state->loc, + state->eof); return 0; } to_read = min(size, min(state->size, (tsize_t)state->eof) - (tsize_t)state->loc); @@ -217,7 +221,12 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset) { } int -_pickUnpackers(Imaging im, ImagingCodecState state, TIFF *tiff, uint16_t planarconfig, ImagingShuffler *unpackers) { +_pickUnpackers( + Imaging im, + ImagingCodecState state, + TIFF *tiff, + uint16_t planarconfig, + ImagingShuffler *unpackers) { // if number of bands is 1, there is no difference with contig case if (planarconfig == PLANARCONFIG_SEPARATE && im->bands > 1) { uint16_t bits_per_sample = 8; @@ -232,10 +241,14 @@ _pickUnpackers(Imaging im, ImagingCodecState state, TIFF *tiff, uint16_t planarc // We'll pick appropriate set of unpackers depending on planar_configuration // It does not matter if data is RGB(A), CMYK or LUV really, // we just copy it plane by plane - unpackers[0] = ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "R;16N" : "R", NULL); - unpackers[1] = ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "G;16N" : "G", NULL); - unpackers[2] = ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "B;16N" : "B", NULL); - unpackers[3] = ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "A;16N" : "A", NULL); + unpackers[0] = + ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "R;16N" : "R", NULL); + unpackers[1] = + ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "G;16N" : "G", NULL); + unpackers[2] = + ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "B;16N" : "B", NULL); + unpackers[3] = + ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "A;16N" : "A", NULL); return im->bands; } else { @@ -247,10 +260,10 @@ _pickUnpackers(Imaging im, ImagingCodecState state, TIFF *tiff, uint16_t planarc int _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { - // To avoid dealing with YCbCr subsampling and other complications, let libtiff handle it - // Use a TIFFRGBAImage wrapping the tiff image, and let libtiff handle - // all of the conversion. Metadata read from the TIFFRGBAImage could - // be different from the metadata that the base tiff returns. + // To avoid dealing with YCbCr subsampling and other complications, let libtiff + // handle it Use a TIFFRGBAImage wrapping the tiff image, and let libtiff handle all + // of the conversion. Metadata read from the TIFFRGBAImage could be different from + // the metadata that the base tiff returns. INT32 current_row; UINT8 *new_data; @@ -259,17 +272,16 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { TIFFRGBAImage img; char emsg[1024] = ""; - // Since using TIFFRGBAImage* functions, we can read whole tiff into rastrr in one call - // Let's select smaller block size. Multiplying image width by (tile length OR rows per strip) - // gives us manageable block size in pixels + // Since using TIFFRGBAImage* functions, we can read whole tiff into rastrr in one + // call Let's select smaller block size. Multiplying image width by (tile length OR + // rows per strip) gives us manageable block size in pixels if (TIFFIsTiled(tiff)) { ret = TIFFGetFieldDefaulted(tiff, TIFFTAG_TILELENGTH, &rows_per_block); - } - else { + } else { ret = TIFFGetFieldDefaulted(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_block); } - if (ret != 1 || rows_per_block==(UINT32)(-1)) { + if (ret != 1 || rows_per_block == (UINT32)(-1)) { rows_per_block = state->ysize; } @@ -357,7 +369,12 @@ decodergba_err: } int -_decodeTile(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, ImagingShuffler *unpackers) { +_decodeTile( + Imaging im, + ImagingCodecState state, + TIFF *tiff, + int planes, + ImagingShuffler *unpackers) { INT32 x, y, tile_y, current_tile_length, current_tile_width; UINT32 tile_width, tile_length; tsize_t tile_bytes_size, row_byte_size; @@ -396,7 +413,8 @@ _decodeTile(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imaging if (tile_bytes_size > ((tile_length * state->bits / planes + 7) / 8) * tile_width) { // If the tile size as expected by LibTiff isn't what we're expecting, abort. - // man: TIFFTileSize returns the equivalent size for a tile of data as it would be returned in a + // man: TIFFTileSize returns the equivalent size for a tile of data as it + // would be returned in a // call to TIFFReadTile ... state->errcode = IMAGING_CODEC_BROKEN; return -1; @@ -428,19 +446,24 @@ _decodeTile(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imaging TRACE(("Read tile at %dx%d; \n\n", x, y)); - current_tile_width = min((INT32) tile_width, state->xsize - x); - current_tile_length = min((INT32) tile_length, state->ysize - y); + current_tile_width = min((INT32)tile_width, state->xsize - x); + current_tile_length = min((INT32)tile_length, state->ysize - y); // iterate over each line in the tile and stuff data into image for (tile_y = 0; tile_y < current_tile_length; tile_y++) { - TRACE(("Writing tile data at %dx%d using tile_width: %d; \n", tile_y + y, x, current_tile_width)); + TRACE( + ("Writing tile data at %dx%d using tile_width: %d; \n", + tile_y + y, + x, + current_tile_width)); // UINT8 * bbb = state->buffer + tile_y * row_byte_size; - // TRACE(("chars: %x%x%x%x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); + // TRACE(("chars: %x%x%x%x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], + // ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); - shuffler((UINT8*) im->image[tile_y + y] + x * im->pixelsize, - state->buffer + tile_y * row_byte_size, - current_tile_width - ); + shuffler( + (UINT8 *)im->image[tile_y + y] + x * im->pixelsize, + state->buffer + tile_y * row_byte_size, + current_tile_width); } } } @@ -450,7 +473,12 @@ _decodeTile(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imaging } int -_decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, ImagingShuffler *unpackers) { +_decodeStrip( + Imaging im, + ImagingCodecState state, + TIFF *tiff, + int planes, + ImagingShuffler *unpackers) { INT32 strip_row = 0; UINT8 *new_data; UINT32 rows_per_strip; @@ -458,7 +486,7 @@ _decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imagin tsize_t strip_size, row_byte_size, unpacker_row_byte_size; ret = TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); - if (ret != 1 || rows_per_strip==(UINT32)(-1)) { + if (ret != 1 || rows_per_strip == (UINT32)(-1)) { rows_per_strip = state->ysize; } @@ -478,7 +506,8 @@ _decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imagin unpacker_row_byte_size = (state->xsize * state->bits / planes + 7) / 8; if (strip_size > (unpacker_row_byte_size * rows_per_strip)) { // If the strip size as expected by LibTiff isn't what we're expecting, abort. - // man: TIFFStripSize returns the equivalent size for a strip of data as it would be returned in a + // man: TIFFStripSize returns the equivalent size for a strip of data as it + // would be returned in a // call to TIFFReadEncodedStrip ... state->errcode = IMAGING_CODEC_BROKEN; return -1; @@ -513,8 +542,13 @@ _decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imagin int plane; for (plane = 0; plane < planes; plane++) { ImagingShuffler shuffler = unpackers[plane]; - if (TIFFReadEncodedStrip(tiff, TIFFComputeStrip(tiff, state->y, plane), (tdata_t)state->buffer, strip_size) == -1) { - TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0))); + if (TIFFReadEncodedStrip( + tiff, + TIFFComputeStrip(tiff, state->y, plane), + (tdata_t)state->buffer, + strip_size) == -1) { + TRACE( + ("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0))); state->errcode = IMAGING_CODEC_BROKEN; return -1; } @@ -523,16 +557,17 @@ _decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imagin // iterate over each row in the strip and stuff data into image for (strip_row = 0; - strip_row < min((INT32) rows_per_strip, state->ysize - state->y); + strip_row < min((INT32)rows_per_strip, state->ysize - state->y); strip_row++) { TRACE(("Writing data into line %d ; \n", state->y + strip_row)); - // UINT8 * bbb = state->buffer + strip_row * (state->bytes / rows_per_strip); - // TRACE(("chars: %x %x %x %x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); + // UINT8 * bbb = state->buffer + strip_row * (state->bytes / + // rows_per_strip); TRACE(("chars: %x %x %x %x\n", ((UINT8 *)bbb)[0], + // ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); shuffler( - (UINT8*) im->image[state->y + state->yoff + strip_row] + - state->xoff * im->pixelsize, + (UINT8 *)im->image[state->y + state->yoff + strip_row] + + state->xoff * im->pixelsize, state->buffer + strip_row * row_byte_size, state->xsize); } @@ -666,7 +701,6 @@ ImagingLibTiffDecode( goto decode_err; } - TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); TIFFGetField(tiff, TIFFTAG_COMPRESSION, &compression); TIFFGetFieldDefaulted(tiff, TIFFTAG_PLANARCONFIG, &planarconfig); @@ -675,16 +709,17 @@ ImagingLibTiffDecode( // Let LibTiff read them as RGBA readAsRGBA = photometric == PHOTOMETRIC_YCBCR; - if (readAsRGBA && compression == COMPRESSION_JPEG && planarconfig == PLANARCONFIG_CONTIG) { - // If using new JPEG compression, let libjpeg do RGB conversion for performance reasons + if (readAsRGBA && compression == COMPRESSION_JPEG && + planarconfig == PLANARCONFIG_CONTIG) { + // If using new JPEG compression, let libjpeg do RGB conversion for performance + // reasons TIFFSetField(tiff, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); readAsRGBA = 0; } if (readAsRGBA) { _decodeAsRGBA(im, state, tiff); - } - else { + } else { planes = _pickUnpackers(im, state, tiff, planarconfig, unpackers); if (planes <= 0) { goto decode_err; @@ -692,8 +727,7 @@ ImagingLibTiffDecode( if (TIFFIsTiled(tiff)) { _decodeTile(im, state, tiff, planes, unpackers); - } - else { + } else { _decodeStrip(im, state, tiff, planes, unpackers); } @@ -702,20 +736,20 @@ ImagingLibTiffDecode( // so we have to convert it to RGBA if (planes > 3 && strcmp(im->mode, "RGBA") == 0) { uint16_t extrasamples; - uint16_t* sampleinfo; + uint16_t *sampleinfo; ImagingShuffler shuffle; INT32 y; - TIFFGetFieldDefaulted(tiff, TIFFTAG_EXTRASAMPLES, &extrasamples, &sampleinfo); + TIFFGetFieldDefaulted( + tiff, TIFFTAG_EXTRASAMPLES, &extrasamples, &sampleinfo); - if (extrasamples >= 1 && - (sampleinfo[0] == EXTRASAMPLE_UNSPECIFIED || sampleinfo[0] == EXTRASAMPLE_ASSOCALPHA) - ) { + if (extrasamples >= 1 && (sampleinfo[0] == EXTRASAMPLE_UNSPECIFIED || + sampleinfo[0] == EXTRASAMPLE_ASSOCALPHA)) { shuffle = ImagingFindUnpacker("RGBA", "RGBa", NULL); for (y = state->yoff; y < state->ysize; y++) { - UINT8* ptr = (UINT8*) im->image[y + state->yoff] + - state->xoff * im->pixelsize; + UINT8 *ptr = (UINT8 *)im->image[y + state->yoff] + + state->xoff * im->pixelsize; shuffle(ptr, ptr, state->xsize); } } @@ -723,7 +757,7 @@ ImagingLibTiffDecode( } } - decode_err: +decode_err: // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup if (clientstate->fp) { // Pillow will manage the closing of the file rather than libtiff diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index 02454ba03..212b7dee6 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -30,7 +30,7 @@ typedef struct { * Should be uint32 for libtiff 3.9.x * uint64 for libtiff 4.0.x */ - TIFF *tiff; /* Used in write */ + TIFF *tiff; /* Used in write */ toff_t eof; int flrealloc; /* may we realloc */ } TIFFSTATE; diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index e351aa2f1..1b84cd68f 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1437,90 +1437,90 @@ band3I(UINT8 *out, const UINT8 *in, int pixels) { } static void -band016B(UINT8* out, const UINT8* in, int pixels) -{ +band016B(UINT8 *out, const UINT8 *in, int pixels) { int i; /* band 0 only, big endian */ for (i = 0; i < pixels; i++) { out[0] = in[0]; - out += 4; in += 2; + out += 4; + in += 2; } } static void -band116B(UINT8* out, const UINT8* in, int pixels) -{ +band116B(UINT8 *out, const UINT8 *in, int pixels) { int i; /* band 1 only, big endian */ for (i = 0; i < pixels; i++) { out[1] = in[0]; - out += 4; in += 2; + out += 4; + in += 2; } } static void -band216B(UINT8* out, const UINT8* in, int pixels) -{ +band216B(UINT8 *out, const UINT8 *in, int pixels) { int i; /* band 2 only, big endian */ for (i = 0; i < pixels; i++) { out[2] = in[0]; - out += 4; in += 2; + out += 4; + in += 2; } } static void -band316B(UINT8* out, const UINT8* in, int pixels) -{ +band316B(UINT8 *out, const UINT8 *in, int pixels) { int i; /* band 3 only, big endian */ for (i = 0; i < pixels; i++) { out[3] = in[0]; - out += 4; in += 2; + out += 4; + in += 2; } } static void -band016L(UINT8* out, const UINT8* in, int pixels) -{ +band016L(UINT8 *out, const UINT8 *in, int pixels) { int i; /* band 0 only, little endian */ for (i = 0; i < pixels; i++) { out[0] = in[1]; - out += 4; in += 2; + out += 4; + in += 2; } } static void -band116L(UINT8* out, const UINT8* in, int pixels) -{ +band116L(UINT8 *out, const UINT8 *in, int pixels) { int i; /* band 1 only, little endian */ for (i = 0; i < pixels; i++) { out[1] = in[1]; - out += 4; in += 2; + out += 4; + in += 2; } } static void -band216L(UINT8* out, const UINT8* in, int pixels) -{ +band216L(UINT8 *out, const UINT8 *in, int pixels) { int i; /* band 2 only, little endian */ for (i = 0; i < pixels; i++) { out[2] = in[1]; - out += 4; in += 2; + out += 4; + in += 2; } } static void -band316L(UINT8* out, const UINT8* in, int pixels) -{ +band316L(UINT8 *out, const UINT8 *in, int pixels) { int i; /* band 3 only, little endian */ for (i = 0; i < pixels; i++) { out[3] = in[1]; - out += 4; in += 2; + out += 4; + in += 2; } } @@ -1687,7 +1687,6 @@ static struct { {"RGB", "G;16N", 16, band116L}, {"RGB", "B;16N", 16, band216L}, - {"RGBA", "R;16N", 16, band016L}, {"RGBA", "G;16N", 16, band116L}, {"RGBA", "B;16N", 16, band216L}, diff --git a/src/path.c b/src/path.c index cc0698c4d..6bc90abed 100644 --- a/src/path.c +++ b/src/path.c @@ -162,24 +162,24 @@ PyPath_Flatten(PyObject *data, double **pxy) { return -1; } -#define assign_item_to_array(op, decref) \ -if (PyFloat_Check(op)) { \ - xy[j++] = PyFloat_AS_DOUBLE(op); \ -} else if (PyLong_Check(op)) { \ - xy[j++] = (float)PyLong_AS_LONG(op); \ -} else if (PyNumber_Check(op)) { \ - xy[j++] = PyFloat_AsDouble(op); \ -} else if (PyArg_ParseTuple(op, "dd", &x, &y)) { \ - xy[j++] = x; \ - xy[j++] = y; \ -} else { \ - PyErr_SetString(PyExc_ValueError, "incorrect coordinate type"); \ - if (decref) { \ - Py_DECREF(op); \ - } \ - free(xy); \ - return -1; \ -} +#define assign_item_to_array(op, decref) \ + if (PyFloat_Check(op)) { \ + xy[j++] = PyFloat_AS_DOUBLE(op); \ + } else if (PyLong_Check(op)) { \ + xy[j++] = (float)PyLong_AS_LONG(op); \ + } else if (PyNumber_Check(op)) { \ + xy[j++] = PyFloat_AsDouble(op); \ + } else if (PyArg_ParseTuple(op, "dd", &x, &y)) { \ + xy[j++] = x; \ + xy[j++] = y; \ + } else { \ + PyErr_SetString(PyExc_ValueError, "incorrect coordinate type"); \ + if (decref) { \ + Py_DECREF(op); \ + } \ + free(xy); \ + return -1; \ + } /* Copy table to path array */ if (PyList_Check(data)) { From 0099de0ed904c2021850fbb5a19908c36f908890 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 25 Apr 2024 16:00:14 +0300 Subject: [PATCH 102/195] Add deprecation helper for Image.new with BGR; modes --- Tests/test_image.py | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index ed80be503..e8339424d 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -63,14 +63,19 @@ image_modes = ( image_mode_names = [name for name, _ in image_modes] +# Deprecation helper +def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: + if mode.startswith("BGR;"): + with pytest.warns(DeprecationWarning): + return Image.new(mode, size) + else: + return Image.new(mode, size) + + class TestImage: @pytest.mark.parametrize("mode", image_mode_names) def test_image_modes_success(self, mode: str) -> None: - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - Image.new(mode, (1, 1)) - else: - Image.new(mode, (1, 1)) + helper_image_new(mode, (1, 1)) @pytest.mark.parametrize("mode", ("", "bad", "very very long")) def test_image_modes_fail(self, mode: str) -> None: @@ -1066,29 +1071,17 @@ class TestImageBytes: im = hopper(mode) source_bytes = im.tobytes() - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - reloaded = Image.new(mode, im.size) - else: - reloaded = Image.new(mode, im.size) + reloaded = helper_image_new(mode, im.size) reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes @pytest.mark.parametrize(("mode", "pixelsize"), image_modes) def test_getdata_putdata(self, mode: str, pixelsize: int) -> None: - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - im = Image.new(mode, (2, 2)) - else: - im = Image.new(mode, (2, 2)) + im = helper_image_new(mode, (2, 2)) source_bytes = bytes(range(im.width * im.height * pixelsize)) im.frombytes(source_bytes) - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - reloaded = Image.new(mode, im.size) - else: - reloaded = Image.new(mode, im.size) + reloaded = helper_image_new(mode, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) From a4080a72494042183cff6fbadeba3026fb6fa711 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 25 Apr 2024 08:51:33 -0500 Subject: [PATCH 103/195] clean up comments in test_image_access.py --- Tests/test_image_access.py | 40 +++++++++++++------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 50afb2a23..2cd2e0c34 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -33,7 +33,7 @@ except ImportError: class AccessTest: - # initial value + # Initial value _init_cffi_access = Image.USE_CFFI_ACCESS _need_cffi_access = False @@ -151,7 +151,7 @@ class TestImageGetPixel(AccessTest): self.color(mode) if expected_color_int is None else expected_color_int ) - # check putpixel + # Check putpixel im = Image.new(mode, (1, 1), None) im.putpixel((0, 0), expected_color) actual_color = im.getpixel((0, 0)) @@ -160,7 +160,7 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # check putpixel negative index + # Check putpixel negative index im.putpixel((-1, -1), expected_color) actual_color = im.getpixel((-1, -1)) assert actual_color == expected_color, ( @@ -168,7 +168,7 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # check 0x0 image with None initial color + # Check 0x0 image with None initial color im = Image.new(mode, (0, 0), None) assert im.load() is not None error = ValueError if self._need_cffi_access else IndexError @@ -176,13 +176,13 @@ class TestImageGetPixel(AccessTest): im.putpixel((0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) - # check negative index + # Check negative index with pytest.raises(error): im.putpixel((-1, -1), expected_color) with pytest.raises(error): im.getpixel((-1, -1)) - # check initial color + # Check initial color im = Image.new(mode, (1, 1), expected_color) actual_color = im.getpixel((0, 0)) assert actual_color == expected_color, ( @@ -190,18 +190,18 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # check initial color negative index + # Check initial color negative index actual_color = im.getpixel((-1, -1)) assert actual_color == expected_color, ( f"initial color failed with negative index for mode {mode}, " f"expected {expected_color} got {actual_color}" ) - # check 0x0 image with initial color + # Check 0x0 image with initial color im = Image.new(mode, (0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) - # check negative index + # Check negative index with pytest.raises(error): im.getpixel((-1, -1)) @@ -216,7 +216,7 @@ class TestImageGetPixel(AccessTest): @pytest.mark.parametrize("mode", ("I;16", "I;16B")) @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) def test_signedness(self, mode: str, expected_color: int) -> None: - # see https://github.com/python-pillow/Pillow/issues/452 + # See https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* self.check(mode, expected_color) @@ -276,13 +276,6 @@ class TestCffi(AccessTest): im = Image.new(mode, (10, 10), 40000) self._test_get_access(im) - # These don't actually appear to be modes that I can actually make, - # as unpack sets them directly into the I mode. - # im = Image.new('I;32L', (10, 10), -2**10) - # self._test_get_access(im) - # im = Image.new('I;32B', (10, 10), 2**10) - # self._test_get_access(im) - def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None: """Are we writing the correct bits into the image? @@ -314,23 +307,18 @@ class TestCffi(AccessTest): self._test_set_access(hopper("LA"), (128, 128)) self._test_set_access(hopper("1"), 255) self._test_set_access(hopper("P"), 128) - # self._test_set_access(i, (128, 128)) #PA -- undone how to make + self._test_set_access(hopper("PA"), (128, 128)) self._test_set_access(hopper("F"), 1024.0) for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): im = Image.new(mode, (10, 10), 40000) self._test_set_access(im, 45000) - # im = Image.new('I;32L', (10, 10), -(2**10)) - # self._test_set_access(im, -(2**13)+1) - # im = Image.new('I;32B', (10, 10), 2**10) - # self._test_set_access(im, 2**13-1) - @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_not_implemented(self) -> None: assert PyAccess.new(hopper("BGR;15")) is None - # ref https://github.com/python-pillow/Pillow/pull/2009 + # Ref https://github.com/python-pillow/Pillow/pull/2009 def test_reference_counting(self) -> None: size = 10 @@ -339,7 +327,7 @@ class TestCffi(AccessTest): with pytest.warns(DeprecationWarning): px = Image.new("L", (size, 1), 0).load() for i in range(size): - # pixels can contain garbage if image is released + # Pixels can contain garbage if image is released assert px[i, 0] == 0 @pytest.mark.parametrize("mode", ("P", "PA")) @@ -456,7 +444,7 @@ int main(int argc, char* argv[]) env = os.environ.copy() env["PATH"] = sys.prefix + ";" + env["PATH"] - # do not display the Windows Error Reporting dialog + # Do not display the Windows Error Reporting dialog getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) process = subprocess.Popen(["embed_pil.exe"], env=env) From c0cb417a44ac705e573e56582e7d9979dccf93ec Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 25 Apr 2024 21:08:24 +0300 Subject: [PATCH 104/195] Add semicolons to fix indent --- src/_imagingcms.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index ba8c81005..dbf7057c5 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -385,7 +385,7 @@ _buildTransform( iRenderingIntent, cmsFLAGS); - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; if (!hTransform) { PyErr_SetString(PyExc_ValueError, "cannot build transform"); @@ -419,7 +419,7 @@ _buildProofTransform( iProofIntent, cmsFLAGS); - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; if (!hTransform) { PyErr_SetString(PyExc_ValueError, "cannot build proof transform"); From 1420e725664114680ce72ad5b9542229faa39a0f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 18:08:50 +0000 Subject: [PATCH 105/195] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_imagingcms.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index dbf7057c5..1a18525d0 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -387,7 +387,7 @@ _buildTransform( Py_END_ALLOW_THREADS; - if (!hTransform) { + if (!hTransform) { PyErr_SetString(PyExc_ValueError, "cannot build transform"); } @@ -421,7 +421,7 @@ _buildProofTransform( Py_END_ALLOW_THREADS; - if (!hTransform) { + if (!hTransform) { PyErr_SetString(PyExc_ValueError, "cannot build proof transform"); } From c3ded3abdaa64d4ba75445b43d09d4fcf3129d73 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 Apr 2024 09:13:00 +1000 Subject: [PATCH 106/195] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 85dc0b43c..c5df1f8f7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 10.4.0 (unreleased) ------------------- +- Deprecate BGR;15, BGR;16 and BGR;24 modes #7978 + [radarhere, hugovk] + +- Fix ImagingAccess for I;16N on big-endian #7921 + [Yay295, radarhere] + - Support reading P mode TIFF images with padding #7996 [radarhere] From 8cc48b24fe83dc81e6a5e6a83f287d333d74a44f Mon Sep 17 00:00:00 2001 From: Cees Timmerman Date: Fri, 26 Apr 2024 17:17:44 +0200 Subject: [PATCH 107/195] Update ExifTags.py Fixed typo. No other instances in this repo. --- src/PIL/ExifTags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 60a4d9774..39b4aa552 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -346,7 +346,7 @@ class Interop(IntEnum): InteropVersion = 2 RelatedImageFileFormat = 4096 RelatedImageWidth = 4097 - RleatedImageHeight = 4098 + RelatedImageHeight = 4098 class IFD(IntEnum): From 86fb383739597acafc5e452e38eb3b87370d5eb1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Apr 2024 14:08:36 +1000 Subject: [PATCH 108/195] Corrected big-endian check --- Tests/test_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index b7f814080..e1490d6a0 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1050,7 +1050,7 @@ class TestImageBytes: @pytest.mark.parametrize("mode", modes) def test_getdata_putdata(self, mode: str) -> None: - if is_big_endian and mode == "BGR;15": + if is_big_endian() and mode == "BGR;15": pytest.xfail("Known failure of BGR;15 on big-endian") im = hopper(mode) reloaded = helper_image_new(mode, im.size) From 39da704c61b4ae5ec9c4117d34a8eda8f058a18e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Apr 2024 07:10:15 +1000 Subject: [PATCH 109/195] Updated libimagequant to 4.3.1 --- depends/install_imagequant.sh | 2 +- docs/installation/building-from-source.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 973b4374f..9dd7742ed 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -2,7 +2,7 @@ # install libimagequant archive_name=libimagequant -archive_version=4.3.0 +archive_version=4.3.1 archive=$archive_name-$archive_version diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 961312b14..7f7dfa6ff 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -68,7 +68,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.3** + * Pillow has been tested with libimagequant **2.6-4.3.1** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From fd8c6a629594968d02b845a4fa2fa52540a86c7e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Apr 2024 13:51:32 +1000 Subject: [PATCH 110/195] Do not indent goto labels --- .clang-format | 1 + src/libImaging/GifEncode.c | 16 ++++++++-------- src/libImaging/JpegEncode.c | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.clang-format b/.clang-format index be32e6d1a..3199e330b 100644 --- a/.clang-format +++ b/.clang-format @@ -9,6 +9,7 @@ BinPackParameters: false BreakBeforeBraces: Attach ColumnLimit: 88 DerivePointerAlignment: false +IndentGotoLabels: false IndentWidth: 4 Language: Cpp PointerAlignment: Right diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c index 831ce432c..9e91944fe 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -82,7 +82,7 @@ glzwe( UINT32 end_of_data) { switch (st->entry_state) { case LZW_TRY_IN1: - get_first_byte: +get_first_byte: if (!*in_avail) { if (end_of_data) { goto end_of_data; @@ -94,7 +94,7 @@ glzwe( (*in_avail)--; case LZW_TRY_IN2: - encode_loop: +encode_loop: if (!*in_avail) { if (end_of_data) { st->code = st->head; @@ -127,7 +127,7 @@ glzwe( st->code = st->head; st->put_state = PUT_HEAD; goto put_code; - insert_code_or_clear: /* jump here after put_code */ +insert_code_or_clear: /* jump here after put_code */ if (st->next_code < CODE_LIMIT) { st->codes[st->probe] = (st->next_code << 20) | (st->head << 8) | st->tail; @@ -140,7 +140,7 @@ glzwe( st->code = st->clear_code; st->put_state = PUT_CLEAR; goto put_code; - reset_after_clear: /* jump here after put_code */ +reset_after_clear: /* jump here after put_code */ glzwe_reset(st); } st->head = st->tail; @@ -150,9 +150,9 @@ glzwe( glzwe_reset(st); st->code = st->clear_code; st->put_state = PUT_INIT_CLEAR; - put_code: +put_code: st->code_bits_left = st->code_width; - check_buf_bits: +check_buf_bits: if (!st->buf_bits_left) { /* out buffer full */ case LZW_TRY_OUT1: @@ -190,11 +190,11 @@ glzwe( return GLZW_INTERNAL_ERROR; } - end_of_data: +end_of_data: st->code = st->end_code; st->put_state = PUT_END; goto put_code; - flush_code_buffer: /* jump here after put_code */ +flush_code_buffer: /* jump here after put_code */ if (st->buf_bits_left < 8) { case LZW_TRY_OUT2: if (!*out_avail) { diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index bcbe65aa4..ba8353c2d 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -346,7 +346,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { } jpeg_finish_compress(&context->cinfo); - cleanup: +cleanup: /* Clean up */ if (context->comment) { free(context->comment); From 5597f618a3b5cf8e208dded44a2107313c47122d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 28 Apr 2024 02:49:42 -0600 Subject: [PATCH 111/195] Change comment style Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/libImaging/GifEncode.c | 4 ++-- src/libImaging/TiffDecode.c | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c index 9e91944fe..45b67616d 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -116,8 +116,8 @@ encode_loop: st->head = st->codes[st->probe] >> 20; goto encode_loop; } else { - /* Reprobe decrement must be non-zero and relatively prime to table - * size. So, any odd positive number for power-of-2 size. */ + // 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/TiffDecode.c b/src/libImaging/TiffDecode.c index 858de9332..e6b57e0a7 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -414,8 +414,7 @@ _decodeTile( if (tile_bytes_size > ((tile_length * state->bits / planes + 7) / 8) * tile_width) { // If the tile size as expected by LibTiff isn't what we're expecting, abort. // man: TIFFTileSize returns the equivalent size for a tile of data as it - // would be returned in a - // call to TIFFReadTile ... + // would be returned in a call to TIFFReadTile ... state->errcode = IMAGING_CODEC_BROKEN; return -1; } @@ -507,8 +506,7 @@ _decodeStrip( if (strip_size > (unpacker_row_byte_size * rows_per_strip)) { // If the strip size as expected by LibTiff isn't what we're expecting, abort. // man: TIFFStripSize returns the equivalent size for a strip of data as it - // would be returned in a - // call to TIFFReadEncodedStrip ... + // would be returned in a call to TIFFReadEncodedStrip ... state->errcode = IMAGING_CODEC_BROKEN; return -1; } From 996c053d8995893bcdac7673f0469091990c8f25 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Apr 2024 22:49:56 +1000 Subject: [PATCH 112/195] Change comment style --- src/libImaging/FliDecode.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index debe7ddd8..6b2518d35 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -231,9 +231,9 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt } /* Note, have to check Data + size, not just ptr + size) */ if (data + (state->xsize * state->ysize) > ptr + bytes) { - /* not enough data for frame */ - /* UNDONE Unclear that we're actually going to leave the buffer at - * the right place. */ + // not enough data for frame + // UNDONE Unclear that we're actually going to leave the buffer at + // the right place. return ptr - buf; /* bytes consumed */ } for (y = 0; y < state->ysize; y++) { From d01e43e796e3d35e6c562145cef13f3e1ffc646d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Apr 2024 09:11:33 +1000 Subject: [PATCH 113/195] Removed direct invocation of setup.py --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 477d92609..1f9b4a370 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,6 @@ .PHONY: clean clean: - python3 setup.py clean rm src/PIL/*.so || true rm -r build || true find . -name __pycache__ | xargs rm -r || true From 36869833c723a6ff54de74ec5c5d630a770d6d1d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Apr 2024 23:06:39 +1000 Subject: [PATCH 114/195] Added Ubuntu 24.04 --- .github/workflows/test-docker.yml | 13 +++++++------ .github/workflows/test-valgrind.yml | 4 ++-- docs/installation/platform-support.rst | 4 +++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 8f4a4d090..c53f23a9f 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -36,8 +36,8 @@ jobs: docker: [ # Run slower jobs first to give them a headstart and reduce waiting time ubuntu-22.04-jammy-arm64v8, - ubuntu-22.04-jammy-ppc64le, - ubuntu-22.04-jammy-s390x, + ubuntu-24.04-noble-ppc64le, + ubuntu-24.04-noble-s390x, # Then run the remainder alpine, amazon-2-amd64, @@ -52,14 +52,15 @@ jobs: gentoo, ubuntu-20.04-focal-amd64, ubuntu-22.04-jammy-amd64, + ubuntu-24.04-noble-amd64, ] dockerTag: [main] include: - docker: "ubuntu-22.04-jammy-arm64v8" qemu-arch: "aarch64" - - docker: "ubuntu-22.04-jammy-ppc64le" + - docker: "ubuntu-24.04-noble-ppc64le" qemu-arch: "ppc64le" - - docker: "ubuntu-22.04-jammy-s390x" + - docker: "ubuntu-24.04-noble-s390x" qemu-arch: "s390x" name: ${{ matrix.docker }} @@ -81,8 +82,8 @@ jobs: - name: Docker build run: | - # The Pillow user in the docker container is UID 1000 - sudo chown -R 1000 $GITHUB_WORKSPACE + # The Pillow user in the docker container is UID 1001 + sudo chown -R 1001 $GITHUB_WORKSPACE docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} sudo chown -R runner $GITHUB_WORKSPACE diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 59bb958ec..63aec586b 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -50,7 +50,7 @@ jobs: - name: Build and Run Valgrind run: | - # The Pillow user in the docker container is UID 1000 - sudo chown -R 1000 $GITHUB_WORKSPACE + # The Pillow user in the docker container is UID 1001 + sudo chown -R 1001 $GITHUB_WORKSPACE docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} sudo chown -R runner $GITHUB_WORKSPACE diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index c08a53a43..888966c51 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -47,7 +47,9 @@ These platforms are built and tested for every change. | Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, 3.13, PyPy3 | | | +----------------------------+---------------------+ -| | 3.10 | arm64v8, ppc64le, | +| | 3.10 | arm64v8 | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, | | | | s390x | +----------------------------------+----------------------------+---------------------+ | Windows Server 2016 | 3.8 | x86-64 | From 65d73ea970c31c33e23a55273dbdea24376efe04 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Apr 2024 18:54:16 +1000 Subject: [PATCH 115/195] Python 3.8 and 3.9 are tested on macOS 13 --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index c08a53a43..02c409356 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -37,7 +37,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 12 Monterey | 3.8, 3.9 | x86-64 | +| macOS 13 Ventura | 3.8, 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | | | PyPy3 | | From 2250fbeb9a8a1b61c9ec8e7df0902a0c35bc495e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Apr 2024 20:19:05 +1000 Subject: [PATCH 116/195] Added type hints --- src/PIL/Image.py | 12 +++++++----- src/PIL/ImageFile.py | 2 +- src/PIL/JpegImagePlugin.py | 8 +++++--- src/PIL/PngImagePlugin.py | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a17edfa39..33b3da9a6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,7 +41,7 @@ import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, cast +from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, cast # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -877,7 +877,7 @@ class Image: return self.pyaccess return self.im.pixel_access(self.readonly) - def verify(self): + def verify(self) -> None: """ Verifies the contents of a file. For data read from a file, this method attempts to determine if the file is broken, without @@ -1267,7 +1267,9 @@ class Image: return im.crop((x0, y0, x1, y1)) - def draft(self, mode, size): + def draft( + self, mode: str, size: tuple[int, int] + ) -> tuple[str, tuple[int, int, float, float]] | None: """ Configures the image file loader so it returns a version of the image that as closely as possible matches the given mode and @@ -1290,7 +1292,7 @@ class Image: """ pass - def _expand(self, xmargin, ymargin=None): + def _expand(self, xmargin: int, ymargin: int | None = None) -> Image: if ymargin is None: ymargin = xmargin self.load() @@ -3450,7 +3452,7 @@ def eval(image, *args): return image.point(args[0]) -def merge(mode, bands): +def merge(mode: str, bands: Sequence[Image]) -> Image: """ Merge a set of single band images into a new multiband image. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 0283fa2fd..27885e654 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -163,7 +163,7 @@ class ImageFile(Image.Image): self.tile = [] super().__setstate__(state) - def verify(self): + def verify(self) -> None: """Check file integrity""" # raise exception if something's wrong. must be called diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index e3c0083e9..715a358a3 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -424,13 +424,15 @@ class JpegImageFile(ImageFile.ImageFile): return s - def draft(self, mode, size): + def draft( + self, mode: str, size: tuple[int, int] + ) -> tuple[str, tuple[int, int, float, float]] | None: if len(self.tile) != 1: - return + return None # Protect from second call if self.decoderconfig: - return + return None d, e, o, a = self.tile[0] scale = 1 diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 8b81e54ea..012e0b61b 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -783,7 +783,7 @@ class PngImageFile(ImageFile.ImageFile): self.seek(frame) return self._text - def verify(self): + def verify(self) -> None: """Verify PNG file""" if self.fp is None: From e8cddfbc6a2a6167e395fc8cfad9ea29a38f108b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 3 May 2024 08:45:39 +1000 Subject: [PATCH 117/195] Updated codecov/codecov-action to v4 --- .github/workflows/test-cygwin.yml | 3 ++- .github/workflows/test-docker.yml | 3 ++- .github/workflows/test-mingw.yml | 3 ++- .github/workflows/test-windows.yml | 3 ++- .github/workflows/test.yml | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 9674a4665..7972730ca 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -132,11 +132,12 @@ jobs: bash.exe .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v3.1.5 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: GHA_Cygwin name: Cygwin Python 3.${{ matrix.python-minor-version }} + token: ${{ secrets.CODECOV_ORG_TOKEN }} success: permissions: diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index c53f23a9f..6afed74db 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -100,11 +100,12 @@ jobs: MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage - uses: codecov/codecov-action@v3.1.5 + uses: codecov/codecov-action@v4 with: flags: GHA_Docker name: ${{ matrix.docker }} gcov: true + token: ${{ secrets.CODECOV_ORG_TOKEN }} success: permissions: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a07a27c46..a773ca453 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -85,8 +85,9 @@ jobs: python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests - name: Upload coverage - uses: codecov/codecov-action@v3.1.5 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: GHA_Windows name: "MSYS2 MinGW" + token: ${{ secrets.CODECOV_ORG_TOKEN }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 40994c60a..9edc15173 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -213,11 +213,12 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v3.1.5 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: GHA_Windows name: ${{ runner.os }} Python ${{ matrix.python-version }} + token: ${{ secrets.CODECOV_ORG_TOKEN }} success: permissions: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4573fde90..aa5646caf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -150,11 +150,12 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v3.1.5 + uses: codecov/codecov-action@v4 with: flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} gcov: true + token: ${{ secrets.CODECOV_ORG_TOKEN }} success: permissions: From ac1eb57c03e182752e1207cd477300650fce4dc0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 3 May 2024 09:43:50 +1000 Subject: [PATCH 118/195] Install git --- .github/workflows/test-cygwin.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7972730ca..1269ef8cb 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -55,6 +55,7 @@ jobs: packages: > gcc-g++ ghostscript + git ImageMagick jpeg libfreetype-devel From 6036d81d973e7b0a4613bb06d4a2d79310eef799 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 4 May 2024 20:51:54 +1000 Subject: [PATCH 119/195] Added type hints --- src/PIL/BlpImagePlugin.py | 6 +++--- src/PIL/BmpImagePlugin.py | 4 ++-- src/PIL/DcxImagePlugin.py | 4 ++-- src/PIL/DdsImagePlugin.py | 2 +- src/PIL/EpsImagePlugin.py | 4 ++-- src/PIL/FliImagePlugin.py | 4 ++-- src/PIL/GifImagePlugin.py | 12 ++++++------ src/PIL/Hdf5StubImagePlugin.py | 2 +- src/PIL/IcnsImagePlugin.py | 2 +- src/PIL/IcoImagePlugin.py | 2 +- src/PIL/ImImagePlugin.py | 4 ++-- src/PIL/Image.py | 7 +++++-- src/PIL/ImageFile.py | 10 +++++----- src/PIL/ImageFilter.py | 5 ++++- src/PIL/ImageWin.py | 4 ++-- src/PIL/MicImagePlugin.py | 4 ++-- src/PIL/MpoImagePlugin.py | 4 ++-- src/PIL/PngImagePlugin.py | 8 ++++---- src/PIL/PsdImagePlugin.py | 9 ++++----- src/PIL/SpiderImagePlugin.py | 6 +++--- src/PIL/TiffImagePlugin.py | 20 ++++++++++---------- src/PIL/WalImageFile.py | 2 +- src/PIL/WebPImagePlugin.py | 4 ++-- 23 files changed, 67 insertions(+), 62 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 8d351ce91..bdf54baae 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -253,7 +253,7 @@ class BlpImageFile(ImageFile.ImageFile): format = "BLP" format_description = "Blizzard Mipmap Format" - def _open(self): + def _open(self) -> None: self.magic = self.fp.read(4) self.fp.seek(5, os.SEEK_CUR) @@ -333,7 +333,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): class BLP1Decoder(_BLPBaseDecoder): - def _load(self): + def _load(self) -> None: if self._blp_compression == Format.JPEG: self._decode_jpeg_stream() @@ -418,7 +418,7 @@ class BLP2Decoder(_BLPBaseDecoder): class BLPEncoder(ImageFile.PyEncoder): _pushes_fd = True - def _write_palette(self): + def _write_palette(self) -> bytes: data = b"" palette = self.im.getpalette("RGBA", "RGBA") for i in range(len(palette) // 4): diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 9ce0fed88..c5d1cd40d 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -283,7 +283,7 @@ class BmpImageFile(ImageFile.ImageFile): ) ] - def _open(self): + def _open(self) -> None: """Open file, check magic number and read header""" # read 14 bytes: magic number, filesize, reserved, header final offset head_data = self.fp.read(14) @@ -376,7 +376,7 @@ class DibImageFile(BmpImageFile): format = "DIB" format_description = "Windows Bitmap" - def _open(self): + def _open(self) -> None: self._bitmap() diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index b24c16329..1c455b032 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -63,7 +63,7 @@ class DcxImageFile(PcxImageFile): self.is_animated = self.n_frames > 1 self.seek(0) - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return self.frame = frame @@ -71,7 +71,7 @@ class DcxImageFile(PcxImageFile): self.fp.seek(self._offset[frame]) PcxImageFile._open(self) - def tell(self): + def tell(self) -> int: return self.frame diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 3032e4aec..59ee0f8a0 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -331,7 +331,7 @@ class DdsImageFile(ImageFile.ImageFile): format = "DDS" format_description = "DirectDraw Surface" - def _open(self): + def _open(self) -> None: if not _accept(self.fp.read(4)): msg = "not a DDS file" raise SyntaxError(msg) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index ec6705742..b57daca56 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -178,7 +178,7 @@ class PSFile: self.char = None self.fp.seek(offset, whence) - def readline(self): + def readline(self) -> str: s = [self.char or b""] self.char = None @@ -212,7 +212,7 @@ class EpsImageFile(ImageFile.ImageFile): mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"} - def _open(self): + def _open(self) -> None: (length, offset) = self._find_offset(self.fp) # go to offset - start of "%!PS" diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 7a233d015..eea2c0c95 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -123,7 +123,7 @@ class FliImageFile(ImageFile.ImageFile): palette[i] = (r, g, b) i += 1 - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return if frame < self.__frame: @@ -162,7 +162,7 @@ class FliImageFile(ImageFile.ImageFile): self.__offset += framesize - def tell(self): + def tell(self) -> int: return self.__frame diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 93be7fefb..26e595819 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -76,7 +76,7 @@ class GifImageFile(ImageFile.ImageFile): global_palette = None - def data(self): + def data(self) -> bytes | None: s = self.fp.read(1) if s and s[0]: return self.fp.read(s[0]) @@ -88,7 +88,7 @@ class GifImageFile(ImageFile.ImageFile): return True return False - def _open(self): + def _open(self) -> None: # Screen s = self.fp.read(13) if not _accept(s): @@ -147,7 +147,7 @@ class GifImageFile(ImageFile.ImageFile): self.seek(current) return self._is_animated - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return if frame < self.__frame: @@ -417,7 +417,7 @@ class GifImageFile(ImageFile.ImageFile): elif k in self.info: del self.info[k] - def load_prepare(self): + def load_prepare(self) -> None: temp_mode = "P" if self._frame_palette else "L" self._prev_im = None if self.__frame == 0: @@ -437,7 +437,7 @@ class GifImageFile(ImageFile.ImageFile): super().load_prepare() - def load_end(self): + def load_end(self) -> None: if self.__frame == 0: if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: if self._frame_transparency is not None: @@ -463,7 +463,7 @@ class GifImageFile(ImageFile.ImageFile): else: self.im.paste(frame_im, self.dispose_extent) - def tell(self): + def tell(self) -> int: return self.__frame diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index f50e6bf16..afbfd1639 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -37,7 +37,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile): format = "HDF5" format_description = "HDF5" - def _open(self): + def _open(self) -> None: offset = self.fp.tell() if not _accept(self.fp.read(8)): diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index c2c950863..0a86ba883 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -252,7 +252,7 @@ class IcnsImageFile(ImageFile.ImageFile): format = "ICNS" format_description = "Mac OS icns resource" - def _open(self): + def _open(self) -> None: self.icns = IcnsFile(self.fp) self._mode = "RGBA" self.info["sizes"] = self.icns.itersizes() diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 82b190eb8..eacffbae6 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -302,7 +302,7 @@ class IcoImageFile(ImageFile.ImageFile): format = "ICO" format_description = "Windows Icon" - def _open(self): + def _open(self) -> None: self.ico = IcoFile(self.fp) self.info["sizes"] = self.ico.sizes() self.size = self.ico.entry[0]["dim"] diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 4613e40b6..0de7d6492 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -278,7 +278,7 @@ class ImImageFile(ImageFile.ImageFile): def is_animated(self): return self.info[FRAMES] > 1 - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return @@ -296,7 +296,7 @@ class ImImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] - def tell(self): + def tell(self) -> int: return self.frame diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 33b3da9a6..0f2e146bb 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1298,7 +1298,10 @@ class Image: self.load() return self._new(self.im.expand(xmargin, ymargin)) - def filter(self, filter): + if TYPE_CHECKING: + from . import ImageFilter + + def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: """ Filters this image using the given filter. For a list of available filters, see the :py:mod:`~PIL.ImageFilter` module. @@ -1310,7 +1313,7 @@ class Image: self.load() - if isinstance(filter, Callable): + if callable(filter): filter = filter() if not hasattr(filter, "filter"): msg = "filter argument should be ImageFilter.Filter instance or class" diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 27885e654..b93e2ad2c 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -311,7 +311,7 @@ class ImageFile(Image.Image): return Image.Image.load(self) - def load_prepare(self): + def load_prepare(self) -> None: # create image memory if necessary if not self.im or self.im.mode != self.mode or self.im.size != self.size: self.im = Image.core.new(self.mode, self.size) @@ -319,7 +319,7 @@ class ImageFile(Image.Image): if self.mode == "P": Image.Image.load(self) - def load_end(self): + def load_end(self) -> None: # may be overridden pass @@ -390,7 +390,7 @@ class Parser: offset = 0 finished = 0 - def reset(self): + def reset(self) -> None: """ (Consumer) Reset the parser. Note that you can only call this method immediately after you've created a parser; parser @@ -605,7 +605,7 @@ def _safe_read(fp, size): class PyCodecState: - def __init__(self): + def __init__(self) -> None: self.xsize = 0 self.ysize = 0 self.xoff = 0 @@ -634,7 +634,7 @@ class PyCodec: """ self.args = args - def cleanup(self): + def cleanup(self) -> None: """ Override to perform codec specific cleanup diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index b2c4950d6..fa9ebd9de 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -16,11 +16,14 @@ # from __future__ import annotations +import abc import functools class Filter: - pass + @abc.abstractmethod + def filter(self, image): + pass class MultibandFilter(Filter): diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index 75910d2d9..2c439038d 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -204,7 +204,7 @@ class Window: def ui_handle_damage(self, x0, y0, x1, y1): pass - def ui_handle_destroy(self): + def ui_handle_destroy(self) -> None: pass def ui_handle_repair(self, dc, x0, y0, x1, y1): @@ -213,7 +213,7 @@ class Window: def ui_handle_resize(self, width, height): pass - def mainloop(self): + def mainloop(self) -> None: Image.core.eventloop() diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 96de386a8..5aef94dfb 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -38,7 +38,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): format_description = "Microsoft Image Composer" _close_exclusive_fp_after_loading = False - def _open(self): + def _open(self) -> None: # read the OLE directory and see if this is a likely # to be a Microsoft Image Composer file @@ -88,7 +88,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): def tell(self): return self.frame - def close(self): + def close(self) -> None: self.__fp.close() self.ole.close() super().close() diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index ac9820bbf..fb6620e75 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -127,7 +127,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): def load_seek(self, pos): self._fp.seek(pos) - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return self.fp = self._fp @@ -149,7 +149,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])] self.__frame = frame - def tell(self): + def tell(self) -> int: return self.__frame @staticmethod diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 012e0b61b..39faa0f78 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -800,7 +800,7 @@ class PngImageFile(ImageFile.ImageFile): self.fp.close() self.fp = None - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return if frame < self.__frame: @@ -909,10 +909,10 @@ class PngImageFile(ImageFile.ImageFile): else: self.dispose = None - def tell(self): + def tell(self) -> int: return self.__frame - def load_prepare(self): + def load_prepare(self) -> None: """internal: prepare to read PNG file""" if self.info.get("interlace"): @@ -954,7 +954,7 @@ class PngImageFile(ImageFile.ImageFile): return self.fp.read(read_bytes) - def load_end(self): + def load_end(self) -> None: """internal: finished reading image data""" if self.__idat != 0: self.fp.read(self.__idat) diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index b15918313..86c1a6763 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -57,7 +57,7 @@ class PsdImageFile(ImageFile.ImageFile): format_description = "Adobe Photoshop" _close_exclusive_fp_after_loading = False - def _open(self): + def _open(self) -> None: read = self.fp.read # @@ -141,23 +141,22 @@ class PsdImageFile(ImageFile.ImageFile): self.frame = 1 self._min_frame = 1 - def seek(self, layer): + def seek(self, layer: int) -> None: if not self._seek_check(layer): return # seek to given layer (1..max) try: - name, mode, bbox, tile = self.layers[layer - 1] + _, mode, _, tile = self.layers[layer - 1] self._mode = mode self.tile = tile self.frame = layer self.fp = self._fp - return name, bbox except IndexError as e: msg = "no such layer" raise EOFError(msg) from e - def tell(self): + def tell(self) -> int: # return layer number (0=image, 1..max=layers) return self.frame diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 86582fb12..01a39e97c 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -97,7 +97,7 @@ class SpiderImageFile(ImageFile.ImageFile): format_description = "Spider 2D image" _close_exclusive_fp_after_loading = False - def _open(self): + def _open(self) -> None: # check header n = 27 * 4 # read 27 float values f = self.fp.read(n) @@ -165,13 +165,13 @@ class SpiderImageFile(ImageFile.ImageFile): return self._nimages > 1 # 1st image index is zero (although SPIDER imgnumber starts at 1) - def tell(self): + def tell(self) -> int: if self.imgnumber < 1: return 0 else: return self.imgnumber - 1 - def seek(self, frame): + def seek(self, frame: int) -> None: if self.istack == 0: msg = "attempt to seek in a non-stack file" raise EOFError(msg) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index c78c223b3..1be717de1 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1143,7 +1143,7 @@ class TiffImageFile(ImageFile.ImageFile): self.seek(current) return self._n_frames - def seek(self, frame): + def seek(self, frame: int) -> None: """Select a given frame as current image""" if not self._seek_check(frame): return @@ -1198,7 +1198,7 @@ class TiffImageFile(ImageFile.ImageFile): self.__frame = frame self._setup() - def tell(self): + def tell(self) -> int: """Return the current frame number""" return self.__frame @@ -1237,7 +1237,7 @@ class TiffImageFile(ImageFile.ImageFile): return self._load_libtiff() return super().load() - def load_end(self): + def load_end(self) -> None: # allow closing if we're on the first frame, there's no next # This is the ImageFile.load path only, libtiff specific below. if not self.is_animated: @@ -1942,7 +1942,7 @@ class AppendingTiffWriter: self.beginning = self.f.tell() self.setup() - def setup(self): + def setup(self) -> None: # Reset everything. self.f.seek(self.beginning, os.SEEK_SET) @@ -1967,7 +1967,7 @@ class AppendingTiffWriter: self.skipIFDs() self.goToEnd() - def finalize(self): + def finalize(self) -> None: if self.isFirst: return @@ -1990,7 +1990,7 @@ class AppendingTiffWriter: self.f.seek(ifd_offset) self.fixIFD() - def newFrame(self): + def newFrame(self) -> None: # Call this to finish a frame. self.finalize() self.setup() @@ -2013,7 +2013,7 @@ class AppendingTiffWriter: self.f.seek(offset, whence) return self.tell() - def goToEnd(self): + def goToEnd(self) -> None: self.f.seek(0, os.SEEK_END) pos = self.f.tell() @@ -2029,7 +2029,7 @@ class AppendingTiffWriter: self.shortFmt = self.endian + "H" self.tagFormat = self.endian + "HHL" - def skipIFDs(self): + def skipIFDs(self) -> None: while True: ifd_offset = self.readLong() if ifd_offset == 0: @@ -2084,11 +2084,11 @@ class AppendingTiffWriter: msg = f"wrote only {bytes_written} bytes but wanted 4" raise RuntimeError(msg) - def close(self): + def close(self) -> None: self.finalize() self.f.close() - def fixIFD(self): + def fixIFD(self) -> None: num_tags = self.readShort() for i in range(num_tags): diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index c5bf3e04c..fbd7be6ed 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -32,7 +32,7 @@ class WalImageFile(ImageFile.ImageFile): format = "WAL" format_description = "Quake2 Texture" - def _open(self): + def _open(self) -> None: self._mode = "P" # read header fields diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 9c8d53336..61ae9eae5 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -109,7 +109,7 @@ class WebPImageFile(ImageFile.ImageFile): """ return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {} - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return @@ -174,7 +174,7 @@ class WebPImageFile(ImageFile.ImageFile): def load_seek(self, pos): pass - def tell(self): + def tell(self) -> int: if not _webp.HAVE_WEBPANIM: return super().tell() From 74063feadca98b847ee8e239531d2cfb73de12e5 Mon Sep 17 00:00:00 2001 From: mrKazzila Date: Sat, 4 May 2024 19:21:49 +0300 Subject: [PATCH 120/195] chore: add f-string formatting --- src/PIL/DdsImagePlugin.py | 8 ++++---- src/PIL/ImImagePlugin.py | 2 +- src/PIL/Image.py | 14 +++++++------- src/PIL/ImageMath.py | 4 ++-- src/PIL/ImageMode.py | 8 ++++---- src/PIL/ImageMorph.py | 2 +- src/PIL/ImageWin.py | 2 +- src/PIL/PalmImagePlugin.py | 2 +- src/PIL/PdfParser.py | 4 ++-- src/PIL/PngImagePlugin.py | 2 +- src/PIL/SpiderImagePlugin.py | 10 +++++----- src/PIL/TiffImagePlugin.py | 12 ++++++------ src/PIL/features.py | 4 ++-- 13 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 3032e4aec..2496088af 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -271,16 +271,16 @@ class D3DFMT(IntEnum): module = sys.modules[__name__] for item in DDSD: assert item.name is not None - setattr(module, "DDSD_" + item.name, item.value) + setattr(module, f"DDSD_{item.name}", item.value) for item1 in DDSCAPS: assert item1.name is not None - setattr(module, "DDSCAPS_" + item1.name, item1.value) + setattr(module, f"DDSCAPS_{item1.name}", item1.value) for item2 in DDSCAPS2: assert item2.name is not None - setattr(module, "DDSCAPS2_" + item2.name, item2.value) + setattr(module, f"DDSCAPS2_{item2.name}", item2.value) for item3 in DDPF: assert item3.name is not None - setattr(module, "DDPF_" + item3.name, item3.value) + setattr(module, f"DDPF_{item3.name}", item3.value) DDS_FOURCC = DDPF.FOURCC DDS_RGB = DDPF.RGB diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 4613e40b6..77b396387 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -196,7 +196,7 @@ class ImImageFile(ImageFile.ImageFile): n += 1 else: - msg = "Syntax error in IM header: " + s.decode("ascii", "replace") + msg = f"Syntax error in IM header: {s.decode('ascii', 'replace')}" raise SyntaxError(msg) if not n: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 33b3da9a6..2184ef8ea 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -405,7 +405,7 @@ def _getdecoder(mode, decoder_name, args, extra=()): try: # get decoder - decoder = getattr(core, decoder_name + "_decoder") + decoder = getattr(core, f"{decoder_name}_decoder") except AttributeError as e: msg = f"decoder {decoder_name} not available" raise OSError(msg) from e @@ -428,7 +428,7 @@ def _getencoder(mode, encoder_name, args, extra=()): try: # get encoder - encoder = getattr(core, encoder_name + "_encoder") + encoder = getattr(core, f"{encoder_name}_encoder") except AttributeError as e: msg = f"encoder {encoder_name} not available" raise OSError(msg) from e @@ -603,7 +603,7 @@ class Image: ) -> str: suffix = "" if format: - suffix = "." + format + suffix = f".{format}" if not file: f, filename = tempfile.mkstemp(suffix) @@ -2180,7 +2180,7 @@ class Image: (Resampling.HAMMING, "Image.Resampling.HAMMING"), ) ] - msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" raise ValueError(msg) if reducing_gap is not None and reducing_gap < 1.0: @@ -2825,7 +2825,7 @@ class Image: (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), ) ] - msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" raise ValueError(msg) image.load() @@ -3223,8 +3223,8 @@ _fromarray_typemap = { ((1, 1, 3), "|u1"): ("RGB", "RGB"), ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), # shortcuts: - ((1, 1), _ENDIAN + "i4"): ("I", "I"), - ((1, 1), _ENDIAN + "f4"): ("F", "F"), + ((1, 1), f"{_ENDIAN}i4"): ("I", "I"), + ((1, 1), f"{_ENDIAN}f4"): ("F", "F"), } diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 77472a24c..6664434ea 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -61,7 +61,7 @@ class _Operand: out = Image.new(mode or im_1.mode, im_1.size, None) im_1.load() try: - op = getattr(_imagingmath, op + "_" + im_1.mode) + op = getattr(_imagingmath, f"{op}_{im_1.mode}") except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e @@ -89,7 +89,7 @@ class _Operand: im_1.load() im_2.load() try: - op = getattr(_imagingmath, op + "_" + im_1.mode) + op = getattr(_imagingmath, f"{op}_{im_1.mode}") except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 7bd2afcf2..92a08d2cb 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -44,8 +44,8 @@ def getmode(mode: str) -> ModeDescriptor: # Bits need to be extended to bytes "1": ("L", "L", ("1",), "|b1"), "L": ("L", "L", ("L",), "|u1"), - "I": ("L", "I", ("I",), endian + "i4"), - "F": ("L", "F", ("F",), endian + "f4"), + "I": ("L", "I", ("I",), f"{endian}i4"), + "F": ("L", "F", ("F",), f"{endian}f4"), "P": ("P", "L", ("P",), "|u1"), "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), @@ -78,8 +78,8 @@ def getmode(mode: str) -> ModeDescriptor: "I;16LS": "u2", "I;16BS": ">i2", - "I;16N": endian + "u2", - "I;16NS": endian + "i2", + "I;16N": f"{endian}u2", + "I;16NS": f"{endian}i2", "I;32": "u4", "I;32L": " Date: Sat, 4 May 2024 19:26:22 +0300 Subject: [PATCH 121/195] chore: update __repr__ for PdfName --- src/PIL/PdfParser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index d6f2ebd44..1485cafe0 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -225,7 +225,7 @@ class PdfName: return hash(self.name) def __repr__(self): - return f"PdfName({repr(self.name)})" + return f"{self.__class__.__name__}({repr(self.name)})" @classmethod def from_pdf_stream(cls, data): From 71b8d99b3699bab3e7217af7453199853671409f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 4 May 2024 19:27:42 +0000 Subject: [PATCH 122/195] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/PdfParser.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 1485cafe0..c1ed78797 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -144,9 +144,7 @@ class XrefTable: elif key in self.deleted_entries: generation = self.deleted_entries[key] else: - msg = ( - f"object ID {key} cannot be deleted because it doesn't exist" - ) + msg = f"object ID {key} cannot be deleted because it doesn't exist" raise IndexError(msg) def __contains__(self, key): From b8e3e0a43059dc2c1a1b410ac87fca0772de3af1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 17:25:18 +0000 Subject: [PATCH 123/195] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.3.4 → v0.4.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.4...v0.4.3) - [github.com/psf/black-pre-commit-mirror: 24.3.0 → 24.4.2](https://github.com/psf/black-pre-commit-mirror/compare/24.3.0...24.4.2) - [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0) - [github.com/python-jsonschema/check-jsonschema: 0.28.1 → 0.28.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.28.1...0.28.2) - [github.com/tox-dev/pyproject-fmt: 1.7.0 → 1.8.0](https://github.com/tox-dev/pyproject-fmt/compare/1.7.0...1.8.0) --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 51625eb4c..1272913c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + rev: v0.4.3 hooks: - id: ruff args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.3.0 + rev: 24.4.2 hooks: - id: black @@ -29,7 +29,7 @@ repos: - id: rst-backticks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable @@ -43,7 +43,7 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.28.1 + rev: 0.28.2 hooks: - id: check-github-workflows - id: check-readthedocs @@ -55,7 +55,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 1.7.0 + rev: 1.8.0 hooks: - id: pyproject-fmt From b17f1e507b1e44246b89938e5e4b5d53716751f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 7 May 2024 14:01:08 +1000 Subject: [PATCH 124/195] Use f-strings --- Tests/test_file_eps.py | 4 +--- selftest.py | 4 ++-- src/PIL/IptcImagePlugin.py | 2 +- src/PIL/PdfParser.py | 5 +++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index d01884f96..1c21aa8ca 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -336,9 +336,7 @@ def test_readline_psfile(tmp_path: Path) -> None: strings = ["something", "else", "baz", "bif"] def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None: - ending = "Failure with line ending: %s" % ( - "".join("%s" % ord(s) for s in ending) - ) + ending = f"Failure with line ending: {''.join(str(ord(s)) for s in ending)}" assert t.readline().strip("\r\n") == "something", ending assert t.readline().strip("\r\n") == "else", ending assert t.readline().strip("\r\n") == "baz", ending diff --git a/selftest.py b/selftest.py index 661abcddb..9e049367e 100755 --- a/selftest.py +++ b/selftest.py @@ -165,9 +165,9 @@ if __name__ == "__main__": print("Running selftest:") status = doctest.testmod(sys.modules[__name__]) if status[0]: - print("*** %s tests of %d failed." % status) + print(f"*** {status[0]} tests of {status[1]} failed.") exit_status = 1 else: - print("--- %s tests passed." % status[1]) + print(f"--- {status[1]} tests passed.") sys.exit(exit_status) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 409609434..73df83bfb 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -57,7 +57,7 @@ def dump(c: Sequence[int | bytes]) -> None: """.. deprecated:: 10.2.0""" deprecate("IptcImagePlugin.dump", 12) for i in c: - print("%02x" % _i8(i), end=" ") + print(f"{_i8(i):02x}", end=" ") print() diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index c1ed78797..65db70e13 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -825,8 +825,9 @@ class PdfParser: try: stream_len = int(result[b"Length"]) except (TypeError, KeyError, ValueError) as e: - msg = "bad or missing Length in stream dict (%r)" % result.get( - b"Length", None + msg = ( + "bad or missing Length in stream dict " + f"({result.get(b'Length')})" ) raise PdfFormatError(msg) from e stream_data = data[m.end() : m.end() + stream_len] From 7d81cbd0ede0dd9e516f7c3b5e2a42988dd105b5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 7 May 2024 13:59:30 +1000 Subject: [PATCH 125/195] Do not use percent format --- Tests/test_image_access.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 02c75073a..f37ae6096 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -415,7 +415,9 @@ class TestEmbeddable: int main(int argc, char* argv[]) { - char *home = "%s"; + char *home = \"""" + + sys.prefix.replace("\\", "\\\\") + + """\"; wchar_t *whome = Py_DecodeLocale(home, NULL); Py_SetPythonHome(whome); @@ -432,7 +434,6 @@ int main(int argc, char* argv[]) return 0; } """ - % sys.prefix.replace("\\", "\\\\") ) compiler = getattr(build_ext, "new_compiler")() From c92f59d758e0a1e308b148f31dd3b7f3f68f94b4 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 7 May 2024 14:30:34 +0200 Subject: [PATCH 126/195] Add various type annotations --- src/PIL/Image.py | 58 +++++++++++++++++++++++++++++------------- src/PIL/ImageDraw.py | 19 +++++++------- src/PIL/ImageFont.py | 31 ++++++++++++---------- src/PIL/_imagingft.pyi | 37 ++++++++++++++++++++++++++- 4 files changed, 104 insertions(+), 41 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 2184ef8ea..f81e95695 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,7 +41,7 @@ import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, cast +from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, cast, overload # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -481,6 +481,8 @@ def _getscaleoffset(expr): # -------------------------------------------------------------------- # Implementation wrapper +class _GetDataTransform(Protocol): + def getdata(self) -> tuple[Transform, Sequence[int]]: ... class Image: """ @@ -1687,7 +1689,7 @@ class Image: return self.im.entropy(extrema) return self.im.entropy() - def paste(self, im, box=None, mask=None) -> None: + def paste(self, im: Image | str | int | tuple[int, ...], box: tuple[int, int, int, int] | tuple[int, int] | None = None, mask: Image | None = None) -> None: """ Pastes another image into this image. The box argument is either a 2-tuple giving the upper left corner, a 4-tuple defining the @@ -2122,7 +2124,7 @@ class Image: min(self.size[1], math.ceil(box[3] + support_y)), ) - def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image: + def resize(self, size: tuple[int, int], resample: Resampling | None = None, box: tuple[float, float, float, float] | None = None, reducing_gap: float | None = None) -> Image: """ Returns a resized copy of this image. @@ -2228,7 +2230,7 @@ class Image: return self._new(self.im.resize(size, resample, box)) - def reduce(self, factor, box=None): + def reduce(self, factor: int | tuple[int, int], box: tuple[int, int, int, int] | None = None) -> Image: """ Returns a copy of the image reduced ``factor`` times. If the size of the image is not dividable by ``factor``, @@ -2263,13 +2265,13 @@ class Image: def rotate( self, - angle, - resample=Resampling.NEAREST, - expand=0, - center=None, - translate=None, - fillcolor=None, - ): + angle: float, + resample: Resampling = Resampling.NEAREST, + expand: bool = False, + center: tuple[int, int] | None = None, + translate: tuple[int, int] | None = None, + fillcolor: float | tuple[float, ...] | str | None = None, + ) -> Image: """ Returns a rotated copy of this image. This method returns a copy of this image, rotated the given number of degrees counter @@ -2576,7 +2578,7 @@ class Image: """ return 0 - def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0): + def thumbnail(self, size: tuple[int, int], resample: Resampling = Resampling.BICUBIC, reducing_gap: float = 2.0) -> None: """ Make this image into a thumbnail. This method modifies the image to contain a thumbnail version of itself, no larger than @@ -2664,14 +2666,34 @@ class Image: # FIXME: the different transform methods need further explanation # instead of bloating the method docs, add a separate chapter. + @overload def transform( self, - size, - method, - data=None, - resample=Resampling.NEAREST, - fill=1, - fillcolor=None, + size: tuple[int, int], + method: Transform | ImageTransformHandler, + data: Sequence[int], + resample: Resampling = Resampling.NEAREST, + fill: int = 1, + fillcolor: float | tuple[float, ...] | str | None = None, + ) -> Image: ... + @overload + def transform( + self, + size: tuple[int, int], + method: _GetDataTransform, + data: None = None, + resample: Resampling = Resampling.NEAREST, + fill: int = 1, + fillcolor: float | tuple[float, ...] | str | None = None, + ) -> Image: ... + def transform( + self, + size: tuple[int, int], + method: Transform | ImageTransformHandler | _GetDataTransform, + data: Sequence[int] | None = None, + resample: Resampling = Resampling.NEAREST, + fill: int = 1, + fillcolor: float | tuple[float, ...] | str | None = None, ) -> Image: """ Transforms this image. This method creates a new image with the diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index d3efe6486..579489fde 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -34,10 +34,11 @@ from __future__ import annotations import math import numbers import struct -from typing import Sequence, cast +from typing import AnyStr, Sequence, cast from . import Image, ImageColor from ._typing import Coords +from .ImageFont import FreeTypeFont, ImageFont """ A simple 2D drawing interface for PIL images. @@ -92,7 +93,7 @@ class ImageDraw: self.fontmode = "L" # aliasing is okay for other modes self.fill = False - def getfont(self): + def getfont(self) -> FreeTypeFont | ImageFont: """ Get the current default font. @@ -450,12 +451,12 @@ class ImageDraw: right[3] -= r + 1 self.draw.draw_rectangle(right, ink, 1) - def _multiline_check(self, text) -> bool: + def _multiline_check(self, text: str | bytes) -> bool: split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text - def _multiline_split(self, text) -> list[str | bytes]: + def _multiline_split(self, text: AnyStr) -> list[AnyStr]: split_character = "\n" if isinstance(text, str) else b"\n" return text.split(split_character) @@ -469,7 +470,7 @@ class ImageDraw: def text( self, - xy, + xy: tuple[int, int], text, fill=None, font=None, @@ -591,7 +592,7 @@ class ImageDraw: def multiline_text( self, - xy, + xy: tuple[int, int], text, fill=None, font=None, @@ -678,15 +679,15 @@ class ImageDraw: def textlength( self, - text, - font=None, + text: str, + font: FreeTypeFont | ImageFont | None = None, direction=None, features=None, language=None, embedded_color=False, *, font_size=None, - ): + ) -> float: """Get the length of a given string, in pixels with 1/64 precision.""" if self._multiline_check(text): msg = "can't measure length of multiline text" diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 256c581df..536ee5fe6 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -33,12 +33,15 @@ import sys import warnings from enum import IntEnum from io import BytesIO -from typing import BinaryIO +from typing import TYPE_CHECKING, BinaryIO from . import Image from ._typing import StrOrBytesPath from ._util import is_directory, is_path +if TYPE_CHECKING: + from _imagingft import Font + class Layout(IntEnum): BASIC = 0 @@ -56,7 +59,7 @@ except ImportError as ex: core = DeferredError.new(ex) -def _string_length_check(text): +def _string_length_check(text: str | bytes) -> None: if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: msg = "too many characters in string" raise ValueError(msg) @@ -81,7 +84,9 @@ def _string_length_check(text): class ImageFont: """PIL font wrapper""" - def _load_pilfont(self, filename): + font: Font + + def _load_pilfont(self, filename: str) -> None: with open(filename, "rb") as fp: image = None for ext in (".png", ".gif", ".pbm"): @@ -153,7 +158,7 @@ class ImageFont: Image._decompression_bomb_check(self.font.getsize(text)) return self.font.getmask(text, mode) - def getbbox(self, text, *args, **kwargs): + def getbbox(self, text: str, *args: object, **kwargs: object) -> tuple[int, int, int, int]: """ Returns bounding box (in pixels) of given text. @@ -171,7 +176,7 @@ class ImageFont: width, height = self.font.getsize(text) return 0, 0, width, height - def getlength(self, text, *args, **kwargs): + def getlength(self, text: str, *args: object, **kwargs: object) -> int: """ Returns length (in pixels) of given text. This is the amount by which following text should be offset. @@ -254,7 +259,7 @@ class FreeTypeFont: path, size, index, encoding, layout_engine = state self.__init__(path, size, index, encoding, layout_engine) - def getname(self): + def getname(self) -> tuple[str, str]: """ :return: A tuple of the font family (e.g. Helvetica) and the font style (e.g. Bold) @@ -269,7 +274,7 @@ class FreeTypeFont: """ return self.font.ascent, self.font.descent - def getlength(self, text, mode="", direction=None, features=None, language=None): + def getlength(self, text: str, mode="", direction=None, features=None, language=None) -> float: """ Returns length (in pixels with 1/64 precision) of given text when rendered in font with provided direction, features, and language. @@ -343,14 +348,14 @@ class FreeTypeFont: def getbbox( self, - text, + text: str, mode="", direction=None, features=None, language=None, stroke_width=0, anchor=None, - ): + ) -> tuple[int, int, int, int]: """ Returns bounding box (in pixels) of given text relative to given anchor when rendered in font with provided direction, features, and language. @@ -725,7 +730,7 @@ class TransposedFont: return self.font.getlength(text, *args, **kwargs) -def load(filename): +def load(filename: str) -> ImageFont: """ Load a font file. This function loads a font object from the given bitmap font file, and returns the corresponding font object. @@ -739,7 +744,7 @@ def load(filename): return f -def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): +def truetype(font: StrOrBytesPath | BinaryIO | None = None, size: float = 10, index: int = 0, encoding: str = "", layout_engine: Layout | None = None) -> FreeTypeFont: """ Load a TrueType or OpenType font from a file or file-like object, and create a font object. @@ -800,7 +805,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): :exception ValueError: If the font size is not greater than zero. """ - def freetype(font): + def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont: return FreeTypeFont(font, size, index, encoding, layout_engine) try: @@ -850,7 +855,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): raise -def load_path(filename): +def load_path(filename: str | bytes) -> ImageFont: """ Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a bitmap font along the Python path. diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index e27843e53..2c2ea9a54 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -1,3 +1,38 @@ -from typing import Any +from typing import Any, TypedDict + +class _Axis(TypedDict): + minimum: int | None + default: int | None + maximum: int | None + name: str | None + + +class Font: + @property + def family(self) -> str | None: ... + @property + def style(self) -> str | None: ... + @property + def ascent(self) -> int: ... + @property + def descent(self) -> int: ... + @property + def height(self) -> int: ... + @property + def x_ppem(self) -> int: ... + @property + def y_ppem(self) -> int: ... + @property + def glyphs(self) -> int: ... + + def render(self, string: str, fill, mode = ..., dir = ..., features = ..., lang = ..., stroke_width = ..., anchor = ..., foreground_ink_long = ..., x_start = ..., y_start = ..., /) -> tuple[Any, tuple[int, int]]: ... + def getsize(self, string: str, mode = ..., dir = ..., features = ..., lang = ..., anchor = ..., /) -> tuple[tuple[int, int], tuple[int, int]]: ... + def getlength(self, string: str, mode = ..., dir = ..., features = ..., lang = ..., /) -> int: ... + def getvarnames(self) -> list[str]: ... + def getvaraxes(self) -> list[_Axis]: ... + def setvarname(self, instance_index: int, /) -> None: ... + def setvaraxes(self, axes: list[float], /) -> None: ... + +def getfont(filename: str | bytes | bytearray, size, index = ..., encoding = ..., font_bytes = ..., layout_engine = ...) -> Font: ... def __getattr__(name: str) -> Any: ... From 1aa3886ed76b3f8fc60d604a34dffe573b491c20 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 12:33:59 +0000 Subject: [PATCH 127/195] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/Image.py | 30 ++++++++++++++++++++++++++---- src/PIL/ImageFont.py | 16 +++++++++++++--- src/PIL/_imagingft.pyi | 36 +++++++++++++++++++++++++++++------- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f81e95695..9f55ea924 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -481,9 +481,11 @@ def _getscaleoffset(expr): # -------------------------------------------------------------------- # Implementation wrapper + class _GetDataTransform(Protocol): def getdata(self) -> tuple[Transform, Sequence[int]]: ... + class Image: """ This class represents an image object. To create @@ -1689,7 +1691,12 @@ class Image: return self.im.entropy(extrema) return self.im.entropy() - def paste(self, im: Image | str | int | tuple[int, ...], box: tuple[int, int, int, int] | tuple[int, int] | None = None, mask: Image | None = None) -> None: + def paste( + self, + im: Image | str | int | tuple[int, ...], + box: tuple[int, int, int, int] | tuple[int, int] | None = None, + mask: Image | None = None, + ) -> None: """ Pastes another image into this image. The box argument is either a 2-tuple giving the upper left corner, a 4-tuple defining the @@ -2124,7 +2131,13 @@ class Image: min(self.size[1], math.ceil(box[3] + support_y)), ) - def resize(self, size: tuple[int, int], resample: Resampling | None = None, box: tuple[float, float, float, float] | None = None, reducing_gap: float | None = None) -> Image: + def resize( + self, + size: tuple[int, int], + resample: Resampling | None = None, + box: tuple[float, float, float, float] | None = None, + reducing_gap: float | None = None, + ) -> Image: """ Returns a resized copy of this image. @@ -2230,7 +2243,11 @@ class Image: return self._new(self.im.resize(size, resample, box)) - def reduce(self, factor: int | tuple[int, int], box: tuple[int, int, int, int] | None = None) -> Image: + def reduce( + self, + factor: int | tuple[int, int], + box: tuple[int, int, int, int] | None = None, + ) -> Image: """ Returns a copy of the image reduced ``factor`` times. If the size of the image is not dividable by ``factor``, @@ -2578,7 +2595,12 @@ class Image: """ return 0 - def thumbnail(self, size: tuple[int, int], resample: Resampling = Resampling.BICUBIC, reducing_gap: float = 2.0) -> None: + def thumbnail( + self, + size: tuple[int, int], + resample: Resampling = Resampling.BICUBIC, + reducing_gap: float = 2.0, + ) -> None: """ Make this image into a thumbnail. This method modifies the image to contain a thumbnail version of itself, no larger than diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 536ee5fe6..fb7e1d8b6 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -158,7 +158,9 @@ class ImageFont: Image._decompression_bomb_check(self.font.getsize(text)) return self.font.getmask(text, mode) - def getbbox(self, text: str, *args: object, **kwargs: object) -> tuple[int, int, int, int]: + def getbbox( + self, text: str, *args: object, **kwargs: object + ) -> tuple[int, int, int, int]: """ Returns bounding box (in pixels) of given text. @@ -274,7 +276,9 @@ class FreeTypeFont: """ return self.font.ascent, self.font.descent - def getlength(self, text: str, mode="", direction=None, features=None, language=None) -> float: + def getlength( + self, text: str, mode="", direction=None, features=None, language=None + ) -> float: """ Returns length (in pixels with 1/64 precision) of given text when rendered in font with provided direction, features, and language. @@ -744,7 +748,13 @@ def load(filename: str) -> ImageFont: return f -def truetype(font: StrOrBytesPath | BinaryIO | None = None, size: float = 10, index: int = 0, encoding: str = "", layout_engine: Layout | None = None) -> FreeTypeFont: +def truetype( + font: StrOrBytesPath | BinaryIO | None = None, + size: float = 10, + index: int = 0, + encoding: str = "", + layout_engine: Layout | None = None, +) -> FreeTypeFont: """ Load a TrueType or OpenType font from a file or file-like object, and create a font object. diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 2c2ea9a54..987e7fd6f 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -6,7 +6,6 @@ class _Axis(TypedDict): maximum: int | None name: str | None - class Font: @property def family(self) -> str | None: ... @@ -24,15 +23,38 @@ class Font: def y_ppem(self) -> int: ... @property def glyphs(self) -> int: ... - - def render(self, string: str, fill, mode = ..., dir = ..., features = ..., lang = ..., stroke_width = ..., anchor = ..., foreground_ink_long = ..., x_start = ..., y_start = ..., /) -> tuple[Any, tuple[int, int]]: ... - def getsize(self, string: str, mode = ..., dir = ..., features = ..., lang = ..., anchor = ..., /) -> tuple[tuple[int, int], tuple[int, int]]: ... - def getlength(self, string: str, mode = ..., dir = ..., features = ..., lang = ..., /) -> int: ... + def render( + self, + string: str, + fill, + mode=..., + dir=..., + features=..., + lang=..., + stroke_width=..., + anchor=..., + foreground_ink_long=..., + x_start=..., + y_start=..., + /, + ) -> tuple[Any, tuple[int, int]]: ... + def getsize( + self, string: str, mode=..., dir=..., features=..., lang=..., anchor=..., / + ) -> tuple[tuple[int, int], tuple[int, int]]: ... + def getlength( + self, string: str, mode=..., dir=..., features=..., lang=..., / + ) -> int: ... def getvarnames(self) -> list[str]: ... def getvaraxes(self) -> list[_Axis]: ... def setvarname(self, instance_index: int, /) -> None: ... def setvaraxes(self, axes: list[float], /) -> None: ... -def getfont(filename: str | bytes | bytearray, size, index = ..., encoding = ..., font_bytes = ..., layout_engine = ...) -> Font: ... - +def getfont( + filename: str | bytes | bytearray, + size, + index=..., + encoding=..., + font_bytes=..., + layout_engine=..., +) -> Font: ... def __getattr__(name: str) -> Any: ... From d44e9fccb16c63005fbffce06c16a0afc2b26667 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 7 May 2024 14:53:26 +0200 Subject: [PATCH 128/195] Various fixes --- src/PIL/Image.py | 46 ++++++++++++++++++++++++++++---------------- src/PIL/ImageFont.py | 2 +- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9f55ea924..f6f070fee 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -483,7 +483,7 @@ def _getscaleoffset(expr): class _GetDataTransform(Protocol): - def getdata(self) -> tuple[Transform, Sequence[int]]: ... + def getdata(self) -> tuple[Transform, Sequence[float]]: ... class Image: @@ -2134,7 +2134,7 @@ class Image: def resize( self, size: tuple[int, int], - resample: Resampling | None = None, + resample: int | None = None, box: tuple[float, float, float, float] | None = None, reducing_gap: float | None = None, ) -> Image: @@ -2202,13 +2202,13 @@ class Image: msg = "reducing_gap must be 1.0 or greater" raise ValueError(msg) - size = tuple(size) + size = cast(tuple[int, int], tuple(size)) self.load() if box is None: box = (0, 0) + self.size else: - box = tuple(box) + box = cast(tuple[float, float, float, float], tuple(box)) if self.size == size and box == (0, 0) + self.size: return self.copy() @@ -2266,7 +2266,7 @@ class Image: if box is None: box = (0, 0) + self.size else: - box = tuple(box) + box = cast(tuple[int, int, int, int], tuple(box)) if factor == (1, 1) and box == (0, 0) + self.size: return self.copy() @@ -2283,7 +2283,7 @@ class Image: def rotate( self, angle: float, - resample: Resampling = Resampling.NEAREST, + resample: int = Resampling.NEAREST, expand: bool = False, center: tuple[int, int] | None = None, translate: tuple[int, int] | None = None, @@ -2598,7 +2598,7 @@ class Image: def thumbnail( self, size: tuple[int, int], - resample: Resampling = Resampling.BICUBIC, + resample: int = Resampling.BICUBIC, reducing_gap: float = 2.0, ) -> None: """ @@ -2661,20 +2661,22 @@ class Image: box = None if reducing_gap is not None: - size = preserve_aspect_ratio() - if size is None: + preserved_size = preserve_aspect_ratio() + if preserved_size is None: return + size = preserved_size - res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) + res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) # type: ignore[arg-type] if res is not None: box = res[1] if box is None: self.load() # load() may have changed the size of the image - size = preserve_aspect_ratio() - if size is None: + preserved_size = preserve_aspect_ratio() + if preserved_size is None: return + size = preserved_size if self.size != size: im = self.resize(size, resample, box=box, reducing_gap=reducing_gap) @@ -2693,8 +2695,8 @@ class Image: self, size: tuple[int, int], method: Transform | ImageTransformHandler, - data: Sequence[int], - resample: Resampling = Resampling.NEAREST, + data: Sequence[float], + resample: int = Resampling.NEAREST, fill: int = 1, fillcolor: float | tuple[float, ...] | str | None = None, ) -> Image: ... @@ -2704,7 +2706,17 @@ class Image: size: tuple[int, int], method: _GetDataTransform, data: None = None, - resample: Resampling = Resampling.NEAREST, + resample: int = Resampling.NEAREST, + fill: int = 1, + fillcolor: float | tuple[float, ...] | str | None = None, + ) -> Image: ... + @overload + def transform( + self, + size: tuple[int, int], + method: Transform | ImageTransformHandler | _GetDataTransform, + data: Sequence[float] | None = None, + resample: int = Resampling.NEAREST, fill: int = 1, fillcolor: float | tuple[float, ...] | str | None = None, ) -> Image: ... @@ -2712,8 +2724,8 @@ class Image: self, size: tuple[int, int], method: Transform | ImageTransformHandler | _GetDataTransform, - data: Sequence[int] | None = None, - resample: Resampling = Resampling.NEAREST, + data: Sequence[float] | None = None, + resample: int = Resampling.NEAREST, fill: int = 1, fillcolor: float | tuple[float, ...] | str | None = None, ) -> Image: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index fb7e1d8b6..a1b722765 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -40,7 +40,7 @@ from ._typing import StrOrBytesPath from ._util import is_directory, is_path if TYPE_CHECKING: - from _imagingft import Font + from ._imagingft import Font class Layout(IntEnum): From d63caf266d2561b1646ed378761332e0855dd73d Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 7 May 2024 15:59:20 +0200 Subject: [PATCH 129/195] Various fixes --- src/PIL/Image.py | 44 +++++++-------------------------------- src/PIL/ImageDraw.py | 24 ++++++++++----------- src/PIL/ImageFont.py | 13 +++++++++--- src/PIL/ImageTransform.py | 4 ++-- src/PIL/_imaging.pyi | 15 +++++++++++++ 5 files changed, 47 insertions(+), 53 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f6f070fee..9b0c24ec0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,7 +41,7 @@ import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, cast, overload +from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, cast # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -483,7 +483,9 @@ def _getscaleoffset(expr): class _GetDataTransform(Protocol): - def getdata(self) -> tuple[Transform, Sequence[float]]: ... + def getdata( + self, + ) -> tuple[Transform, Sequence[Any]]: ... class Image: @@ -2690,41 +2692,11 @@ class Image: # FIXME: the different transform methods need further explanation # instead of bloating the method docs, add a separate chapter. - @overload - def transform( - self, - size: tuple[int, int], - method: Transform | ImageTransformHandler, - data: Sequence[float], - resample: int = Resampling.NEAREST, - fill: int = 1, - fillcolor: float | tuple[float, ...] | str | None = None, - ) -> Image: ... - @overload - def transform( - self, - size: tuple[int, int], - method: _GetDataTransform, - data: None = None, - resample: int = Resampling.NEAREST, - fill: int = 1, - fillcolor: float | tuple[float, ...] | str | None = None, - ) -> Image: ... - @overload def transform( self, size: tuple[int, int], method: Transform | ImageTransformHandler | _GetDataTransform, - data: Sequence[float] | None = None, - resample: int = Resampling.NEAREST, - fill: int = 1, - fillcolor: float | tuple[float, ...] | str | None = None, - ) -> Image: ... - def transform( - self, - size: tuple[int, int], - method: Transform | ImageTransformHandler | _GetDataTransform, - data: Sequence[float] | None = None, + data: Sequence[Any] | None = None, resample: int = Resampling.NEAREST, fill: int = 1, fillcolor: float | tuple[float, ...] | str | None = None, @@ -2803,7 +2775,7 @@ class Image: im.info = self.info.copy() if method == Transform.MESH: # list of quads - for box, quad in data: + for box, quad in cast(Sequence[tuple[float, float]], data): im.__transformer( box, self, Transform.QUAD, quad, resample, fillcolor is None ) @@ -2961,7 +2933,7 @@ class ImageTransformHandler: self, size: tuple[int, int], image: Image, - **options: dict[str, str | int | tuple[int, ...] | list[int]], + **options: dict[str, str | int | tuple[int, ...] | list[int]] | int, ) -> Image: pass @@ -3830,7 +3802,7 @@ class Exif(_ExifBase): return self._fixup_dict(info) def _get_head(self): - version = b"\x2B" if self.bigtiff else b"\x2A" + version = b"\x2b" if self.bigtiff else b"\x2a" if self.endian == "<": head = b"II" + version + b"\x00" + o32le(8) else: diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 579489fde..ec8a9a67d 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -118,7 +118,7 @@ class ImageDraw: self.font = ImageFont.load_default() return self.font - def _getfont(self, font_size: float | None): + def _getfont(self, font_size: float | None) -> FreeTypeFont | ImageFont: if font_size is not None: from . import ImageFont @@ -451,13 +451,13 @@ class ImageDraw: right[3] -= r + 1 self.draw.draw_rectangle(right, ink, 1) - def _multiline_check(self, text: str | bytes) -> bool: - split_character = "\n" if isinstance(text, str) else b"\n" + def _multiline_check(self, text: AnyStr) -> bool: + split_character = cast(AnyStr, "\n" if isinstance(text, str) else b"\n") return split_character in text def _multiline_split(self, text: AnyStr) -> list[AnyStr]: - split_character = "\n" if isinstance(text, str) else b"\n" + split_character = cast(AnyStr, "\n" if isinstance(text, str) else b"\n") return text.split(split_character) @@ -470,10 +470,10 @@ class ImageDraw: def text( self, - xy: tuple[int, int], - text, + xy: tuple[float, float], + text: str, fill=None, - font=None, + font: FreeTypeFont | ImageFont | None = None, anchor=None, spacing=4, align="left", @@ -527,7 +527,7 @@ class ImageDraw: coord.append(int(xy[i])) start.append(math.modf(xy[i])[0]) try: - mask, offset = font.getmask2( + mask, offset = font.getmask2( # type: ignore[union-attr,misc] text, mode, direction=direction, @@ -543,7 +543,7 @@ class ImageDraw: coord = [coord[0] + offset[0], coord[1] + offset[1]] except AttributeError: try: - mask = font.getmask( + mask = font.getmask( # type: ignore[misc] text, mode, direction, @@ -592,7 +592,7 @@ class ImageDraw: def multiline_text( self, - xy: tuple[int, int], + xy: tuple[float, float], text, fill=None, font=None, @@ -625,7 +625,7 @@ class ImageDraw: font = self._getfont(font_size) widths = [] - max_width = 0 + max_width: float = 0 lines = self._multiline_split(text) line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: @@ -779,7 +779,7 @@ class ImageDraw: font = self._getfont(font_size) widths = [] - max_width = 0 + max_width: float = 0 lines = self._multiline_split(text) line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a1b722765..9eca3bc98 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -35,11 +35,14 @@ from enum import IntEnum from io import BytesIO from typing import TYPE_CHECKING, BinaryIO +from PIL import ImageFile + from . import Image from ._typing import StrOrBytesPath from ._util import is_directory, is_path if TYPE_CHECKING: + from ._imaging import ImagingFont from ._imagingft import Font @@ -84,11 +87,11 @@ def _string_length_check(text: str | bytes) -> None: class ImageFont: """PIL font wrapper""" - font: Font + font: ImagingFont def _load_pilfont(self, filename: str) -> None: with open(filename, "rb") as fp: - image = None + image: ImageFile.ImageFile | None = None for ext in (".png", ".gif", ".pbm"): if image: image.close() @@ -198,6 +201,8 @@ class ImageFont: class FreeTypeFont: """FreeType font wrapper (requires _imagingft service)""" + font: Font + def __init__( self, font: StrOrBytesPath | BinaryIO | None = None, @@ -261,7 +266,7 @@ class FreeTypeFont: path, size, index, encoding, layout_engine = state self.__init__(path, size, index, encoding, layout_engine) - def getname(self) -> tuple[str, str]: + def getname(self) -> tuple[str | None, str | None]: """ :return: A tuple of the font family (e.g. Helvetica) and the font style (e.g. Bold) @@ -876,6 +881,7 @@ def load_path(filename: str | bytes) -> ImageFont: """ for directory in sys.path: if is_directory(directory): + assert isinstance(directory, str) if not isinstance(filename, str): filename = filename.decode("utf-8") try: @@ -900,6 +906,7 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: :return: A font object. """ + f: FreeTypeFont | ImageFont if core.__class__.__name__ == "module" or size is not None: f = truetype( BytesIO( diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 6aa82dadd..80a6116b7 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -14,7 +14,7 @@ # from __future__ import annotations -from typing import Sequence +from typing import Any, Sequence from . import Image @@ -34,7 +34,7 @@ class Transform(Image.ImageTransformHandler): self, size: tuple[int, int], image: Image.Image, - **options: dict[str, str | int | tuple[int, ...] | list[int]], + **options: Any, ) -> Image.Image: """Perform the transform. Called from :py:meth:`.Image.transform`.""" # can be overridden diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi index e27843e53..d85eb84fa 100644 --- a/src/PIL/_imaging.pyi +++ b/src/PIL/_imaging.pyi @@ -1,3 +1,18 @@ from typing import Any +from typing_extensions import Buffer + +class ImagingCore: + def __getattr__(self, name: str) -> Any: ... + +class ImagingFont: + def __getattr__(self, name: str) -> Any: ... + +class ImagingDraw: + def __getattr__(self, name: str) -> Any: ... + +class PixelAccess: + def __getattr__(self, name: str) -> Any: ... + +def font(image, glyphdata: Buffer) -> ImagingFont: ... def __getattr__(name: str) -> Any: ... From ed0867abecd7f4ce8eb300c4896229fcedeee6c4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 May 2024 06:30:43 +1000 Subject: [PATCH 130/195] Set stream length for later use --- src/PIL/PdfParser.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 65db70e13..c43f2da7b 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -823,12 +823,10 @@ class PdfParser: m = cls.re_stream_start.match(data, offset) if m: try: - stream_len = int(result[b"Length"]) - except (TypeError, KeyError, ValueError) as e: - msg = ( - "bad or missing Length in stream dict " - f"({result.get(b'Length')})" - ) + stream_len_str = result.get(b"Length") + stream_len = int(stream_len_str) + except (TypeError, ValueError) as e: + msg = f"bad or missing Length in stream dict ({stream_len_str})" raise PdfFormatError(msg) from e stream_data = data[m.end() : m.end() + stream_len] m = cls.re_stream_end.match(data, m.end() + stream_len) From a3356879fd5b0685b3741d6f43802626a3f5d2e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 May 2024 17:57:36 +1000 Subject: [PATCH 131/195] Use f-string --- Tests/test_image_access.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index f37ae6096..e55a4d9c1 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -409,15 +409,14 @@ class TestEmbeddable: from setuptools.command import build_ext with open("embed_pil.c", "w", encoding="utf-8") as fh: + home = sys.prefix.replace("\\", "\\\\") fh.write( - """ + f""" #include "Python.h" int main(int argc, char* argv[]) -{ - char *home = \"""" - + sys.prefix.replace("\\", "\\\\") - + """\"; +{{ + char *home = "{home}"; wchar_t *whome = Py_DecodeLocale(home, NULL); Py_SetPythonHome(whome); @@ -432,7 +431,7 @@ int main(int argc, char* argv[]) PyMem_RawFree(whome); return 0; -} +}} """ ) From ef35d7926439e6fe8c36abc0846f859aaf3a893d Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Wed, 8 May 2024 12:14:37 +0200 Subject: [PATCH 132/195] Python 3.8 compatibility --- 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 9b0c24ec0..31e6fdb83 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2204,7 +2204,7 @@ class Image: msg = "reducing_gap must be 1.0 or greater" raise ValueError(msg) - size = cast(tuple[int, int], tuple(size)) + size = cast("tuple[int, int]", tuple(size)) self.load() if box is None: From 7ae8d37138c8678e4a84210aa899df422fadaab1 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Wed, 8 May 2024 12:14:59 +0200 Subject: [PATCH 133/195] Make `GetDataTransform` public --- 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 31e6fdb83..ed1621e62 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -482,7 +482,7 @@ def _getscaleoffset(expr): # Implementation wrapper -class _GetDataTransform(Protocol): +class GetDataTransform(Protocol): def getdata( self, ) -> tuple[Transform, Sequence[Any]]: ... @@ -2695,7 +2695,7 @@ class Image: def transform( self, size: tuple[int, int], - method: Transform | ImageTransformHandler | _GetDataTransform, + method: Transform | ImageTransformHandler | GetDataTransform, data: Sequence[Any] | None = None, resample: int = Resampling.NEAREST, fill: int = 1, From 296050f3823c4648e6e7eb351e433343eddc9cee Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Wed, 8 May 2024 12:26:45 +0200 Subject: [PATCH 134/195] More Python 3.8 compatibility --- 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 ed1621e62..8348ea257 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2210,7 +2210,7 @@ class Image: if box is None: box = (0, 0) + self.size else: - box = cast(tuple[float, float, float, float], tuple(box)) + box = cast("tuple[float, float, float, float]", tuple(box)) if self.size == size and box == (0, 0) + self.size: return self.copy() @@ -2268,7 +2268,7 @@ class Image: if box is None: box = (0, 0) + self.size else: - box = cast(tuple[int, int, int, int], tuple(box)) + box = cast("tuple[int, int, int, int]", tuple(box)) if factor == (1, 1) and box == (0, 0) + self.size: return self.copy() From bb8718e58162cdcd6a9b80eca45f7b2c8321bca9 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Wed, 8 May 2024 12:54:44 +0200 Subject: [PATCH 135/195] Hopefully the last Python 3.8 instance :/ --- 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 8348ea257..f39580996 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2775,7 +2775,7 @@ class Image: im.info = self.info.copy() if method == Transform.MESH: # list of quads - for box, quad in cast(Sequence[tuple[float, float]], data): + for box, quad in cast("Sequence[tuple[float, float]]", data): im.__transformer( box, self, Transform.QUAD, quad, resample, fillcolor is None ) From 47580f257b1ae7c9b461108d1bf4ba7d2b65f1ac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 May 2024 08:51:12 +1000 Subject: [PATCH 136/195] Updated libjpeg-turbo to 3.0.3 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0d45d5a20..930289c2a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -18,7 +18,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.13.2 HARFBUZZ_VERSION=8.4.0 LIBPNG_VERSION=1.6.43 -JPEGTURBO_VERSION=3.0.2 +JPEGTURBO_VERSION=3.0.3 OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.4.5 TIFF_VERSION=4.6.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0d6da7754..9875d71e7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -114,7 +114,7 @@ V = { "FREETYPE": "2.13.2", "FRIBIDI": "1.0.13", "HARFBUZZ": "8.4.0", - "JPEGTURBO": "3.0.2", + "JPEGTURBO": "3.0.3", "LCMS2": "2.16", "LIBPNG": "1.6.43", "LIBWEBP": "1.3.2", From 431fe0dcc8ff8a28fbe89c1668d7090f247aaed8 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Fri, 10 May 2024 11:46:35 +0200 Subject: [PATCH 137/195] Rename protocol to SupportsGetData --- 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 f39580996..154862a6f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -482,7 +482,7 @@ def _getscaleoffset(expr): # Implementation wrapper -class GetDataTransform(Protocol): +class SupportsGetData(Protocol): def getdata( self, ) -> tuple[Transform, Sequence[Any]]: ... @@ -2695,7 +2695,7 @@ class Image: def transform( self, size: tuple[int, int], - method: Transform | ImageTransformHandler | GetDataTransform, + method: Transform | ImageTransformHandler | SupportsGetData, data: Sequence[Any] | None = None, resample: int = Resampling.NEAREST, fill: int = 1, From 9b44abb6b7f77043ac337fe8171d0ecdbb4b7882 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Fri, 10 May 2024 11:48:36 +0200 Subject: [PATCH 138/195] Add SupportsGetData to documentation --- docs/reference/Image.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 0d9b4d93d..c0d9095cd 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -365,6 +365,12 @@ Classes .. autoclass:: PIL.Image.ImagePointHandler .. autoclass:: PIL.Image.ImageTransformHandler +Protocols +--------- + +.. autoclass:: SupportsGetData + :show-inheritance: + Constants --------- From 57399ce204d79c74c80612c622bb788e20d786e8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 May 2024 22:43:56 +1000 Subject: [PATCH 139/195] Parse _version contents instead of using exec() --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 7d8e1c1ee..abdd87ea2 100644 --- a/setup.py +++ b/setup.py @@ -23,8 +23,7 @@ from setuptools.command.build_ext import build_ext def get_version(): version_file = "src/PIL/_version.py" with open(version_file, encoding="utf-8") as f: - exec(compile(f.read(), version_file, "exec")) - return locals()["__version__"] + return f.read().split('"')[1] configuration = {} From 18b87c8515941f7131b764d4293e3cdf638ba2ff Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 May 2024 10:48:09 +1000 Subject: [PATCH 140/195] Added type hints --- src/PIL/BufrStubImagePlugin.py | 2 +- src/PIL/CurImagePlugin.py | 2 +- src/PIL/FpxImagePlugin.py | 2 +- src/PIL/FtexImagePlugin.py | 2 +- src/PIL/GbrImagePlugin.py | 2 +- src/PIL/GribStubImagePlugin.py | 2 +- src/PIL/ImImagePlugin.py | 2 +- src/PIL/ImageTk.py | 4 ++-- src/PIL/Jpeg2KImagePlugin.py | 4 ++-- src/PIL/JpegImagePlugin.py | 2 +- src/PIL/MpoImagePlugin.py | 2 +- src/PIL/PSDraw.py | 2 +- src/PIL/PdfParser.py | 16 ++++++++-------- src/PIL/PngImagePlugin.py | 6 +++--- src/PIL/PyAccess.py | 2 +- src/PIL/QoiImagePlugin.py | 2 +- src/PIL/WebPImagePlugin.py | 2 +- src/PIL/WmfImagePlugin.py | 2 +- src/PIL/XpmImagePlugin.py | 2 +- 19 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 1cbd50d19..271db7258 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -37,7 +37,7 @@ class BufrStubImageFile(ImageFile.StubImageFile): format = "BUFR" format_description = "BUFR" - def _open(self): + def _open(self) -> None: offset = self.fp.tell() if not _accept(self.fp.read(4)): diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index b8790e209..85e2145e7 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -37,7 +37,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): format = "CUR" format_description = "Windows Cursor" - def _open(self): + def _open(self) -> None: offset = self.fp.tell() # check magic diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index cfaf86239..4ba93bb39 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -237,7 +237,7 @@ class FpxImageFile(ImageFile.ImageFile): return ImageFile.ImageFile.load(self) - def close(self): + def close(self) -> None: self.ole.close() super().close() diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index a746959a3..7fcf81376 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -71,7 +71,7 @@ class FtexImageFile(ImageFile.ImageFile): format = "FTEX" format_description = "Texture File Format (IW2:EOC)" - def _open(self): + def _open(self) -> None: if not _accept(self.fp.read(4)): msg = "not an FTEX file" raise SyntaxError(msg) diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 62197e36c..93e89b1e6 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -41,7 +41,7 @@ class GbrImageFile(ImageFile.ImageFile): format = "GBR" format_description = "GIMP brush file" - def _open(self): + def _open(self) -> None: header_size = i32(self.fp.read(4)) if header_size < 20: msg = "not a GIMP brush" diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index a80fe0a23..13bdfa616 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -37,7 +37,7 @@ class GribStubImageFile(ImageFile.StubImageFile): format = "GRIB" format_description = "GRIB" - def _open(self): + def _open(self) -> None: offset = self.fp.tell() if not _accept(self.fp.read(8)): diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 9c16159e9..a325f8552 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -119,7 +119,7 @@ class ImImageFile(ImageFile.ImageFile): format_description = "IFUNC Image Memory" _close_exclusive_fp_after_loading = False - def _open(self): + def _open(self) -> None: # Quick rejection: if there's not an LF among the first # 100 bytes, this is (probably) not a text header. diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 10b2cc69a..2f9d7f505 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -128,7 +128,7 @@ class PhotoImage: if image: self.paste(image) - def __del__(self): + def __del__(self) -> None: name = self.__photo.name self.__photo.name = None try: @@ -219,7 +219,7 @@ class BitmapImage: kw["data"] = image.tobitmap() self.__photo = tkinter.BitmapImage(**kw) - def __del__(self): + def __del__(self) -> None: name = self.__photo.name self.__photo.name = None try: diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 697bad221..81ef32253 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -215,7 +215,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): format = "JPEG2000" format_description = "JPEG 2000 (ISO 15444)" - def _open(self): + def _open(self) -> None: sig = self.fp.read(4) if sig == b"\xff\x4f\xff\x51": self.codec = "j2k" @@ -267,7 +267,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): ) ] - def _parse_comment(self): + def _parse_comment(self) -> None: hdr = self.fp.read(2) length = _binary.i16be(hdr) self.fp.seek(length - 2, os.SEEK_CUR) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 715a358a3..7a3c99b6c 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -462,7 +462,7 @@ class JpegImageFile(ImageFile.ImageFile): box = (0, 0, original_size[0] / scale, original_size[1] / scale) return self.mode, box - def load_djpeg(self): + def load_djpeg(self) -> None: # ALTERNATIVE: handle JPEGs via the IJG command line utilities f, path = tempfile.mkstemp() diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index fb6620e75..eba35fb4d 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -100,7 +100,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): format_description = "MPO (CIPA DC-007)" _close_exclusive_fp_after_loading = False - def _open(self): + def _open(self) -> None: self.fp.seek(0) # prep the fp in order to pass the JPEG test JpegImagePlugin.JpegImageFile._open(self) self._after_jpeg_open() diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index 848fc2f71..49c06ce13 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -54,7 +54,7 @@ class PSDraw: self.fp.write(b"%%EndProlog\n") self.isofont = {} - def end_document(self): + def end_document(self) -> None: """Ends printing. (Write PostScript DSC footer.)""" self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n") if hasattr(self.fp, "flush"): diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index c43f2da7b..077c9ec8b 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -409,28 +409,28 @@ class PdfParser: self.close() return False # do not suppress exceptions - def start_writing(self): + def start_writing(self) -> None: self.close_buf() self.seek_end() - def close_buf(self): + def close_buf(self) -> None: try: self.buf.close() except AttributeError: pass self.buf = None - def close(self): + def close(self) -> None: if self.should_close_buf: self.close_buf() if self.f is not None and self.should_close_file: self.f.close() self.f = None - def seek_end(self): + def seek_end(self) -> None: self.f.seek(0, os.SEEK_END) - def write_header(self): + def write_header(self) -> None: self.f.write(b"%PDF-1.4\n") def write_comment(self, s): @@ -450,7 +450,7 @@ class PdfParser: ) return self.root_ref - def rewrite_pages(self): + def rewrite_pages(self) -> None: pages_tree_nodes_to_delete = [] for i, page_ref in enumerate(self.orig_pages): page_info = self.cached_objects[page_ref] @@ -529,7 +529,7 @@ class PdfParser: f.write(b"endobj\n") return ref - def del_root(self): + def del_root(self) -> None: if self.root_ref is None: return del self.xref_table[self.root_ref.object_id] @@ -547,7 +547,7 @@ class PdfParser: except ValueError: # cannot mmap an empty file return b"" - def read_pdf_info(self): + def read_pdf_info(self) -> None: self.file_size_total = len(self.buf) self.file_size_this = self.file_size_total - self.start_offset self.read_trailer() diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 1547edde5..76e0abc31 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -179,7 +179,7 @@ class ChunkStream: def __exit__(self, *args): self.close() - def close(self): + def close(self) -> None: self.queue = self.fp = None def push(self, cid, pos, length): @@ -370,14 +370,14 @@ class PngStream(ChunkStream): ) raise ValueError(msg) - def save_rewind(self): + def save_rewind(self) -> None: self.rewind_state = { "info": self.im_info.copy(), "tile": self.im_tile, "seq_num": self._seq_num, } - def rewind(self): + def rewind(self) -> None: self.im_info = self.rewind_state["info"].copy() self.im_tile = self.rewind_state["tile"] self._seq_num = self.rewind_state["seq_num"] diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 2c831913d..a9da90613 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -70,7 +70,7 @@ class PyAccess: # logger.debug("%s", vals) self._post_init() - def _post_init(self): + def _post_init(self) -> None: pass def __setitem__(self, xy, color): diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 2875b8d75..cea8b60da 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -21,7 +21,7 @@ class QoiImageFile(ImageFile.ImageFile): format = "QOI" format_description = "Quite OK Image" - def _open(self): + def _open(self) -> None: if not _accept(self.fp.read(4)): msg = "not a QOI file" raise SyntaxError(msg) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 61ae9eae5..052f253cf 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -43,7 +43,7 @@ class WebPImageFile(ImageFile.ImageFile): __loaded = 0 __logical_frame = 0 - def _open(self): + def _open(self) -> None: if not _webp.HAVE_WEBPANIM: # Legacy mode data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode( diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 7f045ec7d..b0328657b 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -79,7 +79,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): format = "WMF" format_description = "Windows Metafile" - def _open(self): + def _open(self) -> None: self._inch = None # check placable header diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index a638547af..88d14e9c2 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -36,7 +36,7 @@ class XpmImageFile(ImageFile.ImageFile): format = "XPM" format_description = "X11 Pixel Map" - def _open(self): + def _open(self) -> None: if not _accept(self.fp.read(9)): msg = "not an XPM file" raise SyntaxError(msg) From 13cf2bc70f4bb5de7c0a083303d4a232104d7852 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 May 2024 11:16:52 +1000 Subject: [PATCH 141/195] Moved SupportsArrayInterface under Protocols heading --- docs/reference/Image.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index c0d9095cd..d917a3c92 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -78,8 +78,6 @@ Constructing images ^^^^^^^^^^^^^^^^^^^ .. autofunction:: new -.. autoclass:: SupportsArrayInterface - :show-inheritance: .. autofunction:: fromarray .. autofunction:: frombytes .. autofunction:: frombuffer @@ -368,6 +366,8 @@ Classes Protocols --------- +.. autoclass:: SupportsArrayInterface + :show-inheritance: .. autoclass:: SupportsGetData :show-inheritance: From 6310280428a49ea5495953a824b1dfa85a4d5223 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Sat, 11 May 2024 10:44:52 +0200 Subject: [PATCH 142/195] Move an import behind the TYPE_CHECKING flag Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageFont.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 9eca3bc98..f2936bae6 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -35,13 +35,12 @@ from enum import IntEnum from io import BytesIO from typing import TYPE_CHECKING, BinaryIO -from PIL import ImageFile - from . import Image from ._typing import StrOrBytesPath from ._util import is_directory, is_path if TYPE_CHECKING: + from . import ImageFile from ._imaging import ImagingFont from ._imagingft import Font From 6d6dfd176cf00a864a62dff6dd881099cc3bcec8 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Sat, 11 May 2024 10:46:20 +0200 Subject: [PATCH 143/195] Revert unnecessary formatting change --- 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 154862a6f..53f38f0b2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3802,7 +3802,7 @@ class Exif(_ExifBase): return self._fixup_dict(info) def _get_head(self): - version = b"\x2b" if self.bigtiff else b"\x2a" + version = b"\x2B" if self.bigtiff else b"\x2A" if self.endian == "<": head = b"II" + version + b"\x00" + o32le(8) else: From db4714c280c96ea4b61f1b3360c55e730ffaf62d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 May 2024 21:20:46 +1000 Subject: [PATCH 144/195] Removed helper.py modes --- Tests/helper.py | 27 --------------------------- Tests/test_image.py | 9 ++++----- Tests/test_image_access.py | 13 +++++++------ 3 files changed, 11 insertions(+), 38 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 1297c1c43..5fd4fe332 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -29,33 +29,6 @@ elif "GITHUB_ACTIONS" in os.environ: uploader = "github_actions" -modes = ( - "1", - "L", - "LA", - "La", - "P", - "PA", - "F", - "I", - "I;16", - "I;16L", - "I;16B", - "I;16N", - "RGB", - "RGBA", - "RGBa", - "RGBX", - "BGR;15", - "BGR;16", - "BGR;24", - "CMYK", - "YCbCr", - "HSV", - "LAB", -) - - def upload(a: Image.Image, b: Image.Image) -> str | None: if uploader == "show": # local img.show for errors. diff --git a/Tests/test_image.py b/Tests/test_image.py index e1490d6a0..742d0dfe4 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -31,7 +31,6 @@ from .helper import ( is_big_endian, is_win32, mark_if_feature_version, - modes, skip_unless_feature, ) @@ -46,7 +45,7 @@ def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: class TestImage: - @pytest.mark.parametrize("mode", modes) + @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) def test_image_modes_success(self, mode: str) -> None: helper_image_new(mode, (1, 1)) @@ -1027,7 +1026,7 @@ class TestImage: class TestImageBytes: - @pytest.mark.parametrize("mode", modes) + @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) def test_roundtrip_bytes_constructor(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() @@ -1039,7 +1038,7 @@ class TestImageBytes: reloaded = Image.frombytes(mode, im.size, source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", modes) + @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) def test_roundtrip_bytes_method(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() @@ -1048,7 +1047,7 @@ class TestImageBytes: reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", modes) + @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) def test_getdata_putdata(self, mode: str) -> None: if is_big_endian() and mode == "BGR;15": pytest.xfail("Known failure of BGR;15 on big-endian") diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index e55a4d9c1..9d6006679 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -10,7 +10,7 @@ import pytest from PIL import Image -from .helper import assert_image_equal, hopper, is_win32, modes +from .helper import assert_image_equal, hopper, is_win32 # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -205,12 +205,13 @@ class TestImageGetPixel(AccessTest): with pytest.raises(error): im.getpixel((-1, -1)) - @pytest.mark.parametrize("mode", modes) + @pytest.mark.parametrize("mode", Image.MODES) def test_basic(self, mode: str) -> None: - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - self.check(mode) - else: + self.check(mode) + + @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) + def test_deprecated(self, mode: str) -> None: + with pytest.warns(DeprecationWarning): self.check(mode) def test_list(self) -> None: From 00e5e43da42ec9cd4da6c80ae269536a975f1217 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 May 2024 11:43:08 +0000 Subject: [PATCH 145/195] chore(deps): update dependency cibuildwheel to v2.18.0 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 45c2af975..8d39ea9bb 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.17.0 +cibuildwheel==2.18.0 From a8d154877d92d549f1d18411383441d1c9238e7c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 May 2024 18:47:51 +1000 Subject: [PATCH 146/195] Added type hints --- src/PIL/FliImagePlugin.py | 2 +- src/PIL/GifImagePlugin.py | 10 +++++----- src/PIL/GimpPaletteFile.py | 2 +- src/PIL/ImImagePlugin.py | 4 ++-- src/PIL/ImageDraw.py | 7 +++++-- src/PIL/ImageFilter.py | 2 +- src/PIL/ImagePalette.py | 6 +++--- src/PIL/ImageTk.py | 12 ++++++------ src/PIL/Jpeg2KImagePlugin.py | 4 ++-- src/PIL/PdfParser.py | 20 ++++++++++---------- src/PIL/SpiderImagePlugin.py | 10 +++++++--- src/PIL/TiffImagePlugin.py | 14 +++++++------- src/PIL/WebPImagePlugin.py | 2 +- 13 files changed, 51 insertions(+), 44 deletions(-) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index eea2c0c95..dceb83927 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -132,7 +132,7 @@ class FliImageFile(ImageFile.ImageFile): for f in range(self.__frame + 1, frame + 1): self._seek(f) - def _seek(self, frame): + def _seek(self, frame: int) -> None: if frame == 0: self.__frame = -1 self._fp.seek(self.__rewind) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 26e595819..eede41549 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -82,7 +82,7 @@ class GifImageFile(ImageFile.ImageFile): return self.fp.read(s[0]) return None - def _is_palette_needed(self, p): + def _is_palette_needed(self, p: bytes) -> bool: for i in range(0, len(p), 3): if not (i // 3 == p[i] == p[i + 1] == p[i + 2]): return True @@ -474,7 +474,7 @@ class GifImageFile(ImageFile.ImageFile): RAWMODE = {"1": "L", "L": "L", "P": "P"} -def _normalize_mode(im): +def _normalize_mode(im: Image.Image) -> Image.Image: """ Takes an image (or frame), returns an image in a mode that is appropriate for saving in a Gif. @@ -887,7 +887,7 @@ def _get_optimize(im, info): return used_palette_colors -def _get_color_table_size(palette_bytes): +def _get_color_table_size(palette_bytes: bytes) -> int: # calculate the palette size for the header if not palette_bytes: return 0 @@ -897,7 +897,7 @@ def _get_color_table_size(palette_bytes): return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1 -def _get_header_palette(palette_bytes): +def _get_header_palette(palette_bytes: bytes) -> bytes: """ Returns the palette, null padded to the next power of 2 (*3) bytes suitable for direct inclusion in the GIF header @@ -915,7 +915,7 @@ def _get_header_palette(palette_bytes): return palette_bytes -def _get_palette_bytes(im): +def _get_palette_bytes(im: Image.Image) -> bytes: """ Gets the palette for inclusion in the gif header diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index a3109ebaa..2274f1a8b 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -53,5 +53,5 @@ class GimpPaletteFile: self.palette = b"".join(self.palette) - def getpalette(self): + def getpalette(self) -> tuple[bytes, str]: return self.palette, self.rawmode diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index a325f8552..8e949ebaf 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -271,11 +271,11 @@ class ImImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] @property - def n_frames(self): + def n_frames(self) -> int: return self.info[FRAMES] @property - def is_animated(self): + def is_animated(self) -> bool: return self.info[FRAMES] > 1 def seek(self, frame: int) -> None: diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index d3efe6486..42f2ee8c7 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -34,7 +34,7 @@ from __future__ import annotations import math import numbers import struct -from typing import Sequence, cast +from typing import TYPE_CHECKING, Sequence, cast from . import Image, ImageColor from ._typing import Coords @@ -92,7 +92,10 @@ class ImageDraw: self.fontmode = "L" # aliasing is okay for other modes self.fill = False - def getfont(self): + if TYPE_CHECKING: + from . import ImageFont + + def getfont(self) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: """ Get the current default font. diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index fa9ebd9de..678bd29a2 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -544,7 +544,7 @@ class Color3DLUT(MultibandFilter): _copy_table=False, ) - def __repr__(self): + def __repr__(self) -> str: r = [ f"{self.__class__.__name__} from {self.table.__class__.__name__}", "size={:d}x{:d}x{:d}".format(*self.size), diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 770d10025..ae5c5dec0 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -66,7 +66,7 @@ class ImagePalette: def colors(self, colors): self._colors = colors - def copy(self): + def copy(self) -> ImagePalette: new = ImagePalette() new.mode = self.mode @@ -77,7 +77,7 @@ class ImagePalette: return new - def getdata(self): + def getdata(self) -> tuple[str, bytes]: """ Get palette contents in format suitable for the low-level ``im.putpalette`` primitive. @@ -88,7 +88,7 @@ class ImagePalette: return self.rawmode, self.palette return self.mode, self.tobytes() - def tobytes(self): + def tobytes(self) -> bytes: """Convert palette to bytes. .. warning:: This method is experimental. diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 2f9d7f505..6e2e7db1e 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -136,7 +136,7 @@ class PhotoImage: except Exception: pass # ignore internal errors - def __str__(self): + def __str__(self) -> str: """ Get the Tkinter photo image identifier. This method is automatically called by Tkinter whenever a PhotoImage object is passed to a Tkinter @@ -146,7 +146,7 @@ class PhotoImage: """ return str(self.__photo) - def width(self): + def width(self) -> int: """ Get the width of the image. @@ -154,7 +154,7 @@ class PhotoImage: """ return self.__size[0] - def height(self): + def height(self) -> int: """ Get the height of the image. @@ -227,7 +227,7 @@ class BitmapImage: except Exception: pass # ignore internal errors - def width(self): + def width(self) -> int: """ Get the width of the image. @@ -235,7 +235,7 @@ class BitmapImage: """ return self.__size[0] - def height(self): + def height(self) -> int: """ Get the height of the image. @@ -243,7 +243,7 @@ class BitmapImage: """ return self.__size[1] - def __str__(self): + def __str__(self) -> str: """ Get the Tkinter bitmap image identifier. This method is automatically called by Tkinter whenever a BitmapImage object is passed to a Tkinter diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 81ef32253..ce6342bdb 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -63,12 +63,12 @@ class BoxReader: data = self._read_bytes(size) return struct.unpack(field_format, data) - def read_boxes(self): + def read_boxes(self) -> BoxReader: size = self.remaining_in_box data = self._read_bytes(size) return BoxReader(io.BytesIO(data), size) - def has_next_box(self): + def has_next_box(self) -> bool: if self.has_length: return self.fp.tell() + self.remaining_in_box < self.length else: diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 077c9ec8b..68501d625 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -87,10 +87,10 @@ class IndirectReferenceTuple(NamedTuple): class IndirectReference(IndirectReferenceTuple): - def __str__(self): + def __str__(self) -> str: return f"{self.object_id} {self.generation} R" - def __bytes__(self): + def __bytes__(self) -> bytes: return self.__str__().encode("us-ascii") def __eq__(self, other): @@ -108,7 +108,7 @@ class IndirectReference(IndirectReferenceTuple): class IndirectObjectDef(IndirectReference): - def __str__(self): + def __str__(self) -> str: return f"{self.object_id} {self.generation} obj" @@ -150,7 +150,7 @@ class XrefTable: def __contains__(self, key): return key in self.existing_entries or key in self.new_entries - def __len__(self): + def __len__(self) -> int: return len( set(self.existing_entries.keys()) | set(self.new_entries.keys()) @@ -211,7 +211,7 @@ class PdfName: else: self.name = name.encode("us-ascii") - def name_as_str(self): + def name_as_str(self) -> str: return self.name.decode("us-ascii") def __eq__(self, other): @@ -222,7 +222,7 @@ class PdfName: def __hash__(self): return hash(self.name) - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}({repr(self.name)})" @classmethod @@ -231,7 +231,7 @@ class PdfName: allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"} - def __bytes__(self): + def __bytes__(self) -> bytes: result = bytearray(b"/") for b in self.name: if b in self.allowed_chars: @@ -242,7 +242,7 @@ class PdfName: class PdfArray(List[Any]): - def __bytes__(self): + def __bytes__(self) -> bytes: return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" @@ -286,7 +286,7 @@ class PdfDict(_DictBase): value = time.gmtime(calendar.timegm(value) + offset) return value - def __bytes__(self): + def __bytes__(self) -> bytes: out = bytearray(b"<<") for key, value in self.items(): if value is None: @@ -304,7 +304,7 @@ class PdfBinary: def __init__(self, data): self.data = data - def __bytes__(self): + def __bytes__(self) -> bytes: return b"<%s>" % b"".join(b"%02X" % b for b in self.data) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 21509b2d9..5b8ad47f0 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -37,6 +37,7 @@ from __future__ import annotations import os import struct import sys +from typing import TYPE_CHECKING from . import Image, ImageFile @@ -157,11 +158,11 @@ class SpiderImageFile(ImageFile.ImageFile): self._fp = self.fp # FIXME: hack @property - def n_frames(self): + def n_frames(self) -> int: return self._nimages @property - def is_animated(self): + def is_animated(self) -> bool: return self._nimages > 1 # 1st image index is zero (although SPIDER imgnumber starts at 1) @@ -191,8 +192,11 @@ class SpiderImageFile(ImageFile.ImageFile): b = -m * minimum return self.point(lambda i, m=m, b=b: i * m + b).convert("L") + if TYPE_CHECKING: + from . import ImageTk + # returns a ImageTk.PhotoImage object, after rescaling to 0..255 - def tkPhotoImage(self): + def tkPhotoImage(self) -> ImageTk.PhotoImage: from . import ImageTk return ImageTk.PhotoImage(self.convert2byte(), palette=256) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index a7a7e28bd..54faa59c5 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -381,7 +381,7 @@ class IFDRational(Rational): f = self._val.limit_denominator(max_denominator) return f.numerator, f.denominator - def __repr__(self): + def __repr__(self) -> str: return str(float(self._val)) def __hash__(self): @@ -603,7 +603,7 @@ class ImageFileDirectory_v2(_IFDv2Base): self._next = None self._offset = None - def __str__(self): + def __str__(self) -> str: return str(dict(self)) def named(self): @@ -617,7 +617,7 @@ class ImageFileDirectory_v2(_IFDv2Base): for code, value in self.items() } - def __len__(self): + def __len__(self) -> int: return len(set(self._tagdata) | set(self._tags_v2)) def __getitem__(self, tag): @@ -1041,7 +1041,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): ifd.next = original.next # an indicator for multipage tiffs return ifd - def to_v2(self): + def to_v2(self) -> ImageFileDirectory_v2: """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` instance with the same data as is contained in the original @@ -1061,7 +1061,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): def __contains__(self, tag): return tag in self._tags_v1 or tag in self._tagdata - def __len__(self): + def __len__(self) -> int: return len(set(self._tagdata) | set(self._tags_v1)) def __iter__(self): @@ -1154,7 +1154,7 @@ class TiffImageFile(ImageFile.ImageFile): Image._decompression_bomb_check(self.size) self.im = Image.core.new(self.mode, self.size) - def _seek(self, frame): + def _seek(self, frame: int) -> None: self.fp = self._fp # reset buffered io handle in case fp @@ -2003,7 +2003,7 @@ class AppendingTiffWriter: self.close() return False - def tell(self): + def tell(self) -> int: return self.f.tell() - self.offsetOfNewPage def seek(self, offset, whence=io.SEEK_SET): diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 052f253cf..4b8cfe65c 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -144,7 +144,7 @@ class WebPImageFile(ImageFile.ImageFile): timestamp -= duration return data, timestamp, duration - def _seek(self, frame): + def _seek(self, frame: int) -> None: if self.__physical_frame == frame: return # Nothing to do if frame < self.__physical_frame: From e9b15f8091431de34d1a5f82cc0cf954d2cf2d6c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 May 2024 10:09:44 +1000 Subject: [PATCH 147/195] Updated harfbuzz to 8.5.0 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0d45d5a20..c5b279a33 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -16,7 +16,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 -HARFBUZZ_VERSION=8.4.0 +HARFBUZZ_VERSION=8.5.0 LIBPNG_VERSION=1.6.43 JPEGTURBO_VERSION=3.0.2 OPENJPEG_VERSION=2.5.2 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0d6da7754..b654ee8da 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.2", "FRIBIDI": "1.0.13", - "HARFBUZZ": "8.4.0", + "HARFBUZZ": "8.5.0", "JPEGTURBO": "3.0.2", "LCMS2": "2.16", "LIBPNG": "1.6.43", From e419fd7ab4a71bc269a9c624c92d8955b372aaed Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 May 2024 20:19:09 +1000 Subject: [PATCH 148/195] Added type hints --- src/PIL/DdsImagePlugin.py | 2 +- src/PIL/EpsImagePlugin.py | 4 +-- src/PIL/FtexImagePlugin.py | 2 +- src/PIL/IcoImagePlugin.py | 2 +- src/PIL/ImageFile.py | 4 +-- src/PIL/JpegImagePlugin.py | 2 +- src/PIL/MpoImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 74 +++++++++++++++++++++----------------- src/PIL/WebPImagePlugin.py | 2 +- src/PIL/XpmImagePlugin.py | 7 ++-- src/PIL/features.py | 27 +++++++------- 11 files changed, 65 insertions(+), 63 deletions(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index b2ddbe44f..1575f2d88 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -472,7 +472,7 @@ class DdsImageFile(ImageFile.ImageFile): else: self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)] - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: pass diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index b57daca56..5a44baa49 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -42,7 +42,7 @@ gs_binary: str | bool | None = None gs_windows_binary = None -def has_ghostscript(): +def has_ghostscript() -> bool: global gs_binary, gs_windows_binary if gs_binary is None: if sys.platform.startswith("win"): @@ -404,7 +404,7 @@ class EpsImageFile(ImageFile.ImageFile): self.tile = [] return Image.Image.load(self) - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: # we can't incrementally load, so force ImageFile.parser to # use our custom load method by defining this method. pass diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 7fcf81376..5acbb4912 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -103,7 +103,7 @@ class FtexImageFile(ImageFile.ImageFile): self.fp.close() self.fp = BytesIO(data) - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: pass diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index eacffbae6..cea093f9c 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -341,7 +341,7 @@ class IcoImageFile(ImageFile.ImageFile): self.size = im.size - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: # Flag the ImageFile.Parser so that it # just does all the decode at the end. pass diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index b93e2ad2c..33467fc4f 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -324,11 +324,11 @@ class ImageFile(Image.Image): pass # may be defined for contained formats - # def load_seek(self, pos): + # def load_seek(self, pos: int) -> None: # pass # may be defined for blocked formats (e.g. PNG) - # def load_read(self, read_bytes): + # def load_read(self, read_bytes: int) -> bytes: # pass def _seek_check(self, frame): diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 7a3c99b6c..909911dfe 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -408,7 +408,7 @@ class JpegImageFile(ImageFile.ImageFile): msg = "no marker found" raise SyntaxError(msg) - def load_read(self, read_bytes): + def load_read(self, read_bytes: int) -> bytes: """ internal: read more image data For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index eba35fb4d..766e1290c 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -124,7 +124,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): # for now we can only handle reading and individual frame extraction self.readonly = 1 - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: self._fp.seek(pos) def seek(self, frame: int) -> None: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 76e0abc31..c74cbccf1 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -39,6 +39,7 @@ import struct import warnings import zlib from enum import IntEnum +from typing import IO from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16be as i16 @@ -149,14 +150,15 @@ def _crc32(data, seed=0): class ChunkStream: - def __init__(self, fp): - self.fp = fp - self.queue = [] + def __init__(self, fp: IO[bytes]) -> None: + self.fp: IO[bytes] | None = fp + self.queue: list[tuple[bytes, int, int]] | None = [] - def read(self): + def read(self) -> tuple[bytes, int, int]: """Fetch a new chunk. Returns header information.""" cid = None + assert self.fp is not None if self.queue: cid, pos, length = self.queue.pop() self.fp.seek(pos) @@ -173,7 +175,7 @@ class ChunkStream: return cid, pos, length - def __enter__(self): + def __enter__(self) -> ChunkStream: return self def __exit__(self, *args): @@ -182,7 +184,8 @@ class ChunkStream: def close(self) -> None: self.queue = self.fp = None - def push(self, cid, pos, length): + def push(self, cid: bytes, pos: int, length: int) -> None: + assert self.queue is not None self.queue.append((cid, pos, length)) def call(self, cid, pos, length): @@ -191,7 +194,7 @@ class ChunkStream: logger.debug("STREAM %r %s %s", cid, pos, length) return getattr(self, f"chunk_{cid.decode('ascii')}")(pos, length) - def crc(self, cid, data): + def crc(self, cid: bytes, data: bytes) -> None: """Read and verify checksum""" # Skip CRC checks for ancillary chunks if allowed to load truncated @@ -201,6 +204,7 @@ class ChunkStream: self.crc_skip(cid, data) return + assert self.fp is not None try: crc1 = _crc32(data, _crc32(cid)) crc2 = i32(self.fp.read(4)) @@ -211,12 +215,13 @@ class ChunkStream: msg = f"broken PNG file (incomplete checksum in {repr(cid)})" raise SyntaxError(msg) from e - def crc_skip(self, cid, data): + def crc_skip(self, cid: bytes, data: bytes) -> None: """Read checksum""" + assert self.fp is not None self.fp.read(4) - def verify(self, endchunk=b"IEND"): + def verify(self, endchunk: bytes = b"IEND") -> list[bytes]: # Simple approach; just calculate checksum for all remaining # blocks. Must be called directly after open. @@ -361,7 +366,7 @@ class PngStream(ChunkStream): self.text_memory = 0 - def check_text_memory(self, chunklen): + def check_text_memory(self, chunklen: int) -> None: self.text_memory += chunklen if self.text_memory > MAX_TEXT_MEMORY: msg = ( @@ -382,7 +387,7 @@ class PngStream(ChunkStream): self.im_tile = self.rewind_state["tile"] self._seq_num = self.rewind_state["seq_num"] - def chunk_iCCP(self, pos, length): + def chunk_iCCP(self, pos: int, length: int) -> bytes: # ICC profile s = ImageFile._safe_read(self.fp, length) # according to PNG spec, the iCCP chunk contains: @@ -409,7 +414,7 @@ class PngStream(ChunkStream): self.im_info["icc_profile"] = icc_profile return s - def chunk_IHDR(self, pos, length): + def chunk_IHDR(self, pos: int, length: int) -> bytes: # image header s = ImageFile._safe_read(self.fp, length) if length < 13: @@ -446,14 +451,14 @@ class PngStream(ChunkStream): msg = "end of PNG image" raise EOFError(msg) - def chunk_PLTE(self, pos, length): + def chunk_PLTE(self, pos: int, length: int) -> bytes: # palette s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": self.im_palette = "RGB", s return s - def chunk_tRNS(self, pos, length): + def chunk_tRNS(self, pos: int, length: int) -> bytes: # transparency s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": @@ -473,13 +478,13 @@ class PngStream(ChunkStream): self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4) return s - def chunk_gAMA(self, pos, length): + def chunk_gAMA(self, pos: int, length: int) -> bytes: # gamma setting s = ImageFile._safe_read(self.fp, length) self.im_info["gamma"] = i32(s) / 100000.0 return s - def chunk_cHRM(self, pos, length): + def chunk_cHRM(self, pos: int, length: int) -> bytes: # chromaticity, 8 unsigned ints, actual value is scaled by 100,000 # WP x,y, Red x,y, Green x,y Blue x,y @@ -488,7 +493,7 @@ class PngStream(ChunkStream): self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) return s - def chunk_sRGB(self, pos, length): + def chunk_sRGB(self, pos: int, length: int) -> bytes: # srgb rendering intent, 1 byte # 0 perceptual # 1 relative colorimetric @@ -504,7 +509,7 @@ class PngStream(ChunkStream): self.im_info["srgb"] = s[0] return s - def chunk_pHYs(self, pos, length): + def chunk_pHYs(self, pos: int, length: int) -> bytes: # pixels per unit s = ImageFile._safe_read(self.fp, length) if length < 9: @@ -521,7 +526,7 @@ class PngStream(ChunkStream): self.im_info["aspect"] = px, py return s - def chunk_tEXt(self, pos, length): + def chunk_tEXt(self, pos: int, length: int) -> bytes: # text s = ImageFile._safe_read(self.fp, length) try: @@ -540,7 +545,7 @@ class PngStream(ChunkStream): return s - def chunk_zTXt(self, pos, length): + def chunk_zTXt(self, pos: int, length: int) -> bytes: # compressed text s = ImageFile._safe_read(self.fp, length) try: @@ -574,7 +579,7 @@ class PngStream(ChunkStream): return s - def chunk_iTXt(self, pos, length): + def chunk_iTXt(self, pos: int, length: int) -> bytes: # international text r = s = ImageFile._safe_read(self.fp, length) try: @@ -614,13 +619,13 @@ class PngStream(ChunkStream): return s - def chunk_eXIf(self, pos, length): + def chunk_eXIf(self, pos: int, length: int) -> bytes: s = ImageFile._safe_read(self.fp, length) self.im_info["exif"] = b"Exif\x00\x00" + s return s # APNG chunks - def chunk_acTL(self, pos, length): + def chunk_acTL(self, pos: int, length: int) -> bytes: s = ImageFile._safe_read(self.fp, length) if length < 8: if ImageFile.LOAD_TRUNCATED_IMAGES: @@ -640,7 +645,7 @@ class PngStream(ChunkStream): self.im_custom_mimetype = "image/apng" return s - def chunk_fcTL(self, pos, length): + def chunk_fcTL(self, pos: int, length: int) -> bytes: s = ImageFile._safe_read(self.fp, length) if length < 26: if ImageFile.LOAD_TRUNCATED_IMAGES: @@ -669,7 +674,7 @@ class PngStream(ChunkStream): self.im_info["blend"] = s[25] return s - def chunk_fdAT(self, pos, length): + def chunk_fdAT(self, pos: int, length: int) -> bytes: if length < 4: if ImageFile.LOAD_TRUNCATED_IMAGES: s = ImageFile._safe_read(self.fp, length) @@ -701,7 +706,7 @@ class PngImageFile(ImageFile.ImageFile): format = "PNG" format_description = "Portable network graphics" - def _open(self): + def _open(self) -> None: if not _accept(self.fp.read(8)): msg = "not a PNG file" raise SyntaxError(msg) @@ -711,8 +716,8 @@ class PngImageFile(ImageFile.ImageFile): # # Parse headers up to the first IDAT or fDAT chunk - self.private_chunks = [] - self.png = PngStream(self.fp) + self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = [] + self.png: PngStream | None = PngStream(self.fp) while True: # @@ -793,6 +798,7 @@ class PngImageFile(ImageFile.ImageFile): # back up to beginning of IDAT block self.fp.seek(self.tile[0][2] - 8) + assert self.png is not None self.png.verify() self.png.close() @@ -921,9 +927,10 @@ class PngImageFile(ImageFile.ImageFile): self.__idat = self.__prepare_idat # used by load_read() ImageFile.ImageFile.load_prepare(self) - def load_read(self, read_bytes): + def load_read(self, read_bytes: int) -> bytes: """internal: read more image data""" + assert self.png is not None while self.__idat == 0: # end of chunk, skip forward to next one @@ -956,6 +963,7 @@ class PngImageFile(ImageFile.ImageFile): def load_end(self) -> None: """internal: finished reading image data""" + assert self.png is not None if self.__idat != 0: self.fp.read(self.__idat) while True: @@ -1079,7 +1087,7 @@ class _idat: self.fp = fp self.chunk = chunk - def write(self, data): + def write(self, data: bytes) -> None: self.chunk(self.fp, b"IDAT", data) @@ -1091,7 +1099,7 @@ class _fdat: self.chunk = chunk self.seq_num = seq_num - def write(self, data): + def write(self, data: bytes) -> None: self.chunk(self.fp, b"fdAT", o32(self.seq_num), data) self.seq_num += 1 @@ -1436,10 +1444,10 @@ def getchunks(im, **params): class collector: data = [] - def write(self, data): + def write(self, data: bytes) -> None: pass - def append(self, chunk): + def append(self, chunk: bytes) -> None: self.data.append(chunk) def append(fp, cid, *data): diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 4b8cfe65c..cae124e9f 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -171,7 +171,7 @@ class WebPImageFile(ImageFile.ImageFile): return super().load() - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: pass def tell(self) -> int: diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 88d14e9c2..8d56331e6 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -103,16 +103,13 @@ class XpmImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] - def load_read(self, read_bytes): + def load_read(self, read_bytes: int) -> bytes: # # load all image data in one chunk xsize, ysize = self.size - s = [None] * ysize - - for i in range(ysize): - s[i] = self.fp.readline()[1 : xsize + 1].ljust(xsize) + s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)] return b"".join(s) diff --git a/src/PIL/features.py b/src/PIL/features.py index 95c6c84cc..16c749f14 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -18,7 +18,7 @@ modules = { } -def check_module(feature): +def check_module(feature: str) -> bool: """ Checks if a module is available. @@ -42,7 +42,7 @@ def check_module(feature): return False -def version_module(feature): +def version_module(feature: str) -> str | None: """ :param feature: The module to check for. :returns: @@ -54,13 +54,10 @@ def version_module(feature): module, ver = modules[feature] - if ver is None: - return None - return getattr(__import__(module, fromlist=[ver]), ver) -def get_supported_modules(): +def get_supported_modules() -> list[str]: """ :returns: A list of all supported modules. """ @@ -75,7 +72,7 @@ codecs = { } -def check_codec(feature): +def check_codec(feature: str) -> bool: """ Checks if a codec is available. @@ -92,7 +89,7 @@ def check_codec(feature): return f"{codec}_encoder" in dir(Image.core) -def version_codec(feature): +def version_codec(feature: str) -> str | None: """ :param feature: The codec to check for. :returns: @@ -113,7 +110,7 @@ def version_codec(feature): return version -def get_supported_codecs(): +def get_supported_codecs() -> list[str]: """ :returns: A list of all supported codecs. """ @@ -133,7 +130,7 @@ features = { } -def check_feature(feature): +def check_feature(feature: str) -> bool | None: """ Checks if a feature is available. @@ -157,7 +154,7 @@ def check_feature(feature): return None -def version_feature(feature): +def version_feature(feature: str) -> str | None: """ :param feature: The feature to check for. :returns: The version number as a string, or ``None`` if not available. @@ -174,14 +171,14 @@ def version_feature(feature): return getattr(__import__(module, fromlist=[ver]), ver) -def get_supported_features(): +def get_supported_features() -> list[str]: """ :returns: A list of all supported features. """ return [f for f in features if check_feature(f)] -def check(feature): +def check(feature: str) -> bool | None: """ :param feature: A module, codec, or feature name. :returns: @@ -199,7 +196,7 @@ def check(feature): return False -def version(feature): +def version(feature: str) -> str | None: """ :param feature: The module, codec, or feature to check for. @@ -215,7 +212,7 @@ def version(feature): return None -def get_supported(): +def get_supported() -> list[str]: """ :returns: A list of all supported modules, features, and codecs. """ From 3062ec4dd2343258e91c240645d20d1c5b6cecf1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 May 2024 22:55:03 +1000 Subject: [PATCH 149/195] Fix type errors --- src/_imagingcms.c | 2 +- src/display.c | 2 +- src/libImaging/Dib.c | 2 +- src/libImaging/TiffDecode.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 63d78f84d..4b0e21d7a 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -622,7 +622,7 @@ cms_profile_is_intent_supported(CmsProfileObject *self, PyObject *args) { static PyObject * cms_get_display_profile_win32(PyObject *self, PyObject *args) { char filename[MAX_PATH]; - cmsUInt32Number filename_size; + DWORD filename_size; BOOL ok; HANDLE handle = 0; diff --git a/src/display.c b/src/display.c index 6b66ddafb..abf94f1e1 100644 --- a/src/display.c +++ b/src/display.c @@ -716,7 +716,7 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) { HDC dc; RECT rect; PyObject *buffer = NULL; - char *ptr; + void *ptr; char *data; Py_ssize_t datasize; diff --git a/src/libImaging/Dib.c b/src/libImaging/Dib.c index 1b5bfe132..5194bfca3 100644 --- a/src/libImaging/Dib.c +++ b/src/libImaging/Dib.c @@ -95,7 +95,7 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) { } dib->bitmap = - CreateDIBSection(dib->dc, dib->info, DIB_RGB_COLORS, &dib->bits, NULL, 0); + CreateDIBSection(dib->dc, dib->info, DIB_RGB_COLORS, (void **)&dib->bits, NULL, 0); if (!dib->bitmap) { free(dib->info); free(dib); diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index e3b81590e..4874dd26a 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -973,7 +973,7 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt } if (state->state == 1 && !clientstate->fp) { - int read = (int)_tiffReadProc(clientstate, (tdata_t)buffer, (tsize_t)bytes); + int read = (int)_tiffReadProc((thandle_t)clientstate, (tdata_t)buffer, (tsize_t)bytes); TRACE( ("Buffer: %p: %c%c%c%c\n", buffer, From 8a3a72e51d57360df6e91f6f73e73707f181069f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 18 May 2024 16:06:50 +1000 Subject: [PATCH 150/195] Added type hints --- docs/reference/ImageFile.rst | 4 ++++ src/PIL/BlpImagePlugin.py | 22 +++++++++--------- src/PIL/BufrStubImagePlugin.py | 4 ++-- src/PIL/DcxImagePlugin.py | 4 ++-- src/PIL/GimpPaletteFile.py | 9 ++++---- src/PIL/GribStubImagePlugin.py | 4 ++-- src/PIL/Hdf5StubImagePlugin.py | 8 ++++--- src/PIL/Image.py | 4 +++- src/PIL/ImageFile.py | 10 +++++++++ src/PIL/ImageFilter.py | 17 ++++++++------ src/PIL/ImageFont.py | 4 ++-- src/PIL/ImagePalette.py | 14 ++++++------ src/PIL/ImageWin.py | 22 ++++++++++-------- src/PIL/JpegImagePlugin.py | 15 +++++++------ src/PIL/PSDraw.py | 41 ++++++++++++++++++++-------------- src/PIL/PdfParser.py | 10 ++++----- src/PIL/PngImagePlugin.py | 8 +++---- src/PIL/PyAccess.py | 12 ++++++---- src/PIL/TiffImagePlugin.py | 2 +- src/PIL/WebPImagePlugin.py | 5 +++-- src/PIL/WmfImagePlugin.py | 10 ++++----- 21 files changed, 135 insertions(+), 94 deletions(-) diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index 047990f1c..e59c7311a 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -57,6 +57,10 @@ Classes :undoc-members: :show-inheritance: +.. autoclass:: PIL.ImageFile.StubHandler() + :members: + :show-inheritance: + .. autoclass:: PIL.ImageFile.StubImageFile() :members: :show-inheritance: diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index bdf54baae..782e28cf5 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -55,7 +55,7 @@ class AlphaEncoding(IntEnum): DXT5 = 7 -def unpack_565(i): +def unpack_565(i: int) -> tuple[int, int, int]: return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3 @@ -284,7 +284,8 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): raise OSError(msg) from e return -1, 0 - def _read_blp_header(self): + def _read_blp_header(self) -> None: + assert self.fd is not None self.fd.seek(4) (self._blp_compression,) = struct.unpack(" bytes: return ImageFile._safe_read(self.fd, length) - def _read_palette(self): + def _read_palette(self) -> list[tuple[int, int, int, int]]: ret = [] for i in range(256): try: @@ -349,29 +350,30 @@ class BLP1Decoder(_BLPBaseDecoder): msg = f"Unsupported BLP compression {repr(self._blp_encoding)}" raise BLPFormatError(msg) - def _decode_jpeg_stream(self): + def _decode_jpeg_stream(self) -> None: from .JpegImagePlugin import JpegImageFile (jpeg_header_size,) = struct.unpack(" None: palette = self._read_palette() + assert self.fd is not None self.fd.seek(self._blp_offsets[0]) if self._blp_compression == 1: diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 271db7258..826e89daf 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -15,7 +15,7 @@ from . import Image, ImageFile _handler = None -def register_handler(handler): +def register_handler(handler: ImageFile.StubHandler) -> None: """ Install application-specific BUFR image handler. @@ -54,7 +54,7 @@ class BufrStubImageFile(ImageFile.StubImageFile): if loader: loader.open(self) - def _load(self): + def _load(self) -> ImageFile.StubHandler | None: return _handler diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index 1c455b032..f67f27d73 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -42,7 +42,7 @@ class DcxImageFile(PcxImageFile): format_description = "Intel DCX" _close_exclusive_fp_after_loading = False - def _open(self): + def _open(self) -> None: # Header s = self.fp.read(4) if not _accept(s): @@ -58,7 +58,7 @@ class DcxImageFile(PcxImageFile): self._offset.append(offset) self._fp = self.fp - self.frame = None + self.frame = -1 self.n_frames = len(self._offset) self.is_animated = self.n_frames > 1 self.seek(0) diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 2274f1a8b..4cad0ebee 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -16,6 +16,7 @@ from __future__ import annotations import re +from typing import IO from ._binary import o8 @@ -25,8 +26,8 @@ class GimpPaletteFile: rawmode = "RGB" - def __init__(self, fp): - self.palette = [o8(i) * 3 for i in range(256)] + def __init__(self, fp: IO[bytes]) -> None: + palette = [o8(i) * 3 for i in range(256)] if fp.readline()[:12] != b"GIMP Palette": msg = "not a GIMP palette file" @@ -49,9 +50,9 @@ class GimpPaletteFile: msg = "bad palette entry" raise ValueError(msg) - self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) + palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) - self.palette = b"".join(self.palette) + self.palette = b"".join(palette) def getpalette(self) -> tuple[bytes, str]: return self.palette, self.rawmode diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 13bdfa616..c27cffab6 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -15,7 +15,7 @@ from . import Image, ImageFile _handler = None -def register_handler(handler): +def register_handler(handler: ImageFile.StubHandler) -> None: """ Install application-specific GRIB image handler. @@ -54,7 +54,7 @@ class GribStubImageFile(ImageFile.StubImageFile): if loader: loader.open(self) - def _load(self): + def _load(self) -> ImageFile.StubHandler | None: return _handler diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index afbfd1639..c8d7866a3 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -10,12 +10,14 @@ # from __future__ import annotations +from typing import IO + from . import Image, ImageFile _handler = None -def register_handler(handler): +def register_handler(handler: ImageFile.StubHandler) -> None: """ Install application-specific HDF5 image handler. @@ -54,11 +56,11 @@ class HDF5StubImageFile(ImageFile.StubImageFile): if loader: loader.open(self) - def _load(self): + def _load(self) -> ImageFile.StubHandler | None: return _handler -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if _handler is None or not hasattr(_handler, "save"): msg = "HDF5 save handler not installed" raise OSError(msg) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 958b95e3b..38ff0bfe4 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1948,7 +1948,9 @@ class Image: self.im.putband(alpha.im, band) - def putdata(self, data, scale=1.0, offset=0.0): + def putdata( + self, data: Sequence[float], scale: float = 1.0, offset: float = 0.0 + ) -> None: """ Copies pixel data from a flattened sequence object into the image. The values should start at the upper left corner (0, 0), continue to the diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 33467fc4f..f0e492387 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -28,6 +28,7 @@ # from __future__ import annotations +import abc import io import itertools import struct @@ -347,6 +348,15 @@ class ImageFile(Image.Image): return self.tell() != frame +class StubHandler: + def open(self, im: StubImageFile) -> None: + pass + + @abc.abstractmethod + def load(self, im: StubImageFile) -> Image.Image: + pass + + class StubImageFile(ImageFile): """ Base class for stub image loaders. diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 678bd29a2..43e700b7b 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -18,6 +18,7 @@ from __future__ import annotations import abc import functools +from typing import Sequence class Filter: @@ -79,7 +80,7 @@ class RankFilter(Filter): name = "Rank" - def __init__(self, size, rank): + def __init__(self, size: int, rank: int) -> None: self.size = size self.rank = rank @@ -101,7 +102,7 @@ class MedianFilter(RankFilter): name = "Median" - def __init__(self, size=3): + def __init__(self, size: int = 3) -> None: self.size = size self.rank = size * size // 2 @@ -116,7 +117,7 @@ class MinFilter(RankFilter): name = "Min" - def __init__(self, size=3): + def __init__(self, size: int = 3) -> None: self.size = size self.rank = 0 @@ -131,7 +132,7 @@ class MaxFilter(RankFilter): name = "Max" - def __init__(self, size=3): + def __init__(self, size: int = 3) -> None: self.size = size self.rank = size * size - 1 @@ -147,7 +148,7 @@ class ModeFilter(Filter): name = "Mode" - def __init__(self, size=3): + def __init__(self, size: int = 3) -> None: self.size = size def filter(self, image): @@ -165,7 +166,7 @@ class GaussianBlur(MultibandFilter): name = "GaussianBlur" - def __init__(self, radius=2): + def __init__(self, radius: float | Sequence[float] = 2) -> None: self.radius = radius def filter(self, image): @@ -228,7 +229,9 @@ class UnsharpMask(MultibandFilter): name = "UnsharpMask" - def __init__(self, radius=2, percent=150, threshold=3): + def __init__( + self, radius: float = 2, percent: int = 150, threshold: int = 3 + ) -> None: self.radius = radius self.percent = percent self.threshold = threshold diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 256c581df..5446bc0c0 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -261,7 +261,7 @@ class FreeTypeFont: """ return self.font.family, self.font.style - def getmetrics(self): + def getmetrics(self) -> tuple[int, int]: """ :return: A tuple of the font ascent (the distance from the baseline to the highest outline point) and descent (the distance from the @@ -628,7 +628,7 @@ class FreeTypeFont: layout_engine=layout_engine or self.layout_engine, ) - def get_variation_names(self): + def get_variation_names(self) -> list[bytes]: """ :returns: A list of the named styles in a variation font. :exception OSError: If the font is not a variation font. diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index ae5c5dec0..057ccd1d7 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -18,7 +18,7 @@ from __future__ import annotations import array -from typing import Sequence +from typing import IO, Sequence from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile @@ -166,7 +166,7 @@ class ImagePalette: msg = f"unknown color specifier: {repr(color)}" raise ValueError(msg) - def save(self, fp): + def save(self, fp: str | IO[str]) -> None: """Save palette to text file. .. warning:: This method is experimental. @@ -213,29 +213,29 @@ def make_linear_lut(black, white): raise NotImplementedError(msg) # FIXME -def make_gamma_lut(exp): +def make_gamma_lut(exp: float) -> list[int]: return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)] -def negative(mode="RGB"): +def negative(mode: str = "RGB") -> ImagePalette: palette = list(range(256 * len(mode))) palette.reverse() return ImagePalette(mode, [i // len(mode) for i in palette]) -def random(mode="RGB"): +def random(mode: str = "RGB") -> ImagePalette: from random import randint palette = [randint(0, 255) for _ in range(256 * len(mode))] return ImagePalette(mode, palette) -def sepia(white="#fff0c0"): +def sepia(white: str = "#fff0c0") -> ImagePalette: bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)] return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)]) -def wedge(mode="RGB"): +def wedge(mode: str = "RGB") -> ImagePalette: palette = list(range(256 * len(mode))) return ImagePalette(mode, [i // len(mode) for i in palette]) diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index 77e57a415..6c29e2590 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -28,10 +28,10 @@ class HDC: methods. """ - def __init__(self, dc): + def __init__(self, dc: int) -> None: self.dc = dc - def __int__(self): + def __int__(self) -> int: return self.dc @@ -42,10 +42,10 @@ class HWND: methods, instead of a DC. """ - def __init__(self, wnd): + def __init__(self, wnd: int) -> None: self.wnd = wnd - def __int__(self): + def __int__(self) -> int: return self.wnd @@ -149,7 +149,9 @@ class Dib: result = self.image.query_palette(handle) return result - def paste(self, im, box=None): + def paste( + self, im: Image.Image, box: tuple[int, int, int, int] | None = None + ) -> None: """ Paste a PIL image into the bitmap image. @@ -169,16 +171,16 @@ class Dib: else: self.image.paste(im.im) - def frombytes(self, buffer): + def frombytes(self, buffer: bytes) -> None: """ Load display memory contents from byte data. :param buffer: A buffer containing display data (usually data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`) """ - return self.image.frombytes(buffer) + self.image.frombytes(buffer) - def tobytes(self): + def tobytes(self) -> bytes: """ Copy display memory contents to bytes object. @@ -190,7 +192,9 @@ class Dib: class Window: """Create a Window with the given title size.""" - def __init__(self, title="PIL", width=None, height=None): + def __init__( + self, title: str = "PIL", width: int | None = None, height: int | None = None + ) -> None: self.hwnd = Image.core.createwindow( title, self.__dispatcher, width or 0, height or 0 ) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 909911dfe..9fea4e7d1 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -42,6 +42,7 @@ import subprocess import sys import tempfile import warnings +from typing import Any from . import Image, ImageFile from ._binary import i16be as i16 @@ -54,7 +55,7 @@ from .JpegPresets import presets # Parser -def Skip(self, marker): +def Skip(self: JpegImageFile, marker: int) -> None: n = i16(self.fp.read(2)) - 2 ImageFile._safe_read(self.fp, n) @@ -191,7 +192,7 @@ def APP(self, marker): self.info["dpi"] = 72, 72 -def COM(self, marker): +def COM(self: JpegImageFile, marker: int) -> None: # # Comment marker. Store these in the APP dictionary. n = i16(self.fp.read(2)) - 2 @@ -202,7 +203,7 @@ def COM(self, marker): self.applist.append(("COM", s)) -def SOF(self, marker): +def SOF(self: JpegImageFile, marker: int) -> None: # # Start of frame marker. Defines the size and mode of the # image. JPEG is colour blind, so we use some simple @@ -250,7 +251,7 @@ def SOF(self, marker): self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2])) -def DQT(self, marker): +def DQT(self: JpegImageFile, marker: int) -> None: # # Define quantization table. Note that there might be more # than one table in each marker. @@ -493,13 +494,13 @@ class JpegImageFile(ImageFile.ImageFile): self.tile = [] - def _getexif(self): + def _getexif(self) -> dict[str, Any] | None: return _getexif(self) def _getmp(self): return _getmp(self) - def getxmp(self): + def getxmp(self) -> dict[str, Any]: """ Returns a dictionary containing the XMP tags. Requires defusedxml to be installed. @@ -515,7 +516,7 @@ class JpegImageFile(ImageFile.ImageFile): return {} -def _getexif(self): +def _getexif(self) -> dict[str, Any] | None: if "exif" not in self.info: return None return self.getexif()._get_merged_dict() diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index 49c06ce13..4e2b9788e 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -17,6 +17,7 @@ from __future__ import annotations import sys +from typing import TYPE_CHECKING from . import EpsImagePlugin @@ -38,7 +39,7 @@ class PSDraw: fp = sys.stdout self.fp = fp - def begin_document(self, id=None): + def begin_document(self, id: str | None = None) -> None: """Set up printing of a document. (Write PostScript DSC header.)""" # FIXME: incomplete self.fp.write( @@ -52,7 +53,7 @@ class PSDraw: self.fp.write(EDROFF_PS) self.fp.write(VDI_PS) self.fp.write(b"%%EndProlog\n") - self.isofont = {} + self.isofont: dict[bytes, int] = {} def end_document(self) -> None: """Ends printing. (Write PostScript DSC footer.)""" @@ -60,22 +61,24 @@ class PSDraw: if hasattr(self.fp, "flush"): self.fp.flush() - def setfont(self, font, size): + def setfont(self, font: str, size: int) -> None: """ Selects which font to use. :param font: A PostScript font name :param size: Size in points. """ - font = bytes(font, "UTF-8") - if font not in self.isofont: + font_bytes = bytes(font, "UTF-8") + if font_bytes not in self.isofont: # reencode font - self.fp.write(b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font, font)) - self.isofont[font] = 1 + self.fp.write( + b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font_bytes, font_bytes) + ) + self.isofont[font_bytes] = 1 # rough - self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font)) + self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font_bytes)) - def line(self, xy0, xy1): + def line(self, xy0: tuple[int, int], xy1: tuple[int, int]) -> None: """ Draws a line between the two points. Coordinates are given in PostScript point coordinates (72 points per inch, (0, 0) is the lower @@ -83,7 +86,7 @@ class PSDraw: """ self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1)) - def rectangle(self, box): + def rectangle(self, box: tuple[int, int, int, int]) -> None: """ Draws a rectangle. @@ -92,18 +95,22 @@ class PSDraw: """ self.fp.write(b"%d %d M 0 %d %d Vr\n" % box) - def text(self, xy, text): + def text(self, xy: tuple[int, int], text: str) -> None: """ Draws text at the given position. You must use :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. """ - text = bytes(text, "UTF-8") - text = b"\\(".join(text.split(b"(")) - text = b"\\)".join(text.split(b")")) - xy += (text,) - self.fp.write(b"%d %d M (%s) S\n" % xy) + text_bytes = bytes(text, "UTF-8") + text_bytes = b"\\(".join(text_bytes.split(b"(")) + text_bytes = b"\\)".join(text_bytes.split(b")")) + self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,))) - def image(self, box, im, dpi=None): + if TYPE_CHECKING: + from . import Image + + def image( + self, box: tuple[int, int, int, int], im: Image.Image, dpi: int | None = None + ) -> None: """Draw a PIL image, centered in the given box.""" # default resolution depends on mode if not dpi: diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 68501d625..a6c24e671 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Any, List, NamedTuple, Union # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set # on page 656 -def encode_text(s): +def encode_text(s: str) -> bytes: return codecs.BOM_UTF16_BE + s.encode("utf_16_be") @@ -103,7 +103,7 @@ class IndirectReference(IndirectReferenceTuple): def __ne__(self, other): return not (self == other) - def __hash__(self): + def __hash__(self) -> int: return hash((self.object_id, self.generation)) @@ -219,7 +219,7 @@ class PdfName: isinstance(other, PdfName) and other.name == self.name ) or other == self.name - def __hash__(self): + def __hash__(self) -> int: return hash(self.name) def __repr__(self) -> str: @@ -402,7 +402,7 @@ class PdfParser: if f: self.seek_end() - def __enter__(self): + def __enter__(self) -> PdfParser: return self def __exit__(self, exc_type, exc_value, traceback): @@ -436,7 +436,7 @@ class PdfParser: def write_comment(self, s): self.f.write(f"% {s}\n".encode()) - def write_catalog(self): + def write_catalog(self) -> IndirectReference: self.del_root() self.root_ref = self.next_object_id(self.f.tell()) self.pages_ref = self.next_object_id(0) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index c74cbccf1..f7ccc8381 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -39,7 +39,7 @@ import struct import warnings import zlib from enum import IntEnum -from typing import IO +from typing import IO, Any from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16be as i16 @@ -1019,7 +1019,7 @@ class PngImageFile(ImageFile.ImageFile): if self.pyaccess: self.pyaccess = None - def _getexif(self): + def _getexif(self) -> dict[str, Any] | None: if "exif" not in self.info: self.load() if "exif" not in self.info and "Raw profile type exif" not in self.info: @@ -1032,7 +1032,7 @@ class PngImageFile(ImageFile.ImageFile): return super().getexif() - def getxmp(self): + def getxmp(self) -> dict[str, Any]: """ Returns a dictionary containing the XMP tags. Requires defusedxml to be installed. @@ -1234,7 +1234,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) seq_num = fdat_chunks.seq_num -def _save_all(im, fp, filename): +def _save_all(im: Image.Image, fp: IO[bytes], filename: str) -> None: _save(im, fp, filename, save_all=True) diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index a9da90613..f476713ca 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -22,6 +22,7 @@ from __future__ import annotations import logging import sys +from typing import TYPE_CHECKING from ._deprecate import deprecate @@ -48,9 +49,12 @@ except ImportError as ex: logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from . import Image + class PyAccess: - def __init__(self, img, readonly=False): + def __init__(self, img: Image.Image, readonly: bool = False) -> None: deprecate("PyAccess", 11) vals = dict(img.im.unsafe_ptrs) self.readonly = readonly @@ -130,7 +134,7 @@ class PyAccess: putpixel = __setitem__ getpixel = __getitem__ - def check_xy(self, xy): + def check_xy(self, xy: tuple[int, int]) -> tuple[int, int]: (x, y) = xy if not (0 <= x < self.xsize and 0 <= y < self.ysize): msg = "pixel location out of range" @@ -161,7 +165,7 @@ class _PyAccess32_3(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> tuple[int, int, int]: pixel = self.pixels[y][x] return pixel.r, pixel.g, pixel.b @@ -180,7 +184,7 @@ class _PyAccess32_4(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> tuple[int, int, int, int]: pixel = self.pixels[y][x] return pixel.r, pixel.g, pixel.b, pixel.a diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 54faa59c5..f3fa3c24c 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1202,7 +1202,7 @@ class TiffImageFile(ImageFile.ImageFile): """Return the current frame number""" return self.__frame - def getxmp(self): + def getxmp(self) -> dict[str, Any]: """ Returns a dictionary containing the XMP tags. Requires defusedxml to be installed. diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index cae124e9f..ff7402dca 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from typing import Any from . import Image, ImageFile @@ -95,12 +96,12 @@ class WebPImageFile(ImageFile.ImageFile): # Initialize seek state self._reset(reset=False) - def _getexif(self): + def _getexif(self) -> dict[str, Any] | None: if "exif" not in self.info: return None return self.getexif()._get_merged_dict() - def getxmp(self): + def getxmp(self) -> dict[str, Any]: """ Returns a dictionary containing the XMP tags. Requires defusedxml to be installed. diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index b0328657b..fab3e26c5 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -28,7 +28,7 @@ from ._binary import si32le as _long _handler = None -def register_handler(handler): +def register_handler(handler: ImageFile.StubHandler) -> None: """ Install application-specific WMF image handler. @@ -41,12 +41,12 @@ def register_handler(handler): if hasattr(Image.core, "drawwmf"): # install default handler (windows only) - class WmfHandler: - def open(self, im): + class WmfHandler(ImageFile.StubHandler): + def open(self, im: ImageFile.StubImageFile) -> None: im._mode = "RGB" self.bbox = im.info["wmf_bbox"] - def load(self, im): + def load(self, im: ImageFile.StubImageFile) -> Image.Image: im.fp.seek(0) # rewind return Image.frombytes( "RGB", @@ -147,7 +147,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): if loader: loader.open(self) - def _load(self): + def _load(self) -> ImageFile.StubHandler | None: return _handler def load(self, dpi=None): From b2316f46cb4fc084fc15cb2848eca8b19cbc4329 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Sat, 18 May 2024 11:22:57 +0200 Subject: [PATCH 151/195] Use just `str` for `_string_length_check` Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageFont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index f2936bae6..747c0c050 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -61,7 +61,7 @@ except ImportError as ex: core = DeferredError.new(ex) -def _string_length_check(text: str | bytes) -> None: +def _string_length_check(text: str) -> None: if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: msg = "too many characters in string" raise ValueError(msg) From 82910a5e4f1f12472dd85ce31090e7abeefe9814 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 19 May 2024 22:00:45 +1000 Subject: [PATCH 152/195] Lint fixes --- src/libImaging/Dib.c | 4 ++-- src/libImaging/TiffDecode.c | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libImaging/Dib.c b/src/libImaging/Dib.c index 5194bfca3..269be1058 100644 --- a/src/libImaging/Dib.c +++ b/src/libImaging/Dib.c @@ -94,8 +94,8 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) { return (ImagingDIB)ImagingError_MemoryError(); } - dib->bitmap = - CreateDIBSection(dib->dc, dib->info, DIB_RGB_COLORS, (void **)&dib->bits, NULL, 0); + dib->bitmap = CreateDIBSection( + dib->dc, dib->info, DIB_RGB_COLORS, (void **)&dib->bits, NULL, 0); if (!dib->bitmap) { free(dib->info); free(dib); diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 8b4d7aeac..abffdeabc 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -1005,7 +1005,8 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt } if (state->state == 1 && !clientstate->fp) { - int read = (int)_tiffReadProc((thandle_t)clientstate, (tdata_t)buffer, (tsize_t)bytes); + int read = + (int)_tiffReadProc((thandle_t)clientstate, (tdata_t)buffer, (tsize_t)bytes); TRACE( ("Buffer: %p: %c%c%c%c\n", buffer, From ea9dc1e4a5c5c508bbb5c7cbd29f207836e77007 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 21:30:03 +0000 Subject: [PATCH 153/195] chore(deps): update dependency cibuildwheel to v2.18.1 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 8d39ea9bb..7e257b75c 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.18.0 +cibuildwheel==2.18.1 From 3cc26e9ea614def5a5efdcbcfd646ff50e3936e2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 21 May 2024 12:55:48 +1000 Subject: [PATCH 154/195] Added Python 3.13 wheels --- .github/workflows/wheels-test.sh | 2 +- .github/workflows/wheels.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 3fbf3be69..a3376ac92 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -12,7 +12,7 @@ elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then else yum install -y fribidi fi -if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then +if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ] && !([[ "$OSTYPE" == "darwin"* ]] && [[ $(python3 --version) == *"3.13."* ]]); then python3 -m pip install numpy fi diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index b2fbd3140..3d6099c1c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -46,6 +46,7 @@ jobs: - cp310 - cp311 - cp312 + - cp313 spec: - manylinux2014 - manylinux_2_28 @@ -80,6 +81,7 @@ jobs: CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" + CIBW_PRERELEASE_PYTHONS: True # Extra options for manylinux. CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} @@ -133,6 +135,7 @@ jobs: CIBW_BUILD: ${{ matrix.build }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} + CIBW_PRERELEASE_PYTHONS: True CIBW_SKIP: pp38-* CIBW_TEST_SKIP: cp38-macosx_arm64 MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} @@ -204,6 +207,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" + CIBW_PRERELEASE_PYTHONS: True CIBW_SKIP: pp38-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm From d461ff8cef83116abffbd24145459ee1300c520d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 May 2024 11:50:10 +1000 Subject: [PATCH 155/195] Added release notes --- docs/releasenotes/10.4.0.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.4.0.rst b/docs/releasenotes/10.4.0.rst index 3150bf4e0..41f33102f 100644 --- a/docs/releasenotes/10.4.0.rst +++ b/docs/releasenotes/10.4.0.rst @@ -53,7 +53,9 @@ TODO Other Changes ============= -TODO -^^^^ +Python 3.13 beta +^^^^^^^^^^^^^^^^ -TODO +To help others prepare for Python 3.13, wheels have been built against the 3.13 beta as +a preview. This is not official support for Python 3.13, but simply an opportunity for +users to test how Pillow works with the beta and report any problems. From 33e304ed66851ed7cbd6e9018764ec9ec089c25f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 May 2024 19:43:00 +1000 Subject: [PATCH 156/195] Use @cached_property --- src/PIL/GifImagePlugin.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index eede41549..962a92834 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -30,6 +30,7 @@ import math import os import subprocess from enum import IntEnum +from functools import cached_property from . import ( Image, @@ -112,8 +113,7 @@ class GifImageFile(ImageFile.ImageFile): self._fp = self.fp # FIXME: hack self.__rewind = self.fp.tell() - self._n_frames = None - self._is_animated = None + self._n_frames: int | None = None self._seek(0) # get ready to read first frame @property @@ -128,24 +128,23 @@ class GifImageFile(ImageFile.ImageFile): self.seek(current) return self._n_frames - @property - def is_animated(self): - if self._is_animated is None: - if self._n_frames is not None: - self._is_animated = self._n_frames != 1 - else: - current = self.tell() - if current: - self._is_animated = True - else: - try: - self._seek(1, False) - self._is_animated = True - except EOFError: - self._is_animated = False + @cached_property + def is_animated(self) -> bool: + if self._n_frames is not None: + return self._n_frames != 1 - self.seek(current) - return self._is_animated + current = self.tell() + if current: + return True + + try: + self._seek(1, False) + is_animated = True + except EOFError: + is_animated = False + + self.seek(current) + return is_animated def seek(self, frame: int) -> None: if not self._seek_check(frame): From 92d1879a776be3b109a26786d6611cab9ef6181c Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 23 May 2024 13:27:53 -0500 Subject: [PATCH 157/195] add mypy task to makefile --- Makefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Makefile b/Makefile index 1f9b4a370..94f7565d8 100644 --- a/Makefile +++ b/Makefile @@ -118,3 +118,8 @@ lint-fix: python3 -m black . python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff python3 -m ruff --fix . + +.PHONY: mypy +mypy: + python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox + python3 -m tox -e mypy From 16cd3584548d8b5f4cd81d8460805b6604530a6f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 May 2024 20:32:52 +1000 Subject: [PATCH 158/195] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c5df1f8f7..8a531d1e2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 10.4.0 (unreleased) ------------------- +- Add mypy target to Makefile #8077 + [Yay295] + +- Added more modes to Image.MODES #7984 + [radarhere] + - Deprecate BGR;15, BGR;16 and BGR;24 modes #7978 [radarhere, hugovk] From 2c9b5f03607d083665f5880506197405197f34ae Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 May 2024 06:20:03 +1000 Subject: [PATCH 159/195] Updated Ghostscript to 10.3.1 --- .appveyor.yml | 2 +- .github/workflows/test-windows.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 57a8fa5a0..6470dbc4c 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -34,7 +34,7 @@ install: - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images - curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip - 7z x nasm-win64.zip -oc:\ -- choco install ghostscript --version=10.3.0 +- choco install ghostscript --version=10.3.1 - path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 9edc15173..ee265774b 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -86,7 +86,7 @@ jobs: choco install nasm --no-progress echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - choco install ghostscript --version=10.3.0 --no-progress + choco install ghostscript --version=10.3.1 --no-progress echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH # Install extra test images From 1a6b0bb6b5423dba1607dfee6d4ea437a58cc246 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 May 2024 19:30:20 +1000 Subject: [PATCH 160/195] Removed documentation of unused argument --- src/PIL/ImageFont.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 256c581df..ad5f75459 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -160,10 +160,6 @@ class ImageFont: .. versionadded:: 9.2.0 :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. :return: ``(left, top, right, bottom)`` bounding box """ From 1b878189d857bcc823f887cf94650a5f0eaa182a Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 26 May 2024 02:25:45 -0500 Subject: [PATCH 161/195] don't reuse variable name --- src/PIL/PngImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index c74cbccf1..31706cef9 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1294,7 +1294,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): # get the corresponding PNG mode try: - rawmode, mode = _OUTMODES[mode] + rawmode, rawmode_depth_type = _OUTMODES[mode] except KeyError as e: msg = f"cannot write mode {mode} as PNG" raise OSError(msg) from e @@ -1309,7 +1309,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): b"IHDR", o32(size[0]), # 0: size o32(size[1]), - mode, # 8: depth/type + rawmode_depth_type, # 8: depth/type b"\0", # 10: compression b"\0", # 11: filter category b"\0", # 12: interlace flag From 82d992690572a270f4a8280cf48318fb8f70f2c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 26 May 2024 21:18:14 +1000 Subject: [PATCH 162/195] Split depth/type into bit depth and color type --- src/PIL/PngImagePlugin.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 31706cef9..0d5751962 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1050,22 +1050,22 @@ class PngImageFile(ImageFile.ImageFile): # PNG writer _OUTMODES = { - # supported PIL modes, and corresponding rawmodes/bits/color combinations - "1": ("1", b"\x01\x00"), - "L;1": ("L;1", b"\x01\x00"), - "L;2": ("L;2", b"\x02\x00"), - "L;4": ("L;4", b"\x04\x00"), - "L": ("L", b"\x08\x00"), - "LA": ("LA", b"\x08\x04"), - "I": ("I;16B", b"\x10\x00"), - "I;16": ("I;16B", b"\x10\x00"), - "I;16B": ("I;16B", b"\x10\x00"), - "P;1": ("P;1", b"\x01\x03"), - "P;2": ("P;2", b"\x02\x03"), - "P;4": ("P;4", b"\x04\x03"), - "P": ("P", b"\x08\x03"), - "RGB": ("RGB", b"\x08\x02"), - "RGBA": ("RGBA", b"\x08\x06"), + # supported PIL modes, and corresponding rawmode, bit depth and color type + "1": ("1", b"\x01", b"\x00"), + "L;1": ("L;1", b"\x01", b"\x00"), + "L;2": ("L;2", b"\x02", b"\x00"), + "L;4": ("L;4", b"\x04", b"\x00"), + "L": ("L", b"\x08", b"\x00"), + "LA": ("LA", b"\x08", b"\x04"), + "I": ("I;16B", b"\x10", b"\x00"), + "I;16": ("I;16B", b"\x10", b"\x00"), + "I;16B": ("I;16B", b"\x10", b"\x00"), + "P;1": ("P;1", b"\x01", b"\x03"), + "P;2": ("P;2", b"\x02", b"\x03"), + "P;4": ("P;4", b"\x04", b"\x03"), + "P": ("P", b"\x08", b"\x03"), + "RGB": ("RGB", b"\x08", b"\x02"), + "RGBA": ("RGBA", b"\x08", b"\x06"), } @@ -1294,7 +1294,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): # get the corresponding PNG mode try: - rawmode, rawmode_depth_type = _OUTMODES[mode] + rawmode, bit_depth, color_type = _OUTMODES[mode] except KeyError as e: msg = f"cannot write mode {mode} as PNG" raise OSError(msg) from e @@ -1309,7 +1309,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): b"IHDR", o32(size[0]), # 0: size o32(size[1]), - rawmode_depth_type, # 8: depth/type + bit_depth, + color_type, b"\0", # 10: compression b"\0", # 11: filter category b"\0", # 12: interlace flag From 2c4a6e1179a7e2460d3dae9f4558173c547996a7 Mon Sep 17 00:00:00 2001 From: void4 Date: Mon, 27 May 2024 00:23:16 +0200 Subject: [PATCH 163/195] Add function and documentation to draw circle --- docs/reference/ImageDraw.rst | 13 +++++++++++++ src/PIL/ImageDraw.py | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 4ccfacae7..6987adc88 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -240,6 +240,19 @@ Methods .. versionadded:: 5.3.0 +.. py:method:: ImageDraw.circle(xy, radius, fill=None, outline=None, width=1) + + Draws a circle given the center coordinates and a radius. + + :param xy: One point to define the circle center. Sequence: + ``[x, y]`` + :param radius: Radius of the circle + :param outline: Color to use for the outline. + :param fill: Color to use for the fill. + :param width: The line width, in pixels. + + .. versionadded:: ?.?.? + .. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) Draws a line between the coordinates in the ``xy`` list. diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 42f2ee8c7..4b42b32d0 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -181,6 +181,15 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_ellipse(xy, ink, 0, width) + def circle(self, xy: Coords, radius, fill=None, outline=None, width=1) -> None: + """Draw a circle given center coordinates and a radius.""" + ink, fill = self._getink(outline, fill) + ellipse_xy = (xy[0]-radius, xy[1]-radius, xy[0]+radius, xy[1]+radius) + if fill is not None: + self.draw.draw_ellipse(ellipse_xy, fill, 1) + if ink is not None and ink != fill and width != 0: + self.draw.draw_ellipse(ellipse_xy, ink, 0, width) + def line(self, xy: Coords, fill=None, width=0, joint=None) -> None: """Draw a line, or a connected sequence of line segments.""" ink = self._getink(fill)[0] From 2ee3cef50ef6b1917ea3c03ad06849a2a599ee8a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 26 May 2024 22:25:13 +0000 Subject: [PATCH 164/195] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/ImageDraw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 4b42b32d0..03f2637ae 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -184,7 +184,7 @@ class ImageDraw: def circle(self, xy: Coords, radius, fill=None, outline=None, width=1) -> None: """Draw a circle given center coordinates and a radius.""" ink, fill = self._getink(outline, fill) - ellipse_xy = (xy[0]-radius, xy[1]-radius, xy[0]+radius, xy[1]+radius) + ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius) if fill is not None: self.draw.draw_ellipse(ellipse_xy, fill, 1) if ink is not None and ink != fill and width != 0: From 8d9a4dda980f02ff93fa2301d906ef32602c5e07 Mon Sep 17 00:00:00 2001 From: void4 Date: Mon, 27 May 2024 12:57:50 +0200 Subject: [PATCH 165/195] Update docs/reference/ImageDraw.rst - Set versionadded Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/reference/ImageDraw.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 6987adc88..51d5965db 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -251,7 +251,7 @@ Methods :param fill: Color to use for the fill. :param width: The line width, in pixels. - .. versionadded:: ?.?.? + .. versionadded:: 10.4.0 .. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) From 38e6913579c3c7c04bb40384cd7eb797130c5dd8 Mon Sep 17 00:00:00 2001 From: void4 Date: Mon, 27 May 2024 13:01:10 +0200 Subject: [PATCH 166/195] Simplify circle() by reusing ellipse() --- src/PIL/ImageDraw.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 03f2637ae..b270cc6ba 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -183,12 +183,8 @@ class ImageDraw: def circle(self, xy: Coords, radius, fill=None, outline=None, width=1) -> None: """Draw a circle given center coordinates and a radius.""" - ink, fill = self._getink(outline, fill) ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius) - if fill is not None: - self.draw.draw_ellipse(ellipse_xy, fill, 1) - if ink is not None and ink != fill and width != 0: - self.draw.draw_ellipse(ellipse_xy, ink, 0, width) + self.ellipse(ellipse_xy, fill, outline, width) def line(self, xy: Coords, fill=None, width=0, joint=None) -> None: """Draw a line, or a connected sequence of line segments.""" From 35a700a1d4b2de51c8e13d4b60736f765b8b816b Mon Sep 17 00:00:00 2001 From: void4 Date: Mon, 27 May 2024 13:14:04 +0200 Subject: [PATCH 167/195] Update 10.4.0.rst - Add PIL.ImageDraw.circle() API addition --- docs/releasenotes/10.4.0.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/releasenotes/10.4.0.rst b/docs/releasenotes/10.4.0.rst index 41f33102f..3c4258801 100644 --- a/docs/releasenotes/10.4.0.rst +++ b/docs/releasenotes/10.4.0.rst @@ -45,6 +45,10 @@ TODO API Additions ============= +Added PIL.ImageDraw.circle() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Given the circle center coordinate pair and a radius, plots a circle using PIL.ImageDraw.ellipse() + TODO ^^^^ From 773ff20b762247a9fa717683f5e89349830def7e Mon Sep 17 00:00:00 2001 From: void4 Date: Mon, 27 May 2024 13:18:47 +0200 Subject: [PATCH 168/195] Update docs/reference/ImageDraw.rst - move circle method up to indicate it is new Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/reference/ImageDraw.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 51d5965db..ed762c8d6 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -243,6 +243,8 @@ Methods .. py:method:: ImageDraw.circle(xy, radius, fill=None, outline=None, width=1) Draws a circle given the center coordinates and a radius. + + .. versionadded:: 10.4.0 :param xy: One point to define the circle center. Sequence: ``[x, y]`` From 034f3cbed522b862b07bf938bbce0a599018bc57 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 11:19:09 +0000 Subject: [PATCH 169/195] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/reference/ImageDraw.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index ed762c8d6..4996acf71 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -243,7 +243,7 @@ Methods .. py:method:: ImageDraw.circle(xy, radius, fill=None, outline=None, width=1) Draws a circle given the center coordinates and a radius. - + .. versionadded:: 10.4.0 :param xy: One point to define the circle center. Sequence: From 9b7556228e0f045495f6b3dd2fd22e84b65ab0eb Mon Sep 17 00:00:00 2001 From: void4 Date: Mon, 27 May 2024 13:21:41 +0200 Subject: [PATCH 170/195] Update docs/reference/ImageDraw.rst - move versionadded Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/reference/ImageDraw.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 4996acf71..c4ce76cc0 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -253,8 +253,6 @@ Methods :param fill: Color to use for the fill. :param width: The line width, in pixels. - .. versionadded:: 10.4.0 - .. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) Draws a line between the coordinates in the ``xy`` list. From 8db5fbead1955ed6f1f4810e3b36c46483dcc62e Mon Sep 17 00:00:00 2001 From: void4 Date: Mon, 27 May 2024 13:27:56 +0200 Subject: [PATCH 171/195] Update src/PIL/ImageDraw.py - set circle argument xy to type Sequence[float] instead of Coords, radius to float Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageDraw.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index b270cc6ba..17c176430 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -181,7 +181,9 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_ellipse(xy, ink, 0, width) - def circle(self, xy: Coords, radius, fill=None, outline=None, width=1) -> None: + def circle( + self, xy: Sequence[float], radius: float, fill=None, outline=None, width=1 + ) -> None: """Draw a circle given center coordinates and a radius.""" ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius) self.ellipse(ellipse_xy, fill, outline, width) From 12cefd798e7b28d05af4a39abd601bc862081881 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 May 2024 21:48:38 +1000 Subject: [PATCH 172/195] Added method links to release notes --- docs/reference/ImageDraw.rst | 25 ++++++++++++------------- docs/releasenotes/10.4.0.rst | 9 ++++++--- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index c4ce76cc0..1404869ca 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -227,6 +227,18 @@ Methods .. versionadded:: 5.3.0 +.. py:method:: ImageDraw.circle(xy, radius, fill=None, outline=None, width=1) + + Draws a circle with a given radius centering on a point. + + .. versionadded:: 10.4.0 + + :param xy: The point for the center of the circle, e.g. ``(x, y)``. + :param radius: Radius of the circle. + :param outline: Color to use for the outline. + :param fill: Color to use for the fill. + :param width: The line width, in pixels. + .. py:method:: ImageDraw.ellipse(xy, fill=None, outline=None, width=1) Draws an ellipse inside the given bounding box. @@ -240,19 +252,6 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: ImageDraw.circle(xy, radius, fill=None, outline=None, width=1) - - Draws a circle given the center coordinates and a radius. - - .. versionadded:: 10.4.0 - - :param xy: One point to define the circle center. Sequence: - ``[x, y]`` - :param radius: Radius of the circle - :param outline: Color to use for the outline. - :param fill: Color to use for the fill. - :param width: The line width, in pixels. - .. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) Draws a line between the coordinates in the ``xy`` list. diff --git a/docs/releasenotes/10.4.0.rst b/docs/releasenotes/10.4.0.rst index 3c4258801..e0d695a8b 100644 --- a/docs/releasenotes/10.4.0.rst +++ b/docs/releasenotes/10.4.0.rst @@ -45,9 +45,12 @@ TODO API Additions ============= -Added PIL.ImageDraw.circle() -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Given the circle center coordinate pair and a radius, plots a circle using PIL.ImageDraw.ellipse() +ImageDraw.circle +^^^^^^^^^^^^^^^^ + +Added :py:meth:`~PIL.ImageDraw.ImageDraw.circle`. It provides the same functionality as +:py:meth:`~PIL.ImageDraw.ImageDraw.ellipse`, but instead of taking a bounding box, it +takes a center point and radius. TODO ^^^^ From cac1a04329a2bb0864075f0aca4ff6e2c48a45cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 May 2024 21:59:32 +1000 Subject: [PATCH 173/195] Added test --- Tests/test_imagedraw.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 0a699e2ab..69d09e03d 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -2,6 +2,7 @@ from __future__ import annotations import contextlib import os.path +from typing import Sequence import pytest @@ -265,6 +266,21 @@ def test_chord_too_fat() -> None: assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_too_fat.png") +@pytest.mark.parametrize("mode", ("RGB", "L")) +@pytest.mark.parametrize("xy", ((W / 2, H / 2), [W / 2, H / 2])) +def test_circle(mode: str, xy: Sequence[float]) -> None: + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = f"Tests/images/imagedraw_ellipse_{mode}.png" + + # Act + draw.circle(xy, 25, fill="green", outline="blue") + + # Assert + assert_image_similar_tofile(im, expected, 1) + + @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) def test_ellipse(mode: str, bbox: Coords) -> None: From 759ab28757430d0661cdbd32410c30f663b4ca9c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 28 May 2024 22:16:04 +1000 Subject: [PATCH 174/195] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8a531d1e2..dc4016d76 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.4.0 (unreleased) ------------------- +- Added ImageDraw circle() #8085 + [void4, hugovk, radarhere] + - Add mypy target to Makefile #8077 [Yay295] From a6d1daeb4b5e2797a393492d82333870ba7660a7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 May 2024 22:51:02 +1000 Subject: [PATCH 175/195] Added type hints --- Tests/helper.py | 25 +++++++++++-------------- Tests/test_file_webp_animated.py | 10 ++++++---- Tests/test_imagecms.py | 19 ++++++++++++++----- Tests/test_imagefile.py | 6 ++++++ Tests/test_imagefont.py | 23 +++++++++++++++-------- 5 files changed, 52 insertions(+), 31 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 5fd4fe332..fe337c09f 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -174,12 +174,13 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator: def skip_unless_feature_version( feature: str, required: str, reason: str | None = None ) -> pytest.MarkDecorator: - if not features.check(feature): + version = features.version(feature) + if version is None: return pytest.mark.skip(f"{feature} not available") if reason is None: reason = f"{feature} is older than {required}" version_required = parse_version(required) - version_available = parse_version(features.version(feature)) + version_available = parse_version(version) return pytest.mark.skipif(version_available < version_required, reason=reason) @@ -189,12 +190,13 @@ def mark_if_feature_version( version_blacklist: str, reason: str | None = None, ) -> pytest.MarkDecorator: - if not features.check(feature): + version = features.version(feature) + if version is None: return pytest.mark.pil_noop_mark() if reason is None: reason = f"{feature} is {version_blacklist}" version_required = parse_version(version_blacklist) - version_available = parse_version(features.version(feature)) + version_available = parse_version(version) if ( version_available.major == version_required.major and version_available.minor == version_required.minor @@ -220,16 +222,11 @@ class PillowLeakTestCase: from resource import RUSAGE_SELF, getrusage mem = getrusage(RUSAGE_SELF).ru_maxrss - if sys.platform == "darwin": - # man 2 getrusage: - # ru_maxrss - # This is the maximum resident set size utilized (in bytes). - return mem / 1024 # Kb - # linux - # man 2 getrusage - # ru_maxrss (since Linux 2.6.32) - # This is the maximum resident set size used (in kilobytes). - return mem # Kb + # man 2 getrusage: + # ru_maxrss + # This is the maximum resident set size utilized + # in bytes on macOS, in kilobytes on Linux + return mem / 1024 if sys.platform == "darwin" else mem def _test_leak(self, core: Callable[[], None]) -> None: start_mem = self._get_mem_usage() diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 6a9337fa5..ba931f864 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -52,8 +52,9 @@ def test_write_animation_L(tmp_path: Path) -> None: assert_image_similar(im, orig.convert("RGBA"), 32.9) if is_big_endian(): - webp = parse_version(features.version_module("webp")) - if webp < parse_version("1.2.2"): + version = features.version_module("webp") + assert version is not None + if parse_version(version) < parse_version("1.2.2"): pytest.skip("Fails with libwebp earlier than 1.2.2") orig.seek(orig.n_frames - 1) im.seek(im.n_frames - 1) @@ -78,8 +79,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None: # Compare second frame to original if is_big_endian(): - webp = parse_version(features.version_module("webp")) - if webp < parse_version("1.2.2"): + version = features.version_module("webp") + assert version is not None + if parse_version(version) < parse_version("1.2.2"): pytest.skip("Fails with libwebp earlier than 1.2.2") im.seek(1) im.load() diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index bf629fa79..8d2029c21 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -60,10 +60,13 @@ def test_sanity() -> None: assert list(map(type, v)) == [str, str, str, str] # internal version number - assert re.search(r"\d+\.\d+(\.\d+)?$", features.version_module("littlecms2")) + version = features.version_module("littlecms2") + assert version is not None + assert re.search(r"\d+\.\d+(\.\d+)?$", version) skip_missing() i = ImageCms.profileToProfile(hopper(), SRGB, SRGB) + assert i is not None assert_image(i, "RGB", (128, 128)) i = hopper() @@ -72,23 +75,27 @@ def test_sanity() -> None: t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") i = ImageCms.applyTransform(hopper(), t) + assert i is not None assert_image(i, "RGB", (128, 128)) with hopper() as i: t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") ImageCms.applyTransform(hopper(), t, inPlace=True) + assert i is not None assert_image(i, "RGB", (128, 128)) p = ImageCms.createProfile("sRGB") o = ImageCms.getOpenProfile(SRGB) t = ImageCms.buildTransformFromOpenProfiles(p, o, "RGB", "RGB") i = ImageCms.applyTransform(hopper(), t) + assert i is not None assert_image(i, "RGB", (128, 128)) t = ImageCms.buildProofTransform(SRGB, SRGB, SRGB, "RGB", "RGB") assert t.inputMode == "RGB" assert t.outputMode == "RGB" i = ImageCms.applyTransform(hopper(), t) + assert i is not None assert_image(i, "RGB", (128, 128)) # test PointTransform convenience API @@ -260,7 +267,7 @@ def test_simple_lab() -> None: t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") i_lab = ImageCms.applyTransform(i, t) - + assert i_lab is not None assert i_lab.mode == "LAB" k = i_lab.getpixel((0, 0)) @@ -284,6 +291,7 @@ def test_lab_color() -> None: # Need to add a type mapping for some PIL type to TYPE_Lab_8 in findLCMSType, and # have that mapping work back to a PIL mode (likely RGB). i = ImageCms.applyTransform(hopper(), t) + assert i is not None assert_image(i, "LAB", (128, 128)) # i.save('temp.lab.tif') # visually verified vs PS. @@ -298,6 +306,7 @@ def test_lab_srgb() -> None: with Image.open("Tests/images/hopper.Lab.tif") as img: img_srgb = ImageCms.applyTransform(img, t) + assert img_srgb is not None # img_srgb.save('temp.srgb.tif') # visually verified vs ps. @@ -317,11 +326,11 @@ def test_lab_roundtrip() -> None: t2 = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") i = ImageCms.applyTransform(hopper(), t) - + assert i is not None assert i.info["icc_profile"] == ImageCmsProfile(pLab).tobytes() out = ImageCms.applyTransform(i, t2) - + assert out is not None assert_image_similar(hopper(), out, 2) @@ -657,7 +666,7 @@ def test_auxiliary_channels_isolated() -> None: reference_image = ImageCms.applyTransform( source_image.convert(src_format[2]), reference_transform ) - + assert reference_image is not None assert_image_equal(test_image.convert(dst_format[2]), reference_image) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index ddcae80d6..c9dba2943 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -202,6 +202,8 @@ class TestImageFile: class MockPyDecoder(ImageFile.PyDecoder): + last: MockPyDecoder + def __init__(self, mode: str, *args: Any) -> None: MockPyDecoder.last = self @@ -213,6 +215,8 @@ class MockPyDecoder(ImageFile.PyDecoder): class MockPyEncoder(ImageFile.PyEncoder): + last: MockPyEncoder | None + def __init__(self, mode: str, *args: Any) -> None: MockPyEncoder.last = self @@ -315,6 +319,7 @@ class TestPyEncoder(CodecsTest): im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] ) + assert MockPyEncoder.last assert MockPyEncoder.last.state.xoff == xoff assert MockPyEncoder.last.state.yoff == yoff assert MockPyEncoder.last.state.xsize == xsize @@ -329,6 +334,7 @@ class TestPyEncoder(CodecsTest): fp = BytesIO() ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) + assert MockPyEncoder.last assert MockPyEncoder.last.state.xoff == 0 assert MockPyEncoder.last.state.yoff == 0 assert MockPyEncoder.last.state.xsize == 200 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 05b5d4716..4398f8a30 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -34,7 +34,9 @@ pytestmark = skip_unless_feature("freetype2") def test_sanity() -> None: - assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2")) + version = features.version_module("freetype2") + assert version is not None + assert re.search(r"\d+\.\d+\.\d+$", version) @pytest.fixture( @@ -547,11 +549,10 @@ def test_find_font( def loadable_font( filepath: str, size: int, index: int, encoding: str, *args: Any ): + _freeTypeFont = getattr(ImageFont, "_FreeTypeFont") if filepath == path_to_fake: - return ImageFont._FreeTypeFont( - FONT_PATH, size, index, encoding, *args - ) - return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args) + return _freeTypeFont(FONT_PATH, size, index, encoding, *args) + return _freeTypeFont(filepath, size, index, encoding, *args) m.setattr(ImageFont, "FreeTypeFont", loadable_font) font = ImageFont.truetype(fontname) @@ -630,7 +631,9 @@ def test_complex_font_settings() -> None: def test_variation_get(font: ImageFont.FreeTypeFont) -> None: - freetype = parse_version(features.version_module("freetype2")) + version = features.version_module("freetype2") + assert version is not None + freetype = parse_version(version) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): font.get_variation_names() @@ -700,7 +703,9 @@ def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: - freetype = parse_version(features.version_module("freetype2")) + version = features.version_module("freetype2") + assert version is not None + freetype = parse_version(version) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): font.set_variation_by_name("Bold") @@ -725,7 +730,9 @@ def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None: - freetype = parse_version(features.version_module("freetype2")) + version = features.version_module("freetype2") + assert version is not None + freetype = parse_version(version) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): font.set_variation_by_axes([100]) From e68cec640a0b0698fbfea9493089727edaf03379 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 May 2024 12:00:50 +1000 Subject: [PATCH 176/195] Added type hints --- Tests/oss-fuzz/test_fuzzers.py | 5 +++-- Tests/test_features.py | 10 +++++++--- Tests/test_file_jpeg.py | 4 +++- Tests/test_file_jpeg2k.py | 4 +++- Tests/test_file_libtiff.py | 4 +++- Tests/test_file_png.py | 6 +++--- Tests/test_file_webp.py | 4 +++- Tests/test_image_quantize.py | 5 +++-- Tests/test_image_reduce.py | 2 +- Tests/test_imageops_usm.py | 14 +++++++------- 10 files changed, 36 insertions(+), 22 deletions(-) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 58d0213e8..90eb8713a 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -12,8 +12,9 @@ from Tests.helper import skip_unless_feature if sys.platform.startswith("win32"): pytest.skip("Fuzzer is linux only", allow_module_level=True) -if features.check("libjpeg_turbo"): - version = packaging.version.parse(features.version("libjpeg_turbo")) +libjpeg_turbo_version = features.version("libjpeg_turbo") +if libjpeg_turbo_version is not None: + version = packaging.version.parse(libjpeg_turbo_version) if version.major == 2 and version.minor == 0: pytestmark = pytest.mark.valgrind_known_error( reason="Known failing with libjpeg_turbo 2.0" diff --git a/Tests/test_features.py b/Tests/test_features.py index 2d402ca91..59fb49809 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -30,7 +30,7 @@ def test_version() -> None: # Check the correctness of the convenience function # and the format of version numbers - def test(name: str, function: Callable[[str], bool]) -> None: + def test(name: str, function: Callable[[str], str | None]) -> None: version = features.version(name) if not features.check(name): assert version is None @@ -67,12 +67,16 @@ def test_webp_anim() -> None: @skip_unless_feature("libjpeg_turbo") def test_libjpeg_turbo_version() -> None: - assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo")) + version = features.version("libjpeg_turbo") + assert version is not None + assert re.search(r"\d+\.\d+\.\d+$", version) @skip_unless_feature("libimagequant") def test_libimagequant_version() -> None: - assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant")) + version = features.version("libimagequant") + assert version is not None + assert re.search(r"\d+\.\d+\.\d+$", version) @pytest.mark.parametrize("feature", features.modules) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 5d2157651..f24faecaa 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -70,7 +70,9 @@ class TestFileJpeg: def test_sanity(self) -> None: # internal version number - assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) + version = features.version_codec("jpg") + assert version is not None + assert re.search(r"\d+\.\d+$", version) with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index a7cae563a..5a208739f 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -48,7 +48,9 @@ def roundtrip(im: Image.Image, **options: Any) -> Image.Image: def test_sanity() -> None: # Internal version number - assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000")) + version = features.version_codec("jpg_2000") + assert version is not None + assert re.search(r"\d+\.\d+\.\d+$", version) with Image.open("Tests/images/test-card-lossless.jp2") as im: px = im.load() diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 11883ad24..6c13999a5 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -52,7 +52,9 @@ class LibTiffTestCase: class TestFileLibTiff(LibTiffTestCase): def test_version(self) -> None: - assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff")) + version = features.version_codec("libtiff") + assert version is not None + assert re.search(r"\d+\.\d+\.\d+$", version) def test_g4_tiff(self, tmp_path: Path) -> None: """Test the ordinary file path load path""" diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 19462dcb5..c7c9f6fab 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -85,9 +85,9 @@ class TestFilePng: def test_sanity(self, tmp_path: Path) -> None: # internal version number - assert re.search( - r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", features.version_codec("zlib") - ) + version = features.version_codec("zlib") + assert version is not None + assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version) test_file = str(tmp_path / "temp.png") diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 249846da4..e2de84c71 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -49,7 +49,9 @@ class TestFileWebp: def test_version(self) -> None: _webp.WebPDecoderVersion() _webp.WebPDecoderBuggyAlpha() - assert re.search(r"\d+\.\d+\.\d+$", features.version_module("webp")) + version = features.version_module("webp") + assert version is not None + assert re.search(r"\d+\.\d+\.\d+$", version) def test_read_rgb(self) -> None: """ diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index e1aa6252b..2daaf5c3c 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -24,8 +24,9 @@ def test_sanity() -> None: def test_libimagequant_quantize() -> None: image = hopper() if is_ppc64le(): - libimagequant = parse_version(features.version_feature("libimagequant")) - if libimagequant < parse_version("4"): + version = features.version_feature("libimagequant") + assert version is not None + if parse_version(version) < parse_version("4"): pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) assert converted.mode == "P" diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index fcf671daa..f6609a1a0 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -102,7 +102,7 @@ def test_unsupported_modes(mode: str) -> None: def get_image(mode: str) -> Image.Image: mode_info = ImageMode.getmode(mode) if mode_info.basetype == "L": - bands = [gradients_image] + bands: list[Image.Image] = [gradients_image] for _ in mode_info.bands[1:]: # rotate previous image band = bands[-1].transpose(Image.Transpose.ROTATE_90) diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index c15907a55..104c620de 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -4,11 +4,11 @@ from typing import Generator import pytest -from PIL import Image, ImageFilter +from PIL import Image, ImageFile, ImageFilter @pytest.fixture -def test_images() -> Generator[dict[str, Image.Image], None, None]: +def test_images() -> Generator[dict[str, ImageFile.ImageFile], None, None]: ims = { "im": Image.open("Tests/images/hopper.ppm"), "snakes": Image.open("Tests/images/color_snakes.png"), @@ -20,7 +20,7 @@ def test_images() -> Generator[dict[str, Image.Image], None, None]: im.close() -def test_filter_api(test_images: dict[str, Image.Image]) -> None: +def test_filter_api(test_images: dict[str, ImageFile.ImageFile]) -> None: im = test_images["im"] test_filter = ImageFilter.GaussianBlur(2.0) @@ -34,7 +34,7 @@ def test_filter_api(test_images: dict[str, Image.Image]) -> None: assert i.size == (128, 128) -def test_usm_formats(test_images: dict[str, Image.Image]) -> None: +def test_usm_formats(test_images: dict[str, ImageFile.ImageFile]) -> None: im = test_images["im"] usm = ImageFilter.UnsharpMask @@ -52,7 +52,7 @@ def test_usm_formats(test_images: dict[str, Image.Image]) -> None: im.convert("YCbCr").filter(usm) -def test_blur_formats(test_images: dict[str, Image.Image]) -> None: +def test_blur_formats(test_images: dict[str, ImageFile.ImageFile]) -> None: im = test_images["im"] blur = ImageFilter.GaussianBlur @@ -70,7 +70,7 @@ def test_blur_formats(test_images: dict[str, Image.Image]) -> None: im.convert("YCbCr").filter(blur) -def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None: +def test_usm_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None: snakes = test_images["snakes"] src = snakes.convert("RGB") @@ -79,7 +79,7 @@ def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None: assert i.tobytes() == src.tobytes() -def test_blur_accuracy(test_images: dict[str, Image.Image]) -> None: +def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None: snakes = test_images["snakes"] i = snakes.filter(ImageFilter.GaussianBlur(0.4)) From afc7d8d0b012b8be86e00411c5cb69de62478ee5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 May 2024 17:17:22 +1000 Subject: [PATCH 177/195] Added type hints --- Tests/test_font_leaks.py | 2 +- Tests/test_image.py | 20 +++++++++++++++----- Tests/test_imagecms.py | 26 ++++++++++++++------------ Tests/test_imagestat.py | 4 ++-- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 08a0e7431..3fb92a62e 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -12,7 +12,7 @@ class TestTTypeFontLeak(PillowLeakTestCase): iterations = 10 mem_limit = 4096 # k - def _test_font(self, font: ImageFont.FreeTypeFont) -> None: + def _test_font(self, font: ImageFont.FreeTypeFont | ImageFont.ImageFont) -> None: im = Image.new("RGB", (255, 255), "white") draw = ImageDraw.ImageDraw(im) self._test_leak( diff --git a/Tests/test_image.py b/Tests/test_image.py index 742d0dfe4..c7694a0ef 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -99,10 +99,18 @@ class TestImage: JPGFILE = "Tests/images/hopper.jpg" with pytest.raises(TypeError): - with Image.open(PNGFILE, formats=123): + with Image.open(PNGFILE, formats=123): # type: ignore[arg-type] pass - for formats in [["JPEG"], ("JPEG",), ["jpeg"], ["Jpeg"], ["jPeG"], ["JpEg"]]: + format_list: list[list[str] | tuple[str, ...]] = [ + ["JPEG"], + ("JPEG",), + ["jpeg"], + ["Jpeg"], + ["jPeG"], + ["JpEg"], + ] + for formats in format_list: with pytest.raises(UnidentifiedImageError): with Image.open(PNGFILE, formats=formats): pass @@ -138,7 +146,7 @@ class TestImage: def test_bad_mode(self) -> None: with pytest.raises(ValueError): - with Image.open("filename", "bad mode"): + with Image.open("filename", "bad mode"): # type: ignore[arg-type] pass def test_stringio(self) -> None: @@ -497,9 +505,11 @@ class TestImage: def test_check_size(self) -> None: # Checking that the _check_size function throws value errors when we want it to with pytest.raises(ValueError): - Image.new("RGB", 0) # not a tuple + # not a tuple + Image.new("RGB", 0) # type: ignore[arg-type] with pytest.raises(ValueError): - Image.new("RGB", (0,)) # Tuple too short + # tuple too short + Image.new("RGB", (0,)) # type: ignore[arg-type] with pytest.raises(ValueError): Image.new("RGB", (-1, -1)) # w,h < 0 diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 8d2029c21..082eb8162 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -7,7 +7,7 @@ import shutil import sys from io import BytesIO from pathlib import Path -from typing import Any +from typing import Any, Literal, cast import pytest @@ -209,13 +209,13 @@ def test_exceptions() -> None: ImageCms.buildTransform("foo", "bar", "RGB", "RGB") with pytest.raises(ImageCms.PyCMSError, match="Invalid type for Profile"): - ImageCms.getProfileName(None) + ImageCms.getProfileName(None) # type: ignore[arg-type] skip_missing() # Python <= 3.9: "an integer is required (got type NoneType)" # Python > 3.9: "'NoneType' object cannot be interpreted as an integer" with pytest.raises(ImageCms.PyCMSError, match="integer"): - ImageCms.isIntentSupported(SRGB, None, None) + ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type] def test_display_profile() -> None: @@ -239,7 +239,7 @@ def test_unsupported_color_space() -> None: "Color space not supported for on-the-fly profile creation (unsupported)" ), ): - ImageCms.createProfile("unsupported") + ImageCms.createProfile("unsupported") # type: ignore[arg-type] def test_invalid_color_temperature() -> None: @@ -352,7 +352,7 @@ def test_extended_information() -> None: p = o.profile def assert_truncated_tuple_equal( - tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10 + tup1: tuple[Any, ...] | None, tup2: tuple[Any, ...], digits: int = 10 ) -> None: # Helper function to reduce precision of tuples of floats # recursively and then check equality. @@ -368,6 +368,7 @@ def test_extended_information() -> None: for val in tuple_value ) + assert tup1 is not None assert truncate_tuple(tup1) == truncate_tuple(tup2) assert p.attributes == 4294967296 @@ -513,22 +514,22 @@ def test_non_ascii_path(tmp_path: Path) -> None: def test_profile_typesafety() -> None: # does not segfault with pytest.raises(TypeError, match="Invalid type for Profile"): - ImageCms.ImageCmsProfile(0).tobytes() + ImageCms.ImageCmsProfile(0) # type: ignore[arg-type] with pytest.raises(TypeError, match="Invalid type for Profile"): - ImageCms.ImageCmsProfile(1).tobytes() + ImageCms.ImageCmsProfile(1) # type: ignore[arg-type] # also check core function with pytest.raises(TypeError): - ImageCms.core.profile_tobytes(0) + ImageCms.core.profile_tobytes(0) # type: ignore[arg-type] with pytest.raises(TypeError): - ImageCms.core.profile_tobytes(1) + ImageCms.core.profile_tobytes(1) # type: ignore[arg-type] if not is_pypy(): # core profile should not be directly instantiable with pytest.raises(TypeError): ImageCms.core.CmsProfile() with pytest.raises(TypeError): - ImageCms.core.CmsProfile(0) + ImageCms.core.CmsProfile(0) # type: ignore[call-arg] @pytest.mark.skipif(is_pypy(), reason="fails on PyPy") @@ -537,7 +538,7 @@ def test_transform_typesafety() -> None: with pytest.raises(TypeError): ImageCms.core.CmsTransform() with pytest.raises(TypeError): - ImageCms.core.CmsTransform(0) + ImageCms.core.CmsTransform(0) # type: ignore[call-arg] def assert_aux_channel_preserved( @@ -637,7 +638,8 @@ def test_auxiliary_channels_isolated() -> None: continue # convert with and without AUX data, test colors are equal - source_profile = ImageCms.createProfile(src_format[1]) + src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1]) + source_profile = ImageCms.createProfile(src_colorSpace) destination_profile = ImageCms.createProfile(dst_format[1]) source_image = src_format[3] test_transform = ImageCms.buildTransform( diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index b1c1306c1..0dfbc5a2a 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -25,10 +25,10 @@ def test_sanity() -> None: st.stddev with pytest.raises(AttributeError): - st.spam() + st.spam() # type: ignore[attr-defined] with pytest.raises(TypeError): - ImageStat.Stat(1) + ImageStat.Stat(1) # type: ignore[arg-type] def test_hopper() -> None: From 66ab7e0de22251b32eccd8938ac7f985579dc0ac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Jun 2024 21:31:53 +1000 Subject: [PATCH 178/195] Added type hints --- Tests/test_file_gif.py | 5 +++-- Tests/test_file_jpeg.py | 4 ++-- Tests/test_file_libtiff.py | 3 ++- Tests/test_image.py | 4 +++- Tests/test_image_array.py | 2 +- Tests/test_image_crop.py | 26 +++++++++++++------------- Tests/test_image_filter.py | 4 ++-- Tests/test_image_getextrema.py | 2 +- Tests/test_imagecms.py | 8 ++++++-- Tests/test_imagedraw.py | 2 +- 10 files changed, 34 insertions(+), 26 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 48c70db8a..4e790926b 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1252,10 +1252,11 @@ def test_palette_save_L(tmp_path: Path) -> None: im = hopper("P") im_l = Image.frombytes("L", im.size, im.tobytes()) - palette = bytes(im.getpalette()) + palette = im.getpalette() + assert palette is not None out = str(tmp_path / "temp.gif") - im_l.save(out, palette=palette) + im_l.save(out, palette=bytes(palette)) with Image.open(out) as reloaded: assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index f24faecaa..33f9ce00e 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -154,7 +154,7 @@ class TestFileJpeg: assert k > 0.9 def test_rgb(self) -> None: - def getchannels(im: Image.Image) -> tuple[int, int, int]: + def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]: return tuple(v[0] for v in im.layer) im = hopper() @@ -443,7 +443,7 @@ class TestFileJpeg: assert_image(im1, im2.mode, im2.size) def test_subsampling(self) -> None: - def getsampling(im: Image.Image): + def getsampling(im: JpegImagePlugin.JpegImageFile): layer = im.layer return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 6c13999a5..22bcd2856 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -668,7 +668,8 @@ class TestFileLibTiff(LibTiffTestCase): pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) - assert_image_similar_tofile(pilim, buffer_io, 0) + with Image.open(buffer_io) as saved_im: + assert_image_similar(pilim, saved_im, 0) save_bytesio() save_bytesio("raw") diff --git a/Tests/test_image.py b/Tests/test_image.py index c7694a0ef..d6a739c79 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -25,6 +25,7 @@ from PIL import ( from .helper import ( assert_image_equal, assert_image_equal_tofile, + assert_image_similar, assert_image_similar_tofile, assert_not_all_same, hopper, @@ -193,7 +194,8 @@ class TestImage: with tempfile.TemporaryFile() as fp: im.save(fp, "JPEG") fp.seek(0) - assert_image_similar_tofile(im, fp, 20) + with Image.open(fp) as reloaded: + assert_image_similar(im, reloaded, 20) def test_unknown_extension(self, tmp_path: Path) -> None: im = hopper() diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 342bd8654..d7e6c562c 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -86,8 +86,8 @@ def test_fromarray() -> None: assert test("RGBX") == ("RGBA", (128, 100), True) # Test mode is None with no "typestr" in the array interface + wrapped = Wrapper(hopper("L"), {"shape": (100, 128)}) with pytest.raises(TypeError): - wrapped = Wrapper(test("L"), {"shape": (100, 128)}) Image.fromarray(wrapped) diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index d095364ba..07fec2e64 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -18,7 +18,7 @@ def test_crop(mode: str) -> None: def test_wide_crop() -> None: - def crop(*bbox: int) -> tuple[int, ...]: + def crop(bbox: tuple[int, int, int, int]) -> tuple[int, ...]: i = im.crop(bbox) h = i.histogram() while h and not h[-1]: @@ -27,23 +27,23 @@ def test_wide_crop() -> None: im = Image.new("L", (100, 100), 1) - assert crop(0, 0, 100, 100) == (0, 10000) - assert crop(25, 25, 75, 75) == (0, 2500) + assert crop((0, 0, 100, 100)) == (0, 10000) + assert crop((25, 25, 75, 75)) == (0, 2500) # sides - assert crop(-25, 0, 25, 50) == (1250, 1250) - assert crop(0, -25, 50, 25) == (1250, 1250) - assert crop(75, 0, 125, 50) == (1250, 1250) - assert crop(0, 75, 50, 125) == (1250, 1250) + assert crop((-25, 0, 25, 50)) == (1250, 1250) + assert crop((0, -25, 50, 25)) == (1250, 1250) + assert crop((75, 0, 125, 50)) == (1250, 1250) + assert crop((0, 75, 50, 125)) == (1250, 1250) - assert crop(-25, 25, 125, 75) == (2500, 5000) - assert crop(25, -25, 75, 125) == (2500, 5000) + assert crop((-25, 25, 125, 75)) == (2500, 5000) + assert crop((25, -25, 75, 125)) == (2500, 5000) # corners - assert crop(-25, -25, 25, 25) == (1875, 625) - assert crop(75, -25, 125, 25) == (1875, 625) - assert crop(75, 75, 125, 125) == (1875, 625) - assert crop(-25, 75, 25, 125) == (1875, 625) + assert crop((-25, -25, 25, 25)) == (1875, 625) + assert crop((75, -25, 125, 25)) == (1875, 625) + assert crop((75, 75, 125, 125)) == (1875, 625) + assert crop((-25, 75, 25, 125)) == (1875, 625) @pytest.mark.parametrize("box", ((8, 2, 2, 8), (2, 8, 8, 2), (8, 8, 2, 2))) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 47f9ffa3d..1f0644471 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -46,9 +46,9 @@ def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None: @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) def test_sanity_error(mode: str) -> None: + im = hopper(mode) with pytest.raises(TypeError): - im = hopper(mode) - im.filter("hello") + im.filter("hello") # type: ignore[arg-type] # crashes on small images diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index a5b974459..de5956f3e 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -6,7 +6,7 @@ from .helper import hopper def test_extrema() -> None: - def extrema(mode: str) -> tuple[int, int] | tuple[tuple[int, int], ...]: + def extrema(mode: str) -> tuple[float, float] | tuple[tuple[int, int], ...]: return hopper(mode).getextrema() assert extrema("1") == (0, 255) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 082eb8162..55f72c3b9 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -247,7 +247,7 @@ def test_invalid_color_temperature() -> None: ImageCms.PyCMSError, match='Color temperature must be numeric, "invalid" not valid', ): - ImageCms.createProfile("LAB", "invalid") + ImageCms.createProfile("LAB", "invalid") # type: ignore[arg-type] @pytest.mark.parametrize("flag", ("my string", -1)) @@ -256,7 +256,7 @@ def test_invalid_flag(flag: str | int) -> None: with pytest.raises( ImageCms.PyCMSError, match="flags must be an integer between 0 and " ): - ImageCms.profileToProfile(im, "foo", "bar", flags=flag) + ImageCms.profileToProfile(im, "foo", "bar", flags=flag) # type: ignore[arg-type] def test_simple_lab() -> None: @@ -588,11 +588,13 @@ def assert_aux_channel_preserved( ) # apply transform + result_image: Image.Image | None if transform_in_place: ImageCms.applyTransform(source_image, t, inPlace=True) result_image = source_image else: result_image = ImageCms.applyTransform(source_image, t, inPlace=False) + assert result_image is not None result_image_aux = result_image.getchannel(preserved_channel) assert_image_equal(source_image_aux, result_image_aux) @@ -650,6 +652,7 @@ def test_auxiliary_channels_isolated() -> None: ) # test conversion from aux-ful source + test_image: Image.Image | None if transform_in_place: test_image = source_image.copy() ImageCms.applyTransform(test_image, test_transform, inPlace=True) @@ -657,6 +660,7 @@ def test_auxiliary_channels_isolated() -> None: test_image = ImageCms.applyTransform( source_image, test_transform, inPlace=False ) + assert test_image is not None # reference conversion from aux-less source reference_transform = ImageCms.buildTransform( diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 69d09e03d..c221fe008 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1083,8 +1083,8 @@ def test_line_horizontal() -> None: ) +@pytest.mark.xfail(reason="failing test") def test_line_h_s1_w2() -> None: - pytest.skip("failing") img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 6), BLACK, 2) assert_image_equal_tofile( From 54150f2061fa87cb5f629f28958d53c293ea4b90 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 2 Jun 2024 16:26:35 +1000 Subject: [PATCH 179/195] Corrected docstring --- src/PIL/ImageCms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 5f5c5df54..5915cc944 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -777,7 +777,7 @@ def createProfile( :param colorSpace: String, the color space of the profile you wish to create. Currently only "LAB", "XYZ", and "sRGB" are supported. - :param colorTemp: Positive integer for the white point for the profile, in + :param colorTemp: Positive number for the white point for the profile, in degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50 illuminant if omitted (5000k). colorTemp is ONLY applied to LAB profiles, and is ignored for XYZ and sRGB. From 4aba0b8238db501b5f498fee9335f380a7d08f88 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 2 Jun 2024 16:27:05 +1000 Subject: [PATCH 180/195] Changed default colorTemp --- src/PIL/ImageCms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 5915cc944..ec9471f84 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -754,7 +754,7 @@ def applyTransform( def createProfile( - colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = -1 + colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = 0 ) -> core.CmsProfile: """ (pyCMS) Creates a profile. From 8dae9b618f39b8afd9ae2177dcce6a02ea227006 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 2 Jun 2024 17:53:52 +1000 Subject: [PATCH 181/195] Corrected type hint --- src/PIL/ImageCms.py | 2 +- src/PIL/_imagingcms.pyi | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index ec9471f84..19a79facc 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -1089,7 +1089,7 @@ def isIntentSupported( raise PyCMSError(v) from v -def versions() -> tuple[str, str, str, str]: +def versions() -> tuple[str, str | None, str, str]: """ (pyCMS) Fetches versions. """ diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi index f704047be..2abd6d0f7 100644 --- a/src/PIL/_imagingcms.pyi +++ b/src/PIL/_imagingcms.pyi @@ -2,7 +2,7 @@ import datetime import sys from typing import Literal, SupportsFloat, TypedDict -littlecms_version: str +littlecms_version: str | None _Tuple3f = tuple[float, float, float] _Tuple2x3f = tuple[_Tuple3f, _Tuple3f] From d566c04d5b9b2f7587015b110e588b073a24cf2d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Jun 2024 14:20:01 +1000 Subject: [PATCH 182/195] Updated type hints --- src/PIL/Image.py | 22 +++++++--------- src/PIL/ImageDraw.py | 29 +++++++++++++++------ src/PIL/ImageFont.py | 53 +++++++++++++++++++------------------- src/PIL/JpegImagePlugin.py | 2 +- src/PIL/_imaging.pyi | 4 +-- src/PIL/_imagingft.pyi | 23 ++++++++++++----- 6 files changed, 75 insertions(+), 58 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c02c7d6b6..2ea26877d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -506,7 +506,7 @@ def _getscaleoffset(expr): class SupportsGetData(Protocol): def getdata( self, - ) -> tuple[Transform, Sequence[Any]]: ... + ) -> tuple[Transform, Sequence[int]]: ... class Image: @@ -1295,7 +1295,7 @@ class Image: return im.crop((x0, y0, x1, y1)) def draft( - self, mode: str, size: tuple[int, int] + self, mode: str | None, size: tuple[int, int] ) -> tuple[str, tuple[int, int, float, float]] | None: """ Configures the image file loader so it returns a version of the @@ -1719,7 +1719,7 @@ class Image: def paste( self, - im: Image | str | int | tuple[int, ...], + im: Image | str | float | tuple[int, ...], box: tuple[int, int, int, int] | tuple[int, int] | None = None, mask: Image | None = None, ) -> None: @@ -1750,7 +1750,7 @@ class Image: See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to combine images with respect to their alpha channels. - :param im: Source image or pixel value (integer or tuple). + :param im: Source image or pixel value (integer, float or tuple). :param box: An optional 4-tuple giving the region to paste into. If a 2-tuple is used instead, it's treated as the upper left corner. If omitted or None, the source is pasted into the @@ -2228,13 +2228,9 @@ class Image: msg = "reducing_gap must be 1.0 or greater" raise ValueError(msg) - size = cast("tuple[int, int]", tuple(size)) - self.load() if box is None: box = (0, 0) + self.size - else: - box = cast("tuple[float, float, float, float]", tuple(box)) if self.size == size and box == (0, 0) + self.size: return self.copy() @@ -2291,8 +2287,6 @@ class Image: if box is None: box = (0, 0) + self.size - else: - box = cast("tuple[int, int, int, int]", tuple(box)) if factor == (1, 1) and box == (0, 0) + self.size: return self.copy() @@ -2692,7 +2686,9 @@ class Image: return size = preserved_size - res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) # type: ignore[arg-type] + res = self.draft( + None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap)) + ) if res is not None: box = res[1] if box is None: @@ -2799,7 +2795,7 @@ class Image: im.info = self.info.copy() if method == Transform.MESH: # list of quads - for box, quad in cast("Sequence[tuple[float, float]]", data): + for box, quad in data: im.__transformer( box, self, Transform.QUAD, quad, resample, fillcolor is None ) @@ -2957,7 +2953,7 @@ class ImageTransformHandler: self, size: tuple[int, int], image: Image, - **options: dict[str, str | int | tuple[int, ...] | list[int]] | int, + **options: str | int | tuple[int, ...] | list[int], ) -> Image: pass diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 1887a3933..0663d9ddf 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -456,14 +456,12 @@ class ImageDraw: self.draw.draw_rectangle(right, ink, 1) def _multiline_check(self, text: AnyStr) -> bool: - split_character = cast(AnyStr, "\n" if isinstance(text, str) else b"\n") + split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text def _multiline_split(self, text: AnyStr) -> list[AnyStr]: - split_character = cast(AnyStr, "\n" if isinstance(text, str) else b"\n") - - return text.split(split_character) + return text.split("\n" if isinstance(text, str) else b"\n") def _multiline_spacing(self, font, spacing, stroke_width): return ( @@ -477,7 +475,12 @@ class ImageDraw: xy: tuple[float, float], text: str, fill=None, - font: ImageFont.FreeTypeFont | ImageFont.ImageFont | None = None, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, anchor=None, spacing=4, align="left", @@ -597,9 +600,14 @@ class ImageDraw: def multiline_text( self, xy: tuple[float, float], - text, + text: str, fill=None, - font=None, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, anchor=None, spacing=4, align="left", @@ -684,7 +692,12 @@ class ImageDraw: def textlength( self, text: str, - font: ImageFont.FreeTypeFont | ImageFont.ImageFont | None = None, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, direction=None, features=None, language=None, diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 747c0c050..a9925483e 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -33,11 +33,11 @@ import sys import warnings from enum import IntEnum from io import BytesIO -from typing import TYPE_CHECKING, BinaryIO +from typing import IO, TYPE_CHECKING, Any, BinaryIO from . import Image from ._typing import StrOrBytesPath -from ._util import is_directory, is_path +from ._util import is_path if TYPE_CHECKING: from . import ImageFile @@ -61,7 +61,7 @@ except ImportError as ex: core = DeferredError.new(ex) -def _string_length_check(text: str) -> None: +def _string_length_check(text: str | bytes | bytearray) -> None: if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: msg = "too many characters in string" raise ValueError(msg) @@ -113,7 +113,7 @@ class ImageFont: self._load_pilfont_data(fp, image) image.close() - def _load_pilfont_data(self, file, image): + def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: # read PILfont header if file.readline() != b"PILfont\n": msg = "Not a PILfont file" @@ -161,7 +161,7 @@ class ImageFont: return self.font.getmask(text, mode) def getbbox( - self, text: str, *args: object, **kwargs: object + self, text: str | bytes | bytearray, *args: Any, **kwargs: Any ) -> tuple[int, int, int, int]: """ Returns bounding box (in pixels) of given text. @@ -180,7 +180,9 @@ class ImageFont: width, height = self.font.getsize(text) return 0, 0, width, height - def getlength(self, text: str, *args: object, **kwargs: object) -> int: + def getlength( + self, text: str | bytes | bytearray, *args: Any, **kwargs: Any + ) -> int: """ Returns length (in pixels) of given text. This is the amount by which following text should be offset. @@ -357,13 +359,13 @@ class FreeTypeFont: def getbbox( self, text: str, - mode="", - direction=None, - features=None, - language=None, - stroke_width=0, - anchor=None, - ) -> tuple[int, int, int, int]: + mode: str = "", + direction: str | None = None, + features: str | None = None, + language: str | None = None, + stroke_width: float = 0, + anchor: str | None = None, + ) -> tuple[float, float, float, float]: """ Returns bounding box (in pixels) of given text relative to given anchor when rendered in font with provided direction, features, and language. @@ -513,7 +515,7 @@ class FreeTypeFont: def getmask2( self, - text, + text: str, mode="", direction=None, features=None, @@ -641,7 +643,7 @@ class FreeTypeFont: layout_engine=layout_engine or self.layout_engine, ) - def get_variation_names(self): + def get_variation_names(self) -> list[bytes]: """ :returns: A list of the named styles in a variation font. :exception OSError: If the font is not a variation font. @@ -683,10 +685,11 @@ class FreeTypeFont: msg = "FreeType 2.9.1 or greater is required" raise NotImplementedError(msg) from e for axis in axes: - axis["name"] = axis["name"].replace(b"\x00", b"") + if axis["name"]: + axis["name"] = axis["name"].replace(b"\x00", b"") return axes - def set_variation_by_axes(self, axes): + def set_variation_by_axes(self, axes: list[float]) -> None: """ :param axes: A list of values for each axis. :exception OSError: If the font is not a variation font. @@ -731,7 +734,7 @@ class TransposedFont: return 0, 0, height, width return 0, 0, width, height - def getlength(self, text, *args, **kwargs): + def getlength(self, text: str, *args, **kwargs) -> float: if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): msg = "text length is undefined for text rotated by 90 or 270 degrees" raise ValueError(msg) @@ -878,15 +881,13 @@ def load_path(filename: str | bytes) -> ImageFont: :return: A font object. :exception OSError: If the file could not be read. """ + if not isinstance(filename, str): + filename = filename.decode("utf-8") for directory in sys.path: - if is_directory(directory): - assert isinstance(directory, str) - if not isinstance(filename, str): - filename = filename.decode("utf-8") - try: - return load(os.path.join(directory, filename)) - except OSError: - pass + try: + return load(os.path.join(directory, filename)) + except OSError: + pass msg = "cannot find font file" raise OSError(msg) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 909911dfe..e1c61f991 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -425,7 +425,7 @@ class JpegImageFile(ImageFile.ImageFile): return s def draft( - self, mode: str, size: tuple[int, int] + self, mode: str | None, size: tuple[int, int] ) -> tuple[str, tuple[int, int, float, float]] | None: if len(self.tile) != 1: return None diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi index d85eb84fa..1fe954417 100644 --- a/src/PIL/_imaging.pyi +++ b/src/PIL/_imaging.pyi @@ -1,7 +1,5 @@ from typing import Any -from typing_extensions import Buffer - class ImagingCore: def __getattr__(self, name: str) -> Any: ... @@ -14,5 +12,5 @@ class ImagingDraw: class PixelAccess: def __getattr__(self, name: str) -> Any: ... -def font(image, glyphdata: Buffer) -> ImagingFont: ... +def font(image, glyphdata: bytes) -> ImagingFont: ... def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 987e7fd6f..b023efe01 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -1,5 +1,7 @@ from typing import Any, TypedDict +from . import _imaging + class _Axis(TypedDict): minimum: int | None default: int | None @@ -37,21 +39,28 @@ class Font: x_start=..., y_start=..., /, - ) -> tuple[Any, tuple[int, int]]: ... + ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... def getsize( - self, string: str, mode=..., dir=..., features=..., lang=..., anchor=..., / + self, + string: str | bytes | bytearray, + mode=..., + dir=..., + features=..., + lang=..., + anchor=..., + /, ) -> tuple[tuple[int, int], tuple[int, int]]: ... def getlength( self, string: str, mode=..., dir=..., features=..., lang=..., / - ) -> int: ... - def getvarnames(self) -> list[str]: ... - def getvaraxes(self) -> list[_Axis]: ... + ) -> float: ... + def getvarnames(self) -> list[bytes]: ... + def getvaraxes(self) -> list[_Axis] | None: ... def setvarname(self, instance_index: int, /) -> None: ... def setvaraxes(self, axes: list[float], /) -> None: ... def getfont( - filename: str | bytes | bytearray, - size, + filename: str | bytes, + size: float, index=..., encoding=..., font_bytes=..., From f5da04adb07f19bd79aededb2be7291d445e4f96 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Jun 2024 21:58:02 +1000 Subject: [PATCH 183/195] Added type hints Co-authored-by: Nulano --- src/PIL/Image.py | 10 ++++------ src/PIL/ImageDraw.py | 8 +++++++- src/PIL/PyAccess.py | 41 ++++++++++++++++++++++++++--------------- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 958b95e3b..dd6984020 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1511,7 +1511,7 @@ class Image: self._exif._loaded = False self.getexif() - def get_child_images(self): + def get_child_images(self) -> list[ImageFile.ImageFile]: child_images = [] exif = self.getexif() ifds = [] @@ -1535,10 +1535,7 @@ class Image: fp = self.fp thumbnail_offset = ifd.get(513) if thumbnail_offset is not None: - try: - thumbnail_offset += self._exif_offset - except AttributeError: - pass + thumbnail_offset += getattr(self, "_exif_offset", 0) self.fp.seek(thumbnail_offset) data = self.fp.read(ifd.get(514)) fp = io.BytesIO(data) @@ -1604,7 +1601,7 @@ class Image: or "transparency" in self.info ) - def apply_transparency(self): + def apply_transparency(self) -> None: """ If a P mode image has a "transparency" key in the info dictionary, remove the key and instead apply the transparency to the palette. @@ -1616,6 +1613,7 @@ class Image: from . import ImagePalette palette = self.getpalette("RGBA") + assert palette is not None transparency = self.info["transparency"] if isinstance(transparency, bytes): for i, alpha in enumerate(transparency): diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 17c176430..8fe179dd5 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -908,7 +908,13 @@ def getdraw(im=None, hints=None): return im, handler -def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None: +def floodfill( + image: Image.Image, + xy: tuple[int, int], + value: float | tuple[int, ...], + border: float | tuple[int, ...] | None = None, + thresh: float = 0, +) -> None: """ (experimental) Fills a bounded region with a given color. diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index a9da90613..fe12cb641 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -22,6 +22,7 @@ from __future__ import annotations import logging import sys +from typing import TYPE_CHECKING from ._deprecate import deprecate @@ -48,9 +49,12 @@ except ImportError as ex: logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from . import Image + class PyAccess: - def __init__(self, img, readonly=False): + def __init__(self, img: Image.Image, readonly: bool = False) -> None: deprecate("PyAccess", 11) vals = dict(img.im.unsafe_ptrs) self.readonly = readonly @@ -77,7 +81,8 @@ class PyAccess: """ Modifies the pixel at x,y. The color is given as a single numerical value for single band images, and a tuple for - multi-band images + multi-band images. In addition to this, RGB and RGBA tuples + are accepted for P and PA images. :param xy: The pixel coordinate, given as (x, y). See :ref:`coordinate-system`. @@ -108,7 +113,7 @@ class PyAccess: return self.set_pixel(x, y, color) - def __getitem__(self, xy): + def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]: """ Returns the pixel at x,y. The pixel is returned as a single value for single band images or a tuple for multiple band @@ -130,13 +135,19 @@ class PyAccess: putpixel = __setitem__ getpixel = __getitem__ - def check_xy(self, xy): + def check_xy(self, xy: tuple[int, int]) -> tuple[int, int]: (x, y) = xy if not (0 <= x < self.xsize and 0 <= y < self.ysize): msg = "pixel location out of range" raise ValueError(msg) return xy + def get_pixel(self, x: int, y: int) -> float | tuple[int, ...]: + raise NotImplementedError() + + def set_pixel(self, x: int, y: int, color: float | tuple[int, ...]) -> None: + raise NotImplementedError() + class _PyAccess32_2(PyAccess): """PA, LA, stored in first and last bytes of a 32 bit word""" @@ -144,7 +155,7 @@ class _PyAccess32_2(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> tuple[int, int]: pixel = self.pixels[y][x] return pixel.r, pixel.a @@ -161,7 +172,7 @@ class _PyAccess32_3(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> tuple[int, int, int]: pixel = self.pixels[y][x] return pixel.r, pixel.g, pixel.b @@ -180,7 +191,7 @@ class _PyAccess32_4(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> tuple[int, int, int, int]: pixel = self.pixels[y][x] return pixel.r, pixel.g, pixel.b, pixel.a @@ -199,7 +210,7 @@ class _PyAccess8(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = self.image8 - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> int: return self.pixels[y][x] def set_pixel(self, x, y, color): @@ -217,7 +228,7 @@ class _PyAccessI16_N(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("unsigned short **", self.image) - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> int: return self.pixels[y][x] def set_pixel(self, x, y, color): @@ -235,7 +246,7 @@ class _PyAccessI16_L(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_I16 **", self.image) - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> int: pixel = self.pixels[y][x] return pixel.l + pixel.r * 256 @@ -256,7 +267,7 @@ class _PyAccessI16_B(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_I16 **", self.image) - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> int: pixel = self.pixels[y][x] return pixel.l * 256 + pixel.r @@ -277,7 +288,7 @@ class _PyAccessI32_N(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = self.image32 - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> int: return self.pixels[y][x] def set_pixel(self, x, y, color): @@ -296,7 +307,7 @@ class _PyAccessI32_Swap(PyAccess): chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0] return ffi.cast("int *", chars)[0] - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> int: return self.reverse(self.pixels[y][x]) def set_pixel(self, x, y, color): @@ -309,7 +320,7 @@ class _PyAccessF(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("float **", self.image32) - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> float: return self.pixels[y][x] def set_pixel(self, x, y, color): @@ -357,7 +368,7 @@ else: mode_map["I;32B"] = _PyAccessI32_N -def new(img, readonly=False): +def new(img: Image.Image, readonly: bool = False) -> PyAccess | None: access_type = mode_map.get(img.mode, None) if not access_type: logger.debug("PyAccess Not Implemented: %s", img.mode) From 322814d7ce8d48bcbf45ac912aecef445f6743b2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 17:24:10 +0000 Subject: [PATCH 184/195] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.4.3 → v0.4.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.3...v0.4.7) - [github.com/pre-commit/mirrors-clang-format: v18.1.4 → v18.1.5](https://github.com/pre-commit/mirrors-clang-format/compare/v18.1.4...v18.1.5) - [github.com/python-jsonschema/check-jsonschema: 0.28.2 → 0.28.4](https://github.com/python-jsonschema/check-jsonschema/compare/0.28.2...0.28.4) - [github.com/abravalheri/validate-pyproject: v0.16 → v0.18](https://github.com/abravalheri/validate-pyproject/compare/v0.16...v0.18) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e848eb670..6a76e8c00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.3 + rev: v0.4.7 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v18.1.4 + rev: v18.1.5 hooks: - id: clang-format types: [c] @@ -50,7 +50,7 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.28.2 + rev: 0.28.4 hooks: - id: check-github-workflows - id: check-readthedocs @@ -67,7 +67,7 @@ repos: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.16 + rev: v0.18 hooks: - id: validate-pyproject From 6e40601f69875e0734dce467e4a3b969649f65bf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 4 Jun 2024 20:37:09 +1000 Subject: [PATCH 185/195] Added type hints --- src/PIL/BlpImagePlugin.py | 3 ++- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/FpxImagePlugin.py | 6 +++--- src/PIL/ImageEnhance.py | 13 ++++++++----- src/PIL/ImageFilter.py | 25 ++++++++++++++++--------- src/PIL/ImageMorph.py | 6 +++--- src/PIL/Jpeg2KImagePlugin.py | 6 +++--- src/PIL/PaletteFile.py | 2 +- src/PIL/QoiImagePlugin.py | 2 +- src/PIL/SpiderImagePlugin.py | 2 +- src/PIL/WebPImagePlugin.py | 2 +- 11 files changed, 40 insertions(+), 29 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 782e28cf5..2db115ccc 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -35,6 +35,7 @@ import os import struct from enum import IntEnum from io import BytesIO +from typing import IO from . import Image, ImageFile @@ -448,7 +449,7 @@ class BLPEncoder(ImageFile.PyEncoder): return len(data), 0, data -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode != "P": msg = "Unsupported BLP image mode" raise ValueError(msg) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 5a44baa49..d24a2ba80 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -228,7 +228,7 @@ class EpsImageFile(ImageFile.ImageFile): reading_trailer_comments = False trailer_reached = False - def check_required_header_comments(): + def check_required_header_comments() -> None: if "PS-Adobe" not in self.info: msg = 'EPS header missing "%!PS-Adobe" comment' raise SyntaxError(msg) diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 4ba93bb39..b3e6c6e36 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -70,7 +70,7 @@ class FpxImageFile(ImageFile.ImageFile): self._open_index(1) - def _open_index(self, index=1): + def _open_index(self, index: int = 1) -> None: # # get the Image Contents Property Set @@ -85,7 +85,7 @@ class FpxImageFile(ImageFile.ImageFile): size = max(self.size) i = 1 while size > 64: - size = size / 2 + size = size // 2 i += 1 self.maxid = i - 1 @@ -118,7 +118,7 @@ class FpxImageFile(ImageFile.ImageFile): self._open_subimage(1, self.maxid) - def _open_subimage(self, index=1, subimage=0): + def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: # # setup tile descriptors for a given subimage diff --git a/src/PIL/ImageEnhance.py b/src/PIL/ImageEnhance.py index 93a50d2a2..d7e99a968 100644 --- a/src/PIL/ImageEnhance.py +++ b/src/PIL/ImageEnhance.py @@ -23,7 +23,10 @@ from . import Image, ImageFilter, ImageStat class _Enhance: - def enhance(self, factor): + image: Image.Image + degenerate: Image.Image + + def enhance(self, factor: float) -> Image.Image: """ Returns an enhanced image. @@ -46,7 +49,7 @@ class Color(_Enhance): the original image. """ - def __init__(self, image): + def __init__(self, image: Image.Image) -> None: self.image = image self.intermediate_mode = "L" if "A" in image.getbands(): @@ -63,7 +66,7 @@ class Contrast(_Enhance): gives a solid gray image. A factor of 1.0 gives the original image. """ - def __init__(self, image): + def __init__(self, image: Image.Image) -> None: self.image = image mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5) self.degenerate = Image.new("L", image.size, mean).convert(image.mode) @@ -80,7 +83,7 @@ class Brightness(_Enhance): original image. """ - def __init__(self, image): + def __init__(self, image: Image.Image) -> None: self.image = image self.degenerate = Image.new(image.mode, image.size, 0) @@ -96,7 +99,7 @@ class Sharpness(_Enhance): original image, and a factor of 2.0 gives a sharpened image. """ - def __init__(self, image): + def __init__(self, image: Image.Image) -> None: self.image = image self.degenerate = image.filter(ImageFilter.SMOOTH) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 43e700b7b..02288e135 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -18,7 +18,8 @@ from __future__ import annotations import abc import functools -from typing import Sequence +from types import ModuleType +from typing import Any, Sequence class Filter: @@ -57,7 +58,13 @@ class Kernel(BuiltinFilter): name = "Kernel" - def __init__(self, size, kernel, scale=None, offset=0): + def __init__( + self, + size: tuple[int, int], + kernel: Sequence[float], + scale: float | None = None, + offset: float = 0, + ) -> None: if scale is None: # default scale is sum of kernel scale = functools.reduce(lambda a, b: a + b, kernel) @@ -194,10 +201,8 @@ class BoxBlur(MultibandFilter): name = "BoxBlur" - def __init__(self, radius): - xy = radius - if not isinstance(xy, (tuple, list)): - xy = (xy, xy) + def __init__(self, radius: float | Sequence[float]) -> None: + xy = radius if isinstance(radius, (tuple, list)) else (radius, radius) if xy[0] < 0 or xy[1] < 0: msg = "radius must be >= 0" raise ValueError(msg) @@ -381,7 +386,9 @@ class Color3DLUT(MultibandFilter): name = "Color 3D LUT" - def __init__(self, size, table, channels=3, target_mode=None, **kwargs): + def __init__( + self, size, table, channels: int = 3, target_mode: str | None = None, **kwargs + ): if channels not in (3, 4): msg = "Only 3 or 4 output channels are supported" raise ValueError(msg) @@ -395,7 +402,7 @@ class Color3DLUT(MultibandFilter): items = size[0] * size[1] * size[2] wrong_size = False - numpy = None + numpy: ModuleType | None = None if hasattr(table, "shape"): try: import numpy @@ -442,7 +449,7 @@ class Color3DLUT(MultibandFilter): self.table = table @staticmethod - def _check_size(size): + def _check_size(size: Any) -> list[int]: try: _, _, _ = size except ValueError as e: diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 6ee8c4f25..6a43983d3 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -200,7 +200,7 @@ class MorphOp: elif patterns is not None: self.lut = LutBuilder(patterns=patterns).build_lut() - def apply(self, image: Image.Image): + def apply(self, image: Image.Image) -> tuple[int, Image.Image]: """Run a single morphological operation on an image Returns a tuple of the number of changed pixels and the @@ -216,7 +216,7 @@ class MorphOp: count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage - def match(self, image: Image.Image): + def match(self, image: Image.Image) -> list[tuple[int, int]]: """Get a list of coordinates matching the morphological operation on an image. @@ -231,7 +231,7 @@ class MorphOp: raise ValueError(msg) return _imagingmorph.match(bytes(self.lut), image.im.id) - def get_on_pixels(self, image: Image.Image): + def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: """Get a list of all turned on pixels in a binary image Returns a list of tuples of (x,y) coordinates diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index ce6342bdb..e6395b1cb 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -34,7 +34,7 @@ class BoxReader: self.length = length self.remaining_in_box = -1 - def _can_read(self, num_bytes): + def _can_read(self, num_bytes: int) -> bool: if self.has_length and self.fp.tell() + num_bytes > self.length: # Outside box: ensure we don't read past the known file length return False @@ -44,7 +44,7 @@ class BoxReader: else: return True # No length known, just read - def _read_bytes(self, num_bytes): + def _read_bytes(self, num_bytes: int) -> bytes: if not self._can_read(num_bytes): msg = "Not enough data in header" raise SyntaxError(msg) @@ -74,7 +74,7 @@ class BoxReader: else: return True - def next_box_type(self): + def next_box_type(self) -> bytes: # Skip the rest of the box if it has not been read if self.remaining_in_box > 0: self.fp.seek(self.remaining_in_box, os.SEEK_CUR) diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index dc3175402..eaed5367c 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -48,5 +48,5 @@ class PaletteFile: self.palette = b"".join(self.palette) - def getpalette(self): + def getpalette(self) -> tuple[bytes, str]: return self.palette, self.rawmode diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index cea8b60da..f2cf06d0d 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -38,7 +38,7 @@ class QoiImageFile(ImageFile.ImageFile): class QoiDecoder(ImageFile.PyDecoder): _pulls_fd = True - def _add_to_previous_pixels(self, value): + def _add_to_previous_pixels(self, value: bytes | bytearray) -> None: self._previous_pixel = value r, g, b, a = value diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 5b8ad47f0..e5242395f 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -233,7 +233,7 @@ def loadImageSeries(filelist=None): # For saving images in Spider format -def makeSpiderHeader(im): +def makeSpiderHeader(im: Image.Image) -> list[bytes]: nsam, nrow = im.size lenbyt = nsam * 4 # There are labrec records in the header labrec = int(1024 / lenbyt) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index ff7402dca..463d6a623 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -117,7 +117,7 @@ class WebPImageFile(ImageFile.ImageFile): # Set logical frame to requested position self.__logical_frame = frame - def _reset(self, reset=True): + def _reset(self, reset: bool = True) -> None: if reset: self._decoder.reset() self.__physical_frame = 0 From b3c534cc9aa1acb7d84d0be83c2919072e46af95 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Jun 2024 08:29:28 +1000 Subject: [PATCH 186/195] Added type hints --- src/PIL/BmpImagePlugin.py | 9 ++++++--- src/PIL/BufrStubImagePlugin.py | 4 +++- src/PIL/DdsImagePlugin.py | 3 ++- src/PIL/GifImagePlugin.py | 16 +++++++++------- src/PIL/GribStubImagePlugin.py | 4 +++- src/PIL/IcoImagePlugin.py | 6 ++++-- src/PIL/ImImagePlugin.py | 5 +++-- src/PIL/Image.py | 20 ++++++++++---------- src/PIL/MpoImagePlugin.py | 3 ++- src/PIL/PdfImagePlugin.py | 3 ++- src/PIL/SpiderImagePlugin.py | 6 +++--- src/PIL/TiffImagePlugin.py | 4 ++-- src/PIL/WmfImagePlugin.py | 4 +++- 13 files changed, 52 insertions(+), 35 deletions(-) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index c5d1cd40d..2df1d8d33 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -25,6 +25,7 @@ from __future__ import annotations import os +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -52,7 +53,7 @@ def _accept(prefix: bytes) -> bool: return prefix[:2] == b"BM" -def _dib_accept(prefix): +def _dib_accept(prefix: bytes) -> bool: return i32(prefix) in [12, 40, 52, 56, 64, 108, 124] @@ -394,11 +395,13 @@ SAVE = { } -def _dib_save(im, fp, filename): +def _dib_save(im: Image.Image, fp: IO[bytes], filename: str) -> None: _save(im, fp, filename, False) -def _save(im, fp, filename, bitmap_header=True): +def _save( + im: Image.Image, fp: IO[bytes], filename: str, bitmap_header: bool = True +) -> None: try: rawmode, bits, colors = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 826e89daf..6f52204b8 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -10,6 +10,8 @@ # from __future__ import annotations +from typing import IO + from . import Image, ImageFile _handler = None @@ -58,7 +60,7 @@ class BufrStubImageFile(ImageFile.StubImageFile): return _handler -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if _handler is None or not hasattr(_handler, "save"): msg = "BUFR save handler not installed" raise OSError(msg) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 1575f2d88..a3efadb03 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -16,6 +16,7 @@ import io import struct import sys from enum import IntEnum, IntFlag +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i32le as i32 @@ -510,7 +511,7 @@ class DdsRgbDecoder(ImageFile.PyDecoder): return -1, 0 -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode not in ("RGB", "RGBA", "L", "LA"): msg = f"cannot write mode {im.mode} as DDS" raise OSError(msg) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 962a92834..e62852db3 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,6 +31,7 @@ import os import subprocess from enum import IntEnum from functools import cached_property +from typing import IO from . import ( Image, @@ -336,14 +337,13 @@ class GifImageFile(ImageFile.ImageFile): self._mode = "RGB" self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) - def _rgb(color): + def _rgb(color: int) -> tuple[int, int, int]: if self._frame_palette: if color * 3 + 3 > len(self._frame_palette.palette): color = 0 - color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) + return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) else: - color = (color, color, color) - return color + return (color, color, color) self.dispose_extent = frame_dispose_extent try: @@ -709,11 +709,13 @@ def _write_multiple_frames(im, fp, palette): return True -def _save_all(im, fp, filename): +def _save_all(im: Image.Image, fp: IO[bytes], filename: str) -> None: _save(im, fp, filename, save_all=True) -def _save(im, fp, filename, save_all=False): +def _save( + im: Image.Image, fp: IO[bytes], filename: str, save_all: bool = False +) -> None: # header if "palette" in im.encoderinfo or "palette" in im.info: palette = im.encoderinfo.get("palette", im.info.get("palette")) @@ -730,7 +732,7 @@ def _save(im, fp, filename, save_all=False): fp.flush() -def get_interlace(im): +def get_interlace(im: Image.Image) -> int: interlace = im.encoderinfo.get("interlace", 1) # workaround for @PIL153 diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index c27cffab6..b24dcded2 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -10,6 +10,8 @@ # from __future__ import annotations +from typing import IO + from . import Image, ImageFile _handler = None @@ -58,7 +60,7 @@ class GribStubImageFile(ImageFile.StubImageFile): return _handler -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if _handler is None or not hasattr(_handler, "save"): msg = "GRIB save handler not installed" raise OSError(msg) diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index cea093f9c..af94e5a2e 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -25,6 +25,7 @@ from __future__ import annotations import warnings from io import BytesIO from math import ceil, log +from typing import IO from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin from ._binary import i16le as i16 @@ -39,7 +40,7 @@ from ._binary import o32le as o32 _MAGIC = b"\0\0\1\0" -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: fp.write(_MAGIC) # (2+2) bmp = im.encoderinfo.get("bitmap_format") == "bmp" sizes = im.encoderinfo.get( @@ -194,7 +195,7 @@ class IcoFile: """ return self.frame(self.getentryindex(size, bpp)) - def frame(self, idx): + def frame(self, idx: int) -> Image.Image: """ Get an image from frame idx """ @@ -205,6 +206,7 @@ class IcoFile: data = self.buf.read(8) self.buf.seek(header["offset"]) + im: Image.Image if data[:8] == PngImagePlugin._MAGIC: # png frame im = PngImagePlugin.PngImageFile(self.buf) diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 8e949ebaf..c98cfb098 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -28,6 +28,7 @@ from __future__ import annotations import os import re +from typing import IO, Any from . import Image, ImageFile, ImagePalette @@ -103,7 +104,7 @@ for j in range(2, 33): split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") -def number(s): +def number(s: Any) -> float: try: return int(s) except ValueError: @@ -325,7 +326,7 @@ SAVE = { } -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: try: image_type, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6385f204b..8cb4b7e32 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2875,7 +2875,7 @@ class Image: self.load() return self._new(self.im.transpose(method)) - def effect_spread(self, distance): + def effect_spread(self, distance: int) -> Image: """ Randomly spread pixels in an image. @@ -3012,7 +3012,7 @@ def new( return im._new(core.fill(mode, size, color)) -def frombytes(mode, size, data, decoder_name="raw", *args) -> Image: +def frombytes(mode, size, data, decoder_name: str = "raw", *args) -> Image: """ Creates a copy of an image memory from pixel data in a buffer. @@ -3051,7 +3051,7 @@ def frombytes(mode, size, data, decoder_name="raw", *args) -> Image: return im -def frombuffer(mode, size, data, decoder_name="raw", *args) -> Image: +def frombuffer(mode: str, size, data, decoder_name: str = "raw", *args) -> Image: """ Creates an image memory referencing pixel data in a byte buffer. @@ -3553,7 +3553,7 @@ def register_save(id: str, driver) -> None: SAVE[id.upper()] = driver -def register_save_all(id, driver) -> None: +def register_save_all(id: str, driver) -> None: """ Registers an image function to save all the frames of a multiframe format. This function should not be @@ -3565,7 +3565,7 @@ def register_save_all(id, driver) -> None: SAVE_ALL[id.upper()] = driver -def register_extension(id, extension) -> None: +def register_extension(id: str, extension: str) -> None: """ Registers an image extension. This function should not be used in application code. @@ -3576,7 +3576,7 @@ def register_extension(id, extension) -> None: EXTENSION[extension.lower()] = id.upper() -def register_extensions(id, extensions) -> None: +def register_extensions(id: str, extensions: list[str]) -> None: """ Registers image extensions. This function should not be used in application code. @@ -3588,7 +3588,7 @@ def register_extensions(id, extensions) -> None: register_extension(id, extension) -def registered_extensions(): +def registered_extensions() -> dict[str, str]: """ Returns a dictionary containing all file extensions belonging to registered plugins @@ -3650,7 +3650,7 @@ def effect_mandelbrot(size, extent, quality): return Image()._new(core.effect_mandelbrot(size, extent, quality)) -def effect_noise(size, sigma): +def effect_noise(size: tuple[int, int], sigma: float) -> Image: """ Generate Gaussian noise centered around 128. @@ -3661,7 +3661,7 @@ def effect_noise(size, sigma): return Image()._new(core.effect_noise(size, sigma)) -def linear_gradient(mode): +def linear_gradient(mode: str) -> Image: """ Generate 256x256 linear gradient from black to white, top to bottom. @@ -3670,7 +3670,7 @@ def linear_gradient(mode): return Image()._new(core.linear_gradient(mode)) -def radial_gradient(mode): +def radial_gradient(mode: str) -> Image: """ Generate 256x256 radial gradient from black to white, centre to edge. diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 766e1290c..6716722f2 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -22,6 +22,7 @@ from __future__ import annotations import itertools import os import struct +from typing import IO from . import ( Image, @@ -32,7 +33,7 @@ from . import ( from ._binary import o32le -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: JpegImagePlugin._save(im, fp, filename) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 1777f1f20..ccd28f343 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -25,6 +25,7 @@ import io import math import os import time +from typing import IO from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features @@ -39,7 +40,7 @@ from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features # 5. page contents -def _save_all(im, fp, filename): +def _save_all(im: Image.Image, fp: IO[bytes], filename: str) -> None: _save(im, fp, filename, save_all=True) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index e5242395f..98dd91c0e 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -37,7 +37,7 @@ from __future__ import annotations import os import struct import sys -from typing import TYPE_CHECKING +from typing import IO, TYPE_CHECKING from . import Image, ImageFile @@ -263,7 +263,7 @@ def makeSpiderHeader(im: Image.Image) -> list[bytes]: return [struct.pack("f", v) for v in hdr] -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode[0] != "F": im = im.convert("F") @@ -279,7 +279,7 @@ def _save(im, fp, filename): ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) -def _save_spider(im, fp, filename): +def _save_spider(im: Image.Image, fp: IO[bytes], filename: str) -> None: # get the filename extension and register it with Image ext = os.path.splitext(filename)[1] Image.register_extension(SpiderImageFile.format, ext) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index f3fa3c24c..04f36744b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1995,7 +1995,7 @@ class AppendingTiffWriter: self.finalize() self.setup() - def __enter__(self): + def __enter__(self) -> AppendingTiffWriter: return self def __exit__(self, exc_type, exc_value, traceback): @@ -2023,7 +2023,7 @@ class AppendingTiffWriter: self.f.write(bytes(pad_bytes)) self.offsetOfNewPage = self.f.tell() - def setEndian(self, endian): + def setEndian(self, endian: str) -> None: self.endian = endian self.longFmt = f"{self.endian}L" self.shortFmt = f"{self.endian}H" diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index fab3e26c5..25a4545db 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -20,6 +20,8 @@ # http://wvware.sourceforge.net/caolan/ora-wmf.html from __future__ import annotations +from typing import IO + from . import Image, ImageFile from ._binary import i16le as word from ._binary import si16le as short @@ -161,7 +163,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): return super().load() -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if _handler is None or not hasattr(_handler, "save"): msg = "WMF save handler not installed" raise OSError(msg) From 923d4e5e1a971ea64ce56c5b016a620be33d51eb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Jun 2024 22:27:23 +1000 Subject: [PATCH 187/195] Added type hints --- Tests/bench_cffi_access.py | 1 + Tests/test_features.py | 2 +- Tests/test_file_bmp.py | 2 +- Tests/test_file_bufrstub.py | 11 ++++++----- Tests/test_file_gribstub.py | 4 ++-- Tests/test_file_hdf5stub.py | 7 ++++--- Tests/test_file_jpeg.py | 2 +- Tests/test_file_webp.py | 4 +++- Tests/test_file_webp_animated.py | 2 +- Tests/test_file_wmf.py | 12 ++++++++---- Tests/test_image_access.py | 6 ++++++ Tests/test_image_rotate.py | 4 ++-- Tests/test_image_thumbnail.py | 4 +++- Tests/test_imageops_usm.py | 1 - Tests/test_qt_image_qapplication.py | 2 +- src/PIL/BufrStubImagePlugin.py | 2 +- src/PIL/GribStubImagePlugin.py | 2 +- src/PIL/Hdf5StubImagePlugin.py | 2 +- src/PIL/WmfImagePlugin.py | 2 +- 19 files changed, 44 insertions(+), 28 deletions(-) diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index d2a08c07b..c7d105836 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -44,6 +44,7 @@ def test_direct() -> None: caccess = im.im.pixel_access(False) access = PyAccess.new(im, False) + assert access is not None assert caccess[(0, 0)] == access[(0, 0)] print(f"Size: {im.width}x{im.height}") diff --git a/Tests/test_features.py b/Tests/test_features.py index 59fb49809..de418115e 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -124,7 +124,7 @@ def test_unsupported_module() -> None: @pytest.mark.parametrize("supported_formats", (True, False)) -def test_pilinfo(supported_formats) -> None: +def test_pilinfo(supported_formats: bool) -> None: buf = io.StringIO() features.pilinfo(buf, supported_formats=supported_formats) out = buf.getvalue() diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index c7c9b24e7..2ff4160bd 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -140,7 +140,7 @@ def test_load_dib() -> None: (124, "g/pal8v5.bmp"), ), ) -def test_dib_header_size(header_size, path): +def test_dib_header_size(header_size: int, path: str) -> None: image_path = "Tests/images/bmp/" + path with open(image_path, "rb") as fp: data = fp.read()[14:] diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 3dd24533a..939e82e77 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,10 +1,11 @@ from __future__ import annotations from pathlib import Path +from typing import IO import pytest -from PIL import BufrStubImagePlugin, Image +from PIL import BufrStubImagePlugin, Image, ImageFile from .helper import hopper @@ -50,20 +51,20 @@ def test_save(tmp_path: Path) -> None: def test_handler(tmp_path: Path) -> None: - class TestHandler: + class TestHandler(ImageFile.StubHandler): opened = False loaded = False saved = False - def open(self, im) -> None: + def open(self, im: ImageFile.StubImageFile) -> None: self.opened = True - def load(self, im): + def load(self, im: ImageFile.StubImageFile) -> Image.Image: self.loaded = True im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename) -> None: + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index 096a5b88b..86a9064fc 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -5,7 +5,7 @@ from typing import IO import pytest -from PIL import GribStubImagePlugin, Image +from PIL import GribStubImagePlugin, Image, ImageFile from .helper import hopper @@ -51,7 +51,7 @@ def test_save(tmp_path: Path) -> None: def test_handler(tmp_path: Path) -> None: - class TestHandler: + class TestHandler(ImageFile.StubHandler): opened = False loaded = False saved = False diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index f871e2eff..ee1544c51 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,11 +1,12 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path from typing import IO import pytest -from PIL import Hdf5StubImagePlugin, Image +from PIL import Hdf5StubImagePlugin, Image, ImageFile TEST_FILE = "Tests/images/hdf5.h5" @@ -41,7 +42,7 @@ def test_load() -> None: def test_save() -> None: # Arrange with Image.open(TEST_FILE) as im: - dummy_fp = None + dummy_fp = BytesIO() dummy_filename = "dummy.filename" # Act / Assert: stub cannot save without an implemented handler @@ -52,7 +53,7 @@ def test_save() -> None: def test_handler(tmp_path: Path) -> None: - class TestHandler: + class TestHandler(ImageFile.StubHandler): opened = False loaded = False saved = False diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 33f9ce00e..18dc752d8 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -171,7 +171,7 @@ class TestFileJpeg: [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], ) def test_dpi(self, test_image_path: str) -> None: - def test(xdpi: int, ydpi: int | None = None): + def test(xdpi: int, ydpi: int | None = None) -> tuple[int, int] | None: with Image.open(test_image_path) as im: im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) return im.info.get("dpi") diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index e2de84c71..1caf032f6 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -198,7 +198,9 @@ class TestFileWebp: (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), ) @skip_unless_feature("webp_anim") - def test_invalid_background(self, background, tmp_path: Path) -> None: + def test_invalid_background( + self, background: int | tuple[int, ...], tmp_path: Path + ) -> None: temp_file = str(tmp_path / "temp.webp") im = hopper() with pytest.raises(OSError): diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index ba931f864..882dccb32 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -69,7 +69,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: are visually similar to the originals. """ - def check(temp_file) -> None: + def check(temp_file: str) -> None: with Image.open(temp_file) as im: assert im.n_frames == 2 diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index b43e3f296..79e707263 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,10 +1,11 @@ from __future__ import annotations from pathlib import Path +from typing import IO import pytest -from PIL import Image, WmfImagePlugin +from PIL import Image, ImageFile, WmfImagePlugin from .helper import assert_image_similar_tofile, hopper @@ -34,10 +35,13 @@ def test_load() -> None: def test_register_handler(tmp_path: Path) -> None: - class TestHandler: + class TestHandler(ImageFile.StubHandler): methodCalled = False - def save(self, im, fp, filename) -> None: + def load(self, im: ImageFile.StubImageFile) -> Image.Image: + return Image.new("RGB", (1, 1)) + + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: self.methodCalled = True handler = TestHandler() @@ -70,7 +74,7 @@ def test_load_set_dpi() -> None: @pytest.mark.parametrize("ext", (".wmf", ".emf")) -def test_save(ext, tmp_path: Path) -> None: +def test_save(ext: str, tmp_path: Path) -> None: im = hopper() tmpfile = str(tmp_path / ("temp" + ext)) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 9d6006679..8abb1f69f 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -259,6 +259,7 @@ class TestCffi(AccessTest): caccess = im.im.pixel_access(False) with pytest.warns(DeprecationWarning): access = PyAccess.new(im, False) + assert access is not None w, h = im.size for x in range(0, w, 10): @@ -289,6 +290,7 @@ class TestCffi(AccessTest): caccess = im.im.pixel_access(False) with pytest.warns(DeprecationWarning): access = PyAccess.new(im, False) + assert access is not None w, h = im.size for x in range(0, w, 10): @@ -299,6 +301,8 @@ class TestCffi(AccessTest): # Attempt to set the value on a read-only image with pytest.warns(DeprecationWarning): access = PyAccess.new(im, True) + assert access is not None + with pytest.raises(ValueError): access[(0, 0)] = color @@ -341,6 +345,8 @@ class TestCffi(AccessTest): im = Image.new(mode, (1, 1)) with pytest.warns(DeprecationWarning): access = PyAccess.new(im, False) + assert access is not None + access.putpixel((0, 0), color) if len(color) == 3: diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index c10c96da6..252a15db7 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -124,8 +124,8 @@ def test_fastpath_translate() -> None: def test_center() -> None: im = hopper() rotate(im, im.mode, 45, center=(0, 0)) - rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0)) - rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0)) + rotate(im, im.mode, 45, translate=(im.size[0] // 2, 0)) + rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] // 2, 0)) def test_rotate_no_fill() -> None: diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 2ca1d2cfc..1593eaaf7 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -111,7 +111,9 @@ def test_load_first_unless_jpeg() -> None: with Image.open("Tests/images/hopper.jpg") as im: draft = im.draft - def im_draft(mode: str, size: tuple[int, int]): + def im_draft( + mode: str, size: tuple[int, int] + ) -> tuple[str, tuple[int, int, float, float]] | None: result = draft(mode, size) assert result is not None diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 104c620de..dbdd5b317 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -58,7 +58,6 @@ def test_blur_formats(test_images: dict[str, ImageFile.ImageFile]) -> None: blur = ImageFilter.GaussianBlur with pytest.raises(ValueError): im.convert("1").filter(blur) - blur(im.convert("L")) with pytest.raises(ValueError): im.convert("I").filter(blur) with pytest.raises(ValueError): diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 3cd323553..28f66891c 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -46,7 +46,7 @@ def roundtrip(expected: Image.Image) -> None: @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") def test_sanity(tmp_path: Path) -> None: # Segfault test - app = QApplication([]) + app: QApplication | None = QApplication([]) ex = Example() assert app # Silence warning assert ex # Silence warning diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 6f52204b8..7388a2b8a 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -17,7 +17,7 @@ from . import Image, ImageFile _handler = None -def register_handler(handler: ImageFile.StubHandler) -> None: +def register_handler(handler: ImageFile.StubHandler | None) -> None: """ Install application-specific BUFR image handler. diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index b24dcded2..d3655f4dd 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -17,7 +17,7 @@ from . import Image, ImageFile _handler = None -def register_handler(handler: ImageFile.StubHandler) -> None: +def register_handler(handler: ImageFile.StubHandler | None) -> None: """ Install application-specific GRIB image handler. diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index c8d7866a3..b789c215f 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -17,7 +17,7 @@ from . import Image, ImageFile _handler = None -def register_handler(handler: ImageFile.StubHandler) -> None: +def register_handler(handler: ImageFile.StubHandler | None) -> None: """ Install application-specific HDF5 image handler. diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 25a4545db..a68f705a0 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -30,7 +30,7 @@ from ._binary import si32le as _long _handler = None -def register_handler(handler: ImageFile.StubHandler) -> None: +def register_handler(handler: ImageFile.StubHandler | None) -> None: """ Install application-specific WMF image handler. From 148f0d345f92261e12d28ebbbd3b92d027d667ca Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 6 Jun 2024 09:38:38 +0300 Subject: [PATCH 188/195] Use Sphinx long options in Makefile --- docs/Makefile | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 6495e5866..8f13f1aea 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -9,9 +9,9 @@ PAPER = BUILDDIR = _build # Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +PAPEROPT_a4 = --define latex_paper_size=a4 +PAPEROPT_letter = --define latex_paper_size=letter +ALLSPHINXOPTS = --doctree-dir $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . @@ -51,42 +51,42 @@ install-sphinx: .PHONY: html html: $(MAKE) install-sphinx - $(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html + $(SPHINXBUILD) --builder html --fail-on-warning --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(MAKE) install-sphinx - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + $(SPHINXBUILD) --builder dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(MAKE) install-sphinx - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + $(SPHINXBUILD) --builder singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(MAKE) install-sphinx - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + $(SPHINXBUILD) --builder pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(MAKE) install-sphinx - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + $(SPHINXBUILD) --builder json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(MAKE) install-sphinx - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + $(SPHINXBUILD) --builder htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." @@ -94,7 +94,7 @@ htmlhelp: .PHONY: qthelp qthelp: $(MAKE) install-sphinx - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + $(SPHINXBUILD) --builder qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @@ -105,7 +105,7 @@ qthelp: .PHONY: devhelp devhelp: $(MAKE) install-sphinx - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + $(SPHINXBUILD) --builder devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @@ -116,14 +116,14 @@ devhelp: .PHONY: epub epub: $(MAKE) install-sphinx - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + $(SPHINXBUILD) --builder epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: latex latex: $(MAKE) install-sphinx - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + $(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ @@ -132,7 +132,7 @@ latex: .PHONY: latexpdf latexpdf: $(MAKE) install-sphinx - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + $(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." @@ -140,21 +140,21 @@ latexpdf: .PHONY: text text: $(MAKE) install-sphinx - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + $(SPHINXBUILD) --builder text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(MAKE) install-sphinx - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + $(SPHINXBUILD) --builder man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(MAKE) install-sphinx - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + $(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ @@ -163,7 +163,7 @@ texinfo: .PHONY: info info: $(MAKE) install-sphinx - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + $(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." @@ -171,21 +171,21 @@ info: .PHONY: gettext gettext: $(MAKE) install-sphinx - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + $(SPHINXBUILD) --builder gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(MAKE) install-sphinx - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + $(SPHINXBUILD) --builder changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(MAKE) install-sphinx - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto + $(SPHINXBUILD) --builder linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." @@ -193,7 +193,7 @@ linkcheck: .PHONY: doctest doctest: $(MAKE) install-sphinx - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + $(SPHINXBUILD) --builder doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." From 44805bcd1d34446af734052f91428844a604540b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 7 Jun 2024 16:49:03 +1000 Subject: [PATCH 189/195] Updated fribidi to 1.0.15 --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0d6da7754..7ec1eaa82 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -112,7 +112,7 @@ ARCHITECTURES = { V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.2", - "FRIBIDI": "1.0.13", + "FRIBIDI": "1.0.15", "HARFBUZZ": "8.4.0", "JPEGTURBO": "3.0.2", "LCMS2": "2.16", From d2603b779aec4810349621542b795e79c0144d8e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Jun 2024 15:42:24 +1000 Subject: [PATCH 190/195] im color could be a tuple with a single float --- 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 2ea26877d..d9eb73e45 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1719,7 +1719,7 @@ class Image: def paste( self, - im: Image | str | float | tuple[int, ...], + im: Image | str | float | tuple[float, ...], box: tuple[int, int, int, int] | tuple[int, int] | None = None, mask: Image | None = None, ) -> None: From 45cdc53bbb609212590b0558061aa2991cc87a5d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Jun 2024 18:01:26 +1000 Subject: [PATCH 191/195] Updated type hints --- Tests/test_image_rotate.py | 4 ++-- docs/handbook/concepts.rst | 6 ++++++ docs/reference/Image.rst | 1 - src/PIL/Image.py | 10 +++++----- src/PIL/ImageDraw.py | 11 ++++++----- src/PIL/ImageFont.py | 2 +- src/PIL/_imagingft.pyi | 2 +- 7 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index c10c96da6..252a15db7 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -124,8 +124,8 @@ def test_fastpath_translate() -> None: def test_center() -> None: im = hopper() rotate(im, im.mode, 45, center=(0, 0)) - rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0)) - rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0)) + rotate(im, im.mode, 45, translate=(im.size[0] // 2, 0)) + rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] // 2, 0)) def test_rotate_no_fill() -> None: diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 5094dbf3f..7da1078c1 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -144,10 +144,12 @@ pixel, the Python Imaging Library provides different resampling *filters*. .. py:currentmodule:: PIL.Image .. data:: Resampling.NEAREST + :noindex: Pick one nearest pixel from the input image. Ignore all other input pixels. .. data:: Resampling.BOX + :noindex: Each pixel of source image contributes to one pixel of the destination image with identical weights. @@ -158,6 +160,7 @@ pixel, the Python Imaging Library provides different resampling *filters*. .. versionadded:: 3.4.0 .. data:: Resampling.BILINEAR + :noindex: For resize calculate the output pixel value using linear interpolation on all pixels that may contribute to the output value. @@ -165,6 +168,7 @@ pixel, the Python Imaging Library provides different resampling *filters*. in the input image is used. .. data:: Resampling.HAMMING + :noindex: Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have dislocations on local level like with :data:`Resampling.BOX`. @@ -174,6 +178,7 @@ pixel, the Python Imaging Library provides different resampling *filters*. .. versionadded:: 3.4.0 .. data:: Resampling.BICUBIC + :noindex: For resize calculate the output pixel value using cubic interpolation on all pixels that may contribute to the output value. @@ -181,6 +186,7 @@ pixel, the Python Imaging Library provides different resampling *filters*. in the input image is used. .. data:: Resampling.LANCZOS + :noindex: Calculate the output pixel value using a high-quality Lanczos filter (a truncated sinc) on all pixels that may contribute to the output value. diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index d917a3c92..1c095a114 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -424,7 +424,6 @@ See :ref:`concept-filters` for details. .. autoclass:: Resampling :members: :undoc-members: - :noindex: Dither modes ^^^^^^^^^^^^ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d9eb73e45..13d374345 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2303,8 +2303,8 @@ class Image: def rotate( self, angle: float, - resample: int = Resampling.NEAREST, - expand: bool = False, + resample: Resampling = Resampling.NEAREST, + expand: int | bool = False, center: tuple[int, int] | None = None, translate: tuple[int, int] | None = None, fillcolor: float | tuple[float, ...] | str | None = None, @@ -2617,8 +2617,8 @@ class Image: def thumbnail( self, - size: tuple[int, int], - resample: int = Resampling.BICUBIC, + size: tuple[float, float], + resample: Resampling = Resampling.BICUBIC, reducing_gap: float = 2.0, ) -> None: """ @@ -2953,7 +2953,7 @@ class ImageTransformHandler: self, size: tuple[int, int], image: Image, - **options: str | int | tuple[int, ...] | list[int], + **options: Any, ) -> Image: pass diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 0663d9ddf..9796189bb 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -95,7 +95,9 @@ class ImageDraw: if TYPE_CHECKING: from . import ImageFont - def getfont(self) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + def getfont( + self, + ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: """ Get the current default font. @@ -122,14 +124,13 @@ class ImageDraw: def _getfont( self, font_size: float | None - ) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: if font_size is not None: from . import ImageFont - font = ImageFont.load_default(font_size) + return ImageFont.load_default(font_size) else: - font = self.getfont() - return font + return self.getfont() def _getink(self, ink, fill=None) -> tuple[int | None, int | None]: if ink is None and fill is None: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a9925483e..87261f519 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -361,7 +361,7 @@ class FreeTypeFont: text: str, mode: str = "", direction: str | None = None, - features: str | None = None, + features: list[str] | None = None, language: str | None = None, stroke_width: float = 0, anchor: str | None = None, diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index b023efe01..6e0ddd2f1 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -6,7 +6,7 @@ class _Axis(TypedDict): minimum: int | None default: int | None maximum: int | None - name: str | None + name: bytes | None class Font: @property From 985e6053810b7236e67dafcbfcd4b53e71e3fe3d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Jun 2024 19:06:46 +1000 Subject: [PATCH 192/195] Renamed transform2 to transform --- src/PIL/Image.py | 2 +- src/_imaging.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 611d6960d..f61acc1d3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2883,7 +2883,7 @@ class Image: if image.mode in ("1", "P"): resample = Resampling.NEAREST - self.im.transform2(box, image.im, method, data, resample, fill) + self.im.transform(box, image.im, method, data, resample, fill) def transpose(self, method: Transpose) -> Image: """ diff --git a/src/_imaging.c b/src/_imaging.c index c565c21bb..f398c6c7c 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2028,7 +2028,7 @@ im_setmode(ImagingObject *self, PyObject *args) { } static PyObject * -_transform2(ImagingObject *self, PyObject *args) { +_transform(ImagingObject *self, PyObject *args) { static const char *wrong_number = "wrong number of matrix entries"; Imaging imOut; @@ -3647,7 +3647,7 @@ static struct PyMethodDef methods[] = { {"resize", (PyCFunction)_resize, METH_VARARGS}, {"reduce", (PyCFunction)_reduce, METH_VARARGS}, {"transpose", (PyCFunction)_transpose, METH_VARARGS}, - {"transform2", (PyCFunction)_transform2, METH_VARARGS}, + {"transform", (PyCFunction)_transform, METH_VARARGS}, {"isblock", (PyCFunction)_isblock, METH_NOARGS}, From 14a32650ddf8af6016045170e6e7490e1cdcc0b0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Jun 2024 22:26:28 +1000 Subject: [PATCH 193/195] Added type hints --- src/PIL/ImageColor.py | 53 +++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index 5fb80b753..9a15a8eb7 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -25,7 +25,7 @@ from . import Image @lru_cache -def getrgb(color): +def getrgb(color: str) -> tuple[int, int, int] | tuple[int, int, int, int]: """ Convert a color string to an RGB or RGBA tuple. If the string cannot be parsed, this function raises a :py:exc:`ValueError` exception. @@ -44,8 +44,10 @@ def getrgb(color): if rgb: if isinstance(rgb, tuple): return rgb - colormap[color] = rgb = getrgb(rgb) - return rgb + rgb_tuple = getrgb(rgb) + assert len(rgb_tuple) == 3 + colormap[color] = rgb_tuple + return rgb_tuple # check for known string formats if re.match("#[a-f0-9]{3}$", color): @@ -88,15 +90,15 @@ def getrgb(color): if m: from colorsys import hls_to_rgb - rgb = hls_to_rgb( + rgb_floats = hls_to_rgb( float(m.group(1)) / 360.0, float(m.group(3)) / 100.0, float(m.group(2)) / 100.0, ) return ( - int(rgb[0] * 255 + 0.5), - int(rgb[1] * 255 + 0.5), - int(rgb[2] * 255 + 0.5), + int(rgb_floats[0] * 255 + 0.5), + int(rgb_floats[1] * 255 + 0.5), + int(rgb_floats[2] * 255 + 0.5), ) m = re.match( @@ -105,15 +107,15 @@ def getrgb(color): if m: from colorsys import hsv_to_rgb - rgb = hsv_to_rgb( + rgb_floats = hsv_to_rgb( float(m.group(1)) / 360.0, float(m.group(2)) / 100.0, float(m.group(3)) / 100.0, ) return ( - int(rgb[0] * 255 + 0.5), - int(rgb[1] * 255 + 0.5), - int(rgb[2] * 255 + 0.5), + int(rgb_floats[0] * 255 + 0.5), + int(rgb_floats[1] * 255 + 0.5), + int(rgb_floats[2] * 255 + 0.5), ) m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) @@ -124,7 +126,7 @@ def getrgb(color): @lru_cache -def getcolor(color, mode: str) -> tuple[int, ...]: +def getcolor(color: str, mode: str) -> int | tuple[int, ...]: """ Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is @@ -136,33 +138,34 @@ def getcolor(color, mode: str) -> tuple[int, ...]: :param color: A color string :param mode: Convert result to this mode - :return: ``(graylevel[, alpha]) or (red, green, blue[, alpha])`` + :return: ``graylevel, (graylevel, alpha) or (red, green, blue[, alpha])`` """ # same as getrgb, but converts the result to the given mode - color, alpha = getrgb(color), 255 - if len(color) == 4: - color, alpha = color[:3], color[3] + rgb, alpha = getrgb(color), 255 + if len(rgb) == 4: + alpha = rgb[3] + rgb = rgb[:3] if mode == "HSV": from colorsys import rgb_to_hsv - r, g, b = color + r, g, b = rgb h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255) return int(h * 255), int(s * 255), int(v * 255) elif Image.getmodebase(mode) == "L": - r, g, b = color + r, g, b = rgb # ITU-R Recommendation 601-2 for nonlinear RGB # scaled to 24 bits to match the convert's implementation. - color = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 + graylevel = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 if mode[-1] == "A": - return color, alpha - else: - if mode[-1] == "A": - return color + (alpha,) - return color + return graylevel, alpha + return graylevel + elif mode[-1] == "A": + return rgb + (alpha,) + return rgb -colormap = { +colormap: dict[str, str | tuple[int, int, int]] = { # X11 colour table from https://drafts.csswg.org/css-color-4/, with # gray/grey spelling issues fixed. This is a superset of HTML 4.0 # colour names used in CSS 1. From 56fa3c658a9c60facce7acbb3909855f7aea2340 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 9 Jun 2024 07:12:05 +1000 Subject: [PATCH 194/195] Added type hints --- src/PIL/GifImagePlugin.py | 15 +++++++++---- src/PIL/GimpGradientFile.py | 41 ++++++++++++++++++++++++++++-------- src/PIL/Image.py | 2 +- src/PIL/ImageDraw2.py | 6 +++--- src/PIL/ImageTk.py | 6 +++--- src/PIL/Jpeg2KImagePlugin.py | 3 ++- src/PIL/PaletteFile.py | 10 +++++---- src/PIL/TiffImagePlugin.py | 4 ++-- 8 files changed, 60 insertions(+), 27 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index e62852db3..f41bc2b32 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -558,7 +558,11 @@ def _normalize_palette(im, palette, info): return im -def _write_single_frame(im, fp, palette): +def _write_single_frame( + im: Image.Image, + fp: IO[bytes], + palette: bytes | bytearray | list[int] | ImagePalette.ImagePalette, +) -> None: im_out = _normalize_mode(im) for k, v in im_out.info.items(): im.encoderinfo.setdefault(k, v) @@ -579,7 +583,9 @@ def _write_single_frame(im, fp, palette): fp.write(b"\0") # end of image data -def _getbbox(base_im, im_frame): +def _getbbox( + base_im: Image.Image, im_frame: Image.Image +) -> tuple[Image.Image, tuple[int, int, int, int]]: if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im): im_frame = im_frame.convert("RGBA") base_im = base_im.convert("RGBA") @@ -790,7 +796,7 @@ def _write_local_header(fp, im, offset, flags): fp.write(o8(8)) # bits -def _save_netpbm(im, fp, filename): +def _save_netpbm(im: Image.Image, fp: IO[bytes], filename: str) -> None: # Unused by default. # To use, uncomment the register_save call at the end of the file. # @@ -821,6 +827,7 @@ def _save_netpbm(im, fp, filename): ) # Allow ppmquant to receive SIGPIPE if ppmtogif exits + assert quant_proc.stdout is not None quant_proc.stdout.close() retcode = quant_proc.wait() @@ -1080,7 +1087,7 @@ def getdata(im, offset=(0, 0), **params): class Collector: data = [] - def write(self, data): + def write(self, data: bytes) -> None: self.data.append(data) im.load() # make sure raster data is available diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index 2d8c78ea9..92068b904 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -21,6 +21,7 @@ See the GIMP distribution for more information.) from __future__ import annotations from math import log, pi, sin, sqrt +from typing import IO, Callable from ._binary import o8 @@ -28,7 +29,7 @@ EPSILON = 1e-10 """""" # Enable auto-doc for data member -def linear(middle, pos): +def linear(middle: float, pos: float) -> float: if pos <= middle: if middle < EPSILON: return 0.0 @@ -43,19 +44,19 @@ def linear(middle, pos): return 0.5 + 0.5 * pos / middle -def curved(middle, pos): +def curved(middle: float, pos: float) -> float: return pos ** (log(0.5) / log(max(middle, EPSILON))) -def sine(middle, pos): +def sine(middle: float, pos: float) -> float: return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0 -def sphere_increasing(middle, pos): +def sphere_increasing(middle: float, pos: float) -> float: return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2) -def sphere_decreasing(middle, pos): +def sphere_decreasing(middle: float, pos: float) -> float: return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2) @@ -64,9 +65,22 @@ SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] class GradientFile: - gradient = None + gradient: ( + list[ + tuple[ + float, + float, + float, + list[float], + list[float], + Callable[[float, float], float], + ] + ] + | None + ) = None - def getpalette(self, entries=256): + def getpalette(self, entries: int = 256) -> tuple[bytes, str]: + assert self.gradient is not None palette = [] ix = 0 @@ -101,7 +115,7 @@ class GradientFile: class GimpGradientFile(GradientFile): """File handler for GIMP's gradient format.""" - def __init__(self, fp): + def __init__(self, fp: IO[bytes]) -> None: if fp.readline()[:13] != b"GIMP Gradient": msg = "not a GIMP gradient file" raise SyntaxError(msg) @@ -114,7 +128,16 @@ class GimpGradientFile(GradientFile): count = int(line) - gradient = [] + gradient: list[ + tuple[ + float, + float, + float, + list[float], + list[float], + Callable[[float, float], float], + ] + ] = [] for i in range(count): s = fp.readline().split() diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f61acc1d3..f6ffac8a7 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2470,7 +2470,7 @@ class Image: save_all = params.pop("save_all", False) self.encoderinfo = params - self.encoderconfig = () + self.encoderconfig: tuple[Any, ...] = () preinit() diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 35ee5834e..b42f5d9ea 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -30,7 +30,7 @@ from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath class Pen: """Stores an outline color and width.""" - def __init__(self, color, width=1, opacity=255): + def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None: self.color = ImageColor.getrgb(color) self.width = width @@ -38,7 +38,7 @@ class Pen: class Brush: """Stores a fill color""" - def __init__(self, color, opacity=255): + def __init__(self, color: str, opacity: int = 255) -> None: self.color = ImageColor.getrgb(color) @@ -63,7 +63,7 @@ class Draw: self.image = image self.transform = None - def flush(self): + def flush(self) -> Image.Image: return self.image def render(self, op, xy, pen, brush=None): diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 6e2e7db1e..90defdbbc 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -37,7 +37,7 @@ from . import Image _pilbitmap_ok = None -def _pilbitmap_check(): +def _pilbitmap_check() -> int: global _pilbitmap_ok if _pilbitmap_ok is None: try: @@ -162,7 +162,7 @@ class PhotoImage: """ return self.__size[1] - def paste(self, im): + def paste(self, im: Image.Image) -> None: """ Paste a PIL image into the photo image. Note that this can be very slow if the photo image is displayed. @@ -254,7 +254,7 @@ class BitmapImage: return str(self.__photo) -def getimage(photo): +def getimage(photo: PhotoImage) -> Image.Image: """Copies the contents of a PhotoImage to a PIL image memory.""" im = Image.new("RGBA", (photo.width(), photo.height())) block = im.im diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index e6395b1cb..5a0ef0d01 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -18,6 +18,7 @@ from __future__ import annotations import io import os import struct +from typing import IO from . import Image, ImageFile, ImagePalette, _binary @@ -328,7 +329,7 @@ def _accept(prefix: bytes) -> bool: # Save support -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: # Get the keyword arguments info = im.encoderinfo diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index eaed5367c..81652e5ee 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -14,6 +14,8 @@ # from __future__ import annotations +from typing import IO + from ._binary import o8 @@ -22,8 +24,8 @@ class PaletteFile: rawmode = "RGB" - def __init__(self, fp): - self.palette = [(i, i, i) for i in range(256)] + def __init__(self, fp: IO[bytes]) -> None: + palette = [o8(i) * 3 for i in range(256)] while True: s = fp.readline() @@ -44,9 +46,9 @@ class PaletteFile: g = b = r if 0 <= i <= 255: - self.palette[i] = o8(r) + o8(g) + o8(b) + palette[i] = o8(r) + o8(g) + o8(b) - self.palette = b"".join(self.palette) + self.palette = b"".join(palette) def getpalette(self) -> tuple[bytes, str]: return self.palette, self.rawmode diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 04f36744b..0b9601755 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -50,7 +50,7 @@ import warnings from collections.abc import MutableMapping from fractions import Fraction from numbers import Number, Rational -from typing import TYPE_CHECKING, Any, Callable +from typing import IO, TYPE_CHECKING, Any, Callable from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 @@ -2149,7 +2149,7 @@ class AppendingTiffWriter: self.rewriteLastLong(offset) -def _save_all(im, fp, filename): +def _save_all(im: Image.Image, fp: IO[bytes], filename: str) -> None: encoderinfo = im.encoderinfo.copy() encoderconfig = im.encoderconfig append_images = list(encoderinfo.get("append_images", [])) From 56c79b6f523d2bb7733b9193e47fab2f63f5b546 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 9 Jun 2024 22:13:01 +1000 Subject: [PATCH 195/195] Simplified code --- src/PIL/GimpGradientFile.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index 92068b904..220eac57e 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -128,16 +128,7 @@ class GimpGradientFile(GradientFile): count = int(line) - gradient: list[ - tuple[ - float, - float, - float, - list[float], - list[float], - Callable[[float, float], float], - ] - ] = [] + self.gradient = [] for i in range(count): s = fp.readline().split() @@ -155,6 +146,4 @@ class GimpGradientFile(GradientFile): msg = "cannot handle HSV colour space" raise OSError(msg) - gradient.append((x0, x1, xm, rgb0, rgb1, segment)) - - self.gradient = gradient + self.gradient.append((x0, x1, xm, rgb0, rgb1, segment))