From 73b91f58d0bfa387880ab94ccb20afd08f634c53 Mon Sep 17 00:00:00 2001 From: Javier Dehesa Date: Fri, 25 Nov 2022 15:10:05 +0000 Subject: [PATCH 001/132] Support arbitrary number of loaded modules on Windows Changed the TKinter module loading function for Windows to support the rare (but possible) case of having more than 1024 modules loaded. This is an adaptation of the same fix that was added to Matplotlib in [PR #22445](https://github.com/matplotlib/matplotlib/pull/22445). --- src/Tk/tkImaging.c | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 16b9a2edd..58ca23a56 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -310,7 +310,7 @@ load_tkinter_funcs(void) { * Return 0 for success, non-zero for failure. */ - HMODULE hMods[1024]; + HMODULE* hMods = NULL; HANDLE hProcess; DWORD cbNeeded; unsigned int i; @@ -327,33 +327,45 @@ load_tkinter_funcs(void) { /* Returns pseudo-handle that does not need to be closed */ hProcess = GetCurrentProcess(); + /* Allocate module handlers array */ + if (!EnumProcessModules(hProcess, NULL, 0, &cbNeeded)) { + PyErr_SetFromWindowsErr(0); + return 1; + } + if (!(hMods = static_cast(malloc(cbNeeded)))) { + PyErr_NoMemory(); + return 1; + } + /* Iterate through modules in this process looking for Tcl / Tk names */ - if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) { + if (EnumProcessModules(hProcess, hMods, cbNeeded, &cbNeeded)) { for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) { if (!found_tcl) { found_tcl = get_tcl(hMods[i]); if (found_tcl == -1) { - return 1; + goto exit; } } if (!found_tk) { found_tk = get_tk(hMods[i]); if (found_tk == -1) { - return 1; + goto exit; } } if (found_tcl && found_tk) { - return 0; + goto exit; } } } - if (found_tcl == 0) { +exit: + free(hMods); + if (found_tcl != 1) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tcl routines"); - } else { + } else if (found_tk != 1) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines"); } - return 1; + return int((found_tcl != 1) && (found_tk != 1)); } #else /* not Windows */ From 40d9732a40cc577c3cd914dc3d22f0ccdeec9a2e Mon Sep 17 00:00:00 2001 From: Javier Dehesa Date: Fri, 25 Nov 2022 15:57:07 +0000 Subject: [PATCH 002/132] Fix cast syntax --- src/Tk/tkImaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 58ca23a56..68afb988a 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -332,7 +332,7 @@ load_tkinter_funcs(void) { PyErr_SetFromWindowsErr(0); return 1; } - if (!(hMods = static_cast(malloc(cbNeeded)))) { + if (!(hMods = (HMODULE*) malloc(cbNeeded))) { PyErr_NoMemory(); return 1; } From 80d7fa9004e35001a843a64f4accd0cb51e75813 Mon Sep 17 00:00:00 2001 From: Javier Dehesa Date: Fri, 25 Nov 2022 16:09:01 +0000 Subject: [PATCH 003/132] Fix another bad cast syntax --- src/Tk/tkImaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 68afb988a..9b4b1d8f5 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -365,7 +365,7 @@ exit: } else if (found_tk != 1) { 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 */ From 4a36d9d761790d88c98e08dd273dbc7abb0c71b2 Mon Sep 17 00:00:00 2001 From: Javier Dehesa Date: Fri, 25 Nov 2022 22:27:18 +0000 Subject: [PATCH 004/132] Avoid using PyErr_SetFromWindowsErr on Cygwin --- src/Tk/tkImaging.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 9b4b1d8f5..e16f33eb0 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -329,7 +329,11 @@ load_tkinter_funcs(void) { /* Allocate module handlers array */ if (!EnumProcessModules(hProcess, NULL, 0, &cbNeeded)) { +#if defined(__CYGWIN__) + PyErr_SetString(PyExc_OSError, "Call to EnumProcessModules failed"); +#else PyErr_SetFromWindowsErr(0); +#endif return 1; } if (!(hMods = (HMODULE*) malloc(cbNeeded))) { From 406a8478cd73c5c166783e9d3583b2249e3b7068 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 17:41:06 +1100 Subject: [PATCH 005/132] Use break instead of goto --- src/Tk/tkImaging.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index e16f33eb0..506bb7008 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -347,22 +347,21 @@ load_tkinter_funcs(void) { if (!found_tcl) { found_tcl = get_tcl(hMods[i]); if (found_tcl == -1) { - goto exit; + break; } } if (!found_tk) { found_tk = get_tk(hMods[i]); if (found_tk == -1) { - goto exit; + break; } } if (found_tcl && found_tk) { - goto exit; + break; } } } -exit: free(hMods); if (found_tcl != 1) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tcl routines"); From bcdb208fe2365b75c8d87d29783b9cc9f8cb683b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 09:44:53 +1100 Subject: [PATCH 006/132] Restored Image constants, except for duplicate Resampling attributes --- Tests/test_image.py | 10 ++-------- docs/deprecations.rst | 34 +++------------------------------- src/PIL/Image.py | 17 +++++++++-------- 3 files changed, 14 insertions(+), 47 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index a37c90296..a0c50b5f4 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -921,12 +921,7 @@ class TestImage: with pytest.warns(DeprecationWarning): assert Image.CONTAINER == 2 - def test_constants_deprecation(self): - with pytest.warns(DeprecationWarning): - assert Image.NEAREST == 0 - with pytest.warns(DeprecationWarning): - assert Image.NONE == 0 - + def test_constants(self): with pytest.warns(DeprecationWarning): assert Image.LINEAR == Image.Resampling.BILINEAR with pytest.warns(DeprecationWarning): @@ -943,8 +938,7 @@ class TestImage: Image.Quantize, ): for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(Image, name) == enum[name] + assert getattr(Image, name) == enum[name] @pytest.mark.parametrize( "path", diff --git a/docs/deprecations.rst b/docs/deprecations.rst index dec652df8..bbd873800 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -77,37 +77,9 @@ A number of constants have been deprecated and will be removed in Pillow 10.0.0 ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ -``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` -``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` -``Image.ORDERED`` ``Image.Dither.ORDERED`` -``Image.RASTERIZE`` ``Image.Dither.RASTERIZE`` -``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG`` -``Image.WEB`` ``Image.Palette.WEB`` -``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE`` -``Image.AFFINE`` ``Image.Transform.AFFINE`` -``Image.EXTENT`` ``Image.Transform.EXTENT`` -``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE`` -``Image.QUAD`` ``Image.Transform.QUAD`` -``Image.MESH`` ``Image.Transform.MESH`` -``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT`` -``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM`` -``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90`` -``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180`` -``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270`` -``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE`` -``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE`` -``Image.BOX`` ``Image.Resampling.BOX`` -``Image.BILINEAR`` ``Image.Resampling.BILINEAR`` -``Image.LINEAR`` ``Image.Resampling.BILINEAR`` -``Image.HAMMING`` ``Image.Resampling.HAMMING`` -``Image.BICUBIC`` ``Image.Resampling.BICUBIC`` -``Image.CUBIC`` ``Image.Resampling.BICUBIC`` -``Image.LANCZOS`` ``Image.Resampling.LANCZOS`` -``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS`` -``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT`` -``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE`` -``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE`` -``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT`` +``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` +``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` +``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS`` ``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` ``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` ``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a760de575..a31ec3800 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -65,21 +65,16 @@ def __getattr__(name): if name in categories: deprecate("Image categories", 10, "is_animated", plural=True) return categories[name] - elif name in ("NEAREST", "NONE"): - deprecate(name, 10, "Resampling.NEAREST or Dither.NONE") - return 0 old_resampling = { "LINEAR": "BILINEAR", "CUBIC": "BICUBIC", "ANTIALIAS": "LANCZOS", } if name in old_resampling: - deprecate(name, 10, f"Resampling.{old_resampling[name]}") + deprecate( + name, 10, f"{old_resampling[name]} or Resampling.{old_resampling[name]}" + ) return Resampling[old_resampling[name]] - for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize): - if name in enum.__members__: - deprecate(name, 10, f"{enum.__name__}.{name}") - return enum[name] raise AttributeError(f"module '{__name__}' has no attribute '{name}'") @@ -216,6 +211,12 @@ class Quantize(IntEnum): LIBIMAGEQUANT = 3 +module = sys.modules[__name__] +for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize): + for item in enum: + setattr(module, item.name, item.value) + + if hasattr(core, "DEFAULT_STRATEGY"): DEFAULT_STRATEGY = core.DEFAULT_STRATEGY FILTERED = core.FILTERED From 21e811117e3dfa0e1a93c54e239748ec2d221fe8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 13:55:13 +1100 Subject: [PATCH 007/132] Updated release notes --- docs/deprecations.rst | 5 +++++ docs/releasenotes/9.1.0.rst | 5 +++++ docs/releasenotes/9.4.0.rst | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index bbd873800..a84a9fe36 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -74,6 +74,11 @@ Constants A number of constants have been deprecated and will be removed in Pillow 10.0.0 (2023-07-01). Instead, ``enum.IntEnum`` classes have been added. +.. note:: + + Additional ``Image`` constants were deprecated in Pillow 9.1.0, but they + were later restored in Pillow 9.4.0. See :ref:`restored-image-constants` + ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 48ce6fef7..e97b58a41 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -53,6 +53,11 @@ Constants A number of constants have been deprecated and will be removed in Pillow 10.0.0 (2023-07-01). Instead, ``enum.IntEnum`` classes have been added. +.. note:: + + Some of these deprecations were restored in Pillow 9.4.0. See + :ref:`restored-image-constants` + ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index aae3e2b64..e4c47401c 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -103,3 +103,40 @@ Added support for DDS L and LA images Support has been added to read and write L and LA DDS images in the uncompressed format, known as "luminance" textures. + +.. _restored-image-constants: + +Constants +^^^^^^^^^ + +In Pillow 9.1.0, the following constants were deprecated. Those deprecations have now +been restored. + +- ``Image.NONE`` +- ``Image.NEAREST`` +- ``Image.ORDERED`` +- ``Image.RASTERIZE`` +- ``Image.FLOYDSTEINBERG`` +- ``Image.WEB`` +- ``Image.ADAPTIVE`` +- ``Image.AFFINE`` +- ``Image.EXTENT`` +- ``Image.PERSPECTIVE`` +- ``Image.QUAD`` +- ``Image.MESH`` +- ``Image.FLIP_LEFT_RIGHT`` +- ``Image.FLIP_TOP_BOTTOM`` +- ``Image.ROTATE_90`` +- ``Image.ROTATE_180`` +- ``Image.ROTATE_270`` +- ``Image.TRANSPOSE`` +- ``Image.TRANSVERSE`` +- ``Image.BOX`` +- ``Image.BILINEAR`` +- ``Image.HAMMING`` +- ``Image.BICUBIC`` +- ``Image.LANCZOS`` +- ``Image.MEDIANCUT`` +- ``Image.MAXCOVERAGE`` +- ``Image.FASTOCTREE`` +- ``Image.LIBIMAGEQUANT`` From 91b01f4cc2b1728295c97ac4114f800637c5ead2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 30 Dec 2022 16:48:33 +1100 Subject: [PATCH 008/132] Return from ImagingFill early if image has a zero dimension --- Tests/test_image.py | 5 +++++ src/libImaging/Fill.c | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/Tests/test_image.py b/Tests/test_image.py index a37c90296..13c162812 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -512,6 +512,11 @@ class TestImage: i = Image.new("RGB", [1, 1]) assert isinstance(i.size, tuple) + @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) + @pytest.mark.timeout(0.5) + def test_empty_image(self, size): + Image.new("RGB", size) + def test_storage_neg(self): # Storage.c accepted negative values for xsize, ysize. Was # test_neg_ppm, but the core function for that has been diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index f72060228..5b6bfb89c 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -24,6 +24,11 @@ ImagingFill(Imaging im, const void *colour) { int x, y; ImagingSectionCookie cookie; + /* 0-width or 0-height image. No need to do anything */ + if (!im->linesize || !im->ysize) { + return im; + } + if (im->type == IMAGING_TYPE_SPECIAL) { /* use generic API */ ImagingAccess access = ImagingAccessNew(im); From 559b7ae476d512165d851747f8c9da7df743a1b9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 09:43:36 +1100 Subject: [PATCH 009/132] Updated wording --- docs/deprecations.rst | 5 +++-- docs/releasenotes/9.4.0.rst | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a84a9fe36..4d48b822a 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -76,8 +76,9 @@ A number of constants have been deprecated and will be removed in Pillow 10.0.0 .. note:: - Additional ``Image`` constants were deprecated in Pillow 9.1.0, but they - were later restored in Pillow 9.4.0. See :ref:`restored-image-constants` + Additional ``Image`` constants were deprecated in Pillow 9.1.0, but that + was reversed in Pillow 9.4.0 and those constants will now remain available. + See :ref:`restored-image-constants` ===================================================== ============================================================ Deprecated Use instead diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 20a5afa63..2b111d5e4 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -115,8 +115,8 @@ format, known as "luminance" textures. Constants ^^^^^^^^^ -In Pillow 9.1.0, the following constants were deprecated. Those deprecations have now -been restored. +In Pillow 9.1.0, the following constants were deprecated. That has been reversed and +these constants will now remain available. - ``Image.NONE`` - ``Image.NEAREST`` From 2494e128ab099ee28601aad9ef2745f2dea15a41 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 11:50:43 +1100 Subject: [PATCH 010/132] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 904c73629..df24f562f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Reversed deprecations for Image constants, except for duplicate Resampling attributes #6830 + [radarhere] + - Improve exception traceback readability #6836 [hugovk, radarhere] From 280330476345c10a3c95ef44b8dba260c6694502 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 13:47:07 +1100 Subject: [PATCH 011/132] Skip timeout checks on slower running valgrind job --- .github/workflows/test-valgrind.yml | 2 +- Tests/test_file_pdf.py | 1 + Tests/test_image.py | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 219189cf2..f8b050f76 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -48,5 +48,5 @@ jobs: run: | # The Pillow user in the docker container is UID 1000 sudo chown -R 1000 $GITHUB_WORKSPACE - docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + 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/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 9667b6a4a..5299febe9 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -286,6 +286,7 @@ def test_pdf_append_to_bytesio(): @pytest.mark.timeout(1) +@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") @pytest.mark.parametrize("newline", (b"\r", b"\n")) def test_redos(newline): malicious = b" trailer<<>>" + newline * 3456 diff --git a/Tests/test_image.py b/Tests/test_image.py index 13c162812..890769fcd 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -512,8 +512,11 @@ class TestImage: i = Image.new("RGB", [1, 1]) assert isinstance(i.size, tuple) + @pytest.mark.timeout(0.75) + @pytest.mark.skipif( + "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" + ) @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) - @pytest.mark.timeout(0.5) def test_empty_image(self, size): Image.new("RGB", size) From 13306974e749871822dac413be66e699a0f4645e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 20:14:17 +1100 Subject: [PATCH 012/132] Updated copyright year --- LICENSE | 2 +- docs/COPYING | 2 +- docs/conf.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 40aabc323..616808a48 100644 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2022 by Alex Clark and contributors + Copyright © 2010-2023 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source HPND License: diff --git a/docs/COPYING b/docs/COPYING index 25f03b343..b400381d3 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2022 by Alex Clark and contributors + Copyright © 2010-2023 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/conf.py b/docs/conf.py index 04823e2d7..fb58d25ed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,7 +52,7 @@ master_doc = "index" # General information about the project. project = "Pillow (PIL Fork)" -copyright = "1995-2011 Fredrik Lundh, 2010-2022 Alex Clark and Contributors" +copyright = "1995-2011 Fredrik Lundh, 2010-2023 Alex Clark and Contributors" author = "Fredrik Lundh, Alex Clark and Contributors" # The version info for the project you're documenting, acts as replacement for From 7f1708415c23241ef86f2509418a2558e6990320 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 22:24:58 +1100 Subject: [PATCH 013/132] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index df24f562f..655089ab2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Return from ImagingFill early if image has a zero dimension #6842 + [radarhere] + - Reversed deprecations for Image constants, except for duplicate Resampling attributes #6830 [radarhere] From 87d1770c182bb3e19229f1a652cabedce29b891e Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Wed, 2 Nov 2022 23:11:57 +0100 Subject: [PATCH 014/132] Fix null pointer dereference crash with malformed font --- Tests/fonts/fuzz_font-5203009437302784 | 10 ++++++++++ Tests/test_font_crash.py | 21 +++++++++++++++++++++ src/_imagingft.c | 6 ++++++ 3 files changed, 37 insertions(+) create mode 100644 Tests/fonts/fuzz_font-5203009437302784 create mode 100644 Tests/test_font_crash.py diff --git a/Tests/fonts/fuzz_font-5203009437302784 b/Tests/fonts/fuzz_font-5203009437302784 new file mode 100644 index 000000000..0465e48c2 --- /dev/null +++ b/Tests/fonts/fuzz_font-5203009437302784 @@ -0,0 +1,10 @@ +STARTFONT +FONT +SIZE 10 +FONTBOUNDINGBOX +CHARS +STARTCHAR +ENCODING +BBX 2 5 +ENDCHAR +ENDFONT diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py new file mode 100644 index 000000000..020ddfcd9 --- /dev/null +++ b/Tests/test_font_crash.py @@ -0,0 +1,21 @@ +from PIL import Image, ImageDraw, ImageFont + +import pytest + +from .helper import skip_unless_feature + +class TestFontCrash: + def _fuzz_font(self, font): + # from fuzzers.fuzz_font + font.getbbox("ABC") + font.getmask("test text") + with Image.new(mode="RGBA", size=(200, 200)) as im: + draw = ImageDraw.Draw(im) + draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) + draw.text((10, 10), "Test Text", font=font, fill="#000") + + @skip_unless_feature("freetype2") + def test_segfault(self): + with pytest.raises(OSError): + font= ImageFont.truetype('Tests/fonts/fuzz_font-5203009437302784') + self._fuzz_font(font) diff --git a/src/_imagingft.c b/src/_imagingft.c index b52d6353e..319098897 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -921,6 +921,12 @@ font_render(FontObject *self, PyObject *args) { yy = -(py + glyph_slot->bitmap_top); } + // Null buffer, is dereferenced in FT_Bitmap_Convert + if (!bitmap.buffer && bitmap.rows) { + return geterror(0x9D); // Bitmap missing + goto glyph_error; + } + /* convert non-8bpp bitmaps */ switch (bitmap.pixel_mode) { case FT_PIXEL_MODE_MONO: From f2b36a1833f1b95d7a8d336432170cad091c6236 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Nov 2022 09:18:47 +1100 Subject: [PATCH 015/132] Lint fixes --- Tests/test_font_crash.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index 020ddfcd9..e8d612a7f 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -4,6 +4,7 @@ import pytest from .helper import skip_unless_feature + class TestFontCrash: def _fuzz_font(self, font): # from fuzzers.fuzz_font @@ -17,5 +18,5 @@ class TestFontCrash: @skip_unless_feature("freetype2") def test_segfault(self): with pytest.raises(OSError): - font= ImageFont.truetype('Tests/fonts/fuzz_font-5203009437302784') + font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") self._fuzz_font(font) From 1c57ab84294845a49b4f5dc2b2444a8eaff70110 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 3 Nov 2022 21:26:17 +0100 Subject: [PATCH 016/132] Return a PyError instead of a fake fterror. * Update Tests to IOError rather than OSError --- Tests/oss-fuzz/test_fuzzers.py | 4 +++- Tests/test_font_crash.py | 2 +- src/_imagingft.c | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 629e9ac00..1b0b1d3dc 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -57,6 +57,8 @@ def test_fuzz_fonts(path): with open(path, "rb") as f: try: fuzzers.fuzz_font(f.read()) - except (Image.DecompressionBombError, Image.DecompressionBombWarning): + except (Image.DecompressionBombError, + Image.DecompressionBombWarning, + IOError): pass assert True diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index e8d612a7f..9a2110c0c 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -17,6 +17,6 @@ class TestFontCrash: @skip_unless_feature("freetype2") def test_segfault(self): - with pytest.raises(OSError): + with pytest.raises(IOError): font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") self._fuzz_font(font) diff --git a/src/_imagingft.c b/src/_imagingft.c index 319098897..053ef1e7d 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -923,7 +923,7 @@ font_render(FontObject *self, PyObject *args) { // Null buffer, is dereferenced in FT_Bitmap_Convert if (!bitmap.buffer && bitmap.rows) { - return geterror(0x9D); // Bitmap missing + PyErr_SetString(PyExc_IOError, "Bitmap missing for glyph"); goto glyph_error; } From 51d95add6a306ea9dc1b0e2dacc202f69e4565e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Nov 2022 15:41:17 +1100 Subject: [PATCH 017/132] Replaced IOError with OSError --- Tests/oss-fuzz/test_fuzzers.py | 2 +- Tests/test_font_crash.py | 2 +- src/_imagingft.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 1b0b1d3dc..fb8f87e86 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -59,6 +59,6 @@ def test_fuzz_fonts(path): fuzzers.fuzz_font(f.read()) except (Image.DecompressionBombError, Image.DecompressionBombWarning, - IOError): + OSError): pass assert True diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index 9a2110c0c..e8d612a7f 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -17,6 +17,6 @@ class TestFontCrash: @skip_unless_feature("freetype2") def test_segfault(self): - with pytest.raises(IOError): + with pytest.raises(OSError): font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") self._fuzz_font(font) diff --git a/src/_imagingft.c b/src/_imagingft.c index 053ef1e7d..0db17a5a6 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -923,7 +923,7 @@ font_render(FontObject *self, PyObject *args) { // Null buffer, is dereferenced in FT_Bitmap_Convert if (!bitmap.buffer && bitmap.rows) { - PyErr_SetString(PyExc_IOError, "Bitmap missing for glyph"); + PyErr_SetString(PyExc_OSError, "Bitmap missing for glyph"); goto glyph_error; } From c977526cfeda89e86d0144f5f8dca06cd05dbef5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 17:45:09 +1100 Subject: [PATCH 018/132] Lint fixes --- Tests/oss-fuzz/test_fuzzers.py | 4 +--- Tests/test_font_crash.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index fb8f87e86..dc111c38b 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -57,8 +57,6 @@ def test_fuzz_fonts(path): with open(path, "rb") as f: try: fuzzers.fuzz_font(f.read()) - except (Image.DecompressionBombError, - Image.DecompressionBombWarning, - OSError): + except (Image.DecompressionBombError, Image.DecompressionBombWarning, OSError): pass assert True diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index e8d612a7f..27663f396 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -1,7 +1,7 @@ -from PIL import Image, ImageDraw, ImageFont - import pytest +from PIL import Image, ImageDraw, ImageFont + from .helper import skip_unless_feature From 009bbe25ecbcb14f4e238089f11915d01dfcf1b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 1 Jan 2023 23:26:00 +1100 Subject: [PATCH 019/132] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 655089ab2..e6a494674 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Fixed null pointer dereference crash with malformed font #6846 + [wiredfool, radarhere] + - Return from ImagingFill early if image has a zero dimension #6842 [radarhere] From a632b7a3e71a0122caa9be27fb0b1701ffb49e26 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 00:25:49 +1100 Subject: [PATCH 020/132] Added release notes for #6842 --- docs/releasenotes/9.4.0.rst | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 2b111d5e4..a0d26dc52 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -1,30 +1,6 @@ 9.4.0 ----- -Backwards Incompatible Changes -============================== - -TODO -^^^^ - -TODO - -Deprecations -============ - -TODO -^^^^ - -TODO - -API Changes -=========== - -TODO -^^^^ - -TODO - API Additions ============= @@ -96,10 +72,14 @@ When saving a JPEG image, a comment can now be written from Security ======== -TODO -^^^^ +Fix memory DOS in ImageFont +^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +A corrupt or specially crafted TTF font could have font metrics that lead to +unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not +check the image size before allocating memory for it. This dates to the PIL +fork. Pilllow 8.2.0 added a check for large sizes, but did not consider the +case where one dimension was zero. Other Changes ============= From 35b4c433b33da3fa1e9a3193809c3fd7ec58d042 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 00:32:35 +1100 Subject: [PATCH 021/132] Added release notes for #6846 --- docs/releasenotes/9.4.0.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index a0d26dc52..2d83b7bf5 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -81,6 +81,13 @@ check the image size before allocating memory for it. This dates to the PIL fork. Pilllow 8.2.0 added a check for large sizes, but did not consider the case where one dimension was zero. +Null pointer dereference crash in ImageFont +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow attempted to dereference a null pointer in ``ImageFont``, leading to a +crash. An error is now raised instead. This would have been present since +Pillow 8.0.0. + Other Changes ============= From e908afea40ec54c43954c9a70be78af670dfb442 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 2 Jan 2023 08:17:47 +1100 Subject: [PATCH 022/132] Updated security descriptions Co-authored-by: Hugo van Kemenade --- docs/releasenotes/9.4.0.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 2d83b7bf5..0af5bc8ca 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -78,14 +78,14 @@ Fix memory DOS in ImageFont A corrupt or specially crafted TTF font could have font metrics that lead to unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not check the image size before allocating memory for it. This dates to the PIL -fork. Pilllow 8.2.0 added a check for large sizes, but did not consider the -case where one dimension was zero. +fork. Pillow 8.2.0 added a check for large sizes, but did not consider the +case where one dimension is zero. Null pointer dereference crash in ImageFont ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow attempted to dereference a null pointer in ``ImageFont``, leading to a -crash. An error is now raised instead. This would have been present since +crash. An error is now raised instead. This has been present since Pillow 8.0.0. Other Changes From d4d981dc9ff923a099f0e5be95eb9a2449b74f35 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 08:41:50 +1100 Subject: [PATCH 023/132] Updated size parameter descriptions --- src/PIL/Image.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6c6b07d61..4e1c3a021 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1271,7 +1271,8 @@ class Image: currently implemented only for JPEG and MPO images. :param mode: The requested mode. - :param size: The requested size. + :param size: The requested size in pixels, as a 2-tuple: + (width, height). """ pass @@ -2551,7 +2552,8 @@ class Image: apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original image. - :param size: Requested size. + :param size: The requested size in pixels, as a 2-tuple: + (width, height). :param resample: Optional resampling filter. This can be one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, @@ -2638,7 +2640,8 @@ class Image: given size, and the same mode as the original, and copies data to the new image using the given transform. - :param size: The output size. + :param size: The output size in pixels, as a 2-tuple: + (width, height). :param method: The transformation method. This is one of :py:data:`Transform.EXTENT` (cut out a rectangular subregion), :py:data:`Transform.AFFINE` (affine transform), From a5bbab1c1e63b439de191ef2040173713b26d2da Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 10:29:07 +1100 Subject: [PATCH 024/132] 9.4.0 version bump --- CHANGES.rst | 2 +- src/PIL/_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e6a494674..7ec7b936d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,7 +2,7 @@ Changelog (Pillow) ================== -9.4.0 (unreleased) +9.4.0 (2023-01-02) ------------------ - Fixed null pointer dereference crash with malformed font #6846 diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 1cc1d0f1c..aca0aba02 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.4.0.dev0" +__version__ = "9.4.0" From 549560cf553d00c6e06a4c7e27fba56f0aba1c41 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 14:03:31 +1100 Subject: [PATCH 025/132] 9.5.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index aca0aba02..7baa9fb6c 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.4.0" +__version__ = "9.5.0.dev0" From 97385d7cc7e983c7c1a22bf777152f5ce1dc917c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 19:54:12 +1100 Subject: [PATCH 026/132] Relaxed child images check to allow for libjpeg --- Tests/test_file_jpeg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index eabc6bf75..fb8954125 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -447,7 +447,7 @@ class TestFileJpeg: ims = im.get_child_images() assert len(ims) == 1 - assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png") + assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1) def test_mp(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: From 6c30b2c00d061f5b634f8c10337eb8a7fe29192d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 21:03:45 +1100 Subject: [PATCH 027/132] arr.tobytes() always exists in Python >= 3.2 --- Tests/test_imagepath.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index de3920cf5..861fb64f0 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -58,10 +58,7 @@ def test_path(): assert list(p) == [(0.0, 1.0)] arr = array.array("f", [0, 1]) - if hasattr(arr, "tobytes"): - p = ImagePath.Path(arr.tobytes()) - else: - p = ImagePath.Path(arr.tostring()) + p = ImagePath.Path(arr.tobytes()) assert list(p) == [(0.0, 1.0)] From 9342f9a0e67cf5dacf5fee06b1c806a99a68e1fe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Jan 2023 18:34:37 +0000 Subject: [PATCH 028/132] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/isort: 5.11.1 → 5.11.4](https://github.com/PyCQA/isort/compare/5.11.1...5.11.4) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d019d3e7f..d790e7850 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: types: [] - repo: https://github.com/PyCQA/isort - rev: 5.11.1 + rev: 5.11.4 hooks: - id: isort From ea9a1b84aa5a8f6ab077974a052e54ff52ee2c50 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Jan 2023 14:09:45 +1100 Subject: [PATCH 029/132] LOAD_TRUNCATED_IMAGES may allow PNG images to open --- src/PIL/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index e65b155b2..4b76e893f 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -75,6 +75,10 @@ _plugins = [ class UnidentifiedImageError(OSError): """ Raised in :py:meth:`PIL.Image.open` if an image cannot be opened and identified. + + If a PNG image raises this error, setting :data:`.ImageFile.LOAD_TRUNCATED_IMAGES` + to true may allow the image to be opened after all. The setting will ignore missing + data and checksum failures. """ pass From e653aaee899e87b2b886251538c4eaa7b593e8b0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Jan 2023 00:43:48 +1100 Subject: [PATCH 030/132] NotImplementedError will not be raised if xclip is available --- Tests/test_imagegrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 317db4c01..fa88065f4 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -64,7 +64,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200 ) p.communicate() else: - if not shutil.which("wl-paste"): + if not shutil.which("wl-paste") and not shutil.which("xclip"): with pytest.raises( NotImplementedError, match="wl-paste or xclip is required for" From 2d6f9c16fcb6d5474833a301629f43818d5a8aac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Jan 2023 14:06:47 +1100 Subject: [PATCH 031/132] Announce releases on Fosstodon [ci skip] --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index b05067484..27c21be87 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -111,7 +111,7 @@ Released as needed privately to individual vendors for critical security-related ## Publicize Release -* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 +* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Fosstodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 ## Documentation From b6a9fccd87faf03d140030aa9f654af0ea10d520 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Jan 2023 14:44:54 +1100 Subject: [PATCH 032/132] Added Fosstodon badge [ci skip] --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 8ee68f9b8..9f549ece6 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,9 @@ As of 2019, Pillow development is Follow on https://twitter.com/PythonPillow + Follow on https://fosstodon.org/@pillow From fc84d6e37f1e9e2bff64f93c17e35eec28d9d01f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Jan 2023 14:59:10 +1100 Subject: [PATCH 033/132] Added Fosstodon URL to setup.cfg --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index b562e2934..2dc552a2c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,7 @@ project_urls = Release notes=https://pillow.readthedocs.io/en/stable/releasenotes/index.html Changelog=https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst Twitter=https://twitter.com/PythonPillow + Mastodon=https://fosstodon.org/@pillow [options] packages = PIL From e82f545ed0c0321556ab1bd2e924a5cb03fe6b27 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 5 Jan 2023 09:56:59 +1100 Subject: [PATCH 034/132] Refer to Mastodon [ci skip] Co-authored-by: Hugo van Kemenade --- README.md | 3 ++- RELEASING.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f549ece6..dd1d2c60f 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,8 @@ As of 2019, Pillow development is src="https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg"> Follow on https://fosstodon.org/@pillow + src="https://img.shields.io/badge/publish-on%20Mastodon-595aff" + rel="me"> diff --git a/RELEASING.md b/RELEASING.md index 27c21be87..c203a9c12 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -111,7 +111,7 @@ Released as needed privately to individual vendors for critical security-related ## Publicize Release -* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Fosstodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 +* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 ## Documentation From 0421b2f2a04e7ab17c866735c2a6b3257f65045b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 5 Jan 2023 10:43:01 +1100 Subject: [PATCH 035/132] Added social links to docs --- README.md | 2 +- docs/index.rst | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dd1d2c60f..489d3db54 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ As of 2019, Pillow development is src="https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg"> Follow on https://fosstodon.org/@pillow diff --git a/docs/index.rst b/docs/index.rst index 5bcd5afa5..674b31bd7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -73,6 +73,18 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Thu, 5 Jan 2023 00:04:50 +0000 Subject: [PATCH 036/132] Fix tcl/tk loading error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/Tk/tkImaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 506bb7008..6ad3aaba1 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -368,7 +368,7 @@ load_tkinter_funcs(void) { } else if (found_tk != 1) { 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 */ From da39e4e38e866e715eccf29feb23a444ab23f60b Mon Sep 17 00:00:00 2001 From: Javier Dehesa Date: Thu, 5 Jan 2023 00:05:36 +0000 Subject: [PATCH 037/132] Fix tcl/tk loading error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/Tk/tkImaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 6ad3aaba1..8225756ba 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -365,7 +365,7 @@ load_tkinter_funcs(void) { free(hMods); if (found_tcl != 1) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tcl routines"); - } else if (found_tk != 1) { + } else if (found_tk == 0) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines"); } return (int) ((found_tcl != 1) || (found_tk != 1)); From 2cc40cc7f4a7d51b29c66c0da4d785a5d5920c3c Mon Sep 17 00:00:00 2001 From: Javier Dehesa Date: Thu, 5 Jan 2023 00:05:54 +0000 Subject: [PATCH 038/132] Fix tcl/tk loading error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/Tk/tkImaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 8225756ba..ad503baec 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -363,7 +363,7 @@ load_tkinter_funcs(void) { } free(hMods); - if (found_tcl != 1) { + if (found_tcl == 0) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tcl routines"); } else if (found_tk == 0) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines"); From d3d7566d9a4d63415fa4fc76864f95451f289725 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Jan 2023 11:27:43 +1100 Subject: [PATCH 039/132] Refer to Resampling enum --- docs/handbook/concepts.rst | 50 ++++++++++++------------ docs/reference/Image.rst | 1 + docs/releasenotes/2.7.0.rst | 76 +++++++++++++++++-------------------- src/PIL/ImageOps.py | 12 ++++-- 4 files changed, 69 insertions(+), 70 deletions(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 01f75e9a3..45c662bd6 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -148,44 +148,44 @@ pixel, the Python Imaging Library provides different resampling *filters*. .. py:currentmodule:: PIL.Image -.. data:: NEAREST +.. data:: Resampling.NEAREST Pick one nearest pixel from the input image. Ignore all other input pixels. -.. data:: BOX +.. data:: Resampling.BOX Each pixel of source image contributes to one pixel of the destination image with identical weights. - For upscaling is equivalent of :data:`NEAREST`. + For upscaling is equivalent of :data:`Resampling.NEAREST`. This filter can only be used with the :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` methods. .. versionadded:: 3.4.0 -.. data:: BILINEAR +.. data:: Resampling.BILINEAR For resize calculate the output pixel value using linear interpolation on all pixels that may contribute to the output value. For other transformations linear interpolation over a 2x2 environment in the input image is used. -.. data:: HAMMING +.. data:: Resampling.HAMMING - Produces a sharper image than :data:`BILINEAR`, doesn't have dislocations - on local level like with :data:`BOX`. + Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have + dislocations on local level like with :data:`Resampling.BOX`. This filter can only be used with the :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` methods. .. versionadded:: 3.4.0 -.. data:: BICUBIC +.. data:: Resampling.BICUBIC For resize calculate the output pixel value using cubic interpolation on all pixels that may contribute to the output value. For other transformations cubic interpolation over a 4x4 environment in the input image is used. -.. data:: LANCZOS +.. data:: Resampling.LANCZOS Calculate the output pixel value using a high-quality Lanczos filter (a truncated sinc) on all pixels that may contribute to the output value. @@ -198,19 +198,19 @@ pixel, the Python Imaging Library provides different resampling *filters*. Filters comparison table ~~~~~~~~~~~~~~~~~~~~~~~~ -+----------------+-------------+-----------+-------------+ -| Filter | Downscaling | Upscaling | Performance | -| | quality | quality | | -+================+=============+===========+=============+ -|:data:`NEAREST` | | | ⭐⭐⭐⭐⭐ | -+----------------+-------------+-----------+-------------+ -|:data:`BOX` | ⭐ | | ⭐⭐⭐⭐ | -+----------------+-------------+-----------+-------------+ -|:data:`BILINEAR`| ⭐ | ⭐ | ⭐⭐⭐ | -+----------------+-------------+-----------+-------------+ -|:data:`HAMMING` | ⭐⭐ | | ⭐⭐⭐ | -+----------------+-------------+-----------+-------------+ -|:data:`BICUBIC` | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | -+----------------+-------------+-----------+-------------+ -|:data:`LANCZOS` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | -+----------------+-------------+-----------+-------------+ ++---------------------------+-------------+-----------+-------------+ +| Filter | Downscaling | Upscaling | Performance | +| | quality | quality | | ++===========================+=============+===========+=============+ +|:data:`Resampling.NEAREST` | | | ⭐⭐⭐⭐⭐ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.BOX` | ⭐ | | ⭐⭐⭐⭐ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.BILINEAR`| ⭐ | ⭐ | ⭐⭐⭐ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.HAMMING` | ⭐⭐ | | ⭐⭐⭐ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.BICUBIC` | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.LANCZOS` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | ++---------------------------+-------------+-----------+-------------+ diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 7f6f666c3..ad0abbbd9 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -430,6 +430,7 @@ See :ref:`concept-filters` for details. .. autoclass:: Resampling :members: :undoc-members: + :noindex: Some deprecated filters are also available under the following names: diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index dda814c1f..0b3eeeb49 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -29,84 +29,78 @@ Image resizing filters Image resizing methods :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` take a ``resample`` argument, which tells which filter should be used for resampling. Possible values are: -:py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BILINEAR`, -:py:data:`PIL.Image.BICUBIC` and :py:data:`PIL.Image.ANTIALIAS`. -Almost all of them were changed in this version. +``NEAREST``, ``BILINEAR``, ``BICUBIC`` and ``ANTIALIAS``. Almost all of them +were changed in this version. Bicubic and bilinear downscaling ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -From the beginning :py:data:`~PIL.Image.BILINEAR` and -:py:data:`~PIL.Image.BICUBIC` filters were based on affine transformations -and used a fixed number of pixels from the source image for every destination -pixel (2x2 pixels for :py:data:`~PIL.Image.BILINEAR` and 4x4 for -:py:data:`~PIL.Image.BICUBIC`). This gave an unsatisfactory result for -downscaling. At the same time, a high quality convolutions-based algorithm with -flexible kernel was used for :py:data:`~PIL.Image.ANTIALIAS` filter. +From the beginning ``BILINEAR`` and ``BICUBIC`` filters were based on affine +transformations and used a fixed number of pixels from the source image for +every destination pixel (2x2 pixels for ``BILINEAR`` and 4x4 for ``BICUBIC``). +This gave an unsatisfactory result for downscaling. At the same time, a high +quality convolutions-based algorithm with flexible kernel was used for +``ANTIALIAS`` filter. Starting from Pillow 2.7.0, a high quality convolutions-based algorithm is used for all of these three filters. If you have previously used any tricks to maintain quality when downscaling with -:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` filters -(for example, reducing within several steps), they are unnecessary now. +``BILINEAR`` and ``BICUBIC`` filters (for example, reducing within several +steps), they are unnecessary now. Antialias renamed to Lanczos ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A new :py:data:`PIL.Image.LANCZOS` constant was added instead of -:py:data:`~PIL.Image.ANTIALIAS`. +A new ``LANCZOS`` constant was added instead of ``ANTIALIAS``. -When :py:data:`~PIL.Image.ANTIALIAS` was initially added, it was the only -high-quality filter based on convolutions. It's name was supposed to reflect -this. Starting from Pillow 2.7.0 all resize method are based on convolutions. -All of them are antialias from now on. And the real name of the -:py:data:`~PIL.Image.ANTIALIAS` filter is Lanczos filter. +When ``ANTIALIAS`` was initially added, it was the only high-quality filter +based on convolutions. It's name was supposed to reflect this. Starting from +Pillow 2.7.0 all resize method are based on convolutions. All of them are +antialias from now on. And the real name of the ``ANTIALIAS`` filter is Lanczos +filter. -The :py:data:`~PIL.Image.ANTIALIAS` constant is left for backward compatibility -and is an alias for :py:data:`~PIL.Image.LANCZOS`. +The ``ANTIALIAS`` constant is left for backward compatibility and is an alias +for ``LANCZOS``. Lanczos upscaling quality ^^^^^^^^^^^^^^^^^^^^^^^^^ -The image upscaling quality with :py:data:`~PIL.Image.LANCZOS` filter was -almost the same as :py:data:`~PIL.Image.BILINEAR` due to bug. This has been fixed. +The image upscaling quality with ``LANCZOS`` filter was almost the same as +``BILINEAR`` due to a bug. This has been fixed. Bicubic upscaling quality ^^^^^^^^^^^^^^^^^^^^^^^^^ -The :py:data:`~PIL.Image.BICUBIC` filter for affine transformations produced -sharp, slightly pixelated image for upscaling. Bicubic for convolutions is -more soft. +The ``BICUBIC`` filter for affine transformations produced sharp, slightly +pixelated image for upscaling. Bicubic for convolutions is more soft. Resize performance ^^^^^^^^^^^^^^^^^^ In most cases, convolution is more a expensive algorithm for downscaling because it takes into account all the pixels of source image. Therefore -:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` filters' -performance can be lower than before. On the other hand the quality of -:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` was close to -:py:data:`~PIL.Image.NEAREST`. So if such quality is suitable for your tasks -you can switch to :py:data:`~PIL.Image.NEAREST` filter for downscaling, -which will give a huge improvement in performance. +``BILINEAR`` and ``BICUBIC`` filters' performance can be lower than before. +On the other hand the quality of ``BILINEAR`` and ``BICUBIC`` was close to +``NEAREST``. So if such quality is suitable for your tasks you can switch to +``NEAREST`` filter for downscaling, which will give a huge improvement in +performance. At the same time performance of convolution resampling for downscaling has been improved by around a factor of two compared to the previous version. -The upscaling performance of the :py:data:`~PIL.Image.LANCZOS` filter has -remained the same. For :py:data:`~PIL.Image.BILINEAR` filter it has improved by -1.5 times and for :py:data:`~PIL.Image.BICUBIC` by four times. +The upscaling performance of the ``LANCZOS`` filter has remained the same. For +``BILINEAR`` filter it has improved by 1.5 times and for ``BICUBIC`` by four +times. Default filter for thumbnails ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In Pillow 2.5 the default filter for :py:meth:`~PIL.Image.Image.thumbnail` was -changed from :py:data:`~PIL.Image.NEAREST` to :py:data:`~PIL.Image.ANTIALIAS`. -Antialias was chosen because all the other filters gave poor quality for -reduction. Starting from Pillow 2.7.0, :py:data:`~PIL.Image.ANTIALIAS` has been -replaced with :py:data:`~PIL.Image.BICUBIC`, because it's faster and -:py:data:`~PIL.Image.ANTIALIAS` doesn't give any advantages after -downscaling with libjpeg, which uses supersampling internally, not convolutions. +changed from ``NEAREST`` to ``ANTIALIAS``. Antialias was chosen because all the +other filters gave poor quality for reduction. Starting from Pillow 2.7.0, +``ANTIALIAS`` has been replaced with ``BICUBIC``, because it's faster and +``ANTIALIAS`` doesn't give any advantages after downscaling with libjpeg, which +uses supersampling internally, not convolutions. Image transposition ------------------- diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index e2168ce62..e7719fcf9 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -248,7 +248,8 @@ def contain(image, size, method=Image.Resampling.BICUBIC): :param size: The requested output size in pixels, given as a (width, height) tuple. :param method: Resampling method to use. Default is - :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. + :py:attr:`PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. :return: An image. """ @@ -276,7 +277,8 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 :param size: The requested output size in pixels, given as a (width, height) tuple. :param method: Resampling method to use. Default is - :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. + :py:attr:`PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. :param color: The background color of the padded image. :param centering: Control the position of the original image within the padded version. @@ -328,7 +330,8 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC): :param image: The image to rescale. :param factor: The expansion factor, as a float. :param resample: Resampling method to use. Default is - :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. + :py:attr:`PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. :returns: An :py:class:`~PIL.Image.Image` object. """ if factor == 1: @@ -425,7 +428,8 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, :param size: The requested output size in pixels, given as a (width, height) tuple. :param method: Resampling method to use. Default is - :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. + :py:attr:`PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. :param bleed: Remove a border around the outside of the image from all four edges. The value is a decimal percentage (use 0.01 for one percent). The default value is 0 (no border). From 86634b835257a7d3438eaf783d70eca12e20b13e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Jan 2023 17:53:21 +1100 Subject: [PATCH 040/132] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7ec7b936d..9a267bc9c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ Changelog (Pillow) ================== +9.5.0 (unreleased) +------------------ + +- Support arbitrary number of loaded modules on Windows #6761 + [javidcf, radarhere, nulano] + 9.4.0 (2023-01-02) ------------------ From 52ed578947c8715aeaa83f34d299c73cb4db74ac Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 8 Oct 2022 17:14:11 -0500 Subject: [PATCH 041/132] add extra variable so linter doesn't split line --- Tests/test_pdfparser.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index ea9b33dfc..43e244c7b 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -88,9 +88,8 @@ def test_parsing(): b"D:20180729214124+08'00'": "20180729134124", b"D:20180729214124-05'00'": "20180730024124", }.items(): - d = PdfParser.get_value(b"<>", 0)[ - 0 - ] + b = b"<>" + d = PdfParser.get_value(b, 0)[0] assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value From 4e6e69aafb9b0aabb98c42aab030b5b2254d302a Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 9 Oct 2022 02:55:01 -0500 Subject: [PATCH 042/132] remove loop left from before parametrization --- Tests/test_image_resample.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 53ceb6df0..be49955dd 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -135,16 +135,15 @@ class TestImagingCoreResampleAccuracy: @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) def test_reduce_bicubic(self, mode): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (12, 12), 0xE1) - case = case.resize((6, 6), Image.Resampling.BICUBIC) - # fmt: off - data = ("e1 e3 d4" - "e3 e5 d6" - "d4 d6 c9") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (6, 6))) + case = self.make_case(mode, (12, 12), 0xE1) + case = case.resize((6, 6), Image.Resampling.BICUBIC) + # fmt: off + data = ("e1 e3 d4" + "e3 e5 d6" + "d4 d6 c9") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (6, 6))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) def test_reduce_lanczos(self, mode): From 48b6d4fd60d2f792ae7ee203aea686effd9617fd Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 9 Oct 2022 02:57:15 -0500 Subject: [PATCH 043/132] remove no-format tags and fix comment locations --- Tests/test_image_transform.py | 50 +++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index a78349801..7411f0b78 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -42,12 +42,12 @@ class TestImageTransform: def test_extent(self): im = hopper("RGB") (w, h) = im.size - # fmt: off - transformed = im.transform(im.size, Image.Transform.EXTENT, - (0, 0, - w//2, h//2), # ul -> lr - Image.Resampling.BILINEAR) - # fmt: on + transformed = im.transform( + im.size, + Image.Transform.EXTENT, + (0, 0, w // 2, h // 2), # ul -> lr + Image.Resampling.BILINEAR, + ) scaled = im.resize((w * 2, h * 2), Image.Resampling.BILINEAR).crop((0, 0, w, h)) @@ -58,13 +58,12 @@ class TestImageTransform: # one simple quad transform, equivalent to scale & crop upper left quad im = hopper("RGB") (w, h) = im.size - # fmt: off - transformed = im.transform(im.size, Image.Transform.QUAD, - (0, 0, 0, h//2, - # ul -> ccw around quad: - w//2, h//2, w//2, 0), - Image.Resampling.BILINEAR) - # fmt: on + transformed = im.transform( + im.size, + Image.Transform.QUAD, + (0, 0, 0, h // 2, w // 2, h // 2, w // 2, 0), # ul -> ccw around quad + Image.Resampling.BILINEAR, + ) scaled = im.transform( (w, h), @@ -99,16 +98,21 @@ class TestImageTransform: # this should be a checkerboard of halfsized hoppers in ul, lr im = hopper("RGBA") (w, h) = im.size - # fmt: off - transformed = im.transform(im.size, Image.Transform.MESH, - [((0, 0, w//2, h//2), # box - (0, 0, 0, h, - w, h, w, 0)), # ul -> ccw around quad - ((w//2, h//2, w, h), # box - (0, 0, 0, h, - w, h, w, 0))], # ul -> ccw around quad - Image.Resampling.BILINEAR) - # fmt: on + transformed = im.transform( + im.size, + Image.Transform.MESH, + ( + ( + (0, 0, w // 2, h // 2), # box + (0, 0, 0, h, w, h, w, 0), # ul -> ccw around quad + ), + ( + (w // 2, h // 2, w, h), # box + (0, 0, 0, h, w, h, w, 0), # ul -> ccw around quad + ), + ), + Image.Resampling.BILINEAR, + ) scaled = im.transform( (w // 2, h // 2), From 04199b6066aedbdef961008fadcc726d0546a0e1 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 9 Oct 2022 02:58:14 -0500 Subject: [PATCH 044/132] sort colors before comparing them --- Tests/test_image_transform.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 7411f0b78..64a5c9459 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -178,11 +178,13 @@ class TestImageTransform: im = op(im, (40, 10)) - colors = im.getcolors() - assert colors == [ - (20 * 10, opaque), - (20 * 10, transparent), - ] + colors = sorted(im.getcolors()) + assert colors == sorted( + ( + (20 * 10, opaque), + (20 * 10, transparent), + ) + ) @pytest.mark.parametrize("mode", ("RGBA", "LA")) def test_nearest_resize(self, mode): From 246f6fa46a387e554cb608c87d6086b8cd368e36 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 8 Jan 2023 04:45:16 +1100 Subject: [PATCH 045/132] Simply attribute reference Co-authored-by: Hugo van Kemenade --- src/PIL/ImageOps.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index e7719fcf9..16c83f4e4 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -248,7 +248,7 @@ def contain(image, size, method=Image.Resampling.BICUBIC): :param size: The requested output size in pixels, given as a (width, height) tuple. :param method: Resampling method to use. Default is - :py:attr:`PIL.Image.Resampling.BICUBIC`. + :py:attr:`~PIL.Image.Resampling.BICUBIC`. See :ref:`concept-filters`. :return: An image. """ @@ -277,7 +277,7 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 :param size: The requested output size in pixels, given as a (width, height) tuple. :param method: Resampling method to use. Default is - :py:attr:`PIL.Image.Resampling.BICUBIC`. + :py:attr:`~PIL.Image.Resampling.BICUBIC`. See :ref:`concept-filters`. :param color: The background color of the padded image. :param centering: Control the position of the original image within the @@ -330,7 +330,7 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC): :param image: The image to rescale. :param factor: The expansion factor, as a float. :param resample: Resampling method to use. Default is - :py:attr:`PIL.Image.Resampling.BICUBIC`. + :py:attr:`~PIL.Image.Resampling.BICUBIC`. See :ref:`concept-filters`. :returns: An :py:class:`~PIL.Image.Image` object. """ @@ -428,7 +428,7 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, :param size: The requested output size in pixels, given as a (width, height) tuple. :param method: Resampling method to use. Default is - :py:attr:`PIL.Image.Resampling.BICUBIC`. + :py:attr:`~PIL.Image.Resampling.BICUBIC`. See :ref:`concept-filters`. :param bleed: Remove a border around the outside of the image from all four edges. The value is a decimal percentage (use 0.01 for From b2b8c833aaf2778defea86b99e3c12c1198ffd64 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 7 Jan 2023 20:25:50 +0200 Subject: [PATCH 046/132] Use single isinstance call for multiple types --- src/PIL/PdfParser.py | 4 +--- winbuild/build_prepare.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index aa5ea2fbb..1b3cb52a2 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -328,9 +328,7 @@ def pdf_repr(x): return b"null" elif isinstance(x, (PdfName, PdfDict, PdfArray, PdfBinary)): return bytes(x) - elif isinstance(x, int): - return str(x).encode("us-ascii") - elif isinstance(x, float): + elif isinstance(x, (int, float)): return str(x).encode("us-ascii") elif isinstance(x, time.struct_time): return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")" diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f5050946c..df39260e0 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -39,7 +39,7 @@ def cmd_rmdir(path): def cmd_nmake(makefile=None, target="", params=None): if params is None: params = "" - elif isinstance(params, list) or isinstance(params, tuple): + elif isinstance(params, (list, tuple)): params = " ".join(params) else: params = str(params) @@ -58,7 +58,7 @@ def cmd_nmake(makefile=None, target="", params=None): def cmd_cmake(params=None, file="."): if params is None: params = "" - elif isinstance(params, list) or isinstance(params, tuple): + elif isinstance(params, (list, tuple)): params = " ".join(params) else: params = str(params) From 2df4865e427a7d4dfc288ffe87d0b40c402b1375 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 7 Jan 2023 20:36:17 +0200 Subject: [PATCH 047/132] Use 'key in mydict' instead of 'key in mydict.keys()' --- Tests/test_file_png.py | 2 +- src/PIL/GifImagePlugin.py | 2 +- src/PIL/Image.py | 2 +- src/PIL/TiffImagePlugin.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 9481cd5dd..133f3e47e 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -593,7 +593,7 @@ class TestFilePng: def test_textual_chunks_after_idat(self): with Image.open("Tests/images/hopper.png") as im: - assert "comment" in im.text.keys() + assert "comment" in im.text for k, v in { "date:create": "2014-09-04T09:37:08+03:00", "date:modify": "2014-09-04T09:37:08+03:00", diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index d01315b20..6ee1bd3d8 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -487,7 +487,7 @@ def _normalize_mode(im): if Image.getmodebase(im.mode) == "RGB": im = im.convert("P", palette=Image.Palette.ADAPTIVE) if im.palette.mode == "RGBA": - for rgba in im.palette.colors.keys(): + for rgba in im.palette.colors: if rgba[3] == 0: im.info["transparency"] = im.palette.colors[rgba] break diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4e1c3a021..b0ff5173c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3841,7 +3841,7 @@ class Exif(MutableMapping): def __str__(self): if self._info is not None: # Load all keys into self._data - for tag in self._info.keys(): + for tag in self._info: self[tag] return str(self._data) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 431edfd9b..431a95701 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -257,7 +257,7 @@ OPEN_INFO = { (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), } -MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO.keys()) +MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) PREFIXES = [ b"MM\x00\x2A", # Valid TIFF header with big-endian byte order @@ -1222,7 +1222,7 @@ class TiffImageFile(ImageFile.ImageFile): # load IFD data from fp before it is closed exif = self.getexif() - for key in TiffTags.TAGS_V2_GROUPS.keys(): + for key in TiffTags.TAGS_V2_GROUPS: if key not in exif: continue exif.get_ifd(key) @@ -1629,7 +1629,7 @@ def _save(im, fp, filename): if isinstance(info, ImageFileDirectory_v1): info = info.to_v2() for key in info: - if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS.keys(): + if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS: ifd[key] = info.get_ifd(key) else: ifd[key] = info.get(key) From 8d5eb71d267c7e740abe07aa9f34277d47fb5ad6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 7 Jan 2023 20:45:55 +0200 Subject: [PATCH 048/132] Use enumerate --- Tests/test_file_mpo.py | 4 +--- src/PIL/PsdImagePlugin.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 3e5476222..f0dedc2de 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -168,8 +168,7 @@ def test_mp_no_data(): def test_mp_attribute(test_file): with Image.open(test_file) as im: mpinfo = im._getmp() - frame_number = 0 - for mpentry in mpinfo[0xB002]: + for frame_number, mpentry in enumerate(mpinfo[0xB002]): mpattr = mpentry["Attribute"] if frame_number: assert not mpattr["RepresentativeImageFlag"] @@ -180,7 +179,6 @@ def test_mp_attribute(test_file): assert mpattr["ImageDataFormat"] == "JPEG" assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" assert mpattr["Reserved"] == 0 - frame_number += 1 @pytest.mark.parametrize("test_file", test_files) diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index c1ca30a03..7e8d12759 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -238,15 +238,13 @@ def _layerinfo(fp, ct_bytes): layers.append((name, mode, (x0, y0, x1, y1))) # get tiles - i = 0 - for name, mode, bbox in layers: + for i, (name, mode, bbox) in enumerate(layers): tile = [] for m in mode: t = _maketile(fp, m, bbox, 1) if t: tile.extend(t) layers[i] = name, mode, bbox, tile - i += 1 return layers From a5e046fb4964f62837c229b9487fefe5758d1e54 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 8 Jan 2023 14:37:46 +0200 Subject: [PATCH 049/132] Convert test_properties to use parametrize --- Tests/test_image_mode.py | 44 ++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 670b2f4eb..6de2566b2 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -1,3 +1,5 @@ +import pytest + from PIL import Image, ImageMode from .helper import hopper @@ -49,23 +51,25 @@ def test_sanity(): assert m.typestr == "|u1" -def test_properties(): - def check(mode, *result): - signature = ( - Image.getmodebase(mode), - Image.getmodetype(mode), - Image.getmodebands(mode), - Image.getmodebandnames(mode), - ) - assert signature == result - - check("1", "L", "L", 1, ("1",)) - check("L", "L", "L", 1, ("L",)) - check("P", "P", "L", 1, ("P",)) - check("I", "L", "I", 1, ("I",)) - check("F", "L", "F", 1, ("F",)) - check("RGB", "RGB", "L", 3, ("R", "G", "B")) - check("RGBA", "RGB", "L", 4, ("R", "G", "B", "A")) - check("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")) - check("CMYK", "RGB", "L", 4, ("C", "M", "Y", "K")) - check("YCbCr", "RGB", "L", 3, ("Y", "Cb", "Cr")) +@pytest.mark.parametrize( + "mode, expected_base, expected_type, expected_bands, expected_band_names", + ( + ("1", "L", "L", 1, ("1",)), + ("L", "L", "L", 1, ("L",)), + ("P", "P", "L", 1, ("P",)), + ("I", "L", "I", 1, ("I",)), + ("F", "L", "F", 1, ("F",)), + ("RGB", "RGB", "L", 3, ("R", "G", "B")), + ("RGBA", "RGB", "L", 4, ("R", "G", "B", "A")), + ("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")), + ("CMYK", "RGB", "L", 4, ("C", "M", "Y", "K")), + ("YCbCr", "RGB", "L", 3, ("Y", "Cb", "Cr")), + ), +) +def test_properties( + mode, expected_base, expected_type, expected_bands, expected_band_names +): + assert Image.getmodebase(mode) == expected_base + assert Image.getmodetype(mode) == expected_type + assert Image.getmodebands(mode) == expected_bands + assert Image.getmodebandnames(mode) == expected_band_names From e24dd745f7386f52d7a617a9cb5c61fbd1d0ade0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 8 Jan 2023 14:48:56 +0200 Subject: [PATCH 050/132] Convert test_optimize_correctness to use parametrize --- Tests/test_file_gif.py | 63 ++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index d48fc1442..6fbc0ee30 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -158,39 +158,42 @@ def test_optimize(): assert test_bilevel(1) == 799 -def test_optimize_correctness(): - # 256 color Palette image, posterize to > 128 and < 128 levels - # Size bigger and smaller than 512x512 +@pytest.mark.parametrize( + "colors, size, expected_palette_length", + ( + # These do optimize the palette + (256, 511, 256), + (255, 511, 255), + (129, 511, 129), + (128, 511, 128), + (64, 511, 64), + (4, 511, 4), + # These don't optimize the palette + (128, 513, 256), + (64, 513, 256), + (4, 513, 256), + ), +) +def test_optimize_correctness(colors, size, expected_palette_length): + # 256 color Palette image, posterize to > 128 and < 128 levels. + # Size bigger and smaller than 512x512. # Check the palette for number of colors allocated. - # Check for correctness after conversion back to RGB - def check(colors, size, expected_palette_length): - # make an image with empty colors in the start of the palette range - im = Image.frombytes( - "P", (colors, colors), bytes(range(256 - colors, 256)) * colors - ) - im = im.resize((size, size)) - outfile = BytesIO() - im.save(outfile, "GIF") - outfile.seek(0) - with Image.open(outfile) as reloaded: - # check palette length - palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v) - assert expected_palette_length == palette_length + # Check for correctness after conversion back to RGB. - assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) + # make an image with empty colors in the start of the palette range + im = Image.frombytes( + "P", (colors, colors), bytes(range(256 - colors, 256)) * colors + ) + im = im.resize((size, size)) + outfile = BytesIO() + im.save(outfile, "GIF") + outfile.seek(0) + with Image.open(outfile) as reloaded: + # check palette length + palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v) + assert expected_palette_length == palette_length - # These do optimize the palette - check(256, 511, 256) - check(255, 511, 255) - check(129, 511, 129) - check(128, 511, 128) - check(64, 511, 64) - check(4, 511, 4) - - # These don't optimize the palette - check(128, 513, 256) - check(64, 513, 256) - check(4, 513, 256) + assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) def test_optimize_full_l(): From 08c7b17e236d3e7a431ff40f862ca4de2a5c67df Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 9 Jan 2023 19:04:55 +0200 Subject: [PATCH 051/132] Raise ValueError for BoxBlur filter with negative radius --- Tests/test_image_filter.py | 6 ++++++ src/PIL/ImageFilter.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index cfe46b658..ece98f73d 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -24,6 +24,7 @@ from .helper import assert_image_equal, hopper ImageFilter.ModeFilter, ImageFilter.GaussianBlur, ImageFilter.GaussianBlur(5), + ImageFilter.BoxBlur(0), ImageFilter.BoxBlur(5), ImageFilter.UnsharpMask, ImageFilter.UnsharpMask(10), @@ -173,3 +174,8 @@ def test_consistency_5x5(mode): Image.merge(mode, source[: len(mode)]).filter(kernel), Image.merge(mode, reference[: len(mode)]), ) + + +def test_invalid_box_blur_filter(): + with pytest.raises(ValueError): + ImageFilter.BoxBlur(-2) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 59e2c18b9..63d6dcf5c 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -183,6 +183,9 @@ class BoxBlur(MultibandFilter): name = "BoxBlur" def __init__(self, radius): + if radius < 0: + msg = "radius must be >= 0" + raise ValueError(msg) self.radius = radius def filter(self, image): From 07a3aef3ef93cd35d102e1176eda97df9b3eb5a6 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 9 Jan 2023 20:46:07 +0100 Subject: [PATCH 052/132] list `--{dis,en}able-raqm` options in installation documentation --- docs/installation.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 42fe8c254..2a83ed151 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -369,21 +369,21 @@ Build Options available, as many as are present. * Build flags: ``--disable-zlib``, ``--disable-jpeg``, - ``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``, - ``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``, - ``--disable-imagequant``, ``--disable-xcb``. + ``--disable-tiff``, ``--disable-freetype``, ``--disable-raqm``, + ``--disable-lcms``, ``--disable-webp``, ``--disable-webpmux``, + ``--disable-jpeg2000``, ``--disable-imagequant``, ``--disable-xcb``. Disable building the corresponding feature even if the development libraries are present on the building machine. * Build flags: ``--enable-zlib``, ``--enable-jpeg``, - ``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``, - ``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``, - ``--enable-imagequant``, ``--enable-xcb``. + ``--enable-tiff``, ``--enable-freetype``, ``--enable-raqm``, + ``--enable-lcms``, ``--enable-webp``, ``--enable-webpmux``, + ``--enable-jpeg2000``, ``--enable-imagequant``, ``--enable-xcb``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP support. Tcl and Tk also must be used together. -* Build flags: ``--vendor-raqm --vendor-fribidi`` +* Build flags: ``--vendor-raqm``, ``--vendor-fribidi``. These flags are used to compile a modified version of libraqm and a shim that dynamically loads libfribidi at runtime. These are used to compile the standard Pillow wheels. Compiling libraqm requires From 7f57c93b89804fd6468a264cac0350403a2a097b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 11 Jan 2023 08:50:20 +1100 Subject: [PATCH 053/132] Only read when necessary --- src/PIL/EpsImagePlugin.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 016e3c135..f7d376364 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -316,21 +316,22 @@ class EpsImageFile(ImageFile.ImageFile): def _find_offset(self, fp): - s = fp.read(160) + s = fp.read(4) - if s[:4] == b"%!PS": + if s == b"%!PS": # for HEAD without binary preview fp.seek(0, io.SEEK_END) length = fp.tell() offset = 0 - elif i32(s, 0) == 0xC6D3D0C5: + elif i32(s) == 0xC6D3D0C5: # FIX for: Some EPS file not handled correctly / issue #302 # EPS can contain binary data # or start directly with latin coding # more info see: # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf - offset = i32(s, 4) - length = i32(s, 8) + s = fp.read(8) + offset = i32(s) + length = i32(s, 4) else: msg = "not an EPS file" raise SyntaxError(msg) From 173b65d0956e0e5f15c52b0bb46c6694446eace5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 11 Jan 2023 20:02:10 +1100 Subject: [PATCH 054/132] Raise ValueError during filter operation as well --- Tests/test_image_filter.py | 6 ++++++ src/libImaging/BoxBlur.c | 3 +++ 2 files changed, 9 insertions(+) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index ece98f73d..a2ef2280b 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -179,3 +179,9 @@ def test_consistency_5x5(mode): def test_invalid_box_blur_filter(): with pytest.raises(ValueError): ImageFilter.BoxBlur(-2) + + im = hopper() + box_blur_filter = ImageFilter.BoxBlur(2) + box_blur_filter.radius = -2 + with pytest.raises(ValueError): + im.filter(box_blur_filter) diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 2e45a3358..5afe7cf50 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -237,6 +237,9 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) { if (n < 1) { return ImagingError_ValueError("number of passes must be greater than zero"); } + if (radius < 0) { + return ImagingError_ValueError("radius must be >= 0"); + } if (strcmp(imIn->mode, imOut->mode) || imIn->type != imOut->type || imIn->bands != imOut->bands || imIn->xsize != imOut->xsize || From 5a2369fc33818aa85131862ad881cc1135252cfd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 11 Jan 2023 17:18:02 +0200 Subject: [PATCH 055/132] Verify the Mastodon docs link --- docs/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 674b31bd7..a4663bac8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -85,6 +85,10 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more + Overview ======== From 335cde81b4f813c25bd830fa7cfe2663502bb616 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Jan 2023 08:41:14 +1100 Subject: [PATCH 056/132] Updated xz to 5.4.1 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index df39260e0..a34e8b342 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ deps = { "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.0.tar.gz/download", - "filename": "xz-5.4.0.tar.gz", - "dir": "xz-5.4.0", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.1.tar.gz/download", + "filename": "xz-5.4.1.tar.gz", + "dir": "xz-5.4.1", "license": "COPYING", "patch": { r"src\liblzma\api\lzma.h": { From 9e4aa4e1cb817ab4b63efdbfbe7426dee2741e67 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Jan 2023 09:21:25 +1100 Subject: [PATCH 057/132] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9a267bc9c..bf3017ca9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Raise ValueError for BoxBlur filter with negative radius #6874 + [hugovk, radarhere] + - Support arbitrary number of loaded modules on Windows #6761 [javidcf, radarhere, nulano] From a75a1a95142ddfac63d1a07506362083fd8faa71 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Jan 2023 11:49:08 +1100 Subject: [PATCH 058/132] Updated raqm to 0.10.0 --- depends/install_raqm.sh | 2 +- src/thirdparty/raqm/README.md | 4 +- src/thirdparty/raqm/raqm-version.h | 4 +- src/thirdparty/raqm/raqm.c | 554 ++++++++++++++++++++++++----- src/thirdparty/raqm/raqm.h | 15 + 5 files changed, 476 insertions(+), 103 deletions(-) diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index 992503650..d1b31cfa5 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,7 +2,7 @@ # install raqm -archive=libraqm-0.9.0 +archive=libraqm-0.10.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/src/thirdparty/raqm/README.md b/src/thirdparty/raqm/README.md index 3354a4d25..315e0c8d8 100644 --- a/src/thirdparty/raqm/README.md +++ b/src/thirdparty/raqm/README.md @@ -11,7 +11,7 @@ It currently provides bidirectional text support (using [FriBiDi][1] or As a result, Raqm can support most writing systems covered by Unicode. The documentation can be accessed on the web at: -> http://host-oman.github.io/libraqm/ +> https://host-oman.github.io/libraqm/ Raqm (Arabic: رَقْم) is writing, also number or digit and the Arabic word for digital (رَقَمِيّ) shares the same root, so it is a play on “digital writing”. @@ -81,5 +81,5 @@ The following projects have patches to support complex text layout using Raqm: [1]: https://github.com/fribidi/fribidi [2]: https://github.com/Tehreer/SheenBidi [3]: https://github.com/harfbuzz/harfbuzz -[4]: https://freetype.org/ +[4]: https://www.freetype.org [5]: https://www.gtk.org/gtk-doc diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h index 78b70a561..bdb6fb662 100644 --- a/src/thirdparty/raqm/raqm-version.h +++ b/src/thirdparty/raqm/raqm-version.h @@ -32,10 +32,10 @@ #define _RAQM_VERSION_H_ #define RAQM_VERSION_MAJOR 0 -#define RAQM_VERSION_MINOR 9 +#define RAQM_VERSION_MINOR 10 #define RAQM_VERSION_MICRO 0 -#define RAQM_VERSION_STRING "0.9.0" +#define RAQM_VERSION_STRING "0.10.0" #define RAQM_VERSION_ATLEAST(major,minor,micro) \ ((major)*10000+(minor)*100+(micro) <= \ diff --git a/src/thirdparty/raqm/raqm.c b/src/thirdparty/raqm/raqm.c index 13f6e1f02..770ea3018 100644 --- a/src/thirdparty/raqm/raqm.c +++ b/src/thirdparty/raqm/raqm.c @@ -171,19 +171,23 @@ typedef FriBidiLevel _raqm_bidi_level_t; #endif -typedef struct { +typedef struct +{ FT_Face ftface; int ftloadflags; hb_language_t lang; hb_script_t script; + int spacing_after; } _raqm_text_info; typedef struct _raqm_run raqm_run_t; -struct _raqm { +struct _raqm +{ int ref_count; uint32_t *text; + uint16_t *text_utf16; char *text_utf8; size_t text_len; size_t text_capacity_bytes; @@ -205,7 +209,8 @@ struct _raqm { int invisible_glyph; }; -struct _raqm_run { +struct _raqm_run +{ uint32_t pos; uint32_t len; @@ -217,9 +222,13 @@ struct _raqm_run { raqm_run_t *next; }; -static uint32_t -_raqm_u8_to_u32_index (raqm_t *rq, - uint32_t index); +static size_t +_raqm_encoding_to_u32_index (raqm_t *rq, + size_t index); + +static bool +_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char, + hb_codepoint_t r_char); static void _raqm_init_text_info (raqm_t *rq) @@ -231,6 +240,7 @@ _raqm_init_text_info (raqm_t *rq) rq->text_info[i].ftloadflags = -1; rq->text_info[i].lang = default_lang; rq->text_info[i].script = HB_SCRIPT_INVALID; + rq->text_info[i].spacing_after = 0; } } @@ -263,6 +273,8 @@ _raqm_compare_text_info (_raqm_text_info a, if (a.script != b.script) return false; + /* Spacing shouldn't break runs, so we don't compare them here. */ + return true; } @@ -273,6 +285,7 @@ _raqm_free_text(raqm_t* rq) rq->text = NULL; rq->text_info = NULL; rq->text_utf8 = NULL; + rq->text_utf16 = NULL; rq->text_len = 0; rq->text_capacity_bytes = 0; } @@ -280,12 +293,15 @@ _raqm_free_text(raqm_t* rq) static bool _raqm_alloc_text(raqm_t *rq, size_t len, - bool need_utf8) + bool need_utf8, + bool need_utf16) { /* Allocate contiguous memory block for texts and text_info */ size_t mem_size = (sizeof (uint32_t) + sizeof (_raqm_text_info)) * len; if (need_utf8) mem_size += sizeof (char) * len; + else if (need_utf16) + mem_size += sizeof (uint16_t) * len; if (mem_size > rq->text_capacity_bytes) { @@ -302,6 +318,7 @@ _raqm_alloc_text(raqm_t *rq, rq->text_info = (_raqm_text_info*)(rq->text + len); rq->text_utf8 = need_utf8 ? (char*)(rq->text_info + len) : NULL; + rq->text_utf16 = need_utf16 ? (uint16_t*)(rq->text_info + len) : NULL; return true; } @@ -357,7 +374,7 @@ _raqm_free_runs (raqm_run_t *runs) * Return value: * A newly allocated #raqm_t with a reference count of 1. The initial reference * count should be released with raqm_destroy() when you are done using the - * #raqm_t. Returns %NULL in case of error. + * #raqm_t. Returns `NULL` in case of error. * * Since: 0.1 */ @@ -381,6 +398,7 @@ raqm_create (void) rq->invisible_glyph = 0; rq->text = NULL; + rq->text_utf16 = NULL; rq->text_utf8 = NULL; rq->text_info = NULL; rq->text_capacity_bytes = 0; @@ -498,7 +516,7 @@ raqm_clear_contents (raqm_t *rq) * separately can give improper output. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -518,7 +536,7 @@ raqm_set_text (raqm_t *rq, if (!len) return true; - if (!_raqm_alloc_text(rq, len, false)) + if (!_raqm_alloc_text(rq, len, false, false)) return false; rq->text_len = len; @@ -575,6 +593,53 @@ _raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode) return (out_utf32 - unicode); } +static void * +_raqm_get_utf16_codepoint (const void *str, + uint32_t *out_codepoint) +{ + const uint16_t *s = (const uint16_t *)str; + + if (s[0] > 0xD800 && s[0] < 0xDBFF) + { + if (s[1] > 0xDC00 && s[1] < 0xDFFF) + { + uint32_t X = ((s[0] & ((1 << 6) -1)) << 10) | (s[1] & ((1 << 10) -1)); + uint32_t W = (s[0] >> 6) & ((1 << 5) - 1); + *out_codepoint = (W+1) << 16 | X; + s += 2; + } + else + { + /* A single high surrogate, this is an error. */ + *out_codepoint = s[0]; + s += 1; + } + } + else + { + *out_codepoint = s[0]; + s += 1; + } + return (void *)s; +} + +static size_t +_raqm_u16_to_u32 (const uint16_t *text, size_t len, uint32_t *unicode) +{ + size_t in_len = 0; + uint32_t *out_utf32 = unicode; + const uint16_t *in_utf16 = text; + + while ((*in_utf16 != '\0') && (in_len < len)) + { + in_utf16 = _raqm_get_utf16_codepoint (in_utf16, out_utf32); + ++out_utf32; + ++in_len; + } + + return (out_utf32 - unicode); +} + /** * raqm_set_text_utf8: * @rq: a #raqm_t. @@ -584,7 +649,7 @@ _raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode) * Same as raqm_set_text(), but for text encoded in UTF-8 encoding. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -604,7 +669,7 @@ raqm_set_text_utf8 (raqm_t *rq, if (!len) return true; - if (!_raqm_alloc_text(rq, len, true)) + if (!_raqm_alloc_text(rq, len, true, false)) return false; rq->text_len = _raqm_u8_to_u32 (text, len, rq->text); @@ -614,6 +679,44 @@ raqm_set_text_utf8 (raqm_t *rq, return true; } +/** + * raqm_set_text_utf16: + * @rq: a #raqm_t. + * @text: a UTF-16 encoded text string. + * @len: the length of @text in UTF-16 shorts. + * + * Same as raqm_set_text(), but for text encoded in UTF-16 encoding. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.10 + */ +bool +raqm_set_text_utf16 (raqm_t *rq, + const uint16_t *text, + size_t len) +{ + if (!rq || !text) + return false; + + /* Call raqm_clear_contents to reuse this raqm_t */ + if (rq->text_len) + return false; + + /* Empty string, don’t fail but do nothing */ + if (!len) + return true; + + if (!_raqm_alloc_text(rq, len, false, true)) + return false; + + rq->text_len = _raqm_u16_to_u32 (text, len, rq->text); + memcpy (rq->text_utf16, text, sizeof (uint16_t) * len); + _raqm_init_text_info (rq); + + return true; +} /** * raqm_set_par_direction: * @rq: a #raqm_t. @@ -640,7 +743,7 @@ raqm_set_text_utf8 (raqm_t *rq, * text. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -673,7 +776,7 @@ raqm_set_par_direction (raqm_t *rq, * parts of the text. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Stability: * Unstable @@ -687,7 +790,7 @@ raqm_set_language (raqm_t *rq, size_t len) { hb_language_t language; - size_t end = start + len; + size_t end; if (!rq) return false; @@ -695,11 +798,8 @@ raqm_set_language (raqm_t *rq, if (!rq->text_len) return true; - if (rq->text_utf8) - { - start = _raqm_u8_to_u32_index (rq, start); - end = _raqm_u8_to_u32_index (rq, end); - } + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); if (start >= rq->text_len || end > rq->text_len) return false; @@ -716,11 +816,37 @@ raqm_set_language (raqm_t *rq, return true; } +static bool +_raqm_add_font_feature (raqm_t *rq, + hb_feature_t fea) +{ + void* new_features; + + if (!rq) + return false; + + new_features = realloc (rq->features, + sizeof (hb_feature_t) * (rq->features_len + 1)); + if (!new_features) + return false; + + if (fea.start != HB_FEATURE_GLOBAL_START) + fea.start = _raqm_encoding_to_u32_index (rq, fea.start); + if (fea.end != HB_FEATURE_GLOBAL_END) + fea.end = _raqm_encoding_to_u32_index (rq, fea.end); + + rq->features = new_features; + rq->features[rq->features_len] = fea; + rq->features_len++; + + return true; +} + /** * raqm_add_font_feature: * @rq: a #raqm_t. * @feature: (transfer none): a font feature string. - * @len: length of @feature, -1 for %NULL-terminated. + * @len: length of @feature, -1 for `NULL`-terminated. * * Adds a font feature to be used by the #raqm_t during text layout. This is * usually used to turn on optional font features that are not enabled by @@ -734,7 +860,7 @@ raqm_set_language (raqm_t *rq, * end of the features list and can potentially override previous features. * * Return value: - * %true if parsing @feature succeeded, %false otherwise. + * `true` if parsing @feature succeeded, `false` otherwise. * * Since: 0.1 */ @@ -751,16 +877,7 @@ raqm_add_font_feature (raqm_t *rq, ok = hb_feature_from_string (feature, len, &fea); if (ok) - { - void* new_features = realloc (rq->features, - sizeof (hb_feature_t) * (rq->features_len + 1)); - if (!new_features) - return false; - - rq->features = new_features; - rq->features[rq->features_len] = fea; - rq->features_len++; - } + _raqm_add_font_feature (rq, fea); return ok; } @@ -817,7 +934,7 @@ _raqm_set_freetype_face (raqm_t *rq, * See also raqm_set_freetype_face_range(). * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -832,21 +949,23 @@ raqm_set_freetype_face (raqm_t *rq, * raqm_set_freetype_face_range: * @rq: a #raqm_t. * @face: an #FT_Face. - * @start: index of first character that should use @face. - * @len: number of characters using @face. + * @start: index of first character that should use @face from the input string. + * @len: number of elements using @face. * * Sets an #FT_Face to be used for @len-number of characters staring at @start. - * The @start and @len are input string array indices (i.e. counting bytes in - * UTF-8 and scaler values in UTF-32). + * The @start and @len are input string array indices, counting elements + * according to the underlying encoding. @start must always be aligned to the + * start of an encoded codepoint, and @len must always end at a codepoint's + * final element. * * This method can be used repeatedly to set different faces for different * parts of the text. It is the responsibility of the client to make sure that - * face ranges cover the whole text. + * face ranges cover the whole text, and is properly aligned. * * See also raqm_set_freetype_face(). * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -856,7 +975,7 @@ raqm_set_freetype_face_range (raqm_t *rq, size_t start, size_t len) { - size_t end = start + len; + size_t end; if (!rq) return false; @@ -864,11 +983,8 @@ raqm_set_freetype_face_range (raqm_t *rq, if (!rq->text_len) return true; - if (rq->text_utf8) - { - start = _raqm_u8_to_u32_index (rq, start); - end = _raqm_u8_to_u32_index (rq, end); - } + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); return _raqm_set_freetype_face (rq, face, start, end); } @@ -909,7 +1025,7 @@ _raqm_set_freetype_load_flags (raqm_t *rq, * older version the flags will be ignored. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.3 */ @@ -943,7 +1059,7 @@ raqm_set_freetype_load_flags (raqm_t *rq, * See also raqm_set_freetype_load_flags(). * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.9 */ @@ -953,7 +1069,7 @@ raqm_set_freetype_load_flags_range (raqm_t *rq, size_t start, size_t len) { - size_t end = start + len; + size_t end; if (!rq) return false; @@ -961,15 +1077,161 @@ raqm_set_freetype_load_flags_range (raqm_t *rq, if (!rq->text_len) return true; - if (rq->text_utf8) - { - start = _raqm_u8_to_u32_index (rq, start); - end = _raqm_u8_to_u32_index (rq, end); - } + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); return _raqm_set_freetype_load_flags (rq, flags, start, end); } +static bool +_raqm_set_spacing (raqm_t *rq, + int spacing, + bool word_spacing, + size_t start, + size_t end) +{ + if (!rq) + return false; + + if (!rq->text_len) + return true; + + if (start >= rq->text_len || end > rq->text_len) + return false; + + if (!rq->text_info) + return false; + + for (size_t i = start; i < end; i++) + { + bool set_spacing = i == 0; + if (!set_spacing) + set_spacing = _raqm_allowed_grapheme_boundary (rq->text[i-1], rq->text[i]); + + if (set_spacing) + { + if (word_spacing) + { + if (_raqm_allowed_grapheme_boundary (rq->text[i], rq->text[i+1])) + { + /* CSS word seperators, word spacing is only applied on these.*/ + if (rq->text[i] == 0x0020 || /* Space */ + rq->text[i] == 0x00A0 || /* No Break Space */ + rq->text[i] == 0x1361 || /* Ethiopic Word Space */ + rq->text[i] == 0x10100 || /* Aegean Word Seperator Line */ + rq->text[i] == 0x10101 || /* Aegean Word Seperator Dot */ + rq->text[i] == 0x1039F || /* Ugaric Word Divider */ + rq->text[i] == 0x1091F) /* Phoenician Word Separator */ + { + rq->text_info[i].spacing_after = spacing; + } + } + } + else + { + rq->text_info[i].spacing_after = spacing; + } + } + } + + return true; +} + +/** + * raqm_set_letter_spacing_range: + * @rq: a #raqm_t. + * @spacing: amount of spacing in Freetype Font Units (26.6 format). + * @start: index of first character that should use @spacing. + * @len: number of characters using @spacing. + * + * Set the letter spacing or tracking for a given range, the value + * will be added onto the advance and offset for RTL, and the advance for + * other directions. Letter spacing will be applied between characters, so + * the last character will not have spacing applied after it. + * Note that not all scripts have a letter-spacing tradition, + * for example, Arabic does not, while Devanagari does. + * + * This will also add “disable `liga`, `clig`, `hlig`, `dlig`, and `calt`” font + * features to the internal features list, so call this function after setting + * the font features for best spacing results. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.10 + */ +bool +raqm_set_letter_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len) +{ + size_t end; + + if (!rq) + return false; + + if (!rq->text_len) + return true; + + end = start + len - 1; + + if (spacing != 0) + { +#define NUM_TAGS 5 + static char *tags[NUM_TAGS] = { "clig", "liga", "hlig", "dlig", "calt" }; + for (size_t i = 0; i < NUM_TAGS; i++) + { + hb_feature_t fea = { hb_tag_from_string(tags[i], 5), 0, start, end }; + _raqm_add_font_feature (rq, fea); + } +#undef NUM_TAGS + } + + start = _raqm_encoding_to_u32_index (rq, start); + end = _raqm_encoding_to_u32_index (rq, end); + + return _raqm_set_spacing (rq, spacing, false, start, end); +} + +/** + * raqm_set_word_spacing_range: + * @rq: a #raqm_t. + * @spacing: amount of spacing in Freetype Font Units (26.6 format). + * @start: index of first character that should use @spacing. + * @len: number of characters using @spacing. + * + * Set the word spacing for a given range. Word spacing will only be applied to + * 'word separator' characters, such as 'space', 'no break space' and + * Ethiopic word separator'. + * The value will be added onto the advance and offset for RTL, and the advance + * for other directions. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.10 + */ +bool +raqm_set_word_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len) +{ + size_t end; + + if (!rq) + return false; + + if (!rq->text_len) + return true; + + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); + + return _raqm_set_spacing (rq, spacing, true, start, end); +} + /** * raqm_set_invisible_glyph: * @rq: a #raqm_t. @@ -984,7 +1246,7 @@ raqm_set_freetype_load_flags_range (raqm_t *rq, * If @gid is a positive number, it will be used for invisible glyphs. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.6 */ @@ -1014,7 +1276,7 @@ _raqm_shape (raqm_t *rq); * text shaping, and any other part of the layout process. * * Return value: - * %true if the layout process was successful, %false otherwise. + * `true` if the layout process was successful, `false` otherwise. * * Since: 0.1 */ @@ -1048,7 +1310,9 @@ raqm_layout (raqm_t *rq) static uint32_t _raqm_u32_to_u8_index (raqm_t *rq, uint32_t index); - +static uint32_t +_raqm_u32_to_u16_index (raqm_t *rq, + uint32_t index); /** * raqm_get_glyphs: * @rq: a #raqm_t. @@ -1059,7 +1323,7 @@ _raqm_u32_to_u8_index (raqm_t *rq, * information. * * Return value: (transfer none): - * An array of #raqm_glyph_t, or %NULL in case of error. This is owned by @rq + * An array of #raqm_glyph_t, or `NULL` in case of error. This is owned by @rq * and must not be freed. * * Since: 0.1 @@ -1147,6 +1411,12 @@ raqm_get_glyphs (raqm_t *rq, RAQM_TEST ("\n"); #endif } + else if (rq->text_utf16) + { + for (size_t i = 0; i < count; i++) + rq->glyphs[i].cluster = _raqm_u32_to_u16_index (rq, + rq->glyphs[i].cluster); + } return rq->glyphs; } @@ -1194,8 +1464,10 @@ raqm_get_direction_at_index (raqm_t *rq, for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) { - if (run->pos <= index && index < run->pos + run->len) { - switch (run->direction) { + if (run->pos <= index && index < run->pos + run->len) + { + switch (run->direction) + { case HB_DIRECTION_LTR: return RAQM_DIRECTION_LTR; case HB_DIRECTION_RTL: @@ -1227,7 +1499,8 @@ _raqm_hb_dir (raqm_t *rq, _raqm_bidi_level_t level) return dir; } -typedef struct { +typedef struct +{ size_t pos; size_t len; _raqm_bidi_level_t level; @@ -1264,10 +1537,10 @@ _raqm_bidi_itemize (raqm_t *rq, size_t *run_count) line = SBParagraphCreateLine (par, 0, par_len); *run_count = SBLineGetRunCount (line); - if (SBParagraphGetBaseLevel (par) == 0) - rq->resolved_dir = RAQM_DIRECTION_LTR; - else + if (SBParagraphGetBaseLevel (par) == 1) rq->resolved_dir = RAQM_DIRECTION_RTL; + else + rq->resolved_dir = RAQM_DIRECTION_LTR; runs = malloc (sizeof (_raqm_bidi_run) * (*run_count)); if (runs) @@ -1418,10 +1691,10 @@ _raqm_bidi_itemize (raqm_t *rq, size_t *run_count) rq->text_len, &par_type, levels); - if (par_type == FRIBIDI_PAR_LTR) - rq->resolved_dir = RAQM_DIRECTION_LTR; - else + if (par_type == FRIBIDI_PAR_RTL) rq->resolved_dir = RAQM_DIRECTION_RTL; + else + rq->resolved_dir = RAQM_DIRECTION_LTR; if (max_level == 0) goto done; @@ -1447,22 +1720,15 @@ _raqm_itemize (raqm_t *rq) bool ok = true; #ifdef RAQM_TESTING - switch (rq->base_dir) - { - case RAQM_DIRECTION_RTL: - RAQM_TEST ("Direction is: RTL\n\n"); - break; - case RAQM_DIRECTION_LTR: - RAQM_TEST ("Direction is: LTR\n\n"); - break; - case RAQM_DIRECTION_TTB: - RAQM_TEST ("Direction is: TTB\n\n"); - break; - case RAQM_DIRECTION_DEFAULT: - default: - RAQM_TEST ("Direction is: DEFAULT\n\n"); - break; - } + static char *dir_names[] = { + "DEFAULT", + "RTL", + "LTR", + "TTB" + }; + + assert (rq->base_dir < sizeof (dir_names)); + RAQM_TEST ("Direction is: %s\n\n", dir_names[rq->base_dir]); #endif if (!_raqm_resolve_scripts (rq)) @@ -1483,9 +1749,9 @@ _raqm_itemize (raqm_t *rq) runs->len = rq->text_len; runs->level = 0; } - } else { - runs = _raqm_bidi_itemize (rq, &run_count); } + else + runs = _raqm_bidi_itemize (rq, &run_count); if (!runs) { @@ -1494,6 +1760,9 @@ _raqm_itemize (raqm_t *rq) } #ifdef RAQM_TESTING + assert (rq->resolved_dir < sizeof (dir_names)); + if (rq->base_dir == RAQM_DIRECTION_DEFAULT) + RAQM_TEST ("Resolved direction is: %s\n\n", dir_names[rq->resolved_dir]); RAQM_TEST ("Number of runs before script itemization: %zu\n\n", run_count); RAQM_TEST ("BiDi Runs:\n"); @@ -1617,7 +1886,8 @@ done: } /* Stack to handle script detection */ -typedef struct { +typedef struct +{ size_t capacity; size_t size; int *pair_index; @@ -1910,15 +2180,47 @@ _raqm_shape (raqm_t *rq) { FT_Matrix matrix; + hb_glyph_info_t *info; hb_glyph_position_t *pos; unsigned int len; FT_Get_Transform (hb_ft_font_get_face (run->font), &matrix, NULL); pos = hb_buffer_get_glyph_positions (run->buffer, &len); + info = hb_buffer_get_glyph_infos (run->buffer, &len); + for (unsigned int i = 0; i < len; i++) { _raqm_ft_transform (&pos[i].x_advance, &pos[i].y_advance, matrix); _raqm_ft_transform (&pos[i].x_offset, &pos[i].y_offset, matrix); + + bool set_spacing = false; + if (run->direction == HB_DIRECTION_RTL) + { + set_spacing = i == 0; + if (!set_spacing) + set_spacing = info[i].cluster != info[i-1].cluster; + } + else + { + set_spacing = i == len - 1; + if (!set_spacing) + set_spacing = info[i].cluster != info[i+1].cluster; + } + + _raqm_text_info rq_info = rq->text_info[info[i].cluster]; + + if (rq_info.spacing_after != 0 && set_spacing) + { + if (run->direction == HB_DIRECTION_TTB) + pos[i].y_advance -= rq_info.spacing_after; + else if (run->direction == HB_DIRECTION_RTL) + { + pos[i].x_advance += rq_info.spacing_after; + pos[i].x_offset += rq_info.spacing_after; + } + else + pos[i].x_advance += rq_info.spacing_after; + } } } } @@ -1954,9 +2256,9 @@ _raqm_u32_to_u8_index (raqm_t *rq, } /* Convert index from UTF-8 to UTF-32 */ -static uint32_t +static size_t _raqm_u8_to_u32_index (raqm_t *rq, - uint32_t index) + size_t index) { const unsigned char *s = (const unsigned char *) rq->text_utf8; const unsigned char *t = s; @@ -1982,9 +2284,64 @@ _raqm_u8_to_u32_index (raqm_t *rq, return length; } -static bool -_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char, - hb_codepoint_t r_char); +/* Count equivalent UTF-16 short in codepoint */ +static size_t +_raqm_count_codepoint_utf16_short (uint32_t chr) +{ + if (chr > 0x010000) + return 2; + else + return 1; +} + +/* Convert index from UTF-32 to UTF-16 */ +static uint32_t +_raqm_u32_to_u16_index (raqm_t *rq, + uint32_t index) +{ + size_t length = 0; + + for (uint32_t i = 0; i < index; ++i) + length += _raqm_count_codepoint_utf16_short (rq->text[i]); + + return length; +} + +/* Convert index from UTF-16 to UTF-32 */ +static size_t +_raqm_u16_to_u32_index (raqm_t *rq, + size_t index) +{ + const uint16_t *s = (const uint16_t *) rq->text_utf16; + const uint16_t *t = s; + size_t length = 0; + + while (((size_t) (s - t) < index) && ('\0' != *s)) + { + if (*s < 0xD800 || *s > 0xDBFF) + s += 1; + else + s += 2; + + length++; + } + + if ((size_t) (s-t) > index) + length--; + + return length; +} + +static inline size_t +_raqm_encoding_to_u32_index (raqm_t *rq, + size_t index) +{ + if (rq->text_utf8) + return _raqm_u8_to_u32_index (rq, index); + else if (rq->text_utf16) + return _raqm_u16_to_u32_index (rq, index); + return index; +} static bool _raqm_in_hangul_syllable (hb_codepoint_t ch); @@ -2001,7 +2358,7 @@ _raqm_in_hangul_syllable (hb_codepoint_t ch); * character is left-to-right, then the cursor will be at the right of it. * * Return value: - * %true if the process was successful, %false otherwise. + * `true` if the process was successful, `false` otherwise. * * Since: 0.2 */ @@ -2018,8 +2375,7 @@ raqm_index_to_position (raqm_t *rq, if (rq == NULL) return false; - if (rq->text_utf8) - *index = _raqm_u8_to_u32_index (rq, *index); + *index = _raqm_encoding_to_u32_index (rq, *index); if (*index >= rq->text_len) return false; @@ -2077,6 +2433,8 @@ raqm_index_to_position (raqm_t *rq, found: if (rq->text_utf8) *index = _raqm_u32_to_u8_index (rq, *index); + else if (rq->text_utf16) + *index = _raqm_u32_to_u16_index (rq, *index); RAQM_TEST ("The position is %d at index %zu\n",*x ,*index); return true; } @@ -2093,7 +2451,7 @@ found: * @index. * * Return value: - * %true if the process was successful, %false in case of error. + * `true` if the process was successful, `false` in case of error. * * Since: 0.2 */ @@ -2371,8 +2729,8 @@ raqm_version_string (void) * Checks if library version is less than or equal the specified version. * * Return value: - * %true if library version is less than or equal the specfied version, %false - * otherwise. + * `true` if library version is less than or equal the specified version, + * `false` otherwise. * * Since: 0.7 **/ @@ -2393,8 +2751,8 @@ raqm_version_atleast (unsigned int major, * Checks if library version is less than or equal the specified version. * * Return value: - * %true if library version is less than or equal the specfied version, %false - * otherwise. + * `true` if library version is less than or equal the specified version, + * `false` otherwise. * * Since: 0.7 **/ diff --git a/src/thirdparty/raqm/raqm.h b/src/thirdparty/raqm/raqm.h index bdb5a50d8..2fd836c86 100644 --- a/src/thirdparty/raqm/raqm.h +++ b/src/thirdparty/raqm/raqm.h @@ -118,6 +118,10 @@ RAQM_API bool raqm_set_text_utf8 (raqm_t *rq, const char *text, size_t len); +RAQM_API bool +raqm_set_text_utf16 (raqm_t *rq, + const uint16_t *text, + size_t len); RAQM_API bool raqm_set_par_direction (raqm_t *rq, @@ -154,6 +158,17 @@ raqm_set_freetype_load_flags_range (raqm_t *rq, size_t start, size_t len); +RAQM_API bool +raqm_set_letter_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len); +RAQM_API bool +raqm_set_word_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len); + RAQM_API bool raqm_set_invisible_glyph (raqm_t *rq, int gid); From ad46630cdfa3cd9d0c2fe1de6e599f6aa49355c5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Jan 2023 18:04:41 +1100 Subject: [PATCH 059/132] Updated macOS tested Pillow versions [ci skip] --- docs/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 2a83ed151..cc7d0258b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -478,13 +478,13 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ -| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ | macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | | +---------------------------+------------------+--------------+ -| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |x86-64 | +| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 | | +---------------------------+------------------+ | | | 3.6 | 8.4.0 | | +----------------------------------+---------------------------+------------------+--------------+ From a2edefb45532d20cda0a2915d3f7363dd6ad8754 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jan 2023 07:18:56 +1100 Subject: [PATCH 060/132] Only install python-pyqt6 package on 64-bit --- .github/workflows/test-mingw.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index ccf6e193a..6a60bc7f0 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -49,7 +49,6 @@ jobs: ${{ matrix.package }}-python3-numpy \ ${{ matrix.package }}-python3-olefile \ ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python-pyqt6 \ ${{ matrix.package }}-python3-setuptools \ ${{ matrix.package }}-freetype \ ${{ matrix.package }}-gcc \ @@ -63,6 +62,11 @@ jobs: ${{ matrix.package }}-openjpeg2 \ subversion + if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then + pacman -S --noconfirm \ + ${{ matrix.package }}-python-pyqt6 + fi + python3 -m pip install pyroma pytest pytest-cov pytest-timeout pushd depends && ./install_extra_test_images.sh && popd From ba0d71fec84aff535b0e497a6e2e65b87ad1261b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jan 2023 15:59:51 +1100 Subject: [PATCH 061/132] Updated libwebp to 1.3.0 --- depends/install_webp.sh | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 05867b7d4..f8b985a7a 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.2.4 +archive=libwebp-1.3.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 a34e8b342..fd12240e5 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -177,9 +177,9 @@ deps = { "libs": [r"windows\vs2019\Release\{msbuild_arch}\liblzma\liblzma.lib"], }, "libwebp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.4.tar.gz", - "filename": "libwebp-1.2.4.tar.gz", - "dir": "libwebp-1.2.4", + "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.0.tar.gz", + "filename": "libwebp-1.3.0.tar.gz", + "dir": "libwebp-1.3.0", "license": "COPYING", "build": [ cmd_rmdir(r"output\release-static"), # clean From e48aead015996de2284ebbbbc4a00a726d61af9b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jan 2023 21:02:42 +1100 Subject: [PATCH 062/132] Allow writing IFDRational to BYTE tag --- Tests/test_file_tiff_metadata.py | 5 +++-- src/PIL/TiffImagePlugin.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 48797ea08..1061f7d05 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -202,14 +202,15 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path): assert reloaded.tag_v2[271] == expected -def test_writing_int_to_bytes(tmp_path): +@pytest.mark.parametrize("value", (1, IFDRational(1))) +def test_writing_other_types_to_bytes(value, tmp_path): im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[700] assert tag.type == TiffTags.BYTE - info[700] = 1 + info[700] = value out = str(tmp_path / "temp.tiff") im.save(out, tiffinfo=info) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 431a95701..baa9abad8 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -722,6 +722,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(1) # Basic type, except for the legacy API. def write_byte(self, data): + if isinstance(data, IFDRational): + data = int(data) if isinstance(data, int): data = bytes((data,)) return data From 43bb03539e0f3dca6ee399cbb8162c21e257c05d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jan 2023 20:02:16 +1100 Subject: [PATCH 063/132] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bf3017ca9..b3dd16ace 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Allow writing IFDRational to BYTE tag #6890 + [radarhere] + - Raise ValueError for BoxBlur filter with negative radius #6874 [hugovk, radarhere] From c5d1b1582452d7314cbd8f2b1cd9fe58d1fc6894 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jan 2023 22:45:29 +1100 Subject: [PATCH 064/132] Do not unintentionally load TIFF format at first --- src/PIL/JpegImagePlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 9657ae9d0..b9c80236e 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -41,7 +41,7 @@ import sys import tempfile import warnings -from . import Image, ImageFile, TiffImagePlugin +from . import Image, ImageFile from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 @@ -524,6 +524,8 @@ def _getmp(self): head = file_contents.read(8) endianness = ">" if head[:4] == b"\x4d\x4d\x00\x2a" else "<" # process dictionary + from . import TiffImagePlugin + try: info = TiffImagePlugin.ImageFileDirectory_v2(head) file_contents.seek(info.next) From 5f9285eea6b46d675daf5f6d733efa872086760d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jan 2023 23:22:35 +1100 Subject: [PATCH 065/132] Do not retry specified formats if they failed --- 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 b0ff5173c..7fc8f496e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3267,7 +3267,7 @@ def open(fp, mode="r", formats=None): im = _open_core(fp, filename, prefix, formats) - if im is None: + if im is None and formats is ID: if init(): im = _open_core(fp, filename, prefix, formats) From 55ce251a8943f419701242ed06d366df2b5a28e3 Mon Sep 17 00:00:00 2001 From: Alex Clark Date: Sat, 14 Jan 2023 12:36:22 -0500 Subject: [PATCH 066/132] Alex Clark -> Jeffrey A. Clark (Alex) I'm still "Alex", just on a Jeffrey A. Clark roll lately. --- LICENSE | 2 +- README.md | 4 ++-- docs/COPYING | 2 +- docs/conf.py | 8 ++++---- docs/index.rst | 2 +- setup.cfg | 4 ++-- src/PIL/__init__.py | 4 ++-- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/LICENSE b/LICENSE index 616808a48..125bdcc44 100644 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2023 by Alex Clark and contributors + Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors. Like PIL, Pillow is licensed under the open source HPND License: diff --git a/README.md b/README.md index 489d3db54..af1ca57c2 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ ## Python Imaging Library (Fork) -Pillow is the friendly PIL fork by [Alex Clark and -Contributors](https://github.com/python-pillow/Pillow/graphs/contributors). +Pillow is the friendly PIL fork by [Jeffrey A. Clark (Alex) and +contributors](https://github.com/python-pillow/Pillow/graphs/contributors). PIL is the Python Imaging Library by Fredrik Lundh and Contributors. As of 2019, Pillow development is [supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise). diff --git a/docs/COPYING b/docs/COPYING index b400381d3..bc44ba388 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2023 by Alex Clark and contributors + Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/conf.py b/docs/conf.py index fb58d25ed..96324423a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,8 +52,8 @@ master_doc = "index" # General information about the project. project = "Pillow (PIL Fork)" -copyright = "1995-2011 Fredrik Lundh, 2010-2023 Alex Clark and Contributors" -author = "Fredrik Lundh, Alex Clark and Contributors" +copyright = "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors" +author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -243,7 +243,7 @@ latex_documents = [ master_doc, "PillowPILFork.tex", "Pillow (PIL Fork) Documentation", - "Alex Clark", + "Jeffrey A. Clark (Alex)", "manual", ) ] @@ -293,7 +293,7 @@ texinfo_documents = [ "Pillow (PIL Fork) Documentation", author, "PillowPILFork", - "Pillow is the friendly PIL fork by Alex Clark and Contributors.", + "Pillow is the friendly PIL fork by Jeffrey A. Clark (Alex) and contributors.", "Miscellaneous", ) ] diff --git a/docs/index.rst b/docs/index.rst index a4663bac8..418844ba7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ Pillow ====== -Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +Pillow is the friendly PIL fork by `Jeffrey A. Clark (Alex) and contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and contributors. Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_. diff --git a/setup.cfg b/setup.cfg index 2dc552a2c..824cae088 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,8 +4,8 @@ description = Python Imaging Library (Fork) long_description = file: README.md long_description_content_type = text/markdown url = https://python-pillow.org -author = Alex Clark (PIL Fork Author) -author_email = aclark@python-pillow.org +author = Jeffrey A. Clark (Alex) +author_email = aclark@aclark.net license = HPND classifiers = Development Status :: 6 - Mature diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 4b76e893f..0e6f82092 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -1,11 +1,11 @@ """Pillow (Fork of the Python Imaging Library) -Pillow is the friendly PIL fork by Alex Clark and Contributors. +Pillow is the friendly PIL fork by Jeffrey A. Clark (Alex) and contributors. https://github.com/python-pillow/Pillow/ Pillow is forked from PIL 1.1.7. -PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +PIL is the Python Imaging Library by Fredrik Lundh and contributors. Copyright (c) 1999 by Secret Labs AB. Use PIL.__version__ for this Pillow version. From 5a71fe804154f627cd9ba28eafe47c893a0d6ea6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 14 Jan 2023 17:39:33 +0000 Subject: [PATCH 067/132] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 96324423a..e1ffa49b8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,7 +52,9 @@ master_doc = "index" # General information about the project. project = "Pillow (PIL Fork)" -copyright = "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors" +copyright = ( + "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors" +) author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors" # The version info for the project you're documenting, acts as replacement for From 3360b5a756f761051653d3c854a7601cea33d064 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Jan 2023 19:49:13 +1100 Subject: [PATCH 068/132] Stop reading when a line becomes too long --- src/PIL/EpsImagePlugin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index f7d376364..dd68c13e5 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -173,11 +173,13 @@ class PSFile: self.fp.seek(offset, whence) def readline(self): - s = [self.char or b""] - self.char = None + s = [] + if self.char: + s.append(self.char) + self.char = None c = self.fp.read(1) - while (c not in b"\r\n") and len(c): + while (c not in b"\r\n") and len(c) and len(b"".join(s).strip(b"\r\n")) <= 255: s.append(c) c = self.fp.read(1) From 04cf5e2cfc5dc1676efd9f5c8d605ddfccb0f9ee Mon Sep 17 00:00:00 2001 From: Bas Couwenberg Date: Sat, 14 Jan 2023 19:09:43 +0100 Subject: [PATCH 069/132] Handle more than one directory returned by pkg-config. tiff (4.5.0-1) in Debian results in two include directories being returned: ``` -I/usr/include/x86_64-linux-gnu -I/usr/include ``` --- setup.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 243365681..b4ebbb9c2 100755 --- a/setup.py +++ b/setup.py @@ -263,18 +263,20 @@ def _pkg_config(name): if not DEBUG: command_libs.append("--silence-errors") command_cflags.append("--silence-errors") - libs = ( + libs = re.split( + r"\s*-L", subprocess.check_output(command_libs, stderr=stderr) .decode("utf8") - .strip() - .replace("-L", "") + .strip(), ) - cflags = ( - subprocess.check_output(command_cflags) + libs.remove("") + cflags = re.split( + r"\s*-I", + subprocess.check_output(command_cflags, stderr=stderr) .decode("utf8") - .strip() - .replace("-I", "") + .strip(), ) + cflags.remove("") return libs, cflags except Exception: pass @@ -473,8 +475,12 @@ class pil_build_ext(build_ext): else: lib_root = include_root = root - _add_directory(library_dirs, lib_root) - _add_directory(include_dirs, include_root) + if lib_root is not None: + for lib_dir in lib_root: + _add_directory(library_dirs, lib_dir) + if include_root is not None: + for include_dir in include_root: + _add_directory(include_dirs, include_dir) # respect CFLAGS/CPPFLAGS/LDFLAGS for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"): From cd4656410f8d8ddcf806717e7404bc2f0392d88d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 17:32:58 -0600 Subject: [PATCH 070/132] parametrize test_file_tar::test_sanity() --- Tests/test_file_tar.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 5daab47fc..49451ff44 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -10,18 +10,21 @@ from .helper import is_pypy TEST_TAR_FILE = "Tests/images/hopper.tar" -def test_sanity(): - for codec, test_path, format in [ - ["zlib", "hopper.png", "PNG"], - ["jpg", "hopper.jpg", "JPEG"], - ]: - if features.check(codec): - with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: - with Image.open(tar) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == format +@pytest.mark.parametrize( + ("codec", "test_path", "format"), + ( + ("zlib", "hopper.png", "PNG"), + ("jpg", "hopper.jpg", "JPEG"), + ), +) +def test_sanity(codec, test_path, format): + if features.check(codec): + with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: + with Image.open(tar) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == format @pytest.mark.skipif(is_pypy(), reason="Requires CPython") From c2176f2747cd64cd2cf1d7ba859fde1a26f3db52 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 19:36:52 -0600 Subject: [PATCH 071/132] use string for parametrization name declaration Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_tar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 49451ff44..799c243d6 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -11,7 +11,7 @@ TEST_TAR_FILE = "Tests/images/hopper.tar" @pytest.mark.parametrize( - ("codec", "test_path", "format"), + "codec, test_path, format", ( ("zlib", "hopper.png", "PNG"), ("jpg", "hopper.jpg", "JPEG"), From 2332a1c796eaee8f79cf3d1772c1c5352a4c977b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Jan 2023 08:27:49 +1100 Subject: [PATCH 072/132] Updated libimagequant to 4.0.5 --- depends/install_imagequant.sh | 2 +- docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 64dd024bd..541ec8fda 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.0.4 +archive=libimagequant-4.0.5 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/installation.rst b/docs/installation.rst index cc7d0258b..0cea725b4 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -169,7 +169,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.0.4** + * Pillow has been tested with libimagequant **2.6-4.0.5** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From 0635c180307850db3737e25e8efa49502bf38db6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Jan 2023 23:02:19 +1100 Subject: [PATCH 073/132] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b3dd16ace..ed41d46c7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Stop reading when EPS line becomes too long #6897 + [radarhere] + - Allow writing IFDRational to BYTE tag #6890 [radarhere] From bf0abdca27cd84dafd185bd44206c82b5c14330d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 Jan 2023 08:06:30 +1100 Subject: [PATCH 074/132] Do not retry past formats when loading all formats for the first time --- src/PIL/Image.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7fc8f496e..833473f78 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3268,8 +3268,14 @@ def open(fp, mode="r", formats=None): im = _open_core(fp, filename, prefix, formats) if im is None and formats is ID: + checked_formats = formats.copy() if init(): - im = _open_core(fp, filename, prefix, formats) + im = _open_core( + fp, + filename, + prefix, + tuple(format for format in formats if format not in checked_formats), + ) if im: im._exclusive_fp = exclusive_fp From 9b660db62de0cac22f2d1bf37aabbd412ee7bc62 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 20 Jan 2023 14:35:11 +0200 Subject: [PATCH 075/132] Handling for deprecations to be removed in Pillow 11 --- Tests/test_deprecate.py | 5 +++++ src/PIL/_deprecate.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 30ed4a808..3375eb6b2 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -11,6 +11,11 @@ from PIL import _deprecate "Old thing is deprecated and will be removed in Pillow 10 " r"\(2023-07-01\)\. Use new thing instead\.", ), + ( + 11, + "Old thing is deprecated and will be removed in Pillow 11 " + r"\(2024-10-15\)\. Use new thing instead\.", + ), ( None, r"Old thing is deprecated and will be removed in a future version\. " diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 7c4b1623d..fa6e1d00c 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -47,6 +47,8 @@ def deprecate( raise RuntimeError(msg) elif when == 10: removed = "Pillow 10 (2023-07-01)" + elif when == 11: + removed = "Pillow 11 (2024-10-15)" else: msg = f"Unknown removal version, update {__name__}?" raise ValueError(msg) From e01f5556586a0f789c0ae0fc00d306a2a6513c3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 22 Jan 2023 06:41:19 +1100 Subject: [PATCH 076/132] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ed41d46c7..27dbd69bb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Do not retry past formats when loading all formats for the first time #6902 + [radarhere] + +- Do not retry specified formats if they failed when opening #6893 + [radarhere] + +- Do not unintentionally load TIFF format at first #6892 + [radarhere] + - Stop reading when EPS line becomes too long #6897 [radarhere] From 20c54ba1108c831484e2e22c5e143952259a67bb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 23 Jan 2023 07:37:20 +1100 Subject: [PATCH 077/132] Updated libimagequant to 4.1.0 --- depends/install_imagequant.sh | 2 +- docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 541ec8fda..8b847b894 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.0.5 +archive=libimagequant-4.1.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/installation.rst b/docs/installation.rst index 0cea725b4..ea8722c56 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -169,7 +169,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.0.5** + * Pillow has been tested with libimagequant **2.6-4.1** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From 9dc9e82bef9462ee17cdc9c623654a75eab64a6d Mon Sep 17 00:00:00 2001 From: Renat Nasyrov Date: Tue, 24 Jan 2023 00:11:27 +0100 Subject: [PATCH 078/132] Specify correct description for mode L. --- docs/handbook/concepts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 45c662bd6..0aa2f1119 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -31,7 +31,7 @@ INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current re supports the following standard modes: * ``1`` (1-bit pixels, black and white, stored with one pixel per byte) - * ``L`` (8-bit pixels, black and white) + * ``L`` (8-bit pixels, grayscale) * ``P`` (8-bit pixels, mapped to any other mode using a color palette) * ``RGB`` (3x8-bit pixels, true color) * ``RGBA`` (4x8-bit pixels, true color with transparency mask) From e76fa1674e65a38092dcb4b7cbafb2eaaaaaa6c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jan 2023 14:15:51 +1100 Subject: [PATCH 079/132] Relax roundtrip check --- Tests/test_qt_image_qapplication.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 1fc816146..34609314c 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -6,7 +6,7 @@ with warnings.catch_warnings(): warnings.simplefilter("ignore", category=DeprecationWarning) from PIL import ImageQt -from .helper import assert_image_equal, assert_image_equal_tofile, hopper +from .helper import assert_image_equal_tofile, assert_image_similar, hopper if ImageQt.qt_is_installed: from PIL.ImageQt import QPixmap @@ -48,7 +48,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_equal(result, expected.convert("RGB")) + assert_image_similar(result, expected.convert("RGB"), 0.3) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") From a0492f796876c2a9b8ba445d72c771b84eff93a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jan 2023 19:27:51 +1100 Subject: [PATCH 080/132] Ensure that pkg-config paths are split by spaces --- setup.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index b4ebbb9c2..4382c1a97 100755 --- a/setup.py +++ b/setup.py @@ -264,19 +264,17 @@ def _pkg_config(name): command_libs.append("--silence-errors") command_cflags.append("--silence-errors") libs = re.split( - r"\s*-L", + r"(^|\s+)-L", subprocess.check_output(command_libs, stderr=stderr) .decode("utf8") .strip(), - ) - libs.remove("") + )[::2][1:] cflags = re.split( - r"\s*-I", + r"(^|\s+)-I", subprocess.check_output(command_cflags, stderr=stderr) .decode("utf8") .strip(), - ) - cflags.remove("") + )[::2][1:] return libs, cflags except Exception: pass From 3e37a919b136d35447bde7694ecf579a2096b163 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jan 2023 22:43:04 +1100 Subject: [PATCH 081/132] Prevent register_open from adding duplicates to ID --- Tests/test_image.py | 11 +++++++++++ src/PIL/Image.py | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index d261638f9..ad3346b5a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -398,6 +398,17 @@ class TestImage: with pytest.raises(ValueError): source.alpha_composite(over, (0, 0), (0, -1)) + def test_register_open_duplicates(self): + # Arrange + factory, accept = Image.OPEN["JPEG"] + id_length = len(Image.ID) + + # Act + Image.register_open("JPEG", factory, accept) + + # Assert + assert len(Image.ID) == id_length + def test_registered_extensions_uninitialized(self): # Arrange Image._initialized = 0 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 833473f78..ad0d25add 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3406,7 +3406,8 @@ def register_open(id, factory, accept=None): reject images having another format. """ id = id.upper() - ID.append(id) + if id not in ID: + ID.append(id) OPEN[id] = factory, accept From 1de6c958dfedbe9762266d73559dfc1635b7744c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Jan 2023 18:43:40 +1100 Subject: [PATCH 082/132] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 27dbd69bb..e35a55965 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Handle more than one directory returned by pkg-config #6896 + [sebastic, radarhere] + - Do not retry past formats when loading all formats for the first time #6902 [radarhere] From 446cfddb5d11ae678ccb7a8aac4c948b9555fd3d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Jan 2023 20:05:35 +1100 Subject: [PATCH 083/132] pre-commit autoupdate --- .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 d790e7850..5214d352d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: types: [] - repo: https://github.com/PyCQA/isort - rev: 5.11.4 + rev: 5.12.0 hooks: - id: isort @@ -26,7 +26,7 @@ repos: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.3.1 + rev: v1.4.2 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) @@ -39,7 +39,7 @@ repos: [flake8-2020, flake8-errmsg, flake8-implicit-str-concat] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 + rev: v1.10.0 hooks: - id: python-check-blanket-noqa - id: rst-backticks @@ -57,7 +57,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 0.5.2 + rev: 0.6.1 hooks: - id: tox-ini-fmt From 9932d0cb5ccc9bfc49ee4c7383f215fec06e4b6a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 28 Jan 2023 16:40:11 +0200 Subject: [PATCH 084/132] Sort dependencies --- .github/workflows/test-cygwin.yml | 28 ++++++++++++++++++++++------ .github/workflows/test-mingw.yml | 10 +++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7b8070d34..1dfb36f44 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -34,18 +34,34 @@ jobs: with: platform: x86_64 packages: > - ImageMagick gcc-g++ ghostscript jpeg libfreetype-devel - libimagequant-devel libjpeg-devel liblapack-devel - liblcms2-devel libopenjp2-devel libraqm-devel - libtiff-devel libwebp-devel libxcb-devel libxcb-xinerama0 - make netpbm perl + gcc-g++ + ghostscript + ImageMagick + jpeg + libfreetype-devel + libimagequant-devel + libjpeg-devel + liblapack-devel + liblcms2-devel + libopenjp2-devel + libraqm-devel + libtiff-devel + libwebp-devel + libxcb-devel + libxcb-xinerama0 + make + netpbm + perl python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter - qt5-devel-tools subversion xorg-server-extra zlib-devel + qt5-devel-tools + subversion + xorg-server-extra + zlib-devel - name: Add Lapack to PATH uses: egor-tensin/cleanup-path@v3 diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 6a60bc7f0..24575f6c7 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -45,11 +45,6 @@ jobs: - name: Install dependencies run: | pacman -S --noconfirm \ - ${{ matrix.package }}-python3-cffi \ - ${{ matrix.package }}-python3-numpy \ - ${{ matrix.package }}-python3-olefile \ - ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-setuptools \ ${{ matrix.package }}-freetype \ ${{ matrix.package }}-gcc \ ${{ matrix.package }}-ghostscript \ @@ -60,6 +55,11 @@ jobs: ${{ matrix.package }}-libtiff \ ${{ matrix.package }}-libwebp \ ${{ matrix.package }}-openjpeg2 \ + ${{ matrix.package }}-python3-cffi \ + ${{ matrix.package }}-python3-numpy \ + ${{ matrix.package }}-python3-olefile \ + ${{ matrix.package }}-python3-pip \ + ${{ matrix.package }}-python3-setuptools \ subversion if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then From c8966013bd47fe59f729969463360f12763641c4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 27 Jan 2023 23:19:03 +0200 Subject: [PATCH 085/132] Replace SVN with Git for installing extra test images --- .appveyor.yml | 4 +++- .github/workflows/test-windows.yml | 5 ++++- depends/install_extra_test_images.sh | 21 +++++++++------------ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index b817cd9d8..d4dd2dc95 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -21,9 +21,11 @@ environment: install: - '%PYTHON%\%EXECUTABLE% --version' - curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip +- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - 7z x pillow-depends.zip -oc:\ +- 7z x pillow-test-images.zip -oc:\ - mv c:\pillow-depends-main c:\pillow-depends -- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images +- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images - 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ - ..\pillow-depends\gs1000w32.exe /S - path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH% diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 487c3586f..48825dc30 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -62,7 +62,10 @@ jobs: winbuild\depends\gs1000w32.exe /S echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH - xcopy /S /Y winbuild\depends\test_images\* Tests\images\ + # Install extra test images + curl -fsSL -o pillow-test-images.zip https://github.com/hugovk/test-images/archive/main.zip + 7z x pillow-test-images.zip -oc:\ + xcopy /S /Y c:\test-images-main\* Tests\images\ # make cache key depend on VS version & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 02da12d61..7381d3767 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -1,15 +1,12 @@ -#!/bin/bash +#!/usr/bin/env bash # install extra test images -# Use SVN to just fetch a single Git subdirectory -svn_export() -{ - if [ ! -z $1 ]; then - echo "" - echo "Retrying svn export..." - echo "" - fi +archive=test-images-main - svn export --force https://github.com/python-pillow/pillow-depends/trunk/test_images ../Tests/images -} -svn_export || svn_export retry || svn_export retry || svn_export retry +./download-and-extract.sh $archive https://github.com/hugovk/test-images/archive/refs/heads/main.tar.gz + +mv $archive/* ../Tests/images/ + +# Cleanup old tarball and empty directory +rm $archive.tar.gz +rm -r $archive From 120d56b4ba49871a6d3032b7d0d4c8159e8273ec Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 28 Jan 2023 16:40:11 +0200 Subject: [PATCH 086/132] Sort dependencies --- .github/workflows/test-cygwin.yml | 28 ++++++++++++++++++++++------ .github/workflows/test-mingw.yml | 10 +++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7b8070d34..1dfb36f44 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -34,18 +34,34 @@ jobs: with: platform: x86_64 packages: > - ImageMagick gcc-g++ ghostscript jpeg libfreetype-devel - libimagequant-devel libjpeg-devel liblapack-devel - liblcms2-devel libopenjp2-devel libraqm-devel - libtiff-devel libwebp-devel libxcb-devel libxcb-xinerama0 - make netpbm perl + gcc-g++ + ghostscript + ImageMagick + jpeg + libfreetype-devel + libimagequant-devel + libjpeg-devel + liblapack-devel + liblcms2-devel + libopenjp2-devel + libraqm-devel + libtiff-devel + libwebp-devel + libxcb-devel + libxcb-xinerama0 + make + netpbm + perl python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter - qt5-devel-tools subversion xorg-server-extra zlib-devel + qt5-devel-tools + subversion + xorg-server-extra + zlib-devel - name: Add Lapack to PATH uses: egor-tensin/cleanup-path@v3 diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 6a60bc7f0..24575f6c7 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -45,11 +45,6 @@ jobs: - name: Install dependencies run: | pacman -S --noconfirm \ - ${{ matrix.package }}-python3-cffi \ - ${{ matrix.package }}-python3-numpy \ - ${{ matrix.package }}-python3-olefile \ - ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-setuptools \ ${{ matrix.package }}-freetype \ ${{ matrix.package }}-gcc \ ${{ matrix.package }}-ghostscript \ @@ -60,6 +55,11 @@ jobs: ${{ matrix.package }}-libtiff \ ${{ matrix.package }}-libwebp \ ${{ matrix.package }}-openjpeg2 \ + ${{ matrix.package }}-python3-cffi \ + ${{ matrix.package }}-python3-numpy \ + ${{ matrix.package }}-python3-olefile \ + ${{ matrix.package }}-python3-pip \ + ${{ matrix.package }}-python3-setuptools \ subversion if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then From 7e35e15eeeca12a4eec61d39ff7fc819d08d3554 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 28 Jan 2023 16:40:25 +0200 Subject: [PATCH 087/132] Replace subversion with wget package --- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 1dfb36f44..451181434 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -59,7 +59,7 @@ jobs: python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter qt5-devel-tools - subversion + wget xorg-server-extra zlib-devel diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 24575f6c7..ef8214649 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -60,7 +60,7 @@ jobs: ${{ matrix.package }}-python3-olefile \ ${{ matrix.package }}-python3-pip \ ${{ matrix.package }}-python3-setuptools \ - subversion + ${{ matrix.package }}-wget if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then pacman -S --noconfirm \ From 772567a4ce9ca1890ecbee976bb777e82db2577a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 20:13:28 +1100 Subject: [PATCH 088/132] Switched to python-pillow repositories --- .github/workflows/test-windows.yml | 2 +- depends/install_extra_test_images.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 48825dc30..cf160a997 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -63,7 +63,7 @@ jobs: echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH # Install extra test images - curl -fsSL -o pillow-test-images.zip https://github.com/hugovk/test-images/archive/main.zip + 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\* Tests\images\ diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 7381d3767..ffdfe17f2 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -3,7 +3,7 @@ archive=test-images-main -./download-and-extract.sh $archive https://github.com/hugovk/test-images/archive/refs/heads/main.tar.gz +./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/refs/heads/main.tar.gz mv $archive/* ../Tests/images/ From a119b19c074869267f186051af0d1b0878f9cca0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Feb 2023 08:36:06 +1100 Subject: [PATCH 089/132] Updated libjpeg-turbo to 2.1.5 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index fd12240e5..89903c621 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -109,9 +109,9 @@ header = [ deps = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/2.1.4/libjpeg-turbo-2.1.4.tar.gz/download", - "filename": "libjpeg-turbo-2.1.4.tar.gz", - "dir": "libjpeg-turbo-2.1.4", + + "/libjpeg-turbo/files/2.1.5/libjpeg-turbo-2.1.5.tar.gz/download", + "filename": "libjpeg-turbo-2.1.5.tar.gz", + "dir": "libjpeg-turbo-2.1.5", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" From cc71b4c1b2226330c77210f2243deaa8b254afdc Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 4 Feb 2023 10:04:29 +0200 Subject: [PATCH 090/132] Simpler URL Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- depends/install_extra_test_images.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index ffdfe17f2..941bfbe84 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -3,7 +3,7 @@ archive=test-images-main -./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/refs/heads/main.tar.gz +./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/main.tar.gz mv $archive/* ../Tests/images/ From cb4eb0d40ff0f9b6a6f3deb2c299ed477c1afc00 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 19:25:53 +0000 Subject: [PATCH 091/132] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5214d352d..45c1f3c5f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.1.0 hooks: - id: black args: [--target-version=py37] From 24183d652e02404e1586ff00f7fe4a055b05ce58 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 19:27:15 +0000 Subject: [PATCH 092/132] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/check_fli_overflow.py | 1 - Tests/check_png_dos.py | 1 - Tests/test_bmp_reference.py | 1 - Tests/test_file_bmp.py | 1 - Tests/test_file_bufrstub.py | 2 -- Tests/test_file_dcx.py | 2 -- Tests/test_file_eps.py | 1 - Tests/test_file_fits.py | 1 - Tests/test_file_fli.py | 2 -- Tests/test_file_gif.py | 13 +------------ Tests/test_file_gribstub.py | 2 -- Tests/test_file_hdf5stub.py | 2 -- Tests/test_file_icns.py | 1 - Tests/test_file_ico.py | 1 - Tests/test_file_im.py | 1 - Tests/test_file_iptc.py | 3 --- Tests/test_file_jpeg.py | 13 ------------- Tests/test_file_jpeg2k.py | 1 - Tests/test_file_libtiff.py | 2 -- Tests/test_file_msp.py | 2 -- Tests/test_file_pdf.py | 2 -- Tests/test_file_png.py | 4 ---- Tests/test_file_psd.py | 2 -- Tests/test_file_spider.py | 1 - Tests/test_file_sun.py | 1 - Tests/test_file_tga.py | 3 --- Tests/test_file_tiff.py | 7 ------- Tests/test_file_tiff_metadata.py | 3 --- Tests/test_file_webp_metadata.py | 4 ---- Tests/test_file_wmf.py | 1 - Tests/test_file_xbm.py | 2 -- Tests/test_file_xvthumb.py | 1 - Tests/test_image.py | 2 -- Tests/test_image_convert.py | 1 - Tests/test_image_crop.py | 1 - Tests/test_image_getbbox.py | 1 - Tests/test_image_mode.py | 1 - Tests/test_image_rotate.py | 2 +- Tests/test_image_tobitmap.py | 1 - Tests/test_imagechops.py | 21 --------------------- Tests/test_imagedraw.py | 1 - Tests/test_imagefile.py | 1 - Tests/test_imageops.py | 3 --- Tests/test_imagepalette.py | 3 --- Tests/test_imagepath.py | 1 - Tests/test_imagesequence.py | 1 - Tests/test_imagestat.py | 3 --- Tests/test_lib_image.py | 1 - Tests/test_mode_i16.py | 2 -- Tests/test_numpy.py | 1 - Tests/test_pickle.py | 1 - Tests/test_tiff_ifdrational.py | 2 -- Tests/test_webp_leaks.py | 1 - setup.py | 2 -- src/PIL/BmpImagePlugin.py | 2 -- src/PIL/BufrStubImagePlugin.py | 2 -- src/PIL/CurImagePlugin.py | 2 -- src/PIL/DcxImagePlugin.py | 2 -- src/PIL/EpsImagePlugin.py | 2 -- src/PIL/FitsImagePlugin.py | 1 - src/PIL/FitsStubImagePlugin.py | 1 - src/PIL/FliImagePlugin.py | 2 -- src/PIL/FontFile.py | 1 - src/PIL/FpxImagePlugin.py | 5 ----- src/PIL/GbrImagePlugin.py | 1 - src/PIL/GdImageFile.py | 1 - src/PIL/GifImagePlugin.py | 6 ------ src/PIL/GimpGradientFile.py | 5 ----- src/PIL/GimpPaletteFile.py | 3 --- src/PIL/GribStubImagePlugin.py | 2 -- src/PIL/Hdf5StubImagePlugin.py | 2 -- src/PIL/IcnsImagePlugin.py | 3 +-- src/PIL/IcoImagePlugin.py | 2 +- src/PIL/ImImagePlugin.py | 7 ------- src/PIL/Image.py | 3 +-- src/PIL/ImageDraw.py | 4 ++-- src/PIL/ImageFile.py | 3 --- src/PIL/ImageFont.py | 2 -- src/PIL/ImageOps.py | 2 -- src/PIL/ImagePalette.py | 2 -- src/PIL/ImageShow.py | 1 - src/PIL/ImageTk.py | 2 -- src/PIL/ImtImagePlugin.py | 5 ----- src/PIL/IptcImagePlugin.py | 3 --- src/PIL/JpegImagePlugin.py | 5 ----- src/PIL/McIdasImagePlugin.py | 2 -- src/PIL/MicImagePlugin.py | 2 -- src/PIL/MpegImagePlugin.py | 2 -- src/PIL/MpoImagePlugin.py | 1 - src/PIL/MspImagePlugin.py | 4 ---- src/PIL/PaletteFile.py | 3 --- src/PIL/PalmImagePlugin.py | 4 ---- src/PIL/PcdImagePlugin.py | 2 -- src/PIL/PcfFontFile.py | 7 ------- src/PIL/PcxImagePlugin.py | 3 --- src/PIL/PixarImagePlugin.py | 2 -- src/PIL/PngImagePlugin.py | 17 ----------------- src/PIL/PpmImagePlugin.py | 1 - src/PIL/PsdImagePlugin.py | 4 ---- src/PIL/SgiImagePlugin.py | 2 -- src/PIL/SpiderImagePlugin.py | 3 +-- src/PIL/SunImagePlugin.py | 2 -- src/PIL/TarIO.py | 1 - src/PIL/TgaImagePlugin.py | 3 --- src/PIL/TiffImagePlugin.py | 4 ---- src/PIL/WalImageFile.py | 1 - src/PIL/WebPImagePlugin.py | 1 - src/PIL/WmfImagePlugin.py | 2 -- src/PIL/XVThumbImagePlugin.py | 2 -- src/PIL/XbmImagePlugin.py | 3 --- src/PIL/XpmImagePlugin.py | 7 ------- 111 files changed, 8 insertions(+), 298 deletions(-) diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 08a55d349..c600c45ed 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -4,7 +4,6 @@ TEST_FILE = "Tests/images/fli_overflow.fli" def test_fli_overflow(): - # this should not crash with a malloc error or access violation with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index d8d645189..f4a129f50 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -23,7 +23,6 @@ def test_ignore_dos_text(): def test_dos_text(): - try: im = Image.open(TEST_FILE) im.load() diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index ed9aff9cc..002a44a4f 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -18,7 +18,6 @@ def test_bad(): """These shouldn't crash/dos, but they shouldn't return anything either""" for f in get_files("b"): - # Assert that there is no unclosed file warning with warnings.catch_warnings(): try: diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 5f6d52355..9e79937e9 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -141,7 +141,6 @@ def test_rgba_bitfields(): # This test image has been manually hexedited # to change the bitfield compression in the header from XBGR to RGBA with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: - # So before the comparing the image, swap the channels b, g, r = im.split()[1:] im = Image.merge("RGB", (r, g, b)) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index e330404d6..76f185b9a 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "BUFR" @@ -31,7 +30,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 0f09c4b99..ef378b24a 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -15,7 +15,6 @@ def test_sanity(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.size == (128, 128) assert isinstance(im, DcxImagePlugin.DcxImageFile) @@ -54,7 +53,6 @@ def test_invalid_file(): def test_tell(): # Arrange with Image.open(TEST_FILE) as im: - # Act frame = im.tell() diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 015dda992..ac6e84447 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -80,7 +80,6 @@ def test_invalid_file(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_cmyk(): with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: - assert cmyk_image.mode == "CMYK" assert cmyk_image.size == (100, 100) assert cmyk_image.format == "EPS" diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 447888acd..d2f5a6d17 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -12,7 +12,6 @@ TEST_FILE = "Tests/images/hopper.fits" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "FITS" assert im.size == (128, 128) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index b8b999d70..70d4d76db 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -64,7 +64,6 @@ def test_context_manager(): def test_tell(): # Arrange with Image.open(static_test_file) as im: - # Act frame = im.tell() @@ -110,7 +109,6 @@ def test_eoferror(): def test_seek_tell(): with Image.open(animated_test_file) as im: - layer_number = im.tell() assert layer_number == 0 diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 6fbc0ee30..bce72d192 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -209,7 +209,7 @@ def test_optimize_if_palette_can_be_reduced_by_half(): im = im.resize((591, 443)) im_rgb = im.convert("RGB") - for (optimize, colors) in ((False, 256), (True, 8)): + for optimize, colors in ((False, 256), (True, 8)): out = BytesIO() im_rgb.save(out, "GIF", optimize=optimize) with Image.open(out) as reloaded: @@ -221,7 +221,6 @@ def test_roundtrip(tmp_path): im = hopper() im.save(out) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), im, 50) @@ -232,7 +231,6 @@ def test_roundtrip2(tmp_path): im2 = im.copy() im2.save(out) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), hopper(), 50) @@ -242,7 +240,6 @@ def test_roundtrip_save_all(tmp_path): im = hopper() im.save(out, save_all=True) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), im, 50) # Multiframe image @@ -284,13 +281,11 @@ def test_headers_saving_for_animated_gifs(tmp_path): important_headers = ["background", "version", "duration", "loop"] # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - info = im.info.copy() out = str(tmp_path / "temp.gif") im.save(out, save_all=True) with Image.open(out) as reread: - for header in important_headers: assert info[header] == reread.info[header] @@ -308,7 +303,6 @@ def test_palette_handling(tmp_path): im2.save(f, optimize=True) with Image.open(f) as reloaded: - assert_image_similar(im, reloaded.convert("RGB"), 10) @@ -324,7 +318,6 @@ def test_palette_434(tmp_path): orig = "Tests/images/test.colors.gif" with Image.open(orig) as im: - with roundtrip(im) as reloaded: assert_image_similar(im, reloaded, 1) with roundtrip(im, optimize=True) as reloaded: @@ -575,7 +568,6 @@ def test_save_dispose(tmp_path): ) with Image.open(out) as img: - for i in range(2): img.seek(img.tell() + 1) assert img.disposal_method == i + 1 @@ -773,7 +765,6 @@ def test_multiple_duration(tmp_path): out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: - for duration in duration_list: assert reread.info["duration"] == duration try: @@ -786,7 +777,6 @@ def test_multiple_duration(tmp_path): out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list) ) with Image.open(out) as reread: - for duration in duration_list: assert reread.info["duration"] == duration try: @@ -844,7 +834,6 @@ def test_identical_frames(tmp_path): out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: - # Assert that the first three frames were combined assert reread.n_frames == 2 diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index fd427746e..768ac12bd 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "GRIB" @@ -31,7 +30,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 20b4b9619..98dc5443c 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -8,7 +8,6 @@ TEST_FILE = "Tests/images/hdf5.h5" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "HDF5" @@ -29,7 +28,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 55632909c..42275424d 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -16,7 +16,6 @@ def test_sanity(): # Loading this icon by default should result in the largest size # (512x512@2x) being loaded with Image.open(TEST_FILE) as im: - # Assert that there is no unclosed file warning with warnings.catch_warnings(): im.load() diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index afb17b1af..9c1c3cf17 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -175,7 +175,6 @@ def test_save_256x256(tmp_path): # Act im.save(outfile) with Image.open(outfile) as im_saved: - # Assert assert im_saved.size == (256, 256) diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 5cf93713b..425e690d6 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -51,7 +51,6 @@ def test_context_manager(): def test_tell(): # Arrange with Image.open(TEST_IM) as im: - # Act frame = im.tell() diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 2d0e6977a..2d99528d3 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -11,7 +11,6 @@ TEST_FILE = "Tests/images/iptc.jpg" def test_getiptcinfo_jpg_none(): # Arrange with hopper() as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) @@ -22,7 +21,6 @@ def test_getiptcinfo_jpg_none(): def test_getiptcinfo_jpg_found(): # Arrange with Image.open(TEST_FILE) as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) @@ -35,7 +33,6 @@ def test_getiptcinfo_jpg_found(): def test_getiptcinfo_tiff_none(): # Arrange with Image.open("Tests/images/hopper.tif") as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index eabc6bf75..e3c5abcbd 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -57,7 +57,6 @@ class TestFileJpeg: return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) def test_sanity(self): - # internal version number assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) @@ -368,7 +367,6 @@ class TestFileJpeg: def test_exif_gps_typeerror(self): with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: - # Should not raise a TypeError im._getexif() @@ -682,7 +680,6 @@ class TestFileJpeg: # Shouldn't raise error fn = "Tests/images/sugarshack_bad_mpo_header.jpg" with pytest.warns(UserWarning, Image.open, fn) as im: - # Assert assert im.format == "JPEG" @@ -704,7 +701,6 @@ class TestFileJpeg: # Arrange outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: - # Act im.save(outfile, "JPEG", dpi=im.info["dpi"]) @@ -731,7 +727,6 @@ class TestFileJpeg: # This Photoshop CC 2017 image has DPI in EXIF not metadata # EXIF XResolution is (2000000, 10000) with Image.open("Tests/images/photoshop-200dpi.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (200, 200) @@ -740,7 +735,6 @@ class TestFileJpeg: # This image has DPI in EXIF not metadata # EXIF XResolution is 72 with Image.open("Tests/images/exif-72dpi-int.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (72, 72) @@ -749,7 +743,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg with Image.open("Tests/images/exif-200dpcm.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (508, 508) @@ -758,7 +751,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg with Image.open("Tests/images/exif-dpi-zerodivision.jpg") as im: - # Act / Assert # This should return the default, and not raise a ZeroDivisionError assert im.info.get("dpi") == (72, 72) @@ -767,7 +759,6 @@ class TestFileJpeg: # Arrange # 0x011A tag in this exif contains string '300300\x02' with Image.open("Tests/images/broken_exif_dpi.jpg") as im: - # Act / Assert # This should return the default assert im.info.get("dpi") == (72, 72) @@ -777,7 +768,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with resolution removed from EXIF: # exiftool "-*resolution*"= photoshop-200dpi.jpg with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: - # Act / Assert # "When the image resolution is unknown, 72 [dpi] is designated." # https://exiv2.org/tags.html @@ -787,7 +777,6 @@ class TestFileJpeg: # This is no-dpi-in-exif with the tiff header of the exif block # hexedited from MM * to FF FF FF FF with Image.open("Tests/images/invalid-exif.jpg") as im: - # This should return the default, and not a SyntaxError or # OSError for unidentified image. assert im.info.get("dpi") == (72, 72) @@ -810,7 +799,6 @@ class TestFileJpeg: def test_invalid_exif_x_resolution(self): # When no x or y resolution is defined in EXIF with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im: - # This should return the default, and not a ValueError or # OSError for an unidentified image. assert im.info.get("dpi") == (72, 72) @@ -820,7 +808,6 @@ class TestFileJpeg: # This image has been manually hexedited to have an IFD offset of 10, # in contrast to normal 8 with Image.open("Tests/images/exif-ifd-offset.jpg") as im: - # Act / Assert assert im._getexif()[306] == "2017:03:13 23:03:09" diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 0229b2243..de622c478 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -270,7 +270,6 @@ def test_rgba(): # Arrange with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2: - # Act j2k.load() jp2.load() diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 1109cd15e..f886d3aae 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -645,7 +645,6 @@ class TestFileLibTiff(LibTiffTestCase): pilim = hopper() def save_bytesio(compression=None): - buffer_io = io.BytesIO() pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) @@ -740,7 +739,6 @@ class TestFileLibTiff(LibTiffTestCase): def test_multipage_compression(self): with Image.open("Tests/images/compression.tif") as im: - im.seek(0) assert im._compression == "tiff_ccitt" assert im.size == (10, 10) diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 50d7c590b..497052b05 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -44,7 +44,6 @@ def test_open_windows_v1(): # Arrange # Act with Image.open(TEST_FILE) as im: - # Assert assert_image_equal(im, hopper("1")) assert isinstance(im, MspImagePlugin.MspImageFile) @@ -59,7 +58,6 @@ def _assert_file_image_equal(source_path, target_path): not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) def test_open_windows_v2(): - files = ( os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 5299febe9..216b93ca9 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -89,7 +89,6 @@ def test_save_all(tmp_path): # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") im.save(outfile, save_all=True) @@ -123,7 +122,6 @@ def test_save_all(tmp_path): def test_multiframe_normal_save(tmp_path): # Test saving a multiframe image without save_all with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") im.save(outfile) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 133f3e47e..c4db97905 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -78,7 +78,6 @@ class TestFilePng: return chunks def test_sanity(self, tmp_path): - # internal version number assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib")) @@ -156,7 +155,6 @@ class TestFilePng: assert im.info == {"spam": "egg"} def test_bad_itxt(self): - im = load(HEAD + chunk(b"iTXt") + TAIL) assert im.info == {} @@ -201,7 +199,6 @@ class TestFilePng: assert im.info["spam"].tkey == "Spam" def test_interlace(self): - test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) @@ -495,7 +492,6 @@ class TestFilePng: # Check reading images with null tRNS value, issue #1239 test_file = "Tests/images/tRNS_null_1x1.png" with Image.open(test_file) as im: - assert im.info["transparency"] == 0 def test_save_icc_profile(self): diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 4f934375c..036cb9d4b 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -77,7 +77,6 @@ def test_eoferror(): def test_seek_tell(): with Image.open(test_file) as im: - layer_number = im.tell() assert layer_number == 1 @@ -95,7 +94,6 @@ def test_seek_tell(): def test_seek_eoferror(): with Image.open(test_file) as im: - with pytest.raises(EOFError): im.seek(-1) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 0e3b705a2..011e208d8 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -79,7 +79,6 @@ def test_is_spider_image(): def test_tell(): # Arrange with Image.open(TEST_FILE) as im: - # Act index = im.tell() diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 05c78c316..edb320603 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -16,7 +16,6 @@ def test_sanity(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (128, 128) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 7d8b5139a..bac00e855 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -78,7 +78,6 @@ def test_id_field(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (100, 100) @@ -89,7 +88,6 @@ def test_id_field_rle(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (199, 199) @@ -171,7 +169,6 @@ def test_save_id_section(tmp_path): test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: - # Save with no id section im.save(out, id_section="") with Image.open(out) as test_im: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 4f3c8e390..70142747c 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -25,7 +25,6 @@ except ImportError: class TestFileTiff: def test_sanity(self, tmp_path): - filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename) @@ -157,7 +156,6 @@ class TestFileTiff: def test_xyres_tiff(self): filename = "Tests/images/pil168.tif" with Image.open(filename) as im: - # legacy api assert isinstance(im.tag[X_RESOLUTION][0], tuple) assert isinstance(im.tag[Y_RESOLUTION][0], tuple) @@ -171,7 +169,6 @@ class TestFileTiff: def test_xyres_fallback_tiff(self): filename = "Tests/images/compression.tif" with Image.open(filename) as im: - # v2 api assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) @@ -186,7 +183,6 @@ class TestFileTiff: def test_int_resolution(self): filename = "Tests/images/pil168.tif" with Image.open(filename) as im: - # Try to read a file where X,Y_RESOLUTION are ints im.tag_v2[X_RESOLUTION] = 71 im.tag_v2[Y_RESOLUTION] = 71 @@ -381,7 +377,6 @@ class TestFileTiff: def test___str__(self): filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: - # Act ret = str(im.ifd) @@ -392,7 +387,6 @@ class TestFileTiff: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: - # v2 interface v2_tags = { 256: 55, @@ -630,7 +624,6 @@ class TestFileTiff: filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename, **kwargs) with Image.open(filename) as im: - # legacy interface assert im.tag[X_RESOLUTION][0][0] == 72 assert im.tag[Y_RESOLUTION][0][0] == 36 diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 1061f7d05..a4481d85f 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -54,7 +54,6 @@ def test_rt_metadata(tmp_path): img.save(f, tiffinfo=info) with Image.open(f) as loaded: - assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) @@ -74,14 +73,12 @@ def test_rt_metadata(tmp_path): info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) img.save(f, tiffinfo=info) with Image.open(f) as loaded: - assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) def test_read_metadata(): with Image.open("Tests/images/hopper_g4.tif") as img: - assert { "YResolution": IFDRational(4294967295, 113653537), "PlanarConfiguration": 1, diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 4f513d82b..037479f9f 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -18,10 +18,8 @@ except ImportError: def test_read_exif_metadata(): - file_path = "Tests/images/flower.webp" with Image.open(file_path) as image: - assert image.format == "WEBP" exif_data = image.info.get("exif", None) assert exif_data @@ -64,10 +62,8 @@ def test_write_exif_metadata(): def test_read_icc_profile(): - file_path = "Tests/images/flower2.webp" with Image.open(file_path) as image: - assert image.format == "WEBP" assert image.info.get("icc_profile", None) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 439cb15bc..7c8b54fd1 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -6,7 +6,6 @@ from .helper import assert_image_similar_tofile, hopper def test_load_raw(): - # Test basic EMF open and rendering with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 9c54c6755..d2c05b78a 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -44,7 +44,6 @@ def test_open(): # Act with Image.open(filename) as im: - # Assert assert im.mode == "1" assert im.size == (128, 128) @@ -57,7 +56,6 @@ def test_open_filename_with_underscore(): # Act with Image.open(filename) as im: - # Assert assert im.mode == "1" assert im.size == (128, 128) diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index ae53d2b63..9efe7ec14 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/hopper.p7" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "XVThumb" diff --git a/Tests/test_image.py b/Tests/test_image.py index ad3346b5a..85e3ff55b 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -69,7 +69,6 @@ class TestImage: assert issubclass(UnidentifiedImageError, OSError) def test_sanity(self): - im = Image.new("L", (100, 100)) assert repr(im)[:45] == ">> im.save('Tests/images/hopper_45.png') with Image.open("Tests/images/hopper_45.png") as target: - for (resample, epsilon) in ( + for resample, epsilon in ( (Image.Resampling.NEAREST, 10), (Image.Resampling.BILINEAR, 5), (Image.Resampling.BICUBIC, 0), diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 178cfcef3..a12ce329f 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -4,7 +4,6 @@ from .helper import assert_image_equal, fromstring, hopper def test_sanity(): - with pytest.raises(ValueError): hopper().tobitmap() diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index b839a7b14..d0fea3854 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -50,7 +50,6 @@ def test_add(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add(im1, im2) @@ -63,7 +62,6 @@ def test_add_scale_offset(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add(im1, im2, scale=2.5, offset=100) @@ -87,7 +85,6 @@ def test_add_modulo(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add_modulo(im1, im2) @@ -111,7 +108,6 @@ def test_blend(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.blend(im1, im2, 0.5) @@ -137,7 +133,6 @@ def test_darker_image(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.darker(im1, im2) @@ -149,7 +144,6 @@ def test_darker_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.darker(im1, im2) @@ -161,7 +155,6 @@ def test_difference(): # Arrange with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1: with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2: - # Act new = ImageChops.difference(im1, im2) @@ -173,7 +166,6 @@ def test_difference_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: - # Act new = ImageChops.difference(im1, im2) @@ -195,7 +187,6 @@ def test_duplicate(): def test_invert(): # Arrange with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: - # Act new = ImageChops.invert(im) @@ -209,7 +200,6 @@ def test_lighter_image(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.lighter(im1, im2) @@ -221,7 +211,6 @@ def test_lighter_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.lighter(im1, im2) @@ -275,7 +264,6 @@ def test_offset(): xoffset = 45 yoffset = 20 with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im: - # Act new = ImageChops.offset(im, xoffset, yoffset) @@ -292,7 +280,6 @@ def test_screen(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.screen(im1, im2) @@ -305,7 +292,6 @@ def test_subtract(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2) @@ -319,7 +305,6 @@ def test_subtract_scale_offset(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) @@ -332,7 +317,6 @@ def test_subtract_clip(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2) @@ -344,7 +328,6 @@ def test_subtract_modulo(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract_modulo(im1, im2) @@ -358,7 +341,6 @@ def test_subtract_modulo_no_clip(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.subtract_modulo(im1, im2) @@ -370,7 +352,6 @@ def test_soft_light(): # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act new = ImageChops.soft_light(im1, im2) @@ -383,7 +364,6 @@ def test_hard_light(): # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act new = ImageChops.hard_light(im1, im2) @@ -396,7 +376,6 @@ def test_overlay(): # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act new = ImageChops.overlay(im1, im2) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 4c4c41b7b..d4723c924 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -52,7 +52,6 @@ def test_sanity(): def test_valueerror(): with Image.open("Tests/images/chi.gif") as im: - draw = ImageDraw.Draw(im) draw.line((0, 0), fill=(0, 0, 0)) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index fc0fbfb9b..412bc10d9 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -30,7 +30,6 @@ SAFEBLOCK = ImageFile.SAFEBLOCK class TestImageFile: def test_parser(self): def roundtrip(format): - im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index c9b2fd865..d390f3c1e 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -21,7 +21,6 @@ deformer = Deformer() def test_sanity(): - ImageOps.autocontrast(hopper("L")) ImageOps.autocontrast(hopper("RGB")) @@ -419,7 +418,6 @@ def test_autocontrast_cutoff(): def test_autocontrast_mask_toy_input(): # Test the mask argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: - rect_mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(rect_mask) x0 = img.size[0] // 4 @@ -439,7 +437,6 @@ def test_autocontrast_mask_toy_input(): def test_autocontrast_mask_real_input(): # Test the autocontrast with a rectangular mask with Image.open("Tests/images/iptc.jpg") as img: - rect_mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(rect_mask) x0, y0 = img.size[0] // 2, img.size[1] // 2 diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 5bda28117..ac99ef381 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -6,7 +6,6 @@ from .helper import assert_image_equal, assert_image_equal_tofile def test_sanity(): - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) assert len(palette.colors) == 256 @@ -23,7 +22,6 @@ def test_reload(): def test_getcolor(): - palette = ImagePalette.ImagePalette() assert len(palette.palette) == 0 assert len(palette.colors) == 0 @@ -84,7 +82,6 @@ def test_getcolor_not_special(index, palette): def test_file(tmp_path): - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) f = str(tmp_path / "temp.lut") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 861fb64f0..8f8a9f449 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -8,7 +8,6 @@ from PIL import Image, ImagePath def test_path(): - p = ImagePath.Path(list(range(10))) # sequence interface diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 6af7e7602..62f528332 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -6,7 +6,6 @@ from .helper import assert_image_equal, hopper, skip_unless_feature def test_sanity(tmp_path): - test_file = str(tmp_path / "temp.im") im = hopper("RGB") diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 5717fe150..b3b5db13f 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -6,7 +6,6 @@ from .helper import hopper def test_sanity(): - im = hopper() st = ImageStat.Stat(im) @@ -31,7 +30,6 @@ def test_sanity(): def test_hopper(): - im = hopper() st = ImageStat.Stat(im) @@ -45,7 +43,6 @@ def test_hopper(): def test_constant(): - im = Image.new("L", (128, 128), 128) st = ImageStat.Stat(im) diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 37ed3659d..f6818be46 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -4,7 +4,6 @@ from PIL import Image def test_setmode(): - im = Image.new("L", (1, 1), 255) im.im.setmode("1") assert im.im.getpixel((0, 0)) == 255 diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index efcdab9ec..dcdee3d41 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -42,7 +42,6 @@ def test_basic(tmp_path, mode): im_in.save(filename) with Image.open(filename) as im_out: - verify(im_in) verify(im_out) @@ -87,7 +86,6 @@ def test_tobytes(): def test_convert(): - im = original.copy() verify(im.convert("I;16")) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 3de7ec30f..a8bbcbdb8 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -235,7 +235,6 @@ def test_no_resource_warning_for_numpy_array(): test_file = "Tests/images/hopper.png" with Image.open(test_file) as im: - # Act/Assert with warnings.catch_warnings(): array(im) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 23eb9e39f..2f6d05888 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -89,7 +89,6 @@ def test_pickle_la_mode_with_palette(tmp_path): def test_pickle_tell(): # Arrange with Image.open("Tests/images/hopper.webp") as image: - # Act: roundtrip unpickled_image = pickle.loads(pickle.dumps(image)) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 12f475df0..6e3fcec90 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -7,7 +7,6 @@ from .helper import hopper def _test_equal(num, denom, target): - t = IFDRational(num, denom) assert target == t @@ -15,7 +14,6 @@ def _test_equal(num, denom, target): def test_sanity(): - _test_equal(1, 1, 1) _test_equal(1, 1, Fraction(1, 1)) diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 34197c14f..5bd9bacdb 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -9,7 +9,6 @@ test_file = "Tests/images/hopper.webp" @skip_unless_feature("webp") class TestWebPLeaks(PillowLeakTestCase): - mem_limit = 3 * 1024 # kb iterations = 100 diff --git a/setup.py b/setup.py index 4382c1a97..8f7f223f8 100755 --- a/setup.py +++ b/setup.py @@ -430,7 +430,6 @@ class pil_build_ext(build_ext): return sdk_path def build_extensions(self): - library_dirs = [] include_dirs = [] @@ -917,7 +916,6 @@ class pil_build_ext(build_ext): self.summary_report(feature) def summary_report(self, feature): - print("-" * 68) print("PIL SETUP SUMMARY") print("-" * 68) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index e13b18f27..5bda0a5b0 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -223,7 +223,6 @@ class BmpImageFile(ImageFile.ImageFile): # --------------- Once the header is processed, process the palette/LUT if self.mode == "P": # Paletted for 1, 4 and 8 bit images - # ---------------------------------------------------- 1-bit images if not (0 < file_info["colors"] <= 65536): msg = f"Unsupported BMP Palette size ({file_info['colors']})" @@ -360,7 +359,6 @@ class BmpRleDecoder(ImageFile.PyDecoder): # Image plugin for the DIB format (BMP alias) # ============================================================================= class DibImageFile(BmpImageFile): - format = "DIB" format_description = "Windows Bitmap" diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index a0da1b786..0425bbd75 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class BufrStubImageFile(ImageFile.StubImageFile): - format = "BUFR" format_description = "BUFR" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(4)): diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index aedc6ce7f..94efff341 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -32,12 +32,10 @@ def _accept(prefix): class CurImageFile(BmpImagePlugin.BmpImageFile): - format = "CUR" format_description = "Windows Cursor" def _open(self): - offset = self.fp.tell() # check magic diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index 81c0314f0..cde9d42f0 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -37,13 +37,11 @@ def _accept(prefix): class DcxImageFile(PcxImageFile): - format = "DCX" format_description = "Intel DCX" _close_exclusive_fp_after_loading = False def _open(self): - # Header s = self.fp.read(4) if not _accept(s): diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index dd68c13e5..60cb46df9 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -286,7 +286,6 @@ class EpsImageFile(ImageFile.ImageFile): # Scan for an "ImageData" descriptor while s[:1] == "%": - if len(s) > 255: msg = "not an EPS file" raise SyntaxError(msg) @@ -317,7 +316,6 @@ class EpsImageFile(ImageFile.ImageFile): raise OSError(msg) def _find_offset(self, fp): - s = fp.read(4) if s == b"%!PS": diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 536bc1fe6..1185ef2d3 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -19,7 +19,6 @@ def _accept(prefix): class FitsImageFile(ImageFile.ImageFile): - format = "FITS" format_description = "FITS" diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py index 86eb2d5a2..50948ec42 100644 --- a/src/PIL/FitsStubImagePlugin.py +++ b/src/PIL/FitsStubImagePlugin.py @@ -44,7 +44,6 @@ def register_handler(handler): class FITSStubImageFile(ImageFile.StubImageFile): - format = FitsImagePlugin.FitsImageFile.format format_description = FitsImagePlugin.FitsImageFile.format_description diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 66681939d..f4e89a03e 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -40,13 +40,11 @@ def _accept(prefix): class FliImageFile(ImageFile.ImageFile): - format = "FLI" format_description = "Autodesk FLI/FLC Animation" _close_exclusive_fp_after_loading = False def _open(self): - # HEAD s = self.fp.read(128) if not (_accept(s) and s[20:22] == b"\x00\x00"): diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index c5fc80b37..5ec0a6632 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -36,7 +36,6 @@ class FontFile: bitmap = None def __init__(self): - self.info = {} self.glyph = [None] * 256 diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 8ddc6b40b..d145d01f7 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -48,7 +48,6 @@ def _accept(prefix): class FpxImageFile(ImageFile.ImageFile): - format = "FPX" format_description = "FlashPix" @@ -157,7 +156,6 @@ class FpxImageFile(ImageFile.ImageFile): self.tile = [] for i in range(0, len(s), length): - x1 = min(xsize, x + xtile) y1 = min(ysize, y + ytile) @@ -174,7 +172,6 @@ class FpxImageFile(ImageFile.ImageFile): ) elif compression == 1: - # FIXME: the fill decoder is not implemented self.tile.append( ( @@ -186,7 +183,6 @@ class FpxImageFile(ImageFile.ImageFile): ) elif compression == 2: - internal_color_conversion = s[14] jpeg_tables = s[15] rawmode = self.rawmode @@ -234,7 +230,6 @@ class FpxImageFile(ImageFile.ImageFile): self.fp = None def load(self): - if not self.fp: self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 828a45ced..994a6e8eb 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -37,7 +37,6 @@ def _accept(prefix): class GbrImageFile(ImageFile.ImageFile): - format = "GBR" format_description = "GIMP brush file" diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 3875dc866..7dda4f143 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -44,7 +44,6 @@ class GdImageFile(ImageFile.ImageFile): format_description = "GD uncompressed images" def _open(self): - # Header s = self.fp.read(1037) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 6ee1bd3d8..eadee1560 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -61,7 +61,6 @@ def _accept(prefix): class GifImageFile(ImageFile.ImageFile): - format = "GIF" format_description = "Compuserve GIF" _close_exclusive_fp_after_loading = False @@ -81,7 +80,6 @@ class GifImageFile(ImageFile.ImageFile): return False def _open(self): - # Screen s = self.fp.read(13) if not _accept(s): @@ -157,7 +155,6 @@ class GifImageFile(ImageFile.ImageFile): raise EOFError(msg) from e def _seek(self, frame, update_image=True): - if frame == 0: # rewind self.__offset = 0 @@ -195,7 +192,6 @@ class GifImageFile(ImageFile.ImageFile): interlace = None frame_dispose_extent = None while True: - if not s: s = self.fp.read(1) if not s or s == b";": @@ -579,7 +575,6 @@ def _getbbox(base_im, im_frame): def _write_multiple_frames(im, fp, palette): - duration = im.encoderinfo.get("duration") disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) @@ -752,7 +747,6 @@ def _write_local_header(fp, im, offset, flags): def _save_netpbm(im, fp, filename): - # Unused by default. # To use, uncomment the register_save call at the end of the file. # diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index b5c5e3ca4..8e801be0b 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -64,18 +64,15 @@ SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] class GradientFile: - gradient = None def getpalette(self, entries=256): - palette = [] ix = 0 x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] for i in range(entries): - x = i / (entries - 1) while x1 < x: @@ -105,7 +102,6 @@ class GimpGradientFile(GradientFile): """File handler for GIMP's gradient format.""" def __init__(self, fp): - if fp.readline()[:13] != b"GIMP Gradient": msg = "not a GIMP gradient file" raise SyntaxError(msg) @@ -121,7 +117,6 @@ class GimpGradientFile(GradientFile): gradient = [] for i in range(count): - s = fp.readline().split() w = [float(x) for x in s[:11]] diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 2e9cbe58d..d38892894 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -25,7 +25,6 @@ class GimpPaletteFile: rawmode = "RGB" def __init__(self, fp): - self.palette = [o8(i) * 3 for i in range(256)] if fp.readline()[:12] != b"GIMP Palette": @@ -33,7 +32,6 @@ class GimpPaletteFile: raise SyntaxError(msg) for i in range(256): - s = fp.readline() if not s: break @@ -55,5 +53,4 @@ class GimpPaletteFile: self.palette = b"".join(self.palette) def getpalette(self): - return self.palette, self.rawmode diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 2088eb7b0..8a799f19c 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class GribStubImageFile(ImageFile.StubImageFile): - format = "GRIB" format_description = "GRIB" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(8)): diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index d6f283739..bba05ed65 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class HDF5StubImageFile(ImageFile.StubImageFile): - format = "HDF5" format_description = "HDF5" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(8)): diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index e76d0c35a..c2f050edd 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -135,7 +135,6 @@ def read_png_or_jpeg2000(fobj, start_length, size): class IcnsFile: - SIZES = { (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)], (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)], @@ -189,7 +188,7 @@ class IcnsFile: def itersizes(self): sizes = [] for size, fmts in self.SIZES.items(): - for (fmt, reader) in fmts: + for fmt, reader in fmts: if fmt in self.dct: sizes.append(size) break diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 568e6d38d..a188f8fdc 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -185,7 +185,7 @@ class IcoFile: return {(h["width"], h["height"]) for h in self.entry} def getentryindex(self, size, bpp=False): - for (i, h) in enumerate(self.entry): + for i, h in enumerate(self.entry): if size == h["dim"] and (bpp is False or bpp == h["color_depth"]): return i return 0 diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 875a20326..746743f65 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -115,13 +115,11 @@ def number(s): class ImImageFile(ImageFile.ImageFile): - format = "IM" format_description = "IFUNC Image Memory" _close_exclusive_fp_after_loading = False def _open(self): - # Quick rejection: if there's not an LF among the first # 100 bytes, this is (probably) not a text header. @@ -140,7 +138,6 @@ class ImImageFile(ImageFile.ImageFile): self.rawmode = "L" while True: - s = self.fp.read(1) # Some versions of IFUNC uses \n\r instead of \r\n... @@ -169,7 +166,6 @@ class ImImageFile(ImageFile.ImageFile): raise SyntaxError(msg) from e if m: - k, v = m.group(1, 2) # Don't know if this is the correct encoding, @@ -200,7 +196,6 @@ class ImImageFile(ImageFile.ImageFile): n += 1 else: - msg = "Syntax error in IM header: " + s.decode("ascii", "replace") raise SyntaxError(msg) @@ -252,7 +247,6 @@ class ImImageFile(ImageFile.ImageFile): self._fp = self.fp # FIXME: hack if self.rawmode[:2] == "F;": - # ifunc95 formats try: # use bit decoder (if necessary) @@ -332,7 +326,6 @@ SAVE = { def _save(im, fp, filename): - try: image_type, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ad0d25add..81123d070 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -153,6 +153,7 @@ def isImageType(t): # # Constants + # transpose class Transpose(IntEnum): FLIP_LEFT_RIGHT = 0 @@ -391,7 +392,6 @@ def init(): def _getdecoder(mode, decoder_name, args, extra=()): - # tweak arguments if args is None: args = () @@ -415,7 +415,6 @@ def _getdecoder(mode, decoder_name, args, extra=()): def _getencoder(mode, encoder_name, args, extra=()): - # tweak arguments if args is None: args = () diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index ce29a163b..163828d31 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -928,8 +928,8 @@ def floodfill(image, xy, value, border=None, thresh=0): full_edge = set() while edge: new_edge = set() - for (x, y) in edge: # 4 adjacent method - for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): + for x, y in edge: # 4 adjacent method + for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): # If already processed, or if a coordinate is negative, skip if (s, t) in full_edge or s < 0 or t < 0: continue diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 12391955f..132490a8e 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -395,7 +395,6 @@ class Parser: # parse what we have if self.decoder: - if self.offset > 0: # skip header skip = min(len(self.data), self.offset) @@ -420,14 +419,12 @@ class Parser: self.data = self.data[n:] elif self.image: - # if we end up here with no decoder, this file cannot # be incrementally parsed. wait until we've gotten all # available data pass else: - # attempt to open this file try: with io.BytesIO(self.data) as fp: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b144c3dd2..bd13c391e 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -90,7 +90,6 @@ class ImageFont: """PIL font wrapper""" def _load_pilfont(self, filename): - with open(filename, "rb") as fp: image = None for ext in (".png", ".gif", ".pbm"): @@ -116,7 +115,6 @@ class ImageFont: image.close() def _load_pilfont_data(self, file, image): - # read PILfont header if file.readline() != b"PILfont\n": msg = "Not a PILfont file" diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 16c83f4e4..301c593c7 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -205,7 +205,6 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the mapping (2-color) if mid is None: - range_map = range(0, whitepoint - blackpoint) for i in range_map: @@ -215,7 +214,6 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the mapping (3-color) else: - range_map1 = range(0, midpoint - blackpoint) range_map2 = range(0, whitepoint - midpoint) diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index fe0d32155..e455c0459 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -248,11 +248,9 @@ def wedge(mode="RGB"): def load(filename): - # FIXME: supports GIMP gradients only with open(filename, "rb") as fp: - for paletteHandler in [ GimpPaletteFile.GimpPaletteFile, GimpGradientFile.GimpGradientFile, diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 29d900bef..f0e73fb90 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -390,7 +390,6 @@ else: if __name__ == "__main__": - if len(sys.argv) < 2: print("Syntax: python3 ImageShow.py imagefile [title]") sys.exit() diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 09a6356fa..ef569ed2e 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -97,7 +97,6 @@ class PhotoImage: """ def __init__(self, image=None, size=None, **kw): - # Tk compatibility: file or data if image is None: image = _get_image_from_kw(kw) @@ -209,7 +208,6 @@ class BitmapImage: """ def __init__(self, image=None, **kw): - # Tk compatibility: file or data if image is None: image = _get_image_from_kw(kw) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index cfeadd53c..ac267457b 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -30,12 +30,10 @@ field = re.compile(rb"([a-z]*) ([^ \r\n]*)") class ImtImageFile(ImageFile.ImageFile): - format = "IMT" format_description = "IM Tools" def _open(self): - # Quick rejection: if there's not a LF among the first # 100 bytes, this is (probably) not a text header. @@ -47,7 +45,6 @@ class ImtImageFile(ImageFile.ImageFile): xsize = ysize = 0 while True: - if buffer: s = buffer[:1] buffer = buffer[1:] @@ -57,7 +54,6 @@ class ImtImageFile(ImageFile.ImageFile): break if s == b"\x0C": - # image data begins self.tile = [ ( @@ -71,7 +67,6 @@ class ImtImageFile(ImageFile.ImageFile): break else: - # read key/value pair if b"\n" not in buffer: buffer += self.fp.read(100) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 774817569..4c47b55c1 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -48,7 +48,6 @@ def dump(c): class IptcImageFile(ImageFile.ImageFile): - format = "IPTC" format_description = "IPTC/NAA" @@ -84,7 +83,6 @@ class IptcImageFile(ImageFile.ImageFile): return tag, size def _open(self): - # load descriptive fields while True: offset = self.fp.tell() @@ -134,7 +132,6 @@ class IptcImageFile(ImageFile.ImageFile): ] def load(self): - if len(self.tile) != 1 or self.tile[0][0] != "iptc": return ImageFile.ImageFile.load(self) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index b9c80236e..d7ddbe0d9 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -344,12 +344,10 @@ def _accept(prefix): class JpegImageFile(ImageFile.ImageFile): - format = "JPEG" format_description = "JPEG (ISO 10918)" def _open(self): - s = self.fp.read(3) if not _accept(s): @@ -370,7 +368,6 @@ class JpegImageFile(ImageFile.ImageFile): self.icclist = [] while True: - i = s[0] if i == 0xFF: s = s + self.fp.read(1) @@ -418,7 +415,6 @@ class JpegImageFile(ImageFile.ImageFile): return s def draft(self, mode, size): - if len(self.tile) != 1: return @@ -455,7 +451,6 @@ class JpegImageFile(ImageFile.ImageFile): return self.mode, box def load_djpeg(self): - # ALTERNATIVE: handle JPEGs via the IJG command line utilities f, path = tempfile.mkstemp() diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 8d4d826aa..17c008b9a 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -30,12 +30,10 @@ def _accept(s): class McIdasImageFile(ImageFile.ImageFile): - format = "MCIDAS" format_description = "McIdas area file" def _open(self): - # parse area file directory s = self.fp.read(256) if not _accept(s) or len(s) != 256: diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index e7e1054a3..8dd9f2909 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -34,13 +34,11 @@ def _accept(prefix): class MicImageFile(TiffImagePlugin.TiffImageFile): - format = "MIC" format_description = "Microsoft Image Composer" _close_exclusive_fp_after_loading = False def _open(self): - # read the OLE directory and see if this is a likely # to be a Microsoft Image Composer file diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index 2d799d6d8..d96d3a11c 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -58,12 +58,10 @@ class BitStream: class MpegImageFile(ImageFile.ImageFile): - format = "MPEG" format_description = "MPEG" def _open(self): - s = BitStream(self.fp) if s.read(32) != 0x1B3: diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index b1ec2c7bc..f9261c77d 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -101,7 +101,6 @@ def _save_all(im, fp, filename): class MpoImageFile(JpegImagePlugin.JpegImageFile): - format = "MPO" format_description = "MPO (CIPA DC-007)" _close_exclusive_fp_after_loading = False diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 5420894dc..c6567b2ae 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -44,12 +44,10 @@ def _accept(prefix): class MspImageFile(ImageFile.ImageFile): - format = "MSP" format_description = "Windows Paint" def _open(self): - # Header s = self.fp.read(32) if not _accept(s): @@ -111,7 +109,6 @@ class MspDecoder(ImageFile.PyDecoder): _pulls_fd = True def decode(self, buffer): - img = io.BytesIO() blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) try: @@ -162,7 +159,6 @@ Image.register_decoder("MSP", MspDecoder) def _save(im, fp, filename): - if im.mode != "1": msg = f"cannot write mode {im.mode} as MSP" raise OSError(msg) diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index 07acd5580..4a2c497fc 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -22,11 +22,9 @@ class PaletteFile: rawmode = "RGB" def __init__(self, fp): - self.palette = [(i, i, i) for i in range(256)] while True: - s = fp.readline() if not s: @@ -50,5 +48,4 @@ class PaletteFile: self.palette = b"".join(self.palette) def getpalette(self): - return self.palette, self.rawmode diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 109aad9ab..a88a90791 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -112,9 +112,7 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} def _save(im, fp, filename): - if im.mode == "P": - # we assume this is a color Palm image with the standard colormap, # unless the "info" dict has a "custom-colormap" field @@ -147,14 +145,12 @@ def _save(im, fp, filename): version = 1 elif im.mode == "1": - # monochrome -- write it inverted, as is the Palm standard rawmode = "1;I" bpp = 1 version = 0 else: - msg = f"cannot write mode {im.mode} as Palm" raise OSError(msg) diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 5802d386a..e390f3fe5 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -24,12 +24,10 @@ from . import Image, ImageFile class PcdImageFile(ImageFile.ImageFile): - format = "PCD" format_description = "Kodak PhotoCD" def _open(self): - # rough self.fp.seek(2048) s = self.fp.read(2048) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index ecce1b097..d5f510f03 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -58,7 +58,6 @@ class PcfFontFile(FontFile.FontFile): name = "name" def __init__(self, fp, charset_encoding="iso8859-1"): - self.charset_encoding = charset_encoding magic = l32(fp.read(4)) @@ -92,7 +91,6 @@ class PcfFontFile(FontFile.FontFile): self.glyph[ch] = glyph def _getformat(self, tag): - format, size, offset = self.toc[tag] fp = self.fp @@ -108,7 +106,6 @@ class PcfFontFile(FontFile.FontFile): return fp, format, i16, i32 def _load_properties(self): - # # font properties @@ -136,7 +133,6 @@ class PcfFontFile(FontFile.FontFile): return properties def _load_metrics(self): - # # font metrics @@ -147,7 +143,6 @@ class PcfFontFile(FontFile.FontFile): append = metrics.append if (format & 0xFF00) == 0x100: - # "compressed" metrics for i in range(i16(fp.read(2))): left = i8(fp.read(1)) - 128 @@ -160,7 +155,6 @@ class PcfFontFile(FontFile.FontFile): append((xsize, ysize, left, right, width, ascent, descent, 0)) else: - # "jumbo" metrics for i in range(i32(fp.read(4))): left = i16(fp.read(2)) @@ -176,7 +170,6 @@ class PcfFontFile(FontFile.FontFile): return metrics def _load_bitmaps(self, metrics): - # # bitmap data diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 3202475dc..f42c2456b 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -45,12 +45,10 @@ def _accept(prefix): class PcxImageFile(ImageFile.ImageFile): - format = "PCX" format_description = "Paintbrush" def _open(self): - # header s = self.fp.read(128) if not _accept(s): @@ -143,7 +141,6 @@ SAVE = { def _save(im, fp, filename): - try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 8d0a34dba..7eb82228a 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -35,12 +35,10 @@ def _accept(prefix): class PixarImageFile(ImageFile.ImageFile): - format = "PIXAR" format_description = "PIXAR raster image" def _open(self): - # assuming a 4-byte magic label s = self.fp.read(4) if not _accept(s): diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index b6626bbc5..9078957dc 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -161,7 +161,6 @@ def _crc32(data, seed=0): class ChunkStream: def __init__(self, fp): - self.fp = fp self.queue = [] @@ -195,7 +194,6 @@ class ChunkStream: self.queue = self.fp = None def push(self, cid, pos, length): - self.queue.append((cid, pos, length)) def call(self, cid, pos, length): @@ -230,7 +228,6 @@ class ChunkStream: self.fp.read(4) def verify(self, endchunk=b"IEND"): - # Simple approach; just calculate checksum for all remaining # blocks. Must be called directly after open. @@ -397,7 +394,6 @@ class PngStream(ChunkStream): self._seq_num = self.rewind_state["seq_num"] def chunk_iCCP(self, pos, length): - # ICC profile s = ImageFile._safe_read(self.fp, length) # according to PNG spec, the iCCP chunk contains: @@ -425,7 +421,6 @@ class PngStream(ChunkStream): return s def chunk_IHDR(self, pos, length): - # image header s = ImageFile._safe_read(self.fp, length) if length < 13: @@ -446,7 +441,6 @@ class PngStream(ChunkStream): return s def chunk_IDAT(self, pos, length): - # image data if "bbox" in self.im_info: tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] @@ -459,12 +453,10 @@ class PngStream(ChunkStream): raise EOFError def chunk_IEND(self, pos, length): - # end of PNG image raise EOFError def chunk_PLTE(self, pos, length): - # palette s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": @@ -472,7 +464,6 @@ class PngStream(ChunkStream): return s def chunk_tRNS(self, pos, length): - # transparency s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": @@ -524,7 +515,6 @@ class PngStream(ChunkStream): return s def chunk_pHYs(self, pos, length): - # pixels per unit s = ImageFile._safe_read(self.fp, length) if length < 9: @@ -542,7 +532,6 @@ class PngStream(ChunkStream): return s def chunk_tEXt(self, pos, length): - # text s = ImageFile._safe_read(self.fp, length) try: @@ -562,7 +551,6 @@ class PngStream(ChunkStream): return s def chunk_zTXt(self, pos, length): - # compressed text s = ImageFile._safe_read(self.fp, length) try: @@ -597,7 +585,6 @@ class PngStream(ChunkStream): return s def chunk_iTXt(self, pos, length): - # international text r = s = ImageFile._safe_read(self.fp, length) try: @@ -721,12 +708,10 @@ def _accept(prefix): class PngImageFile(ImageFile.ImageFile): - format = "PNG" format_description = "Portable network graphics" def _open(self): - if not _accept(self.fp.read(8)): msg = "not a PNG file" raise SyntaxError(msg) @@ -740,7 +725,6 @@ class PngImageFile(ImageFile.ImageFile): self.png = PngStream(self.fp) while True: - # # get next chunk @@ -1264,7 +1248,6 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): mode = im.mode if mode == "P": - # # attempt to minimize storage requirements for palette images if "bits" in im.encoderinfo: diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index dee2f1e15..5aa418044 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -51,7 +51,6 @@ def _accept(prefix): class PpmImageFile(ImageFile.ImageFile): - format = "PPM" format_description = "Pbmplus image" diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 7e8d12759..5a5d60d56 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -51,13 +51,11 @@ def _accept(prefix): class PsdImageFile(ImageFile.ImageFile): - format = "PSD" format_description = "Adobe Photoshop" _close_exclusive_fp_after_loading = False def _open(self): - read = self.fp.read # @@ -177,7 +175,6 @@ def _layerinfo(fp, ct_bytes): raise SyntaxError(msg) for _ in range(abs(ct)): - # bounding box y0 = i32(read(4)) x0 = i32(read(4)) @@ -250,7 +247,6 @@ def _layerinfo(fp, ct_bytes): def _maketile(file, mode, bbox, channels): - tile = None read = file.read diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index d533c55e5..3662ffd15 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -49,12 +49,10 @@ MODES = { ## # Image plugin for SGI images. class SgiImageFile(ImageFile.ImageFile): - format = "SGI" format_description = "SGI Image File Format" def _open(self): - # HEAD headlen = 512 s = self.fp.read(headlen) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 1192c2d73..eac27e679 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -91,7 +91,6 @@ def isSpiderImage(filename): class SpiderImageFile(ImageFile.ImageFile): - format = "SPIDER" format_description = "Spider 2D image" _close_exclusive_fp_after_loading = False @@ -200,6 +199,7 @@ class SpiderImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # Image series + # given a list of filenames, return a list of images def loadImageSeries(filelist=None): """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" @@ -289,7 +289,6 @@ Image.register_open(SpiderImageFile.format, SpiderImageFile) Image.register_save(SpiderImageFile.format, _save_spider) if __name__ == "__main__": - if len(sys.argv) < 2: print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]") sys.exit() diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index c64de4444..6712583d7 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -30,12 +30,10 @@ def _accept(prefix): class SunImageFile(ImageFile.ImageFile): - format = "SUN" format_description = "Sun Raster File" def _open(self): - # The Sun Raster file header is 32 bytes in length # and has the following format: diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index 20e8a083f..32928f6af 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -32,7 +32,6 @@ class TarIO(ContainerIO.ContainerIO): self.fh = open(tarfile, "rb") while True: - s = self.fh.read(512) if len(s) != 512: msg = "unexpected end of tar file" diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 53fe6ef5c..67dfc3d3c 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -46,12 +46,10 @@ MODES = { class TgaImageFile(ImageFile.ImageFile): - format = "TGA" format_description = "Targa" def _open(self): - # process header s = self.fp.read(18) @@ -174,7 +172,6 @@ SAVE = { def _save(im, fp, filename): - try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index baa9abad8..2cf5b173f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -793,7 +793,6 @@ class ImageFileDirectory_v2(MutableMapping): return ret def load(self, fp): - self.reset() self._offset = fp.tell() @@ -938,7 +937,6 @@ class ImageFileDirectory_v2(MutableMapping): return result def save(self, fp): - if fp.tell() == 0: # skip TIFF header on subsequent pages # tiff header -- PIL always starts the first IFD at offset 8 fp.write(self._prefix + self._pack("HL", 42, 8)) @@ -1059,7 +1057,6 @@ ImageFileDirectory = ImageFileDirectory_v1 class TiffImageFile(ImageFile.ImageFile): - format = "TIFF" format_description = "Adobe TIFF" _close_exclusive_fp_after_loading = False @@ -1582,7 +1579,6 @@ SAVE_INFO = { def _save(im, fp, filename): - try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index 0dc695a88..e4f47aa04 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -28,7 +28,6 @@ from ._binary import i32le as i32 class WalImageFile(ImageFile.ImageFile): - format = "WAL" format_description = "Quake2 Texture" diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 1d074f78c..d060dd4b8 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -35,7 +35,6 @@ def _accept(prefix): class WebPImageFile(ImageFile.ImageFile): - format = "WEBP" format_description = "WebP image" __loaded = 0 diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 639730b8e..0ecab56a8 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -75,7 +75,6 @@ def _accept(prefix): class WmfStubImageFile(ImageFile.StubImageFile): - format = "WMF" format_description = "Windows Metafile" @@ -86,7 +85,6 @@ class WmfStubImageFile(ImageFile.StubImageFile): s = self.fp.read(80) if s[:6] == b"\xd7\xcd\xc6\x9a\x00\x00": - # placeable windows metafile # get units per inch diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index f0e05e867..aa4a01f4e 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -41,12 +41,10 @@ def _accept(prefix): class XVThumbImageFile(ImageFile.ImageFile): - format = "XVThumb" format_description = "XV thumbnail image" def _open(self): - # check magic if not _accept(self.fp.read(6)): msg = "not an XV thumbnail file" diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index ad18e0031..3c12564c9 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -44,12 +44,10 @@ def _accept(prefix): class XbmImageFile(ImageFile.ImageFile): - format = "XBM" format_description = "X11 Bitmap" def _open(self): - m = xbm_head.match(self.fp.read(512)) if not m: @@ -69,7 +67,6 @@ class XbmImageFile(ImageFile.ImageFile): def _save(im, fp, filename): - if im.mode != "1": msg = f"cannot write mode {im.mode} as XBM" raise OSError(msg) diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 5fae4cd68..5d5bdc3ed 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class XpmImageFile(ImageFile.ImageFile): - format = "XPM" format_description = "X11 Pixel Map" def _open(self): - if not _accept(self.fp.read(9)): msg = "not an XPM file" raise SyntaxError(msg) @@ -68,7 +66,6 @@ class XpmImageFile(ImageFile.ImageFile): palette = [b"\0\0\0"] * 256 for _ in range(pal): - s = self.fp.readline() if s[-2:] == b"\r\n": s = s[:-2] @@ -79,9 +76,7 @@ class XpmImageFile(ImageFile.ImageFile): s = s[2:-2].split() for i in range(0, len(s), 2): - if s[i] == b"c": - # process colour key rgb = s[i + 1] if rgb == b"None": @@ -99,7 +94,6 @@ class XpmImageFile(ImageFile.ImageFile): break else: - # missing colour key msg = "cannot read this XPM file" raise ValueError(msg) @@ -110,7 +104,6 @@ class XpmImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] def load_read(self, bytes): - # # load all image data in one chunk From e79460e775a69c2cd9896144c3b340c4f98e3ad5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Feb 2023 09:46:01 +1100 Subject: [PATCH 093/132] Removed wget dependency --- .github/workflows/test-mingw.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index ef8214649..737da7b94 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -59,8 +59,7 @@ jobs: ${{ matrix.package }}-python3-numpy \ ${{ matrix.package }}-python3-olefile \ ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-setuptools \ - ${{ matrix.package }}-wget + ${{ matrix.package }}-python3-setuptools if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then pacman -S --noconfirm \ From e7d2750997cfbf88a6c93cbe1e598c8054d1676a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Feb 2023 10:01:10 +1100 Subject: [PATCH 094/132] Updated test images origin --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 790404535..1dd6c9175 100644 --- a/.gitignore +++ b/.gitignore @@ -79,7 +79,7 @@ docs/_build/ # JetBrains .idea -# Extra test images installed from pillow-depends/test_images +# Extra test images installed from python-pillow/test-images Tests/images/README.md Tests/images/crash_1.tif Tests/images/crash_2.tif From ed1d6633a1db0bf7e894bd372dcccd647cc0ee85 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Feb 2023 10:53:59 +1100 Subject: [PATCH 095/132] Use checkout action for test-images repository --- .github/workflows/test-windows.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index cf160a997..e938e999d 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -38,6 +38,12 @@ jobs: repository: python-pillow/pillow-depends path: winbuild\depends + - name: Checkout extra test images + uses: actions/checkout@v3 + with: + repository: python-pillow/test-images + path: Tests\test-images + # sets env: pythonLocation - name: Set up Python uses: actions/setup-python@v4 @@ -63,9 +69,7 @@ jobs: echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH # Install extra test images - 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\* Tests\images\ + xcopy /S /Y Tests\test-images\* Tests\images # make cache key depend on VS version & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` From 165675314605d7363605918698df091b405ba283 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 7 Feb 2023 22:48:33 -0800 Subject: [PATCH 096/132] Add docstrings for getixif() and Exif --- src/PIL/Image.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 81123d070..d47c57334 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1432,6 +1432,11 @@ class Image: return {get_name(root.tag): get_value(root)} def getexif(self): + """ + Gets EXIF data of the image. + + :returns: an :py:class:`~PIL.Image.Exif` object. + """ if self._exif is None: self._exif = Exif() self._exif._loaded = False @@ -3601,6 +3606,20 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): + """ + Exif class provides read and write access to EXIF image data. + + Only basic information is available on the root level, in Exif object + itself. In order to access the rest, obtain their respective IFDs using + :py:meth:`~PIL.Image.Exif.get_ifd` method and one of + :py:class:`~PIL.ExifTags.IFD` members (most notably `Exif` and + `GPSInfo`). + + Both root Exif and child IFD objects support dict interface and can be + indexed by int values that are available as enum members of + :py:class:`~PIL.ExifTags.Base`, :py:class:`~PIL.ExifTags.GPS`, and + :py:class:`~PIL.ExifTags.Interop`. + """ endian = None bigtiff = False From ed3cd75630352b26ee3b90a05c3d9b7fc0de041f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Feb 2023 10:11:54 +0200 Subject: [PATCH 097/132] Use 'rmdir' instead of 'rm -r' Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- depends/install_extra_test_images.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 941bfbe84..1ef6f4e97 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -9,4 +9,4 @@ mv $archive/* ../Tests/images/ # Cleanup old tarball and empty directory rm $archive.tar.gz -rm -r $archive +rmdir $archive From f679e410bd3ee7be8e4c6dce0df0ca4cb4d1fb47 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Feb 2023 10:12:14 +0200 Subject: [PATCH 098/132] Use 'rmdir' instead of 'rm -r' --- depends/download-and-extract.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index d9608e782..a318bfafd 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -8,5 +8,5 @@ if [ ! -f $archive.tar.gz ]; then wget -O $archive.tar.gz $url fi -rm -r $archive +rmdir $archive tar -xvzf $archive.tar.gz From c0a811e11678c3a6d8869fdc3739c7635b83ddd4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Feb 2023 08:36:41 +1100 Subject: [PATCH 099/132] Updated libjpeg-turbo to 2.1.5.1 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 89903c621..8a4494103 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -109,9 +109,9 @@ header = [ deps = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/2.1.5/libjpeg-turbo-2.1.5.tar.gz/download", - "filename": "libjpeg-turbo-2.1.5.tar.gz", - "dir": "libjpeg-turbo-2.1.5", + + "/libjpeg-turbo/files/2.1.5.1/libjpeg-turbo-2.1.5.1.tar.gz/download", + "filename": "libjpeg-turbo-2.1.5.1.tar.gz", + "dir": "libjpeg-turbo-2.1.5.1", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" From 0eac4f1942b278f618761c91ccec3f4422b90300 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 8 Feb 2023 20:34:45 -0800 Subject: [PATCH 100/132] Fix syntax --- 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 d47c57334..f5fa4ec0d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3612,8 +3612,8 @@ class Exif(MutableMapping): Only basic information is available on the root level, in Exif object itself. In order to access the rest, obtain their respective IFDs using :py:meth:`~PIL.Image.Exif.get_ifd` method and one of - :py:class:`~PIL.ExifTags.IFD` members (most notably `Exif` and - `GPSInfo`). + :py:class:`~PIL.ExifTags.IFD` members (most notably ``Exif`` and + ``GPSInfo``). Both root Exif and child IFD objects support dict interface and can be indexed by int values that are available as enum members of From 074c6afdc7d6c6990b9738c68c0fa3951d4f0855 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Feb 2023 04:40:57 +0000 Subject: [PATCH 101/132] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/Image.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f5fa4ec0d..813c237ad 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1434,7 +1434,7 @@ class Image: def getexif(self): """ Gets EXIF data of the image. - + :returns: an :py:class:`~PIL.Image.Exif` object. """ if self._exif is None: @@ -3608,18 +3608,19 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): """ Exif class provides read and write access to EXIF image data. - + Only basic information is available on the root level, in Exif object itself. In order to access the rest, obtain their respective IFDs using :py:meth:`~PIL.Image.Exif.get_ifd` method and one of :py:class:`~PIL.ExifTags.IFD` members (most notably ``Exif`` and ``GPSInfo``). - + Both root Exif and child IFD objects support dict interface and can be indexed by int values that are available as enum members of :py:class:`~PIL.ExifTags.Base`, :py:class:`~PIL.ExifTags.GPS`, and :py:class:`~PIL.ExifTags.Interop`. """ + endian = None bigtiff = False From a45211b811f1a0e4313b672c1629bedca857745a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Feb 2023 20:33:08 +1100 Subject: [PATCH 102/132] Updated freetype to 2.13 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 8a4494103..ff95581fa 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -253,9 +253,9 @@ deps = { "libs": ["*.lib"], }, "freetype": { - "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.12.1.tar.gz", # noqa: E501 - "filename": "freetype-2.12.1.tar.gz", - "dir": "freetype-2.12.1", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.0.tar.gz", # noqa: E501 + "filename": "freetype-2.13.0.tar.gz", + "dir": "freetype-2.13.0", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { From 997932bc93c778919fe22f4838ef0929482fe520 Mon Sep 17 00:00:00 2001 From: Marcel Telka Date: Thu, 9 Feb 2023 23:23:29 +0100 Subject: [PATCH 103/132] Update HPND wording in LICENSE file --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 125bdcc44..cf65e86d7 100644 --- a/LICENSE +++ b/LICENSE @@ -13,8 +13,8 @@ By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions: -Permission to use, copy, modify, and distribute this software and its -associated documentation for any purpose and without fee is hereby granted, +Permission to use, copy, modify and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be From a8e03e4dabd0fcb55fec922825dec9861e7ef103 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Feb 2023 20:11:50 +1100 Subject: [PATCH 104/132] Added Exif code examples --- src/PIL/Image.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 813c237ad..a280935a5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1433,7 +1433,7 @@ class Image: def getexif(self): """ - Gets EXIF data of the image. + Gets EXIF data from the image. :returns: an :py:class:`~PIL.Image.Exif` object. """ @@ -3607,18 +3607,36 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): """ - Exif class provides read and write access to EXIF image data. + This class provides read and write access to EXIF image data:: - Only basic information is available on the root level, in Exif object - itself. In order to access the rest, obtain their respective IFDs using - :py:meth:`~PIL.Image.Exif.get_ifd` method and one of - :py:class:`~PIL.ExifTags.IFD` members (most notably ``Exif`` and - ``GPSInfo``). + from PIL import Image + im = Image.open("exif.png") + exif = im.getexif() # Returns an instance of this class - Both root Exif and child IFD objects support dict interface and can be - indexed by int values that are available as enum members of - :py:class:`~PIL.ExifTags.Base`, :py:class:`~PIL.ExifTags.GPS`, and - :py:class:`~PIL.ExifTags.Interop`. + Information can be read and written, iterated over or deleted:: + + print(exif[274]) # 1 + exif[274] = 2 + for k, v in exif.items(): + print("Tag", k, "Value", v) # Tag 274 Value 2 + del exif[274] + + To access information beyond IFD0, :py:meth:`~PIL.Image.Exif.get_ifd` + returns a dictionary:: + + from PIL import ExifTags + im = Image.open("exif_gps.jpg") + exif = im.getexif() + gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) + print(gps_ifd) + + Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``, + ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. + + :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: + + print(exif[ExifTags.Base.Software]) # PIL + print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # '1999:99:99 99:99:99' """ endian = None From bb524018d35ea6450e56c3cd3db489eeb0f5a79c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Feb 2023 16:20:27 +1100 Subject: [PATCH 105/132] Raise an error when EXIF data is too long --- Tests/test_file_jpeg.py | 5 ++++- src/PIL/JpegImagePlugin.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index e3c5abcbd..b84661330 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -270,7 +270,10 @@ class TestFileJpeg: # https://github.com/python-pillow/Pillow/issues/148 f = str(tmp_path / "temp.jpg") im = hopper() - im.save(f, "JPEG", quality=90, exif=b"1" * 65532) + im.save(f, "JPEG", quality=90, exif=b"1" * 65533) + + with pytest.raises(ValueError): + im.save(f, "JPEG", quality=90, exif=b"1" * 65534) def test_exif_typeerror(self): with Image.open("Tests/images/exif_typeerror.jpg") as im: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index d7ddbe0d9..71ae84c04 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -730,10 +730,10 @@ def _save(im, fp, filename): extra = info.get("extra", b"") + MAX_BYTES_IN_MARKER = 65533 icc_profile = info.get("icc_profile") if icc_profile: ICC_OVERHEAD_LEN = 14 - MAX_BYTES_IN_MARKER = 65533 MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN markers = [] while icc_profile: @@ -764,6 +764,9 @@ def _save(im, fp, filename): exif = info.get("exif", b"") if isinstance(exif, Image.Exif): exif = exif.tobytes() + if len(exif) > MAX_BYTES_IN_MARKER: + msg = "EXIF data is too long" + raise ValueError(msg) # get keyword arguments im.encoderconfig = ( From 20daa1d049b8b0e321c282af073e05b114fb216e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Feb 2023 20:49:09 +1100 Subject: [PATCH 106/132] Fixed typo --- src/PIL/TiffTags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 9b5277138..ac048ba56 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -312,7 +312,7 @@ TAGS = { 34910: "HylaFAX FaxRecvTime", 36864: "ExifVersion", 36867: "DateTimeOriginal", - 36868: "DateTImeDigitized", + 36868: "DateTimeDigitized", 37121: "ComponentsConfiguration", 37122: "CompressedBitsPerPixel", 37724: "ImageSourceData", From ac6b9632b4c90ff820c23420e699c68092166d63 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 10 Feb 2023 11:11:39 +0200 Subject: [PATCH 107/132] Test Python 3.12-dev on macOS and Ubuntu --- .ci/install.sh | 3 ++- .github/workflows/macos-install.sh | 3 ++- .github/workflows/test.yml | 1 + docs/installation.rst | 12 ++++++------ 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 518b66acc..6aa122cc5 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -37,7 +37,8 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma if [[ $(uname) != CYGWIN* ]]; then - python3 -m pip install numpy + # TODO Remove condition when NumPy supports 3.12 + if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index dfd7d0553..1fc6262f4 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -13,7 +13,8 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -python3 -m pip install numpy +# TODO Remove condition when NumPy supports 3.12 +if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11c7b77be..8e06de4cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,7 @@ jobs: python-version: [ "pypy3.9", "pypy3.8", + "3.12-dev", "3.11", "3.10", "3.9", diff --git a/docs/installation.rst b/docs/installation.rst index ea8722c56..93260e141 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -442,15 +442,15 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | PyPy3 | | +| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, PyPy3 | | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | PyPy3 | | -+----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.10 | arm64v8, ppc64le, | | | | s390x, x86-64 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2016 | 3.7 | x86-64 | From 826c98156ceaf619493af22808189500f9b0ae46 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 11 Feb 2023 16:36:13 +0200 Subject: [PATCH 108/132] Remove unused listwindows functions for Windows/3.12 support --- src/_imaging.c | 3 --- src/display.c | 73 -------------------------------------------------- 2 files changed, 76 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 05e1370f6..cece2e93a 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3984,8 +3984,6 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args); extern PyObject * PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args); extern PyObject * -PyImaging_ListWindowsWin32(PyObject *self, PyObject *args); -extern PyObject * PyImaging_EventLoopWin32(PyObject *self, PyObject *args); extern PyObject * PyImaging_DrawWmf(PyObject *self, PyObject *args); @@ -4069,7 +4067,6 @@ static PyMethodDef functions[] = { {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, METH_VARARGS}, {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, METH_VARARGS}, {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, METH_VARARGS}, - {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, METH_VARARGS}, {"drawwmf", (PyCFunction)PyImaging_DrawWmf, METH_VARARGS}, #endif #ifdef HAVE_XCB diff --git a/src/display.c b/src/display.c index 0ce10e249..a50fc3e24 100644 --- a/src/display.c +++ b/src/display.c @@ -421,79 +421,6 @@ error: return NULL; } -static BOOL CALLBACK -list_windows_callback(HWND hwnd, LPARAM lParam) { - PyObject *window_list = (PyObject *)lParam; - PyObject *item; - PyObject *title; - RECT inner, outer; - int title_size; - int status; - - /* get window title */ - title_size = GetWindowTextLength(hwnd); - if (title_size > 0) { - title = PyUnicode_FromStringAndSize(NULL, title_size); - if (title) { - GetWindowTextW(hwnd, PyUnicode_AS_UNICODE(title), title_size + 1); - } - } else { - title = PyUnicode_FromString(""); - } - if (!title) { - return 0; - } - - /* get bounding boxes */ - GetClientRect(hwnd, &inner); - GetWindowRect(hwnd, &outer); - - item = Py_BuildValue( - F_HANDLE "N(iiii)(iiii)", - hwnd, - title, - inner.left, - inner.top, - inner.right, - inner.bottom, - outer.left, - outer.top, - outer.right, - outer.bottom); - if (!item) { - return 0; - } - - status = PyList_Append(window_list, item); - - Py_DECREF(item); - - if (status < 0) { - return 0; - } - - return 1; -} - -PyObject * -PyImaging_ListWindowsWin32(PyObject *self, PyObject *args) { - PyObject *window_list; - - window_list = PyList_New(0); - if (!window_list) { - return NULL; - } - - EnumWindows(list_windows_callback, (LPARAM)window_list); - - if (PyErr_Occurred()) { - Py_DECREF(window_list); - return NULL; - } - - return window_list; -} - /* -------------------------------------------------------------------- */ /* Windows clipboard grabber */ From ab2809a44c2211741ce2eca132d9cc60619ea2b5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 10 Feb 2023 11:11:39 +0200 Subject: [PATCH 109/132] Test Python 3.12-dev on macOS and Ubuntu --- .github/workflows/test-windows.yml | 2 +- docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e938e999d..306e34ca9 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] architecture: ["x86", "x64"] include: # PyPy 7.3.4+ only ships 64-bit binaries for Windows diff --git a/docs/installation.rst b/docs/installation.rst index 93260e141..1bfc65f4b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -456,7 +456,7 @@ These platforms are built and tested for every change. | Windows Server 2016 | 3.7 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | -| | PyPy3 | | +| | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.9 (MinGW) | x86, x86-64 | | +----------------------------+---------------------+ From f6040bc8792e355a0b62ba961093cde8127d39d4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 11 Feb 2023 23:14:47 +0200 Subject: [PATCH 110/132] Docker tests: enable gcov support for codecov/codecov-action --- .github/workflows/test-docker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 7331cf8ee..7d2b20d65 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -87,6 +87,7 @@ jobs: with: flags: GHA_Docker name: ${{ matrix.docker }} + gcov: true success: permissions: From 0836c747f08fdfcdfd9be5ee858d3ad10bb5430f Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 11 Feb 2023 23:16:13 +0000 Subject: [PATCH 111/132] add gcov coverage to test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11c7b77be..87cad1f36 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -107,9 +107,9 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v3 with: - file: ./coverage.xml flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} + gcov: true success: permissions: From 42683781d6d25913cf9274dd7cd14153e542cfd1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Feb 2023 13:41:35 +1100 Subject: [PATCH 112/132] Updated CI targets --- docs/installation.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 1bfc65f4b..1b5719a8e 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -447,11 +447,13 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.10 | arm64v8, ppc64le, | -| | | s390x, x86-64 | +| | | s390x | +----------------------------------+----------------------------+---------------------+ | Windows Server 2016 | 3.7 | x86-64 | +----------------------------------+----------------------------+---------------------+ From d9085541bf9ef82be182f209931e5326629df055 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 12 Feb 2023 15:18:53 +1100 Subject: [PATCH 113/132] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e35a55965..626860e71 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Raise an error if EXIF data is too long when saving JPEG #6939 + [radarhere] + - Handle more than one directory returned by pkg-config #6896 [sebastic, radarhere] From da38395396c2f6322073a0f24a38b5780e46e89f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 12 Feb 2023 21:56:23 +1100 Subject: [PATCH 114/132] Removed quotes from result in docstring --- 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 a280935a5..63bad83a1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3636,7 +3636,7 @@ class Exif(MutableMapping): :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: print(exif[ExifTags.Base.Software]) # PIL - print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # '1999:99:99 99:99:99' + print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 """ endian = None From bbbb8e6e21108b13a3ae978b4aa0b783aa10bddf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Feb 2023 22:05:51 +1100 Subject: [PATCH 115/132] Updated harfbuzz to 7.0.0 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index ff95581fa..bef8afa9d 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -356,9 +356,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/6.0.0.zip", - "filename": "harfbuzz-6.0.0.zip", - "dir": "harfbuzz-6.0.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.0.zip", + "filename": "harfbuzz-7.0.0.zip", + "dir": "harfbuzz-7.0.0", "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"), From ad0e9dbaaf41e5c7abddf08d850cf0c0d98a3a92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Feb 2023 10:52:32 +1100 Subject: [PATCH 116/132] Fixed writing int as UNDEFINED tag --- Tests/test_file_tiff_metadata.py | 16 ++++++++++++++++ src/PIL/TiffImagePlugin.py | 2 ++ 2 files changed, 18 insertions(+) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index a4481d85f..fdabae3a3 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -216,6 +216,22 @@ def test_writing_other_types_to_bytes(value, tmp_path): assert reloaded.tag_v2[700] == b"\x01" +def test_writing_other_types_to_undefined(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[33723] + assert tag.type == TiffTags.UNDEFINED + + info[33723] = 1 + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[33723] == b"1" + + def test_undefined_zero(tmp_path): # Check that the tag has not been changed since this test was created tag = TiffTags.TAGS_V2[45059] diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 2cf5b173f..aaaf8fcb9 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -764,6 +764,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(7) def write_undefined(self, value): + if isinstance(value, int): + value = str(value).encode("ascii", "replace") return value @_register_loader(10, 8) From b7630bd675bd260dfac5eb53bc23afe1ab8e09d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Feb 2023 11:41:32 +1100 Subject: [PATCH 117/132] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 626860e71..aa03cfc40 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Fixed writing int as UNDEFINED tag #6950 + [radarhere] + - Raise an error if EXIF data is too long when saving JPEG #6939 [radarhere] From 06ba226e7b6b9fbcbf74839bf123b4095d6f9c92 Mon Sep 17 00:00:00 2001 From: James Zern Date: Tue, 14 Feb 2023 17:28:16 -0800 Subject: [PATCH 118/132] image-file-formats.rst: correct WebP quality range 0-100, not 1-100 --- 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 a41ef7cf8..56937b5c7 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1126,7 +1126,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: If present and true, instructs the WebP writer to use lossless compression. **quality** - Integer, 1-100, Defaults to 80. For lossy, 0 gives the smallest + Integer, 0-100, Defaults to 80. For lossy, 0 gives the smallest size and 100 the largest. For lossless, this parameter is the amount of effort put into the compression: 0 is the fastest, but gives larger files compared to the slowest, but best, 100. From 8935dad32e31a121906a92bfc3eacdc3e410a711 Mon Sep 17 00:00:00 2001 From: James Zern Date: Tue, 14 Feb 2023 17:29:06 -0800 Subject: [PATCH 119/132] image-file-formats.rst: document WebP 'xmp' option --- docs/handbook/image-file-formats.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a41ef7cf8..a85ca59dd 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1147,6 +1147,10 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: The exif data to include in the saved file. Only supported if the system WebP library was built with webpmux support. +**xmp** + The XMP data to include in the saved file. Only supported if + the system WebP library was built with webpmux support. + Saving sequences ~~~~~~~~~~~~~~~~ From 43682de4bdb6c5f93340107386f86becbfbad25a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 21 Feb 2023 08:12:57 +1100 Subject: [PATCH 120/132] Updated harfbuzz to 7.0.1 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index bef8afa9d..35980f19c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -356,9 +356,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.0.zip", - "filename": "harfbuzz-7.0.0.zip", - "dir": "harfbuzz-7.0.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.1.zip", + "filename": "harfbuzz-7.0.1.zip", + "dir": "harfbuzz-7.0.1", "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"), From 36bcc0a89866bf9ca3f79c61470d9ab4a58d74ec Mon Sep 17 00:00:00 2001 From: Jasper van der Neut Date: Tue, 21 Feb 2023 10:34:41 +0100 Subject: [PATCH 121/132] Support saving PDF with different X and Y resolution. Add a `dpi` parameter to the PDF save function, which accepts a tuple with X and Y dpi. This is useful for converting tiffg3 (fax) images to pdf, which have split dpi like (204,391), (204,196) or (204,98). --- Tests/test_file_pdf.py | 42 ++++++++++++++++++++++++++++ docs/handbook/image-file-formats.rst | 5 ++++ src/PIL/PdfImagePlugin.py | 19 ++++++++----- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 216b93ca9..a45b22bf6 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -80,6 +80,48 @@ def test_resolution(tmp_path): assert size == (61.44, 61.44) +def test_dpi(tmp_path): + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, dpi=(75, 150)) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (122.88, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (122.88, 61.44) + + +def test_resolution_and_dpi(tmp_path): + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, resolution=200, dpi=(75, 150)) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (122.88, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (122.88, 61.44) + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 926a2f75a..081b84963 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1497,6 +1497,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum image, will determine the physical dimensions of the page that will be saved in the PDF. +**dpi** + A tuple of (x_resolution, y_resolution), with inches as the resolution + unit. If both the ``resolution`` parameter and the ``dpi`` parameter are + present, ``resolution`` will be ignored. + **title** The document’s title. If not appending to an existing PDF file, this will default to the filename. diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index baad4939f..d4f1ef93a 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -53,7 +53,12 @@ def _save(im, fp, filename, save_all=False): else: existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") - resolution = im.encoderinfo.get("resolution", 72.0) + x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) + + dpi = im.encoderinfo.get("dpi") + if dpi: + x_resolution = dpi[0] + y_resolution = dpi[1] info = { "title": None @@ -214,8 +219,8 @@ def _save(im, fp, filename, save_all=False): stream=stream, Type=PdfParser.PdfName("XObject"), Subtype=PdfParser.PdfName("Image"), - Width=width, # * 72.0 / resolution, - Height=height, # * 72.0 / resolution, + Width=width, # * 72.0 / x_resolution, + Height=height, # * 72.0 / y_resolution, Filter=filter, BitsPerComponent=bits, Decode=decode, @@ -235,8 +240,8 @@ def _save(im, fp, filename, save_all=False): MediaBox=[ 0, 0, - width * 72.0 / resolution, - height * 72.0 / resolution, + width * 72.0 / x_resolution, + height * 72.0 / y_resolution, ], Contents=contents_refs[page_number], ) @@ -245,8 +250,8 @@ def _save(im, fp, filename, save_all=False): # page contents page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( - width * 72.0 / resolution, - height * 72.0 / resolution, + width * 72.0 / x_resolution, + height * 72.0 / y_resolution, ) existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) From be489287d2d8923ad57695f1bdf0d28db8be5757 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Feb 2023 18:59:51 +1100 Subject: [PATCH 122/132] Parametrized test --- Tests/test_file_pdf.py | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index a45b22bf6..2afec9960 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -80,32 +80,18 @@ def test_resolution(tmp_path): assert size == (61.44, 61.44) -def test_dpi(tmp_path): +@pytest.mark.parametrize( + "params", + ( + {"dpi": (75, 150)}, + {"dpi": (75, 150), "resolution": 200}, + ), +) +def test_dpi(params, tmp_path): im = hopper() outfile = str(tmp_path / "temp.pdf") - im.save(outfile, dpi=(75, 150)) - - with open(outfile, "rb") as fp: - contents = fp.read() - - size = tuple( - float(d) - for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") - ) - assert size == (122.88, 61.44) - - size = tuple( - float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() - ) - assert size == (122.88, 61.44) - - -def test_resolution_and_dpi(tmp_path): - im = hopper() - - outfile = str(tmp_path / "temp.pdf") - im.save(outfile, resolution=200, dpi=(75, 150)) + im.save(outfile, **params) with open(outfile, "rb") as fp: contents = fp.read() From 0d667f5e0b756844e22dbb78b71c84279992ca2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Feb 2023 21:12:11 +1100 Subject: [PATCH 123/132] Do not read "resolution" parameter if it will not be used --- src/PIL/PdfImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index d4f1ef93a..4fa1998ba 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -53,12 +53,12 @@ def _save(im, fp, filename, save_all=False): else: existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") - x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) - dpi = im.encoderinfo.get("dpi") if dpi: x_resolution = dpi[0] y_resolution = dpi[1] + else: + x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) info = { "title": None From 21d13e1dea032d734572a9bb954c827aba2b29d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Feb 2023 23:22:18 +1100 Subject: [PATCH 124/132] Relax roundtrip check --- Tests/test_qt_image_qapplication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 34609314c..f36ad5d46 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -48,7 +48,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_similar(result, expected.convert("RGB"), 0.3) + assert_image_similar(result, expected.convert("RGB"), 0.5) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") From acc7e0b469ac973bf1ab79bf8369a8b13769d169 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:00:24 +1100 Subject: [PATCH 125/132] Highlight code example --- 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 926a2f75a..9a3ddcab7 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1104,7 +1104,7 @@ using the general tags available through tiffinfo. Either an integer or a float. **dpi** - A tuple of (x_resolution, y_resolution), with inches as the resolution + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution unit. For consistency with other image formats, the x and y resolutions of the dpi will be rounded to the nearest integer. From 19299acbb6688d2aaf33e53f64c9cbb04545e274 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:39:48 +1100 Subject: [PATCH 126/132] Relax roundtrip check --- Tests/test_qt_image_qapplication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index f36ad5d46..4929fa933 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -48,7 +48,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_similar(result, expected.convert("RGB"), 0.5) + assert_image_similar(result, expected.convert("RGB"), 1) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") From 3c3d88845072b0b8b8a9a00787fb03d733def3b7 Mon Sep 17 00:00:00 2001 From: Jasper van der Neut - Stulen Date: Thu, 23 Feb 2023 23:19:13 +0100 Subject: [PATCH 127/132] Update docs/handbook/image-file-formats.rst Co-authored-by: Hugo van Kemenade --- 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 081b84963..597d1b644 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1498,7 +1498,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum saved in the PDF. **dpi** - A tuple of (x_resolution, y_resolution), with inches as the resolution + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution unit. If both the ``resolution`` parameter and the ``dpi`` parameter are present, ``resolution`` will be ignored. From 57acab55cbf0f00f3064d11ccfd3843c55cdceb0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 12:55:13 +1100 Subject: [PATCH 128/132] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index aa03cfc40..d37ba4ab3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Support saving PDF with different X and Y resolutions #6961 + [jvanderneutstulen, radarhere, hugovk] + - Fixed writing int as UNDEFINED tag #6950 [radarhere] From 8a1b9aa0adbd84cd944c15a667af4a65052ac522 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 16:44:07 +1100 Subject: [PATCH 129/132] Allow comments in FITS images --- Tests/test_file_fits.py | 6 ++++++ src/PIL/FitsImagePlugin.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index d2f5a6d17..3048827e0 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -44,6 +44,12 @@ def test_naxis_zero(): pass +def test_comment(): + image_data = b"SIMPLE = T / comment string" + with pytest.raises(OSError): + FitsImagePlugin.FitsImageFile(BytesIO(image_data)) + + def test_stub_deprecated(): class Handler: opened = False diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 1185ef2d3..1359aeb12 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -32,7 +32,7 @@ class FitsImageFile(ImageFile.ImageFile): keyword = header[:8].strip() if keyword == b"END": break - value = header[8:].strip() + value = header[8:].split(b"/")[0].strip() if value.startswith(b"="): value = value[1:].strip() if not headers and (not _accept(keyword) or value != b"T"): From dbcd7372e554ee420a156b968eb9a609ec66ffd1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 18:46:07 +1100 Subject: [PATCH 130/132] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d37ba4ab3..15decc7f0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Allow comments in FITS images #6973 + [radarhere] + - Support saving PDF with different X and Y resolutions #6961 [jvanderneutstulen, radarhere, hugovk] From 132fb9360b291eafedd3ea8238ceebd93a094e87 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 19:10:47 +1100 Subject: [PATCH 131/132] Added memoryview support to frombytes() --- Tests/test_image_frombytes.py | 11 +++++++++-- src/decode.c | 7 +++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 7fb05cda7..c299e4544 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,10 +1,17 @@ +import pytest + from PIL import Image from .helper import assert_image_equal, hopper -def test_sanity(): +@pytest.mark.parametrize("data_type", ("bytes", "memoryview")) +def test_sanity(data_type): im1 = hopper() - im2 = Image.frombytes(im1.mode, im1.size, im1.tobytes()) + + data = im1.tobytes() + if data_type == "memoryview": + data = memoryview(data) + im2 = Image.frombytes(im1.mode, im1.size, data) assert_image_equal(im1, im2) diff --git a/src/decode.c b/src/decode.c index 7a9b956c5..82a3af832 100644 --- a/src/decode.c +++ b/src/decode.c @@ -116,12 +116,11 @@ _dealloc(ImagingDecoderObject *decoder) { static PyObject * _decode(ImagingDecoderObject *decoder, PyObject *args) { - UINT8 *buffer; - Py_ssize_t bufsize; + Py_buffer buffer; int status; ImagingSectionCookie cookie; - if (!PyArg_ParseTuple(args, "y#", &buffer, &bufsize)) { + if (!PyArg_ParseTuple(args, "y*", &buffer)) { return NULL; } @@ -129,7 +128,7 @@ _decode(ImagingDecoderObject *decoder, PyObject *args) { ImagingSectionEnter(&cookie); } - status = decoder->decode(decoder->im, &decoder->state, buffer, bufsize); + status = decoder->decode(decoder->im, &decoder->state, buffer.buf, buffer.len); if (!decoder->pulls_fd) { ImagingSectionLeave(&cookie); From 36489c2c396d8801bfa632b6e8d00a094dab5347 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 20:54:35 +1100 Subject: [PATCH 132/132] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 15decc7f0..fe0230c34 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Added memoryview support to frombytes() #6974 + [radarhere] + - Allow comments in FITS images #6973 [radarhere]