From f5b27f90f7efcf01a68f7e3d84531d03e9ebfc5e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Aug 2022 20:38:47 +1000 Subject: [PATCH 01/28] 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 c71d4f5f22b..310619fb255 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 2109a6f52cb..d1b34be487e 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 02/28] 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 a43548ae0f3..3084425a406 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1011,14 +1011,18 @@ def test_save_multistrip(self, compression, tmp_path): # 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 d1b34be487e..181a05b8d26 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -25,7 +25,7 @@ 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 0dd49340d4b..da33cc5a501 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 1112ad67a35eb25d0363d68227fbbb3eea9f2b36 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Aug 2022 14:18:31 +1000 Subject: [PATCH 03/28] 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 48b41d87fda..44214fead5e 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 04/28] 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 44214fead5e..0c3f900caac 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 e77a7b6b4f0b496a70cabd8360f0988d32bea063 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 Aug 2022 23:29:58 +1000 Subject: [PATCH 05/28] 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 b4b5b7a0c65..4f934375c7c 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -4,7 +4,7 @@ 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 04c2e4fe379..bd10e3b95dd 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -75,6 +75,9 @@ def _open(self): 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 04d976131673b94c6065d5cf92b0eab53c4469f3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Aug 2022 17:29:44 +1000 Subject: [PATCH 06/28] 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 23bc756bb14..961b4d08130 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 8970471d3b2..712ec6e0912 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -46,6 +46,8 @@ class ImageDraw: + font = None + def __init__(self, im, mode=None): """ Create a drawing instance. @@ -86,7 +88,6 @@ def __init__(self, im, mode=None): 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 07/28] 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 a3b711c6077..efd702b8685 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 08/28] 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 efd702b8685..9386d008602 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 09/28] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b821e7732f6..5f99d9d254e 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 10/28] 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 c2d72c804c1..1ef9079fba0 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 c64423b0152..a8db4edd655 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 712ec6e0912..e84dafb12e9 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -93,6 +93,11 @@ def getfont(self): """ 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 84bdb635c2d144ec416382210d9825e5cdda065d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 16:36:46 +1000 Subject: [PATCH 11/28] 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 d46c1a40911..a381d636dd8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -108,9 +108,9 @@ def cmd_msbuild( 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 55d94558fbaa809c0cc03c072bf7119fb2b27e78 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 23:14:32 +1000 Subject: [PATCH 12/28] 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 16a056dd585..7ead209bec2 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 06b82964559..bb0bcd6803e 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 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 13/28] 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 a8db4edd655..7109a09f2b4 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 14/28] 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 5dc17db60d0..f0d42f7ff47 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 6b35dc2a8ab238145460af37096c9b53a301a235 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Aug 2022 19:17:41 +1000 Subject: [PATCH 15/28] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5f99d9d254e..fb634eabad1 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 f9d33b40ad0d9a3cea4be3aa2fa65b3beb477e2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 19 Aug 2022 12:09:47 +1000 Subject: [PATCH 16/28] 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 f147fa6a732..42cd7df9d31 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 17/28] 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 1728c8e0579..7db7b117a77 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 fd47eed73a7aa178848f280f09435b55bbaefd69 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Aug 2022 09:23:42 -0500 Subject: [PATCH 18/28] parametrize Tests/test_image_paste.py --- Tests/test_image_paste.py | 486 +++++++++++++++++++++----------------- 1 file changed, 266 insertions(+), 220 deletions(-) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 4ea1d73ce16..bb01ff11067 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 @@ def gradient_RGBa(self): ], ) - def test_image_solid(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "red") - im2 = getattr(self, "gradient_" + mode) - - im.paste(im2, (12, 23)) - - 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) - - 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) - - 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) - - 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) - - 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) - - 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") - - 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 - - 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)] - - 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" - - 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" - - 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" - - 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), - ], - ) + @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 = im.crop((12, 23, im2.width + 12, im2.height + 23)) + assert_image_equal(im, im2) + + @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), + ], + ) + + @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), + ], + ) + + @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), + ], + ) + + @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), + ], + ) + + @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), + ], + ) + + @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) + + hist = im.crop(rect).histogram() + while hist: + head, hist = hist[:256], hist[256:] + assert head[255] == 128 * 128 + assert sum(head[:255]) == 0 + + @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), + ], + ) + + @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), + ], + ) + + @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), + ], + ) + + @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), + ], + ) 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 19/28] [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 bb01ff11067..0b40ba671fb 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -103,11 +103,14 @@ def gradient_RGBa(self): ], ) - @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 @@ def test_image_solid(self, mode): 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 @@ def test_image_mask_1(self, mode): ], ) - @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 @@ def test_image_mask_L(self, mode): ], ) - @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 @@ def test_image_mask_LA(self, mode): ], ) - @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 @@ def test_image_mask_RGBA(self, mode): ], ) - @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 @@ def test_image_mask_RGBa(self, mode): ], ) - @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 @@ def test_color_solid(self, mode): 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 @@ def test_color_mask_1(self, mode): ], ) - @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 @@ def test_color_mask_L(self, mode): ], ) - @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 @@ def test_color_mask_RGBA(self, mode): ], ) - @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 20/28] 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 0b40ba671fb..1ab02017de1 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -103,14 +103,7 @@ def gradient_RGBa(self): ], ) - @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 @@ def test_image_solid(self, mode): 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 @@ def test_image_mask_1(self, mode): ], ) - @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 @@ def test_image_mask_L(self, mode): ], ) - @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 @@ def test_image_mask_LA(self, mode): ], ) - @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 @@ def test_image_mask_RGBA(self, mode): ], ) - @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 @@ def test_image_mask_RGBa(self, mode): ], ) - @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 @@ def test_color_solid(self, mode): 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 @@ def test_color_mask_1(self, mode): ], ) - @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 @@ def test_color_mask_L(self, mode): ], ) - @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 @@ def test_color_mask_RGBA(self, mode): ], ) - @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 21/28] 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 9b3088b9450..76f4cb95f3f 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 42cd7df9d31..a8cd5e4415a 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 22/28] 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 310619fb255..df0b7abe642 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 23/28] Parametrize tests --- Tests/test_file_apng.py | 18 +- Tests/test_file_container.py | 132 ++++++++------- Tests/test_file_im.py | 15 +- Tests/test_file_libtiff.py | 82 ++++----- Tests/test_file_mpo.py | 195 +++++++++++---------- Tests/test_file_tga.py | 77 ++++----- Tests/test_file_wmf.py | 10 +- Tests/test_image.py | 85 +++++----- Tests/test_image_access.py | 35 ++-- Tests/test_image_convert.py | 57 +++---- Tests/test_image_copy.py | 53 +++--- Tests/test_image_crop.py | 19 +-- Tests/test_image_resample.py | 288 +++++++++++++++---------------- Tests/test_image_resize.py | 27 +-- Tests/test_image_rotate.py | 30 ++-- Tests/test_image_transpose.py | 289 +++++++++++++++----------------- Tests/test_imagedraw.py | 20 +-- Tests/test_qt_image_toqimage.py | 58 +++---- 18 files changed, 730 insertions(+), 760 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index ad61a07ccc5..d624bbb849c 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -325,8 +325,9 @@ def open(): 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 b752e217faa..65cf6a75ea3 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) - - # Act - data = container.readlines() - - # Assert - if bytesmode: - data = [line.decode() for line in data] - assert data == expected + 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() + + # 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 675210c30d2..e458a197ca4 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 f9d8e282647..86a0fda04e5 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -135,50 +135,50 @@ def test_adobe_deflate_tiff(self): 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) - - 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", - ] - - 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" + 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() + + # 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() + + 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 849857d31d6..d94bdaa96c9 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 - - -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_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 + + +@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 - - -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_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 + + +@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 0c8c9f30485..cbbb7df1d12 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -18,51 +18,48 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} -def test_sanity(tmp_path): - for mode in _MODES: - - def roundtrip(original_im): - out = str(tmp_path / "temp.tga") +@pytest.mark.parametrize("mode", _MODES) +def test_sanity(mode, tmp_path): + 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: + 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() + + 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() - - assert_image_equal(saved_im, original_im) + if mode == "P": + assert original_im.getpalette() == reference_im.getpalette() - 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" - ) + assert_image_equal(original_im, reference_im) - 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 d6769a24b0b..439cb15bca9 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 6dc89918f05..7cebed127d9 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -22,8 +22,9 @@ class TestImage: - def test_image_modes_success(self): - for mode in [ + @pytest.mark.parametrize( + "mode", + ( "1", "P", "PA", @@ -44,22 +45,18 @@ def test_image_modes_success(self): "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 @@ def test_linear_gradient_wrong_mode(self): 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 @@ def test_radial_gradient_wrong_mode(self): 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) - - # 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) + + # 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) def test_register_extensions(self): test_format = "a" diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 617274a576d..bb75eb0b5a9 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -184,8 +184,9 @@ def check(self, mode, c=None): 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 @@ def test_basic(self): "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) - - 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) + self.check(mode, 2**15 - 1) + self.check(mode, 2**15) + self.check(mode, 2**15 + 1) + self.check(mode, 2**16 - 1) + + @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 e5639e10533..8f4b8b43c64 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" - - # 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") +@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) + + # 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 21e438654b1..591832147d7 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 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 + + # 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 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 6574e6efd1a..4aa41de2792 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -5,17 +5,14 @@ from .helper import assert_image_equal, hopper -def test_crop(): - def 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) +@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) def test_wide_crop(): diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 6d050efccba..883bb9b195f 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -100,40 +100,41 @@ def serialize_image(self, image): 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))) - - 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))) - - 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))) - - def test_reduce_bicubic(self): + @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))) + + @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))) + + @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))) + + @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 @@ def test_reduce_bicubic(self): 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))) - - 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))) - - 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))) - - 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))) - - 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))) - - 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_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))) + + @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))) + + @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))) + + @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))) + + @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))) + + @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 @@ def test_nonzero_coefficients(self): 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)) - - 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 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)) + ), + ) + 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(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 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 @@ def test_subsample(self): 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 8347fabb9e5..ae12202e4ed 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -22,24 +22,15 @@ def resize(self, im, size, f): 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 f96864c53df..a19f19831fd 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,25 +24,25 @@ 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) +@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)) + 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)) - rotate(im, im.mode, angle) +@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(): diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index 6408e156491..877f439ca26 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 @@ } -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 - - 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) - - -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 - - 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) - - -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] - - 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) - - -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 - - 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) - - -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] - - 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) - - -def test_transpose(): - def 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) - - -def test_tranverse(): - def transpose(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) - - -def test_roundtrip(): - for mode in HOPPER: - im = HOPPER[mode] - - 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), - ) +@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)) + + +@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)) + + +@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)) + + +@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)) + + +@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)) + + +@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)) + + +@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)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_roundtrip(mode): + im = HOPPER[mode] + + 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), + ) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 961b4d08130..d1dd1e47c1c 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 60bfaeb9b75..af0b0c2935f 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -16,32 +16,32 @@ 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) - - 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) - - 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 - - # 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) +@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() + + # 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 + return + + # 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) From 1c391fe31f902b604a7bc4ebd9b4315fa5ef8e1f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Aug 2022 08:11:02 +1000 Subject: [PATCH 24/28] 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 d624bbb849c..0ff05f608c2 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -326,7 +326,7 @@ def open(): @pytest.mark.parametrize( - "f", + "test_file", ( "sequence_start.png", "sequence_gap.png", @@ -337,9 +337,9 @@ def open(): "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 25/28] 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 af0b0c2935f..c1983031a14 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 26/28] Further parametrizations --- Tests/test_image_resample.py | 16 ++- Tests/test_image_resize.py | 241 ++++++++++++++++++----------------- 2 files changed, 135 insertions(+), 122 deletions(-) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 883bb9b195f..5ce98a23568 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -514,13 +514,15 @@ def test_subsample(self): 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 ae12202e4ed..83c54cf6211 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -46,33 +46,58 @@ def test_convolution_modes(self): 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 @@ def test_endianness(self): } samples["dirty"].putpixel((1, 1), 128) - for f in [ - 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 [ + # 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, - ]: - 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 @@ def test_reducing_gap_values(self, gradients_image): (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 - ) + @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) + with pytest.raises(AssertionError): + assert_image_equal(ref, im) - assert_image_similar(ref, im, epsilon) + 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 - ) + @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) + with pytest.raises(AssertionError): + assert_image_equal(ref, im) - assert_image_similar(ref, im, epsilon) + 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 - ) + @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) + with pytest.raises(AssertionError): + assert_image_equal(ref, im) - assert_image_similar(ref, im, epsilon) + 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", (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) + 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 - ) + @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) + assert_image_similar(ref, im, epsilon) class TestImageResize: @@ -264,15 +276,14 @@ def test_load_first(self): 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)) - - for mode in "1", "P": - im = hopper(mode) - assert im.resize((20, 20), Image.Resampling.NEAREST) == 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 "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 27/28] 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 a381d636dd8..94e5dd87114 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -226,21 +226,21 @@ def cmd_msbuild( "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 5a38c7f95357e29b05edadcfd86a78eec1cc6ed9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Aug 2022 13:05:21 +1000 Subject: [PATCH 28/28] 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 76f4cb95f3f..64dd024bd7f 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 a8cd5e4415a..bb547c1adaa 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.