From 3f32b79303bcf62c27ba4df50585e56891fe38ae Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 15 Jul 2022 10:50:48 +0300 Subject: [PATCH 001/100] Replace version tables with RST csv-table --- docs/installation.rst | 36 ++++++++---------------------------- docs/newer-versions.csv | 8 ++++++++ docs/older-versions.csv | 5 +++++ 3 files changed, 21 insertions(+), 28 deletions(-) create mode 100644 docs/newer-versions.csv create mode 100644 docs/older-versions.csv diff --git a/docs/installation.rst b/docs/installation.rst index b4cee7b69..f73c1f5a5 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -15,35 +15,15 @@ Python Support Pillow supports these Python versions. -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Python |3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 3.5 | 3.4 | 2.7 | -+======================+=====+=====+=====+=====+=====+=====+=====+=====+ -| Pillow >= 9.0 | Yes | Yes | Yes | Yes | | | | | -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 8.3.2 - 8.4 | Yes | Yes | Yes | Yes | Yes | | | | -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 8.0 - 8.3.1 | | Yes | Yes | Yes | Yes | | | | -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 7.0 - 7.2 | | | Yes | Yes | Yes | Yes | | | -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 6.2.1 - 6.2.2 | | | Yes | Yes | Yes | Yes | | Yes | -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 6.0 - 6.2.0 | | | | Yes | Yes | Yes | | Yes | -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 5.2 - 5.4 | | | | Yes | Yes | Yes | Yes | Yes | -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +.. csv-table:: Newer versions + :file: newer-versions.csv + :header-rows: 1 + :widths: auto -+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ -| Python | 3.6 | 3.5 | 3.4 | 3.3 | 3.2 | 2.7 | 2.6 | 2.5 | 2.4 | -+==================+=====+=====+=====+=====+=====+=====+=====+=====+=====+ -| Pillow 5.0 - 5.1 | Yes | Yes | Yes | | | Yes | | | | -+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 4 | Yes | Yes | Yes | Yes | | Yes | | | | -+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 2 - 3 | | Yes | Yes | Yes | Yes | Yes | Yes | | | -+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow < 2 | | | | | | Yes | Yes | Yes | Yes | -+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ +.. csv-table:: Older versions + :file: older-versions.csv + :header-rows: 1 + :widths: auto Basic Installation ------------------ diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv new file mode 100644 index 000000000..b9630add1 --- /dev/null +++ b/docs/newer-versions.csv @@ -0,0 +1,8 @@ +Python,3.10,3.9,3.8,3.7,3.6,3.5,3.4,2.7 +Pillow >= 9.0,Yes,Yes,Yes,Yes,,,, +Pillow 8.3.2 - 8.4,Yes,Yes,Yes,Yes,Yes,,, +Pillow 8.0 - 8.3.1,,Yes,Yes,Yes,Yes,,, +Pillow 7.0 - 7.2,,,Yes,Yes,Yes,Yes,, +Pillow 6.2.1 - 6.2.2,,,Yes,Yes,Yes,Yes,,Yes +Pillow 6.0 - 6.2.0,,,,Yes,Yes,Yes,,Yes +Pillow 5.2 - 5.4,,,,Yes,Yes,Yes,Yes,Yes diff --git a/docs/older-versions.csv b/docs/older-versions.csv new file mode 100644 index 000000000..f8c707588 --- /dev/null +++ b/docs/older-versions.csv @@ -0,0 +1,5 @@ +Python,3.6,3.5,3.4,3.3,3.2,2.7,2.6,2.5,2.4 +Pillow 5.0 - 5.1,Yes,Yes,Yes,,,Yes,,, +Pillow 4,Yes,Yes,Yes,Yes,,Yes,,, +Pillow 2 - 3,,Yes,Yes,Yes,Yes,Yes,Yes,, +Pillow < 2,,,,,,Yes,Yes,Yes,Yes \ No newline at end of file From 0f3ad23e1b1dc38a6af2fdb1c9c640b5ae106c71 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 15 Jul 2022 11:15:49 +0300 Subject: [PATCH 002/100] Add Python 3.11 for Pillow >= 9.3 --- docs/newer-versions.csv | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv index b9630add1..c3f655e2f 100644 --- a/docs/newer-versions.csv +++ b/docs/newer-versions.csv @@ -1,8 +1,9 @@ -Python,3.10,3.9,3.8,3.7,3.6,3.5,3.4,2.7 -Pillow >= 9.0,Yes,Yes,Yes,Yes,,,, -Pillow 8.3.2 - 8.4,Yes,Yes,Yes,Yes,Yes,,, -Pillow 8.0 - 8.3.1,,Yes,Yes,Yes,Yes,,, -Pillow 7.0 - 7.2,,,Yes,Yes,Yes,Yes,, -Pillow 6.2.1 - 6.2.2,,,Yes,Yes,Yes,Yes,,Yes -Pillow 6.0 - 6.2.0,,,,Yes,Yes,Yes,,Yes -Pillow 5.2 - 5.4,,,,Yes,Yes,Yes,Yes,Yes +Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5,3.4,2.7 +Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,,,, +Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,,,, +Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes,,, +Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes,,, +Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes,, +Pillow 6.2.1 - 6.2.2,,,,Yes,Yes,Yes,Yes,,Yes +Pillow 6.0 - 6.2.0,,,,,Yes,Yes,Yes,,Yes +Pillow 5.2 - 5.4,,,,,Yes,Yes,Yes,Yes,Yes From 48ad0b1f381cabc2dacd932bf0272945fb815078 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 15 Jul 2022 11:25:33 +0300 Subject: [PATCH 003/100] Rebalance version tables --- docs/newer-versions.csv | 15 ++++++--------- docs/older-versions.csv | 13 ++++++++----- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv index c3f655e2f..ed2369259 100644 --- a/docs/newer-versions.csv +++ b/docs/newer-versions.csv @@ -1,9 +1,6 @@ -Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5,3.4,2.7 -Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,,,, -Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,,,, -Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes,,, -Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes,,, -Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes,, -Pillow 6.2.1 - 6.2.2,,,,Yes,Yes,Yes,Yes,,Yes -Pillow 6.0 - 6.2.0,,,,,Yes,Yes,Yes,,Yes -Pillow 5.2 - 5.4,,,,,Yes,Yes,Yes,Yes,Yes +Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5 +Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,, +Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,, +Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes, +Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes, +Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes diff --git a/docs/older-versions.csv b/docs/older-versions.csv index f8c707588..6058f0524 100644 --- a/docs/older-versions.csv +++ b/docs/older-versions.csv @@ -1,5 +1,8 @@ -Python,3.6,3.5,3.4,3.3,3.2,2.7,2.6,2.5,2.4 -Pillow 5.0 - 5.1,Yes,Yes,Yes,,,Yes,,, -Pillow 4,Yes,Yes,Yes,Yes,,Yes,,, -Pillow 2 - 3,,Yes,Yes,Yes,Yes,Yes,Yes,, -Pillow < 2,,,,,,Yes,Yes,Yes,Yes \ No newline at end of file +Python,3.8,3.7,3.6,3.5,3.4,3.3,3.2,2.7,2.6,2.5,2.4 +Pillow 6.2.1 - 6.2.2,Yes,Yes,Yes,Yes,,,,Yes,,, +Pillow 6.0 - 6.2.0,,Yes,Yes,Yes,,,,Yes,,, +Pillow 5.2 - 5.4,,Yes,Yes,Yes,Yes,,,Yes,,, +Pillow 5.0 - 5.1,,,Yes,Yes,Yes,,,Yes,,, +Pillow 4,,,Yes,Yes,Yes,Yes,,Yes,,, +Pillow 2 - 3,,,,Yes,Yes,Yes,Yes,Yes,Yes,, +Pillow < 2,,,,,,,,Yes,Yes,Yes,Yes \ No newline at end of file From 2944ff18d6ac79b881e3a3ec8d656fe08a41d1a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 16 Jul 2022 20:02:58 +1000 Subject: [PATCH 004/100] Support saving multiple MPO frames --- Tests/test_file_mpo.py | 41 +++++++++++++++++--- docs/handbook/image-file-formats.rst | 11 ++++++ src/PIL/JpegImagePlugin.py | 2 +- src/PIL/MpoImagePlugin.py | 57 ++++++++++++++++++++++++++-- 4 files changed, 101 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index d093f26cc..849857d31 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -5,15 +5,19 @@ import pytest from PIL import Image -from .helper import assert_image_similar, is_pypy, skip_unless_feature +from .helper import ( + assert_image_equal, + assert_image_similar, + is_pypy, + skip_unless_feature, +) test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] pytestmark = skip_unless_feature("jpg") -def frame_roundtrip(im, **options): - # Note that for now, there is no MPO saving functionality +def roundtrip(im, **options): out = BytesIO() im.save(out, "MPO", **options) test_bytes = out.tell() @@ -237,13 +241,38 @@ def test_image_grab(): def test_save(): - # Note that only individual frames can be saved at present for test_file in test_files: with Image.open(test_file) as im: assert im.tell() == 0 - jpg0 = frame_roundtrip(im) + jpg0 = roundtrip(im) assert_image_similar(im, jpg0, 30) im.seek(1) assert im.tell() == 1 - jpg1 = frame_roundtrip(im) + jpg1 = roundtrip(im) assert_image_similar(im, jpg1, 30) + + +def test_save_all(): + for test_file in test_files: + with Image.open(test_file) as im: + im_reloaded = roundtrip(im, save_all=True) + + im.seek(0) + assert_image_similar(im, im_reloaded, 30) + + im.seek(1) + im_reloaded.seek(1) + assert_image_similar(im, im_reloaded, 30) + + im = Image.new("RGB", (1, 1)) + im2 = Image.new("RGB", (1, 1), "#f00") + im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) + + assert_image_equal(im, im_reloaded) + + im_reloaded.seek(1) + assert_image_similar(im2, im_reloaded, 1) + + # Test that a single frame image will not be saved as an MPO + jpg = roundtrip(im, save_all=True) + assert "mp" not in jpg.info diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 30452c4a6..1728c8e05 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1209,6 +1209,17 @@ image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL methods may be used to read other pictures from the file. The pictures are zero-indexed and random access is supported. +When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default +only the first frame of a multiframe image will be saved. If the ``save_all`` +argument is present and true, then all frames will be saved, and the following +option will also be available. + +**append_images** + A list of images to append as additional pictures. Each of the + images in the list can be single or multiframe images. + + .. versionadded:: 9.3.0 + PCD ^^^ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 4efe6281a..a6ed223bc 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -711,7 +711,7 @@ def _save(im, fp, filename): qtables = getattr(im, "quantization", None) qtables = validate_qtables(qtables) - extra = b"" + extra = info.get("extra", b"") icc_profile = info.get("icc_profile") if icc_profile: diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 27c30958c..5bfd8efc1 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -18,16 +18,66 @@ # See the README file for information on usage and redistribution. # -from . import Image, ImageFile, JpegImagePlugin +import itertools +import os +import struct + +from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin from ._binary import i16be as i16 +from ._binary import o32le # def _accept(prefix): # return JpegImagePlugin._accept(prefix) def _save(im, fp, filename): - # Note that we can only save the current frame at present - return JpegImagePlugin._save(im, fp, filename) + JpegImagePlugin._save(im, fp, filename) + + +def _save_all(im, fp, filename): + append_images = im.encoderinfo.get("append_images", []) + if not append_images: + try: + animated = im.is_animated + except AttributeError: + animated = False + if not animated: + _save(im, fp, filename) + return + + offsets = [] + for imSequence in itertools.chain([im], append_images): + for im_frame in ImageSequence.Iterator(imSequence): + if not offsets: + # APP2 marker + im.encoderinfo["extra"] = ( + b"\xFF\xE2" + struct.pack(">H", 6 + 70) + b"MPF\0" + b" " * 70 + ) + JpegImagePlugin._save(im_frame, fp, filename) + offsets.append(fp.tell()) + else: + im_frame.save(fp, "JPEG") + offsets.append(fp.tell() - offsets[-1]) + + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[0xB001] = len(offsets) + + mpentries = b"" + data_offset = 0 + for i, size in enumerate(offsets): + if i == 0: + mptype = 0x030000 # Baseline MP Primary Image + else: + mptype = 0x000000 # Undefined + mpentries += struct.pack(" Date: Mon, 18 Jul 2022 16:16:06 +1000 Subject: [PATCH 005/100] Do not quote Pillow version for setuptools >= 60 --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 71e853dce..37477216d 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,9 @@ import subprocess import sys import warnings -from setuptools import Extension, setup +from setuptools import Extension +from setuptools import __version__ as setuptools_version +from setuptools import setup from setuptools.command.build_ext import build_ext @@ -850,6 +852,7 @@ class pil_build_ext(build_ext): sys.platform == "win32" and sys.version_info < (3, 9) and not (PLATFORM_PYPY or PLATFORM_MINGW) + and int(setuptools_version.split('.')[0]) < 60 ): defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""')) else: From aba0859db9261e94feaa592615b9d70cbd2b99ae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Jul 2022 06:17:07 +0000 Subject: [PATCH 006/100] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 37477216d..a2b2c6910 100755 --- a/setup.py +++ b/setup.py @@ -852,7 +852,7 @@ class pil_build_ext(build_ext): sys.platform == "win32" and sys.version_info < (3, 9) and not (PLATFORM_PYPY or PLATFORM_MINGW) - and int(setuptools_version.split('.')[0]) < 60 + and int(setuptools_version.split(".")[0]) < 60 ): defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""')) else: From 77402067fbe06dd9224422c6a39a738d6bef66db Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 18 Jul 2022 15:30:00 +0300 Subject: [PATCH 007/100] Omit ":widths: auto" Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/installation.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index f73c1f5a5..9e9dc52b3 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -18,12 +18,10 @@ Pillow supports these Python versions. .. csv-table:: Newer versions :file: newer-versions.csv :header-rows: 1 - :widths: auto .. csv-table:: Older versions :file: older-versions.csv :header-rows: 1 - :widths: auto Basic Installation ------------------ From 3a7e29306a1c109a2e5cb5b6dabcba9f6e0eb9fc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 19 Jul 2022 07:40:25 +1000 Subject: [PATCH 008/100] Added release notes --- docs/releasenotes/9.3.0.rst | 59 +++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 60 insertions(+) create mode 100644 docs/releasenotes/9.3.0.rst diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst new file mode 100644 index 000000000..da045a50a --- /dev/null +++ b/docs/releasenotes/9.3.0.rst @@ -0,0 +1,59 @@ +9.3.0 +----- + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Saving multiple MPO frames +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Multiple MPO frames can now be saved. Using the ``save_all`` argument, all of +an image's frames will be saved to file:: + + from PIL import Image + im = Image.open("frozenpond.mpo") + im.save(out, save_all=True) + +Additional images can also be appended when saving, by combining the +``save_all`` argument with the ``append_images`` argument:: + + im.save(out, save_all=True, append_images=[im1, im2, ...]) + + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 597c804f8..8c436be3b 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 9.3.0 9.2.0 9.1.1 9.1.0 From 13acf0a545da94a77804832a5da956d1a713862e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 19 Jul 2022 07:50:04 +1000 Subject: [PATCH 009/100] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 19d6fa994..d3c96fefd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Do not double quote Pillow version for setuptools >= 60 #6450 + [radarhere] + - Added ABGR BMP mask mode #6436 [radarhere] From 37e794245ea770662f54250a383e206ee0efaf15 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 19 Jul 2022 13:11:17 +1000 Subject: [PATCH 010/100] Updated libimagequant to 4.0.1 --- 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 31fc2adaa..9b3088b94 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.0.0 +archive=libimagequant-4.0.1 ./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 9e9dc52b3..f147fa6a7 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -166,7 +166,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.0** + * Pillow has been tested with libimagequant **2.6-4.0.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 0844fb0ed33b01656d4cf6ff0c9cd63342611476 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 21 Jul 2022 09:05:14 +1000 Subject: [PATCH 011/100] Do not clear tile if not updating the image when seeking --- Tests/images/comment_after_only_frame.gif | Bin 0 -> 54 bytes Tests/test_file_gif.py | 5 +++++ src/PIL/GifImagePlugin.py | 4 ++-- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 Tests/images/comment_after_only_frame.gif diff --git a/Tests/images/comment_after_only_frame.gif b/Tests/images/comment_after_only_frame.gif new file mode 100644 index 0000000000000000000000000000000000000000..8188b68473246080bb04e13f3a7b412aab4de3bf GIT binary patch literal 54 tcmZ?wbh9u|WMp7uXk Date: Fri, 22 Jul 2022 07:59:30 +1000 Subject: [PATCH 012/100] Moved code into separate function --- src/PIL/ImageFile.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 99b77a37f..9f08493c1 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -499,9 +499,14 @@ def _save(im, fp, tile, bufsize=0): try: fh = fp.fileno() fp.flush() - exc = None - except (AttributeError, io.UnsupportedOperation) as e: - exc = e + _encode_tile(im, fp, tile, bufsize, fh) + except (AttributeError, io.UnsupportedOperation) as exc: + _encode_tile(im, fp, tile, bufsize, None, exc) + if hasattr(fp, "flush"): + fp.flush() + + +def _encode_tile(im, fp, tile, bufsize, fh, exc=None): for e, b, o, a in tile: if o > 0: fp.seek(o) @@ -526,8 +531,6 @@ def _save(im, fp, tile, bufsize=0): raise OSError(f"encoder error {s} when writing image file") from exc finally: encoder.cleanup() - if hasattr(fp, "flush"): - fp.flush() def _safe_read(fp, size): From ad2c6a20fe874958d8d9adecbbfeb81856155f05 Mon Sep 17 00:00:00 2001 From: REDxEYE Date: Sat, 23 Jul 2022 00:30:27 +0300 Subject: [PATCH 013/100] Add support for ATI1/2(BC4/BC5) DDS files This commit adds support for loading DDS with ATI1 and ATI2 fourcc pixel format --- Tests/images/ati2.dds | Bin 0 -> 22000 bytes Tests/images/ati2.png | Bin 0 -> 28408 bytes Tests/test_file_dds.py | 14 ++++++++++++++ src/PIL/DdsImagePlugin.py | 9 ++++++++- 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 Tests/images/ati2.dds create mode 100644 Tests/images/ati2.png diff --git a/Tests/images/ati2.dds b/Tests/images/ati2.dds new file mode 100644 index 0000000000000000000000000000000000000000..7199dcad7620d3b6e3e839370892f6ee901082fa GIT binary patch literal 22000 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 literal 0 HcmV?d00001 diff --git a/Tests/images/ati2.png b/Tests/images/ati2.png new file mode 100644 index 0000000000000000000000000000000000000000..ac166965944dbfc5f0f6adf43a8c4593b6564e3b GIT binary patch literal 28408 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%0AZtXOaoytljRcS#p6{ob-0~K|ppPSLo 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 f-Gc2k+VKAe$zhIzoL_Oj00000NkvXXu0mjf6s1 Date: Sat, 23 Jul 2022 01:02:06 +0300 Subject: [PATCH 014/100] Add test and test files for ATI1 pixel format and fix image mode for ATI1 --- Tests/images/ati1.dds | Bin 0 -> 11064 bytes Tests/images/ati1.png | Bin 0 -> 1134 bytes Tests/test_file_dds.py | 13 +++++++++++++ src/PIL/DdsImagePlugin.py | 2 +- 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 Tests/images/ati1.dds create mode 100644 Tests/images/ati1.png diff --git a/Tests/images/ati1.dds b/Tests/images/ati1.dds new file mode 100644 index 0000000000000000000000000000000000000000..dc9445d89ab698838d671e42281af3905b23132a GIT binary patch literal 11064 zcmZ>930A0KU|?Vu;9_V1(jd&B03sL|I3N_!qykVS3lKYocp75U#3A5-O?XsnGz3ON zU^E0qLtr!n23-g+F?dc=;S^#UbPl7jgIIz7cIlj~VDjB8$0ct^dr<4P+}j&uZ-T^C zJe$;LZ2f>6|DMrvUdzde0{_~Zv>Jpp%pBg#Hu$K$e>>OXZN3uyf9k>EP%uevz#U3u z2LSywi=}h6j{HTpSFPt3Z@-fx@beci%I}4WiG=5u3NQQZyHDj+zn$QfEq@Qyf3Rm} z@N80XoMc92XAGEe%u!}J(rxKG{uT$yU%VIf>}7hc(%wg&;eT^KZsAK>=o$O&&3rq} zP{t>J>*uRitom;9ZFB97^|Ch^LHWXClFEQNoJtl8-ZV?nH_N*HKk9(Rrfl(`AE){5 zZi-Hy`hQy8!~#RN#Q$E85ASSd>9G6s=l1zV+uO`)hB-Gjf_ne=2p2f*f=XM+Ku<WPbxJ}4>oP;=e-T&Gco`ZJ20GZ=^HNMz$dw`Y#ZFO<>V~yDj^@+apz>mv1lFoV={o`F5KuHMxXhn> zb2!C0$S+JXW;us}@qR>pVnWB1w%kuC&qA2~eF*tk)xff5!g||(n=dG>WfO2H|I{LT zb9(KK;pYE2jGl9jc+L>Hk^3R~_tx`P8?Air*L=Nr^=t1OXAg<3W!rxLIBfrOvb|+e z|KFR_{|=k@XU^Z2c(^Uq!ud^D)Bm6U|Nou5fce0I9pSH&%aZ=DUe5U=Z0iReZBwrQ z-&rIU2sJw~-E+p;a%lCA%(YUo?GPhrLS?+bo?yuFiU^5^>7 zEY(xkOCR{pe|CZY$^U;00t`T8^V@RH`O3dCtPI{wDrJ*Yhg&`v^$&eR08+0`Qn<$m ztXK1mEWq`UuMpkp*E`If^HNSu75JNeK{ssc%q6;~HvGM5KX*UaT^>R-OWz=WF7ceO zKlG)_k+1!`l*X>wWNSQoon) zFW77U=hjC0kJBl!^&}xYnwLOP`Nrnj&UO+yn?x2@? z=5(MVx8+(eU*E=iVB6d<$hbgc2hCm^;K*IWA z{$o}J*`Ig>yLn!SsRc;hdzRxEpA1NA69=6TnK;j71-)@3I;&UHdScoczL5 S$VP~Paly8%)!VqZAUXlo!UFXG literal 0 HcmV?d00001 diff --git a/Tests/images/ati1.png b/Tests/images/ati1.png new file mode 100644 index 0000000000000000000000000000000000000000..da475dfd7b01c0c4ae1a8235ce03e641948e040a GIT binary patch literal 1134 zcmV-!1d;oRP)+TM;^CeW+LlqUaDGMnwc)_T&V?Au6~R zMIWRvpF3fUp*UgKStnxFYFDXSTM26hHffS3wd7Qgnl^qDHsTwfR~R<0~+|Id&?C6&Baa#`!krJPr0N zpR8CJUICxq8K3eK2;9hWjny4H0!}A^=f^LUQd$H7-$*g{gIi@xX8*Tv>PLDUt2qvNX#|3`pizS zT|N`nGB{W$peW|;)|pWt)j(gYvl5tnCX<1MB~Z3=`Ub=4y8zh@P5TZ~5X$@R@PtX2iBRZA65I3=#?`QZoStp?3Yl!MP3%DvP z?*QLjSOSC5-5bqjnE)KtqgP$pLR}qtV~J_Cgh&WOd|sURa7rXaGAXWFeBjK1 zcSSOpJYS^X0DL8WJ6_EA@K#m_;>U)fSo@QE1Or>K+D{^pNF)-8L?V$$Boc{4B9TZW z5=jA2t-g4a<77cKQ6a;nh{q_PnhksY1T+;PP=p18;aq1czix8r(n|huA64MJ567FT zmTgy}94afD`eMOClPu6^i^alW5f;CQhn&s-p}CFMhF})p$h^KE2UX5*sP9(OnvJ!!HLIo61gyPgULwnZ$ntCUwd0m>Kln}{ZAg2;cP#%Q9 zb9l>10xab(kV_Lb1K`8g5)4uUuvJijffE(wtkUWGCKy~@1)wBYrF@;$B{&r8=IMP@0pu2V1 zfgLq<=y={&GP7OpVfvbTc=bT_y$AZYi_29zw3-fz%eR-pH=Oi;k zD$gH6b^->p4>C>j)@(1th%6!iTdJvF Date: Fri, 22 Jul 2022 22:02:55 +0000 Subject: [PATCH 015/100] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_dds.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 537302a09..d215e901d 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -76,6 +76,7 @@ def test_sanity_ati1(): assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) + def test_sanity_ati2(): """Check ATI2 images can be opened""" From 4a13857aa6ac1407bbab945d4b1d781c93c2f2d4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 23 Jul 2022 15:29:01 +1000 Subject: [PATCH 016/100] Switch to GitHub Actions artifacts for Windows wheels --- RELEASING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index aa7511c8a..b05067484 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -96,8 +96,8 @@ Released as needed privately to individual vendors for critical security-related ## Binary Distributions ### Windows -* [ ] Contact `@cgohlke` for Windows binaries via release ticket e.g. https://github.com/python-pillow/Pillow/issues/1174. -* [ ] Download and extract tarball from `@cgohlke` and copy into `dist/` +* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) + and copy into `dist/` ### Mac and Linux * [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels): From 1050d13350d7f27f26c82298f1bd4ed0199ef768 Mon Sep 17 00:00:00 2001 From: REDxEYE Date: Sat, 23 Jul 2022 12:44:03 +0300 Subject: [PATCH 017/100] Replace test files with images with compatible license --- Tests/images/ati1.dds | Bin 11064 -> 2896 bytes Tests/images/ati1.png | Bin 1134 -> 969 bytes Tests/images/ati2.dds | Bin 22000 -> 87536 bytes Tests/images/ati2.png | Bin 28408 -> 0 bytes Tests/test_file_dds.py | 6 +++--- 5 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Tests/images/ati2.png diff --git a/Tests/images/ati1.dds b/Tests/images/ati1.dds index dc9445d89ab698838d671e42281af3905b23132a..747e4b1b98ae610f2fc6f70fcc1e870c63b457af 100644 GIT binary patch literal 2896 zcmeH}T}V@57{_0?H9cp}%{c|7Y_*k8hMIce2e@FnID%k)G(JZu!BHfjSQon}_@DDT=l{I#|2gm8 zoiT%MiiB_^i4-6ed89ISS@yBNkjFiA}rh?1*)ub4u{wF(%a8?~ii1PdHm-UK1 zbJDV?)Y>*WuHG)4RjOzdK_(euL<(l_PwT&toqq8cw}sT5u5Em~tI_&z*De zUQ&G8v~WH#kk>p8bsVR91NCBP zXddKp3`{F#zMPe=zePow~t{;gx;SzI^n2SUo zi9RyizBmtak(i6*1B#DI(CW-!PaeqGTiufj;yR8ToB?5u5V;QN#Y~X1cdIWBv^ujF zr{DuMaXe_h{Qc@eebN8h{=vEGTYk`#g|fAfvVaV0^Fi20hy^4ou^iM|xbqHk)q3f#3AJGe0bGRPtT literal 11064 zcmZ>930A0KU|?Vu;9_V1(jd&B03sL|I3N_!qykVS3lKYocp75U#3A5-O?XsnGz3ON zU^E0qLtr!n23-g+F?dc=;S^#UbPl7jgIIz7cIlj~VDjB8$0ct^dr<4P+}j&uZ-T^C zJe$;LZ2f>6|DMrvUdzde0{_~Zv>Jpp%pBg#Hu$K$e>>OXZN3uyf9k>EP%uevz#U3u z2LSywi=}h6j{HTpSFPt3Z@-fx@beci%I}4WiG=5u3NQQZyHDj+zn$QfEq@Qyf3Rm} z@N80XoMc92XAGEe%u!}J(rxKG{uT$yU%VIf>}7hc(%wg&;eT^KZsAK>=o$O&&3rq} zP{t>J>*uRitom;9ZFB97^|Ch^LHWXClFEQNoJtl8-ZV?nH_N*HKk9(Rrfl(`AE){5 zZi-Hy`hQy8!~#RN#Q$E85ASSd>9G6s=l1zV+uO`)hB-Gjf_ne=2p2f*f=XM+Ku<WPbxJ}4>oP;=e-T&Gco`ZJ20GZ=^HNMz$dw`Y#ZFO<>V~yDj^@+apz>mv1lFoV={o`F5KuHMxXhn> zb2!C0$S+JXW;us}@qR>pVnWB1w%kuC&qA2~eF*tk)xff5!g||(n=dG>WfO2H|I{LT zb9(KK;pYE2jGl9jc+L>Hk^3R~_tx`P8?Air*L=Nr^=t1OXAg<3W!rxLIBfrOvb|+e z|KFR_{|=k@XU^Z2c(^Uq!ud^D)Bm6U|Nou5fce0I9pSH&%aZ=DUe5U=Z0iReZBwrQ z-&rIU2sJw~-E+p;a%lCA%(YUo?GPhrLS?+bo?yuFiU^5^>7 zEY(xkOCR{pe|CZY$^U;00t`T8^V@RH`O3dCtPI{wDrJ*Yhg&`v^$&eR08+0`Qn<$m ztXK1mEWq`UuMpkp*E`If^HNSu75JNeK{ssc%q6;~HvGM5KX*UaT^>R-OWz=WF7ceO zKlG)_k+1!`l*X>wWNSQoon) zFW77U=hjC0kJBl!^&}xYnwLOP`Nrnj&UO+yn?x2@? z=5(MVx8+(eU*E=iVB6d<$hbgc2hCm^;K*IWA z{$o}J*`Ig>yLn!SsRc;hdzRxEpA1NA69=6TnK;j71-)@3I;&UHdScoczL5 S$VP~Paly8%)!VqZAUXlo!UFXG diff --git a/Tests/images/ati1.png b/Tests/images/ati1.png index da475dfd7b01c0c4ae1a8235ce03e641948e040a..790d7d7dbb2deede17643a59f2515f7dbb2e2b17 100644 GIT binary patch literal 969 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�%%^RnpX`^({= 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 YGx*xW*^;UXz65-5kb*BqhyV2d0WH*dIsgCw literal 22000 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 diff --git a/Tests/images/ati2.png b/Tests/images/ati2.png deleted file mode 100644 index ac166965944dbfc5f0f6adf43a8c4593b6564e3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28408 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%0AZtXOaoytljRcS#p6{ob-0~K|ppPSLo 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 f-Gc2k+VKAe$zhIzoL_Oj00000NkvXXu0mjf6s1 Date: Mon, 25 Jul 2022 07:53:17 +1000 Subject: [PATCH 018/100] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d3c96fefd..14293586c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Support saving multiple MPO frames #6444 + [radarhere] + - Do not double quote Pillow version for setuptools >= 60 #6450 [radarhere] From ce7af49eed2929f2e052842a98baae6db263fa94 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 25 Jul 2022 18:09:06 +1000 Subject: [PATCH 019/100] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 14293586c..8616ff09c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Do not clear GIF tile when checking number of frames #6455 + [radarhere] + - Support saving multiple MPO frames #6444 [radarhere] From 6e97da02603fcd1fe2e16f6164f737200107d924 Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Tue, 26 Jul 2022 00:30:32 +0300 Subject: [PATCH 020/100] fixing xmp tag orientation generated by exiftool --- Tests/images/xmp_orientation_exifool.png | Bin 0 -> 1935 bytes Tests/test_imageops.py | 7 +++++++ src/PIL/Image.py | 6 ++++++ src/PIL/ImageOps.py | 5 +++++ 4 files changed, 18 insertions(+) create mode 100644 Tests/images/xmp_orientation_exifool.png diff --git a/Tests/images/xmp_orientation_exifool.png b/Tests/images/xmp_orientation_exifool.png new file mode 100644 index 0000000000000000000000000000000000000000..fb30199c86acbc8953d1cf16ddfa9e3ec41ece19 GIT binary patch literal 1935 zcmY+F2{hZ;9>@QxtxWn1Gu2jWOXx_a$WnUdQ2t5V` z06@{v!PW(w`C!LG_JE_;L6P4`et*gwlVAAXEGzg8a;B2x}{-B@YV%Lg*YS zj29Bj48!tph+QrgT<^G12-t22CkTgd450?lu~<7E!=J)p1;a3>kcMak2nnV6V_j_R zzaT&hhX~|w*jN;b%jF`uhDcUu019JnZjM5qLY+Eg07e*ug)=!+o&hsV2P8mYy9`@; zm|rM^&0(;ZupK7VmvxncLm;5AFK7?GmO$a< z&F8iKbD6B(wdmL!K_T9S2q}c`15?RJXrp<;mb@oa+pNAZ5oVT{7IZ}6esn!{T`N9% ze`nFJoi55XkKts6B| z2Pdr~l<<_(Gx(5BY`|M;*F#uEFcj%G(&WlJ9@6LRsrRNDa{5Z^^#nw;4nBTC4q7F0am|R}D?90? z#r)z{Ho&Yi=oKapG8f_Z2$U0$pZVzW(er*>6`FTZ>B$)M?OuBIa7|5(Q<3o9#>bBz zU%ysS+!Jvq$TcAU)9!(IKDtq%_s*fzEyU{h++1*Ycs8l6s>-E=5~$Aq%XE|}*M1cG z=Oip$uDwv!_lI%7XQH$P4>8PsHaV#S-wQV~F>wl9Q8LT5vf$7E9!b^h?70 zW;7xap)6k7Y7-zBlH`e+(uC2!MH_O(g8adL8s}DZLqkJbn-cI;whTUzya9JlPa{%w zU*NOn&vDk)dWq4PsO#6eRJ$e!c=>B3W26XyGZt0(6I*)429nlIGs$hKuOAmVB4jd| zg@uKxA2I$N#rU#P^glW_XSr&Uxjw;*;hQ&aG6o3}8_2-dffKgc=T4}U4$SuSBtCiv zA5Z|)H8rDI-t+C2w1U?UhC%OPYG&489AQ~M0F}<61x#k#Lyp5=#1_d1e z*lhNA*$Z5^Y&qHf-f-|oYsg-dxH-4kuXAtf7B4c=Rpq;U^5Vk6VQ8$g?mdP_jfaMY zhL?v&zmX!`)~3*<1aiL3T|esDHPtw{$wl&}9Uwb9lRApYlm4uWEox|NoXz~SxoHXD z2d{2RL#tX_M}tkSdI{*N%7^71Zlt8-c4`v8A(l{}px{pTB?tsUb5A9dW&w z36=}C`i1y}1fbTMYAvZ=T&XK5b%Na8e&L%(N2063z> zsy^KO80ALcUA;3;(9V*=JHtxFjAL={KYcPcHa5oLS}H2o;o-y8=>r3<@EUrFTUk!= z7OwDc7o1=Y>9T};Zql*0c+VX3$b7rR&+i;z06?O!!q|kArtBgb6r`jS6cjL-Oe0cL zV`GnO+0f8%ZEa0;(!aW-^m3)UB@VahDk~_I%#4g&tPKC4bHo?ed>E^G#_^5mquUnD zgC3cyA3hLIL$H=UNqQO3EuVl`46|;e$OXKJ9FW*49Ls5{s;EF)SzQthHW`S8qX%x~ zUcs<_>z<#!)1X00HKqBs>e#2&9e)Cwqu6GJtf1H0HDqxAfPk;y4W5rEC*^*c&Y0w@ zh5dQu!SHi|kMM`>dR(dN`ktx548kjEhL#UsPhRhV_f%ziR_cE7vtJ^e;R`vN*zMM;q%9liD=p7U(yl0Y zNPLQ>sGN4tQB6EC*;zifuRm^S@3pIylfE-C-s0xwk<|RhHx#^ctNxOcCIL^^)jI}u P{!vFevh5?HPxOBPc@S?M literal 0 HcmV?d00001 diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 87fffa7b7..855b6bccd 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -351,6 +351,13 @@ def test_exif_transpose(): transposed_im = ImageOps.exif_transpose(im) assert 0x0112 not in transposed_im.getexif() + # Orientation from "XML:com.adobe.xmp" info key (from exiftool) + with Image.open("Tests/images/xmp_orientation_exiftool.png") as im: + assert im.getexif()[0x0112] == 8 + + transposed_im = ImageOps.exif_transpose(im) + assert 0x0112 not in transposed_im.getexif() + # Orientation from "Raw profile type exif" info key # This test image has been manually hexedited from exif_imagemagick.png # to have a different orientation diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6abb12491..816ea94db 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1407,6 +1407,12 @@ class Image: match = re.search(r'tiff:Orientation="([0-9])"', xmp_tags) if match: self._exif[0x0112] = int(match[1]) + else: + match = re.search( + r"([0-9])", xmp_tags + ) + if match: + self._exif[0x0112] = int(match[1]) return self._exif diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index f0d4545ba..b26b1858b 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -606,5 +606,10 @@ def exif_transpose(image): "", transposed_image.info["XML:com.adobe.xmp"], ) + transposed_image.info["XML:com.adobe.xmp"] = re.sub( + r"([0-9])", + "", + transposed_image.info["XML:com.adobe.xmp"], + ) return transposed_image return image.copy() From db20d0f8feaf2928ef68791e7e1fdffc8658cd9e Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Tue, 26 Jul 2022 00:45:23 +0300 Subject: [PATCH 021/100] fixing typo in filetest name --- ...xifool.png => xmp_tags_orientation_exiftool.png} | Bin Tests/test_imageops.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename Tests/images/{xmp_orientation_exifool.png => xmp_tags_orientation_exiftool.png} (100%) diff --git a/Tests/images/xmp_orientation_exifool.png b/Tests/images/xmp_tags_orientation_exiftool.png similarity index 100% rename from Tests/images/xmp_orientation_exifool.png rename to Tests/images/xmp_tags_orientation_exiftool.png diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 855b6bccd..95b49596e 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -352,7 +352,7 @@ def test_exif_transpose(): assert 0x0112 not in transposed_im.getexif() # Orientation from "XML:com.adobe.xmp" info key (from exiftool) - with Image.open("Tests/images/xmp_orientation_exiftool.png") as im: + with Image.open("Tests/images/xmp_tags_orientation_exiftool.png") as im: assert im.getexif()[0x0112] == 8 transposed_im = ImageOps.exif_transpose(im) From f42e2552068dd3d6a02e1b544ff07abf08e77036 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 26 Jul 2022 11:58:44 +1000 Subject: [PATCH 022/100] Simplified code --- src/PIL/Image.py | 10 ++-------- src/PIL/ImageOps.py | 13 +++++-------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 816ea94db..4eb2dead6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1404,15 +1404,9 @@ class Image: if 0x0112 not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") if xmp_tags: - match = re.search(r'tiff:Orientation="([0-9])"', xmp_tags) + match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) if match: - self._exif[0x0112] = int(match[1]) - else: - match = re.search( - r"([0-9])", xmp_tags - ) - if match: - self._exif[0x0112] = int(match[1]) + self._exif[0x0112] = int(match[2]) return self._exif diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index b26b1858b..48b41d87f 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -601,15 +601,12 @@ def exif_transpose(image): "Raw profile type exif" ] = transposed_exif.tobytes().hex() elif "XML:com.adobe.xmp" in transposed_image.info: - transposed_image.info["XML:com.adobe.xmp"] = re.sub( + for pattern in ( r'tiff:Orientation="([0-9])"', - "", - transposed_image.info["XML:com.adobe.xmp"], - ) - transposed_image.info["XML:com.adobe.xmp"] = re.sub( r"([0-9])", - "", - transposed_image.info["XML:com.adobe.xmp"], - ) + ): + transposed_image.info["XML:com.adobe.xmp"] = re.sub( + pattern, "", transposed_image.info["XML:com.adobe.xmp"] + ) return transposed_image return image.copy() From 42763400740a06009d4cbcecf32a82501fbfc154 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Jul 2022 21:32:48 +1000 Subject: [PATCH 023/100] Sorted formats by n --- src/PIL/DdsImagePlugin.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 0f2cce1e5..bba480161 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -82,6 +82,7 @@ DDS_CUBEMAP_NEGATIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY DDS_CUBEMAP_POSITIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ + # DXT1 DXT1_FOURCC = 0x31545844 @@ -155,6 +156,14 @@ class DdsImageFile(ImageFile.ImageFile): elif fourcc == b"DXT5": self.pixel_format = "DXT5" n = 3 + elif fourcc == b"ATI1": + self.pixel_format = "BC4" + n = 4 + self.mode = "L" + elif fourcc == b"ATI2": + self.pixel_format = "BC5" + n = 5 + self.mode = "RGB" elif fourcc == b"BC5S": self.pixel_format = "BC5S" n = 5 @@ -192,14 +201,6 @@ class DdsImageFile(ImageFile.ImageFile): raise NotImplementedError( f"Unimplemented DXGI format {dxgi_format}" ) - elif fourcc == b"ATI1": - self.pixel_format = "BC4" - n = 4 - self.mode = "L" - elif fourcc == b"ATI2": - self.pixel_format = "BC5" - n = 5 - self.mode = "RGB" else: raise NotImplementedError(f"Unimplemented pixel format {repr(fourcc)}") From 7e1261c6a0001c2302d391da24a44e2b5d669b57 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Jul 2022 22:18:39 +1000 Subject: [PATCH 024/100] Simplified test code --- .../images/xmp_tags_orientation_exiftool.png | Bin 1935 -> 1258 bytes Tests/test_imageops.py | 16 +++++----------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Tests/images/xmp_tags_orientation_exiftool.png b/Tests/images/xmp_tags_orientation_exiftool.png index fb30199c86acbc8953d1cf16ddfa9e3ec41ece19..10f0f44009ae9237c1a5712ae8b0c7269b527711 100644 GIT binary patch literal 1258 zcmbtUO=uHA6rN($novXq`-gO`;GVjFO|jV~DKV)grKy@mnu=s}AL+KVR@ylDkNREki%i75Ie4RNh42p)E4-uJ%wX1=%ccJlDR z!FpG-3n5e=jfCSclDVsH6THt0&mY22n+?W-2+dt^f0s5wOr;|67(%7J2u++t=o>&2 zpAZ`75Ly~RNW6+rgFf@%hz}B5^vJM@kb4JtYS8pdBSIUN>hyjw(_Xi(;=WXA+v0r+9(!cxVtY(it%xK2VW> z$WM=1mM*evp-^B79gJpVS>EgQvYfyQLOU?p&7x{acDris0R#cAkz!yu5IE?+fSxe5WW0qtbW=ROn6y3mP*+Hj|{R<<9 zFN22+8AItwkpmqSq7$nUlV9m7e^a2<#IoWG7f<4zI;@g8x-E zxX)`}5nIw7MFW}`GO%Q62D!|E^2b(4NOdp*18!F{$oUBt)6#OL7?vy?5IEi=@|?(Z zQCugPbakz)b8yx)vvMtl)CbuW$7~h5p9Ta4^x>bn4=17usDH zT3r0`vte^Cb2518`jfjiw?`gvp35)47H2=-Ug&G7ZR_g`-q`iNd)u*RCw8(;^K;YQ zd#Q!D?rR@Ed}*yunypi9$0x50ox9&W*uV6q``sOWs(rcjMGfBC(%^no^pHPFw09sp IyMN@&FNzYMoB#j- delta 1598 zcmV-E2EqC236BpYiBL{Q4GJ0x0000DNk~Le0001h0000$2m$~A09~}`C;$KfiIH}Y z0y8j^2mx)AVgfFcxdIFjI6NP zABchm#igLdQV_MYhS1}}ceuxJ-!x4x$)%}}_nNA`++E-A``oWw@&`f)xKR{^Kk=`{ zvMm0@L!9ZP^!!k*e>iREVSv;jQ4VMt0J7gTwve#}S%Vup7iHfX4->MK1VB%+#xrehZ4r zj6pG=Z=(0CK%bq6(}37Tccsrx#9%;lx_4XC_}U+~q4kW~e}?`U5QXlXt4KDPUo)b% zA#wwR>+UUj&)E*=F0pI2QP}Wjcz9TNrWAqF_iX8GS&E`S2oXYtVc3#Uxm^|X<%Dn# zB3TKmsv2H>g@Lmynwb7+SynEW8yOjyn3zBa(M((`*0TslalU_vCxRnDi0%OjvaeEA zm3n{>N+y$pe~?zI)o!;f%c70fZto$ZK~yLRi-0h#-dAIH6l7nesw#vqm&;|d*|oK` zN~NOf`u6tr)YKFW0(uo|RcD>~qd`USH{?9q?T*@uURY1cv1LkQIl zXyQa7Q79Cu)#}R1imvM+)dv6^9v*^kU+uv>WS+k89RWP`^XP3H!zKU#LO3}&S+CdG zc9h9vYPA|6#QrC>VQXs(0B~?{;H%C($>;Npe@3I(Z1z701TVzAo4jMi1it-veSK9F zC7n+1?(P-}g-j-+Xo}O-QZZu81y1L5e^D{Fu_xJZ0<7Ts|s%o)VJUu=2 zRp*|h)9KsWTd{faBNt)-{H%o-Pf6A{u9rJ*6Izz_^72AQ2s%7qj4PE2{a|N>e=%;i z+oe*eR;vL3ip8Sen!B>FurNM8Ua!}?EySYU3zD00F>l7n?dYGm9ZiupWMaM}Mv=E9 z!yxQbM3`;#F!#Y7=OJciX9*#>T&`3q0RWDVkIUthZ1v2W@Fc`U`j z4?dPcVP~1a@R-7O7R1hI?e2wYe~mFtr_-%gE0s#oeLyyiYpdsP_yV_1<GKjeejT)4{Q*OfugXxfz@V1O$F zE2v-Ov6PsR2E@#eeM}+ln*2KczOsz{L)y?ETwdD8ej#m$jvlYAWD`jn#Ny+{t?UKT whVTS@b+O}O>4aazyu90yowR`mUz@W30osfJ$oL$>)c^nh07*qoM6N<$f|I@g`~Uy| diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 95b49596e..bd5f44e50 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -345,18 +345,12 @@ def test_exif_transpose(): check(orientation_im) # Orientation from "XML:com.adobe.xmp" info key - with Image.open("Tests/images/xmp_tags_orientation.png") as im: - assert im.getexif()[0x0112] == 3 + for suffix in ("", "_exiftool"): + with Image.open("Tests/images/xmp_tags_orientation" + suffix + ".png") as im: + assert im.getexif()[0x0112] == 3 - transposed_im = ImageOps.exif_transpose(im) - assert 0x0112 not in transposed_im.getexif() - - # Orientation from "XML:com.adobe.xmp" info key (from exiftool) - with Image.open("Tests/images/xmp_tags_orientation_exiftool.png") as im: - assert im.getexif()[0x0112] == 8 - - transposed_im = ImageOps.exif_transpose(im) - assert 0x0112 not in transposed_im.getexif() + transposed_im = ImageOps.exif_transpose(im) + assert 0x0112 not in transposed_im.getexif() # Orientation from "Raw profile type exif" info key # This test image has been manually hexedited from exif_imagemagick.png From bac83f7dd3ac738f8bc16a6647f69f373916ae2b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Jul 2022 22:27:43 +1000 Subject: [PATCH 025/100] Check that orientation is still absent after reloading Exif --- Tests/test_imageops.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index bd5f44e50..01e40e6d4 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -352,6 +352,9 @@ def test_exif_transpose(): transposed_im = ImageOps.exif_transpose(im) assert 0x0112 not in transposed_im.getexif() + transposed_im._reload_exif() + assert 0x0112 not in transposed_im.getexif() + # Orientation from "Raw profile type exif" info key # This test image has been manually hexedited from exif_imagemagick.png # to have a different orientation From 78a6bb4c992664904896c37f20100225023a7300 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Jul 2022 22:33:27 +1000 Subject: [PATCH 026/100] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8616ff09c..ed157abec 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Added support for reading ATI1/ATI2 (BC4/BC5) DDS images #6457 + [REDxEYE, radarhere] + - Do not clear GIF tile when checking number of frames #6455 [radarhere] From cbe292212b96a27aa82501c89596efb13c263d59 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 28 Jul 2022 08:35:10 +1000 Subject: [PATCH 027/100] Added release notes for #6457 --- docs/releasenotes/9.3.0.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index da045a50a..c64423b01 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -53,7 +53,7 @@ TODO Other Changes ============= -TODO -^^^^ +Added DDS ATI1 and ATI2 reading +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Support has been added to read the ATI1 and ATI2 formats of DDS images. From f2ce07cf228024d7d35d31c74fa385833262c9d6 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 30 Jul 2022 13:29:10 +1000 Subject: [PATCH 028/100] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ed157abec..b821e7732 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Parse orientation from XMP tag contents #6463 + [bigcat88, radarhere] + - Added support for reading ATI1/ATI2 (BC4/BC5) DDS images #6457 [REDxEYE, radarhere] From 5cc9ab5b1d78dd154d7a60883acb3d7fa5fd09c3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Aug 2022 08:55:31 +1000 Subject: [PATCH 029/100] Updated harfbuzz to 5.1.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 bc5fb4d02..b1e6e4b8e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -281,9 +281,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/4.4.1.zip", - "filename": "harfbuzz-4.4.1.zip", - "dir": "harfbuzz-4.4.1", + "url": "https://github.com/harfbuzz/harfbuzz/archive/5.1.0.zip", + "filename": "harfbuzz-5.1.0.zip", + "dir": "harfbuzz-5.1.0", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), From f5b27f90f7efcf01a68f7e3d84531d03e9ebfc5e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Aug 2022 20:38:47 +1000 Subject: [PATCH 030/100] Save 1 mode PDF using CCITTFaxDecode filter --- Tests/test_file_pdf.py | 2 +- src/PIL/PdfImagePlugin.py | 44 +++++++++++++++++++++++++++++++-------- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index c71d4f5f2..310619fb2 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -43,7 +43,7 @@ def test_monochrome(tmp_path): # Act / Assert outfile = helper_save_as_pdf(tmp_path, mode) - assert os.path.getsize(outfile) < 15000 + assert os.path.getsize(outfile) < 5000 def test_greyscale(tmp_path): diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 2109a6f52..d1b34be48 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -21,10 +21,11 @@ ## import io +import math import os import time -from . import Image, ImageFile, ImageSequence, PdfParser, __version__ +from . import Image, ImageFile, ImageSequence, PdfParser, TiffImagePlugin, __version__ # # -------------------------------------------------------------------- @@ -123,8 +124,26 @@ def _save(im, fp, filename, save_all=False): params = None decode = None + # + # Get image characteristics + + width, height = im.size + if im.mode == "1": - filter = "DCTDecode" + filter = "CCITTFaxDecode" + bits = 1 + params = PdfParser.PdfArray( + [ + PdfParser.PdfDict( + { + "K": -1, + "BlackIs1": True, + "Columns": width, + "Rows": height, + } + ) + ] + ) colorspace = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "L": @@ -161,6 +180,11 @@ def _save(im, fp, filename, save_all=False): if filter == "ASCIIHexDecode": ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) + elif filter == "CCITTFaxDecode": + original_strip_size = TiffImagePlugin.STRIP_SIZE + TiffImagePlugin.STRIP_SIZE = math.ceil(im.width / 8) * im.height + im.save(op, "TIFF", compression="group4") + TiffImagePlugin.STRIP_SIZE = original_strip_size elif filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) elif filter == "FlateDecode": @@ -170,22 +194,24 @@ def _save(im, fp, filename, save_all=False): else: raise ValueError(f"unsupported PDF filter ({filter})") - # - # Get image characteristics - - width, height = im.size + stream = op.getvalue() + if filter == "CCITTFaxDecode": + stream = stream[8:] + filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) + else: + filter = PdfParser.PdfName(filter) existing_pdf.write_obj( image_refs[page_number], - stream=op.getvalue(), + stream=stream, Type=PdfParser.PdfName("XObject"), Subtype=PdfParser.PdfName("Image"), Width=width, # * 72.0 / resolution, Height=height, # * 72.0 / resolution, - Filter=PdfParser.PdfName(filter), + Filter=filter, BitsPerComponent=bits, Decode=decode, - DecodeParams=params, + DecodeParms=params, ColorSpace=colorspace, ) From 2b14d83549b2100c5d08dd8cd9231dd53dde377b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Aug 2022 21:41:17 +1000 Subject: [PATCH 031/100] Added strip_size as TIFF encoder argument --- Tests/test_file_libtiff.py | 12 ++++++++---- src/PIL/PdfImagePlugin.py | 13 ++++++++----- src/PIL/TiffImagePlugin.py | 3 ++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index a43548ae0..3084425a4 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1011,14 +1011,18 @@ class TestFileLibTiff(LibTiffTestCase): # Assert that there are multiple strips assert len(im.tag_v2[STRIPOFFSETS]) > 1 - def test_save_single_strip(self, tmp_path): + @pytest.mark.parametrize("argument", (True, False)) + def test_save_single_strip(self, argument, tmp_path): im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") - TiffImagePlugin.STRIP_SIZE = 2**18 + if not argument: + TiffImagePlugin.STRIP_SIZE = 2**18 try: - - im.save(out, compression="tiff_adobe_deflate") + arguments = {"compression": "tiff_adobe_deflate"} + if argument: + arguments["strip_size"] = 2**18 + im.save(out, **arguments) with Image.open(out) as im: assert len(im.tag_v2[STRIPOFFSETS]) == 1 diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index d1b34be48..181a05b8d 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -25,7 +25,7 @@ import math import os import time -from . import Image, ImageFile, ImageSequence, PdfParser, TiffImagePlugin, __version__ +from . import Image, ImageFile, ImageSequence, PdfParser, __version__ # # -------------------------------------------------------------------- @@ -181,10 +181,13 @@ def _save(im, fp, filename, save_all=False): if filter == "ASCIIHexDecode": ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) elif filter == "CCITTFaxDecode": - original_strip_size = TiffImagePlugin.STRIP_SIZE - TiffImagePlugin.STRIP_SIZE = math.ceil(im.width / 8) * im.height - im.save(op, "TIFF", compression="group4") - TiffImagePlugin.STRIP_SIZE = original_strip_size + im.save( + op, + "TIFF", + compression="group4", + # use a single strip + strip_size=math.ceil(im.width / 8) * im.height, + ) elif filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) elif filter == "FlateDecode": diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0dd49340d..da33cc5a5 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1684,7 +1684,8 @@ def _save(im, fp, filename): stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) # aim for given strip size (64 KB by default) when using libtiff writer if libtiff: - rows_per_strip = 1 if stride == 0 else min(STRIP_SIZE // stride, im.size[1]) + im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) + rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1]) # JPEG encoder expects multiple of 8 rows if compression == "jpeg": rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1]) From 82974a404c4df2f4fac63c1a6c3a4607217a847b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 18:14:07 +0000 Subject: [PATCH 032/100] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/Lucas-C/pre-commit-hooks: v1.2.0 → v1.3.0](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.2.0...v1.3.0) - [github.com/PyCQA/flake8: 4.0.1 → 5.0.2](https://github.com/PyCQA/flake8/compare/4.0.1...5.0.2) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e5e1f3557..1bb71bd72 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,13 +19,13 @@ repos: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.2.0 + rev: v1.3.0 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 5.0.2 hooks: - id: flake8 additional_dependencies: [flake8-2020, flake8-implicit-str-concat] From 8464ed423b20741a6f71385ba03d56cea91c455f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Aug 2022 07:46:41 +1000 Subject: [PATCH 033/100] Updated Valgrind job to Jammy --- .github/workflows/test-valgrind.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 013e5ca4a..dda1b3577 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: docker: [ - ubuntu-20.04-focal-amd64-valgrind, + ubuntu-22.04-jammy-amd64-valgrind, ] dockerTag: [main] From 1112ad67a35eb25d0363d68227fbbb3eea9f2b36 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Aug 2022 14:18:31 +1000 Subject: [PATCH 034/100] Document that orientation data is removed by exif_transpose() --- src/PIL/ImageOps.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 48b41d87f..44214fead 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -572,8 +572,11 @@ def solarize(image, threshold=128): def exif_transpose(image): """ - If an image has an EXIF Orientation tag, return a new image that is - transposed accordingly. Otherwise, return a copy of the image. + If an image has an EXIF Orientation tag return a new image that is + transposed accordingly. The new image will have the orientation data + removed. + + Otherwise, return a copy of the image. :param image: The image to transpose. :return: An image. From 1197e1998214ca54e41772d2f804b02e528a7bab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Aug 2022 14:19:17 +1000 Subject: [PATCH 035/100] Document that exif_transpose() does not change orientations of 1 --- src/PIL/ImageOps.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 44214fead..0c3f900ca 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -572,9 +572,9 @@ def solarize(image, threshold=128): def exif_transpose(image): """ - If an image has an EXIF Orientation tag return a new image that is - transposed accordingly. The new image will have the orientation data - removed. + If an image has an EXIF Orientation tag, other than 1, return a new image + that is transposed accordingly. The new image will have the orientation + data removed. Otherwise, return a copy of the image. From 101f1158534f77594d6383125b4a16652d43ae91 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Aug 2022 20:03:24 +1000 Subject: [PATCH 036/100] Increased tolerance to allow for libtiff with libjpeg-turbo --- Tests/test_file_libtiff.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index a43548ae0..01f29fbd1 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -856,7 +856,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_strip_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: - assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) + assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2) @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" @@ -864,7 +864,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_strip_ycbcr_jpeg_1x1_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: - assert_image_equal_tofile(im, "Tests/images/flower2.jpg") + assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) def test_tiled_cmyk_jpeg(self): infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif" @@ -877,7 +877,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_tiled_ycbcr_jpeg_1x1_sampling(self): infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: - assert_image_equal_tofile(im, "Tests/images/flower2.jpg") + assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" @@ -885,7 +885,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_tiled_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: - assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) + assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.5) def test_strip_planar_rgb(self): # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ From e77a7b6b4f0b496a70cabd8360f0988d32bea063 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 Aug 2022 23:29:58 +1000 Subject: [PATCH 037/100] Added support for RGBA PSD images --- Tests/images/rgba.psd | Bin 0 -> 2448 bytes Tests/test_file_psd.py | 7 ++++++- src/PIL/PsdImagePlugin.py | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 Tests/images/rgba.psd diff --git a/Tests/images/rgba.psd b/Tests/images/rgba.psd new file mode 100644 index 0000000000000000000000000000000000000000..45fb7c3cca0cbae6a57dc605931f9abcbba65013 GIT binary patch literal 2448 zcmcC;3J7LkWPkt`Ae92f91P4*F&PUdPhaM@V4eVwWCTM5{RSxZ) z8MZ0E)<#kC-9HyiiMjyLSY%W1C^>5gw1|Uw!%uTwF$)WhOkNXUK*X#b5%f{(auzz$&qhRY}%4S~TI0_+UbffV{|?*E2SL~IB!G|U}EqaiTVLf|38 P-Tx068b%~V>kJD3HVMyL literal 0 HcmV?d00001 diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index b4b5b7a0c..4f934375c 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -4,7 +4,7 @@ import pytest from PIL import Image, PsdImagePlugin -from .helper import assert_image_similar, hopper, is_pypy +from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy test_file = "Tests/images/hopper.psd" @@ -107,6 +107,11 @@ def test_open_after_exclusive_load(): im.load() +def test_rgba(): + with Image.open("Tests/images/rgba.psd") as im: + assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png") + + def test_icc_profile(): with Image.open(test_file) as im: assert "icc_profile" in im.info diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 04c2e4fe3..bd10e3b95 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -75,6 +75,9 @@ class PsdImageFile(ImageFile.ImageFile): if channels > psd_channels: raise OSError("not enough channels") + if mode == "RGB" and psd_channels == 4: + mode = "RGBA" + channels = 4 self.mode = mode self._size = i32(s, 18), i32(s, 14) From 61ec41511da16f8083e8ebab1c60c5b60625a336 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Aug 2022 15:40:10 +1000 Subject: [PATCH 038/100] Updated libwebp to 1.2.4 --- 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 ed17f2228..05867b7d4 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.2.3 +archive=libwebp-1.2.4 ./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 b1e6e4b8e..d46c1a409 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -157,9 +157,9 @@ deps = { # "bins": [r"libtiff\*.dll"], }, "libwebp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.3.tar.gz", - "filename": "libwebp-1.2.3.tar.gz", - "dir": "libwebp-1.2.3", + "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", "build": [ cmd_rmdir(r"output\release-static"), # clean cmd_nmake( From 04d976131673b94c6065d5cf92b0eab53c4469f3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Aug 2022 17:29:44 +1000 Subject: [PATCH 039/100] Changed "font" to class variable --- Tests/test_imagedraw.py | 17 +++++++++++++++++ src/PIL/ImageDraw.py | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 23bc756bb..961b4d081 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1314,6 +1314,23 @@ def test_stroke_multiline(): assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3) +def test_setting_default_font(): + # Arrange + im = Image.new("RGB", (100, 250)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + ImageDraw.ImageDraw.font = font + + # Assert + try: + assert draw.getfont() == font + finally: + ImageDraw.ImageDraw.font = None + assert isinstance(draw.getfont(), ImageFont.ImageFont) + + def test_same_color_outline(): # Prepare shape x0, y0 = 5, 5 diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 8970471d3..712ec6e09 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -46,6 +46,8 @@ directly. class ImageDraw: + font = None + def __init__(self, im, mode=None): """ Create a drawing instance. @@ -86,7 +88,6 @@ class ImageDraw: else: self.fontmode = "L" # aliasing is okay for other modes self.fill = 0 - self.font = None def getfont(self): """ From 42a5a743c18d87d9c54ed5ff11303caf9fcd0b4b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Aug 2022 22:48:10 +1000 Subject: [PATCH 040/100] Note to Windows users that FreeType will keep the font file open --- src/PIL/ImageFont.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a3b711c60..efd702b86 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -906,9 +906,10 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): This function loads a font object from the given file or file-like object, and creates a font object for a font of the given size. - Pillow uses FreeType to open font files. If you are opening many fonts - simultaneously on Windows, be aware that Windows limits the number of files - that can be open in C at once to 512. If you approach that limit, an + Pillow uses FreeType to open font files. On Windows, be aware that FreeType + will keep the file open as long as the FreeTypeFont object exists. Windows + limits the number of files that can be open in C at once to 512, so if many + fonts are opened simultaneously and that limit is approached, an ``OSError`` may be thrown, reporting that FreeType "cannot open resource". This function requires the _imagingft service. From c24b6ef4f095ba2b9e3f35d8f470d931b1310a11 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Aug 2022 23:01:36 +1000 Subject: [PATCH 041/100] Document workaround --- src/PIL/ImageFont.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index efd702b86..9386d0086 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -911,6 +911,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): limits the number of files that can be open in C at once to 512, so if many fonts are opened simultaneously and that limit is approached, an ``OSError`` may be thrown, reporting that FreeType "cannot open resource". + A workaround would be to copy the file(s) into memory, and open that instead. This function requires the _imagingft service. From 5d71ba3ca140914ff05ad8246b6d0a7053216556 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 Aug 2022 09:13:06 +1000 Subject: [PATCH 042/100] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b821e7732..5f99d9d25 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Save 1 mode PDF using CCITTFaxDecode filter #6470 + [radarhere] + +- Added support for RGBA PSD images #6481 + [radarhere] + - Parse orientation from XMP tag contents #6463 [bigcat88, radarhere] From 8135bd5cfbfa1e9eacd7c24adbbfac14bb92c9e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Aug 2022 10:35:44 +1000 Subject: [PATCH 043/100] Added documentation --- docs/reference/ImageDraw.rst | 7 ++++++- docs/releasenotes/9.3.0.rst | 10 ++++++++++ src/PIL/ImageDraw.py | 5 +++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index c2d72c804..1ef9079fb 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -64,7 +64,7 @@ Fonts PIL can use bitmap fonts or OpenType/TrueType fonts. -Bitmap fonts are stored in PIL’s own format, where each font typically consists +Bitmap fonts are stored in PIL's own format, where each font typically consists of two files, one named .pil and the other usually named .pbm. The former contains font metrics, the latter raster data. @@ -146,6 +146,11 @@ Methods Get the current default font. + To set the default font for all future ImageDraw instances:: + + from PIL import ImageDraw, ImageFont + ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + :returns: An image font. .. py:method:: ImageDraw.arc(xy, start, end, fill=None, width=0) diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index c64423b01..a8db4edd6 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -26,6 +26,16 @@ TODO API Additions ============= +Allow default ImageDraw font to be set +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Rather than specifying a font when calling text-related ImageDraw methods, or +setting a font on each ImageDraw instance, the default font can now be set for +all future ImageDraw operations. + + from PIL import ImageDraw, ImageFont + ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + Saving multiple MPO frames ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 712ec6e09..e84dafb12 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -93,6 +93,11 @@ class ImageDraw: """ Get the current default font. + To set the default font for all future ImageDraw instances:: + + from PIL import ImageDraw, ImageFont + ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + :returns: An image font.""" if not self.font: # FIXME: should add a font repository From 34591207326fbb2fdcf603324b6a0bb98726c654 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Aug 2022 20:46:58 +1000 Subject: [PATCH 044/100] Fixed writing bytes as ASCII tag --- Tests/test_file_tiff_metadata.py | 16 ++++++++++++++++ src/PIL/TiffImagePlugin.py | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index d7a0d9377..d38c1c523 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -185,6 +185,22 @@ def test_iptc(tmp_path): im.save(out) +def test_writing_bytes_to_ascii(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[271] + assert tag.type == TiffTags.ASCII + + info[271] = b"test" + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[271] == "test" + + 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 da33cc5a5..b4c42799e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -727,7 +727,9 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(2) def write_string(self, value): # remerge of https://github.com/python-pillow/Pillow/pull/1416 - return b"" + value.encode("ascii", "replace") + b"\0" + if not isinstance(value, bytes): + value = value.encode("ascii", "replace") + return value + b"\0" @_register_loader(5, 8) def load_rational(self, data, legacy_api=True): From 84bdb635c2d144ec416382210d9825e5cdda065d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 16:36:46 +1000 Subject: [PATCH 045/100] Updated libjpeg-turbo to 2.1.4 --- 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 d46c1a409..a381d636d 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -108,9 +108,9 @@ header = [ deps = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/2.1.3/libjpeg-turbo-2.1.3.tar.gz/download", - "filename": "libjpeg-turbo-2.1.3.tar.gz", - "dir": "libjpeg-turbo-2.1.3", + + "/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", "build": [ cmd_cmake( [ From 7e1a0ca54436bddfa38386a0b8ed5dc025ddee92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 18:32:29 +1000 Subject: [PATCH 046/100] Open 1 bit EPS in mode 1 --- Tests/images/1.eps | Bin 0 -> 45834 bytes Tests/test_file_eps.py | 5 +++++ src/PIL/EpsImagePlugin.py | 13 ++++++++----- 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 Tests/images/1.eps diff --git a/Tests/images/1.eps b/Tests/images/1.eps new file mode 100644 index 0000000000000000000000000000000000000000..727dc9b7f044a76c5021361e57abe2764cd8567b GIT binary patch literal 45834 zcmeHw349b)x^Dvl8d=;$MePK`o^;pN)fGrWS64S{7Dz(E8ahd5F`e$v-GKlD+&lMP zy^hXs@j5!V3@)Q6q6mm6jBs@nHXX<1yleZMn=F>q_gLZ=9xW=qVzKe#hg&Yj(-oHUmWwRi%o^Q# zu=F@QcQev6VlfhX@YsJQU5sry}nyd*6#u2 z-(aMVAwATn-yLbLTX#z`?oIgcNMD8f0JI%{TQ`gI28(4R(lxj1I9x*m#r4nUk}Q`E zX}!BDLS=qyesw4sid2W{t-O%VTZiWO%4>p@GYpXJJfC5AW^jTP-=dPiJ1tIie9lD| zF@PllyG^(BxFL0Hr|&=={|9!@w_G$KZ(Q!c?y}`#-p)DgBF_!%KHAcY<9S|ESRbjZ zkIM!=RMF$XJAU*u1o8~Ks>J@THg@GV^7M^Z@ZVSo-dVYSmJfYjm%EO`h;%Yx8 z7d%-LtTM^hj|!#$Cp8p}hUx;LU=?+XtB+YiC?w?fHS?#`R8=PyTxu2ow|IS$jvhxD zz{@NZxOaoV8FwZ*gIQTW_RYSs|H-2BTVM7_l6@1ad0pG*9^bR)OV~HQpl8t65aLa6 zx*--|Sk{)5X`FAnk3lv*MnAeXVj6_iy`D;Xc<|MWQpQEIL3m$m@mTUL1(s4vjwR2M zi;pRSMn&UhupfJ(c5F5it0Y!1^IHYW5^M;N#FK9ARTgRpRz%wIb$OIS8AF?xAx?2* zwwp1F+B&lgjVY}YN2v9R1Zv9tk*R@6#CqM@^wX1-A^Qq^db5Y|T0&pFtHPm%`Zig; zLgAXKnz&Z+6cF@0mIoMpeGSpjSbxwT_C@{FyQOeJy{QJ1^vaY&CF6Bv_>UfckA`rd zjWT7#Z<~`7scNfmd-(#eDNh0_qpVfK9^sbbm`?+_wUoE|J4Joyz=TRuN-iBA9fix{h|GLx4xHN+P7cd z%lh@Z?5ckK`dvj|{jSoRT;3S~vH!AMeld_A*P~mCrTgXGdR*Qu_8DZgXY93Z*}$@A zH+BJGy5QAyzo=)g-WOkzbZNKth0vEh%)-ko-7f0Zqx(hOd-m>ianBxo1QcH07F zUVSX;xb}mhFa3Yt@UMT`)$rHvy+iK#!@oWM+t)t)+lj|FzP|gblT$0_-FEL2FKqwl zK$ASSxN83G_pRFW#-4+xF1K{=4s0)CIQ8z;Q(`D2b5~ph4Cba>*^~e0#l2pva%_N=-UjSUTh z|8_F4^pnpD_|<=pS;l{`{`^~Kg>J(RY_7_?Zo$dumZN9o-1S@5Zs50-6xVVyR#Y9$ z^L~_h=H{w$e#Gk4->rLjYEe_(Zocl#H>WSCTH|}}%Z6*V2qTgnIO;s}o2Q2^Uj6Cq zv+i5|FNZdyohdneA{w)lW-s1d_WsIWuguHebmG*B&mWurc(gX-z%|}FW%tQGM}Bep zgLiFxxMch0`@ea$rS9I714q6SUS|(KFl)u^7mB79e^7fi`{TK5zL|Y@TFa;#daRj# zB=XHQ{xAGRziTS3oZEL3e{RQp4Ff+}mr-fUfBc@(rgMSj?5e3dW-Qqq+Si=xzhl@} z3!h#wJ2y9LV!*LyT2N`MuGkv0yihzZb-=t0SN6W*+{BgshCd&B{PyOmeZOuVy*kX* za4#NjSXMi5`05pr8za~M{qG;`+Pc1Z-{B2M=PlgtU%ht5k*zz6Pt2=K`+V)m^3~g( zsw_M_^07hvu5TV`Ym_$aI&^x-Wn(d2k?|pLOb$1khJmRf)H=MX{&x+r? z{q1LW&d#~#{_h9w`P)*i+TXwC@n_E#Y+7bVf$6oGp|GI(uC+{16HnnNt{xwVPzN>UY>ZrYo$F4d#d-;-_ zji24QVesmkO1A%P-Nup|zP^9PlGy?m3A}Nr$q}>cJw4#`y#>3Tv(Me;FUx9vc1vE# zmM8z*T)J%K@?~d*-G{F`wKrJwk@UolMd7WlEGZf=Ei-%7fv;vY-8|8MdfHV>c5N7P z;=13x;6A*ybn=Zujuba%m-T(~t=AvjHLEU?*=J4e;}0#*k>3BX`L@i)Q`3HZxcp=< zf6@B=&wY6^Z|_S3e%D-gxUt_muk3i{qs*4EyWiUI`Aj?kPF<$J?iVa%|)8Hp)%Gd5h+L`^>vVxLn z`d#DAd4Biu(95YxX6W&XIa{9ja`}J$^R9)zw{L#%nzFKjUDFoSMZerLKmX0Q-hJh@ zH=la9CA|Io&JBYGubj~@W+`ZDDE|AbInn;pSAS=FW7Var0~-W>b?p}afltowoqf}Y z&x)rUnl<8*WgAZi&Zawp^G__w+H=R(vzF~Xnf=U}GY^%^1HK3zy6#@NY4PdQC*B#k zdSCt0Y9V#gq-s3^Jp!u6$-}hVZ+0x~8 zYqsyWYHIk~&r2&Edk%~m(K};*>7b_4@k<8h7A-j4cp|Xj(LJ}%_CJ-gdUM0zcNSLV zZb?7(*hh1PXV%~O?2}(Ucpvw!(sIkm-lK*LUemPhbmQL7A3Ehf@ta2$@839lTaV1t z_r5AWnf-LjpeGJ~e4u%r-0$3^MLCae+CAs6>@R8+U9E~Z$G*9 zmE|iR@=h3^7kmG}++H`8dJo_Hw{yRkG;rZ}$9HbMeOg_^4BzH8DWB~B@YtK%(q25% z|7`lcVcS<_eDmDq_xi6oKl#7jzBc>mlYzSyUAL^R_-~6Jedq9JC2McE{Jzbfd>h!j zW$o*SPX6WCiSN$8vH0_Q2jAH6<<_^Cytiuia~tYvpFOf+}=byp4F zoN@aj?r+!B?)_j!)68Ei`)J|pvHOnhc>QeNX9!B{&mN>bsv25=7uNtMP6_GFso_f>5X3=+c4y&Uu}IqXX_j5?|Sy>`CqSYkv=TG z>z<euu{nXkumHvjE zPoF+}{`KZUFy}tYQrd7o!7u;2INZIS~{dMoWf}3vi7A<>ymF?!o zD}@_Iq@Dgu-4S@zjyL0o)d#1nk+MBPw@z@CY{lhJ8+u%>W{p?pa z|HrG%0}g)G@Zi$*({e-khZmH5{?OH*?Fzj1`r6?S2DWZ_Ag^TmN$!K?(X8cntoV>W ze(1NG@A~GU70W8qy_wR=f_bT*FPm^^!F4Z2KR9=5@ZD9y6`w!;;ZtXOWk3Gm&=VOC z*pE!RXYp@WEI6_M*YgVxM>l@?%OkTN4pvSbyL!hj0=L$GH+S>ef|i%=SR|}%NzIgd zyubE}Kklnt_2;#>)y*rP*06lVo=ivi$)|6=_nViKo_>AnVQ}1g@4Rt%-pPUUcF$UT z^Uk6}4+Z*Eep%)`aIod^Gu`$$j*Oi6Z0-H$K7XwI`)|M7 zy00q#v*q6Gr*}11j(z6v7cJWc?RtH}+?`WiJDZ;N;$8Eyi(a4q$+zpDdEtQvZ@BAh z=Jo4C8&8})|5k3M=z~v?y|-lBoIk}ZwpoX63I2BX+6}8JXMFQ%c4fhx)pai^>5HCz z;_Ydkt9RdZZuHU(?|-;IZ*R;}^2U}Y$_^fwHumVi&EFk+ef+T4UVXik%$5tq`0hpv zr2ZBcLJ}cMnZ<9hA}CRfbQGTmOVy*sRXa;;qi}_Tx<=Nf~2MAdu9Y0OEe?O zTaT!o8FhZRKbCUH?}57K@i8OK6s_ow=r09ME&_^|Ax(jUB&0ivI#YWyx;zkd%;@qh zNOu%nrYN!*UCu#z6yWy+3?I^2h)h%el}KkJO*qU$`tt5Q8J0!X!L&} zKlR`B$n;DAtH*vY*Enn z*C4`p<30v>QA{M+xLyDiu2bR+uS ztq0$>BftCIIu7qqeIx%PqAV(-76t&|3mk95RyZh!kN7=Nd6B!89lE;T15Wk$QO~8kiHaY zFA?H#9iJ(b#)Y9<{@hy+!X+V1TND{~F@xu<9G`(b5lQJV4;u!0>xT&fef00OxYOL~ za!pnb4Rjm>cp*>e^V!JM>}x@#$xkZ&irag3le8z z-#@kcXzcLW&tp>;#h!@84g==0w-+yMSrlt(IXnC4{{7jp*wQtx4L;klDi+%t`QjG` zv-4u-XYTu8wRHZ~SZwx?2QPi&{5!E@r<#ZVq3q!IvE#NoV--t2Z;GAY?`V4N`|K-Y zvDX*G*7uN-VzJoyGd<@0Vsk8Z{M7HqD8GL{w(|J;<~hA%N-XyFOXvD%OP`J%|J9z@ z2T3oTi+y%_ckIt8FF&(1cJS_(#zgl5)#glTz=^uhd$FU>yxJ?aZ*45L>yLl8Z~o2} zi*4ETY5!+3Z$7@XsrNBY^Qpzhj>pbNttZxfdd2b2?yF4y{?yvzEgM4%5B)xO?Z()) z1=qhfdEcJqmd{q$_U#!Gt2%q=+{;CO8M5Wz+4FZb7w&yIwlubX44;?%eD?R&K-j>Bp zv+rGd@Z}Y=PMuMlJFjH5?1EiP52p*fWEGXNO1r7n~s!`BTUmx~I zB3dXq)>jjBhwAG6!HAEx9I03fl{eJ+gHdc=Vk;xULfnxIs~Yf?*P`5|<(0Za!6;fr zN^HzHTDE60FRopaj1T5UjR@bRArR5KCm*j!)g?n>G5^GDnD)OWmusw ztPa@<-Hah(__8&nj6l{zS&$;oz?%(bP5K%+MK zt$kU-v*wT)!6?hy+CLyGvQriv62Tx?A~3_V*`Em_0cVFK^PjzywBVc-RJ#@NA6#wCJzQR*>gfYH<}w2Oj+M!}n<@dCTVJ7if9wF^u1 zv7n6yt=rElK_ouxs02NMRz?XM3rUZ0fdgdBqN4+dpt!>=UdTgCZgsxxXawLq0W&YR z>C_x!sBTj)OwNaXp@0wMh~+-JQy8m)+Sz6Pe6aL z%e>>kY{&2rxkNgcw2RlVi(ISi5=7IYJ9MCi4`V*CyD3lcc>r$Ij+#oKgXG0CXe!bj zee1r10V0lk8;}=1Tu`r-re?n;9v3!_=d^>;3L{=4o`_eCC#Z@P`1p!AEH8MYq*irlhp56Q_qkDp3Phfd1|4nUuuc0lQDW1KEF> z>Jf}q@4ZdGu!5K;NGNJcv_cRSNFzLbQG@K5UL<5Mj)${-iyy%RYf0ic*(Cxa2h;1o zy*=-F6FEa{(}HRnBa`9pL9_@D;==_0K_Slvy@+}*Kh=Yz!)Vu{Z7=P5P(cZ(Aov6D z31nLKvawvq$EIeF^Hm#_IIa4BA&oLjv%%c?E`8XywG#Pt;?p)-l`KUC3u1 z2hpPw_1RpSo=yAujHVsrlior4#q;-3buO@p|4>>k85M7ur~dLlaG&Pv3@ z-hkszVDdBYaC^NpRe%U`(Tsp>0+z{)Kk&$-ghRLn-5 z1%Z?Y9xi1Fl-O!<>2KA0sy&ql}Av^0aYwOD?@*~c^7#=P)ZsvNgnX4mscSiL=U&1 z!Mb?hL1Q50k4%Ov>^>T=s+v`@`iVJ4UqiBLtB^XXG z=GLVmJZAU845$LiTppK8a>`Dxq988AyEUg<^h%-=`LOjctDAvY`+C&|FTi&@bae|Qv0^ST@^uS0fmP6am( zlh3&{w^J2(Adirn2kT^21M32e#{)WH8eQ;EICzJI2SRBsuc-10d9o7t)r-a|7*&!z zG(wkGa#8GnbEzPf{72wl5bypUX9MRUVHN1zmVLzd9eA-OcC=ElVHz(aAn zL3szSIy|ZesKU|;m?Cs2GI`-#9;fO8Dh^fVRgcSyQLBo82&*Q6oE*)i%dWT(rSp1K z_+t`mj*5jjuK}Fyk7-WTiF%@ne5c1Hpd3tso&nD-c~s8p)+7$R0AUc_f*rHuf_s2c z@H%`QqKAg5S2?f`hUswFHCgd^IY5KXMKu>D0C+py0v=>d@H#l~jvKQtdp!!UVSb@U zK`cwtB;Mn8cobDZm(BV+1pMnSD_`oCacYibnwL zH8Lv3MB(1G2hpkD!!hWUX?mNge~t1M|ii*pK~mcXMY zVty=%L>iC8;{1YF#xNb=e8ua8Tw(Co48RQ^4RbBaE?P&a0+^dz5Qiox8uYW>?QzTK zf$=)f!(@X+Nl8HLRX6V?nmW7^=jOpDU_Gc-nO7iyiUO7)*@q*n@eZ&*!4Tl<$ehdV zQ6-OpI5wCXWUvchFz5kU0GEL`JsdSum?*$Lz>`pAf%m!%d67X8(i7l6EZKpi3&C;- z4TR6(_JVP-#vpB?%HSx6S5v(%QbSG!^C-#84{~9KBoE9H#9D;m5x{~Hc#I?l<_Jtp zv;+P!!gTPG*tBA39Iy~oh0zp+5}X?kDL)9atf@}u6T$6*$`m+?; zkTfc2Pe?tlf!Uc}S7i}s>%tYOM~Xfx_KO!I9Lz-$Ce=nNrqB|tOPUwE2i_DE1!*7@Km$O& zT$mS7NpwIF3jz$a8+I0R2A*|-LowqjpnBmGF}a0(A!Xr&vw&%actgnG7ho{peNh2p zJIK(oB^B(L2B~y-Nk4(5pktX{$MivSL9n5#pmJOu4NA}htEfQH1Ck4t)($g8<_e?Z zH24Ny?C6qxhG6M?YtR}_w*-9)-T}2_4V(l%hBhIt(tsJJ8l?^w$UvG3;?03EogDZ> zVX{wp0q!Xb14Xmk@L)h6m_;n)AT1c6Q^8IlJY)VDzPjFiza($512F@Z)bc8m8?K~cugSdhM5EfU2gE3q-X#}*&Hzw zOeXjN$_T9L!M-PyCiJYt=0zp>f~|ozfXfAYi!Ep9Gt`qIXl|7mPmS3yP#ew=tPDo) zgbM+atH&OyLwy3-c91Cc3nPCbSQbg~}ALA7; zG29KZ0fZm?G#~>FjoE|ZQaDW@b%lONp(q-L>&03ILW})%OroxX5G94GQv?-`Bl$sq z2jd8D6^x4^!WSn|g@y8x29lg`BoOBlFo`6LOkT*ZaKR4o;Aq(a8^uFu!^(PLMX*<{ zDk^wHh6z(dcn47XXbSfLlLX##F&@LTfe{>#YtYMu1v7b5Ug%!P1C|L`lfpM7v+sof zL3S|XE(L=G!(!YX!)9Zr2MPe%5R#xk^x&8ShzP37kY{*ea5F#@C|igeWPvB`2$6sj zpm-U7V0Ra02*3%l2V=xi&%<~etc)F7w+c>H6}Zr_e@-tZ8{>pC?xE2^2@pL58U7}^ z1|MU61c!sPB)AR+-)mRBZWpvPaTu%*JO{`iZC+yvRV)qhh6BR`-Y%R*C{h)8^9qL- zA26M=3;vlM+<~Pi)+VS9hQ(}wlSRxv)_!n6h{JVW=U}{?sEiqrd8|I5+A%c9I$Sa^ zFwqn%R~UcD2#;CCZZ`BS^R-~MnLfovK4u7P3h^hO4~`Wi4=NO#0>|9Ld0?~PR>S$k z>HzEuQ2?u8j}zSE5%f5nM<#pBjI~n%55bit-Vw;if{*a#h9F?s4RUy(mM}*|L0N+( zCVjy4DeN^YKAB+HDd;j4E)6_EnmVW@Xa;bOOVBt7JL47RtAVQ__^|J?KDQpA3jZ34 zSEkNA@IOF2*egOAGoX>Bce@b)#48xE<6uw+G#%_W7BxJ>16!_SNgzkG?10(FQeK2o zf|pAxIud9B+J(3u9+V5wGCuoD8REtPysqY;-O057J!S~9uIsTMqw3j;P!do ztw85shZ!#|c)ZLuV21;&hv))y6qpfCEonI5D)1N$m>a7z4NDMcNEZ*I4+TJuBy^dbm=~)CFuw?mBa`v+f-NAx@EjpNSdTCmU@-Do zKv%>&Aky%Ipd-A{LBvBKj^c*Zh5b|E<-iq&8Nu=gm;f=DB;7V(G2sB~%TRcc!MUGl4&yZ25r22*e4#6D9*X!UbgvH7LQu(fu(jS0E!;Xo0#AFDNmreKA(BmmA6* zN(<2eEKV?2cmy^c4Q@P-C9%upR2dAcC|#ONV}v$@&IZ4G5Y_?8hyb`j2++|54u-wJ zni~odLnQG5zk3Bp5b*~PbMX`dgurV87GfX@yncA_6flO{j8FqYNiyaeUV#(-i-^z# z3Sr+d`HWA&ouE79488z-Q8WUsa2#PiXz_s}&=j#5EV0QL!u|1JJp^|TD@zDD+be*n zg%b;bgwWx24|D_wE;}utuw+L|$UBS{hDdsaR4JGMf<-uxZlSS2yuh*WC177g5FK6$ z#?Ej6OH=%ZVj&vFM+(MHY$`D&4gE*47J>n_fF-Dg1q~J+WMn}PEOfDcfi{8>hOIz{ z4n@L((Zg2#@UyVUfzg9G_Aq(zDjx7XaD;_Y;Xy!OVqXH09q@t213*}IK|q~sk%t8a zoN5m|DcCCxwvT5ppy5cJz_G!8rzC@?N&ky3m_HeOsKP#SSZ9j}J-{#2c&zu}MZ@xg zSa3s_9|VO31wOU%hOz-`K&66e5Da~N3m)LHEQ3WOhXslX z>lu+k7hrXA{1Kq^NE*aMa4R4iyf`2VkKf_O3IdBF#-|j*B&KshM=7u~u$oXIc6bw{ znh?*FV9l}cg-*s|40D1RSK$DvKm~lK!_Z)%z?Gl~<^;R}$!8iIs#Jp|hi->;f_??V zDp2ScDu#|151_iy2jdUKE)YBA>`544)1Ox6Eq!~^Q zbMC;X#QQA#qhYxN!y?kcjrdfCJf#SnSC^)wKm)n=p6FdIQUsQ4EPriZ+MbW-=G6r7%cxpqBBB2pbiLvMu;M~a5=K)@6UOKu{}>t;{y7*7@pp2*z+2EZAS%eELZgCLpp8^S9avAD3jlEx^@Xkyh;Pw7v_0t%79m6dc~r!1@P5G4vbL2o@i2 z1-0(+@oWPIv2|Q{kwL#`4@kd&v52(!-h~6tSeLQ=RC-~8woo0|bE15D7mwYsPZZBH zz|d|Z!J&B}SpPe4s6WK;84TKrqBMQj1=tB=GIN2T+XFvG;c554*h(Z^7@yLvmAkDNLgM$PQ%BX(>Hsa-a8=?zi9)bkkG4k112pW~qSaG?G zGK5`e|1dtrI7|g`KEjDX*6D%WP<@;dHlq{DB)5(s7@d01d4l)?AKL0;TTqx&!lU&W z8*7LgAbf2{3qcrq2_10A(gSiO-Ct0NuKuO~Zn0<6TF5utz{|9bib< zp~SiiKegbU*t;M<0m@XbLoo2(Hc2eKZx1(LaN+L6H*MsA94swj4NAdL8inzuK3YIW z(hv!ThxUUB7b7qJOqsaMcZtk7H=nVO&Mt=4D=7o5u`s|r_ zn6J;9&(tXonNhH*#%3yEZk--|EVR&1EK?kMK2I+oB=U*DN4%`(vw1P#nK%mec5O^q zAXo|fW7cOd@Y(@(s)-5M#pVYNF%h0dXUd6U;7i0$%qJMC&e5o>;CE)Nu!y1bF(|y9 zbeDmz+ZdxAd$u(8#IazNrH>TqF0l=Q1bRMpy%XuB8TAt zEMY8IHVmyJc&GE*Pz}pw#~adf=rs3Q0k&GEG2eBYF&HMAve}_v}Msft$f}8vTNfUeBS* zCJ&nY+Wwj9unV;>pohV;^olCr*%1}P`vX`mV4oBI2N_em-^1R=19JL5h6nBRQVYCe zM6fy2mxcJmF9SBi%dm&g9ZZ{FfCsgw_jnR@p#c+b^|$d?RgbP$2nP1h+myi;GM&@j zmu32o5F;C!pzjF=Y^1KIh~5ch@o^Z5_-ltI&5X4J8;g+_UvFR)ozUK}daXHVgP)hi z4>iUk`z>f`V_l%MJXGQDn`Eo6sI2paYpwIE{b9d#Q9VxcjD{Qhi>-?)eSrw>HI;tc zQM0@u9LBFyD{3Oqnn1uGuJ?s~b&-P-UZ^c=g5BvKkN;==M#*uaOhGZNnEJ zV68uj9}?3b`zDRfZq#$EjCC?oj&IqPVy&sjOevOf>^b#re|62cg<*fe!ra30g|+2M zMM`#7(&)yF#=3eUf)%_LjAS%srugWD?F^(T+m>QwRid?-Df&;r@ylo{@rhMV=hDiF zIbi!8v;vK!NakwH2T6OQJOl!t5BnbKB*i}R8j zqM0cv)KUC0MpjoJ?VqZ>*uCAM7^N=&)s+^*e4KFaAN4F`qd(9XB>dT36R$*O91a9*EZw z+GfiL9TjWC&+tPH;c~xc9`wwhj_4MNPVu+SYa0r53<}j-rxr6e8pFt#f?w_jtFr8) zZ3$1UQ_Je)geqz(7ihkyKMSnKr`erpoKR>U{6502WY~@N2{qfnV22&}4H%p=%`Sp> z3(Z4Q^xhNdv}jUmLgrB_qe(>C7Pof9noh#m4&#HBp)AR6<6x)o zWw+tP2pcAx+Q!Q}XqVOg9G2E53FB$)M$5u`(~j0k z4q*_&7(rrIyV@?CbP;OA3`hN59BUCFv+WC(`?Ca`ra>%E6!^hP?SOd$zDRZAX&G4{ zrVir5qV{cT!gcd~VSg61TDsZD%uA>n*IXII{JJ<*KxQwHw{$x%j<$7Ny+e4D{8hg4 z1>^Gz-C9kRKN8Kt$`Bj9c-f9NnDG9kUB>nky(Z?MJp-qj2>pV_9d^Nv3V{OglQL>W ztu;HL6;r?&=2;_P6omWXL*tb@2i`^yDVmQxGzVUn!J9eQ`~uA`XmSC#@j-oh0S=YZ zYicSo{ACsP3ZK)N<`W&2X`(2Wrzz!}FU{$z-~~V4th4)M6LdoJ3qb3quKNQ28yI)c z^aAkXz?h-%f&_6f^+;PKfn2j7P{WKl9M_1S3{GZ>X>sD#U$3oyk7kA|1CAZedC7|S z=J_i|!$E{GGxJ)JinlWw;iwlTHVJe^j1@nV!Bzz#jt=}7$jM_*2WOgK$A(@3(oEP1 zO@A0Z-1Q(axk84Cw-<8u3_E-x!8A-dnyl6j1Brwxqap_mMBx@p&`Nr_I~yU5ikGMSaS$YoNZQ}-@%b&5=8r7m)r zl<3sGi(H)|lUb>YTqY$tb?+iqr^sYh>LQm(iB8?S$kiz_nU%W8Wm2M3_bzgEicDsu zE^?Wa=+wQ7T%975S*eR$CM7y`?;=;H$YfUPB9}>tPTjl6)hRNWmAc4fQleA$E^>8> zOlGAna+#Fq)V+&bog$N2sf%1DB|3HQB3Gx#WLD}Tmr03E-Mh%uDKeRry2xcxqEq)S za&?MKW~DB2nUv_%y^CC(B9mFEi(DopI(6?NSEtBiR_Y>`Nr_I~yU5ikGMSaS$YoNZ zQ}-VwSHgKb{vZzFnUB+UaC{Ha>`*8Ds2-eo^kYXI@m6-I5*TjM7jvEz(v z1r25TaY!XOlUPptWG5VQgubk{{G!6v^O1hEH}e=1<7lGxO|hEU#*{n0 z6?%Md`iWIl^aC24xa7$%u(EXDB+;HQBAlvJ%6^n)jVy>n{dM{QQhE-ZJH-w_vXw^s z(W`e> z()LySjsEh!NnFPTR{vaM>_MDs5yj7rtqf4#q(wgf%4$!Stbyp_AF1&}eN|B_{p2vC z(;DMf-PzW7_xel({PX+)z74f7zN!Xa6;4A!k+n7$njZuXZF6~?9oGKnFrzEhO!(Ol zg&%}yvK-@1>o^>)h3PeT&lV2Nk60bD(RX<$5V7K%6#Y@=t)erX7p;+UUjS#Q8DqsM zUpCD@ucq8T7A)8|30#AAp)fnV%xcW4&bRsIiDgjd-*~uUUbjX>tAxKA${b+4h zAbLRq{j@Y&eN8z|2;;0A2o*objARGnsb{B$T~HME&8LIKDsXI?by2l1qWMW8y>w8S zow#d}t)ex@z>6JYW^)y|$B)Nfa$&_*b#8Eqa3~k#c^zCmfueP>5 z18uP}KNwo;7_6nyP<=iQk;6fBQ3Ki{tIfN#P)6<9*=q>|0*_yE$tBiB)>2ep z%rS+Sfk7iUZF1W_#bGiKfP$-{q25{=@K;6&rBa%phBC~JHIeQ(?ye>X>do^7ta|sX zsTD}jNpFeB87KGQfIUMHO&Ty=$m6g&CU}c%x^85r=`q?aTEt94-=yN23Y=y)&R|bj-Ah_ zpCLJPNfuz?1V_jxD>hCiG^$ZnE2k_5BLRZR5%F^<+v)>D)7s)vjR|a3Q;exw&pqT4 zT_22xu`qLhm|8)O&hk2c&`F6lgZp(hVL7ak&iB?CtS0d$s{qqxBxf1=<^riPJ2G_P z46vqIH$j9Jnd;dbmcf3B+8(k6&o5Q=SJed7nxHSdAj8mM`1{`YQx>g));B4;rpTT? z#4|2=+@Q&MHEGFZw-inv?3hqrHhkE)sm^5lWkYwy>_J*?^5CiCYm<|29g&-yoP5jh z-=qpvpH1e=o9n;b6O`U_^3qYGLlgiF5sv zlLt)~Cnb*_ot|bN=XQnx$1dSZ8Id$;)|mVNlF3}k4A0Q2!Akn9%t;m8+_CwEc^QE* z6Xj~(l%jFIwB(do)9aFxYvznANE;G~puyxZQejnUUDD9P84b7QIYUlcYGq&=cT2dq zeCjP@YDVTy$ex&6oRT_g_@HEM?3`)ISvj_n5sH?VQjxAk%97Kk->Oi@zNBGeG7D$r zIU@lsH(Y3&kn9%g)LUGGWo=BpJZskA@;Uj*>X6bZ`91; z`2?XdDJ?ZWTvh7KmdB-5@wFL_ne|g?AgffS7NKtKRd5|wnPL@3N z%J8zR~Q232TKI^kfAe@lZ(eWlZGbOjjFB6cQ=T2H8q1u zxzb59r;1{ly;=@WaHS;=PaQvIP>yR_a$ZUL2rhZFRyI+ZoLhP8$WYp3Avqa;TOlbo z+f_a>oTE%0qfIXi4IMnLWXPOUF~U!n8k{j_cJiQtT7T-WVHr_5CsmjN9Bd=fWSinD zsIDpV;xRcr$uW2GnCZd1tYY`@hH>sugD1Fh8p0!P8G6gesX2M&$-_nrohb? zv$N4j%22+evcAj(8P&Jzm=9i7H=0{8n4EGEwm{OMKDe}}fMwAuRdy;v(+lRkY zHh7jW(dR9gJxd&ul9V-hPNV^+d5P0A3gnrSQ;Jfxxy9LYCb*JQikzMln`3a+s7X0v zCe{qf%?#cmltn9))ag^gzM<1|bMWDU+AKU*6*dvq`|Ze&Kr$mEiVp5YMhX_?8lM3ve( zl4n@%82m-3j4@^2S-y(olzb_Bj;$`Ua!BqdZ}FhX)2A$<6(=1mtZSXVNmXH-jz>qg z($X+hcMPnBVIMq0_{m0LLN#OY7B85wC}Hgf1xZK#GVPVHF8#3uZF@hM&}g?pO*kBw N&d=2kJ?@+IzW}D#8wUUY literal 0 HcmV?d00001 diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 1790f4f77..766c50649 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -146,6 +146,11 @@ def test_bytesio_object(): assert_image_similar(img, image1_scale1_compare, 5) +def test_1_mode(): + with Image.open("Tests/images/1.eps") as im: + assert im.mode == "1" + + def test_image_mode_not_supported(tmp_path): im = hopper("RGBA") tmpfile = str(tmp_path / "temp.eps") diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 3b782d6b3..0e434c5c0 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -288,11 +288,14 @@ class EpsImageFile(ImageFile.ImageFile): # Encoded bitmapped image. x, y, bi, mo = s[11:].split(None, 7)[:4] - if int(bi) != 8: - break - try: - self.mode = self.mode_map[int(mo)] - except ValueError: + if int(bi) == 1: + self.mode = "1" + elif int(bi) == 8: + try: + self.mode = self.mode_map[int(mo)] + except ValueError: + break + else: break self._size = int(x), int(y) From 99e401123bab56bf9c64314b506750a4ea6a9e79 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 19:46:07 +1000 Subject: [PATCH 047/100] Corrected palette size when saving --- Tests/test_file_tga.py | 12 ++++++++++++ src/PIL/TgaImagePlugin.py | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 0c8c9f304..fff127421 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -123,6 +123,18 @@ def test_save(tmp_path): assert test_im.size == (100, 100) +def test_small_palette(tmp_path): + im = Image.new("P", (1, 1)) + colors = [0, 0, 0] + im.putpalette(colors) + + out = str(tmp_path / "temp.tga") + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.getpalette() == colors + + def test_save_wrong_mode(tmp_path): im = hopper("PA") out = str(tmp_path / "temp.tga") diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 59b89e988..7f5075317 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -193,7 +193,8 @@ def _save(im, fp, filename): warnings.warn("id_section has been trimmed to 255 characters") if colormaptype: - colormapfirst, colormaplength, colormapentry = 0, 256, 24 + palette = im.im.getpalette("RGB", "BGR") + colormapfirst, colormaplength, colormapentry = 0, len(palette) // 3, 24 else: colormapfirst, colormaplength, colormapentry = 0, 0, 0 @@ -225,7 +226,7 @@ def _save(im, fp, filename): fp.write(id_section) if colormaptype: - fp.write(im.im.getpalette("RGB", "BGR")) + fp.write(palette) if rle: ImageFile._save( From 5d4fbdfab4fa5dc05f4b3de3304fffdeddb9ff4f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 19:46:46 +1000 Subject: [PATCH 048/100] Simplified code --- src/PIL/TgaImagePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 7f5075317..cd454b755 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -194,9 +194,9 @@ def _save(im, fp, filename): if colormaptype: palette = im.im.getpalette("RGB", "BGR") - colormapfirst, colormaplength, colormapentry = 0, len(palette) // 3, 24 + colormaplength, colormapentry = len(palette) // 3, 24 else: - colormapfirst, colormaplength, colormapentry = 0, 0, 0 + colormaplength, colormapentry = 0, 0 if im.mode in ("LA", "RGBA"): flags = 8 @@ -211,7 +211,7 @@ def _save(im, fp, filename): o8(id_len) + o8(colormaptype) + o8(imagetype) - + o16(colormapfirst) + + o16(0) # colormapfirst + o16(colormaplength) + o8(colormapentry) + o16(0) From 55d94558fbaa809c0cc03c072bf7119fb2b27e78 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 23:14:32 +1000 Subject: [PATCH 049/100] Do not install test-image-results on GitHub Actions --- .ci/install.sh | 1 - .github/workflows/macos-install.sh | 1 - 2 files changed, 2 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 16a056dd5..7ead209be 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -35,7 +35,6 @@ python3 -m pip install -U pytest python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -python3 -m pip install test-image-results if [[ $(uname) != CYGWIN* ]]; then # TODO Remove condition when NumPy supports 3.11 diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 06b829645..bb0bcd680 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -12,7 +12,6 @@ python3 -m pip install -U pytest python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -python3 -m pip install test-image-results echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg # TODO Remove condition when NumPy supports 3.11 From a37593f004247ebf69d5582524da6dc5143cb023 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 14 Aug 2022 14:34:42 +1000 Subject: [PATCH 050/100] Allow RGB and RGBA values for PA image putpixel --- Tests/test_image_access.py | 22 ++++++++++++++-------- docs/reference/PixelAccess.rst | 2 +- src/PIL/Image.py | 11 ++++++++--- src/PIL/PyAccess.py | 11 ++++++++--- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 617274a57..58e784753 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -212,11 +212,14 @@ class TestImageGetPixel(AccessTest): self.check(mode, 2**15 + 1) self.check(mode, 2**16 - 1) - def test_p_putpixel_rgb_rgba(self): - for color in [(255, 0, 0), (255, 0, 0, 255)]: - im = Image.new("P", (1, 1), 0) + @pytest.mark.parametrize("mode", ("P", "PA")) + def test_p_putpixel_rgb_rgba(self, mode): + for color in [(255, 0, 0), (255, 0, 0, 127)]: + im = Image.new(mode, (1, 1)) im.putpixel((0, 0), color) - assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) + + alpha = color[3] if len(color) == 4 and mode == "PA" else 255 + assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) @pytest.mark.skipif(cffi is None, reason="No CFFI") @@ -337,12 +340,15 @@ class TestCffi(AccessTest): # pixels can contain garbage if image is released assert px[i, 0] == 0 - def test_p_putpixel_rgb_rgba(self): - for color in [(255, 0, 0), (255, 0, 0, 255)]: - im = Image.new("P", (1, 1), 0) + @pytest.mark.parametrize("mode", ("P", "PA")) + def test_p_putpixel_rgb_rgba(self, mode): + for color in [(255, 0, 0), (255, 0, 0, 127)]: + im = Image.new(mode, (1, 1)) access = PyAccess.new(im, False) access.putpixel((0, 0), color) - assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) + + alpha = color[3] if len(color) == 4 and mode == "PA" else 255 + assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) class TestImagePutPixelError(AccessTest): diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index d2e80fb8c..b234b7b4e 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -73,7 +73,7 @@ Access using negative indexes is also possible. Modifies the pixel at x,y. The color is given as a single numerical value for single band images, and a tuple for multi-band images. In addition to this, RGB and RGBA tuples - are accepted for P images. + are accepted for P and PA images. :param xy: The pixel coordinate, given as (x, y). :param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4eb2dead6..f3f158db8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1839,7 +1839,7 @@ class Image: Modifies the pixel at the given position. The color is given as a single numerical value for single-band images, and a tuple for multi-band images. In addition to this, RGB and RGBA tuples are - accepted for P images. + accepted for P and PA images. Note that this method is relatively slow. For more extensive changes, use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw` @@ -1864,12 +1864,17 @@ class Image: return self.pyaccess.putpixel(xy, value) if ( - self.mode == "P" + self.mode in ("P", "PA") and isinstance(value, (list, tuple)) and len(value) in [3, 4] ): - # RGB or RGBA value for a P image + # RGB or RGBA value for a P or PA image + if self.mode == "PA": + alpha = value[3] if len(value) == 4 else 255 + value = value[:3] value = self.palette.getcolor(value, self) + if self.mode == "PA": + value = (value, alpha) return self.im.putpixel(xy, value) def remap_palette(self, dest_map, source_palette=None): diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 2a48c53f7..9a2ec48fc 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -58,7 +58,7 @@ class PyAccess: # Keep pointer to im object to prevent dereferencing. self._im = img.im - if self._im.mode == "P": + if self._im.mode in ("P", "PA"): self._palette = img.palette # Debugging is polluting test traces, only useful here @@ -89,12 +89,17 @@ class PyAccess: (x, y) = self.check_xy((x, y)) if ( - self._im.mode == "P" + self._im.mode in ("P", "PA") and isinstance(color, (list, tuple)) and len(color) in [3, 4] ): - # RGB or RGBA value for a P image + # RGB or RGBA value for a P or PA image + if self._im.mode == "PA": + alpha = color[3] if len(color) == 4 else 255 + color = color[:3] color = self._palette.getcolor(color, self._img) + if self._im.mode == "PA": + color = (color, alpha) return self.set_pixel(x, y, color) From 520fa19dab4b60d732d273aab8bff195ce5875cf Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 15 Aug 2022 09:15:35 +1000 Subject: [PATCH 051/100] Fixed formatting Co-authored-by: Hugo van Kemenade --- docs/releasenotes/9.3.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index a8db4edd6..7109a09f2 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -31,7 +31,7 @@ Allow default ImageDraw font to be set Rather than specifying a font when calling text-related ImageDraw methods, or setting a font on each ImageDraw instance, the default font can now be set for -all future ImageDraw operations. +all future ImageDraw operations:: from PIL import ImageDraw, ImageFont ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") From b84816c02f84bb42f440387366e391fa2ed79020 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Aug 2022 22:45:55 +1000 Subject: [PATCH 052/100] Added pa2p --- src/libImaging/Convert.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 5dc17db60..f0d42f7ff 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1026,6 +1026,14 @@ pa2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { } } +static void +pa2p(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = in[0]; + } +} + static void p2pa(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; @@ -1209,6 +1217,8 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { convert = alpha ? pa2l : p2l; } else if (strcmp(mode, "LA") == 0) { convert = alpha ? pa2la : p2la; + } else if (strcmp(mode, "P") == 0) { + convert = pa2p; } else if (strcmp(mode, "PA") == 0) { convert = p2pa; } else if (strcmp(mode, "I") == 0) { @@ -1233,6 +1243,10 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { if (!imOut) { return NULL; } + if (strcmp(mode, "P") == 0) { + ImagingPaletteDelete(imOut->palette); + imOut->palette = ImagingPaletteDuplicate(imIn->palette); + } ImagingSectionEnter(&cookie); for (y = 0; y < imIn->ysize; y++) { From 8a60db322fb1ea752717bba94d248f9f08c38815 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 17:35:38 +1000 Subject: [PATCH 053/100] Copy palette when converting from P to PA --- Tests/test_image_convert.py | 6 ++++++ src/libImaging/Convert.c | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index e5639e105..59e205084 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -236,6 +236,12 @@ def test_p2pa_alpha(): assert im_a.getpixel((x, y)) == alpha +def test_p2pa_palette(): + with Image.open("Tests/images/tiny.png") as im: + im_pa = im.convert("PA") + assert im_pa.getpalette() == im.getpalette() + + def test_matrix_illegal_conversion(): # Arrange im = hopper("CMYK") diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index f0d42f7ff..bdc680be4 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1243,7 +1243,7 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { if (!imOut) { return NULL; } - if (strcmp(mode, "P") == 0) { + if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) { ImagingPaletteDelete(imOut->palette); imOut->palette = ImagingPaletteDuplicate(imIn->palette); } From 6b35dc2a8ab238145460af37096c9b53a301a235 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Aug 2022 19:17:41 +1000 Subject: [PATCH 054/100] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5f99d9d25..fb634eaba 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Allow default ImageDraw font to be set #6484 + [radarhere, hugovk] + - Save 1 mode PDF using CCITTFaxDecode filter #6470 [radarhere] From c463ef4fe370667f1db595a03a28516467f4c07d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Aug 2022 21:13:09 +1000 Subject: [PATCH 055/100] Fallback to not using mmap if buffer is not large enough --- Tests/images/mmap_error.bmp | Bin 0 -> 9253 bytes Tests/test_file_bmp.py | 7 +++++++ src/PIL/ImageFile.py | 3 +++ 3 files changed, 10 insertions(+) create mode 100644 Tests/images/mmap_error.bmp diff --git a/Tests/images/mmap_error.bmp b/Tests/images/mmap_error.bmp new file mode 100644 index 0000000000000000000000000000000000000000..04df163d7fed0433ac4dadaf0d0e5a42ca1c28bb GIT binary patch literal 9253 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 S<(E}?03>_8_`3k(y!9VXh#-Uj literal 0 HcmV?d00001 diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index d58666b44..604d54d88 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -39,6 +39,13 @@ def test_invalid_file(): BmpImagePlugin.BmpImageFile(fp) +def test_fallback_if_mmap_errors(): + # This image has been truncated, + # so that the buffer is not large enough when using mmap + with Image.open("Tests/images/mmap_error.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp") + + def test_save_to_bytes(): output = io.BytesIO() im = hopper() diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 9f08493c1..f281b9e14 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -192,6 +192,9 @@ class ImageFile(Image.Image): with open(self.filename) as fp: self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) + if offset + self.size[1] * args[1] > self.map.size(): + # buffer is not large enough + raise OSError self.im = Image.core.map_buffer( self.map, self.size, decoder_name, offset, args ) From f9d33b40ad0d9a3cea4be3aa2fa65b3beb477e2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 19 Aug 2022 12:09:47 +1000 Subject: [PATCH 056/100] Ubuntu dependencies also apply to Jammy [ci skip] --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index f147fa6a7..42cd7df9d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -367,7 +367,7 @@ In Alpine, the command is:: .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. -Prerequisites for **Ubuntu 16.04 LTS - 20.04 LTS** are installed with:: +Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ From 8a1837c80d8bfb616ef5d37be11522da701d5104 Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Sat, 20 Aug 2022 19:39:04 -0700 Subject: [PATCH 057/100] DOC: fix image-file-formats.rst --- 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 1728c8e05..7db7b117a 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -968,7 +968,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum methods are: :data:`None`, ``"group3"``, ``"group4"``, ``"jpeg"``, ``"lzma"``, ``"packbits"``, ``"tiff_adobe_deflate"``, ``"tiff_ccitt"``, ``"tiff_lzw"``, ``"tiff_raw_16"``, ``"tiff_sgilog"``, ``"tiff_sgilog24"``, ``"tiff_thunderscan"``, - ``"webp"`, ``"zstd"`` + ``"webp"``, ``"zstd"`` **quality** The image quality for JPEG compression, on a scale from 0 (worst) to 100 From 3b4ea7c60275d5912c2954de00e444df4a841149 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Aug 2022 19:57:33 +1000 Subject: [PATCH 058/100] Do not use CCITTFaxDecode filter if libtiff is not available --- Tests/test_file_pdf.py | 4 ++-- src/PIL/PdfImagePlugin.py | 33 ++++++++++++++++++--------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 310619fb2..b27dbeedd 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -6,7 +6,7 @@ import time import pytest -from PIL import Image, PdfParser +from PIL import Image, PdfParser, features from .helper import hopper, mark_if_feature_version @@ -43,7 +43,7 @@ def test_monochrome(tmp_path): # Act / Assert outfile = helper_save_as_pdf(tmp_path, mode) - assert os.path.getsize(outfile) < 5000 + assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000) def test_greyscale(tmp_path): diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 181a05b8d..404759a7f 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -25,7 +25,7 @@ import math import os import time -from . import Image, ImageFile, ImageSequence, PdfParser, __version__ +from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features # # -------------------------------------------------------------------- @@ -130,20 +130,23 @@ def _save(im, fp, filename, save_all=False): width, height = im.size if im.mode == "1": - filter = "CCITTFaxDecode" - bits = 1 - params = PdfParser.PdfArray( - [ - PdfParser.PdfDict( - { - "K": -1, - "BlackIs1": True, - "Columns": width, - "Rows": height, - } - ) - ] - ) + if features.check("libtiff"): + filter = "CCITTFaxDecode" + bits = 1 + params = PdfParser.PdfArray( + [ + PdfParser.PdfDict( + { + "K": -1, + "BlackIs1": True, + "Columns": width, + "Rows": height, + } + ) + ] + ) + else: + filter = "DCTDecode" colorspace = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "L": From fd47eed73a7aa178848f280f09435b55bbaefd69 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Aug 2022 09:23:42 -0500 Subject: [PATCH 059/100] parametrize Tests/test_image_paste.py --- Tests/test_image_paste.py | 440 +++++++++++++++++++++----------------- 1 file changed, 243 insertions(+), 197 deletions(-) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 4ea1d73ce..bb01ff110 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -1,3 +1,5 @@ +import pytest + from PIL import Image from .helper import CachedProperty, assert_image_equal @@ -101,226 +103,270 @@ class TestImagingPaste: ], ) - def test_image_solid(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "red") - im2 = getattr(self, "gradient_" + mode) + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_image_solid(self, mode): + im = Image.new(mode, (200, 200), "red") + im2 = getattr(self, "gradient_" + mode) - im.paste(im2, (12, 23)) + im.paste(im2, (12, 23)) - im = im.crop((12, 23, im2.width + 12, im2.height + 23)) - assert_image_equal(im, im2) + im = im.crop((12, 23, im2.width + 12, im2.height + 23)) + assert_image_equal(im, im2) - def test_image_mask_1(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_image_mask_1(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) - self.assert_9points_paste( - im, - im2, - self.mask_1, - [ - (255, 255, 255, 255), - (255, 255, 255, 255), - (127, 254, 127, 0), - (255, 255, 255, 255), - (255, 255, 255, 255), - (191, 190, 63, 64), - (127, 0, 127, 254), - (191, 64, 63, 190), - (255, 255, 255, 255), - ], - ) + self.assert_9points_paste( + im, + im2, + self.mask_1, + [ + (255, 255, 255, 255), + (255, 255, 255, 255), + (127, 254, 127, 0), + (255, 255, 255, 255), + (255, 255, 255, 255), + (191, 190, 63, 64), + (127, 0, 127, 254), + (191, 64, 63, 190), + (255, 255, 255, 255), + ], + ) - def test_image_mask_L(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_image_mask_L(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) - self.assert_9points_paste( - im, - im2, - self.mask_L, - [ - (128, 191, 255, 191), - (208, 239, 239, 208), - (255, 255, 255, 255), - (112, 111, 206, 207), - (192, 191, 191, 191), - (239, 239, 207, 207), - (128, 1, 128, 254), - (207, 113, 112, 207), - (255, 191, 128, 191), - ], - ) + self.assert_9points_paste( + im, + im2, + self.mask_L, + [ + (128, 191, 255, 191), + (208, 239, 239, 208), + (255, 255, 255, 255), + (112, 111, 206, 207), + (192, 191, 191, 191), + (239, 239, 207, 207), + (128, 1, 128, 254), + (207, 113, 112, 207), + (255, 191, 128, 191), + ], + ) - def test_image_mask_LA(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_image_mask_LA(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) - self.assert_9points_paste( - im, - im2, - self.gradient_LA, - [ - (128, 191, 255, 191), - (112, 207, 206, 111), - (128, 254, 128, 1), - (208, 208, 239, 239), - (192, 191, 191, 191), - (207, 207, 112, 113), - (255, 255, 255, 255), - (239, 207, 207, 239), - (255, 191, 128, 191), - ], - ) + self.assert_9points_paste( + im, + im2, + self.gradient_LA, + [ + (128, 191, 255, 191), + (112, 207, 206, 111), + (128, 254, 128, 1), + (208, 208, 239, 239), + (192, 191, 191, 191), + (207, 207, 112, 113), + (255, 255, 255, 255), + (239, 207, 207, 239), + (255, 191, 128, 191), + ], + ) - def test_image_mask_RGBA(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_image_mask_RGBA(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) - self.assert_9points_paste( - im, - im2, - self.gradient_RGBA, - [ - (128, 191, 255, 191), - (208, 239, 239, 208), - (255, 255, 255, 255), - (112, 111, 206, 207), - (192, 191, 191, 191), - (239, 239, 207, 207), - (128, 1, 128, 254), - (207, 113, 112, 207), - (255, 191, 128, 191), - ], - ) + self.assert_9points_paste( + im, + im2, + self.gradient_RGBA, + [ + (128, 191, 255, 191), + (208, 239, 239, 208), + (255, 255, 255, 255), + (112, 111, 206, 207), + (192, 191, 191, 191), + (239, 239, 207, 207), + (128, 1, 128, 254), + (207, 113, 112, 207), + (255, 191, 128, 191), + ], + ) - def test_image_mask_RGBa(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_image_mask_RGBa(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) - self.assert_9points_paste( - im, - im2, - self.gradient_RGBa, - [ - (128, 255, 126, 255), - (0, 127, 126, 255), - (126, 253, 126, 255), - (128, 127, 254, 255), - (0, 255, 254, 255), - (126, 125, 254, 255), - (128, 1, 128, 255), - (0, 129, 128, 255), - (126, 255, 128, 255), - ], - ) + self.assert_9points_paste( + im, + im2, + self.gradient_RGBa, + [ + (128, 255, 126, 255), + (0, 127, 126, 255), + (126, 253, 126, 255), + (128, 127, 254, 255), + (0, 255, 254, 255), + (126, 125, 254, 255), + (128, 1, 128, 255), + (0, 129, 128, 255), + (126, 255, 128, 255), + ], + ) - def test_color_solid(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "black") + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_color_solid(self, mode): + im = Image.new(mode, (200, 200), "black") - rect = (12, 23, 128 + 12, 128 + 23) - im.paste("white", rect) + rect = (12, 23, 128 + 12, 128 + 23) + im.paste("white", rect) - hist = im.crop(rect).histogram() - while hist: - head, hist = hist[:256], hist[256:] - assert head[255] == 128 * 128 - assert sum(head[:255]) == 0 + hist = im.crop(rect).histogram() + while hist: + head, hist = hist[:256], hist[256:] + assert head[255] == 128 * 128 + assert sum(head[:255]) == 0 - def test_color_mask_1(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) - color = (10, 20, 30, 40)[: len(mode)] + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_color_mask_1(self, mode): + im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) + color = (10, 20, 30, 40)[: len(mode)] - self.assert_9points_paste( - im, - color, - self.mask_1, - [ - (50, 60, 70, 80), - (50, 60, 70, 80), - (10, 20, 30, 40), - (50, 60, 70, 80), - (50, 60, 70, 80), - (10, 20, 30, 40), - (10, 20, 30, 40), - (10, 20, 30, 40), - (50, 60, 70, 80), - ], - ) + self.assert_9points_paste( + im, + color, + self.mask_1, + [ + (50, 60, 70, 80), + (50, 60, 70, 80), + (10, 20, 30, 40), + (50, 60, 70, 80), + (50, 60, 70, 80), + (10, 20, 30, 40), + (10, 20, 30, 40), + (10, 20, 30, 40), + (50, 60, 70, 80), + ], + ) - def test_color_mask_L(self): - for mode in ("RGBA", "RGB", "L"): - im = getattr(self, "gradient_" + mode).copy() - color = "white" + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_color_mask_L(self, mode): + im = getattr(self, "gradient_" + mode).copy() + color = "white" - self.assert_9points_paste( - im, - color, - self.mask_L, - [ - (127, 191, 254, 191), - (111, 207, 206, 110), - (127, 254, 127, 0), - (207, 207, 239, 239), - (191, 191, 190, 191), - (207, 206, 111, 112), - (254, 254, 254, 255), - (239, 206, 206, 238), - (254, 191, 127, 191), - ], - ) + self.assert_9points_paste( + im, + color, + self.mask_L, + [ + (127, 191, 254, 191), + (111, 207, 206, 110), + (127, 254, 127, 0), + (207, 207, 239, 239), + (191, 191, 190, 191), + (207, 206, 111, 112), + (254, 254, 254, 255), + (239, 206, 206, 238), + (254, 191, 127, 191), + ], + ) - def test_color_mask_RGBA(self): - for mode in ("RGBA", "RGB", "L"): - im = getattr(self, "gradient_" + mode).copy() - color = "white" + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_color_mask_RGBA(self, mode): + im = getattr(self, "gradient_" + mode).copy() + color = "white" - self.assert_9points_paste( - im, - color, - self.gradient_RGBA, - [ - (127, 191, 254, 191), - (111, 207, 206, 110), - (127, 254, 127, 0), - (207, 207, 239, 239), - (191, 191, 190, 191), - (207, 206, 111, 112), - (254, 254, 254, 255), - (239, 206, 206, 238), - (254, 191, 127, 191), - ], - ) + self.assert_9points_paste( + im, + color, + self.gradient_RGBA, + [ + (127, 191, 254, 191), + (111, 207, 206, 110), + (127, 254, 127, 0), + (207, 207, 239, 239), + (191, 191, 190, 191), + (207, 206, 111, 112), + (254, 254, 254, 255), + (239, 206, 206, 238), + (254, 191, 127, 191), + ], + ) - def test_color_mask_RGBa(self): - for mode in ("RGBA", "RGB", "L"): - im = getattr(self, "gradient_" + mode).copy() - color = "white" + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_color_mask_RGBa(self, mode): + im = getattr(self, "gradient_" + mode).copy() + color = "white" - self.assert_9points_paste( - im, - color, - self.gradient_RGBa, - [ - (255, 63, 126, 63), - (47, 143, 142, 46), - (126, 253, 126, 255), - (15, 15, 47, 47), - (63, 63, 62, 63), - (142, 141, 46, 47), - (255, 255, 255, 0), - (48, 15, 15, 47), - (126, 63, 255, 63), - ], - ) + self.assert_9points_paste( + im, + color, + self.gradient_RGBa, + [ + (255, 63, 126, 63), + (47, 143, 142, 46), + (126, 253, 126, 255), + (15, 15, 47, 47), + (63, 63, 62, 63), + (142, 141, 46, 47), + (255, 255, 255, 0), + (48, 15, 15, 47), + (126, 63, 255, 63), + ], + ) def test_different_sizes(self): im = Image.new("RGB", (100, 100)) From 1421f94b6de11800a5b6ecc4ef43e6eaeb039dc8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Aug 2022 14:25:29 +0000 Subject: [PATCH 060/100] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_image_paste.py | 143 +++++++++++++++++++++++--------------- 1 file changed, 88 insertions(+), 55 deletions(-) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index bb01ff110..0b40ba671 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -103,11 +103,14 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_image_solid(self, mode): im = Image.new(mode, (200, 200), "red") im2 = getattr(self, "gradient_" + mode) @@ -117,11 +120,14 @@ class TestImagingPaste: im = im.crop((12, 23, im2.width + 12, im2.height + 23)) assert_image_equal(im, im2) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_image_mask_1(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -143,11 +149,14 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_image_mask_L(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -169,11 +178,14 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_image_mask_LA(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -195,11 +207,14 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_image_mask_RGBA(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -221,11 +236,14 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_image_mask_RGBa(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -247,11 +265,14 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_color_solid(self, mode): im = Image.new(mode, (200, 200), "black") @@ -264,11 +285,14 @@ class TestImagingPaste: assert head[255] == 128 * 128 assert sum(head[:255]) == 0 - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_color_mask_1(self, mode): im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) color = (10, 20, 30, 40)[: len(mode)] @@ -290,11 +314,14 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_color_mask_L(self, mode): im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -316,11 +343,14 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_color_mask_RGBA(self, mode): im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -342,11 +372,14 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_color_mask_RGBa(self, mode): im = getattr(self, "gradient_" + mode).copy() color = "white" From b236c61c04c0f6a6cc1ac24f5a56e327e890ad9c Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Aug 2022 13:29:26 -0500 Subject: [PATCH 061/100] make @pytest.mark.parametrize annotations one line --- Tests/test_image_paste.py | 99 +++++---------------------------------- 1 file changed, 11 insertions(+), 88 deletions(-) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 0b40ba671..1ab02017d 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -103,14 +103,7 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_image_solid(self, mode): im = Image.new(mode, (200, 200), "red") im2 = getattr(self, "gradient_" + mode) @@ -120,14 +113,7 @@ class TestImagingPaste: im = im.crop((12, 23, im2.width + 12, im2.height + 23)) assert_image_equal(im, im2) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_image_mask_1(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -149,14 +135,7 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_image_mask_L(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -178,14 +157,7 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_image_mask_LA(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -207,14 +179,7 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_image_mask_RGBA(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -236,14 +201,7 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_image_mask_RGBa(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -265,14 +223,7 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_color_solid(self, mode): im = Image.new(mode, (200, 200), "black") @@ -285,14 +236,7 @@ class TestImagingPaste: assert head[255] == 128 * 128 assert sum(head[:255]) == 0 - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_color_mask_1(self, mode): im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) color = (10, 20, 30, 40)[: len(mode)] @@ -314,14 +258,7 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_color_mask_L(self, mode): im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -343,14 +280,7 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_color_mask_RGBA(self, mode): im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -372,14 +302,7 @@ class TestImagingPaste: ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_color_mask_RGBa(self, mode): im = getattr(self, "gradient_" + mode).copy() color = "white" From b6b42b8e569ad42686f5522c7e4228fbf68101fd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Aug 2022 07:41:12 +1000 Subject: [PATCH 062/100] Updated libimagequant to 4.0.2 --- 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 9b3088b94..76f4cb95f 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.0.1 +archive=libimagequant-4.0.2 ./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 42cd7df9d..a8cd5e441 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -166,7 +166,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.0.1** + * Pillow has been tested with libimagequant **2.6-4.0.2** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From a3e61c1f89ea726d011683486ce81d6c448a2374 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Aug 2022 09:16:40 +1000 Subject: [PATCH 063/100] Temporarily skip valgrind failure --- Tests/test_file_pdf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 310619fb2..df0b7abe6 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -37,6 +37,7 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs): return outfile +@pytest.mark.valgrind_known_error(reason="Temporary skip") def test_monochrome(tmp_path): # Arrange mode = "1" From 0ed03d4a58d5f31d570fc9fc391298ce032ad7ce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Aug 2022 21:41:32 +1000 Subject: [PATCH 064/100] Parametrize tests --- Tests/test_file_apng.py | 18 ++- Tests/test_file_container.py | 128 ++++++++-------- Tests/test_file_im.py | 15 +- Tests/test_file_libtiff.py | 74 ++++----- Tests/test_file_mpo.py | 187 +++++++++++----------- Tests/test_file_tga.py | 75 +++++---- Tests/test_file_wmf.py | 10 +- Tests/test_image.py | 81 +++++----- Tests/test_image_access.py | 33 ++-- Tests/test_image_convert.py | 53 +++---- Tests/test_image_copy.py | 47 +++--- Tests/test_image_crop.py | 17 +- Tests/test_image_resample.py | 264 ++++++++++++++++---------------- Tests/test_image_resize.py | 27 ++-- Tests/test_image_rotate.py | 34 ++-- Tests/test_image_transpose.py | 243 ++++++++++++++--------------- Tests/test_imagedraw.py | 20 +-- Tests/test_qt_image_toqimage.py | 48 +++--- 18 files changed, 672 insertions(+), 702 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index ad61a07cc..d624bbb84 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -325,8 +325,9 @@ def test_apng_syntax_errors(): pytest.warns(UserWarning, open) -def test_apng_sequence_errors(): - test_files = [ +@pytest.mark.parametrize( + "f", + ( "sequence_start.png", "sequence_gap.png", "sequence_repeat.png", @@ -334,12 +335,13 @@ def test_apng_sequence_errors(): "sequence_reorder.png", "sequence_reorder_chunk.png", "sequence_fdat_fctl.png", - ] - for f in test_files: - with pytest.raises(SyntaxError): - with Image.open(f"Tests/images/apng/{f}") as im: - im.seek(im.n_frames - 1) - im.load() + ), +) +def test_apng_sequence_errors(f): + with pytest.raises(SyntaxError): + with Image.open(f"Tests/images/apng/{f}") as im: + im.seek(im.n_frames - 1) + im.load() def test_apng_save(tmp_path): diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index b752e217f..65cf6a75e 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -1,3 +1,5 @@ +import pytest + from PIL import ContainerIO, Image from .helper import hopper @@ -59,89 +61,89 @@ def test_seek_mode_2(): assert container.tell() == 100 -def test_read_n0(): +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_read_n0(bytesmode): # Arrange - for bytesmode in (True, False): - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - # Act - container.seek(81) - data = container.read() + # Act + container.seek(81) + data = container.read() - # Assert - if bytesmode: - data = data.decode() - assert data == "7\nThis is line 8\n" + # Assert + if bytesmode: + data = data.decode() + assert data == "7\nThis is line 8\n" -def test_read_n(): +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_read_n(bytesmode): # Arrange - for bytesmode in (True, False): - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - # Act - container.seek(81) - data = container.read(3) + # Act + container.seek(81) + data = container.read(3) - # Assert - if bytesmode: - data = data.decode() - assert data == "7\nT" + # Assert + if bytesmode: + data = data.decode() + assert data == "7\nT" -def test_read_eof(): +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_read_eof(bytesmode): # Arrange - for bytesmode in (True, False): - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - # Act - container.seek(100) - data = container.read() + # Act + container.seek(100) + data = container.read() - # Assert - if bytesmode: - data = data.decode() - assert data == "" + # Assert + if bytesmode: + data = data.decode() + assert data == "" -def test_readline(): +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_readline(bytesmode): # Arrange - for bytesmode in (True, False): - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 0, 120) + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) - # Act - data = container.readline() + # Act + data = container.readline() - # Assert - if bytesmode: - data = data.decode() - assert data == "This is line 1\n" + # Assert + if bytesmode: + data = data.decode() + assert data == "This is line 1\n" -def test_readlines(): +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_readlines(bytesmode): # Arrange - for bytesmode in (True, False): - expected = [ - "This is line 1\n", - "This is line 2\n", - "This is line 3\n", - "This is line 4\n", - "This is line 5\n", - "This is line 6\n", - "This is line 7\n", - "This is line 8\n", - ] - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 0, 120) + expected = [ + "This is line 1\n", + "This is line 2\n", + "This is line 3\n", + "This is line 4\n", + "This is line 5\n", + "This is line 6\n", + "This is line 7\n", + "This is line 8\n", + ] + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) - # Act - data = container.readlines() + # Act + data = container.readlines() - # Assert - if bytesmode: - data = [line.decode() for line in data] - assert data == expected + # Assert + if bytesmode: + data = [line.decode() for line in data] + assert data == expected diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 675210c30..e458a197c 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -78,15 +78,12 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_roundtrip(tmp_path): - def roundtrip(mode): - out = str(tmp_path / "temp.im") - im = hopper(mode) - im.save(out) - assert_image_equal_tofile(im, out) - - for mode in ["RGB", "P", "PA"]: - roundtrip(mode) +@pytest.mark.parametrize("mode", ("RGB", "P", "PA")) +def test_roundtrip(mode, tmp_path): + out = str(tmp_path / "temp.im") + im = hopper(mode) + im.save(out) + assert_image_equal_tofile(im, out) def test_save_unsupported_mode(tmp_path): diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index f9d8e2826..86a0fda04 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -135,50 +135,50 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_write_metadata(self, tmp_path): + @pytest.mark.parametrize("legacy_api", (False, True)) + def test_write_metadata(self, legacy_api, tmp_path): """Test metadata writing through libtiff""" - for legacy_api in [False, True]: - f = str(tmp_path / "temp.tiff") - with Image.open("Tests/images/hopper_g4.tif") as img: - img.save(f, tiffinfo=img.tag) + f = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper_g4.tif") as img: + img.save(f, tiffinfo=img.tag) - if legacy_api: - original = img.tag.named() - else: - original = img.tag_v2.named() + if legacy_api: + original = img.tag.named() + else: + original = img.tag_v2.named() - # PhotometricInterpretation is set from SAVE_INFO, - # not the original image. - ignored = [ - "StripByteCounts", - "RowsPerStrip", - "PageNumber", - "PhotometricInterpretation", - ] + # PhotometricInterpretation is set from SAVE_INFO, + # not the original image. + ignored = [ + "StripByteCounts", + "RowsPerStrip", + "PageNumber", + "PhotometricInterpretation", + ] - with Image.open(f) as loaded: - if legacy_api: - reloaded = loaded.tag.named() - else: - reloaded = loaded.tag_v2.named() + with Image.open(f) as loaded: + if legacy_api: + reloaded = loaded.tag.named() + else: + reloaded = loaded.tag_v2.named() - for tag, value in itertools.chain(reloaded.items(), original.items()): - if tag not in ignored: - val = original[tag] - if tag.endswith("Resolution"): - if legacy_api: - assert val[0][0] / val[0][1] == ( - 4294967295 / 113653537 - ), f"{tag} didn't roundtrip" - else: - assert val == 37.79000115940079, f"{tag} didn't roundtrip" + for tag, value in itertools.chain(reloaded.items(), original.items()): + if tag not in ignored: + val = original[tag] + if tag.endswith("Resolution"): + if legacy_api: + assert val[0][0] / val[0][1] == ( + 4294967295 / 113653537 + ), f"{tag} didn't roundtrip" else: - assert val == value, f"{tag} didn't roundtrip" + assert val == 37.79000115940079, f"{tag} didn't roundtrip" + else: + assert val == value, f"{tag} didn't roundtrip" - # https://github.com/python-pillow/Pillow/issues/1561 - requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"] - for field in requested_fields: - assert field in reloaded, f"{field} not in metadata" + # https://github.com/python-pillow/Pillow/issues/1561 + requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"] + for field in requested_fields: + assert field in reloaded, f"{field} not in metadata" @pytest.mark.valgrind_known_error(reason="Known invalid metadata") def test_additional_metadata(self, tmp_path): diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 849857d31..d94bdaa96 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -27,13 +27,13 @@ def roundtrip(im, **options): return im -def test_sanity(): - for test_file in test_files: - with Image.open(test_file) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (640, 480) - assert im.format == "MPO" +@pytest.mark.parametrize("test_file", test_files) +def test_sanity(test_file): + with Image.open(test_file) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (640, 480) + assert im.format == "MPO" @pytest.mark.skipif(is_pypy(), reason="Requires CPython") @@ -66,26 +66,25 @@ def test_context_manager(): im.load() -def test_app(): - for test_file in test_files: - # Test APP/COM reader (@PIL135) - with Image.open(test_file) as im: - assert im.applist[0][0] == "APP1" - assert im.applist[1][0] == "APP2" - assert ( - im.applist[1][1][:16] - == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" - ) - assert len(im.applist) == 2 +@pytest.mark.parametrize("test_file", test_files) +def test_app(test_file): + # Test APP/COM reader (@PIL135) + with Image.open(test_file) as im: + assert im.applist[0][0] == "APP1" + assert im.applist[1][0] == "APP2" + assert ( + im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" + ) + assert len(im.applist) == 2 -def test_exif(): - for test_file in test_files: - with Image.open(test_file) as im: - info = im._getexif() - assert info[272] == "Nintendo 3DS" - assert info[296] == 2 - assert info[34665] == 188 +@pytest.mark.parametrize("test_file", test_files) +def test_exif(test_file): + with Image.open(test_file) as im: + info = im._getexif() + assert info[272] == "Nintendo 3DS" + assert info[296] == 2 + assert info[34665] == 188 def test_frame_size(): @@ -137,12 +136,12 @@ def test_reload_exif_after_seek(): assert 296 in exif -def test_mp(): - for test_file in test_files: - with Image.open(test_file) as im: - mpinfo = im._getmp() - assert mpinfo[45056] == b"0100" - assert mpinfo[45057] == 2 +@pytest.mark.parametrize("test_file", test_files) +def test_mp(test_file): + with Image.open(test_file) as im: + mpinfo = im._getmp() + assert mpinfo[45056] == b"0100" + assert mpinfo[45057] == 2 def test_mp_offset(): @@ -162,48 +161,48 @@ def test_mp_no_data(): im.seek(1) -def test_mp_attribute(): - for test_file in test_files: - with Image.open(test_file) as im: - mpinfo = im._getmp() - frame_number = 0 - for mpentry in mpinfo[0xB002]: - mpattr = mpentry["Attribute"] - if frame_number: - assert not mpattr["RepresentativeImageFlag"] - else: - assert mpattr["RepresentativeImageFlag"] - assert not mpattr["DependentParentImageFlag"] - assert not mpattr["DependentChildImageFlag"] - 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) +def test_mp_attribute(test_file): + with Image.open(test_file) as im: + mpinfo = im._getmp() + frame_number = 0 + for mpentry in mpinfo[0xB002]: + mpattr = mpentry["Attribute"] + if frame_number: + assert not mpattr["RepresentativeImageFlag"] + else: + assert mpattr["RepresentativeImageFlag"] + assert not mpattr["DependentParentImageFlag"] + assert not mpattr["DependentChildImageFlag"] + assert mpattr["ImageDataFormat"] == "JPEG" + assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" + assert mpattr["Reserved"] == 0 + frame_number += 1 -def test_seek(): - for test_file in test_files: - with Image.open(test_file) as im: - assert im.tell() == 0 - # prior to first image raises an error, both blatant and borderline - with pytest.raises(EOFError): - im.seek(-1) - with pytest.raises(EOFError): - im.seek(-523) - # after the final image raises an error, - # both blatant and borderline - with pytest.raises(EOFError): - im.seek(2) - with pytest.raises(EOFError): - im.seek(523) - # bad calls shouldn't change the frame - assert im.tell() == 0 - # this one will work - im.seek(1) - assert im.tell() == 1 - # and this one, too - im.seek(0) - assert im.tell() == 0 +@pytest.mark.parametrize("test_file", test_files) +def test_seek(test_file): + with Image.open(test_file) as im: + assert im.tell() == 0 + # prior to first image raises an error, both blatant and borderline + with pytest.raises(EOFError): + im.seek(-1) + with pytest.raises(EOFError): + im.seek(-523) + # after the final image raises an error, + # both blatant and borderline + with pytest.raises(EOFError): + im.seek(2) + with pytest.raises(EOFError): + im.seek(523) + # bad calls shouldn't change the frame + assert im.tell() == 0 + # this one will work + im.seek(1) + assert im.tell() == 1 + # and this one, too + im.seek(0) + assert im.tell() == 0 def test_n_frames(): @@ -225,31 +224,31 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_image_grab(): - for test_file in test_files: - with Image.open(test_file) as im: - assert im.tell() == 0 - im0 = im.tobytes() - im.seek(1) - assert im.tell() == 1 - im1 = im.tobytes() - im.seek(0) - assert im.tell() == 0 - im02 = im.tobytes() - assert im0 == im02 - assert im0 != im1 +@pytest.mark.parametrize("test_file", test_files) +def test_image_grab(test_file): + with Image.open(test_file) as im: + assert im.tell() == 0 + im0 = im.tobytes() + im.seek(1) + assert im.tell() == 1 + im1 = im.tobytes() + im.seek(0) + assert im.tell() == 0 + im02 = im.tobytes() + assert im0 == im02 + assert im0 != im1 -def test_save(): - for test_file in test_files: - with Image.open(test_file) as im: - assert im.tell() == 0 - jpg0 = roundtrip(im) - assert_image_similar(im, jpg0, 30) - im.seek(1) - assert im.tell() == 1 - jpg1 = roundtrip(im) - assert_image_similar(im, jpg1, 30) +@pytest.mark.parametrize("test_file", test_files) +def test_save(test_file): + with Image.open(test_file) as im: + assert im.tell() == 0 + jpg0 = roundtrip(im) + assert_image_similar(im, jpg0, 30) + im.seek(1) + assert im.tell() == 1 + jpg1 = roundtrip(im) + assert_image_similar(im, jpg1, 30) def test_save_all(): diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 0c8c9f304..cbbb7df1d 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -18,51 +18,48 @@ _ORIGINS = ("tl", "bl") _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} -def test_sanity(tmp_path): - for mode in _MODES: +@pytest.mark.parametrize("mode", _MODES) +def test_sanity(mode, tmp_path): + def roundtrip(original_im): + out = str(tmp_path / "temp.tga") - def roundtrip(original_im): - out = str(tmp_path / "temp.tga") + original_im.save(out, rle=rle) + with Image.open(out) as saved_im: + if rle: + assert saved_im.info["compression"] == original_im.info["compression"] + assert saved_im.info["orientation"] == original_im.info["orientation"] + if mode == "P": + assert saved_im.getpalette() == original_im.getpalette() - original_im.save(out, rle=rle) - with Image.open(out) as saved_im: - if rle: + assert_image_equal(saved_im, original_im) + + png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) + + for png_path in png_paths: + with Image.open(png_path) as reference_im: + assert reference_im.mode == mode + + path_no_ext = os.path.splitext(png_path)[0] + for origin, rle in product(_ORIGINS, (True, False)): + tga_path = "{}_{}_{}.tga".format( + path_no_ext, origin, "rle" if rle else "raw" + ) + + with Image.open(tga_path) as original_im: + assert original_im.format == "TGA" + assert original_im.get_format_mimetype() == "image/x-tga" + if rle: + assert original_im.info["compression"] == "tga_rle" assert ( - saved_im.info["compression"] == original_im.info["compression"] + original_im.info["orientation"] + == _ORIGIN_TO_ORIENTATION[origin] ) - assert saved_im.info["orientation"] == original_im.info["orientation"] - if mode == "P": - assert saved_im.getpalette() == original_im.getpalette() + if mode == "P": + assert original_im.getpalette() == reference_im.getpalette() - assert_image_equal(saved_im, original_im) + assert_image_equal(original_im, reference_im) - png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) - - for png_path in png_paths: - with Image.open(png_path) as reference_im: - assert reference_im.mode == mode - - path_no_ext = os.path.splitext(png_path)[0] - for origin, rle in product(_ORIGINS, (True, False)): - tga_path = "{}_{}_{}.tga".format( - path_no_ext, origin, "rle" if rle else "raw" - ) - - with Image.open(tga_path) as original_im: - assert original_im.format == "TGA" - assert original_im.get_format_mimetype() == "image/x-tga" - if rle: - assert original_im.info["compression"] == "tga_rle" - assert ( - original_im.info["orientation"] - == _ORIGIN_TO_ORIENTATION[origin] - ) - if mode == "P": - assert original_im.getpalette() == reference_im.getpalette() - - assert_image_equal(original_im, reference_im) - - roundtrip(original_im) + roundtrip(original_im) def test_palette_depth_16(tmp_path): diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index d6769a24b..439cb15bc 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -66,10 +66,10 @@ def test_load_set_dpi(): assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1) -def test_save(tmp_path): +@pytest.mark.parametrize("ext", (".wmf", ".emf")) +def test_save(ext, tmp_path): im = hopper() - for ext in [".wmf", ".emf"]: - tmpfile = str(tmp_path / ("temp" + ext)) - with pytest.raises(OSError): - im.save(tmpfile) + tmpfile = str(tmp_path / ("temp" + ext)) + with pytest.raises(OSError): + im.save(tmpfile) diff --git a/Tests/test_image.py b/Tests/test_image.py index 6dc89918f..7cebed127 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -22,8 +22,9 @@ from .helper import ( class TestImage: - def test_image_modes_success(self): - for mode in [ + @pytest.mark.parametrize( + "mode", + ( "1", "P", "PA", @@ -44,22 +45,18 @@ class TestImage: "YCbCr", "LAB", "HSV", - ]: - Image.new(mode, (1, 1)) + ), + ) + def test_image_modes_success(self, mode): + Image.new(mode, (1, 1)) - def test_image_modes_fail(self): - for mode in [ - "", - "bad", - "very very long", - "BGR;15", - "BGR;16", - "BGR;24", - "BGR;32", - ]: - with pytest.raises(ValueError) as e: - Image.new(mode, (1, 1)) - assert str(e.value) == "unrecognized image mode" + @pytest.mark.parametrize( + "mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24", "BGR;32") + ) + def test_image_modes_fail(self, mode): + with pytest.raises(ValueError) as e: + Image.new(mode, (1, 1)) + assert str(e.value) == "unrecognized image mode" def test_exception_inheritance(self): assert issubclass(UnidentifiedImageError, OSError) @@ -539,23 +536,22 @@ class TestImage: with pytest.raises(ValueError): Image.linear_gradient(wrong_mode) - def test_linear_gradient(self): - + @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) + def test_linear_gradient(self, mode): # Arrange target_file = "Tests/images/linear_gradient.png" - for mode in ["L", "P", "I", "F"]: - # Act - im = Image.linear_gradient(mode) + # Act + im = Image.linear_gradient(mode) - # Assert - assert im.size == (256, 256) - assert im.mode == mode - assert im.getpixel((0, 0)) == 0 - assert im.getpixel((255, 255)) == 255 - with Image.open(target_file) as target: - target = target.convert(mode) - assert_image_equal(im, target) + # Assert + assert im.size == (256, 256) + assert im.mode == mode + assert im.getpixel((0, 0)) == 0 + assert im.getpixel((255, 255)) == 255 + with Image.open(target_file) as target: + target = target.convert(mode) + assert_image_equal(im, target) def test_radial_gradient_wrong_mode(self): # Arrange @@ -565,23 +561,22 @@ class TestImage: with pytest.raises(ValueError): Image.radial_gradient(wrong_mode) - def test_radial_gradient(self): - + @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) + def test_radial_gradient(self, mode): # Arrange target_file = "Tests/images/radial_gradient.png" - for mode in ["L", "P", "I", "F"]: - # Act - im = Image.radial_gradient(mode) + # Act + im = Image.radial_gradient(mode) - # Assert - assert im.size == (256, 256) - assert im.mode == mode - assert im.getpixel((0, 0)) == 255 - assert im.getpixel((128, 128)) == 0 - with Image.open(target_file) as target: - target = target.convert(mode) - assert_image_equal(im, target) + # Assert + assert im.size == (256, 256) + assert im.mode == mode + assert im.getpixel((0, 0)) == 255 + assert im.getpixel((128, 128)) == 0 + with Image.open(target_file) as target: + target = target.convert(mode) + assert_image_equal(im, target) def test_register_extensions(self): test_format = "a" diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 617274a57..bb75eb0b5 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -184,8 +184,9 @@ class TestImageGetPixel(AccessTest): with pytest.raises(error): im.getpixel((-1, -1)) - def test_basic(self): - for mode in ( + @pytest.mark.parametrize( + "mode", + ( "1", "L", "LA", @@ -200,23 +201,25 @@ class TestImageGetPixel(AccessTest): "RGBX", "CMYK", "YCbCr", - ): - self.check(mode) + ), + ) + def test_basic(self, mode): + self.check(mode) - def test_signedness(self): + @pytest.mark.parametrize("mode", ("I;16", "I;16B")) + def test_signedness(self, mode): # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* - for mode in ("I;16", "I;16B"): - self.check(mode, 2**15 - 1) - self.check(mode, 2**15) - self.check(mode, 2**15 + 1) - self.check(mode, 2**16 - 1) + self.check(mode, 2**15 - 1) + self.check(mode, 2**15) + self.check(mode, 2**15 + 1) + self.check(mode, 2**16 - 1) - def test_p_putpixel_rgb_rgba(self): - for color in [(255, 0, 0), (255, 0, 0, 255)]: - im = Image.new("P", (1, 1), 0) - im.putpixel((0, 0), color) - assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) + @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) + def test_p_putpixel_rgb_rgba(self, color): + im = Image.new("P", (1, 1), 0) + im.putpixel((0, 0), color) + assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) @pytest.mark.skipif(cffi is None, reason="No CFFI") diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index e5639e105..8f4b8b43c 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -268,36 +268,33 @@ def test_matrix_wrong_mode(): im.convert(mode="L", matrix=matrix) -def test_matrix_xyz(): - def matrix_convert(mode): - # Arrange - im = hopper("RGB") - im.info["transparency"] = (255, 0, 0) - # fmt: off - matrix = ( - 0.412453, 0.357580, 0.180423, 0, - 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0) - # fmt: on - assert im.mode == "RGB" +@pytest.mark.parametrize("mode", ("RGB", "L")) +def test_matrix_xyz(mode): + # Arrange + im = hopper("RGB") + im.info["transparency"] = (255, 0, 0) + # fmt: off + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + # fmt: on + assert im.mode == "RGB" - # Act - # Convert an RGB image to the CIE XYZ colour space - converted_im = im.convert(mode=mode, matrix=matrix) + # Act + # Convert an RGB image to the CIE XYZ colour space + converted_im = im.convert(mode=mode, matrix=matrix) - # Assert - assert converted_im.mode == mode - assert converted_im.size == im.size - with Image.open("Tests/images/hopper-XYZ.png") as target: - if converted_im.mode == "RGB": - assert_image_similar(converted_im, target, 3) - assert converted_im.info["transparency"] == (105, 54, 4) - else: - assert_image_similar(converted_im, target.getchannel(0), 1) - assert converted_im.info["transparency"] == 105 - - matrix_convert("RGB") - matrix_convert("L") + # Assert + assert converted_im.mode == mode + assert converted_im.size == im.size + with Image.open("Tests/images/hopper-XYZ.png") as target: + if converted_im.mode == "RGB": + assert_image_similar(converted_im, target, 3) + assert converted_im.info["transparency"] == (105, 54, 4) + else: + assert_image_similar(converted_im, target.getchannel(0), 1) + assert converted_im.info["transparency"] == 105 def test_matrix_identity(): diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 21e438654..591832147 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -1,37 +1,40 @@ import copy +import pytest + from PIL import Image from .helper import hopper -def test_copy(): +@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) +def test_copy(mode): cropped_coordinates = (10, 10, 20, 20) cropped_size = (10, 10) - for mode in "1", "P", "L", "RGB", "I", "F": - # Internal copy method - im = hopper(mode) - out = im.copy() - assert out.mode == im.mode - assert out.size == im.size - # Python's copy method - im = hopper(mode) - out = copy.copy(im) - assert out.mode == im.mode - assert out.size == im.size + # Internal copy method + im = hopper(mode) + out = im.copy() + assert out.mode == im.mode + assert out.size == im.size - # Internal copy method on a cropped image - im = hopper(mode) - out = im.crop(cropped_coordinates).copy() - assert out.mode == im.mode - assert out.size == cropped_size + # Python's copy method + im = hopper(mode) + out = copy.copy(im) + assert out.mode == im.mode + assert out.size == im.size - # Python's copy method on a cropped image - im = hopper(mode) - out = copy.copy(im.crop(cropped_coordinates)) - assert out.mode == im.mode - assert out.size == cropped_size + # Internal copy method on a cropped image + im = hopper(mode) + out = im.crop(cropped_coordinates).copy() + assert out.mode == im.mode + assert out.size == cropped_size + + # Python's copy method on a cropped image + im = hopper(mode) + out = copy.copy(im.crop(cropped_coordinates)) + assert out.mode == im.mode + assert out.size == cropped_size def test_copy_zero(): diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 6574e6efd..4aa41de27 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -5,17 +5,14 @@ from PIL import Image from .helper import assert_image_equal, hopper -def test_crop(): - def crop(mode): - im = hopper(mode) - assert_image_equal(im.crop(), im) +@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) +def test_crop(mode): + im = hopper(mode) + assert_image_equal(im.crop(), im) - cropped = im.crop((50, 50, 100, 100)) - assert cropped.mode == mode - assert cropped.size == (50, 50) - - for mode in "1", "P", "L", "RGB", "I", "F": - crop(mode) + cropped = im.crop((50, 50, 100, 100)) + assert cropped.mode == mode + assert cropped.size == (50, 50) def test_wide_crop(): diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 6d050efcc..883bb9b19 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -100,40 +100,41 @@ class TestImagingCoreResampleAccuracy: for y in range(image.size[1]) ) - def test_reduce_box(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (8, 8), 0xE1) - case = case.resize((4, 4), Image.Resampling.BOX) - # fmt: off - data = ("e1 e1" - "e1 e1") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_box(self, mode): + case = self.make_case(mode, (8, 8), 0xE1) + case = case.resize((4, 4), Image.Resampling.BOX) + # fmt: off + data = ("e1 e1" + "e1 e1") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) - def test_reduce_bilinear(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (8, 8), 0xE1) - case = case.resize((4, 4), Image.Resampling.BILINEAR) - # fmt: off - data = ("e1 c9" - "c9 b7") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_bilinear(self, mode): + case = self.make_case(mode, (8, 8), 0xE1) + case = case.resize((4, 4), Image.Resampling.BILINEAR) + # fmt: off + data = ("e1 c9" + "c9 b7") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) - def test_reduce_hamming(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (8, 8), 0xE1) - case = case.resize((4, 4), Image.Resampling.HAMMING) - # fmt: off - data = ("e1 da" - "da d3") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_hamming(self, mode): + case = self.make_case(mode, (8, 8), 0xE1) + case = case.resize((4, 4), Image.Resampling.HAMMING) + # fmt: off + data = ("e1 da" + "da d3") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) - def test_reduce_bicubic(self): + @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) @@ -145,79 +146,79 @@ class TestImagingCoreResampleAccuracy: for channel in case.split(): self.check_case(channel, self.make_sample(data, (6, 6))) - def test_reduce_lanczos(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (16, 16), 0xE1) - case = case.resize((8, 8), Image.Resampling.LANCZOS) - # fmt: off - data = ("e1 e0 e4 d7" - "e0 df e3 d6" - "e4 e3 e7 da" - "d7 d6 d9 ce") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (8, 8))) + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_lanczos(self, mode): + case = self.make_case(mode, (16, 16), 0xE1) + case = case.resize((8, 8), Image.Resampling.LANCZOS) + # fmt: off + data = ("e1 e0 e4 d7" + "e0 df e3 d6" + "e4 e3 e7 da" + "d7 d6 d9 ce") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (8, 8))) - def test_enlarge_box(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (2, 2), 0xE1) - case = case.resize((4, 4), Image.Resampling.BOX) - # fmt: off - data = ("e1 e1" - "e1 e1") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_box(self, mode): + case = self.make_case(mode, (2, 2), 0xE1) + case = case.resize((4, 4), Image.Resampling.BOX) + # fmt: off + data = ("e1 e1" + "e1 e1") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) - def test_enlarge_bilinear(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (2, 2), 0xE1) - case = case.resize((4, 4), Image.Resampling.BILINEAR) - # fmt: off - data = ("e1 b0" - "b0 98") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_bilinear(self, mode): + case = self.make_case(mode, (2, 2), 0xE1) + case = case.resize((4, 4), Image.Resampling.BILINEAR) + # fmt: off + data = ("e1 b0" + "b0 98") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) - def test_enlarge_hamming(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (2, 2), 0xE1) - case = case.resize((4, 4), Image.Resampling.HAMMING) - # fmt: off - data = ("e1 d2" - "d2 c5") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_hamming(self, mode): + case = self.make_case(mode, (2, 2), 0xE1) + case = case.resize((4, 4), Image.Resampling.HAMMING) + # fmt: off + data = ("e1 d2" + "d2 c5") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) - def test_enlarge_bicubic(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (4, 4), 0xE1) - case = case.resize((8, 8), Image.Resampling.BICUBIC) - # fmt: off - data = ("e1 e5 ee b9" - "e5 e9 f3 bc" - "ee f3 fd c1" - "b9 bc c1 a2") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (8, 8))) + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_bicubic(self, mode): + case = self.make_case(mode, (4, 4), 0xE1) + case = case.resize((8, 8), Image.Resampling.BICUBIC) + # fmt: off + data = ("e1 e5 ee b9" + "e5 e9 f3 bc" + "ee f3 fd c1" + "b9 bc c1 a2") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (8, 8))) - def test_enlarge_lanczos(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (6, 6), 0xE1) - case = case.resize((12, 12), Image.Resampling.LANCZOS) - data = ( - "e1 e0 db ed f5 b8" - "e0 df da ec f3 b7" - "db db d6 e7 ee b5" - "ed ec e6 fb ff bf" - "f5 f4 ee ff ff c4" - "b8 b7 b4 bf c4 a0" - ) - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (12, 12))) + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_lanczos(self, mode): + case = self.make_case(mode, (6, 6), 0xE1) + case = case.resize((12, 12), Image.Resampling.LANCZOS) + data = ( + "e1 e0 db ed f5 b8" + "e0 df da ec f3 b7" + "db db d6 e7 ee b5" + "ed ec e6 fb ff bf" + "f5 f4 ee ff ff c4" + "b8 b7 b4 bf c4 a0" + ) + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (12, 12))) def test_box_filter_correct_range(self): im = Image.new("RGB", (8, 8), "#1688ff").resize( @@ -419,40 +420,43 @@ class TestCoreResampleCoefficients: class TestCoreResampleBox: - def test_wrong_arguments(self): - im = hopper() - for resample in ( + @pytest.mark.parametrize( + "resample", + ( Image.Resampling.NEAREST, Image.Resampling.BOX, Image.Resampling.BILINEAR, Image.Resampling.HAMMING, Image.Resampling.BICUBIC, Image.Resampling.LANCZOS, - ): - im.resize((32, 32), resample, (0, 0, im.width, im.height)) - im.resize((32, 32), resample, (20, 20, im.width, im.height)) - im.resize((32, 32), resample, (20, 20, 20, 100)) - im.resize((32, 32), resample, (20, 20, 100, 20)) + ), + ) + def test_wrong_arguments(self, resample): + im = hopper() + im.resize((32, 32), resample, (0, 0, im.width, im.height)) + im.resize((32, 32), resample, (20, 20, im.width, im.height)) + im.resize((32, 32), resample, (20, 20, 20, 100)) + im.resize((32, 32), resample, (20, 20, 100, 20)) - with pytest.raises(TypeError, match="must be sequence of length 4"): - im.resize((32, 32), resample, (im.width, im.height)) + with pytest.raises(TypeError, match="must be sequence of length 4"): + im.resize((32, 32), resample, (im.width, im.height)) - with pytest.raises(ValueError, match="can't be negative"): - im.resize((32, 32), resample, (-20, 20, 100, 100)) - with pytest.raises(ValueError, match="can't be negative"): - im.resize((32, 32), resample, (20, -20, 100, 100)) + with pytest.raises(ValueError, match="can't be negative"): + im.resize((32, 32), resample, (-20, 20, 100, 100)) + with pytest.raises(ValueError, match="can't be negative"): + im.resize((32, 32), resample, (20, -20, 100, 100)) - with pytest.raises(ValueError, match="can't be empty"): - im.resize((32, 32), resample, (20.1, 20, 20, 100)) - with pytest.raises(ValueError, match="can't be empty"): - im.resize((32, 32), resample, (20, 20.1, 100, 20)) - with pytest.raises(ValueError, match="can't be empty"): - im.resize((32, 32), resample, (20.1, 20.1, 20, 20)) + with pytest.raises(ValueError, match="can't be empty"): + im.resize((32, 32), resample, (20.1, 20, 20, 100)) + with pytest.raises(ValueError, match="can't be empty"): + im.resize((32, 32), resample, (20, 20.1, 100, 20)) + with pytest.raises(ValueError, match="can't be empty"): + im.resize((32, 32), resample, (20.1, 20.1, 20, 20)) - with pytest.raises(ValueError, match="can't exceed"): - im.resize((32, 32), resample, (0, 0, im.width + 1, im.height)) - with pytest.raises(ValueError, match="can't exceed"): - im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) + with pytest.raises(ValueError, match="can't exceed"): + im.resize((32, 32), resample, (0, 0, im.width + 1, im.height)) + with pytest.raises(ValueError, match="can't exceed"): + im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) def resize_tiled(self, im, dst_size, xtiles, ytiles): def split_range(size, tiles): @@ -509,14 +513,14 @@ class TestCoreResampleBox: with pytest.raises(AssertionError, match=r"difference 29\."): assert_image_similar(reference, without_box, 5) - def test_formats(self): + @pytest.mark.parametrize("mode", ("RGB", "L", "RGBA", "LA", "I", "")) + def test_formats(self, mode): for resample in [Image.Resampling.NEAREST, Image.Resampling.BILINEAR]: - for mode in ["RGB", "L", "RGBA", "LA", "I", ""]: - im = hopper(mode) - box = (20, 20, im.size[0] - 20, im.size[1] - 20) - with_box = im.resize((32, 32), resample, box) - cropped = im.crop(box).resize((32, 32), resample) - assert_image_similar(cropped, with_box, 0.4) + im = hopper(mode) + box = (20, 20, im.size[0] - 20, im.size[1] - 20) + with_box = im.resize((32, 32), resample, box) + cropped = im.crop(box).resize((32, 32), resample) + assert_image_similar(cropped, with_box, 0.4) def test_passthrough(self): # When no resize is required diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 8347fabb9..ae12202e4 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -22,24 +22,15 @@ class TestImagingCoreResize: im.load() return im._new(im.im.resize(size, f)) - def test_nearest_mode(self): - for mode in [ - "1", - "P", - "L", - "I", - "F", - "RGB", - "RGBA", - "CMYK", - "YCbCr", - "I;16", - ]: # exotic mode - im = hopper(mode) - r = self.resize(im, (15, 12), Image.Resampling.NEAREST) - assert r.mode == mode - assert r.size == (15, 12) - assert r.im.bands == im.im.bands + @pytest.mark.parametrize( + "mode", ("1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", "I;16") + ) + def test_nearest_mode(self, mode): + im = hopper(mode) + r = self.resize(im, (15, 12), Image.Resampling.NEAREST) + assert r.mode == mode + assert r.size == (15, 12) + assert r.im.bands == im.im.bands def test_convolution_modes(self): with pytest.raises(ValueError): diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index f96864c53..a19f19831 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,3 +1,5 @@ +import pytest + from PIL import Image from .helper import ( @@ -22,26 +24,26 @@ def rotate(im, mode, angle, center=None, translate=None): assert out.size != im.size -def test_mode(): - for mode in ("1", "P", "L", "RGB", "I", "F"): - im = hopper(mode) - rotate(im, mode, 45) +@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) +def test_mode(mode): + im = hopper(mode) + rotate(im, mode, 45) -def test_angle(): - for angle in (0, 90, 180, 270): - with Image.open("Tests/images/test-card.png") as im: - rotate(im, im.mode, angle) - - im = hopper() - assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1)) - - -def test_zero(): - for angle in (0, 45, 90, 180, 270): - im = Image.new("RGB", (0, 0)) +@pytest.mark.parametrize("angle", (0, 90, 180, 270)) +def test_angle(angle): + with Image.open("Tests/images/test-card.png") as im: rotate(im, im.mode, angle) + im = hopper() + assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1)) + + +@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) +def test_zero(angle): + im = Image.new("RGB", (0, 0)) + rotate(im, im.mode, angle) + def test_resample(): # Target image creation, inspected by eye. diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index 6408e1564..877f439ca 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,3 +1,5 @@ +import pytest + from PIL.Image import Transpose from . import helper @@ -9,157 +11,136 @@ HOPPER = { } -def test_flip_left_right(): - def transpose(mode): - im = HOPPER[mode] - out = im.transpose(Transpose.FLIP_LEFT_RIGHT) - assert out.mode == mode - assert out.size == im.size +@pytest.mark.parametrize("mode", HOPPER) +def test_flip_left_right(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.FLIP_LEFT_RIGHT) + assert out.mode == mode + assert out.size == im.size - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1)) - assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) - assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2)) - - for mode in HOPPER: - transpose(mode) + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2)) -def test_flip_top_bottom(): - def transpose(mode): - im = HOPPER[mode] - out = im.transpose(Transpose.FLIP_TOP_BOTTOM) - assert out.mode == mode - assert out.size == im.size +@pytest.mark.parametrize("mode", HOPPER) +def test_flip_top_bottom(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.FLIP_TOP_BOTTOM) + assert out.mode == mode + assert out.size == im.size - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((1, y - 2)) - assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2)) - assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1)) - - for mode in HOPPER: - transpose(mode) + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, y - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1)) -def test_rotate_90(): - def transpose(mode): - im = HOPPER[mode] - out = im.transpose(Transpose.ROTATE_90) - assert out.mode == mode - assert out.size == im.size[::-1] +@pytest.mark.parametrize("mode", HOPPER) +def test_rotate_90(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.ROTATE_90) + assert out.mode == mode + assert out.size == im.size[::-1] - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((1, x - 2)) - assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) - assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1)) - - for mode in HOPPER: - transpose(mode) + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, x - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1)) -def test_rotate_180(): - def transpose(mode): - im = HOPPER[mode] - out = im.transpose(Transpose.ROTATE_180) - assert out.mode == mode - assert out.size == im.size +@pytest.mark.parametrize("mode", HOPPER) +def test_rotate_180(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.ROTATE_180) + assert out.mode == mode + assert out.size == im.size - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2)) - assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2)) - assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) - - for mode in HOPPER: - transpose(mode) + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) -def test_rotate_270(): - def transpose(mode): - im = HOPPER[mode] - out = im.transpose(Transpose.ROTATE_270) - assert out.mode == mode - assert out.size == im.size[::-1] +@pytest.mark.parametrize("mode", HOPPER) +def test_rotate_270(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.ROTATE_270) + assert out.mode == mode + assert out.size == im.size[::-1] - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1)) - assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2)) - assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2)) - - for mode in HOPPER: - transpose(mode) + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2)) -def test_transpose(): - def transpose(mode): - im = HOPPER[mode] - out = im.transpose(Transpose.TRANSPOSE) - assert out.mode == mode - assert out.size == im.size[::-1] +@pytest.mark.parametrize("mode", HOPPER) +def test_transpose(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.TRANSPOSE) + assert out.mode == mode + assert out.size == im.size[::-1] - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((1, 1)) - assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2)) - assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2)) - - for mode in HOPPER: - transpose(mode) + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2)) -def test_tranverse(): - def transpose(mode): - im = HOPPER[mode] - out = im.transpose(Transpose.TRANSVERSE) - assert out.mode == mode - assert out.size == im.size[::-1] +@pytest.mark.parametrize("mode", HOPPER) +def test_tranverse(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.TRANSVERSE) + assert out.mode == mode + assert out.size == im.size[::-1] - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2)) - assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1)) - assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) - - for mode in HOPPER: - transpose(mode) + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) -def test_roundtrip(): - for mode in HOPPER: - im = HOPPER[mode] +@pytest.mark.parametrize("mode", HOPPER) +def test_roundtrip(mode): + im = HOPPER[mode] - def transpose(first, second): - return im.transpose(first).transpose(second) + def transpose(first, second): + return im.transpose(first).transpose(second) - assert_image_equal( - im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT) - ) - assert_image_equal( - im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM) - ) - assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270)) - assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180)) - assert_image_equal( - im.transpose(Transpose.TRANSPOSE), - transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM), - ) - assert_image_equal( - im.transpose(Transpose.TRANSPOSE), - transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT), - ) - assert_image_equal( - im.transpose(Transpose.TRANSVERSE), - transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT), - ) - assert_image_equal( - im.transpose(Transpose.TRANSVERSE), - transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM), - ) - assert_image_equal( - im.transpose(Transpose.TRANSVERSE), - transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE), - ) + assert_image_equal( + im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT) + ) + assert_image_equal( + im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM) + ) + assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270)) + assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180)) + assert_image_equal( + im.transpose(Transpose.TRANSPOSE), + transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM), + ) + assert_image_equal( + im.transpose(Transpose.TRANSPOSE), + transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT), + ) + assert_image_equal( + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT), + ) + assert_image_equal( + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM), + ) + assert_image_equal( + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE), + ) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 961b4d081..d1dd1e47c 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -625,20 +625,20 @@ def test_polygon2(): helper_polygon(POINTS2) -def test_polygon_kite(): +@pytest.mark.parametrize("mode", ("RGB", "L")) +def test_polygon_kite(mode): # Test drawing lines of different gradients (dx>dy, dy>dx) and # vertical (dx==0) and horizontal (dy==0) lines - for mode in ["RGB", "L"]: - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) - expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" - # Act - draw.polygon(KITE_POINTS, fill="blue", outline="yellow") + # Act + draw.polygon(KITE_POINTS, fill="blue", outline="yellow") - # Assert - assert_image_equal_tofile(im, expected) + # Assert + assert_image_equal_tofile(im, expected) def test_polygon_1px_high(): diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 60bfaeb9b..af0b0c293 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -16,32 +16,32 @@ if ImageQt.qt_is_installed: from PIL.ImageQt import QImage -def test_sanity(tmp_path): - for mode in ("RGB", "RGBA", "L", "P", "1"): - src = hopper(mode) - data = ImageQt.toqimage(src) +@pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) +def test_sanity(mode, tmp_path): + src = hopper(mode) + data = ImageQt.toqimage(src) - assert isinstance(data, QImage) - assert not data.isNull() + assert isinstance(data, QImage) + assert not data.isNull() - # reload directly from the qimage - rt = ImageQt.fromqimage(data) - if mode in ("L", "P", "1"): - assert_image_equal(rt, src.convert("RGB")) - else: - assert_image_equal(rt, src) + # reload directly from the qimage + rt = ImageQt.fromqimage(data) + if mode in ("L", "P", "1"): + assert_image_equal(rt, src.convert("RGB")) + else: + assert_image_equal(rt, src) - if mode == "1": - # BW appears to not save correctly on QT4 and QT5 - # kicks out errors on console: - # libpng warning: Invalid color type/bit depth combination - # in IHDR - # libpng error: Invalid IHDR data - continue + if mode == "1": + # BW appears to not save correctly on QT4 and QT5 + # kicks out errors on console: + # libpng warning: Invalid color type/bit depth combination + # in IHDR + # libpng error: Invalid IHDR data + return - # Test saving the file - tempfile = str(tmp_path / f"temp_{mode}.png") - data.save(tempfile) + # Test saving the file + tempfile = str(tmp_path / f"temp_{mode}.png") + data.save(tempfile) - # Check that it actually worked. - assert_image_equal_tofile(src, tempfile) + # Check that it actually worked. + assert_image_equal_tofile(src, tempfile) From 1c391fe31f902b604a7bc4ebd9b4315fa5ef8e1f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Aug 2022 08:11:02 +1000 Subject: [PATCH 065/100] Renamed argument --- Tests/test_file_apng.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index d624bbb84..0ff05f608 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -326,7 +326,7 @@ def test_apng_syntax_errors(): @pytest.mark.parametrize( - "f", + "test_file", ( "sequence_start.png", "sequence_gap.png", @@ -337,9 +337,9 @@ def test_apng_syntax_errors(): "sequence_fdat_fctl.png", ), ) -def test_apng_sequence_errors(f): +def test_apng_sequence_errors(test_file): with pytest.raises(SyntaxError): - with Image.open(f"Tests/images/apng/{f}") as im: + with Image.open(f"Tests/images/apng/{test_file}") as im: im.seek(im.n_frames - 1) im.load() From 8f25ea46ebd471c48eb424c8754ea1747a54776a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 24 Aug 2022 08:12:14 +1000 Subject: [PATCH 066/100] Qt4 is no longer supported Co-authored-by: Hugo van Kemenade --- Tests/test_qt_image_toqimage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index af0b0c293..c1983031a 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -32,7 +32,7 @@ def test_sanity(mode, tmp_path): assert_image_equal(rt, src) if mode == "1": - # BW appears to not save correctly on QT4 and QT5 + # BW appears to not save correctly on QT5 # kicks out errors on console: # libpng warning: Invalid color type/bit depth combination # in IHDR From 3353ea80e1c873acdb11636cf3d387b8e59580c4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Aug 2022 10:37:40 +1000 Subject: [PATCH 067/100] Further parametrizations --- Tests/test_image_resample.py | 16 ++- Tests/test_image_resize.py | 257 ++++++++++++++++++----------------- 2 files changed, 143 insertions(+), 130 deletions(-) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 883bb9b19..5ce98a235 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -514,13 +514,15 @@ class TestCoreResampleBox: assert_image_similar(reference, without_box, 5) @pytest.mark.parametrize("mode", ("RGB", "L", "RGBA", "LA", "I", "")) - def test_formats(self, mode): - for resample in [Image.Resampling.NEAREST, Image.Resampling.BILINEAR]: - im = hopper(mode) - box = (20, 20, im.size[0] - 20, im.size[1] - 20) - with_box = im.resize((32, 32), resample, box) - cropped = im.crop(box).resize((32, 32), resample) - assert_image_similar(cropped, with_box, 0.4) + @pytest.mark.parametrize( + "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR) + ) + def test_formats(self, mode, resample): + im = hopper(mode) + box = (20, 20, im.size[0] - 20, im.size[1] - 20) + with_box = im.resize((32, 32), resample, box) + cropped = im.crop(box).resize((32, 32), resample) + assert_image_similar(cropped, with_box, 0.4) def test_passthrough(self): # When no resize is required diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index ae12202e4..83c54cf62 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -46,33 +46,58 @@ class TestImagingCoreResize: assert r.size == (15, 12) assert r.im.bands == im.im.bands - def test_reduce_filters(self): - for f in [ + @pytest.mark.parametrize( + "resample", + ( Image.Resampling.NEAREST, Image.Resampling.BOX, Image.Resampling.BILINEAR, Image.Resampling.HAMMING, Image.Resampling.BICUBIC, Image.Resampling.LANCZOS, - ]: - r = self.resize(hopper("RGB"), (15, 12), f) - assert r.mode == "RGB" - assert r.size == (15, 12) + ), + ) + def test_reduce_filters(self, resample): + r = self.resize(hopper("RGB"), (15, 12), resample) + assert r.mode == "RGB" + assert r.size == (15, 12) - def test_enlarge_filters(self): - for f in [ + @pytest.mark.parametrize( + "resample", + ( Image.Resampling.NEAREST, Image.Resampling.BOX, Image.Resampling.BILINEAR, Image.Resampling.HAMMING, Image.Resampling.BICUBIC, Image.Resampling.LANCZOS, - ]: - r = self.resize(hopper("RGB"), (212, 195), f) - assert r.mode == "RGB" - assert r.size == (212, 195) + ), + ) + def test_enlarge_filters(self, resample): + r = self.resize(hopper("RGB"), (212, 195), resample) + assert r.mode == "RGB" + assert r.size == (212, 195) - def test_endianness(self): + @pytest.mark.parametrize( + "resample", + ( + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, + ), + ) + @pytest.mark.parametrize( + "mode, channels_set", + ( + ("RGB", ("blank", "filled", "dirty")), + ("RGBA", ("blank", "blank", "filled", "dirty")), + ("LA", ("filled", "dirty")), + ), + ) + def test_endianness(self, resample, mode, channels_set): # Make an image with one colored pixel, in one channel. # When resized, that channel should be the same as a GS image. # Other channels should be unaffected. @@ -86,47 +111,37 @@ class TestImagingCoreResize: } samples["dirty"].putpixel((1, 1), 128) - for f in [ + # samples resized with current filter + references = { + name: self.resize(ch, (4, 4), resample) for name, ch in samples.items() + } + + for channels in set(permutations(channels_set)): + # compile image from different channels permutations + im = Image.merge(mode, [samples[ch] for ch in channels]) + resized = self.resize(im, (4, 4), resample) + + for i, ch in enumerate(resized.split()): + # check what resized channel in image is the same + # as separately resized channel + assert_image_equal(ch, references[channels[i]]) + + @pytest.mark.parametrize( + "resample", + ( Image.Resampling.NEAREST, Image.Resampling.BOX, Image.Resampling.BILINEAR, Image.Resampling.HAMMING, Image.Resampling.BICUBIC, Image.Resampling.LANCZOS, - ]: - # samples resized with current filter - references = { - name: self.resize(ch, (4, 4), f) for name, ch in samples.items() - } - - for mode, channels_set in [ - ("RGB", ("blank", "filled", "dirty")), - ("RGBA", ("blank", "blank", "filled", "dirty")), - ("LA", ("filled", "dirty")), - ]: - for channels in set(permutations(channels_set)): - # compile image from different channels permutations - im = Image.merge(mode, [samples[ch] for ch in channels]) - resized = self.resize(im, (4, 4), f) - - for i, ch in enumerate(resized.split()): - # check what resized channel in image is the same - # as separately resized channel - assert_image_equal(ch, references[channels[i]]) - - def test_enlarge_zero(self): - for f in [ - Image.Resampling.NEAREST, - Image.Resampling.BOX, - Image.Resampling.BILINEAR, - Image.Resampling.HAMMING, - Image.Resampling.BICUBIC, - Image.Resampling.LANCZOS, - ]: - r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), f) - assert r.mode == "RGB" - assert r.size == (212, 195) - assert r.getdata()[0] == (0, 0, 0) + ), + ) + def test_enlarge_zero(self, resample): + r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample) + assert r.mode == "RGB" + assert r.size == (212, 195) + assert r.getdata()[0] == (0, 0, 0) def test_unknown_filter(self): with pytest.raises(ValueError): @@ -170,74 +185,71 @@ class TestReducingGapResize: (52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99 ) - def test_reducing_gap_1(self, gradients_image): - for box, epsilon in [ - (None, 4), - ((1.1, 2.2, 510.8, 510.9), 4), - ((3, 10, 410, 256), 10), - ]: - ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 - ) - - with pytest.raises(AssertionError): - assert_image_equal(ref, im) - - assert_image_similar(ref, im, epsilon) - - def test_reducing_gap_2(self, gradients_image): - for box, epsilon in [ - (None, 1.5), - ((1.1, 2.2, 510.8, 510.9), 1.5), - ((3, 10, 410, 256), 1), - ]: - ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 - ) - - with pytest.raises(AssertionError): - assert_image_equal(ref, im) - - assert_image_similar(ref, im, epsilon) - - def test_reducing_gap_3(self, gradients_image): - for box, epsilon in [ - (None, 1), - ((1.1, 2.2, 510.8, 510.9), 1), - ((3, 10, 410, 256), 0.5), - ]: - ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 - ) - - with pytest.raises(AssertionError): - assert_image_equal(ref, im) - - assert_image_similar(ref, im, epsilon) - - def test_reducing_gap_8(self, gradients_image): - for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]: - ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 - ) + @pytest.mark.parametrize( + "box, epsilon", + ((None, 4), ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10)), + ) + def test_reducing_gap_1(self, gradients_image, box, epsilon): + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 + ) + with pytest.raises(AssertionError): assert_image_equal(ref, im) - def test_box_filter(self, gradients_image): - for box, epsilon in [ - ((0, 0, 512, 512), 5.5), - ((0.9, 1.7, 128, 128), 9.5), - ]: - ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 - ) + assert_image_similar(ref, im, epsilon) - assert_image_similar(ref, im, epsilon) + @pytest.mark.parametrize( + "box, epsilon", + ((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)), + ) + def test_reducing_gap_2(self, gradients_image, box, epsilon): + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 + ) + + with pytest.raises(AssertionError): + assert_image_equal(ref, im) + + assert_image_similar(ref, im, epsilon) + + @pytest.mark.parametrize( + "box, epsilon", + ((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)), + ) + def test_reducing_gap_3(self, gradients_image, box, epsilon): + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 + ) + + with pytest.raises(AssertionError): + assert_image_equal(ref, im) + + assert_image_similar(ref, im, epsilon) + + @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256))) + def test_reducing_gap_8(self, gradients_image, box): + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 + ) + + assert_image_equal(ref, im) + + @pytest.mark.parametrize( + "box, epsilon", + (((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5)), + ) + def test_box_filter(self, gradients_image, box, epsilon): + ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 + ) + + assert_image_similar(ref, im, epsilon) class TestImageResize: @@ -264,15 +276,14 @@ class TestImageResize: im = im.resize((64, 64)) assert im.size == (64, 64) - def test_default_filter(self): - for mode in "L", "RGB", "I", "F": - im = hopper(mode) - assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) + @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F")) + def test_default_filter_bicubic(self, mode): + im = hopper(mode) + assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) - for mode in "1", "P": - im = hopper(mode) - assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) - - for mode in "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16": - im = hopper(mode) - assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) + @pytest.mark.parametrize( + "mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16") + ) + def test_default_filter_nearest(self, mode): + im = hopper(mode) + assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) From 56ba3ff68c678d0bf5f483b08f8c7428009ac226 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Aug 2022 15:39:43 +1000 Subject: [PATCH 068/100] Build lcms2 VC2022 --- winbuild/build_prepare.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a381d636d..94e5dd871 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -226,21 +226,21 @@ deps = { "filename": "lcms2-2.13.1.tar.gz", "dir": "lcms2-2.13.1", "patch": { - r"Projects\VC2019\lcms2_static\lcms2_static.vcxproj": { + r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { # default is /MD for x86 and /MT for x64, we need /MD always "MultiThreaded": "MultiThreadedDLL", # noqa: E501 # retarget to default toolset (selected by vcvarsall.bat) - "v142": "$(DefaultPlatformToolset)", # noqa: E501 + "v143": "$(DefaultPlatformToolset)", # noqa: E501 # retarget to latest (selected by vcvarsall.bat) "10.0": "$(WindowsSDKVersion)", # noqa: E501 } }, "build": [ cmd_rmdir("Lib"), - cmd_rmdir(r"Projects\VC2019\Release"), - cmd_msbuild(r"Projects\VC2019\lcms2.sln", "Release", "Clean"), + cmd_rmdir(r"Projects\VC2022\Release"), + cmd_msbuild(r"Projects\VC2022\lcms2.sln", "Release", "Clean"), cmd_msbuild( - r"Projects\VC2019\lcms2.sln", "Release", "lcms2_static:Rebuild" + r"Projects\VC2022\lcms2.sln", "Release", "lcms2_static:Rebuild" ), cmd_xcopy("include", "{inc_dir}"), ], From f0be6845f7bff340aaf07bea9f4ded35a28f96fa Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 24 Aug 2022 07:42:51 -0500 Subject: [PATCH 069/100] parametrize tests --- Tests/test_image_filter.py | 188 ++++++++++++++++++++----------------- 1 file changed, 104 insertions(+), 84 deletions(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 14a8da9f1..e12e73f97 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -5,90 +5,110 @@ from PIL import Image, ImageFilter from .helper import assert_image_equal, hopper -def test_sanity(): - def apply_filter(filter_to_apply): - for mode in ["L", "RGB", "CMYK"]: - im = hopper(mode) - out = im.filter(filter_to_apply) - assert out.mode == im.mode - assert out.size == im.size +@pytest.mark.parametrize( + "filter_to_apply", + ( + ImageFilter.BLUR, + ImageFilter.CONTOUR, + ImageFilter.DETAIL, + ImageFilter.EDGE_ENHANCE, + ImageFilter.EDGE_ENHANCE_MORE, + ImageFilter.EMBOSS, + ImageFilter.FIND_EDGES, + ImageFilter.SMOOTH, + ImageFilter.SMOOTH_MORE, + ImageFilter.SHARPEN, + ImageFilter.MaxFilter, + ImageFilter.MedianFilter, + ImageFilter.MinFilter, + ImageFilter.ModeFilter, + ImageFilter.GaussianBlur, + ImageFilter.GaussianBlur(5), + ImageFilter.BoxBlur(5), + ImageFilter.UnsharpMask, + ImageFilter.UnsharpMask(10), + ), +) +@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK")) +def test_sanity(filter_to_apply, mode): + im = hopper(mode) + out = im.filter(filter_to_apply) + assert out.mode == im.mode + assert out.size == im.size - apply_filter(ImageFilter.BLUR) - apply_filter(ImageFilter.CONTOUR) - apply_filter(ImageFilter.DETAIL) - apply_filter(ImageFilter.EDGE_ENHANCE) - apply_filter(ImageFilter.EDGE_ENHANCE_MORE) - apply_filter(ImageFilter.EMBOSS) - apply_filter(ImageFilter.FIND_EDGES) - apply_filter(ImageFilter.SMOOTH) - apply_filter(ImageFilter.SMOOTH_MORE) - apply_filter(ImageFilter.SHARPEN) - apply_filter(ImageFilter.MaxFilter) - apply_filter(ImageFilter.MedianFilter) - apply_filter(ImageFilter.MinFilter) - apply_filter(ImageFilter.ModeFilter) - apply_filter(ImageFilter.GaussianBlur) - apply_filter(ImageFilter.GaussianBlur(5)) - apply_filter(ImageFilter.BoxBlur(5)) - apply_filter(ImageFilter.UnsharpMask) - apply_filter(ImageFilter.UnsharpMask(10)) +@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK")) +def test_sanity_error(mode): with pytest.raises(TypeError): - apply_filter("hello") + im = hopper(mode) + out = im.filter("hello") + assert out.mode == im.mode + assert out.size == im.size -def test_crash(): - - # crashes on small images - im = Image.new("RGB", (1, 1)) - im.filter(ImageFilter.SMOOTH) - - im = Image.new("RGB", (2, 2)) - im.filter(ImageFilter.SMOOTH) - - im = Image.new("RGB", (3, 3)) +# crashes on small images +@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3))) +def test_crash(size): + im = Image.new("RGB", size) im.filter(ImageFilter.SMOOTH) -def test_modefilter(): - def modefilter(mode): - im = Image.new(mode, (3, 3), None) - im.putdata(list(range(9))) - # image is: - # 0 1 2 - # 3 4 5 - # 6 7 8 - mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) - im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0 - mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) - return mod, mod2 - - assert modefilter("1") == (4, 0) - assert modefilter("L") == (4, 0) - assert modefilter("P") == (4, 0) - assert modefilter("RGB") == ((4, 0, 0), (0, 0, 0)) +@pytest.mark.parametrize( + "mode,expected", + ( + ("1", (4, 0)), + ("L", (4, 0)), + ("P", (4, 0)), + ("RGB", ((4, 0, 0), (0, 0, 0))), + ), +) +def test_modefilter(mode, expected): + im = Image.new(mode, (3, 3), None) + im.putdata(list(range(9))) + # image is: + # 0 1 2 + # 3 4 5 + # 6 7 8 + mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) + im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0 + mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) + assert (mod, mod2) == expected -def test_rankfilter(): - def rankfilter(mode): - im = Image.new(mode, (3, 3), None) - im.putdata(list(range(9))) - # image is: - # 0 1 2 - # 3 4 5 - # 6 7 8 - minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) - med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) - maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) - return minimum, med, maximum +@pytest.mark.parametrize( + "mode,expected", + ( + ("1", (0, 4, 8)), + ("L", (0, 4, 8)), + ("RGB", ((0, 0, 0), (4, 0, 0), (8, 0, 0))), + ("I", (0, 4, 8)), + ("F", (0.0, 4.0, 8.0)), + ), +) +def test_rankfilter(mode, expected): + im = Image.new(mode, (3, 3), None) + im.putdata(list(range(9))) + # image is: + # 0 1 2 + # 3 4 5 + # 6 7 8 + minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) + med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) + maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) + assert (minimum, med, maximum) == expected - assert rankfilter("1") == (0, 4, 8) - assert rankfilter("L") == (0, 4, 8) + +def test_rankfilter_error(): with pytest.raises(ValueError): - rankfilter("P") - assert rankfilter("RGB") == ((0, 0, 0), (4, 0, 0), (8, 0, 0)) - assert rankfilter("I") == (0, 4, 8) - assert rankfilter("F") == (0.0, 4.0, 8.0) + im = Image.new("P", (3, 3), None) + im.putdata(list(range(9))) + # image is: + # 0 1 2 + # 3 4 5 + # 6 7 8 + im.filter(ImageFilter.MinFilter).getpixel((1, 1)) + im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) + im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) def test_rankfilter_properties(): @@ -110,7 +130,8 @@ def test_kernel_not_enough_coefficients(): ImageFilter.Kernel((3, 3), (0, 0)) -def test_consistency_3x3(): +@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK")) +def test_consistency_3x3(mode): with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper_emboss.bmp") as reference: kernel = ImageFilter.Kernel( @@ -125,14 +146,14 @@ def test_consistency_3x3(): source = source.split() * 2 reference = reference.split() * 2 - for mode in ["L", "LA", "RGB", "CMYK"]: - assert_image_equal( - Image.merge(mode, source[: len(mode)]).filter(kernel), - Image.merge(mode, reference[: len(mode)]), - ) + assert_image_equal( + Image.merge(mode, source[: len(mode)]).filter(kernel), + Image.merge(mode, reference[: len(mode)]), + ) -def test_consistency_5x5(): +@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK")) +def test_consistency_5x5(mode): with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: kernel = ImageFilter.Kernel( @@ -149,8 +170,7 @@ def test_consistency_5x5(): source = source.split() * 2 reference = reference.split() * 2 - for mode in ["L", "LA", "RGB", "CMYK"]: - assert_image_equal( - Image.merge(mode, source[: len(mode)]).filter(kernel), - Image.merge(mode, reference[: len(mode)]), - ) + assert_image_equal( + Image.merge(mode, source[: len(mode)]).filter(kernel), + Image.merge(mode, reference[: len(mode)]), + ) From fa591e11987d846ae726efd81ab5b743675e170e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 24 Aug 2022 07:43:31 -0500 Subject: [PATCH 070/100] parametrize tests --- Tests/test_image_reduce.py | 160 ++++++++++++++++++++----------------- 1 file changed, 86 insertions(+), 74 deletions(-) diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index 70dc87f0a..90beeeb68 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -38,58 +38,64 @@ gradients_image = Image.open("Tests/images/radial_gradients.png") gradients_image.load() -def test_args_factor(): +@pytest.mark.parametrize( + "size,expected", + ( + (3, (4, 4)), + ((3, 1), (4, 10)), + ((1, 3), (10, 4)), + ), +) +def test_args_factor(size, expected): im = Image.new("L", (10, 10)) - - assert (4, 4) == im.reduce(3).size - assert (4, 10) == im.reduce((3, 1)).size - assert (10, 4) == im.reduce((1, 3)).size - - with pytest.raises(ValueError): - im.reduce(0) - with pytest.raises(TypeError): - im.reduce(2.0) - with pytest.raises(ValueError): - im.reduce((0, 10)) + assert expected == im.reduce(size).size -def test_args_box(): +@pytest.mark.parametrize( + "size,error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) +) +def test_args_factor_error(size, error): im = Image.new("L", (10, 10)) - - assert (5, 5) == im.reduce(2, (0, 0, 10, 10)).size - assert (1, 1) == im.reduce(2, (5, 5, 6, 6)).size - - with pytest.raises(TypeError): - im.reduce(2, "stri") - with pytest.raises(TypeError): - im.reduce(2, 2) - with pytest.raises(ValueError): - im.reduce(2, (0, 0, 11, 10)) - with pytest.raises(ValueError): - im.reduce(2, (0, 0, 10, 11)) - with pytest.raises(ValueError): - im.reduce(2, (-1, 0, 10, 10)) - with pytest.raises(ValueError): - im.reduce(2, (0, -1, 10, 10)) - with pytest.raises(ValueError): - im.reduce(2, (0, 5, 10, 5)) - with pytest.raises(ValueError): - im.reduce(2, (5, 0, 5, 10)) + with pytest.raises(error): + im.reduce(size) -def test_unsupported_modes(): +@pytest.mark.parametrize( + "size,expected", + ( + ((0, 0, 10, 10), (5, 5)), + ((5, 5, 6, 6), (1, 1)), + ), +) +def test_args_box(size, expected): + im = Image.new("L", (10, 10)) + assert expected == im.reduce(2, size).size + + +@pytest.mark.parametrize( + "size,error", + ( + ("stri", TypeError), + ((0, 0, 11, 10), ValueError), + ((0, 0, 10, 11), ValueError), + ((-1, 0, 10, 10), ValueError), + ((0, -1, 10, 10), ValueError), + ((0, 5, 10, 5), ValueError), + ((5, 0, 5, 10), ValueError), + ), +) +def test_args_box_error(size, error): + im = Image.new("L", (10, 10)) + with pytest.raises(error): + im.reduce(2, size).size + + +@pytest.mark.parametrize("mode", ("P", "1", "I;16")) +def test_unsupported_modes(mode): im = Image.new("P", (10, 10)) with pytest.raises(ValueError): im.reduce(3) - im = Image.new("1", (10, 10)) - with pytest.raises(ValueError): - im.reduce(3) - - im = Image.new("I;16", (10, 10)) - with pytest.raises(ValueError): - im.reduce(3) - def get_image(mode): mode_info = ImageMode.getmode(mode) @@ -197,63 +203,69 @@ def test_mode_L(): compare_reduce_with_box(im, factor) -def test_mode_LA(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_LA(factor): im = get_image("LA") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor, 0.8, 5) + compare_reduce_with_reference(im, factor, 0.8, 5) + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_LA_opaque(factor): + im = get_image("LA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) -def test_mode_La(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_La(factor): im = get_image("La") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) -def test_mode_RGB(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGB(factor): im = get_image("RGB") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) -def test_mode_RGBA(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGBA(factor): im = get_image("RGBA") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor, 0.8, 5) + compare_reduce_with_reference(im, factor, 0.8, 5) + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGBA_opaque(factor): + im = get_image("RGBA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) -def test_mode_RGBa(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGBa(factor): im = get_image("RGBa") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) -def test_mode_I(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_I(factor): im = get_image("I") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) -def test_mode_F(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_F(factor): im = get_image("F") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor, 0, 0) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor, 0, 0) + compare_reduce_with_box(im, factor) @skip_unless_feature("jpg_2000") From a7f7f6ac054a15e6f88a8b8724017d3ff1ff134c Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 24 Aug 2022 07:43:49 -0500 Subject: [PATCH 071/100] parametrize tests --- Tests/test_image_transform.py | 159 +++++++++++++++++----------------- 1 file changed, 78 insertions(+), 81 deletions(-) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index ac0e74969..14ca0334a 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -75,23 +75,25 @@ class TestImageTransform: assert_image_equal(transformed, scaled) - def test_fill(self): - for mode, pixel in [ - ["RGB", (255, 0, 0)], - ["RGBA", (255, 0, 0, 255)], - ["LA", (76, 0)], - ]: - im = hopper(mode) - (w, h) = im.size - transformed = im.transform( - im.size, - Image.Transform.EXTENT, - (0, 0, w * 2, h * 2), - Image.Resampling.BILINEAR, - fillcolor="red", - ) - - assert transformed.getpixel((w - 1, h - 1)) == pixel + @pytest.mark.parametrize( + "mode,pixel", + ( + ("RGB", (255, 0, 0)), + ("RGBA", (255, 0, 0, 255)), + ("LA", (76, 0)), + ), + ) + def test_fill(self, mode, pixel): + im = hopper(mode) + (w, h) = im.size + transformed = im.transform( + im.size, + Image.Transform.EXTENT, + (0, 0, w * 2, h * 2), + Image.Resampling.BILINEAR, + fillcolor="red", + ) + assert transformed.getpixel((w - 1, h - 1)) == pixel def test_mesh(self): # this should be a checkerboard of halfsized hoppers in ul, lr @@ -222,14 +224,12 @@ class TestImageTransform: with pytest.raises(ValueError): im.transform((100, 100), None) - def test_unknown_resampling_filter(self): + @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) + def test_unknown_resampling_filter(self, resample): with hopper() as im: (w, h) = im.size - for resample in (Image.Resampling.BOX, "unknown"): - with pytest.raises(ValueError): - im.transform( - (100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample - ) + with pytest.raises(ValueError): + im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) class TestImageTransformAffine: @@ -239,7 +239,16 @@ class TestImageTransformAffine: im = hopper("RGB") return im.crop((10, 20, im.width - 10, im.height - 20)) - def _test_rotate(self, deg, transpose): + @pytest.mark.parametrize( + "deg,transpose", + ( + (0, None), + (90, Image.Transpose.ROTATE_90), + (180, Image.Transpose.ROTATE_180), + (270, Image.Transpose.ROTATE_270), + ), + ) + def test_rotate(self, deg, transpose): im = self._test_image() angle = -math.radians(deg) @@ -271,77 +280,65 @@ class TestImageTransformAffine: ) assert_image_equal(transposed, transformed) - def test_rotate_0_deg(self): - self._test_rotate(0, None) - - def test_rotate_90_deg(self): - self._test_rotate(90, Image.Transpose.ROTATE_90) - - def test_rotate_180_deg(self): - self._test_rotate(180, Image.Transpose.ROTATE_180) - - def test_rotate_270_deg(self): - self._test_rotate(270, Image.Transpose.ROTATE_270) - - def _test_resize(self, scale, epsilonscale): + @pytest.mark.parametrize( + "scale,epsilonscale", + ( + (1.1, 6.9), + (1.5, 5.5), + (2.0, 5.5), + (2.3, 3.7), + (2.5, 3.7), + ), + ) + @pytest.mark.parametrize( + "resample,epsilon", + ( + (Image.Resampling.NEAREST, 0), + (Image.Resampling.BILINEAR, 2), + (Image.Resampling.BICUBIC, 1), + ), + ) + def test_resize(self, scale, epsilonscale, resample, epsilon): im = self._test_image() size_up = int(round(im.width * scale)), int(round(im.height * scale)) matrix_up = [1 / scale, 0, 0, 0, 1 / scale, 0, 0, 0] matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0] - for resample, epsilon in [ + transformed = im.transform(size_up, self.transform, matrix_up, resample) + transformed = transformed.transform( + im.size, self.transform, matrix_down, resample + ) + assert_image_similar(transformed, im, epsilon * epsilonscale) + + @pytest.mark.parametrize( + "x,y,epsilonscale", + ( + (0.1, 0, 3.7), + (0.6, 0, 9.1), + (50, 50, 0), + ), + ) + @pytest.mark.parametrize( + "resample,epsilon", + ( (Image.Resampling.NEAREST, 0), - (Image.Resampling.BILINEAR, 2), + (Image.Resampling.BILINEAR, 1.5), (Image.Resampling.BICUBIC, 1), - ]: - transformed = im.transform(size_up, self.transform, matrix_up, resample) - transformed = transformed.transform( - im.size, self.transform, matrix_down, resample - ) - assert_image_similar(transformed, im, epsilon * epsilonscale) - - def test_resize_1_1x(self): - self._test_resize(1.1, 6.9) - - def test_resize_1_5x(self): - self._test_resize(1.5, 5.5) - - def test_resize_2_0x(self): - self._test_resize(2.0, 5.5) - - def test_resize_2_3x(self): - self._test_resize(2.3, 3.7) - - def test_resize_2_5x(self): - self._test_resize(2.5, 3.7) - - def _test_translate(self, x, y, epsilonscale): + ), + ) + def test_translate(self, x, y, epsilonscale, resample, epsilon): im = self._test_image() size_up = int(round(im.width + x)), int(round(im.height + y)) matrix_up = [1, 0, -x, 0, 1, -y, 0, 0] matrix_down = [1, 0, x, 0, 1, y, 0, 0] - for resample, epsilon in [ - (Image.Resampling.NEAREST, 0), - (Image.Resampling.BILINEAR, 1.5), - (Image.Resampling.BICUBIC, 1), - ]: - transformed = im.transform(size_up, self.transform, matrix_up, resample) - transformed = transformed.transform( - im.size, self.transform, matrix_down, resample - ) - assert_image_similar(transformed, im, epsilon * epsilonscale) - - def test_translate_0_1(self): - self._test_translate(0.1, 0, 3.7) - - def test_translate_0_6(self): - self._test_translate(0.6, 0, 9.1) - - def test_translate_50(self): - self._test_translate(50, 50, 0) + transformed = im.transform(size_up, self.transform, matrix_up, resample) + transformed = transformed.transform( + im.size, self.transform, matrix_down, resample + ) + assert_image_similar(transformed, im, epsilon * epsilonscale) class TestImageTransformPerspective(TestImageTransformAffine): From 826ab4b17c1c4622a7210b4c72d50606ed2b2d2a Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 24 Aug 2022 18:15:57 -0500 Subject: [PATCH 072/100] remove unused asserts An exception occurs before they would be checked. --- Tests/test_image_filter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index e12e73f97..1cee8d2c8 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -42,8 +42,6 @@ def test_sanity_error(mode): with pytest.raises(TypeError): im = hopper(mode) out = im.filter("hello") - assert out.mode == im.mode - assert out.size == im.size # crashes on small images From 65694f3fb82bd6b29f1b8750f730ba311e41f8e5 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 24 Aug 2022 18:21:27 -0500 Subject: [PATCH 073/100] parametrize test_rankfilter_error() --- Tests/test_image_filter.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 1cee8d2c8..ee645bd47 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -96,7 +96,8 @@ def test_rankfilter(mode, expected): assert (minimum, med, maximum) == expected -def test_rankfilter_error(): +@pytest.mark.parametrize("filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter)) +def test_rankfilter_error(filter): with pytest.raises(ValueError): im = Image.new("P", (3, 3), None) im.putdata(list(range(9))) @@ -104,9 +105,7 @@ def test_rankfilter_error(): # 0 1 2 # 3 4 5 # 6 7 8 - im.filter(ImageFilter.MinFilter).getpixel((1, 1)) - im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) - im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) + im.filter(filter).getpixel((1, 1)) def test_rankfilter_properties(): From 972961c9fec94969b6ef61a6bbd2443e467a3441 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Aug 2022 23:22:06 +0000 Subject: [PATCH 074/100] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_image_filter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index ee645bd47..ec215cd75 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -96,7 +96,9 @@ def test_rankfilter(mode, expected): assert (minimum, med, maximum) == expected -@pytest.mark.parametrize("filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter)) +@pytest.mark.parametrize( + "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter) +) def test_rankfilter_error(filter): with pytest.raises(ValueError): im = Image.new("P", (3, 3), None) From 2fd3cb55d208237a1fd3812598bb2e1cbc3d4c8a Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 24 Aug 2022 19:13:50 -0500 Subject: [PATCH 075/100] remove unused variable --- Tests/test_image_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index ec215cd75..bec7f21e9 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -41,7 +41,7 @@ def test_sanity(filter_to_apply, mode): def test_sanity_error(mode): with pytest.raises(TypeError): im = hopper(mode) - out = im.filter("hello") + im.filter("hello") # crashes on small images From aa5d67e49281b86631a5b8a7ffc42446dd834265 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 25 Aug 2022 02:57:07 +0200 Subject: [PATCH 076/100] convert TestImageFont and TestImageFont_RaqmLayout into a test fixture --- Tests/test_imagefont.py | 1740 +++++++++++++++++++-------------------- 1 file changed, 855 insertions(+), 885 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 16da87d46..f8ecc193a 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -28,497 +28,527 @@ TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" pytestmark = skip_unless_feature("freetype2") -class TestImageFont: - LAYOUT_ENGINE = ImageFont.Layout.BASIC +def test_sanity(): + assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2")) - def get_font(self): - return ImageFont.truetype( - FONT_PATH, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE - ) - def test_sanity(self): - assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2")) +@pytest.fixture( + scope="module", + params=[ + ImageFont.Layout.BASIC, + pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), + ], +) +def layout_engine(request): + return request.param - def test_font_properties(self): - ttf = self.get_font() - assert ttf.path == FONT_PATH - assert ttf.size == FONT_SIZE - ttf_copy = ttf.font_variant() - assert ttf_copy.path == FONT_PATH - assert ttf_copy.size == FONT_SIZE +@pytest.fixture(scope="module") +def font(layout_engine): + return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine) - ttf_copy = ttf.font_variant(size=FONT_SIZE + 1) - assert ttf_copy.size == FONT_SIZE + 1 - second_font_path = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" - ttf_copy = ttf.font_variant(font=second_font_path) - assert ttf_copy.path == second_font_path +def test_font_properties(font): + assert font.path == FONT_PATH + assert font.size == FONT_SIZE - def test_font_with_name(self): - self.get_font() - self._render(FONT_PATH) + font_copy = font.font_variant() + assert font_copy.path == FONT_PATH + assert font_copy.size == FONT_SIZE - def _font_as_bytes(self): + font_copy = font.font_variant(size=FONT_SIZE + 1) + assert font_copy.size == FONT_SIZE + 1 + + second_font_path = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" + font_copy = font.font_variant(font=second_font_path) + assert font_copy.path == second_font_path + + +def _render(font, layout_engine): + txt = "Hello World!" + ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine) + ttf.getbbox(txt) + + img = Image.new("RGB", (256, 64), "white") + d = ImageDraw.Draw(img) + d.text((10, 10), txt, font=ttf, fill="black") + + return img + + +def test_font_with_name(layout_engine): + _render(FONT_PATH, layout_engine) + + +def test_font_with_filelike(layout_engine): + def _font_as_bytes(): with open(FONT_PATH, "rb") as f: font_bytes = BytesIO(f.read()) return font_bytes - def test_font_with_filelike(self): - ttf = ImageFont.truetype( - self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE - ) - ttf_copy = ttf.font_variant() - assert ttf_copy.font_bytes == ttf.font_bytes + ttf = ImageFont.truetype(_font_as_bytes(), FONT_SIZE, layout_engine=layout_engine) + ttf_copy = ttf.font_variant() + assert ttf_copy.font_bytes == ttf.font_bytes - self._render(self._font_as_bytes()) - # Usage note: making two fonts from the same buffer fails. - # shared_bytes = self._font_as_bytes() - # self._render(shared_bytes) - # with pytest.raises(Exception): - # _render(shared_bytes) + _render(_font_as_bytes(), layout_engine) + # Usage note: making two fonts from the same buffer fails. + # shared_bytes = _font_as_bytes() + # _render(shared_bytes) + # with pytest.raises(Exception): + # _render(shared_bytes) - def test_font_with_open_file(self): - with open(FONT_PATH, "rb") as f: - self._render(f) - def test_non_ascii_path(self, tmp_path): - tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) - try: - shutil.copy(FONT_PATH, tempfile) - except UnicodeEncodeError: - pytest.skip("Non-ASCII path could not be created") +def test_font_with_open_file(layout_engine): + with open(FONT_PATH, "rb") as f: + _render(f, layout_engine) - ImageFont.truetype(tempfile, FONT_SIZE) - def _render(self, font): - txt = "Hello World!" - ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE) - ttf.getbbox(txt) +def test_render_equal(layout_engine): + img_path = _render(FONT_PATH, layout_engine) + with open(FONT_PATH, "rb") as f: + font_filelike = BytesIO(f.read()) + img_filelike = _render(font_filelike, layout_engine) - img = Image.new("RGB", (256, 64), "white") - d = ImageDraw.Draw(img) - d.text((10, 10), txt, font=ttf, fill="black") + assert_image_equal(img_path, img_filelike) - return img - def test_render_equal(self): - img_path = self._render(FONT_PATH) - with open(FONT_PATH, "rb") as f: - font_filelike = BytesIO(f.read()) - img_filelike = self._render(font_filelike) +def test_non_ascii_path(tmp_path, layout_engine): + tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) + try: + shutil.copy(FONT_PATH, tempfile) + except UnicodeEncodeError: + pytest.skip("Non-ASCII path could not be created") - assert_image_equal(img_path, img_filelike) + ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine) - def test_transparent_background(self): - im = Image.new(mode="RGBA", size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = self.get_font() - txt = "Hello World!" - draw.text((10, 10), txt, font=ttf) +def test_transparent_background(font): + im = Image.new(mode="RGBA", size=(300, 100)) + draw = ImageDraw.Draw(im) - target = "Tests/images/transparent_background_text.png" - assert_image_similar_tofile(im, target, 4.09) + txt = "Hello World!" + draw.text((10, 10), txt, font=font) - target = "Tests/images/transparent_background_text_L.png" - assert_image_similar_tofile(im.convert("L"), target, 0.01) + target = "Tests/images/transparent_background_text.png" + assert_image_similar_tofile(im, target, 4.09) - def test_I16(self): - im = Image.new(mode="I;16", size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = self.get_font() + target = "Tests/images/transparent_background_text_L.png" + assert_image_similar_tofile(im.convert("L"), target, 0.01) - txt = "Hello World!" - draw.text((10, 10), txt, font=ttf) - target = "Tests/images/transparent_background_text_L.png" - assert_image_similar_tofile(im.convert("L"), target, 0.01) +def test_I16(font): + im = Image.new(mode="I;16", size=(300, 100)) + draw = ImageDraw.Draw(im) - def test_textbbox_equal(self): - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = self.get_font() + txt = "Hello World!" + draw.text((10, 10), txt, font=font) - txt = "Hello World!" - bbox = draw.textbbox((10, 10), txt, ttf) - draw.text((10, 10), txt, font=ttf) - draw.rectangle(bbox) + target = "Tests/images/transparent_background_text_L.png" + assert_image_similar_tofile(im.convert("L"), target, 0.01) - assert_image_similar_tofile( - im, "Tests/images/rectangle_surrounding_text.png", 2.5 - ) - @pytest.mark.parametrize( - "text, mode, font, size, length_basic, length_raqm", - ( - # basic test - ("text", "L", "FreeMono.ttf", 15, 36, 36), - ("text", "1", "FreeMono.ttf", 15, 36, 36), - # issue 4177 - ("rrr", "L", "DejaVuSans/DejaVuSans.ttf", 18, 21, 22.21875), - ("rrr", "1", "DejaVuSans/DejaVuSans.ttf", 18, 24, 22.21875), - # test 'l' not including extra margin - # using exact value 2047 / 64 for raqm, checked with debugger - ("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), - ("ill", "1", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), - ), +def test_textbbox_equal(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + txt = "Hello World!" + bbox = draw.textbbox((10, 10), txt, font) + draw.text((10, 10), txt, font=font) + draw.rectangle(bbox) + + assert_image_similar_tofile(im, "Tests/images/rectangle_surrounding_text.png", 2.5) + + +@pytest.mark.parametrize( + "text, mode, fontname, size, length_basic, length_raqm", + ( + # basic test + ("text", "L", "FreeMono.ttf", 15, 36, 36), + ("text", "1", "FreeMono.ttf", 15, 36, 36), + # issue 4177 + ("rrr", "L", "DejaVuSans/DejaVuSans.ttf", 18, 21, 22.21875), + ("rrr", "1", "DejaVuSans/DejaVuSans.ttf", 18, 24, 22.21875), + # test 'l' not including extra margin + # using exact value 2047 / 64 for raqm, checked with debugger + ("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), + ("ill", "1", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), + ), +) +def test_getlength( + text, mode, fontname, size, layout_engine, length_basic, length_raqm +): + f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) + + im = Image.new(mode, (1, 1), 0) + d = ImageDraw.Draw(im) + + if layout_engine == ImageFont.Layout.BASIC: + length = d.textlength(text, f) + assert length == length_basic + else: + # disable kerning, kerning metrics changed + length = d.textlength(text, f, features=["-kern"]) + assert length == length_raqm + + +def test_render_multiline(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + line_spacing = font.getbbox("A")[3] + 4 + lines = TEST_TEXT.split("\n") + y = 0 + for line in lines: + draw.text((0, y), line, font=font) + y += line_spacing + + # some versions of freetype have different horizontal spacing. + # setting a tight epsilon, I'm showing the original test failure + # at epsilon = ~38. + assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) + + +def test_render_multiline_text(font): + # Test that text() correctly connects to multiline_text() + # and that align defaults to left + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), TEST_TEXT, font=font) + + assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 0.01) + + # Test that text() can pass on additional arguments + # to multiline_text() + draw.text( + (0, 0), TEST_TEXT, fill=None, font=font, anchor=None, spacing=4, align="left" ) - def test_getlength(self, text, mode, font, size, length_basic, length_raqm): - f = ImageFont.truetype( - "Tests/fonts/" + font, size, layout_engine=self.LAYOUT_ENGINE + draw.text((0, 0), TEST_TEXT, None, font, None, 4, "left") + + +@pytest.mark.parametrize( + "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) +) +def test_render_multiline_text_align(font, align, ext): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) + + assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) + + +def test_unknown_align(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + # Act/Assert + with pytest.raises(ValueError): + draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") + + +def test_draw_align(font): + im = Image.new("RGB", (300, 100), "white") + draw = ImageDraw.Draw(im) + line = "some text" + draw.text((100, 40), line, (0, 0, 0), font=font, align="left") + + +def test_multiline_size(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + with pytest.warns(DeprecationWarning) as log: + # Test that textsize() correctly connects to multiline_textsize() + assert draw.textsize(TEST_TEXT, font=font) == draw.multiline_textsize( + TEST_TEXT, font=font ) - im = Image.new(mode, (1, 1), 0) - d = ImageDraw.Draw(im) - - if self.LAYOUT_ENGINE == ImageFont.Layout.BASIC: - length = d.textlength(text, f) - assert length == length_basic - else: - # disable kerning, kerning metrics changed - length = d.textlength(text, f, features=["-kern"]) - assert length == length_raqm - - def test_render_multiline(self): - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = self.get_font() - line_spacing = ttf.getbbox("A")[3] + 4 - lines = TEST_TEXT.split("\n") - y = 0 - for line in lines: - draw.text((0, y), line, font=ttf) - y += line_spacing - - # some versions of freetype have different horizontal spacing. - # setting a tight epsilon, I'm showing the original test failure - # at epsilon = ~38. - assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) - - def test_render_multiline_text(self): - ttf = self.get_font() - - # Test that text() correctly connects to multiline_text() - # and that align defaults to left - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), TEST_TEXT, font=ttf) - - assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 0.01) - - # Test that text() can pass on additional arguments - # to multiline_text() - draw.text( - (0, 0), TEST_TEXT, fill=None, font=ttf, anchor=None, spacing=4, align="left" - ) - draw.text((0, 0), TEST_TEXT, None, ttf, None, 4, "left") - - # Test align center and right - for align, ext in {"center": "_center", "right": "_right"}.items(): - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align) - - assert_image_similar_tofile( - im, "Tests/images/multiline_text" + ext + ".png", 0.01 - ) - - def test_unknown_align(self): - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = self.get_font() - - # Act/Assert - with pytest.raises(ValueError): - draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align="unknown") - - def test_draw_align(self): - im = Image.new("RGB", (300, 100), "white") - draw = ImageDraw.Draw(im) - ttf = self.get_font() - line = "some text" - draw.text((100, 40), line, (0, 0, 0), font=ttf, align="left") - - def test_multiline_size(self): - ttf = self.get_font() - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - - with pytest.warns(DeprecationWarning) as log: - # Test that textsize() correctly connects to multiline_textsize() - assert draw.textsize(TEST_TEXT, font=ttf) == draw.multiline_textsize( - TEST_TEXT, font=ttf - ) - - # Test that multiline_textsize corresponds to ImageFont.textsize() - # for single line text - assert ttf.getsize("A") == draw.multiline_textsize("A", font=ttf) - - # Test that textsize() can pass on additional arguments - # to multiline_textsize() - draw.textsize(TEST_TEXT, font=ttf, spacing=4) - draw.textsize(TEST_TEXT, ttf, 4) - assert len(log) == 6 - - def test_multiline_bbox(self): - ttf = self.get_font() - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - - # Test that textbbox() correctly connects to multiline_textbbox() - assert draw.textbbox((0, 0), TEST_TEXT, font=ttf) == draw.multiline_textbbox( - (0, 0), TEST_TEXT, font=ttf - ) - - # Test that multiline_textbbox corresponds to ImageFont.textbbox() + # Test that multiline_textsize corresponds to ImageFont.textsize() # for single line text - assert ttf.getbbox("A") == draw.multiline_textbbox((0, 0), "A", font=ttf) + assert font.getsize("A") == draw.multiline_textsize("A", font=font) - # Test that textbbox() can pass on additional arguments - # to multiline_textbbox() - draw.textbbox((0, 0), TEST_TEXT, font=ttf, spacing=4) + # Test that textsize() can pass on additional arguments + # to multiline_textsize() + draw.textsize(TEST_TEXT, font=font, spacing=4) + draw.textsize(TEST_TEXT, font, 4) + assert len(log) == 6 - def test_multiline_width(self): - ttf = self.get_font() - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) +def test_multiline_bbox(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + # Test that textbbox() correctly connects to multiline_textbbox() + assert draw.textbbox((0, 0), TEST_TEXT, font=font) == draw.multiline_textbbox( + (0, 0), TEST_TEXT, font=font + ) + + # Test that multiline_textbbox corresponds to ImageFont.textbbox() + # for single line text + assert font.getbbox("A") == draw.multiline_textbbox((0, 0), "A", font=font) + + # Test that textbbox() can pass on additional arguments + # to multiline_textbbox() + draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) + + +def test_multiline_width(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + assert ( + draw.textbbox((0, 0), "longest line", font=font)[2] + == draw.multiline_textbbox((0, 0), "longest line\nline", font=font)[2] + ) + with pytest.warns(DeprecationWarning) as log: assert ( - draw.textbbox((0, 0), "longest line", font=ttf)[2] - == draw.multiline_textbbox((0, 0), "longest line\nline", font=ttf)[2] + draw.textsize("longest line", font=font)[0] + == draw.multiline_textsize("longest line\nline", font=font)[0] ) - with pytest.warns(DeprecationWarning) as log: - assert ( - draw.textsize("longest line", font=ttf)[0] - == draw.multiline_textsize("longest line\nline", font=ttf)[0] - ) - assert len(log) == 2 + assert len(log) == 2 - def test_multiline_spacing(self): - ttf = self.get_font() - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10) +def test_multiline_spacing(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) - assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 2.5) + assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 2.5) - def test_rotated_transposed_font(self): - img_grey = Image.new("L", (100, 100)) - draw = ImageDraw.Draw(img_grey) - word = "testing" - font = self.get_font() - orientation = Image.Transpose.ROTATE_90 - transposed_font = ImageFont.TransposedFont(font, orientation=orientation) +@pytest.mark.parametrize( + "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) +) +def test_rotated_transposed_font(font, orientation): + img_grey = Image.new("L", (100, 100)) + draw = ImageDraw.Draw(img_grey) + word = "testing" - # Original font - draw.font = font - with pytest.warns(DeprecationWarning) as log: - box_size_a = draw.textsize(word) - assert box_size_a == font.getsize(word) - assert len(log) == 2 - bbox_a = draw.textbbox((10, 10), word) + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - # Rotated font - draw.font = transposed_font - with pytest.warns(DeprecationWarning) as log: - box_size_b = draw.textsize(word) - assert box_size_b == transposed_font.getsize(word) - assert len(log) == 2 - bbox_b = draw.textbbox((20, 20), word) + # Original font + draw.font = font + with pytest.warns(DeprecationWarning) as log: + box_size_a = draw.textsize(word) + assert box_size_a == font.getsize(word) + assert len(log) == 2 + bbox_a = draw.textbbox((10, 10), word) - # Check (w,h) of box a is (h,w) of box b - assert box_size_a[0] == box_size_b[1] - assert box_size_a[1] == box_size_b[0] + # Rotated font + draw.font = transposed_font + with pytest.warns(DeprecationWarning) as log: + box_size_b = draw.textsize(word) + assert box_size_b == transposed_font.getsize(word) + assert len(log) == 2 + bbox_b = draw.textbbox((20, 20), word) - # Check bbox b is (20, 20, 20 + h, 20 + w) - assert bbox_b[0] == 20 - assert bbox_b[1] == 20 - assert bbox_b[2] == 20 + bbox_a[3] - bbox_a[1] - assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0] + # Check (w,h) of box a is (h,w) of box b + assert box_size_a[0] == box_size_b[1] + assert box_size_a[1] == box_size_b[0] - # text length is undefined for vertical text - pytest.raises(ValueError, draw.textlength, word) + # Check bbox b is (20, 20, 20 + h, 20 + w) + assert bbox_b[0] == 20 + assert bbox_b[1] == 20 + assert bbox_b[2] == 20 + bbox_a[3] - bbox_a[1] + assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0] - def test_unrotated_transposed_font(self): - img_grey = Image.new("L", (100, 100)) - draw = ImageDraw.Draw(img_grey) - word = "testing" - font = self.get_font() + # text length is undefined for vertical text + pytest.raises(ValueError, draw.textlength, word) - orientation = None - transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - # Original font - draw.font = font - with pytest.warns(DeprecationWarning) as log: - box_size_a = draw.textsize(word) - assert len(log) == 1 - bbox_a = draw.textbbox((10, 10), word) - length_a = draw.textlength(word) +@pytest.mark.parametrize( + "orientation", + ( + None, + Image.Transpose.ROTATE_180, + Image.Transpose.FLIP_LEFT_RIGHT, + Image.Transpose.FLIP_TOP_BOTTOM, + ), +) +def test_unrotated_transposed_font(font, orientation): + img_grey = Image.new("L", (100, 100)) + draw = ImageDraw.Draw(img_grey) + word = "testing" - # Rotated font - draw.font = transposed_font - with pytest.warns(DeprecationWarning) as log: - box_size_b = draw.textsize(word) - assert len(log) == 1 - bbox_b = draw.textbbox((20, 20), word) - length_b = draw.textlength(word) + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - # Check boxes a and b are same size - assert box_size_a == box_size_b + # Original font + draw.font = font + with pytest.warns(DeprecationWarning) as log: + box_size_a = draw.textsize(word) + assert len(log) == 1 + bbox_a = draw.textbbox((10, 10), word) + length_a = draw.textlength(word) - # Check bbox b is (20, 20, 20 + w, 20 + h) - assert bbox_b[0] == 20 - assert bbox_b[1] == 20 - assert bbox_b[2] == 20 + bbox_a[2] - bbox_a[0] - assert bbox_b[3] == 20 + bbox_a[3] - bbox_a[1] + # Rotated font + draw.font = transposed_font + with pytest.warns(DeprecationWarning) as log: + box_size_b = draw.textsize(word) + assert len(log) == 1 + bbox_b = draw.textbbox((20, 20), word) + length_b = draw.textlength(word) - assert length_a == length_b + # Check boxes a and b are same size + assert box_size_a == box_size_b - def test_rotated_transposed_font_get_mask(self): - # Arrange - text = "mask this" - font = self.get_font() - orientation = Image.Transpose.ROTATE_90 - transposed_font = ImageFont.TransposedFont(font, orientation=orientation) + # Check bbox b is (20, 20, 20 + w, 20 + h) + assert bbox_b[0] == 20 + assert bbox_b[1] == 20 + assert bbox_b[2] == 20 + bbox_a[2] - bbox_a[0] + assert bbox_b[3] == 20 + bbox_a[3] - bbox_a[1] - # Act - mask = transposed_font.getmask(text) + assert length_a == length_b - # Assert - assert mask.size == (13, 108) - def test_unrotated_transposed_font_get_mask(self): - # Arrange - text = "mask this" - font = self.get_font() - orientation = None - transposed_font = ImageFont.TransposedFont(font, orientation=orientation) +@pytest.mark.parametrize( + "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) +) +def test_rotated_transposed_font_get_mask(font, orientation): + # Arrange + text = "mask this" + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - # Act - mask = transposed_font.getmask(text) + # Act + mask = transposed_font.getmask(text) - # Assert - assert mask.size == (108, 13) + # Assert + assert mask.size == (13, 108) - def test_free_type_font_get_name(self): - # Arrange - font = self.get_font() - # Act - name = font.getname() +@pytest.mark.parametrize( + "orientation", + ( + None, + Image.Transpose.ROTATE_180, + Image.Transpose.FLIP_LEFT_RIGHT, + Image.Transpose.FLIP_TOP_BOTTOM, + ), +) +def test_unrotated_transposed_font_get_mask(font, orientation): + # Arrange + text = "mask this" + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - # Assert - assert ("FreeMono", "Regular") == name + # Act + mask = transposed_font.getmask(text) - def test_free_type_font_get_metrics(self): - # Arrange - font = self.get_font() + # Assert + assert mask.size == (108, 13) - # Act - ascent, descent = font.getmetrics() - # Assert - assert isinstance(ascent, int) - assert isinstance(descent, int) - assert (ascent, descent) == (16, 4) # too exact check? +def test_free_type_font_get_name(font): + assert ("FreeMono", "Regular") == font.getname() - def test_free_type_font_get_offset(self): - # Arrange - font = self.get_font() - text = "offset this" - # Act - with pytest.warns(DeprecationWarning) as log: - offset = font.getoffset(text) +def test_free_type_font_get_metrics(font): + ascent, descent = font.getmetrics() - # Assert - assert len(log) == 1 - assert offset == (0, 3) + assert isinstance(ascent, int) + assert isinstance(descent, int) + assert (ascent, descent) == (16, 4) - def test_free_type_font_get_mask(self): - # Arrange - font = self.get_font() - text = "mask this" - # Act - mask = font.getmask(text) +def test_free_type_font_get_offset(font): + # Arrange + text = "offset this" - # Assert - assert mask.size == (108, 13) + # Act + with pytest.warns(DeprecationWarning) as log: + offset = font.getoffset(text) - def test_load_path_not_found(self): - # Arrange - filename = "somefilenamethatdoesntexist.ttf" + # Assert + assert len(log) == 1 + assert offset == (0, 3) - # Act/Assert + +def test_free_type_font_get_mask(font): + # Arrange + text = "mask this" + + # Act + mask = font.getmask(text) + + # Assert + assert mask.size == (108, 13) + + +def test_load_path_not_found(): + # Arrange + filename = "somefilenamethatdoesntexist.ttf" + + # Act/Assert + with pytest.raises(OSError): + ImageFont.load_path(filename) + with pytest.raises(OSError): + ImageFont.truetype(filename) + + +def test_load_non_font_bytes(): + with open("Tests/images/hopper.jpg", "rb") as f: with pytest.raises(OSError): - ImageFont.load_path(filename) - with pytest.raises(OSError): - ImageFont.truetype(filename) + ImageFont.truetype(f) - def test_load_non_font_bytes(self): - with open("Tests/images/hopper.jpg", "rb") as f: - with pytest.raises(OSError): - ImageFont.truetype(f) - def test_default_font(self): - # Arrange - txt = 'This is a "better than nothing" default font.' - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) +def test_default_font(): + # Arrange + txt = 'This is a "better than nothing" default font.' + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) - # Act - default_font = ImageFont.load_default() - draw.text((10, 10), txt, font=default_font) + # Act + default_font = ImageFont.load_default() + draw.text((10, 10), txt, font=default_font) - # Assert - assert_image_equal_tofile(im, "Tests/images/default_font.png") + # Assert + assert_image_equal_tofile(im, "Tests/images/default_font.png") - def test_getbbox_empty(self): - # issue #2614 - font = self.get_font() - # should not crash. - assert (0, 0, 0, 0) == font.getbbox("") - def test_render_empty(self): - # issue 2666 - font = self.get_font() - im = Image.new(mode="RGB", size=(300, 100)) - target = im.copy() - draw = ImageDraw.Draw(im) - # should not crash here. - draw.text((10, 10), "", font=font) - assert_image_equal(im, target) +def test_getbbox_empty(font): + # issue #2614, should not crash. + assert (0, 0, 0, 0) == font.getbbox("") - def test_unicode_pilfont(self): - # should not segfault, should return UnicodeDecodeError - # issue #2826 - font = ImageFont.load_default() - with pytest.raises(UnicodeEncodeError): - font.getbbox("’") - def test_unicode_extended(self): - # issue #3777 - text = "A\u278A\U0001F12B" - target = "Tests/images/unicode_extended.png" +def test_render_empty(font): + # issue 2666 + im = Image.new(mode="RGB", size=(300, 100)) + target = im.copy() + draw = ImageDraw.Draw(im) + # should not crash here. + draw.text((10, 10), "", font=font) + assert_image_equal(im, target) - ttf = ImageFont.truetype( - "Tests/fonts/NotoSansSymbols-Regular.ttf", - FONT_SIZE, - layout_engine=self.LAYOUT_ENGINE, - ) - img = Image.new("RGB", (100, 60)) - d = ImageDraw.Draw(img) - d.text((10, 10), text, font=ttf) - # fails with 14.7 - assert_image_similar_tofile(img, target, 6.2) +def test_unicode_pilfont(): + # should not segfault, should return UnicodeDecodeError + # issue #2826 + font = ImageFont.load_default() + with pytest.raises(UnicodeEncodeError): + font.getbbox("’") - def _test_fake_loading_font(self, monkeypatch, path_to_fake, fontname): + +def test_unicode_extended(layout_engine): + # issue #3777 + text = "A\u278A\U0001F12B" + target = "Tests/images/unicode_extended.png" + + ttf = ImageFont.truetype( + "Tests/fonts/NotoSansSymbols-Regular.ttf", + FONT_SIZE, + layout_engine=layout_engine, + ) + img = Image.new("RGB", (100, 60)) + d = ImageDraw.Draw(img) + d.text((10, 10), text, font=ttf) + + # fails with 14.7 + assert_image_similar_tofile(img, target, 6.2) + + +@pytest.mark.parametrize( + "platform, font_directory", + (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), +) +@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") +def test_find_font(monkeypatch, platform, font_directory): + def _test_fake_loading_font(path_to_fake, fontname): # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) with monkeypatch.context() as m: @@ -539,543 +569,483 @@ class TestImageFont: name = font.getname() assert ("FreeMono", "Regular") == name - @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") - def test_find_linux_font(self, monkeypatch): - # A lot of mocking here - this is more for hitting code and - # catching syntax like errors - font_directory = "/usr/local/share/fonts" - monkeypatch.setattr(sys, "platform", "linux") + # A lot of mocking here - this is more for hitting code and + # catching syntax like errors + monkeypatch.setattr(sys, "platform", platform) + if platform == "linux": monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") - def fake_walker(path): - if path == font_directory: - return [ - ( - path, - [], - ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"], - ) - ] - return [(path, [], ["some_random_font.ttf"])] + def fake_walker(path): + if path == font_directory: + return [ + ( + path, + [], + ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"], + ) + ] + return [(path, [], ["some_random_font.ttf"])] - monkeypatch.setattr(os, "walk", fake_walker) - # Test that the font loads both with and without the - # extension - self._test_fake_loading_font( - monkeypatch, font_directory + "/Arial.ttf", "Arial.ttf" - ) - self._test_fake_loading_font( - monkeypatch, font_directory + "/Arial.ttf", "Arial" - ) + monkeypatch.setattr(os, "walk", fake_walker) - # Test that non-ttf fonts can be found without the - # extension - self._test_fake_loading_font( - monkeypatch, font_directory + "/Single.otf", "Single" - ) + # Test that the font loads both with and without the extension + _test_fake_loading_font(font_directory + "/Arial.ttf", "Arial.ttf") + _test_fake_loading_font(font_directory + "/Arial.ttf", "Arial") - # Test that ttf fonts are preferred if the extension is - # not specified - self._test_fake_loading_font( - monkeypatch, font_directory + "/Duplicate.ttf", "Duplicate" - ) + # Test that non-ttf fonts can be found without the extension + _test_fake_loading_font(font_directory + "/Single.otf", "Single") - @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") - def test_find_macos_font(self, monkeypatch): - # Like the linux test, more cover hitting code rather than testing - # correctness. - font_directory = "/System/Library/Fonts" - monkeypatch.setattr(sys, "platform", "darwin") + # Test that ttf fonts are preferred if the extension is not specified + _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") - def fake_walker(path): - if path == font_directory: - return [ - ( - path, - [], - ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"], - ) - ] - return [(path, [], ["some_random_font.ttf"])] - monkeypatch.setattr(os, "walk", fake_walker) - self._test_fake_loading_font( - monkeypatch, font_directory + "/Arial.ttf", "Arial.ttf" - ) - self._test_fake_loading_font( - monkeypatch, font_directory + "/Arial.ttf", "Arial" - ) - self._test_fake_loading_font( - monkeypatch, font_directory + "/Single.otf", "Single" - ) - self._test_fake_loading_font( - monkeypatch, font_directory + "/Duplicate.ttf", "Duplicate" - ) +def test_imagefont_getters(font): + assert font.getmetrics() == (16, 4) + assert font.font.ascent == 16 + assert font.font.descent == 4 + assert font.font.height == 20 + assert font.font.x_ppem == 20 + assert font.font.y_ppem == 20 + assert font.font.glyphs == 4177 + assert font.getbbox("A") == (0, 4, 12, 16) + assert font.getbbox("AB") == (0, 4, 24, 16) + assert font.getbbox("M") == (0, 4, 12, 16) + assert font.getbbox("y") == (0, 7, 12, 20) + assert font.getbbox("a") == (0, 7, 12, 16) + assert font.getlength("A") == 12 + assert font.getlength("AB") == 24 + assert font.getlength("M") == 12 + assert font.getlength("y") == 12 + assert font.getlength("a") == 12 + with pytest.warns(DeprecationWarning) as log: + assert font.getsize("A") == (12, 16) + assert font.getsize("AB") == (24, 16) + assert font.getsize("M") == (12, 16) + assert font.getsize("y") == (12, 20) + assert font.getsize("a") == (12, 16) + assert font.getsize_multiline("A") == (12, 16) + assert font.getsize_multiline("AB") == (24, 16) + assert font.getsize_multiline("a") == (12, 16) + assert font.getsize_multiline("ABC\n") == (36, 36) + assert font.getsize_multiline("ABC\nA") == (36, 36) + assert font.getsize_multiline("ABC\nAaaa") == (48, 36) + assert len(log) == 11 - def test_imagefont_getters(self): - # Arrange - t = self.get_font() - # Act / Assert - assert t.getmetrics() == (16, 4) - assert t.font.ascent == 16 - assert t.font.descent == 4 - assert t.font.height == 20 - assert t.font.x_ppem == 20 - assert t.font.y_ppem == 20 - assert t.font.glyphs == 4177 - assert t.getbbox("A") == (0, 4, 12, 16) - assert t.getbbox("AB") == (0, 4, 24, 16) - assert t.getbbox("M") == (0, 4, 12, 16) - assert t.getbbox("y") == (0, 7, 12, 20) - assert t.getbbox("a") == (0, 7, 12, 16) - assert t.getlength("A") == 12 - assert t.getlength("AB") == 24 - assert t.getlength("M") == 12 - assert t.getlength("y") == 12 - assert t.getlength("a") == 12 +def test_getsize_stroke(font): + for stroke_width in [0, 2]: + assert font.getbbox("A", stroke_width=stroke_width) == ( + 0 - stroke_width, + 4 - stroke_width, + 12 + stroke_width, + 16 + stroke_width, + ) with pytest.warns(DeprecationWarning) as log: - assert t.getsize("A") == (12, 16) - assert t.getsize("AB") == (24, 16) - assert t.getsize("M") == (12, 16) - assert t.getsize("y") == (12, 20) - assert t.getsize("a") == (12, 16) - assert t.getsize_multiline("A") == (12, 16) - assert t.getsize_multiline("AB") == (24, 16) - assert t.getsize_multiline("a") == (12, 16) - assert t.getsize_multiline("ABC\n") == (36, 36) - assert t.getsize_multiline("ABC\nA") == (36, 36) - assert t.getsize_multiline("ABC\nAaaa") == (48, 36) - assert len(log) == 11 - - def test_getsize_stroke(self): - # Arrange - t = self.get_font() - - # Act / Assert - for stroke_width in [0, 2]: - assert t.getbbox("A", stroke_width=stroke_width) == ( - 0 - stroke_width, - 4 - stroke_width, - 12 + stroke_width, - 16 + stroke_width, + assert font.getsize("A", stroke_width=stroke_width) == ( + 12 + stroke_width * 2, + 16 + stroke_width * 2, ) - with pytest.warns(DeprecationWarning) as log: - assert t.getsize("A", stroke_width=stroke_width) == ( - 12 + stroke_width * 2, - 16 + stroke_width * 2, - ) - assert t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == ( - 48 + stroke_width * 2, - 36 + stroke_width * 4, - ) - assert len(log) == 2 + assert font.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == ( + 48 + stroke_width * 2, + 36 + stroke_width * 4, + ) + assert len(log) == 2 - def test_complex_font_settings(self): - # Arrange - t = self.get_font() - # Act / Assert - if t.layout_engine == ImageFont.Layout.BASIC: - with pytest.raises(KeyError): - t.getmask("абвг", direction="rtl") - with pytest.raises(KeyError): - t.getmask("абвг", features=["-kern"]) - with pytest.raises(KeyError): - t.getmask("абвг", language="sr") - def test_variation_get(self): - font = self.get_font() +def test_complex_font_settings(): + t = ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.BASIC) + with pytest.raises(KeyError): + t.getmask("абвг", direction="rtl") + with pytest.raises(KeyError): + t.getmask("абвг", features=["-kern"]) + with pytest.raises(KeyError): + t.getmask("абвг", language="sr") - freetype = parse_version(features.version_module("freetype2")) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.get_variation_names() - with pytest.raises(NotImplementedError): - font.get_variation_axes() - return - with pytest.raises(OSError): +def test_variation_get(font): + freetype = parse_version(features.version_module("freetype2")) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): font.get_variation_names() - with pytest.raises(OSError): + with pytest.raises(NotImplementedError): font.get_variation_axes() + return - font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf") - assert font.get_variation_names(), [ - b"ExtraLight", - b"Light", - b"Regular", - b"Semibold", - b"Bold", - b"Black", - b"Black Medium Contrast", - b"Black High Contrast", - b"Default", - ] - assert font.get_variation_axes() == [ - {"name": b"Weight", "minimum": 200, "maximum": 900, "default": 389}, - {"name": b"Contrast", "minimum": 0, "maximum": 100, "default": 0}, - ] + with pytest.raises(OSError): + font.get_variation_names() + with pytest.raises(OSError): + font.get_variation_axes() - font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf") - assert font.get_variation_names() == [ - b"20", - b"40", - b"60", - b"80", - b"100", - b"120", - b"140", - b"160", - b"180", - b"200", - b"220", - b"240", - b"260", - b"280", - b"300", - b"Regular", - ] - assert font.get_variation_axes() == [ - {"name": b"Size", "minimum": 0, "maximum": 300, "default": 0} - ] + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf") + assert font.get_variation_names(), [ + b"ExtraLight", + b"Light", + b"Regular", + b"Semibold", + b"Bold", + b"Black", + b"Black Medium Contrast", + b"Black High Contrast", + b"Default", + ] + assert font.get_variation_axes() == [ + {"name": b"Weight", "minimum": 200, "maximum": 900, "default": 389}, + {"name": b"Contrast", "minimum": 0, "maximum": 100, "default": 0}, + ] - def _check_text(self, font, path, epsilon): - im = Image.new("RGB", (100, 75), "white") - d = ImageDraw.Draw(im) - d.text((10, 10), "Text", font=font, fill="black") + font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf") + assert font.get_variation_names() == [ + b"20", + b"40", + b"60", + b"80", + b"100", + b"120", + b"140", + b"160", + b"180", + b"200", + b"220", + b"240", + b"260", + b"280", + b"300", + b"Regular", + ] + assert font.get_variation_axes() == [ + {"name": b"Size", "minimum": 0, "maximum": 300, "default": 0} + ] - try: + +def _check_text(font, path, epsilon): + im = Image.new("RGB", (100, 75), "white") + d = ImageDraw.Draw(im) + d.text((10, 10), "Text", font=font, fill="black") + + try: + assert_image_similar_tofile(im, path, epsilon) + except AssertionError: + if "_adobe" in path: + path = path.replace("_adobe", "_adobe_older_harfbuzz") assert_image_similar_tofile(im, path, epsilon) - except AssertionError: - if "_adobe" in path: - path = path.replace("_adobe", "_adobe_older_harfbuzz") - assert_image_similar_tofile(im, path, epsilon) - else: - raise - - def test_variation_set_by_name(self): - font = self.get_font() - - freetype = parse_version(features.version_module("freetype2")) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.set_variation_by_name("Bold") - return - - with pytest.raises(OSError): - font.set_variation_by_name("Bold") - - font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) - self._check_text(font, "Tests/images/variation_adobe.png", 11) - for name in ["Bold", b"Bold"]: - font.set_variation_by_name(name) - self._check_text(font, "Tests/images/variation_adobe_name.png", 11) - - font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) - self._check_text(font, "Tests/images/variation_tiny.png", 40) - for name in ["200", b"200"]: - font.set_variation_by_name(name) - self._check_text(font, "Tests/images/variation_tiny_name.png", 40) - - def test_variation_set_by_axes(self): - font = self.get_font() - - freetype = parse_version(features.version_module("freetype2")) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.set_variation_by_axes([100]) - return - - with pytest.raises(OSError): - font.set_variation_by_axes([500, 50]) - - font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) - font.set_variation_by_axes([500, 50]) - self._check_text(font, "Tests/images/variation_adobe_axes.png", 11.05) - - font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) - font.set_variation_by_axes([100]) - self._check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) - - def test_textbbox_non_freetypefont(self): - im = Image.new("RGB", (200, 200)) - d = ImageDraw.Draw(im) - default_font = ImageFont.load_default() - with pytest.warns(DeprecationWarning) as log: - width, height = d.textsize("test", font=default_font) - assert len(log) == 1 - assert d.textlength("test", font=default_font) == width - assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, width, height) - - @pytest.mark.parametrize( - "anchor, left, top", - ( - # test horizontal anchors - ("ls", 0, -36), - ("ms", -64, -36), - ("rs", -128, -36), - # test vertical anchors - ("ma", -64, 16), - ("mt", -64, 0), - ("mm", -64, -17), - ("mb", -64, -44), - ("md", -64, -51), - ), - ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), - ) - def test_anchor(self, anchor, left, top): - name, text = "quick", "Quick" - path = f"Tests/images/test_anchor_{name}_{anchor}.png" - - if self.LAYOUT_ENGINE == ImageFont.Layout.RAQM: - width, height = (129, 44) else: - width, height = (128, 44) + raise - bbox_expected = (left, top, left + width, top + height) - f = ImageFont.truetype( - "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE - ) +def test_variation_set_by_name(font): + freetype = parse_version(features.version_module("freetype2")) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): + font.set_variation_by_name("Bold") + return - im = Image.new("RGB", (200, 200), "white") - d = ImageDraw.Draw(im) - d.line(((0, 100), (200, 100)), "gray") - d.line(((100, 0), (100, 200)), "gray") - d.text((100, 100), text, fill="black", anchor=anchor, font=f) + with pytest.raises(OSError): + font.set_variation_by_name("Bold") - assert d.textbbox((0, 0), text, f, anchor=anchor) == bbox_expected + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) + _check_text(font, "Tests/images/variation_adobe.png", 11) + for name in ["Bold", b"Bold"]: + font.set_variation_by_name(name) + _check_text(font, "Tests/images/variation_adobe_name.png", 11) - assert_image_similar_tofile(im, path, 7) + font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) + _check_text(font, "Tests/images/variation_tiny.png", 40) + for name in ["200", b"200"]: + font.set_variation_by_name(name) + _check_text(font, "Tests/images/variation_tiny_name.png", 40) - @pytest.mark.parametrize( - "anchor, align", - ( - # test horizontal anchors - ("lm", "left"), - ("lm", "center"), - ("lm", "right"), - ("mm", "left"), - ("mm", "center"), - ("mm", "right"), - ("rm", "left"), - ("rm", "center"), - ("rm", "right"), - # test vertical anchors - ("ma", "center"), - # ("mm", "center"), # duplicate - ("md", "center"), - ), + +def test_variation_set_by_axes(font): + freetype = parse_version(features.version_module("freetype2")) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): + font.set_variation_by_axes([100]) + return + + with pytest.raises(OSError): + font.set_variation_by_axes([500, 50]) + + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) + font.set_variation_by_axes([500, 50]) + _check_text(font, "Tests/images/variation_adobe_axes.png", 11.05) + + font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) + font.set_variation_by_axes([100]) + _check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) + + +def test_textbbox_non_freetypefont(): + im = Image.new("RGB", (200, 200)) + d = ImageDraw.Draw(im) + default_font = ImageFont.load_default() + with pytest.warns(DeprecationWarning) as log: + width, height = d.textsize("test", font=default_font) + assert len(log) == 1 + assert d.textlength("test", font=default_font) == width + assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, width, height) + + +@pytest.mark.parametrize( + "anchor, left, top", + ( + # test horizontal anchors + ("ls", 0, -36), + ("ms", -64, -36), + ("rs", -128, -36), + # test vertical anchors + ("ma", -64, 16), + ("mt", -64, 0), + ("mm", -64, -17), + ("mb", -64, -44), + ("md", -64, -51), + ), + ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), +) +def test_anchor(layout_engine, anchor, left, top): + name, text = "quick", "Quick" + path = f"Tests/images/test_anchor_{name}_{anchor}.png" + + if layout_engine == ImageFont.Layout.RAQM: + width, height = (129, 44) + else: + width, height = (128, 44) + + bbox_expected = (left, top, left + width, top + height) + + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=layout_engine ) - def test_anchor_multiline(self, anchor, align): - target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" - text = "a\nlong\ntext sample" - f = ImageFont.truetype( - "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE + im = Image.new("RGB", (200, 200), "white") + d = ImageDraw.Draw(im) + d.line(((0, 100), (200, 100)), "gray") + d.line(((100, 0), (100, 200)), "gray") + d.text((100, 100), text, fill="black", anchor=anchor, font=f) + + assert d.textbbox((0, 0), text, f, anchor=anchor) == bbox_expected + + assert_image_similar_tofile(im, path, 7) + + +@pytest.mark.parametrize( + "anchor, align", + ( + # test horizontal anchors + ("lm", "left"), + ("lm", "center"), + ("lm", "right"), + ("mm", "left"), + ("mm", "center"), + ("mm", "right"), + ("rm", "left"), + ("rm", "center"), + ("rm", "right"), + # test vertical anchors + ("ma", "center"), + # ("mm", "center"), # duplicate + ("md", "center"), + ), +) +def test_anchor_multiline(layout_engine, anchor, align): + target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" + text = "a\nlong\ntext sample" + + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=layout_engine + ) + + # test render + im = Image.new("RGB", (600, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (600, 200)), "gray") + d.line(((300, 0), (300, 400)), "gray") + d.multiline_text((300, 200), text, fill="black", anchor=anchor, font=f, align=align) + + assert_image_similar_tofile(im, target, 4) + + +def test_anchor_invalid(font): + im = Image.new("RGB", (100, 100), "white") + d = ImageDraw.Draw(im) + d.font = font + + for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]: + pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor)) + pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor)) + pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor)) + pytest.raises(ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor)) + pytest.raises( + ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), + ) + for anchor in ["lt", "lb"]: + pytest.raises( + ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), ) - # test render - im = Image.new("RGB", (600, 400), "white") - d = ImageDraw.Draw(im) - d.line(((0, 200), (600, 200)), "gray") - d.line(((300, 0), (300, 400)), "gray") - d.multiline_text( - (300, 200), text, fill="black", anchor=anchor, font=f, align=align - ) - assert_image_similar_tofile(im, target, 4) +@pytest.mark.parametrize("bpp", (1, 2, 4, 8)) +def test_bitmap_font(layout_engine, bpp): + text = "Bitmap Font" + layout_name = ["basic", "raqm"][layout_engine] + target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" + font = ImageFont.truetype( + f"Tests/fonts/DejaVuSans/DejaVuSans-24-{bpp}-stripped.ttf", + 24, + layout_engine=layout_engine, + ) - def test_anchor_invalid(self): - font = self.get_font() - im = Image.new("RGB", (100, 100), "white") - d = ImageDraw.Draw(im) - d.font = font + im = Image.new("RGB", (160, 35), "white") + draw = ImageDraw.Draw(im) + draw.text((2, 2), text, "black", font) - for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]: - pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor)) - pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor)) - pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor)) - pytest.raises( - ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor) - ) - pytest.raises( - ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), - ) - for anchor in ["lt", "lb"]: - pytest.raises( - ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), - ) + assert_image_equal_tofile(im, target) - @skip_unless_feature("freetype2") - @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) - def test_bitmap_font(self, bpp): - text = "Bitmap Font" - layout_name = ["basic", "raqm"][self.LAYOUT_ENGINE] - target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" + +def test_bitmap_font_stroke(layout_engine): + text = "Bitmap Font" + layout_name = ["basic", "raqm"][layout_engine] + target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" + font = ImageFont.truetype( + "Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf", + 24, + layout_engine=layout_engine, + ) + + im = Image.new("RGB", (160, 35), "white") + draw = ImageDraw.Draw(im) + draw.text((2, 2), text, "black", font, stroke_width=2, stroke_fill="red") + + assert_image_similar_tofile(im, target, 0.03) + + +def test_standard_embedded_color(layout_engine): + txt = "Hello World!" + ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) + ttf.getbbox(txt) + + im = Image.new("RGB", (300, 64), "white") + d = ImageDraw.Draw(im) + d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True) + + assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 6.2) + + +def test_cbdt(layout_engine): + try: font = ImageFont.truetype( - f"Tests/fonts/DejaVuSans/DejaVuSans-24-{bpp}-stripped.ttf", - 24, - layout_engine=self.LAYOUT_ENGINE, + "Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine ) - im = Image.new("RGB", (160, 35), "white") - draw = ImageDraw.Draw(im) - draw.text((2, 2), text, "black", font) - - assert_image_equal_tofile(im, target) - - def test_bitmap_font_stroke(self): - text = "Bitmap Font" - layout_name = ["basic", "raqm"][self.LAYOUT_ENGINE] - target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" - font = ImageFont.truetype( - "Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf", - 24, - layout_engine=self.LAYOUT_ENGINE, - ) - - im = Image.new("RGB", (160, 35), "white") - draw = ImageDraw.Draw(im) - draw.text((2, 2), text, "black", font, stroke_width=2, stroke_fill="red") - - assert_image_similar_tofile(im, target, 0.03) - - def test_standard_embedded_color(self): - txt = "Hello World!" - ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=self.LAYOUT_ENGINE) - ttf.getbbox(txt) - - im = Image.new("RGB", (300, 64), "white") - d = ImageDraw.Draw(im) - d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True) - - assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 6.2) - - def test_cbdt(self): - try: - font = ImageFont.truetype( - "Tests/fonts/NotoColorEmoji.ttf", - size=109, - layout_engine=self.LAYOUT_ENGINE, - ) - - im = Image.new("RGB", (150, 150), "white") - d = ImageDraw.Draw(im) - - d.text((10, 10), "\U0001f469", font=font, embedded_color=True) - - assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2) - except OSError as e: # pragma: no cover - assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or CBDT support") - - def test_cbdt_mask(self): - try: - font = ImageFont.truetype( - "Tests/fonts/NotoColorEmoji.ttf", - size=109, - layout_engine=self.LAYOUT_ENGINE, - ) - - im = Image.new("RGB", (150, 150), "white") - d = ImageDraw.Draw(im) - - d.text((10, 10), "\U0001f469", "black", font=font) - - assert_image_similar_tofile( - im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2 - ) - except OSError as e: # pragma: no cover - assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or CBDT support") - - def test_sbix(self): - try: - font = ImageFont.truetype( - "Tests/fonts/chromacheck-sbix.woff", - size=300, - layout_engine=self.LAYOUT_ENGINE, - ) - - im = Image.new("RGB", (400, 400), "white") - d = ImageDraw.Draw(im) - - d.text((50, 50), "\uE901", font=font, embedded_color=True) - - assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) - except OSError as e: # pragma: no cover - assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or SBIX support") - - def test_sbix_mask(self): - try: - font = ImageFont.truetype( - "Tests/fonts/chromacheck-sbix.woff", - size=300, - layout_engine=self.LAYOUT_ENGINE, - ) - - im = Image.new("RGB", (400, 400), "white") - d = ImageDraw.Draw(im) - - d.text((50, 50), "\uE901", (100, 0, 0), font=font) - - assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) - except OSError as e: # pragma: no cover - assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or SBIX support") - - @skip_unless_feature_version("freetype2", "2.10.0") - def test_colr(self): - font = ImageFont.truetype( - "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", - size=64, - layout_engine=self.LAYOUT_ENGINE, - ) - - im = Image.new("RGB", (300, 75), "white") + im = Image.new("RGB", (150, 150), "white") d = ImageDraw.Draw(im) - d.text((15, 5), "Bungee", font=font, embedded_color=True) + d.text((10, 10), "\U0001f469", font=font, embedded_color=True) - assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21) + assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2) + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or CBDT support") - @skip_unless_feature_version("freetype2", "2.10.0") - def test_colr_mask(self): + +def test_cbdt_mask(layout_engine): + try: font = ImageFont.truetype( - "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", - size=64, - layout_engine=self.LAYOUT_ENGINE, + "Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine ) - im = Image.new("RGB", (300, 75), "white") + im = Image.new("RGB", (150, 150), "white") d = ImageDraw.Draw(im) - d.text((15, 5), "Bungee", "black", font=font) + d.text((10, 10), "\U0001f469", "black", font=font) - assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) - - def test_fill_deprecation(self): - font = self.get_font() - with pytest.warns(DeprecationWarning): - font.getmask2("Hello world", fill=Image.core.fill) - with pytest.warns(DeprecationWarning): - with pytest.raises(TypeError): - font.getmask2("Hello world", fill=None) + assert_image_similar_tofile( + im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2 + ) + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or CBDT support") -@skip_unless_feature("raqm") -class TestImageFont_RaqmLayout(TestImageFont): - LAYOUT_ENGINE = ImageFont.Layout.RAQM +def test_sbix(layout_engine): + try: + font = ImageFont.truetype( + "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine + ) + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + + d.text((50, 50), "\uE901", font=font, embedded_color=True) + + assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or SBIX support") + + +def test_sbix_mask(layout_engine): + try: + font = ImageFont.truetype( + "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine + ) + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + + d.text((50, 50), "\uE901", (100, 0, 0), font=font) + + assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or SBIX support") + + +@skip_unless_feature_version("freetype2", "2.10.0") +def test_colr(layout_engine): + font = ImageFont.truetype( + "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", + size=64, + layout_engine=layout_engine, + ) + + im = Image.new("RGB", (300, 75), "white") + d = ImageDraw.Draw(im) + + d.text((15, 5), "Bungee", font=font, embedded_color=True) + + assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21) + + +@skip_unless_feature_version("freetype2", "2.10.0") +def test_colr_mask(layout_engine): + font = ImageFont.truetype( + "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", + size=64, + layout_engine=layout_engine, + ) + + im = Image.new("RGB", (300, 75), "white") + d = ImageDraw.Draw(im) + + d.text((15, 5), "Bungee", "black", font=font) + + assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) + + +def test_fill_deprecation(font): + with pytest.warns(DeprecationWarning): + font.getmask2("Hello world", fill=Image.core.fill) + with pytest.warns(DeprecationWarning): + with pytest.raises(TypeError): + font.getmask2("Hello world", fill=None) def test_render_mono_size(): From 5a38c7f95357e29b05edadcfd86a78eec1cc6ed9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Aug 2022 13:05:21 +1000 Subject: [PATCH 077/100] Updated libimagequant to 4.0.4 --- 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 76f4cb95f..64dd024bd 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.0.2 +archive=libimagequant-4.0.4 ./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 a8cd5e441..bb547c1ad 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -166,7 +166,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.0.2** + * Pillow has been tested with libimagequant **2.6-4.0.4** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From ac83011fbf91341ec50761784a8d4e4c5baae9f2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 Aug 2022 18:09:18 +1000 Subject: [PATCH 078/100] NumPy now supports Python 3.11 --- .ci/install.sh | 3 +-- .github/workflows/macos-install.sh | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 7ead209be..518b66acc 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -37,8 +37,7 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma if [[ $(uname) != CYGWIN* ]]; then - # TODO Remove condition when NumPy supports 3.11 - if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi + python3 -m pip install numpy # 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 bb0bcd680..65f2b81d5 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -14,8 +14,7 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg -# TODO Remove condition when NumPy supports 3.11 -if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi +python3 -m pip install numpy # extra test images pushd depends && ./install_extra_test_images.sh && popd From 38b53a9fd704570fb29abd10910ea7939b1185e1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 Aug 2022 20:33:51 +1000 Subject: [PATCH 079/100] Do not call load() before draft() --- Tests/test_image_thumbnail.py | 22 ++++++++++++++++++ src/PIL/Image.py | 42 ++++++++++++++++++++++------------- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 20cc101ed..4fd07a2b4 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -97,6 +97,28 @@ def test_load_first(): im.thumbnail((64, 64)) assert im.size == (64, 10) + # Test thumbnail(), without draft(), + # on an image that is large enough once load() has changed the size + with Image.open("Tests/images/g4_orientation_5.tif") as im: + im.thumbnail((590, 88), reducing_gap=None) + assert im.size == (590, 88) + + +def test_load_first_unless_jpeg(): + # Test that thumbnail() still uses draft() for JPEG + with Image.open("Tests/images/hopper.jpg") as im: + draft = im.draft + + def im_draft(mode, size): + result = draft(mode, size) + assert result is not None + + return result + + im.draft = im_draft + + im.thumbnail((64, 64)) + # valgrind test is failing with memory allocated in libjpeg @pytest.mark.valgrind_known_error(reason="Known Failing") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4eb2dead6..afe1feede 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2473,29 +2473,41 @@ class Image: :returns: None """ - self.load() - x, y = map(math.floor, size) - if x >= self.width and y >= self.height: - return + provided_size = tuple(map(math.floor, size)) - def round_aspect(number, key): - return max(min(math.floor(number), math.ceil(number), key=key), 1) + def preserve_aspect_ratio(): + def round_aspect(number, key): + return max(min(math.floor(number), math.ceil(number), key=key), 1) - # preserve aspect ratio - aspect = self.width / self.height - if x / y >= aspect: - x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) - else: - y = round_aspect( - x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n) - ) - size = (x, y) + x, y = provided_size + if x >= self.width and y >= self.height: + return + + aspect = self.width / self.height + if x / y >= aspect: + x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) + else: + y = round_aspect( + x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n) + ) + return x, y box = None if reducing_gap is not None: + size = preserve_aspect_ratio() + if size is None: + return + res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) if res is not None: box = res[1] + if box is None: + self.load() + + # load() may have changed the size of the image + size = preserve_aspect_ratio() + if size is None: + return if self.size != size: im = self.resize(size, resample, box=box, reducing_gap=reducing_gap) From e58b1960c34185c682fe9108934b91dcd34ee208 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 Aug 2022 22:48:12 +1000 Subject: [PATCH 080/100] Set top-level permissions for remaining GitHub Actions --- .github/workflows/cifuzz.yml | 3 +++ .github/workflows/test-cygwin.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 0e0abaf95..fa1e8a503 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -11,6 +11,9 @@ on: - "**.h" workflow_dispatch: +permissions: + contents: read + jobs: Fuzzing: runs-on: ubuntu-latest diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 417b1f212..794159cec 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -2,6 +2,9 @@ name: Test Cygwin on: [push, pull_request, workflow_dispatch] +permissions: + contents: read + jobs: build: runs-on: windows-latest From e61327177601181d7395fa92f5fad36aeb5b6652 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Aug 2022 18:48:47 +1000 Subject: [PATCH 081/100] Fixed typo --- src/libImaging/TiffDecode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 3bb444c80..04a835dcd 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -916,7 +916,7 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt dump_state(clientstate); if (state->state == 0) { - TRACE(("Encoding line bt line")); + TRACE(("Encoding line by line")); while (state->y < state->ysize) { state->shuffle( state->buffer, From 599637808cfd762529c7ffc6458776a4efb8d0d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Aug 2022 13:22:09 +1000 Subject: [PATCH 082/100] Documented TGA keyword arguments when saving --- docs/handbook/image-file-formats.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 7db7b117a..ff54853a3 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -837,6 +837,24 @@ Pillow reads and writes TGA images containing ``L``, ``LA``, ``P``, ``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and run-length encoded TGAs. +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**compression** + If set to "tga_rle", the file will be run-length encoded. + + .. versionadded:: 5.3.0 + +**id_section** + The identification field. + + .. versionadded:: 5.3.0 + +**orientation** + If present and a positive number, the first pixel is for the top left corner, + rather than the bottom left corner. + + .. versionadded:: 5.3.0 + TIFF ^^^^ From 09a7255cedc0117e530433b689639b37d2b497e0 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 29 Aug 2022 11:35:06 -0500 Subject: [PATCH 083/100] Apply suggestions from code review Co-authored-by: Hugo van Kemenade --- Tests/test_image_filter.py | 2 +- Tests/test_image_reduce.py | 16 ++++++++-------- Tests/test_image_transform.py | 22 +++++++++++----------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index bec7f21e9..07f4d08ad 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -74,7 +74,7 @@ def test_modefilter(mode, expected): @pytest.mark.parametrize( - "mode,expected", + "mode, expected", ( ("1", (0, 4, 8)), ("L", (0, 4, 8)), diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index 90beeeb68..801161511 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -39,7 +39,7 @@ gradients_image.load() @pytest.mark.parametrize( - "size,expected", + "size, expected", ( (3, (4, 4)), ((3, 1), (4, 10)), @@ -52,16 +52,16 @@ def test_args_factor(size, expected): @pytest.mark.parametrize( - "size,error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) + "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) ) -def test_args_factor_error(size, error): +def test_args_factor_error(size, expected_error): im = Image.new("L", (10, 10)) - with pytest.raises(error): + with pytest.raises(expected_error): im.reduce(size) @pytest.mark.parametrize( - "size,expected", + "size, expected", ( ((0, 0, 10, 10), (5, 5)), ((5, 5, 6, 6), (1, 1)), @@ -73,7 +73,7 @@ def test_args_box(size, expected): @pytest.mark.parametrize( - "size,error", + "size, expected_error", ( ("stri", TypeError), ((0, 0, 11, 10), ValueError), @@ -84,9 +84,9 @@ def test_args_box(size, expected): ((5, 0, 5, 10), ValueError), ), ) -def test_args_box_error(size, error): +def test_args_box_error(size, expected_error): im = Image.new("L", (10, 10)) - with pytest.raises(error): + with pytest.raises(expected_error): im.reduce(2, size).size diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 14ca0334a..a78349801 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -76,14 +76,14 @@ class TestImageTransform: assert_image_equal(transformed, scaled) @pytest.mark.parametrize( - "mode,pixel", + "mode, expected_pixel", ( ("RGB", (255, 0, 0)), ("RGBA", (255, 0, 0, 255)), ("LA", (76, 0)), ), ) - def test_fill(self, mode, pixel): + def test_fill(self, mode, expected_pixel): im = hopper(mode) (w, h) = im.size transformed = im.transform( @@ -93,7 +93,7 @@ class TestImageTransform: Image.Resampling.BILINEAR, fillcolor="red", ) - assert transformed.getpixel((w - 1, h - 1)) == pixel + assert transformed.getpixel((w - 1, h - 1)) == expected_pixel def test_mesh(self): # this should be a checkerboard of halfsized hoppers in ul, lr @@ -240,7 +240,7 @@ class TestImageTransformAffine: return im.crop((10, 20, im.width - 10, im.height - 20)) @pytest.mark.parametrize( - "deg,transpose", + "deg, transpose", ( (0, None), (90, Image.Transpose.ROTATE_90), @@ -281,7 +281,7 @@ class TestImageTransformAffine: assert_image_equal(transposed, transformed) @pytest.mark.parametrize( - "scale,epsilonscale", + "scale, epsilon_scale", ( (1.1, 6.9), (1.5, 5.5), @@ -298,7 +298,7 @@ class TestImageTransformAffine: (Image.Resampling.BICUBIC, 1), ), ) - def test_resize(self, scale, epsilonscale, resample, epsilon): + def test_resize(self, scale, epsilon_scale, resample, epsilon): im = self._test_image() size_up = int(round(im.width * scale)), int(round(im.height * scale)) @@ -309,10 +309,10 @@ class TestImageTransformAffine: transformed = transformed.transform( im.size, self.transform, matrix_down, resample ) - assert_image_similar(transformed, im, epsilon * epsilonscale) + assert_image_similar(transformed, im, epsilon * epsilon_scale) @pytest.mark.parametrize( - "x,y,epsilonscale", + "x, y, epsilon_scale", ( (0.1, 0, 3.7), (0.6, 0, 9.1), @@ -320,14 +320,14 @@ class TestImageTransformAffine: ), ) @pytest.mark.parametrize( - "resample,epsilon", + "resample, epsilon", ( (Image.Resampling.NEAREST, 0), (Image.Resampling.BILINEAR, 1.5), (Image.Resampling.BICUBIC, 1), ), ) - def test_translate(self, x, y, epsilonscale, resample, epsilon): + def test_translate(self, x, y, epsilon_scale, resample, epsilon): im = self._test_image() size_up = int(round(im.width + x)), int(round(im.height + y)) @@ -338,7 +338,7 @@ class TestImageTransformAffine: transformed = transformed.transform( im.size, self.transform, matrix_down, resample ) - assert_image_similar(transformed, im, epsilon * epsilonscale) + assert_image_similar(transformed, im, epsilon * epsilon_scale) class TestImageTransformPerspective(TestImageTransformAffine): From 797eb397115c971f785414e6a89f25e7903ae853 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 29 Aug 2022 12:28:14 -0500 Subject: [PATCH 084/100] Apply suggestions from code review Co-authored-by: Hugo van Kemenade --- Tests/test_image_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 07f4d08ad..cfe46b658 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -52,7 +52,7 @@ def test_crash(size): @pytest.mark.parametrize( - "mode,expected", + "mode, expected", ( ("1", (4, 0)), ("L", (4, 0)), From 0ec3d3ec2c0e12fdd98caeac47bf1bcfe2be7701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Mon, 29 Aug 2022 20:34:11 +0200 Subject: [PATCH 085/100] Use pytest.param for consistency Co-authored-by: Hugo van Kemenade --- Tests/test_imagefont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index f8ecc193a..09e5370e2 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -35,7 +35,7 @@ def test_sanity(): @pytest.fixture( scope="module", params=[ - ImageFont.Layout.BASIC, + pytest.param(ImageFont.Layout.BASIC), pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), ], ) From 7b0e56bb211ab5880d08b5cc159c9744c34601a8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Aug 2022 09:21:24 +1000 Subject: [PATCH 086/100] Removed support for Python before interpaddr() --- src/PIL/ImageTk.py | 23 ++++++++++------------- src/_imagingtk.c | 21 ++------------------- 2 files changed, 12 insertions(+), 32 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index c2c4d774c..33c0cdacc 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -68,21 +68,18 @@ def _pyimagingtkcall(command, photo, id): # may raise an error if it cannot attach to Tkinter from . import _imagingtk - try: - if hasattr(tk, "interp"): - # Required for PyPy, which always has CFFI installed - from cffi import FFI + if hasattr(tk, "interp"): + # Required for PyPy, which always has CFFI installed + from cffi import FFI - ffi = FFI() + ffi = FFI() - # PyPy is using an FFI CDATA element - # (Pdb) self.tk.interp - # - _imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1) - else: - _imagingtk.tkinit(tk.interpaddr(), 1) - except AttributeError: - _imagingtk.tkinit(id(tk), 0) + # PyPy is using an FFI CDATA element + # (Pdb) self.tk.interp + # + _imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp))) + else: + _imagingtk.tkinit(tk.interpaddr()) tk.call(command, photo, id) diff --git a/src/_imagingtk.c b/src/_imagingtk.c index 3f154166b..b9273b0b8 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -23,33 +23,16 @@ TkImaging_Init(Tcl_Interp *interp); extern int load_tkinter_funcs(void); -/* copied from _tkinter.c (this isn't as bad as it may seem: for new - versions, we use _tkinter's interpaddr hook instead, and all older - versions use this structure layout) */ - -typedef struct { - PyObject_HEAD Tcl_Interp *interp; -} TkappObject; - static PyObject * _tkinit(PyObject *self, PyObject *args) { Tcl_Interp *interp; PyObject *arg; - int is_interp; - if (!PyArg_ParseTuple(args, "Oi", &arg, &is_interp)) { + if (!PyArg_ParseTuple(args, "O", &arg)) { return NULL; } - if (is_interp) { - interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg); - } else { - TkappObject *app; - /* Do it the hard way. This will break if the TkappObject - layout changes */ - app = (TkappObject *)PyLong_AsVoidPtr(arg); - interp = app->interp; - } + interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg); /* This will bomb if interp is invalid... */ TkImaging_Init(interp); From d6e59bc750c0649ed26fc71a83e86e17ea862ec3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Aug 2022 19:41:14 +1000 Subject: [PATCH 087/100] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fb634eaba..ffb1dc06b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Removed support for tkinter before Python 1.5.2 #6549 + [radarhere] + - Allow default ImageDraw font to be set #6484 [radarhere, hugovk] From 172f1f3369c318338a49e584028640b34a0a475f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Aug 2022 20:30:58 +1000 Subject: [PATCH 088/100] Updated environment list [ci skip] --- winbuild/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/README.md b/winbuild/README.md index 611d1ed1a..d8538fbf3 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -11,8 +11,8 @@ For more extensive info, see the [Windows build instructions](build.rst). * Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires NASM for libjpeg-turbo, a required dependency when using this script. * Requires CMake 3.12 or newer (available as Visual Studio component). -* Tested on Windows Server 2016 with Visual Studio 2017 Community (AppVeyor). -* Tested on Windows Server 2019 with Visual Studio 2019 Enterprise (GitHub Actions). +* Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor). +* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). The following is a simplified version of the script used on AppVeyor: ``` From 54c560f6119dd492ed251a3deb3ba67b4b05b2be Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 30 Aug 2022 14:12:48 +0200 Subject: [PATCH 089/100] Removed support for PyPy before Python 3.6 --- src/PIL/ImageTk.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 33c0cdacc..7c90a0ad8 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -68,18 +68,7 @@ def _pyimagingtkcall(command, photo, id): # may raise an error if it cannot attach to Tkinter from . import _imagingtk - if hasattr(tk, "interp"): - # Required for PyPy, which always has CFFI installed - from cffi import FFI - - ffi = FFI() - - # PyPy is using an FFI CDATA element - # (Pdb) self.tk.interp - # - _imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp))) - else: - _imagingtk.tkinit(tk.interpaddr()) + _imagingtk.tkinit(tk.interpaddr()) tk.call(command, photo, id) From 196210bc804a4575b6cc0b1cb6c9b7b1f09e4ff9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 31 Aug 2022 08:10:35 +1000 Subject: [PATCH 090/100] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ffb1dc06b..63c71cd0f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Open 1 bit EPS in mode 1 #6499 + [radarhere] + - Removed support for tkinter before Python 1.5.2 #6549 [radarhere] From b3dcf17886dcb4a6c392c83eec4f393d7b4efaca Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 31 Aug 2022 20:09:05 +1000 Subject: [PATCH 091/100] Use constants --- src/PIL/TiffImagePlugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index da33cc5a5..c70ed333c 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1153,7 +1153,7 @@ class TiffImageFile(ImageFile.ImageFile): :returns: XMP tags in a dictionary. """ - return self._getxmp(self.tag_v2[700]) if 700 in self.tag_v2 else {} + return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {} def get_photoshop_blocks(self): """ @@ -1328,7 +1328,7 @@ class TiffImageFile(ImageFile.ImageFile): logger.debug(f"- photometric_interpretation: {photo}") logger.debug(f"- planar_configuration: {self._planar_configuration}") logger.debug(f"- fill_order: {fillorder}") - logger.debug(f"- YCbCr subsampling: {self.tag.get(530)}") + logger.debug(f"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}") # size xsize = int(self.tag_v2.get(IMAGEWIDTH)) @@ -1469,8 +1469,8 @@ class TiffImageFile(ImageFile.ImageFile): else: # tiled image offsets = self.tag_v2[TILEOFFSETS] - w = self.tag_v2.get(322) - h = self.tag_v2.get(323) + w = self.tag_v2.get(TILEWIDTH) + h = self.tag_v2.get(TILELENGTH) for offset in offsets: if x + w > xsize: From 96c4f5401209ef7254029e97059fd9d2aaa86b69 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 31 Aug 2022 21:03:21 +1000 Subject: [PATCH 092/100] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 63c71cd0f..7157f12ef 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Do not use CCITTFaxDecode filter if libtiff is not available #6518 + [radarhere] + +- Fallback to not using mmap if buffer is not large enough #6510 + [radarhere] + +- Fixed writing bytes as ASCII tag #6493 + [radarhere] + - Open 1 bit EPS in mode 1 #6499 [radarhere] From 06660a5bad41dbaf9a73410372fe75852c8a7c5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 31 Aug 2022 21:29:27 +1000 Subject: [PATCH 093/100] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7157f12ef..012c6e8cd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Removed support for tkinter in PyPy before Python 3.6 #6551 + [nulano] + - Do not use CCITTFaxDecode filter if libtiff is not available #6518 [radarhere] From 3f960d9a94e5f2cd789da8b9fd0d1d6db8a60cba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Sep 2022 08:37:15 +1000 Subject: [PATCH 094/100] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 012c6e8cd..67d150005 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Copy palette when converting from P to PA #6497 + [radarhere] + +- Allow RGB and RGBA values for PA image putpixel #6504 + [radarhere] + - Removed support for tkinter in PyPy before Python 3.6 #6551 [nulano] From a36b766d3658edff41a80f65fb88295640a3d9a3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Sep 2022 20:53:22 +1000 Subject: [PATCH 095/100] Simplified enum references --- docs/reference/Image.rst | 8 ++--- src/PIL/Image.py | 72 +++++++++++++++++----------------------- 2 files changed, 35 insertions(+), 45 deletions(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index ed37521fd..7f6f666c3 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -53,9 +53,9 @@ Functions To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files which decompress into a huge amount of data and are designed to crash or cause disruption by using up a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the number of pixels in an - image is over a certain limit, :py:data:`PIL.Image.MAX_IMAGE_PIXELS`. + image is over a certain limit, :py:data:`MAX_IMAGE_PIXELS`. - This threshold can be changed by setting :py:data:`PIL.Image.MAX_IMAGE_PIXELS`. It can be disabled + This threshold can be changed by setting :py:data:`MAX_IMAGE_PIXELS`. It can be disabled by setting ``Image.MAX_IMAGE_PIXELS = None``. If desired, the warning can be turned into an error with @@ -63,7 +63,7 @@ Functions ``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also `the logging documentation`_ to have warnings output to the logging facility instead of stderr. - If the number of pixels is greater than twice :py:data:`PIL.Image.MAX_IMAGE_PIXELS`, then a + If the number of pixels is greater than twice :py:data:`MAX_IMAGE_PIXELS`, then a ``DecompressionBombError`` will be raised instead. .. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb @@ -255,7 +255,7 @@ This rotates the input image by ``theta`` degrees counter clockwise: .. automethod:: PIL.Image.Image.transform .. automethod:: PIL.Image.Image.transpose -This flips the input image by using the :data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT` +This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT` method. .. code-block:: python diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f3f158db8..a958f064d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1989,18 +1989,14 @@ class Image: :param size: The requested size in pixels, as a 2-tuple: (width, height). :param resample: An optional resampling filter. This can be - one of :py:data:`PIL.Image.Resampling.NEAREST`, - :py:data:`PIL.Image.Resampling.BOX`, - :py:data:`PIL.Image.Resampling.BILINEAR`, - :py:data:`PIL.Image.Resampling.HAMMING`, - :py:data:`PIL.Image.Resampling.BICUBIC` or - :py:data:`PIL.Image.Resampling.LANCZOS`. + one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, + :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, + :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. If the image has mode "1" or "P", it is always set to - :py:data:`PIL.Image.Resampling.NEAREST`. - If the image mode specifies a number of bits, such as "I;16", then the - default filter is :py:data:`PIL.Image.Resampling.NEAREST`. - Otherwise, the default filter is - :py:data:`PIL.Image.Resampling.BICUBIC`. See: :ref:`concept-filters`. + :py:data:`Resampling.NEAREST`. If the image mode specifies a number + of bits, such as "I;16", then the default filter is + :py:data:`Resampling.NEAREST`. Otherwise, the default filter is + :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`. :param box: An optional 4-tuple of floats providing the source image region to be scaled. The values must be within (0, 0, width, height) rectangle. @@ -2140,12 +2136,12 @@ class Image: :param angle: In degrees counter clockwise. :param resample: An optional resampling filter. This can be - one of :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour), - :py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:data:`PIL.Image.Resampling.BICUBIC` - (cubic spline interpolation in a 4x4 environment). - If omitted, or if the image has mode "1" or "P", it is - set to :py:data:`PIL.Image.Resampling.NEAREST`. See :ref:`concept-filters`. + one of :py:data:`Resampling.NEAREST` (use nearest neighbour), + :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 + environment), or :py:data:`Resampling.BICUBIC` (cubic spline + interpolation in a 4x4 environment). If omitted, or if the image has + mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. + See :ref:`concept-filters`. :param expand: Optional expansion flag. If true, expands the output image to make it large enough to hold the entire rotated image. If false or omitted, make the output image the same size as the @@ -2452,14 +2448,11 @@ class Image: :param size: Requested size. :param resample: Optional resampling filter. This can be one - of :py:data:`PIL.Image.Resampling.NEAREST`, - :py:data:`PIL.Image.Resampling.BOX`, - :py:data:`PIL.Image.Resampling.BILINEAR`, - :py:data:`PIL.Image.Resampling.HAMMING`, - :py:data:`PIL.Image.Resampling.BICUBIC` or - :py:data:`PIL.Image.Resampling.LANCZOS`. - If omitted, it defaults to :py:data:`PIL.Image.Resampling.BICUBIC`. - (was :py:data:`PIL.Image.Resampling.NEAREST` prior to version 2.5.0). + of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, + :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, + :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. + If omitted, it defaults to :py:data:`Resampling.BICUBIC`. + (was :py:data:`Resampling.NEAREST` prior to version 2.5.0). See: :ref:`concept-filters`. :param reducing_gap: Apply optimization by resizing the image in two steps. First, reducing the image by integer times @@ -2530,11 +2523,11 @@ class Image: :param size: The output size. :param method: The transformation method. This is one of - :py:data:`PIL.Image.Transform.EXTENT` (cut out a rectangular subregion), - :py:data:`PIL.Image.Transform.AFFINE` (affine transform), - :py:data:`PIL.Image.Transform.PERSPECTIVE` (perspective transform), - :py:data:`PIL.Image.Transform.QUAD` (map a quadrilateral to a rectangle), or - :py:data:`PIL.Image.Transform.MESH` (map a number of source quadrilaterals + :py:data:`Transform.EXTENT` (cut out a rectangular subregion), + :py:data:`Transform.AFFINE` (affine transform), + :py:data:`Transform.PERSPECTIVE` (perspective transform), + :py:data:`Transform.QUAD` (map a quadrilateral to a rectangle), or + :py:data:`Transform.MESH` (map a number of source quadrilaterals in one operation). It may also be an :py:class:`~PIL.Image.ImageTransformHandler` @@ -2554,11 +2547,11 @@ class Image: return method, data :param data: Extra data to the transformation method. :param resample: Optional resampling filter. It can be one of - :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour), - :py:data:`PIL.Image.Resampling.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:data:`PIL.Image.BICUBIC` (cubic spline + :py:data:`Resampling.NEAREST` (use nearest neighbour), + :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 + environment), or :py:data:`Resampling.BICUBIC` (cubic spline interpolation in a 4x4 environment). If omitted, or if the image - has mode "1" or "P", it is set to :py:data:`PIL.Image.Resampling.NEAREST`. + has mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. See: :ref:`concept-filters`. :param fill: If ``method`` is an :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of @@ -2685,13 +2678,10 @@ class Image: """ Transpose image (flip or rotate in 90 degree steps) - :param method: One of :py:data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT`, - :py:data:`PIL.Image.Transpose.FLIP_TOP_BOTTOM`, - :py:data:`PIL.Image.Transpose.ROTATE_90`, - :py:data:`PIL.Image.Transpose.ROTATE_180`, - :py:data:`PIL.Image.Transpose.ROTATE_270`, - :py:data:`PIL.Image.Transpose.TRANSPOSE` or - :py:data:`PIL.Image.Transpose.TRANSVERSE`. + :param method: One of :py:data:`Transpose.FLIP_LEFT_RIGHT`, + :py:data:`Transpose.FLIP_TOP_BOTTOM`, :py:data:`Transpose.ROTATE_90`, + :py:data:`Transpose.ROTATE_180`, :py:data:`Transpose.ROTATE_270`, + :py:data:`Transpose.TRANSPOSE` or :py:data:`Transpose.TRANSVERSE`. :returns: Returns a flipped or rotated copy of this image. """ From b3683c3e4b58f812c025adbe78790cc6e2e7fa2f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 18:09:19 +0000 Subject: [PATCH 096/100] [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.6.0 → 22.8.0](https://github.com/psf/black/compare/22.6.0...22.8.0) - [github.com/asottile/yesqa: v1.3.0 → v1.4.0](https://github.com/asottile/yesqa/compare/v1.3.0...v1.4.0) - [github.com/Lucas-C/pre-commit-hooks: v1.3.0 → v1.3.1](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.3.0...v1.3.1) - [github.com/PyCQA/flake8: 5.0.2 → 5.0.4](https://github.com/PyCQA/flake8/compare/5.0.2...5.0.4) --- .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 1bb71bd72..eeb4b391e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.8.0 hooks: - id: black args: ["--target-version", "py37"] @@ -14,18 +14,18 @@ repos: - id: isort - repo: https://github.com/asottile/yesqa - rev: v1.3.0 + rev: v1.4.0 hooks: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.3.0 + rev: v1.3.1 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) - repo: https://github.com/PyCQA/flake8 - rev: 5.0.2 + rev: 5.0.4 hooks: - id: flake8 additional_dependencies: [flake8-2020, flake8-implicit-str-concat] From 509dbf7757725cc644f575869d62a4a12a9f3dc4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Sep 2022 08:23:28 +1000 Subject: [PATCH 097/100] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 67d150005..1d4103a7c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Do not call load() before draft() in Image.thumbnail #6539 + [radarhere] + - Copy palette when converting from P to PA #6497 [radarhere] From 7d8b2fb19c596f825617f522a8cb8b869c90bf1a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 12 Sep 2022 10:25:18 +0300 Subject: [PATCH 098/100] Move some static config to setup.cfg --- setup.cfg | 4 ++++ setup.py | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index be3bc4b4f..44feb25ff 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,7 +34,11 @@ project_urls = Twitter=https://twitter.com/PythonPillow [options] +packages = PIL python_requires = >=3.7 +include_package_data = True +package_dir = + = src [options.extras_require] docs = diff --git a/setup.py b/setup.py index a2b2c6910..aa3168aa5 100755 --- a/setup.py +++ b/setup.py @@ -999,9 +999,6 @@ try: version=PILLOW_VERSION, cmdclass={"build_ext": pil_build_ext}, ext_modules=ext_modules, - include_package_data=True, - packages=["PIL"], - package_dir={"": "src"}, zip_safe=not (debug_build() or PLATFORM_MINGW), ) except RequiredDependencyException as err: From 8b2d70d17a4791d68df6c10d8337d769290c6528 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Sep 2022 09:10:03 +1000 Subject: [PATCH 099/100] Corrected BMP palette size when saving --- Tests/test_file_bmp.py | 12 ++++++++++++ src/PIL/BmpImagePlugin.py | 20 ++++++++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index d58666b44..4c964fbea 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -51,6 +51,18 @@ def test_save_to_bytes(): assert reloaded.format == "BMP" +def test_small_palette(tmp_path): + im = Image.new("P", (1, 1)) + colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] + im.putpalette(colors) + + out = str(tmp_path / "temp.bmp") + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.getpalette() == colors + + def test_save_too_large(tmp_path): outfile = str(tmp_path / "temp.bmp") with Image.new("RGB", (1, 1)) as im: diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 7bb73fc93..1041ab763 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -375,6 +375,16 @@ def _save(im, fp, filename, bitmap_header=True): header = 40 # or 64 for OS/2 version 2 image = stride * im.size[1] + if im.mode == "1": + palette = b"".join(o8(i) * 4 for i in (0, 255)) + elif im.mode == "L": + palette = b"".join(o8(i) * 4 for i in range(256)) + elif im.mode == "P": + palette = im.im.getpalette("RGB", "BGRX") + colors = len(palette) // 4 + else: + palette = None + # bitmap header if bitmap_header: offset = 14 + header + colors * 4 @@ -405,14 +415,8 @@ def _save(im, fp, filename, bitmap_header=True): fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) - if im.mode == "1": - for i in (0, 255): - fp.write(o8(i) * 4) - elif im.mode == "L": - for i in range(256): - fp.write(o8(i) * 4) - elif im.mode == "P": - fp.write(im.im.getpalette("RGB", "BGRX")) + if palette: + fp.write(palette) ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]) From 964e0aa0790a7d3d9dadb03b3045de6c7e124a6e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 16 Sep 2022 23:32:58 +1000 Subject: [PATCH 100/100] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1d4103a7c..8c2993abc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Corrected BMP and TGA palette size when saving #6500 + [radarhere] + - Do not call load() before draft() in Image.thumbnail #6539 [radarhere]