diff --git a/CHANGES.rst b/CHANGES.rst index 21a4957da..bcd094c60 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,21 @@ Changelog (Pillow) 7.1.0 (unreleased) ------------------ +- Add JPEG comment to info dictionary #4455 + [radarhere] + +- Fix size calculation of Image.thumbnail() #4404 + [orlnub123] + +- Fixed stroke on FreeType < 2.9 #4401 + [radarhere] + +- If present, only use alpha channel for bounding box #4454 + [radarhere] + +- Warn if an unknown feature is passed to features.check() #4438 + [jdufresne] + - Fix Name field length when saving IM images #4424 [hugovk, radarhere] diff --git a/Tests/helper.py b/Tests/helper.py index d20b64b5f..39d3ed482 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -5,7 +5,6 @@ Helper functions. import logging import os import shutil -import subprocess import sys import tempfile import unittest @@ -192,21 +191,9 @@ class PillowTestCase(unittest.TestCase): self.addCleanup(self.delete_tempfile, path) return path - def open_withImagemagick(self, f): - if not imagemagick_available(): - raise OSError() - outfile = self.tempfile("temp.png") - rc = subprocess.call( - [IMCONVERT, f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ) - if rc: - raise OSError - return Image.open(outfile) - - -@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS") -class PillowLeakTestCase(PillowTestCase): +@pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS") +class PillowLeakTestCase: # requires unix/macOS iterations = 100 # count mem_limit = 512 # k diff --git a/Tests/images/imagedraw_stroke_descender.png b/Tests/images/imagedraw_stroke_descender.png new file mode 100644 index 000000000..93462334a Binary files /dev/null and b/Tests/images/imagedraw_stroke_descender.png differ diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index c4fe25910..ade2901b7 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -3,109 +3,108 @@ import os import pytest from PIL import Image -from .helper import PillowTestCase, assert_image_similar +from .helper import assert_image_similar base = os.path.join("Tests", "images", "bmp") -class TestBmpReference(PillowTestCase): - def get_files(self, d, ext=".bmp"): - return [ - os.path.join(base, d, f) - for f in os.listdir(os.path.join(base, d)) - if ext in f - ] +def get_files(d, ext=".bmp"): + return [ + os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f + ] - def test_bad(self): - """ These shouldn't crash/dos, but they shouldn't return anything - either """ - for f in self.get_files("b"): - def open(f): - try: - with Image.open(f) as im: - im.load() - except Exception: # as msg: - pass +def test_bad(): + """ These shouldn't crash/dos, but they shouldn't return anything + either """ + for f in get_files("b"): - # Assert that there is no unclosed file warning - pytest.warns(None, open, f) - - def test_questionable(self): - """ These shouldn't crash/dos, but it's not well defined that these - are in spec """ - supported = [ - "pal8os2v2.bmp", - "rgb24prof.bmp", - "pal1p1.bmp", - "pal8offs.bmp", - "rgb24lprof.bmp", - "rgb32fakealpha.bmp", - "rgb24largepal.bmp", - "pal8os2sp.bmp", - "rgb32bf-xbgr.bmp", - ] - for f in self.get_files("q"): + def open(f): try: with Image.open(f) as im: im.load() - if os.path.basename(f) not in supported: - print("Please add %s to the partially supported bmp specs." % f) except Exception: # as msg: - if os.path.basename(f) in supported: - raise + pass - def test_good(self): - """ These should all work. There's a set of target files in the - html directory that we can compare against. """ + # Assert that there is no unclosed file warning + pytest.warns(None, open, f) - # Target files, if they're not just replacing the extension - file_map = { - "pal1wb.bmp": "pal1.png", - "pal4rle.bmp": "pal4.png", - "pal8-0.bmp": "pal8.png", - "pal8rle.bmp": "pal8.png", - "pal8topdown.bmp": "pal8.png", - "pal8nonsquare.bmp": "pal8nonsquare-v.png", - "pal8os2.bmp": "pal8.png", - "pal8os2sp.bmp": "pal8.png", - "pal8os2v2.bmp": "pal8.png", - "pal8os2v2-16.bmp": "pal8.png", - "pal8v4.bmp": "pal8.png", - "pal8v5.bmp": "pal8.png", - "rgb16-565pal.bmp": "rgb16-565.png", - "rgb24pal.bmp": "rgb24.png", - "rgb32.bmp": "rgb24.png", - "rgb32bf.bmp": "rgb24.png", - } - def get_compare(f): - name = os.path.split(f)[1] - if name in file_map: - return os.path.join(base, "html", file_map[name]) - name = os.path.splitext(name)[0] - return os.path.join(base, "html", "%s.png" % name) +def test_questionable(): + """ These shouldn't crash/dos, but it's not well defined that these + are in spec """ + supported = [ + "pal8os2v2.bmp", + "rgb24prof.bmp", + "pal1p1.bmp", + "pal8offs.bmp", + "rgb24lprof.bmp", + "rgb32fakealpha.bmp", + "rgb24largepal.bmp", + "pal8os2sp.bmp", + "rgb32bf-xbgr.bmp", + ] + for f in get_files("q"): + try: + with Image.open(f) as im: + im.load() + if os.path.basename(f) not in supported: + print("Please add %s to the partially supported bmp specs." % f) + except Exception: # as msg: + if os.path.basename(f) in supported: + raise - for f in self.get_files("g"): - try: - with Image.open(f) as im: - im.load() - with Image.open(get_compare(f)) as compare: - compare.load() - if im.mode == "P": - # assert image similar doesn't really work - # with paletized image, since the palette might - # be differently ordered for an equivalent image. - im = im.convert("RGBA") - compare = im.convert("RGBA") - assert_image_similar(im, compare, 5) - except Exception as msg: - # there are three here that are unsupported: - unsupported = ( - os.path.join(base, "g", "rgb32bf.bmp"), - os.path.join(base, "g", "pal8rle.bmp"), - os.path.join(base, "g", "pal4rle.bmp"), - ) - if f not in unsupported: - self.fail("Unsupported Image {}: {}".format(f, msg)) +def test_good(): + """ These should all work. There's a set of target files in the + html directory that we can compare against. """ + + # Target files, if they're not just replacing the extension + file_map = { + "pal1wb.bmp": "pal1.png", + "pal4rle.bmp": "pal4.png", + "pal8-0.bmp": "pal8.png", + "pal8rle.bmp": "pal8.png", + "pal8topdown.bmp": "pal8.png", + "pal8nonsquare.bmp": "pal8nonsquare-v.png", + "pal8os2.bmp": "pal8.png", + "pal8os2sp.bmp": "pal8.png", + "pal8os2v2.bmp": "pal8.png", + "pal8os2v2-16.bmp": "pal8.png", + "pal8v4.bmp": "pal8.png", + "pal8v5.bmp": "pal8.png", + "rgb16-565pal.bmp": "rgb16-565.png", + "rgb24pal.bmp": "rgb24.png", + "rgb32.bmp": "rgb24.png", + "rgb32bf.bmp": "rgb24.png", + } + + def get_compare(f): + name = os.path.split(f)[1] + if name in file_map: + return os.path.join(base, "html", file_map[name]) + name = os.path.splitext(name)[0] + return os.path.join(base, "html", "%s.png" % name) + + for f in get_files("g"): + try: + with Image.open(f) as im: + im.load() + with Image.open(get_compare(f)) as compare: + compare.load() + if im.mode == "P": + # assert image similar doesn't really work + # with paletized image, since the palette might + # be differently ordered for an equivalent image. + im = im.convert("RGBA") + compare = im.convert("RGBA") + assert_image_similar(im, compare, 5) + + except Exception as msg: + # there are three here that are unsupported: + unsupported = ( + os.path.join(base, "g", "rgb32bf.bmp"), + os.path.join(base, "g", "pal8rle.bmp"), + os.path.join(base, "g", "pal4rle.bmp"), + ) + assert f in unsupported, "Unsupported Image {}: {}".format(f, msg) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 3b3ebc59f..a8fe8bfeb 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,10 +1,9 @@ import sys -import unittest import pytest from PIL import Image -from .helper import PillowTestCase, is_pypy +from .helper import is_pypy def test_get_stats(): @@ -32,8 +31,8 @@ def test_reset_stats(): assert stats["blocks_cached"] == 0 -class TestCoreMemory(PillowTestCase): - def tearDown(self): +class TestCoreMemory: + def teardown_method(self): # Restore default values Image.core.set_alignment(1) Image.core.set_block_size(1024 * 1024) @@ -114,7 +113,7 @@ class TestCoreMemory(PillowTestCase): with pytest.raises(ValueError): Image.core.set_blocks_max(2 ** 29) - @unittest.skipIf(is_pypy(), "images are not collected") + @pytest.mark.skipif(is_pypy(), reason="Images not collected") def test_set_blocks_max_stats(self): Image.core.reset_stats() Image.core.set_blocks_max(128) @@ -129,7 +128,7 @@ class TestCoreMemory(PillowTestCase): assert stats["freed_blocks"] == 0 assert stats["blocks_cached"] == 64 - @unittest.skipIf(is_pypy(), "images are not collected") + @pytest.mark.skipif(is_pypy(), reason="Images not collected") def test_clear_cache_stats(self): Image.core.reset_stats() Image.core.clear_cache() @@ -163,8 +162,8 @@ class TestCoreMemory(PillowTestCase): assert stats["freed_blocks"] >= 16 -class TestEnvVars(PillowTestCase): - def tearDown(self): +class TestEnvVars: + def teardown_method(self): # Restore default values Image.core.set_alignment(1) Image.core.set_block_size(1024 * 1024) diff --git a/Tests/test_features.py b/Tests/test_features.py index 10799df33..7cfa08071 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -44,6 +44,13 @@ def test_check_modules(): assert features.check_codec(feature) in [True, False] +def test_check_warns_on_nonexistent(): + with pytest.warns(UserWarning) as cm: + has_feature = features.check("typo") + assert has_feature is False + assert str(cm[-1].message) == "Unknown feature 'typo'." + + def test_supported_modules(): assert isinstance(features.get_supported_modules(), list) assert isinstance(features.get_supported_codecs(), list) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index d0bfe6eda..94c469c7f 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,20 +1,21 @@ from PIL import Image -from .helper import PillowTestCase, assert_image_equal +from .helper import assert_image_equal -class TestFileBlp(PillowTestCase): - def test_load_blp2_raw(self): - with Image.open("Tests/images/blp/blp2_raw.blp") as im: - with Image.open("Tests/images/blp/blp2_raw.png") as target: - assert_image_equal(im, target) +def test_load_blp2_raw(): + with Image.open("Tests/images/blp/blp2_raw.blp") as im: + with Image.open("Tests/images/blp/blp2_raw.png") as target: + assert_image_equal(im, target) - def test_load_blp2_dxt1(self): - with Image.open("Tests/images/blp/blp2_dxt1.blp") as im: - with Image.open("Tests/images/blp/blp2_dxt1.png") as target: - assert_image_equal(im, target) - def test_load_blp2_dxt1a(self): - with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: - with Image.open("Tests/images/blp/blp2_dxt1a.png") as target: - assert_image_equal(im, target) +def test_load_blp2_dxt1(): + with Image.open("Tests/images/blp/blp2_dxt1.blp") as im: + with Image.open("Tests/images/blp/blp2_dxt1.png") as target: + assert_image_equal(im, target) + + +def test_load_blp2_dxt1a(): + with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: + with Image.open("Tests/images/blp/blp2_dxt1a.png") as target: + assert_image_equal(im, target) diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index 67ce8506f..9b4375cd4 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,15 +1,15 @@ from PIL import Image -from .helper import PillowTestCase, assert_image_equal, assert_image_similar +from .helper import assert_image_equal, assert_image_similar -class TestFileFtex(PillowTestCase): - def test_load_raw(self): - with Image.open("Tests/images/ftex_uncompressed.ftu") as im: - with Image.open("Tests/images/ftex_uncompressed.png") as target: - assert_image_equal(im, target) +def test_load_raw(): + with Image.open("Tests/images/ftex_uncompressed.ftu") as im: + with Image.open("Tests/images/ftex_uncompressed.png") as target: + assert_image_equal(im, target) - def test_load_dxt1(self): - with Image.open("Tests/images/ftex_dxt1.ftc") as im: - with Image.open("Tests/images/ftex_dxt1.png") as target: - assert_image_similar(im, target.convert("RGBA"), 15) + +def test_load_dxt1(): + with Image.open("Tests/images/ftex_dxt1.ftc") as im: + with Image.open("Tests/images/ftex_dxt1.png") as target: + assert_image_similar(im, target.convert("RGBA"), 15) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index f704ac60a..330451228 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -6,7 +6,6 @@ import pytest from PIL import ExifTags, Image, ImageFile, JpegImagePlugin from .helper import ( - PillowTestCase, assert_image, assert_image_equal, assert_image_similar, @@ -15,14 +14,13 @@ from .helper import ( hopper, is_win32, skip_unless_feature, - unittest, ) TEST_FILE = "Tests/images/hopper.jpg" @skip_unless_feature("jpg") -class TestFileJpeg(PillowTestCase): +class TestFileJpeg: def roundtrip(self, im, **options): out = BytesIO() im.save(out, "JPEG", **options) @@ -62,6 +60,8 @@ class TestFileJpeg(PillowTestCase): ) assert len(im.applist) == 2 + assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" + def test_cmyk(self): # Test CMYK handling. Thanks to Tim and Charlie for test data, # Michael for getting me to look one more time. @@ -101,13 +101,13 @@ class TestFileJpeg(PillowTestCase): assert test(100, 200) == (100, 200) assert test(0) is None # square pixels - def test_icc(self): + def test_icc(self, tmp_path): # Test ICC support with Image.open("Tests/images/rgb.jpg") as im1: icc_profile = im1.info["icc_profile"] assert len(icc_profile) == 3144 # Roundtrip via physical file. - f = self.tempfile("temp.jpg") + f = str(tmp_path / "temp.jpg") im1.save(f, icc_profile=icc_profile) with Image.open(f) as im2: assert im2.info.get("icc_profile") == icc_profile @@ -140,12 +140,12 @@ class TestFileJpeg(PillowTestCase): test(ImageFile.MAXBLOCK + 1) # full buffer block plus one byte test(ImageFile.MAXBLOCK * 4 + 3) # large block - def test_large_icc_meta(self): + def test_large_icc_meta(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/148 # Sometimes the meta data on the icc_profile block is bigger than # Image.MAXBLOCK or the image size. with Image.open("Tests/images/icc_profile_big.jpg") as im: - f = self.tempfile("temp.jpg") + f = str(tmp_path / "temp.jpg") icc_profile = im.info["icc_profile"] # Should not raise IOError for image with icc larger than image size. im.save( @@ -166,9 +166,9 @@ class TestFileJpeg(PillowTestCase): assert im1.bytes >= im2.bytes assert im1.bytes >= im3.bytes - def test_optimize_large_buffer(self): + def test_optimize_large_buffer(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/148 - f = self.tempfile("temp.jpg") + f = str(tmp_path / "temp.jpg") # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", optimize=True) @@ -184,14 +184,14 @@ class TestFileJpeg(PillowTestCase): assert_image_equal(im1, im3) assert im1.bytes >= im3.bytes - def test_progressive_large_buffer(self): - f = self.tempfile("temp.jpg") + def test_progressive_large_buffer(self, tmp_path): + f = str(tmp_path / "temp.jpg") # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", progressive=True) - def test_progressive_large_buffer_highest_quality(self): - f = self.tempfile("temp.jpg") + def test_progressive_large_buffer_highest_quality(self, tmp_path): + f = str(tmp_path / "temp.jpg") im = self.gen_random_image((255, 255)) # this requires more bytes than pixels in the image im.save(f, format="JPEG", progressive=True, quality=100) @@ -202,9 +202,9 @@ class TestFileJpeg(PillowTestCase): im = self.gen_random_image((256, 256), "CMYK") im.save(f, format="JPEG", progressive=True, quality=94) - def test_large_exif(self): + def test_large_exif(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/148 - f = self.tempfile("temp.jpg") + f = str(tmp_path / "temp.jpg") im = hopper() im.save(f, "JPEG", quality=90, exif=b"1" * 65532) @@ -301,7 +301,7 @@ class TestFileJpeg(PillowTestCase): im3 = self.roundtrip(hopper(), quality=0) assert_image(im1, im3.mode, im3.size) - self.assertGreater(im2.bytes, im3.bytes) + assert im2.bytes > im3.bytes def test_smooth(self): im1 = self.roundtrip(hopper()) @@ -346,18 +346,18 @@ class TestFileJpeg(PillowTestCase): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: assert im._getmp() is None - def test_quality_keep(self): + def test_quality_keep(self, tmp_path): # RGB with Image.open("Tests/images/hopper.jpg") as im: - f = self.tempfile("temp.jpg") + f = str(tmp_path / "temp.jpg") im.save(f, quality="keep") # Grayscale with Image.open("Tests/images/hopper_gray.jpg") as im: - f = self.tempfile("temp.jpg") + f = str(tmp_path / "temp.jpg") im.save(f, quality="keep") # CMYK with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: - f = self.tempfile("temp.jpg") + f = str(tmp_path / "temp.jpg") im.save(f, quality="keep") def test_junk_jpeg_header(self): @@ -389,16 +389,16 @@ class TestFileJpeg(PillowTestCase): with pytest.raises(IOError): im.load() - def _n_qtables_helper(self, n, test_file): - with Image.open(test_file) as im: - f = self.tempfile("temp.jpg") - im.save(f, qtables=[[n] * 64] * n) - with Image.open(f) as im: - assert len(im.quantization) == n - reloaded = self.roundtrip(im, qtables="keep") - assert im.quantization == reloaded.quantization + def test_qtables(self, tmp_path): + def _n_qtables_helper(n, test_file): + with Image.open(test_file) as im: + f = str(tmp_path / "temp.jpg") + im.save(f, qtables=[[n] * 64] * n) + with Image.open(f) as im: + assert len(im.quantization) == n + reloaded = self.roundtrip(im, qtables="keep") + assert im.quantization == reloaded.quantization - def test_qtables(self): with Image.open("Tests/images/hopper.jpg") as im: qtables = im.quantization reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) @@ -470,14 +470,14 @@ class TestFileJpeg(PillowTestCase): 30, ) - self._n_qtables_helper(1, "Tests/images/hopper_gray.jpg") - self._n_qtables_helper(1, "Tests/images/pil_sample_rgb.jpg") - self._n_qtables_helper(2, "Tests/images/pil_sample_rgb.jpg") - self._n_qtables_helper(3, "Tests/images/pil_sample_rgb.jpg") - self._n_qtables_helper(1, "Tests/images/pil_sample_cmyk.jpg") - self._n_qtables_helper(2, "Tests/images/pil_sample_cmyk.jpg") - self._n_qtables_helper(3, "Tests/images/pil_sample_cmyk.jpg") - self._n_qtables_helper(4, "Tests/images/pil_sample_cmyk.jpg") + _n_qtables_helper(1, "Tests/images/hopper_gray.jpg") + _n_qtables_helper(1, "Tests/images/pil_sample_rgb.jpg") + _n_qtables_helper(2, "Tests/images/pil_sample_rgb.jpg") + _n_qtables_helper(3, "Tests/images/pil_sample_rgb.jpg") + _n_qtables_helper(1, "Tests/images/pil_sample_cmyk.jpg") + _n_qtables_helper(2, "Tests/images/pil_sample_cmyk.jpg") + _n_qtables_helper(3, "Tests/images/pil_sample_cmyk.jpg") + _n_qtables_helper(4, "Tests/images/pil_sample_cmyk.jpg") # not a sequence with pytest.raises(ValueError): @@ -496,16 +496,16 @@ class TestFileJpeg(PillowTestCase): with pytest.raises(ValueError): self.roundtrip(im, qtables=[[1, 2, 3, 4]]) - @unittest.skipUnless(djpeg_available(), "djpeg not available") + @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg(self): with Image.open(TEST_FILE) as img: img.load_djpeg() assert_image_similar(img, Image.open(TEST_FILE), 0) - @unittest.skipUnless(cjpeg_available(), "cjpeg not available") - def test_save_cjpeg(self): + @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") + def test_save_cjpeg(self, tmp_path): with Image.open(TEST_FILE) as img: - tempfile = self.tempfile("temp.jpg") + tempfile = str(tmp_path / "temp.jpg") JpegImagePlugin._save_cjpeg(img, 0, tempfile) # Default save quality is 75%, so a tiny bit of difference is alright assert_image_similar(img, Image.open(tempfile), 17) @@ -518,9 +518,9 @@ class TestFileJpeg(PillowTestCase): assert tag_ids["RelatedImageWidth"] == 0x1001 assert tag_ids["RelatedImageLength"] == 0x1002 - def test_MAXBLOCK_scaling(self): + def test_MAXBLOCK_scaling(self, tmp_path): im = self.gen_random_image((512, 512)) - f = self.tempfile("temp.jpeg") + f = str(tmp_path / "temp.jpeg") im.save(f, quality=100, optimize=True) with Image.open(f) as reloaded: @@ -555,9 +555,9 @@ class TestFileJpeg(PillowTestCase): with pytest.raises(IOError): img.save(out, "JPEG") - def test_save_tiff_with_dpi(self): + def test_save_tiff_with_dpi(self, tmp_path): # Arrange - outfile = self.tempfile("temp.tif") + outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: # Act @@ -577,8 +577,8 @@ class TestFileJpeg(PillowTestCase): with Image.open("Tests/images/iptc_roundDown.jpg") as im: assert im.info["dpi"] == (2, 2) - def test_save_dpi_rounding(self): - outfile = self.tempfile("temp.jpg") + def test_save_dpi_rounding(self, tmp_path): + outfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.jpg") as im: im.save(outfile, dpi=(72.2, 72.2)) @@ -690,11 +690,11 @@ class TestFileJpeg(PillowTestCase): assert [65504, 24] == apps_13_lengths -@unittest.skipUnless(is_win32(), "Windows only") +@pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") -class TestFileCloseW32(PillowTestCase): - def test_fd_leak(self): - tmpfile = self.tempfile("temp.jpg") +class TestFileCloseW32: + def test_fd_leak(self, tmp_path): + tmpfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.jpg") as im: im.save(tmpfile) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 1a0e9a358..e37b46a41 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -5,7 +5,6 @@ import pytest from PIL import Image, ImageFile, Jpeg2KImagePlugin from .helper import ( - PillowTestCase, assert_image_equal, assert_image_similar, is_big_endian, @@ -13,6 +12,8 @@ from .helper import ( skip_unless_feature, ) +pytestmark = skip_unless_feature("jpg_2000") + test_card = Image.open("Tests/images/test-card.png") test_card.load() @@ -21,190 +22,207 @@ test_card.load() # 'Not enough memory to handle tile data' -@skip_unless_feature("jpg_2000") -class TestFileJpeg2k(PillowTestCase): - def roundtrip(self, im, **options): - out = BytesIO() - im.save(out, "JPEG2000", **options) - test_bytes = out.tell() - out.seek(0) - im = Image.open(out) - im.bytes = test_bytes # for testing only +def roundtrip(im, **options): + out = BytesIO() + im.save(out, "JPEG2000", **options) + test_bytes = out.tell() + out.seek(0) + im = Image.open(out) + im.bytes = test_bytes # for testing only + im.load() + return im + + +def test_sanity(): + # Internal version number + assert re.search(r"\d+\.\d+\.\d+$", Image.core.jp2klib_version) + + with Image.open("Tests/images/test-card-lossless.jp2") as im: + px = im.load() + assert px[0, 0] == (0, 0, 0) + assert im.mode == "RGB" + assert im.size == (640, 480) + assert im.format == "JPEG2000" + assert im.get_format_mimetype() == "image/jp2" + + +def test_jpf(): + with Image.open("Tests/images/balloon.jpf") as im: + assert im.format == "JPEG2000" + assert im.get_format_mimetype() == "image/jpx" + + +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) + + +def test_bytesio(): + with open("Tests/images/test-card-lossless.jp2", "rb") as f: + data = BytesIO(f.read()) + with Image.open(data) as im: im.load() - return im - - def test_sanity(self): - # Internal version number - assert re.search(r"\d+\.\d+\.\d+$", Image.core.jp2klib_version) - - with Image.open("Tests/images/test-card-lossless.jp2") as im: - px = im.load() - assert px[0, 0] == (0, 0, 0) - assert im.mode == "RGB" - assert im.size == (640, 480) - assert im.format == "JPEG2000" - assert im.get_format_mimetype() == "image/jp2" - - def test_jpf(self): - with Image.open("Tests/images/balloon.jpf") as im: - assert im.format == "JPEG2000" - assert im.get_format_mimetype() == "image/jpx" - - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) - - def test_bytesio(self): - with open("Tests/images/test-card-lossless.jp2", "rb") as f: - data = BytesIO(f.read()) - with Image.open(data) as im: - im.load() - assert_image_similar(im, test_card, 1.0e-3) - - # These two test pre-written JPEG 2000 files that were not written with - # PIL (they were made using Adobe Photoshop) - - def test_lossless(self): - with Image.open("Tests/images/test-card-lossless.jp2") as im: - im.load() - outfile = self.tempfile("temp_test-card.png") - im.save(outfile) assert_image_similar(im, test_card, 1.0e-3) - def test_lossy_tiled(self): - with Image.open("Tests/images/test-card-lossy-tiled.jp2") as im: - im.load() - assert_image_similar(im, test_card, 2.0) - def test_lossless_rt(self): - im = self.roundtrip(test_card) - assert_image_equal(im, test_card) +# These two test pre-written JPEG 2000 files that were not written with +# PIL (they were made using Adobe Photoshop) - def test_lossy_rt(self): - im = self.roundtrip(test_card, quality_layers=[20]) + +def test_lossless(tmp_path): + with Image.open("Tests/images/test-card-lossless.jp2") as im: + im.load() + outfile = str(tmp_path / "temp_test-card.png") + im.save(outfile) + assert_image_similar(im, test_card, 1.0e-3) + + +def test_lossy_tiled(): + with Image.open("Tests/images/test-card-lossy-tiled.jp2") as im: + im.load() assert_image_similar(im, test_card, 2.0) - def test_tiled_rt(self): - im = self.roundtrip(test_card, tile_size=(128, 128)) - assert_image_equal(im, test_card) - def test_tiled_offset_rt(self): - im = self.roundtrip( - test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32) - ) - assert_image_equal(im, test_card) +def test_lossless_rt(): + im = roundtrip(test_card) + assert_image_equal(im, test_card) - def test_tiled_offset_too_small(self): + +def test_lossy_rt(): + im = roundtrip(test_card, quality_layers=[20]) + assert_image_similar(im, test_card, 2.0) + + +def test_tiled_rt(): + im = roundtrip(test_card, tile_size=(128, 128)) + assert_image_equal(im, test_card) + + +def test_tiled_offset_rt(): + im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) + assert_image_equal(im, test_card) + + +def test_tiled_offset_too_small(): + with pytest.raises(ValueError): + roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) + + +def test_irreversible_rt(): + im = roundtrip(test_card, irreversible=True, quality_layers=[20]) + assert_image_similar(im, test_card, 2.0) + + +def test_prog_qual_rt(): + im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") + assert_image_similar(im, test_card, 2.0) + + +def test_prog_res_rt(): + im = roundtrip(test_card, num_resolutions=8, progression="RLCP") + assert_image_equal(im, test_card) + + +def test_reduce(): + with Image.open("Tests/images/test-card-lossless.jp2") as im: + im.reduce = 2 + im.load() + assert im.size == (160, 120) + + +def test_layers_type(tmp_path): + outfile = str(tmp_path / "temp_layers.jp2") + for quality_layers in [[100, 50, 10], (100, 50, 10), None]: + test_card.save(outfile, quality_layers=quality_layers) + + for quality_layers in ["quality_layers", ("100", "50", "10")]: with pytest.raises(ValueError): - self.roundtrip( - test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32) - ) - - def test_irreversible_rt(self): - im = self.roundtrip(test_card, irreversible=True, quality_layers=[20]) - assert_image_similar(im, test_card, 2.0) - - def test_prog_qual_rt(self): - im = self.roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") - assert_image_similar(im, test_card, 2.0) - - def test_prog_res_rt(self): - im = self.roundtrip(test_card, num_resolutions=8, progression="RLCP") - assert_image_equal(im, test_card) - - def test_reduce(self): - with Image.open("Tests/images/test-card-lossless.jp2") as im: - im.reduce = 2 - im.load() - assert im.size == (160, 120) - - def test_layers_type(self): - outfile = self.tempfile("temp_layers.jp2") - for quality_layers in [[100, 50, 10], (100, 50, 10), None]: test_card.save(outfile, quality_layers=quality_layers) - for quality_layers in ["quality_layers", ("100", "50", "10")]: - with pytest.raises(ValueError): - test_card.save(outfile, quality_layers=quality_layers) - def test_layers(self): - out = BytesIO() - test_card.save( - out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP" - ) - out.seek(0) +def test_layers(): + out = BytesIO() + test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") + out.seek(0) - with Image.open(out) as im: - im.layers = 1 - im.load() - assert_image_similar(im, test_card, 13) + with Image.open(out) as im: + im.layers = 1 + im.load() + assert_image_similar(im, test_card, 13) - out.seek(0) - with Image.open(out) as im: - im.layers = 3 - im.load() - assert_image_similar(im, test_card, 0.4) + out.seek(0) + with Image.open(out) as im: + im.layers = 3 + im.load() + assert_image_similar(im, test_card, 0.4) - def test_rgba(self): - # Arrange - with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: - with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2: - # Act - j2k.load() - jp2.load() +def test_rgba(): + # Arrange + with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: + with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2: - # Assert - assert j2k.mode == "RGBA" - assert jp2.mode == "RGBA" - - def test_16bit_monochrome_has_correct_mode(self): - with Image.open("Tests/images/16bit.cropped.j2k") as j2k: + # Act j2k.load() - assert j2k.mode == "I;16" - - with Image.open("Tests/images/16bit.cropped.jp2") as jp2: jp2.load() - assert jp2.mode == "I;16" - @pytest.mark.xfail(is_big_endian() and on_ci(), reason="Fails on big-endian") - def test_16bit_monochrome_jp2_like_tiff(self): - with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: - with Image.open("Tests/images/16bit.cropped.jp2") as jp2: - assert_image_similar(jp2, tiff_16bit, 1e-3) + # Assert + assert j2k.mode == "RGBA" + assert jp2.mode == "RGBA" - @pytest.mark.xfail(is_big_endian() and on_ci(), reason="Fails on big-endian") - def test_16bit_monochrome_j2k_like_tiff(self): - with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: - with Image.open("Tests/images/16bit.cropped.j2k") as j2k: - assert_image_similar(j2k, tiff_16bit, 1e-3) - def test_16bit_j2k_roundtrips(self): - with Image.open("Tests/images/16bit.cropped.j2k") as j2k: - im = self.roundtrip(j2k) - assert_image_equal(im, j2k) +def test_16bit_monochrome_has_correct_mode(): + with Image.open("Tests/images/16bit.cropped.j2k") as j2k: + j2k.load() + assert j2k.mode == "I;16" - def test_16bit_jp2_roundtrips(self): + with Image.open("Tests/images/16bit.cropped.jp2") as jp2: + jp2.load() + assert jp2.mode == "I;16" + + +@pytest.mark.xfail(is_big_endian() and on_ci(), reason="Fails on big-endian") +def test_16bit_monochrome_jp2_like_tiff(): + with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: with Image.open("Tests/images/16bit.cropped.jp2") as jp2: - im = self.roundtrip(jp2) - assert_image_equal(im, jp2) + assert_image_similar(jp2, tiff_16bit, 1e-3) - def test_unbound_local(self): - # prepatch, a malformed jp2 file could cause an UnboundLocalError - # exception. - with pytest.raises(IOError): - Image.open("Tests/images/unbound_variable.jp2") - def test_parser_feed(self): - # Arrange - with open("Tests/images/test-card-lossless.jp2", "rb") as f: - data = f.read() +@pytest.mark.xfail(is_big_endian() and on_ci(), reason="Fails on big-endian") +def test_16bit_monochrome_j2k_like_tiff(): + with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: + with Image.open("Tests/images/16bit.cropped.j2k") as j2k: + assert_image_similar(j2k, tiff_16bit, 1e-3) - # Act - p = ImageFile.Parser() - p.feed(data) - # Assert - assert p.image.size == (640, 480) +def test_16bit_j2k_roundtrips(): + with Image.open("Tests/images/16bit.cropped.j2k") as j2k: + im = roundtrip(j2k) + assert_image_equal(im, j2k) + + +def test_16bit_jp2_roundtrips(): + with Image.open("Tests/images/16bit.cropped.jp2") as jp2: + im = roundtrip(jp2) + assert_image_equal(im, jp2) + + +def test_unbound_local(): + # prepatch, a malformed jp2 file could cause an UnboundLocalError exception. + with pytest.raises(IOError): + Image.open("Tests/images/unbound_variable.jp2") + + +def test_parser_feed(): + # Arrange + with open("Tests/images/test-card-lossless.jp2", "rb") as f: + data = f.read() + + # Act + p = ImageFile.Parser() + p.feed(data) + + # Assert + assert p.image.size == (640, 480) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 175ae987f..923bd6107 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -10,7 +10,6 @@ import pytest from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags from .helper import ( - PillowTestCase, assert_image_equal, assert_image_equal_tofile, assert_image_similar, @@ -23,8 +22,8 @@ logger = logging.getLogger(__name__) @skip_unless_feature("libtiff") -class LibTiffTestCase(PillowTestCase): - def _assert_noerr(self, im): +class LibTiffTestCase: + def _assert_noerr(self, tmp_path, im): """Helper tests that assert basic sanity about the g4 tiff reading""" # 1 bit assert im.mode == "1" @@ -40,7 +39,7 @@ class LibTiffTestCase(PillowTestCase): print(dir(im)) # can we write it back out, in a different form. - out = self.tempfile("temp.png") + out = str(tmp_path / "temp.png") im.save(out) out_bytes = io.BytesIO() @@ -48,29 +47,29 @@ class LibTiffTestCase(PillowTestCase): class TestFileLibTiff(LibTiffTestCase): - def test_g4_tiff(self): + def test_g4_tiff(self, tmp_path): """Test the ordinary file path load path""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as im: assert im.size == (500, 500) - self._assert_noerr(im) + self._assert_noerr(tmp_path, im) - def test_g4_large(self): + def test_g4_large(self, tmp_path): test_file = "Tests/images/pport_g4.tif" with Image.open(test_file) as im: - self._assert_noerr(im) + self._assert_noerr(tmp_path, im) - def test_g4_tiff_file(self): + def test_g4_tiff_file(self, tmp_path): """Testing the string load path""" test_file = "Tests/images/hopper_g4_500.tif" with open(test_file, "rb") as f: with Image.open(f) as im: assert im.size == (500, 500) - self._assert_noerr(im) + self._assert_noerr(tmp_path, im) - def test_g4_tiff_bytesio(self): + def test_g4_tiff_bytesio(self, tmp_path): """Testing the stringio loading code path""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() @@ -79,9 +78,9 @@ class TestFileLibTiff(LibTiffTestCase): s.seek(0) with Image.open(s) as im: assert im.size == (500, 500) - self._assert_noerr(im) + self._assert_noerr(tmp_path, im) - def test_g4_non_disk_file_object(self): + def test_g4_non_disk_file_object(self, tmp_path): """Testing loading from non-disk non-BytesIO file object""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() @@ -91,7 +90,7 @@ class TestFileLibTiff(LibTiffTestCase): r = io.BufferedReader(s) with Image.open(r) as im: assert im.size == (500, 500) - self._assert_noerr(im) + self._assert_noerr(tmp_path, im) def test_g4_eq_png(self): """ Checking that we're actually getting the data that we expect""" @@ -106,18 +105,18 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/g4-fillorder-test.tif") as g4: assert_image_equal(g4, png) - def test_g4_write(self): + def test_g4_write(self, tmp_path): """Checking to see that the saved image is the same as what we wrote""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") rot = orig.transpose(Image.ROTATE_90) assert rot.size == (500, 500) rot.save(out) with Image.open(out) as reread: assert reread.size == (500, 500) - self._assert_noerr(reread) + self._assert_noerr(tmp_path, reread) assert_image_equal(reread, rot) assert reread.info["compression"] == "group4" @@ -135,10 +134,10 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_write_metadata(self): + def test_write_metadata(self, tmp_path): """ Test metadata writing through libtiff """ for legacy_api in [False, True]: - f = self.tempfile("temp.tiff") + f = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper_g4.tif") as img: img.save(f, tiffinfo=img.tag) @@ -183,7 +182,7 @@ class TestFileLibTiff(LibTiffTestCase): for field in requested_fields: assert field in reloaded, "%s not in metadata" % field - def test_additional_metadata(self): + def test_additional_metadata(self, tmp_path): # these should not crash. Seriously dummy data, most of it doesn't make # any sense, so we're running up against limits where we're asking # libtiff to do stupid things. @@ -232,14 +231,14 @@ class TestFileLibTiff(LibTiffTestCase): # Extra samples really doesn't make sense in this application. del new_ifd[338] - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True im.save(out, tiffinfo=new_ifd) TiffImagePlugin.WRITE_LIBTIFF = False - def test_custom_metadata(self): + def test_custom_metadata(self, tmp_path): tc = namedtuple("test_case", "value,type,supported_by_default") custom = { 37000 + k: v @@ -284,7 +283,7 @@ class TestFileLibTiff(LibTiffTestCase): def check_tags(tiffinfo): im = hopper() - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") im.save(out, tiffinfo=tiffinfo) with Image.open(out) as reloaded: @@ -323,26 +322,26 @@ class TestFileLibTiff(LibTiffTestCase): ) TiffImagePlugin.WRITE_LIBTIFF = False - def test_int_dpi(self): + def test_int_dpi(self, tmp_path): # issue #1765 im = hopper("RGB") - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True im.save(out, dpi=(72, 72)) TiffImagePlugin.WRITE_LIBTIFF = False with Image.open(out) as reloaded: assert reloaded.info["dpi"] == (72.0, 72.0) - def test_g3_compression(self): + def test_g3_compression(self, tmp_path): with Image.open("Tests/images/hopper_g4_500.tif") as i: - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") i.save(out, compression="group3") with Image.open(out) as reread: assert reread.info["compression"] == "group3" assert_image_equal(reread, i) - def test_little_endian(self): + def test_little_endian(self, tmp_path): with Image.open("Tests/images/16bit.deflate.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16" @@ -352,7 +351,7 @@ class TestFileLibTiff(LibTiffTestCase): assert b[0] == ord(b"\xe0") assert b[1] == ord(b"\x01") - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") # out = "temp.le.tif" im.save(out) with Image.open(out) as reread: @@ -361,7 +360,7 @@ class TestFileLibTiff(LibTiffTestCase): # UNDONE - libtiff defaults to writing in native endian, so # on big endian, we'll get back mode = 'I;16B' here. - def test_big_endian(self): + def test_big_endian(self, tmp_path): with Image.open("Tests/images/16bit.MM.deflate.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16B" @@ -372,17 +371,17 @@ class TestFileLibTiff(LibTiffTestCase): assert b[0] == ord(b"\x01") assert b[1] == ord(b"\xe0") - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") im.save(out) with Image.open(out) as reread: assert reread.info["compression"] == im.info["compression"] assert reread.getpixel((0, 0)) == 480 - def test_g4_string_info(self): + def test_g4_string_info(self, tmp_path): """Tests String data in info directory""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") orig.tag[269] = "temp.tif" orig.save(out) @@ -406,10 +405,10 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") - def test_blur(self): + def test_blur(self, tmp_path): # test case from irc, how to do blur on b/w image # and save to compressed tif. - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") with Image.open("Tests/images/pport_g4.tif") as im: im = im.convert("L") @@ -421,11 +420,11 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal(im, im2) - def test_compressions(self): + def test_compressions(self, tmp_path): # Test various tiff compressions and assert similar image content but reduced # file sizes. im = hopper("RGB") - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") im.save(out) size_raw = os.path.getsize(out) @@ -449,9 +448,9 @@ class TestFileLibTiff(LibTiffTestCase): assert size_compressed > size_jpeg assert size_jpeg > size_jpeg_30 - def test_quality(self): + def test_quality(self, tmp_path): im = hopper("RGB") - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") with pytest.raises(ValueError): im.save(out, compression="tiff_lzw", quality=50) @@ -464,21 +463,21 @@ class TestFileLibTiff(LibTiffTestCase): im.save(out, compression="jpeg", quality=0) im.save(out, compression="jpeg", quality=100) - def test_cmyk_save(self): + def test_cmyk_save(self, tmp_path): im = hopper("CMYK") - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") im.save(out, compression="tiff_adobe_deflate") with Image.open(out) as im2: assert_image_equal(im, im2) - def xtest_bw_compression_w_rgb(self): + def xtest_bw_compression_w_rgb(self, tmp_path): """ This test passes, but when running all tests causes a failure due to output on stderr from the error thrown by libtiff. We need to capture that but not now""" im = hopper("RGB") - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") with pytest.raises(IOError): im.save(out, compression="tiff_ccitt") @@ -619,22 +618,22 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False - def test_crashing_metadata(self): + def test_crashing_metadata(self, tmp_path): # issue 1597 with Image.open("Tests/images/rdf.tif") as im: - out = self.tempfile("temp.tif") + out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True # this shouldn't crash im.save(out, format="TIFF") TiffImagePlugin.WRITE_LIBTIFF = False - def test_page_number_x_0(self): + def test_page_number_x_0(self, tmp_path): # Issue 973 # Test TIFF with tag 297 (Page Number) having value of 0 0. # The first number is the current page number. # The second is the total number of pages, zero means not available. - outfile = self.tempfile("temp.tif") + outfile = str(tmp_path / "temp.tif") # Created by printing a page in Chrome to PDF, then: # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif # -dNOPAUSE /tmp/test.pdf -c quit @@ -643,10 +642,10 @@ class TestFileLibTiff(LibTiffTestCase): # Should not divide by zero im.save(outfile) - def test_fd_duplication(self): + def test_fd_duplication(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/1651 - tmpfile = self.tempfile("temp.tif") + tmpfile = str(tmp_path / "temp.tif") with open(tmpfile, "wb") as f: with open("Tests/images/g4-multi.tiff", "rb") as src: f.write(src.read()) @@ -685,9 +684,9 @@ class TestFileLibTiff(LibTiffTestCase): assert im.size == (10, 10) im.load() - def test_save_tiff_with_jpegtables(self): + def test_save_tiff_with_jpegtables(self, tmp_path): # Arrange - outfile = self.tempfile("temp.tif") + outfile = str(tmp_path / "temp.tif") # Created with ImageMagick: convert hopper.jpg hopper_jpg.tif # Contains JPEGTables (347) tag diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 5bac8ebf7..593a8eda8 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -15,16 +15,16 @@ class TestFileLibTiffSmall(LibTiffTestCase): file just before reading in libtiff. These tests remain to ensure that it stays fixed. """ - def test_g4_hopper_file(self): + def test_g4_hopper_file(self, tmp_path): """Testing the open file load path""" test_file = "Tests/images/hopper_g4.tif" with open(test_file, "rb") as f: with Image.open(f) as im: assert im.size == (128, 128) - self._assert_noerr(im) + self._assert_noerr(tmp_path, im) - def test_g4_hopper_bytesio(self): + def test_g4_hopper_bytesio(self, tmp_path): """Testing the bytesio loading code path""" test_file = "Tests/images/hopper_g4.tif" s = BytesIO() @@ -33,12 +33,12 @@ class TestFileLibTiffSmall(LibTiffTestCase): s.seek(0) with Image.open(s) as im: assert im.size == (128, 128) - self._assert_noerr(im) + self._assert_noerr(tmp_path, im) - def test_g4_hopper(self): + def test_g4_hopper(self, tmp_path): """The 128x128 lena image failed for some reason.""" test_file = "Tests/images/hopper_g4.tif" with Image.open(test_file) as im: assert im.size == (128, 128) - self._assert_noerr(im) + self._assert_noerr(tmp_path, im) diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 909272532..886332dea 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -1,71 +1,90 @@ import os.path +import subprocess import pytest +from PIL import Image from .helper import ( - PillowTestCase, + IMCONVERT, assert_image_equal, hopper, imagemagick_available, skip_known_bad_test, ) +_roundtrip = imagemagick_available() -class TestFilePalm(PillowTestCase): - _roundtrip = imagemagick_available() - def helper_save_as_palm(self, mode): - # Arrange - im = hopper(mode) - outfile = self.tempfile("temp_" + mode + ".palm") +def helper_save_as_palm(tmp_path, mode): + # Arrange + im = hopper(mode) + outfile = str(tmp_path / ("temp_" + mode + ".palm")) - # Act - im.save(outfile) + # Act + im.save(outfile) - # Assert - assert os.path.isfile(outfile) - assert os.path.getsize(outfile) > 0 + # Assert + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 - def roundtrip(self, mode): - if not self._roundtrip: - return - im = hopper(mode) - outfile = self.tempfile("temp.palm") +def open_with_imagemagick(tmp_path, f): + if not imagemagick_available(): + raise OSError() - im.save(outfile) - converted = self.open_withImagemagick(outfile) - assert_image_equal(converted, im) + outfile = str(tmp_path / "temp.png") + rc = subprocess.call( + [IMCONVERT, f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + if rc: + raise OSError + return Image.open(outfile) - def test_monochrome(self): - # Arrange - mode = "1" - # Act / Assert - self.helper_save_as_palm(mode) - self.roundtrip(mode) +def roundtrip(tmp_path, mode): + if not _roundtrip: + return - def test_p_mode(self): - # Arrange - mode = "P" + im = hopper(mode) + outfile = str(tmp_path / "temp.palm") - # Act / Assert - self.helper_save_as_palm(mode) - skip_known_bad_test("Palm P image is wrong") - self.roundtrip(mode) + im.save(outfile) + converted = open_with_imagemagick(tmp_path, outfile) + assert_image_equal(converted, im) - def test_l_ioerror(self): - # Arrange - mode = "L" - # Act / Assert - with pytest.raises(IOError): - self.helper_save_as_palm(mode) +def test_monochrome(tmp_path): + # Arrange + mode = "1" - def test_rgb_ioerror(self): - # Arrange - mode = "RGB" + # Act / Assert + helper_save_as_palm(tmp_path, mode) + roundtrip(tmp_path, mode) - # Act / Assert - with pytest.raises(IOError): - self.helper_save_as_palm(mode) + +def test_p_mode(tmp_path): + # Arrange + mode = "P" + + # Act / Assert + helper_save_as_palm(tmp_path, mode) + skip_known_bad_test("Palm P image is wrong") + roundtrip(tmp_path, mode) + + +def test_l_ioerror(tmp_path): + # Arrange + mode = "L" + + # Act / Assert + with pytest.raises(IOError): + helper_save_as_palm(tmp_path, mode) + + +def test_rgb_ioerror(tmp_path): + # Arrange + mode = "RGB" + + # Act / Assert + with pytest.raises(IOError): + helper_save_as_palm(tmp_path, mode) diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index a59631e1a..dc45a48c1 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -1,18 +1,15 @@ from PIL import Image -from .helper import PillowTestCase +def test_load_raw(): + with Image.open("Tests/images/hopper.pcd") as im: + im.load() # should not segfault. -class TestFilePcd(PillowTestCase): - def test_load_raw(self): - with Image.open("Tests/images/hopper.pcd") as im: - im.load() # should not segfault. + # Note that this image was created with a resized hopper + # image, which was then converted to pcd with imagemagick + # and the colors are wonky in Pillow. It's unclear if this + # is a pillow or a convert issue, as other images not generated + # from convert look find on pillow and not imagemagick. - # Note that this image was created with a resized hopper - # image, which was then converted to pcd with imagemagick - # and the colors are wonky in Pillow. It's unclear if this - # is a pillow or a convert issue, as other images not generated - # from convert look find on pillow and not imagemagick. - - # target = hopper().resize((768,512)) - # assert_image_similar(im, target, 10) + # target = hopper().resize((768,512)) + # assert_image_similar(im, target, 10) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 3ae27213c..5af7469c7 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,130 +1,142 @@ import pytest from PIL import Image, ImageFile, PcxImagePlugin -from .helper import PillowTestCase, assert_image_equal, hopper +from .helper import assert_image_equal, hopper -class TestFilePcx(PillowTestCase): - def _roundtrip(self, im): - f = self.tempfile("temp.pcx") +def _roundtrip(tmp_path, im): + f = str(tmp_path / "temp.pcx") + im.save(f) + with Image.open(f) as im2: + assert im2.mode == im.mode + assert im2.size == im.size + assert im2.format == "PCX" + assert im2.get_format_mimetype() == "image/x-pcx" + assert_image_equal(im2, im) + + +def test_sanity(tmp_path): + for mode in ("1", "L", "P", "RGB"): + _roundtrip(tmp_path, hopper(mode)) + + # Test an unsupported mode + f = str(tmp_path / "temp.pcx") + im = hopper("RGBA") + with pytest.raises(ValueError): im.save(f) - with Image.open(f) as im2: - assert im2.mode == im.mode - assert im2.size == im.size - assert im2.format == "PCX" - assert im2.get_format_mimetype() == "image/x-pcx" - assert_image_equal(im2, im) - def test_sanity(self): - for mode in ("1", "L", "P", "RGB"): - self._roundtrip(hopper(mode)) - # Test an unsupported mode - f = self.tempfile("temp.pcx") - im = hopper("RGBA") - with pytest.raises(ValueError): - im.save(f) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError): + PcxImagePlugin.PcxImageFile(invalid_file) - with pytest.raises(SyntaxError): - PcxImagePlugin.PcxImageFile(invalid_file) - def test_odd(self): - # see issue #523, odd sized images should have a stride that's even. - # not that imagemagick or gimp write pcx that way. - # we were not handling properly. - for mode in ("1", "L", "P", "RGB"): - # larger, odd sized images are better here to ensure that - # we handle interrupted scan lines properly. - self._roundtrip(hopper(mode).resize((511, 511))) +def test_odd(tmp_path): + # See issue #523, odd sized images should have a stride that's even. + # Not that ImageMagick or GIMP write PCX that way. + # We were not handling properly. + for mode in ("1", "L", "P", "RGB"): + # larger, odd sized images are better here to ensure that + # we handle interrupted scan lines properly. + _roundtrip(tmp_path, hopper(mode).resize((511, 511))) - def test_pil184(self): - # Check reading of files where xmin/xmax is not zero. - test_file = "Tests/images/pil184.pcx" - with Image.open(test_file) as im: - assert im.size == (447, 144) - assert im.tile[0][1] == (0, 0, 447, 144) +def test_pil184(): + # Check reading of files where xmin/xmax is not zero. - # Make sure all pixels are either 0 or 255. - assert im.histogram()[0] + im.histogram()[255] == 447 * 144 + test_file = "Tests/images/pil184.pcx" + with Image.open(test_file) as im: + assert im.size == (447, 144) + assert im.tile[0][1] == (0, 0, 447, 144) - def test_1px_width(self): - im = Image.new("L", (1, 256)) - px = im.load() - for y in range(256): - px[0, y] = y - self._roundtrip(im) + # Make sure all pixels are either 0 or 255. + assert im.histogram()[0] + im.histogram()[255] == 447 * 144 - def test_large_count(self): - im = Image.new("L", (256, 1)) - px = im.load() + +def test_1px_width(tmp_path): + im = Image.new("L", (1, 256)) + px = im.load() + for y in range(256): + px[0, y] = y + _roundtrip(tmp_path, im) + + +def test_large_count(tmp_path): + im = Image.new("L", (256, 1)) + px = im.load() + for x in range(256): + px[x, 0] = x // 67 * 67 + _roundtrip(tmp_path, im) + + +def _test_buffer_overflow(tmp_path, im, size=1024): + _last = ImageFile.MAXBLOCK + ImageFile.MAXBLOCK = size + try: + _roundtrip(tmp_path, im) + finally: + ImageFile.MAXBLOCK = _last + + +def test_break_in_count_overflow(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(4): for x in range(256): - px[x, 0] = x // 67 * 67 - self._roundtrip(im) + px[x, y] = x % 128 + _test_buffer_overflow(tmp_path, im) - def _test_buffer_overflow(self, im, size=1024): - _last = ImageFile.MAXBLOCK - ImageFile.MAXBLOCK = size - try: - self._roundtrip(im) - finally: - ImageFile.MAXBLOCK = _last - def test_break_in_count_overflow(self): - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(4): - for x in range(256): - px[x, y] = x % 128 - self._test_buffer_overflow(im) +def test_break_one_in_loop(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(5): + for x in range(256): + px[x, y] = x % 128 + _test_buffer_overflow(tmp_path, im) - def test_break_one_in_loop(self): - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(5): - for x in range(256): - px[x, y] = x % 128 - self._test_buffer_overflow(im) - def test_break_many_in_loop(self): - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(4): - for x in range(256): - px[x, y] = x % 128 - for x in range(8): - px[x, 4] = 16 - self._test_buffer_overflow(im) +def test_break_many_in_loop(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(4): + for x in range(256): + px[x, y] = x % 128 + for x in range(8): + px[x, 4] = 16 + _test_buffer_overflow(tmp_path, im) - def test_break_one_at_end(self): - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(5): - for x in range(256): - px[x, y] = x % 128 - px[0, 3] = 128 + 64 - self._test_buffer_overflow(im) - def test_break_many_at_end(self): - im = Image.new("L", (256, 5)) - px = im.load() - for y in range(5): - for x in range(256): - px[x, y] = x % 128 - for x in range(4): - px[x * 2, 3] = 128 + 64 - px[x + 256 - 4, 3] = 0 - self._test_buffer_overflow(im) +def test_break_one_at_end(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(5): + for x in range(256): + px[x, y] = x % 128 + px[0, 3] = 128 + 64 + _test_buffer_overflow(tmp_path, im) - def test_break_padding(self): - im = Image.new("L", (257, 5)) - px = im.load() - for y in range(5): - for x in range(257): - px[x, y] = x % 128 - for x in range(5): - px[x, 3] = 0 - self._test_buffer_overflow(im) + +def test_break_many_at_end(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(5): + for x in range(256): + px[x, y] = x % 128 + for x in range(4): + px[x * 2, 3] = 128 + 64 + px[x + 256 - 4, 3] = 0 + _test_buffer_overflow(tmp_path, im) + + +def test_break_padding(tmp_path): + im = Image.new("L", (257, 5)) + px = im.load() + for y in range(5): + for x in range(257): + px[x, y] = x % 128 + for x in range(5): + px[x, 3] = 0 + _test_buffer_overflow(tmp_path, im) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 54bbd4bdc..b6c95eb32 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,5 +1,4 @@ import re -import unittest import zlib from io import BytesIO @@ -8,7 +7,6 @@ from PIL import Image, ImageFile, PngImagePlugin from .helper import ( PillowLeakTestCase, - PillowTestCase, assert_image, assert_image_equal, hopper, @@ -55,7 +53,7 @@ def roundtrip(im, **options): @skip_unless_feature("zlib") -class TestFilePng(PillowTestCase): +class TestFilePng: def get_chunks(self, filename): chunks = [] with open(filename, "rb") as fp: @@ -72,12 +70,12 @@ class TestFilePng(PillowTestCase): return chunks @pytest.mark.xfail(is_big_endian() and on_ci(), reason="Fails on big-endian") - def test_sanity(self): + def test_sanity(self, tmp_path): # internal version number assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", Image.core.zlib_version) - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") hopper("RGB").save(test_file) @@ -232,14 +230,14 @@ class TestFilePng(PillowTestCase): # image has 876 transparent pixels assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_p_transparent_palette(self): + def test_save_p_transparent_palette(self, tmp_path): in_file = "Tests/images/pil123p.png" with Image.open(in_file) as im: # 'transparency' contains a byte string with the opacity for # each palette entry assert len(im.info["transparency"]) == 256 - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") im.save(test_file) # check if saved image contains same transparency @@ -253,14 +251,14 @@ class TestFilePng(PillowTestCase): # image has 124 unique alpha values assert len(im.getchannel("A").getcolors()) == 124 - def test_save_p_single_transparency(self): + def test_save_p_single_transparency(self, tmp_path): in_file = "Tests/images/p_trns_single.png" with Image.open(in_file) as im: # pixel value 164 is full transparent assert im.info["transparency"] == 164 assert im.getpixel((31, 31)) == 164 - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") im.save(test_file) # check if saved image contains same transparency @@ -276,14 +274,14 @@ class TestFilePng(PillowTestCase): # image has 876 transparent pixels assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_p_transparent_black(self): + def test_save_p_transparent_black(self, tmp_path): # check if solid black image with full transparency # is supported (check for #1838) im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) assert im.getcolors() == [(100, (0, 0, 0, 0))] im = im.convert("P") - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") im.save(test_file) # check if saved image contains same transparency @@ -294,7 +292,7 @@ class TestFilePng(PillowTestCase): assert_image(im, "RGBA", (10, 10)) assert im.getcolors() == [(100, (0, 0, 0, 0))] - def test_save_greyscale_transparency(self): + def test_save_greyscale_transparency(self, tmp_path): for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items(): in_file = "Tests/images/" + mode.lower() + "_trns.png" with Image.open(in_file) as im: @@ -304,7 +302,7 @@ class TestFilePng(PillowTestCase): im_rgba = im.convert("RGBA") assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") im.save(test_file) with Image.open(test_file) as test_im: @@ -315,10 +313,10 @@ class TestFilePng(PillowTestCase): test_im_rgba = test_im.convert("RGBA") assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent - def test_save_rgb_single_transparency(self): + def test_save_rgb_single_transparency(self, tmp_path): in_file = "Tests/images/caption_6_33_22.png" with Image.open(in_file) as im: - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") im.save(test_file) def test_load_verify(self): @@ -483,12 +481,12 @@ class TestFilePng(PillowTestCase): im = roundtrip(im, transparency=(0, 1, 2)) assert im.info["transparency"] == (0, 1, 2) - def test_trns_p(self): + def test_trns_p(self, tmp_path): # Check writing a transparency of 0, issue #528 im = hopper("P") im.info["transparency"] = 0 - f = self.tempfile("temp.png") + f = str(tmp_path / "temp.png") im.save(f) with Image.open(f) as im2: @@ -539,9 +537,9 @@ class TestFilePng(PillowTestCase): assert repr_png.format == "PNG" assert_image_equal(im, repr_png) - def test_chunk_order(self): + def test_chunk_order(self, tmp_path): with Image.open("Tests/images/icc_profile.png") as im: - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") im.convert("P").save(test_file, dpi=(100, 100)) chunks = self.get_chunks(test_file) @@ -598,34 +596,34 @@ class TestFilePng(PillowTestCase): exif = im._getexif() assert exif[274] == 1 - def test_exif_save(self): + def test_exif_save(self, tmp_path): with Image.open("Tests/images/exif.png") as im: - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") im.save(test_file) with Image.open(test_file) as reloaded: exif = reloaded._getexif() assert exif[274] == 1 - def test_exif_from_jpg(self): + def test_exif_from_jpg(self, tmp_path): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") im.save(test_file) with Image.open(test_file) as reloaded: exif = reloaded._getexif() assert exif[305] == "Adobe Photoshop CS Macintosh" - def test_exif_argument(self): + def test_exif_argument(self, tmp_path): with Image.open(TEST_PNG_FILE) as im: - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") im.save(test_file, exif=b"exifstring") with Image.open(test_file) as reloaded: assert reloaded.info["exif"] == b"Exif\x00\x00exifstring" -@unittest.skipIf(is_win32(), "requires Unix or macOS") +@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @skip_unless_feature("zlib") class TestTruncatedPngPLeaks(PillowLeakTestCase): mem_limit = 2 * 1024 # max increase in K diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index dc8daa98f..6b91ba28a 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,77 +1,82 @@ import pytest from PIL import Image -from .helper import PillowTestCase, assert_image_equal, assert_image_similar, hopper +from .helper import assert_image_equal, assert_image_similar, hopper # sample ppm stream -test_file = "Tests/images/hopper.ppm" +TEST_FILE = "Tests/images/hopper.ppm" -class TestFilePpm(PillowTestCase): - def test_sanity(self): - with Image.open(test_file) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format, "PPM" - assert im.get_format_mimetype() == "image/x-portable-pixmap" +def test_sanity(): + with Image.open(TEST_FILE) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format, "PPM" + assert im.get_format_mimetype() == "image/x-portable-pixmap" - def test_16bit_pgm(self): - with Image.open("Tests/images/16_bit_binary.pgm") as im: - im.load() - assert im.mode == "I" - assert im.size == (20, 100) - assert im.get_format_mimetype() == "image/x-portable-graymap" - with Image.open("Tests/images/16_bit_binary_pgm.png") as tgt: - assert_image_equal(im, tgt) +def test_16bit_pgm(): + with Image.open("Tests/images/16_bit_binary.pgm") as im: + im.load() + assert im.mode == "I" + assert im.size == (20, 100) + assert im.get_format_mimetype() == "image/x-portable-graymap" - def test_16bit_pgm_write(self): - with Image.open("Tests/images/16_bit_binary.pgm") as im: - im.load() + with Image.open("Tests/images/16_bit_binary_pgm.png") as tgt: + assert_image_equal(im, tgt) - f = self.tempfile("temp.pgm") - im.save(f, "PPM") - with Image.open(f) as reloaded: - assert_image_equal(im, reloaded) +def test_16bit_pgm_write(tmp_path): + with Image.open("Tests/images/16_bit_binary.pgm") as im: + im.load() - def test_pnm(self): - with Image.open("Tests/images/hopper.pnm") as im: - assert_image_similar(im, hopper(), 0.0001) + f = str(tmp_path / "temp.pgm") + im.save(f, "PPM") - f = self.tempfile("temp.pnm") - im.save(f) + with Image.open(f) as reloaded: + assert_image_equal(im, reloaded) - with Image.open(f) as reloaded: - assert_image_equal(im, reloaded) - def test_truncated_file(self): - path = self.tempfile("temp.pgm") - with open(path, "w") as f: - f.write("P6") +def test_pnm(tmp_path): + with Image.open("Tests/images/hopper.pnm") as im: + assert_image_similar(im, hopper(), 0.0001) - with pytest.raises(ValueError): - Image.open(path) + f = str(tmp_path / "temp.pnm") + im.save(f) - def test_neg_ppm(self): - # Storage.c accepted negative values for xsize, ysize. the - # internal open_ppm function didn't check for sanity but it - # has been removed. The default opener doesn't accept negative - # sizes. + with Image.open(f) as reloaded: + assert_image_equal(im, reloaded) - with pytest.raises(IOError): - Image.open("Tests/images/negative_size.ppm") - def test_mimetypes(self): - path = self.tempfile("temp.pgm") +def test_truncated_file(tmp_path): + path = str(tmp_path / "temp.pgm") + with open(path, "w") as f: + f.write("P6") - with open(path, "w") as f: - f.write("P4\n128 128\n255") - with Image.open(path) as im: - assert im.get_format_mimetype() == "image/x-portable-bitmap" + with pytest.raises(ValueError): + Image.open(path) - with open(path, "w") as f: - f.write("PyCMYK\n128 128\n255") - with Image.open(path) as im: - assert im.get_format_mimetype() == "image/x-portable-anymap" + +def test_neg_ppm(): + # Storage.c accepted negative values for xsize, ysize. the + # internal open_ppm function didn't check for sanity but it + # has been removed. The default opener doesn't accept negative + # sizes. + + with pytest.raises(IOError): + Image.open("Tests/images/negative_size.ppm") + + +def test_mimetypes(tmp_path): + path = str(tmp_path / "temp.pgm") + + with open(path, "w") as f: + f.write("P4\n128 128\n255") + with Image.open(path) as im: + assert im.get_format_mimetype() == "image/x-portable-bitmap" + + with open(path, "w") as f: + f.write("PyCMYK\n128 128\n255") + with Image.open(path) as im: + assert im.get_format_mimetype() == "image/x-portable-anymap" diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 4a606548c..cb16276ce 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,92 +1,100 @@ import pytest from PIL import Image, SgiImagePlugin -from .helper import PillowTestCase, assert_image_equal, assert_image_similar, hopper +from .helper import assert_image_equal, assert_image_similar, hopper -class TestFileSgi(PillowTestCase): - def test_rgb(self): - # Created with ImageMagick then renamed: - # convert hopper.ppm -compress None sgi:hopper.rgb - test_file = "Tests/images/hopper.rgb" +def test_rgb(): + # Created with ImageMagick then renamed: + # convert hopper.ppm -compress None sgi:hopper.rgb + test_file = "Tests/images/hopper.rgb" - with Image.open(test_file) as im: - assert_image_equal(im, hopper()) - assert im.get_format_mimetype() == "image/rgb" + with Image.open(test_file) as im: + assert_image_equal(im, hopper()) + assert im.get_format_mimetype() == "image/rgb" - def test_rgb16(self): - test_file = "Tests/images/hopper16.rgb" - with Image.open(test_file) as im: - assert_image_equal(im, hopper()) +def test_rgb16(): + test_file = "Tests/images/hopper16.rgb" - def test_l(self): - # Created with ImageMagick - # convert hopper.ppm -monochrome -compress None sgi:hopper.bw - test_file = "Tests/images/hopper.bw" + with Image.open(test_file) as im: + assert_image_equal(im, hopper()) - with Image.open(test_file) as im: - assert_image_similar(im, hopper("L"), 2) - assert im.get_format_mimetype() == "image/sgi" - def test_rgba(self): - # Created with ImageMagick: - # convert transparent.png -compress None transparent.sgi - test_file = "Tests/images/transparent.sgi" +def test_l(): + # Created with ImageMagick + # convert hopper.ppm -monochrome -compress None sgi:hopper.bw + test_file = "Tests/images/hopper.bw" - with Image.open(test_file) as im: - with Image.open("Tests/images/transparent.png") as target: - assert_image_equal(im, target) - assert im.get_format_mimetype() == "image/sgi" + with Image.open(test_file) as im: + assert_image_similar(im, hopper("L"), 2) + assert im.get_format_mimetype() == "image/sgi" - def test_rle(self): - # Created with ImageMagick: - # convert hopper.ppm hopper.sgi - test_file = "Tests/images/hopper.sgi" - with Image.open(test_file) as im: - with Image.open("Tests/images/hopper.rgb") as target: - assert_image_equal(im, target) +def test_rgba(): + # Created with ImageMagick: + # convert transparent.png -compress None transparent.sgi + test_file = "Tests/images/transparent.sgi" - def test_rle16(self): - test_file = "Tests/images/tv16.sgi" + with Image.open(test_file) as im: + with Image.open("Tests/images/transparent.png") as target: + assert_image_equal(im, target) + assert im.get_format_mimetype() == "image/sgi" - with Image.open(test_file) as im: - with Image.open("Tests/images/tv.rgb") as target: - assert_image_equal(im, target) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" +def test_rle(): + # Created with ImageMagick: + # convert hopper.ppm hopper.sgi + test_file = "Tests/images/hopper.sgi" - with pytest.raises(ValueError): - SgiImagePlugin.SgiImageFile(invalid_file) + with Image.open(test_file) as im: + with Image.open("Tests/images/hopper.rgb") as target: + assert_image_equal(im, target) - def test_write(self): - def roundtrip(img): - out = self.tempfile("temp.sgi") - img.save(out, format="sgi") - with Image.open(out) as reloaded: - assert_image_equal(img, reloaded) - for mode in ("L", "RGB", "RGBA"): - roundtrip(hopper(mode)) +def test_rle16(): + test_file = "Tests/images/tv16.sgi" - # Test 1 dimension for an L mode image - roundtrip(Image.new("L", (10, 1))) + with Image.open(test_file) as im: + with Image.open("Tests/images/tv.rgb") as target: + assert_image_equal(im, target) - def test_write16(self): - test_file = "Tests/images/hopper16.rgb" - with Image.open(test_file) as im: - out = self.tempfile("temp.sgi") - im.save(out, format="sgi", bpc=2) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - with Image.open(out) as reloaded: - assert_image_equal(im, reloaded) + with pytest.raises(ValueError): + SgiImagePlugin.SgiImageFile(invalid_file) - def test_unsupported_mode(self): - im = hopper("LA") - out = self.tempfile("temp.sgi") - with pytest.raises(ValueError): - im.save(out, format="sgi") +def test_write(tmp_path): + def roundtrip(img): + out = str(tmp_path / "temp.sgi") + img.save(out, format="sgi") + with Image.open(out) as reloaded: + assert_image_equal(img, reloaded) + + for mode in ("L", "RGB", "RGBA"): + roundtrip(hopper(mode)) + + # Test 1 dimension for an L mode image + roundtrip(Image.new("L", (10, 1))) + + +def test_write16(tmp_path): + test_file = "Tests/images/hopper16.rgb" + + with Image.open(test_file) as im: + out = str(tmp_path / "temp.sgi") + im.save(out, format="sgi", bpc=2) + + with Image.open(out) as reloaded: + assert_image_equal(im, reloaded) + + +def test_unsupported_mode(tmp_path): + im = hopper("LA") + out = str(tmp_path / "temp.sgi") + + with pytest.raises(ValueError): + im.save(out, format="sgi") diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 0569e15cc..a996f0b0e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,6 +1,5 @@ import logging import os -import unittest from io import BytesIO import pytest @@ -8,7 +7,6 @@ from PIL import Image, TiffImagePlugin from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION from .helper import ( - PillowTestCase, assert_image_equal, assert_image_equal_tofile, assert_image_similar, @@ -21,10 +19,10 @@ from .helper import ( logger = logging.getLogger(__name__) -class TestFileTiff(PillowTestCase): - def test_sanity(self): +class TestFileTiff: + def test_sanity(self, tmp_path): - filename = self.tempfile("temp.tif") + filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename) @@ -54,7 +52,7 @@ class TestFileTiff(PillowTestCase): with Image.open(filename): pass - @unittest.skipIf(is_pypy(), "Requires CPython") + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(self): def open(): im = Image.open("Tests/images/multipage.tiff") @@ -155,8 +153,8 @@ class TestFileTiff(PillowTestCase): assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit assert im.info["dpi"] == (dpi[1], dpi[1]) - def test_save_dpi_rounding(self): - outfile = self.tempfile("temp.tif") + def test_save_dpi_rounding(self, tmp_path): + outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: for dpi in (72.2, 72.8): im.save(outfile, dpi=(dpi, dpi)) @@ -190,14 +188,14 @@ class TestFileTiff(PillowTestCase): # Should not raise struct.error. pytest.warns(UserWarning, i._getexif) - def test_save_rgba(self): + def test_save_rgba(self, tmp_path): im = hopper("RGBA") - outfile = self.tempfile("temp.tif") + outfile = str(tmp_path / "temp.tif") im.save(outfile) - def test_save_unsupported_mode(self): + def test_save_unsupported_mode(self, tmp_path): im = hopper("HSV") - outfile = self.tempfile("temp.tif") + outfile = str(tmp_path / "temp.tif") with pytest.raises(IOError): im.save(outfile) @@ -459,9 +457,9 @@ class TestFileTiff(PillowTestCase): assert im2.mode == "L" assert_image_equal(im, im2) - def test_with_underscores(self): + def test_with_underscores(self, tmp_path): kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} - filename = self.tempfile("temp.tif") + filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename, **kwargs) with Image.open(filename) as im: @@ -473,14 +471,14 @@ class TestFileTiff(PillowTestCase): assert im.tag_v2[X_RESOLUTION] == 72 assert im.tag_v2[Y_RESOLUTION] == 36 - def test_roundtrip_tiff_uint16(self): + def test_roundtrip_tiff_uint16(self, tmp_path): # Test an image of all '0' values pixel_value = 0x1234 infile = "Tests/images/uint16_1_4660.tif" with Image.open(infile) as im: assert im.getpixel((0, 0)) == pixel_value - tmpfile = self.tempfile("temp.tif") + tmpfile = str(tmp_path / "temp.tif") im.save(tmpfile) with Image.open(tmpfile) as reloaded: @@ -512,9 +510,9 @@ class TestFileTiff(PillowTestCase): with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_palette(self): - for mode in ["P", "PA"]: - outfile = self.tempfile("temp.tif") + def test_palette(self, tmp_path): + def roundtrip(mode): + outfile = str(tmp_path / "temp.tif") im = hopper(mode) im.save(outfile) @@ -522,6 +520,9 @@ class TestFileTiff(PillowTestCase): with Image.open(outfile) as reloaded: assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) + for mode in ["P", "PA"]: + roundtrip(mode) + def test_tiff_save_all(self): mp = BytesIO() with Image.open("Tests/images/multipage.tiff") as im: @@ -552,7 +553,7 @@ class TestFileTiff(PillowTestCase): with Image.open(mp) as reread: assert reread.n_frames == 3 - def test_saving_icc_profile(self): + def test_saving_icc_profile(self, tmp_path): # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs # as libtiff does not support embedded ICC profiles, @@ -561,14 +562,14 @@ class TestFileTiff(PillowTestCase): im.info["icc_profile"] = "Dummy value" # Try save-load round trip to make sure both handle icc_profile. - tmpfile = self.tempfile("temp.tif") + tmpfile = str(tmp_path / "temp.tif") im.save(tmpfile, "TIFF", compression="raw") with Image.open(tmpfile) as reloaded: assert b"Dummy value" == reloaded.info["icc_profile"] - def test_close_on_load_exclusive(self): + def test_close_on_load_exclusive(self, tmp_path): # similar to test_fd_leak, but runs on unixlike os - tmpfile = self.tempfile("temp.tif") + tmpfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) @@ -579,8 +580,8 @@ class TestFileTiff(PillowTestCase): im.load() assert fp.closed - def test_close_on_load_nonexclusive(self): - tmpfile = self.tempfile("temp.tif") + def test_close_on_load_nonexclusive(self, tmp_path): + tmpfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) @@ -601,10 +602,10 @@ class TestFileTiff(PillowTestCase): Image.open("Tests/images/string_dimension.tiff") -@unittest.skipUnless(is_win32(), "Windows only") -class TestFileTiffW32(PillowTestCase): - def test_fd_leak(self): - tmpfile = self.tempfile("temp.tif") +@pytest.mark.skipif(not is_win32(), reason="Windows only") +class TestFileTiffW32: + def test_fd_leak(self, tmp_path): + tmpfile = str(tmp_path / "temp.tif") # this is an mmaped file. with Image.open("Tests/images/uint16_1_4660.tif") as im: diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 5554a25e9..9fe601bd6 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -5,321 +5,335 @@ import pytest from PIL import Image, TiffImagePlugin, TiffTags from PIL.TiffImagePlugin import IFDRational -from .helper import PillowTestCase, assert_deep_equal, hopper - -tag_ids = {info.name: info.value for info in TiffTags.TAGS_V2.values()} - - -class TestFileTiffMetadata(PillowTestCase): - def test_rt_metadata(self): - """ Test writing arbitrary metadata into the tiff image directory - Use case is ImageJ private tags, one numeric, one arbitrary - data. https://github.com/python-pillow/Pillow/issues/291 - """ - - img = hopper() - - # Behaviour change: re #1416 - # Pre ifd rewrite, ImageJMetaData was being written as a string(2), - # Post ifd rewrite, it's defined as arbitrary bytes(7). It should - # roundtrip with the actual bytes, rather than stripped text - # of the premerge tests. - # - # For text items, we still have to decode('ascii','replace') because - # the tiff file format can't take 8 bit bytes in that field. - - basetextdata = "This is some arbitrary metadata for a text field" - bindata = basetextdata.encode("ascii") + b" \xff" - textdata = basetextdata + " " + chr(255) - reloaded_textdata = basetextdata + " ?" - floatdata = 12.345 - doubledata = 67.89 - info = TiffImagePlugin.ImageFileDirectory() - - ImageJMetaData = tag_ids["ImageJMetaData"] - ImageJMetaDataByteCounts = tag_ids["ImageJMetaDataByteCounts"] - ImageDescription = tag_ids["ImageDescription"] - - info[ImageJMetaDataByteCounts] = len(bindata) - info[ImageJMetaData] = bindata - info[tag_ids["RollAngle"]] = floatdata - info.tagtype[tag_ids["RollAngle"]] = 11 - info[tag_ids["YawAngle"]] = doubledata - info.tagtype[tag_ids["YawAngle"]] = 12 - - info[ImageDescription] = textdata - - f = self.tempfile("temp.tif") - - img.save(f, tiffinfo=info) - - with Image.open(f) as loaded: - - assert loaded.tag[ImageJMetaDataByteCounts] == (len(bindata),) - assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bindata),) - - assert loaded.tag[ImageJMetaData] == bindata - assert loaded.tag_v2[ImageJMetaData] == bindata - - assert loaded.tag[ImageDescription] == (reloaded_textdata,) - assert loaded.tag_v2[ImageDescription] == reloaded_textdata - - loaded_float = loaded.tag[tag_ids["RollAngle"]][0] - assert round(abs(loaded_float - floatdata), 5) == 0 - loaded_double = loaded.tag[tag_ids["YawAngle"]][0] - assert round(abs(loaded_double - doubledata), 7) == 0 - - # check with 2 element ImageJMetaDataByteCounts, issue #2006 - - info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8) - img.save(f, tiffinfo=info) - with Image.open(f) as loaded: - - assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bindata) - 8) - assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bindata) - 8) - - def test_read_metadata(self): - with Image.open("Tests/images/hopper_g4.tif") as img: - - assert { - "YResolution": IFDRational(4294967295, 113653537), - "PlanarConfiguration": 1, - "BitsPerSample": (1,), - "ImageLength": 128, - "Compression": 4, - "FillOrder": 1, - "RowsPerStrip": 128, - "ResolutionUnit": 3, - "PhotometricInterpretation": 0, - "PageNumber": (0, 1), - "XResolution": IFDRational(4294967295, 113653537), - "ImageWidth": 128, - "Orientation": 1, - "StripByteCounts": (1968,), - "SamplesPerPixel": 1, - "StripOffsets": (8,), - } == img.tag_v2.named() - - assert { - "YResolution": ((4294967295, 113653537),), - "PlanarConfiguration": (1,), - "BitsPerSample": (1,), - "ImageLength": (128,), - "Compression": (4,), - "FillOrder": (1,), - "RowsPerStrip": (128,), - "ResolutionUnit": (3,), - "PhotometricInterpretation": (0,), - "PageNumber": (0, 1), - "XResolution": ((4294967295, 113653537),), - "ImageWidth": (128,), - "Orientation": (1,), - "StripByteCounts": (1968,), - "SamplesPerPixel": (1,), - "StripOffsets": (8,), - } == img.tag.named() - - def test_write_metadata(self): - """ Test metadata writing through the python code """ - with Image.open("Tests/images/hopper.tif") as img: - f = self.tempfile("temp.tiff") - img.save(f, tiffinfo=img.tag) - - original = img.tag_v2.named() - - with Image.open(f) as loaded: - reloaded = loaded.tag_v2.named() - - ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] - - for tag, value in reloaded.items(): - if tag in ignored: - continue - if isinstance(original[tag], tuple) and isinstance( - original[tag][0], IFDRational - ): - # Need to compare element by element in the tuple, - # not comparing tuples of object references - assert_deep_equal( - original[tag], - value, - "{} didn't roundtrip, {}, {}".format(tag, original[tag], value), - ) - else: - assert original[tag] == value, "{} didn't roundtrip, {}, {}".format( - tag, original[tag], value - ) - - for tag, value in original.items(): - if tag not in ignored: - assert value == reloaded[tag], "%s didn't roundtrip" % tag - - def test_no_duplicate_50741_tag(self): - assert tag_ids["MakerNoteSafety"] == 50741 - assert tag_ids["BestQualityScale"] == 50780 - - def test_empty_metadata(self): - f = io.BytesIO(b"II*\x00\x08\x00\x00\x00") - head = f.read(8) - info = TiffImagePlugin.ImageFileDirectory(head) - # Should not raise struct.error. - pytest.warns(UserWarning, info.load, f) - - def test_iccprofile(self): - # https://github.com/python-pillow/Pillow/issues/1462 - out = self.tempfile("temp.tiff") - with Image.open("Tests/images/hopper.iccprofile.tif") as im: - im.save(out) - - with Image.open(out) as reloaded: - assert not isinstance(im.info["icc_profile"], tuple) - assert im.info["icc_profile"] == reloaded.info["icc_profile"] - - def test_iccprofile_binary(self): - # https://github.com/python-pillow/Pillow/issues/1526 - # We should be able to load this, - # but probably won't be able to save it. - - with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: - assert im.tag_v2.tagtype[34675] == 1 - assert im.info["icc_profile"] - - def test_iccprofile_save_png(self): - with Image.open("Tests/images/hopper.iccprofile.tif") as im: - outfile = self.tempfile("temp.png") - im.save(outfile) - - def test_iccprofile_binary_save_png(self): - with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: - outfile = self.tempfile("temp.png") - im.save(outfile) - - def test_exif_div_zero(self): - im = hopper() - info = TiffImagePlugin.ImageFileDirectory_v2() - info[41988] = TiffImagePlugin.IFDRational(0, 0) - - out = self.tempfile("temp.tiff") - im.save(out, tiffinfo=info, compression="raw") - - with Image.open(out) as reloaded: - assert 0 == reloaded.tag_v2[41988].numerator - assert 0 == reloaded.tag_v2[41988].denominator - - def test_ifd_unsigned_rational(self): - im = hopper() - info = TiffImagePlugin.ImageFileDirectory_v2() - - max_long = 2 ** 32 - 1 - - # 4 bytes unsigned long - numerator = max_long - - info[41493] = TiffImagePlugin.IFDRational(numerator, 1) - - out = self.tempfile("temp.tiff") - im.save(out, tiffinfo=info, compression="raw") - - with Image.open(out) as reloaded: - assert max_long == reloaded.tag_v2[41493].numerator - assert 1 == reloaded.tag_v2[41493].denominator - - # out of bounds of 4 byte unsigned long - numerator = max_long + 1 - - info[41493] = TiffImagePlugin.IFDRational(numerator, 1) - - out = self.tempfile("temp.tiff") - im.save(out, tiffinfo=info, compression="raw") - - with Image.open(out) as reloaded: - assert max_long == reloaded.tag_v2[41493].numerator - assert 1 == reloaded.tag_v2[41493].denominator - - def test_ifd_signed_rational(self): - im = hopper() - info = TiffImagePlugin.ImageFileDirectory_v2() - - # pair of 4 byte signed longs - numerator = 2 ** 31 - 1 - denominator = -(2 ** 31) - - info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) +from .helper import assert_deep_equal, hopper + +TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()} + + +def test_rt_metadata(tmp_path): + """ Test writing arbitrary metadata into the tiff image directory + Use case is ImageJ private tags, one numeric, one arbitrary + data. https://github.com/python-pillow/Pillow/issues/291 + """ + + img = hopper() + + # Behaviour change: re #1416 + # Pre ifd rewrite, ImageJMetaData was being written as a string(2), + # Post ifd rewrite, it's defined as arbitrary bytes(7). It should + # roundtrip with the actual bytes, rather than stripped text + # of the premerge tests. + # + # For text items, we still have to decode('ascii','replace') because + # the tiff file format can't take 8 bit bytes in that field. + + basetextdata = "This is some arbitrary metadata for a text field" + bindata = basetextdata.encode("ascii") + b" \xff" + textdata = basetextdata + " " + chr(255) + reloaded_textdata = basetextdata + " ?" + floatdata = 12.345 + doubledata = 67.89 + info = TiffImagePlugin.ImageFileDirectory() + + ImageJMetaData = TAG_IDS["ImageJMetaData"] + ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"] + ImageDescription = TAG_IDS["ImageDescription"] + + info[ImageJMetaDataByteCounts] = len(bindata) + info[ImageJMetaData] = bindata + info[TAG_IDS["RollAngle"]] = floatdata + info.tagtype[TAG_IDS["RollAngle"]] = 11 + info[TAG_IDS["YawAngle"]] = doubledata + info.tagtype[TAG_IDS["YawAngle"]] = 12 + + info[ImageDescription] = textdata + + f = str(tmp_path / "temp.tif") + + img.save(f, tiffinfo=info) + + with Image.open(f) as loaded: + + assert loaded.tag[ImageJMetaDataByteCounts] == (len(bindata),) + assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bindata),) + + assert loaded.tag[ImageJMetaData] == bindata + assert loaded.tag_v2[ImageJMetaData] == bindata + + assert loaded.tag[ImageDescription] == (reloaded_textdata,) + assert loaded.tag_v2[ImageDescription] == reloaded_textdata + + loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0] + assert round(abs(loaded_float - floatdata), 5) == 0 + loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0] + assert round(abs(loaded_double - doubledata), 7) == 0 + + # check with 2 element ImageJMetaDataByteCounts, issue #2006 + + info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8) + img.save(f, tiffinfo=info) + with Image.open(f) as loaded: + + assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bindata) - 8) + assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bindata) - 8) + + +def test_read_metadata(): + with Image.open("Tests/images/hopper_g4.tif") as img: + + assert { + "YResolution": IFDRational(4294967295, 113653537), + "PlanarConfiguration": 1, + "BitsPerSample": (1,), + "ImageLength": 128, + "Compression": 4, + "FillOrder": 1, + "RowsPerStrip": 128, + "ResolutionUnit": 3, + "PhotometricInterpretation": 0, + "PageNumber": (0, 1), + "XResolution": IFDRational(4294967295, 113653537), + "ImageWidth": 128, + "Orientation": 1, + "StripByteCounts": (1968,), + "SamplesPerPixel": 1, + "StripOffsets": (8,), + } == img.tag_v2.named() + + assert { + "YResolution": ((4294967295, 113653537),), + "PlanarConfiguration": (1,), + "BitsPerSample": (1,), + "ImageLength": (128,), + "Compression": (4,), + "FillOrder": (1,), + "RowsPerStrip": (128,), + "ResolutionUnit": (3,), + "PhotometricInterpretation": (0,), + "PageNumber": (0, 1), + "XResolution": ((4294967295, 113653537),), + "ImageWidth": (128,), + "Orientation": (1,), + "StripByteCounts": (1968,), + "SamplesPerPixel": (1,), + "StripOffsets": (8,), + } == img.tag.named() + + +def test_write_metadata(tmp_path): + """ Test metadata writing through the python code """ + with Image.open("Tests/images/hopper.tif") as img: + f = str(tmp_path / "temp.tiff") + img.save(f, tiffinfo=img.tag) + + original = img.tag_v2.named() + + with Image.open(f) as loaded: + reloaded = loaded.tag_v2.named() + + ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] - out = self.tempfile("temp.tiff") - im.save(out, tiffinfo=info, compression="raw") + for tag, value in reloaded.items(): + if tag in ignored: + continue + if isinstance(original[tag], tuple) and isinstance( + original[tag][0], IFDRational + ): + # Need to compare element by element in the tuple, + # not comparing tuples of object references + assert_deep_equal( + original[tag], + value, + "{} didn't roundtrip, {}, {}".format(tag, original[tag], value), + ) + else: + assert original[tag] == value, "{} didn't roundtrip, {}, {}".format( + tag, original[tag], value + ) - with Image.open(out) as reloaded: - assert numerator == reloaded.tag_v2[37380].numerator - assert denominator == reloaded.tag_v2[37380].denominator + for tag, value in original.items(): + if tag not in ignored: + assert value == reloaded[tag], "%s didn't roundtrip" % tag - numerator = -(2 ** 31) - denominator = 2 ** 31 - 1 - info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) +def test_no_duplicate_50741_tag(): + assert TAG_IDS["MakerNoteSafety"] == 50741 + assert TAG_IDS["BestQualityScale"] == 50780 - out = self.tempfile("temp.tiff") - im.save(out, tiffinfo=info, compression="raw") - with Image.open(out) as reloaded: - assert numerator == reloaded.tag_v2[37380].numerator - assert denominator == reloaded.tag_v2[37380].denominator +def test_empty_metadata(): + f = io.BytesIO(b"II*\x00\x08\x00\x00\x00") + head = f.read(8) + info = TiffImagePlugin.ImageFileDirectory(head) + # Should not raise struct.error. + pytest.warns(UserWarning, info.load, f) - # out of bounds of 4 byte signed long - numerator = -(2 ** 31) - 1 - denominator = 1 - info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) +def test_iccprofile(tmp_path): + # https://github.com/python-pillow/Pillow/issues/1462 + out = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper.iccprofile.tif") as im: + im.save(out) - out = self.tempfile("temp.tiff") - im.save(out, tiffinfo=info, compression="raw") + with Image.open(out) as reloaded: + assert not isinstance(im.info["icc_profile"], tuple) + assert im.info["icc_profile"] == reloaded.info["icc_profile"] - with Image.open(out) as reloaded: - assert 2 ** 31 - 1 == reloaded.tag_v2[37380].numerator - assert -1 == reloaded.tag_v2[37380].denominator - def test_ifd_signed_long(self): - im = hopper() - info = TiffImagePlugin.ImageFileDirectory_v2() +def test_iccprofile_binary(): + # https://github.com/python-pillow/Pillow/issues/1526 + # We should be able to load this, + # but probably won't be able to save it. - info[37000] = -60000 + with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: + assert im.tag_v2.tagtype[34675] == 1 + assert im.info["icc_profile"] - out = self.tempfile("temp.tiff") - im.save(out, tiffinfo=info, compression="raw") - with Image.open(out) as reloaded: - assert reloaded.tag_v2[37000] == -60000 +def test_iccprofile_save_png(tmp_path): + with Image.open("Tests/images/hopper.iccprofile.tif") as im: + outfile = str(tmp_path / "temp.png") + im.save(outfile) - def test_empty_values(self): - data = io.BytesIO( - b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x98\x82\x02\x00\x07\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00a " - b"text\x00\x00" - ) - head = data.read(8) - info = TiffImagePlugin.ImageFileDirectory_v2(head) - info.load(data) - # Should not raise ValueError. - info = dict(info) - assert 33432 in info - def test_PhotoshopInfo(self): - with Image.open("Tests/images/issue_2278.tif") as im: - assert len(im.tag_v2[34377]) == 1 - assert isinstance(im.tag_v2[34377][0], bytes) - out = self.tempfile("temp.tiff") - im.save(out) - with Image.open(out) as reloaded: - assert len(reloaded.tag_v2[34377]) == 1 - assert isinstance(reloaded.tag_v2[34377][0], bytes) +def test_iccprofile_binary_save_png(tmp_path): + with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: + outfile = str(tmp_path / "temp.png") + im.save(outfile) - def test_too_many_entries(self): - ifd = TiffImagePlugin.ImageFileDirectory_v2() - # 277: ("SamplesPerPixel", SHORT, 1), - ifd._tagdata[277] = struct.pack("hh", 4, 4) - ifd.tagtype[277] = TiffTags.SHORT +def test_exif_div_zero(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + info[41988] = TiffImagePlugin.IFDRational(0, 0) - # Should not raise ValueError. - pytest.warns(UserWarning, lambda: ifd[277]) + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert 0 == reloaded.tag_v2[41988].numerator + assert 0 == reloaded.tag_v2[41988].denominator + + +def test_ifd_unsigned_rational(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + max_long = 2 ** 32 - 1 + + # 4 bytes unsigned long + numerator = max_long + + info[41493] = TiffImagePlugin.IFDRational(numerator, 1) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert max_long == reloaded.tag_v2[41493].numerator + assert 1 == reloaded.tag_v2[41493].denominator + + # out of bounds of 4 byte unsigned long + numerator = max_long + 1 + + info[41493] = TiffImagePlugin.IFDRational(numerator, 1) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert max_long == reloaded.tag_v2[41493].numerator + assert 1 == reloaded.tag_v2[41493].denominator + + +def test_ifd_signed_rational(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + # pair of 4 byte signed longs + numerator = 2 ** 31 - 1 + denominator = -(2 ** 31) + + info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert numerator == reloaded.tag_v2[37380].numerator + assert denominator == reloaded.tag_v2[37380].denominator + + numerator = -(2 ** 31) + denominator = 2 ** 31 - 1 + + info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert numerator == reloaded.tag_v2[37380].numerator + assert denominator == reloaded.tag_v2[37380].denominator + + # out of bounds of 4 byte signed long + numerator = -(2 ** 31) - 1 + denominator = 1 + + info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert 2 ** 31 - 1 == reloaded.tag_v2[37380].numerator + assert -1 == reloaded.tag_v2[37380].denominator + + +def test_ifd_signed_long(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + info[37000] = -60000 + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[37000] == -60000 + + +def test_empty_values(): + data = io.BytesIO( + b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x98\x82\x02\x00\x07\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00a " + b"text\x00\x00" + ) + head = data.read(8) + info = TiffImagePlugin.ImageFileDirectory_v2(head) + info.load(data) + # Should not raise ValueError. + info = dict(info) + assert 33432 in info + + +def test_PhotoshopInfo(tmp_path): + with Image.open("Tests/images/issue_2278.tif") as im: + assert len(im.tag_v2[34377]) == 1 + assert isinstance(im.tag_v2[34377][0], bytes) + out = str(tmp_path / "temp.tiff") + im.save(out) + with Image.open(out) as reloaded: + assert len(reloaded.tag_v2[34377]) == 1 + assert isinstance(reloaded.tag_v2[34377][0], bytes) + + +def test_too_many_entries(): + ifd = TiffImagePlugin.ImageFileDirectory_v2() + + # 277: ("SamplesPerPixel", SHORT, 1), + ifd._tagdata[277] = struct.pack("hh", 4, 4) + ifd.tagtype[277] = TiffTags.SHORT + + # Should not raise ValueError. + pytest.warns(UserWarning, lambda: ifd[277]) diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index b940b823d..d10b1acfd 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -3,142 +3,134 @@ import itertools from PIL import Image -from .helper import PillowTestCase, assert_image_similar, hopper +from .helper import assert_image_similar, hopper -class TestFormatHSV(PillowTestCase): - def int_to_float(self, i): - return i / 255 +def int_to_float(i): + return i / 255 - def str_to_float(self, i): - return ord(i) / 255 - def tuple_to_ints(self, tp): - x, y, z = tp - return int(x * 255.0), int(y * 255.0), int(z * 255.0) +def str_to_float(i): + return ord(i) / 255 - def test_sanity(self): - Image.new("HSV", (100, 100)) - def wedge(self): - w = Image._wedge() - w90 = w.rotate(90) +def tuple_to_ints(tp): + x, y, z = tp + return int(x * 255.0), int(y * 255.0), int(z * 255.0) - (px, h) = w.size - r = Image.new("L", (px * 3, h)) - g = r.copy() - b = r.copy() +def test_sanity(): + Image.new("HSV", (100, 100)) - r.paste(w, (0, 0)) - r.paste(w90, (px, 0)) - g.paste(w90, (0, 0)) - g.paste(w, (2 * px, 0)) +def wedge(): + w = Image._wedge() + w90 = w.rotate(90) - b.paste(w, (px, 0)) - b.paste(w90, (2 * px, 0)) + (px, h) = w.size - img = Image.merge("RGB", (r, g, b)) + r = Image.new("L", (px * 3, h)) + g = r.copy() + b = r.copy() - return img + r.paste(w, (0, 0)) + r.paste(w90, (px, 0)) - def to_xxx_colorsys(self, im, func, mode): - # convert the hard way using the library colorsys routines. + g.paste(w90, (0, 0)) + g.paste(w, (2 * px, 0)) - (r, g, b) = im.split() + b.paste(w, (px, 0)) + b.paste(w90, (2 * px, 0)) - conv_func = self.int_to_float + img = Image.merge("RGB", (r, g, b)) - converted = [ - self.tuple_to_ints(func(conv_func(_r), conv_func(_g), conv_func(_b))) - for (_r, _g, _b) in itertools.zip_longest( - r.tobytes(), g.tobytes(), b.tobytes() - ) - ] + return img - new_bytes = b"".join( - bytes(chr(h) + chr(s) + chr(v), "latin-1") for (h, s, v) in converted - ) - hsv = Image.frombytes(mode, r.size, new_bytes) +def to_xxx_colorsys(im, func, mode): + # convert the hard way using the library colorsys routines. - return hsv + (r, g, b) = im.split() - def to_hsv_colorsys(self, im): - return self.to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") + conv_func = int_to_float - def to_rgb_colorsys(self, im): - return self.to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") + converted = [ + tuple_to_ints(func(conv_func(_r), conv_func(_g), conv_func(_b))) + for (_r, _g, _b) in itertools.zip_longest(r.tobytes(), g.tobytes(), b.tobytes()) + ] - def test_wedge(self): - src = self.wedge().resize((3 * 32, 32), Image.BILINEAR) - im = src.convert("HSV") - comparable = self.to_hsv_colorsys(src) + new_bytes = b"".join( + bytes(chr(h) + chr(s) + chr(v), "latin-1") for (h, s, v) in converted + ) - assert_image_similar( - im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" - ) - assert_image_similar( - im.getchannel(1), - comparable.getchannel(1), - 1, - "Saturation conversion is wrong", - ) - assert_image_similar( - im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" - ) + hsv = Image.frombytes(mode, r.size, new_bytes) - comparable = src - im = im.convert("RGB") + return hsv - assert_image_similar( - im.getchannel(0), comparable.getchannel(0), 3, "R conversion is wrong" - ) - assert_image_similar( - im.getchannel(1), comparable.getchannel(1), 3, "G conversion is wrong" - ) - assert_image_similar( - im.getchannel(2), comparable.getchannel(2), 3, "B conversion is wrong" - ) - def test_convert(self): - im = hopper("RGB").convert("HSV") - comparable = self.to_hsv_colorsys(hopper("RGB")) +def to_hsv_colorsys(im): + return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") - assert_image_similar( - im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" - ) - assert_image_similar( - im.getchannel(1), - comparable.getchannel(1), - 1, - "Saturation conversion is wrong", - ) - assert_image_similar( - im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" - ) - def test_hsv_to_rgb(self): - comparable = self.to_hsv_colorsys(hopper("RGB")) - converted = comparable.convert("RGB") - comparable = self.to_rgb_colorsys(comparable) +def to_rgb_colorsys(im): + return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") - assert_image_similar( - converted.getchannel(0), - comparable.getchannel(0), - 3, - "R conversion is wrong", - ) - assert_image_similar( - converted.getchannel(1), - comparable.getchannel(1), - 3, - "G conversion is wrong", - ) - assert_image_similar( - converted.getchannel(2), - comparable.getchannel(2), - 3, - "B conversion is wrong", - ) + +def test_wedge(): + src = wedge().resize((3 * 32, 32), Image.BILINEAR) + im = src.convert("HSV") + comparable = to_hsv_colorsys(src) + + assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" + ) + assert_image_similar( + im.getchannel(1), comparable.getchannel(1), 1, "Saturation conversion is wrong", + ) + assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" + ) + + comparable = src + im = im.convert("RGB") + + assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 3, "R conversion is wrong" + ) + assert_image_similar( + im.getchannel(1), comparable.getchannel(1), 3, "G conversion is wrong" + ) + assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 3, "B conversion is wrong" + ) + + +def test_convert(): + im = hopper("RGB").convert("HSV") + comparable = to_hsv_colorsys(hopper("RGB")) + + assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" + ) + assert_image_similar( + im.getchannel(1), comparable.getchannel(1), 1, "Saturation conversion is wrong", + ) + assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" + ) + + +def test_hsv_to_rgb(): + comparable = to_hsv_colorsys(hopper("RGB")) + converted = comparable.convert("RGB") + comparable = to_rgb_colorsys(comparable) + + assert_image_similar( + converted.getchannel(0), comparable.getchannel(0), 3, "R conversion is wrong", + ) + assert_image_similar( + converted.getchannel(1), comparable.getchannel(1), 3, "G conversion is wrong", + ) + assert_image_similar( + converted.getchannel(2), comparable.getchannel(2), 3, "B conversion is wrong", + ) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 35d61f904..25cc9fef4 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -2,13 +2,12 @@ import ctypes import os import subprocess import sys -import unittest from distutils import ccompiler, sysconfig import pytest from PIL import Image -from .helper import PillowTestCase, assert_image_equal, hopper, is_win32, on_ci +from .helper import assert_image_equal, hopper, is_win32, on_ci # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -22,17 +21,17 @@ else: cffi = None -class AccessTest(PillowTestCase): +class AccessTest: # initial value _init_cffi_access = Image.USE_CFFI_ACCESS _need_cffi_access = False @classmethod - def setUpClass(cls): + def setup_class(cls): Image.USE_CFFI_ACCESS = cls._need_cffi_access @classmethod - def tearDownClass(cls): + def teardown_class(cls): Image.USE_CFFI_ACCESS = cls._init_cffi_access @@ -200,17 +199,17 @@ class TestImageGetPixel(AccessTest): assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) -@unittest.skipIf(cffi is None, "No cffi") +@pytest.mark.skipif(cffi is None, reason="No CFFI") class TestCffiPutPixel(TestImagePutPixel): _need_cffi_access = True -@unittest.skipIf(cffi is None, "No cffi") +@pytest.mark.skipif(cffi is None, reason="No CFFI") class TestCffiGetPixel(TestImageGetPixel): _need_cffi_access = True -@unittest.skipIf(cffi is None, "No cffi") +@pytest.mark.skipif(cffi is None, reason="No CFFI") class TestCffi(AccessTest): _need_cffi_access = True @@ -326,10 +325,11 @@ class TestCffi(AccessTest): assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) -class TestEmbeddable(unittest.TestCase): - @unittest.skipIf( +class TestEmbeddable: + @pytest.mark.skipif( not is_win32() or on_ci(), - "Failing on AppVeyor / GitHub Actions when run from subprocess, not from shell", + reason="Failing on AppVeyor / GitHub Actions when run from subprocess, " + "not from shell", ) def test_embeddable(self): with open("embed_pil.c", "w") as fh: diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index e0b9570e9..cf83922b6 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,250 +1,261 @@ import pytest from PIL import Image -from .helper import ( - PillowTestCase, - assert_image, - assert_image_equal, - assert_image_similar, - hopper, -) +from .helper import assert_image, assert_image_equal, assert_image_similar, hopper -class TestImageConvert(PillowTestCase): - def test_sanity(self): - def convert(im, mode): - out = im.convert(mode) - assert out.mode == mode - assert out.size == im.size +def test_sanity(): + def convert(im, mode): + out = im.convert(mode) + assert out.mode == mode + assert out.size == im.size - modes = ( - "1", - "L", - "LA", - "P", - "PA", - "I", - "F", - "RGB", - "RGBA", - "RGBX", - "CMYK", - "YCbCr", - "HSV", - ) + modes = ( + "1", + "L", + "LA", + "P", + "PA", + "I", + "F", + "RGB", + "RGBA", + "RGBX", + "CMYK", + "YCbCr", + "HSV", + ) + for mode in modes: + im = hopper(mode) for mode in modes: - im = hopper(mode) - for mode in modes: - convert(im, mode) + convert(im, mode) - # Check 0 - im = Image.new(mode, (0, 0)) - for mode in modes: - convert(im, mode) + # Check 0 + im = Image.new(mode, (0, 0)) + for mode in modes: + convert(im, mode) - def test_default(self): - im = hopper("P") - assert_image(im, "P", im.size) - im = im.convert() - assert_image(im, "RGB", im.size) - im = im.convert() - assert_image(im, "RGB", im.size) +def test_default(): - # ref https://github.com/python-pillow/Pillow/issues/274 + im = hopper("P") + assert_image(im, "P", im.size) + im = im.convert() + assert_image(im, "RGB", im.size) + im = im.convert() + assert_image(im, "RGB", im.size) - def _test_float_conversion(self, im): - orig = im.getpixel((5, 5)) - converted = im.convert("F").getpixel((5, 5)) - assert orig == converted - def test_8bit(self): - with Image.open("Tests/images/hopper.jpg") as im: - self._test_float_conversion(im.convert("L")) +# ref https://github.com/python-pillow/Pillow/issues/274 - def test_16bit(self): - with Image.open("Tests/images/16bit.cropped.tif") as im: - self._test_float_conversion(im) - def test_16bit_workaround(self): - with Image.open("Tests/images/16bit.cropped.tif") as im: - self._test_float_conversion(im.convert("I")) +def _test_float_conversion(im): + orig = im.getpixel((5, 5)) + converted = im.convert("F").getpixel((5, 5)) + assert orig == converted - def test_rgba_p(self): - im = hopper("RGBA") - im.putalpha(hopper("L")) - converted = im.convert("P") - comparable = converted.convert("RGBA") +def test_8bit(): + with Image.open("Tests/images/hopper.jpg") as im: + _test_float_conversion(im.convert("L")) - assert_image_similar(im, comparable, 20) - def test_trns_p(self): - im = hopper("P") - im.info["transparency"] = 0 +def test_16bit(): + with Image.open("Tests/images/16bit.cropped.tif") as im: + _test_float_conversion(im) - f = self.tempfile("temp.png") - im_l = im.convert("L") - assert im_l.info["transparency"] == 0 # undone - im_l.save(f) +def test_16bit_workaround(): + with Image.open("Tests/images/16bit.cropped.tif") as im: + _test_float_conversion(im.convert("I")) - im_rgb = im.convert("RGB") - assert im_rgb.info["transparency"] == (0, 0, 0) # undone - im_rgb.save(f) - # ref https://github.com/python-pillow/Pillow/issues/664 +def test_rgba_p(): + im = hopper("RGBA") + im.putalpha(hopper("L")) - def test_trns_p_rgba(self): - # Arrange - im = hopper("P") - im.info["transparency"] = 128 + converted = im.convert("P") + comparable = converted.convert("RGBA") - # Act - im_rgba = im.convert("RGBA") + assert_image_similar(im, comparable, 20) - # Assert - assert "transparency" not in im_rgba.info - # https://github.com/python-pillow/Pillow/issues/2702 - assert im_rgba.palette is None - def test_trns_l(self): - im = hopper("L") - im.info["transparency"] = 128 +def test_trns_p(tmp_path): + im = hopper("P") + im.info["transparency"] = 0 - f = self.tempfile("temp.png") + f = str(tmp_path / "temp.png") - im_rgb = im.convert("RGB") - assert im_rgb.info["transparency"] == (128, 128, 128) # undone - im_rgb.save(f) + im_l = im.convert("L") + assert im_l.info["transparency"] == 0 # undone + im_l.save(f) + im_rgb = im.convert("RGB") + assert im_rgb.info["transparency"] == (0, 0, 0) # undone + im_rgb.save(f) + + +# ref https://github.com/python-pillow/Pillow/issues/664 + + +def test_trns_p_rgba(): + # Arrange + im = hopper("P") + im.info["transparency"] = 128 + + # Act + im_rgba = im.convert("RGBA") + + # Assert + assert "transparency" not in im_rgba.info + # https://github.com/python-pillow/Pillow/issues/2702 + assert im_rgba.palette is None + + +def test_trns_l(tmp_path): + im = hopper("L") + im.info["transparency"] = 128 + + f = str(tmp_path / "temp.png") + + im_rgb = im.convert("RGB") + assert im_rgb.info["transparency"] == (128, 128, 128) # undone + im_rgb.save(f) + + im_p = im.convert("P") + assert "transparency" in im_p.info + im_p.save(f) + + im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) + assert "transparency" not in im_p.info + im_p.save(f) + + +def test_trns_RGB(tmp_path): + im = hopper("RGB") + im.info["transparency"] = im.getpixel((0, 0)) + + f = str(tmp_path / "temp.png") + + im_l = im.convert("L") + assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone + im_l.save(f) + + im_p = im.convert("P") + assert "transparency" in im_p.info + im_p.save(f) + + im_rgba = im.convert("RGBA") + assert "transparency" not in im_rgba.info + im_rgba.save(f) + + im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) + assert "transparency" not in im_p.info + im_p.save(f) + + +def test_gif_with_rgba_palette_to_p(): + # See https://github.com/python-pillow/Pillow/issues/2433 + with Image.open("Tests/images/hopper.gif") as im: + im.info["transparency"] = 255 + im.load() + assert im.palette.mode == "RGBA" im_p = im.convert("P") - assert "transparency" in im_p.info - im_p.save(f) - im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) - assert "transparency" not in im_p.info - im_p.save(f) + # Should not raise ValueError: unrecognized raw mode + im_p.load() - def test_trns_RGB(self): + +def test_p_la(): + im = hopper("RGBA") + alpha = hopper("L") + im.putalpha(alpha) + + comparable = im.convert("P").convert("LA").getchannel("A") + + assert_image_similar(alpha, comparable, 5) + + +def test_matrix_illegal_conversion(): + # Arrange + im = hopper("CMYK") + # 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 / Assert + with pytest.raises(ValueError): + im.convert(mode="CMYK", matrix=matrix) + + +def test_matrix_wrong_mode(): + # Arrange + im = hopper("L") + # 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 == "L" + + # Act / Assert + with pytest.raises(ValueError): + im.convert(mode="L", matrix=matrix) + + +def test_matrix_xyz(): + def matrix_convert(mode): + # Arrange im = hopper("RGB") - im.info["transparency"] = im.getpixel((0, 0)) - - f = self.tempfile("temp.png") - - im_l = im.convert("L") - assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone - im_l.save(f) - - im_p = im.convert("P") - assert "transparency" in im_p.info - im_p.save(f) - - im_rgba = im.convert("RGBA") - assert "transparency" not in im_rgba.info - im_rgba.save(f) - - im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) - assert "transparency" not in im_p.info - im_p.save(f) - - def test_gif_with_rgba_palette_to_p(self): - # See https://github.com/python-pillow/Pillow/issues/2433 - with Image.open("Tests/images/hopper.gif") as im: - im.info["transparency"] = 255 - im.load() - assert im.palette.mode == "RGBA" - im_p = im.convert("P") - - # Should not raise ValueError: unrecognized raw mode - im_p.load() - - def test_p_la(self): - im = hopper("RGBA") - alpha = hopper("L") - im.putalpha(alpha) - - comparable = im.convert("P").convert("LA").getchannel("A") - - assert_image_similar(alpha, comparable, 5) - - def test_matrix_illegal_conversion(self): - # Arrange - im = hopper("CMYK") + 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 / Assert - with pytest.raises(ValueError): - im.convert(mode="CMYK", matrix=matrix) - - def test_matrix_wrong_mode(self): - # Arrange - im = hopper("L") - # 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 == "L" - - # Act / Assert - with pytest.raises(ValueError): - im.convert(mode="L", matrix=matrix) - - def test_matrix_xyz(self): - 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") - - def test_matrix_identity(self): - # Arrange - im = hopper("RGB") - # fmt: off - identity_matrix = ( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0) - # fmt: on assert im.mode == "RGB" # Act - # Convert with an identity matrix - converted_im = im.convert(mode="RGB", matrix=identity_matrix) + # Convert an RGB image to the CIE XYZ colour space + converted_im = im.convert(mode=mode, matrix=matrix) # Assert - # No change - assert_image_equal(converted_im, im) + 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") + + +def test_matrix_identity(): + # Arrange + im = hopper("RGB") + # fmt: off + identity_matrix = ( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0) + # fmt: on + assert im.mode == "RGB" + + # Act + # Convert with an identity matrix + converted_im = im.convert(mode="RGB", matrix=identity_matrix) + + # Assert + # No change + assert_image_equal(converted_im, im) diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index a80a1ca9d..c86e33eb2 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -10,29 +10,32 @@ def test_sanity(): def test_bbox(): + def check(im, fill_color): + assert im.getbbox() is None + + im.paste(fill_color, (10, 25, 90, 75)) + assert im.getbbox() == (10, 25, 90, 75) + + im.paste(fill_color, (25, 10, 75, 90)) + assert im.getbbox() == (10, 10, 90, 90) + + im.paste(fill_color, (-10, -10, 110, 110)) + assert im.getbbox() == (0, 0, 100, 100) # 8-bit mode im = Image.new("L", (100, 100), 0) - assert im.getbbox() is None - - im.paste(255, (10, 25, 90, 75)) - assert im.getbbox() == (10, 25, 90, 75) - - im.paste(255, (25, 10, 75, 90)) - assert im.getbbox() == (10, 10, 90, 90) - - im.paste(255, (-10, -10, 110, 110)) - assert im.getbbox() == (0, 0, 100, 100) + check(im, 255) # 32-bit mode im = Image.new("RGB", (100, 100), 0) - assert im.getbbox() is None + check(im, 255) - im.paste(255, (10, 25, 90, 75)) - assert im.getbbox() == (10, 25, 90, 75) + for mode in ("RGBA", "RGBa"): + for color in ((0, 0, 0, 0), (127, 127, 127, 0), (255, 255, 255, 0)): + im = Image.new(mode, (100, 100), color) + check(im, (255, 255, 255, 255)) - im.paste(255, (25, 10, 75, 90)) - assert im.getbbox() == (10, 10, 90, 90) - - im.paste(255, (-10, -10, 110, 110)) - assert im.getbbox() == (0, 0, 100, 100) + for mode in ("La", "LA", "PA"): + for color in ((0, 0), (127, 0), (255, 0)): + im = Image.new(mode, (100, 100), color) + check(im, (255, 255)) diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 207d5f925..fbed276b8 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,62 +1,63 @@ from PIL import Image, features -from .helper import PillowTestCase, assert_image_equal, hopper +from .helper import assert_image_equal, hopper -class TestImageSplit(PillowTestCase): - def test_split(self): - def split(mode): - layers = hopper(mode).split() - return [(i.mode, i.size[0], i.size[1]) for i in layers] +def test_split(): + def split(mode): + layers = hopper(mode).split() + return [(i.mode, i.size[0], i.size[1]) for i in layers] - assert split("1") == [("1", 128, 128)] - assert split("L") == [("L", 128, 128)] - assert split("I") == [("I", 128, 128)] - assert split("F") == [("F", 128, 128)] - assert split("P") == [("P", 128, 128)] - assert split("RGB") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] - assert split("RGBA") == [ - ("L", 128, 128), - ("L", 128, 128), - ("L", 128, 128), - ("L", 128, 128), - ] - assert split("CMYK") == [ - ("L", 128, 128), - ("L", 128, 128), - ("L", 128, 128), - ("L", 128, 128), - ] - assert split("YCbCr") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] + assert split("1") == [("1", 128, 128)] + assert split("L") == [("L", 128, 128)] + assert split("I") == [("I", 128, 128)] + assert split("F") == [("F", 128, 128)] + assert split("P") == [("P", 128, 128)] + assert split("RGB") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] + assert split("RGBA") == [ + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ] + assert split("CMYK") == [ + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ] + assert split("YCbCr") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] - def test_split_merge(self): - def split_merge(mode): - return Image.merge(mode, hopper(mode).split()) - assert_image_equal(hopper("1"), split_merge("1")) - assert_image_equal(hopper("L"), split_merge("L")) - assert_image_equal(hopper("I"), split_merge("I")) - assert_image_equal(hopper("F"), split_merge("F")) - assert_image_equal(hopper("P"), split_merge("P")) - assert_image_equal(hopper("RGB"), split_merge("RGB")) - assert_image_equal(hopper("RGBA"), split_merge("RGBA")) - assert_image_equal(hopper("CMYK"), split_merge("CMYK")) - assert_image_equal(hopper("YCbCr"), split_merge("YCbCr")) +def test_split_merge(): + def split_merge(mode): + return Image.merge(mode, hopper(mode).split()) - def test_split_open(self): - if features.check("zlib"): - test_file = self.tempfile("temp.png") - else: - test_file = self.tempfile("temp.pcx") + assert_image_equal(hopper("1"), split_merge("1")) + assert_image_equal(hopper("L"), split_merge("L")) + assert_image_equal(hopper("I"), split_merge("I")) + assert_image_equal(hopper("F"), split_merge("F")) + assert_image_equal(hopper("P"), split_merge("P")) + assert_image_equal(hopper("RGB"), split_merge("RGB")) + assert_image_equal(hopper("RGBA"), split_merge("RGBA")) + assert_image_equal(hopper("CMYK"), split_merge("CMYK")) + assert_image_equal(hopper("YCbCr"), split_merge("YCbCr")) - def split_open(mode): - hopper(mode).save(test_file) - with Image.open(test_file) as im: - return len(im.split()) - assert split_open("1") == 1 - assert split_open("L") == 1 - assert split_open("P") == 1 - assert split_open("RGB") == 3 - if features.check("zlib"): - assert split_open("RGBA") == 4 +def test_split_open(tmp_path): + if features.check("zlib"): + test_file = str(tmp_path / "temp.png") + else: + test_file = str(tmp_path / "temp.pcx") + + def split_open(mode): + hopper(mode).save(test_file) + with Image.open(test_file) as im: + return len(im.split()) + + assert split_open("1") == 1 + assert split_open("L") == 1 + assert split_open("P") == 1 + assert split_open("RGB") == 3 + if features.check("zlib"): + assert split_open("RGBA") == 4 diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index de15f5e2e..f4ed8e746 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -38,9 +38,9 @@ def test_aspect(): im.thumbnail((100, 50)) assert im.size == (100, 50) - im = Image.new("L", (128, 128)) + im = Image.new("L", (64, 64)) im.thumbnail((100, 100)) - assert im.size == (100, 100) + assert im.size == (64, 64) im = Image.new("L", (256, 162)) # ratio is 1.5802469136 im.thumbnail((33, 33)) @@ -50,11 +50,23 @@ def test_aspect(): im.thumbnail((33, 33)) assert im.size == (21, 33) # ratio is 0.6363636364 + im = Image.new("L", (145, 100)) # ratio is 1.45 + im.thumbnail((50, 50)) + assert im.size == (50, 34) # ratio is 1.47058823529 + + im = Image.new("L", (100, 145)) # ratio is 0.689655172414 + im.thumbnail((50, 50)) + assert im.size == (34, 50) # ratio is 0.68 + + im = Image.new("L", (100, 30)) # ratio is 3.333333333333 + im.thumbnail((75, 75)) + assert im.size == (75, 23) # ratio is 3.260869565217 + def test_float(): im = Image.new("L", (128, 128)) im.thumbnail((99.9, 99.9)) - assert im.size == (100, 100) + assert im.size == (99, 99) def test_no_resize(): diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 9036e1842..f6eabb21a 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -944,6 +944,22 @@ def test_stroke(): ) +@skip_unless_feature("freetype2") +def test_stroke_descender(): + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.text((10, 0), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0") + + # Assert + assert_image_similar( + im, Image.open("Tests/images/imagedraw_stroke_descender.png"), 6.76 + ) + + @skip_unless_feature("freetype2") def test_stroke_multiline(): # Arrange diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index f606dead0..29771cf03 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,140 +1,149 @@ import pytest from PIL import Image, ImagePalette -from .helper import PillowTestCase, assert_image_equal +from .helper import assert_image_equal -class TestImagePalette(PillowTestCase): - def test_sanity(self): +def test_sanity(): - ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - with pytest.raises(ValueError): - ImagePalette.ImagePalette("RGB", list(range(256)) * 2) + ImagePalette.ImagePalette("RGB", list(range(256)) * 3) + with pytest.raises(ValueError): + ImagePalette.ImagePalette("RGB", list(range(256)) * 2) - def test_getcolor(self): - palette = ImagePalette.ImagePalette() +def test_getcolor(): - test_map = {} - for i in range(256): - test_map[palette.getcolor((i, i, i))] = i + palette = ImagePalette.ImagePalette() - assert len(test_map) == 256 - with pytest.raises(ValueError): - palette.getcolor((1, 2, 3)) + test_map = {} + for i in range(256): + test_map[palette.getcolor((i, i, i))] = i - # Test unknown color specifier - with pytest.raises(ValueError): - palette.getcolor("unknown") + assert len(test_map) == 256 + with pytest.raises(ValueError): + palette.getcolor((1, 2, 3)) - def test_file(self): + # Test unknown color specifier + with pytest.raises(ValueError): + palette.getcolor("unknown") - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - f = self.tempfile("temp.lut") +def test_file(tmp_path): + palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) + + f = str(tmp_path / "temp.lut") + + palette.save(f) + + p = ImagePalette.load(f) + + # load returns raw palette information + assert len(p[0]) == 768 + assert p[1] == "RGB" + + p = ImagePalette.raw(p[1], p[0]) + assert isinstance(p, ImagePalette.ImagePalette) + assert p.palette == palette.tobytes() + + +def test_make_linear_lut(): + # Arrange + black = 0 + white = 255 + + # Act + lut = ImagePalette.make_linear_lut(black, white) + + # Assert + assert isinstance(lut, list) + assert len(lut) == 256 + # Check values + for i in range(0, len(lut)): + assert lut[i] == i + + +def test_make_linear_lut_not_yet_implemented(): + # Update after FIXME + # Arrange + black = 1 + white = 255 + + # Act + with pytest.raises(NotImplementedError): + ImagePalette.make_linear_lut(black, white) + + +def test_make_gamma_lut(): + # Arrange + exp = 5 + + # Act + lut = ImagePalette.make_gamma_lut(exp) + + # Assert + assert isinstance(lut, list) + assert len(lut) == 256 + # Check a few values + assert lut[0] == 0 + assert lut[63] == 0 + assert lut[127] == 8 + assert lut[191] == 60 + assert lut[255] == 255 + + +def test_rawmode_valueerrors(tmp_path): + # Arrange + palette = ImagePalette.raw("RGB", list(range(256)) * 3) + + # Act / Assert + with pytest.raises(ValueError): + palette.tobytes() + with pytest.raises(ValueError): + palette.getcolor((1, 2, 3)) + f = str(tmp_path / "temp.lut") + with pytest.raises(ValueError): palette.save(f) - p = ImagePalette.load(f) - # load returns raw palette information - assert len(p[0]) == 768 - assert p[1] == "RGB" +def test_getdata(): + # Arrange + data_in = list(range(256)) * 3 + palette = ImagePalette.ImagePalette("RGB", data_in) - p = ImagePalette.raw(p[1], p[0]) - assert isinstance(p, ImagePalette.ImagePalette) - assert p.palette == palette.tobytes() + # Act + mode, data_out = palette.getdata() - def test_make_linear_lut(self): - # Arrange - black = 0 - white = 255 + # Assert + assert mode == "RGB;L" - # Act - lut = ImagePalette.make_linear_lut(black, white) - # Assert - assert isinstance(lut, list) - assert len(lut) == 256 - # Check values - for i in range(0, len(lut)): - assert lut[i] == i +def test_rawmode_getdata(): + # Arrange + data_in = list(range(256)) * 3 + palette = ImagePalette.raw("RGB", data_in) - def test_make_linear_lut_not_yet_implemented(self): - # Update after FIXME - # Arrange - black = 1 - white = 255 + # Act + rawmode, data_out = palette.getdata() - # Act - with pytest.raises(NotImplementedError): - ImagePalette.make_linear_lut(black, white) + # Assert + assert rawmode == "RGB" + assert data_in == data_out - def test_make_gamma_lut(self): - # Arrange - exp = 5 - # Act - lut = ImagePalette.make_gamma_lut(exp) +def test_2bit_palette(tmp_path): + # issue #2258, 2 bit palettes are corrupted. + outfile = str(tmp_path / "temp.png") - # Assert - assert isinstance(lut, list) - assert len(lut) == 256 - # Check a few values - assert lut[0] == 0 - assert lut[63] == 0 - assert lut[127] == 8 - assert lut[191] == 60 - assert lut[255] == 255 + rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2 + img = Image.frombytes("P", (6, 1), rgb) + img.putpalette(b"\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF") # RGB + img.save(outfile, format="PNG") - def test_rawmode_valueerrors(self): - # Arrange - palette = ImagePalette.raw("RGB", list(range(256)) * 3) + with Image.open(outfile) as reloaded: + assert_image_equal(img, reloaded) - # Act / Assert - with pytest.raises(ValueError): - palette.tobytes() - with pytest.raises(ValueError): - palette.getcolor((1, 2, 3)) - f = self.tempfile("temp.lut") - with pytest.raises(ValueError): - palette.save(f) - def test_getdata(self): - # Arrange - data_in = list(range(256)) * 3 - palette = ImagePalette.ImagePalette("RGB", data_in) - - # Act - mode, data_out = palette.getdata() - - # Assert - assert mode == "RGB;L" - - def test_rawmode_getdata(self): - # Arrange - data_in = list(range(256)) * 3 - palette = ImagePalette.raw("RGB", data_in) - - # Act - rawmode, data_out = palette.getdata() - - # Assert - assert rawmode == "RGB" - assert data_in == data_out - - def test_2bit_palette(self): - # issue #2258, 2 bit palettes are corrupted. - outfile = self.tempfile("temp.png") - - rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2 - img = Image.frombytes("P", (6, 1), rgb) - img.putpalette(b"\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF") # RGB - img.save(outfile, format="PNG") - - with Image.open(outfile) as reloaded: - assert_image_equal(img, reloaded) - - def test_invalid_palette(self): - with pytest.raises(IOError): - ImagePalette.load("Tests/images/hopper.jpg") +def test_invalid_palette(): + with pytest.raises(IOError): + ImagePalette.load("Tests/images/hopper.jpg") diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 4af196ce9..b3fe9df97 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,98 +1,105 @@ import pytest from PIL import Image, ImageSequence, TiffImagePlugin -from .helper import PillowTestCase, assert_image_equal, hopper, skip_unless_feature +from .helper import assert_image_equal, hopper, skip_unless_feature -class TestImageSequence(PillowTestCase): - def test_sanity(self): +def test_sanity(tmp_path): - test_file = self.tempfile("temp.im") + test_file = str(tmp_path / "temp.im") - im = hopper("RGB") - im.save(test_file) + im = hopper("RGB") + im.save(test_file) - seq = ImageSequence.Iterator(im) + seq = ImageSequence.Iterator(im) - index = 0 - for frame in seq: - assert_image_equal(im, frame) - assert im.tell() == index - index += 1 + index = 0 + for frame in seq: + assert_image_equal(im, frame) + assert im.tell() == index + index += 1 - assert index == 1 + assert index == 1 - with pytest.raises(AttributeError): - ImageSequence.Iterator(0) + with pytest.raises(AttributeError): + ImageSequence.Iterator(0) - def test_iterator(self): - with Image.open("Tests/images/multipage.tiff") as im: - i = ImageSequence.Iterator(im) - for index in range(0, im.n_frames): - assert i[index] == next(i) - with pytest.raises(IndexError): - i[index + 1] - with pytest.raises(StopIteration): - next(i) - def test_iterator_min_frame(self): - with Image.open("Tests/images/hopper.psd") as im: - i = ImageSequence.Iterator(im) - for index in range(1, im.n_frames): - assert i[index] == next(i) +def test_iterator(): + with Image.open("Tests/images/multipage.tiff") as im: + i = ImageSequence.Iterator(im) + for index in range(0, im.n_frames): + assert i[index] == next(i) + with pytest.raises(IndexError): + i[index + 1] + with pytest.raises(StopIteration): + next(i) - def _test_multipage_tiff(self): - with Image.open("Tests/images/multipage.tiff") as im: - for index, frame in enumerate(ImageSequence.Iterator(im)): - frame.load() - assert index == im.tell() - frame.convert("RGB") - def test_tiff(self): - self._test_multipage_tiff() +def test_iterator_min_frame(): + with Image.open("Tests/images/hopper.psd") as im: + i = ImageSequence.Iterator(im) + for index in range(1, im.n_frames): + assert i[index] == next(i) - @skip_unless_feature("libtiff") - def test_libtiff(self): - TiffImagePlugin.READ_LIBTIFF = True - self._test_multipage_tiff() - TiffImagePlugin.READ_LIBTIFF = False - def test_consecutive(self): - with Image.open("Tests/images/multipage.tiff") as im: - firstFrame = None - for frame in ImageSequence.Iterator(im): - if firstFrame is None: - firstFrame = frame.copy() - for frame in ImageSequence.Iterator(im): - assert_image_equal(frame, firstFrame) - break +def _test_multipage_tiff(): + with Image.open("Tests/images/multipage.tiff") as im: + for index, frame in enumerate(ImageSequence.Iterator(im)): + frame.load() + assert index == im.tell() + frame.convert("RGB") - def test_palette_mmap(self): - # Using mmap in ImageFile can require to reload the palette. - with Image.open("Tests/images/multipage-mmap.tiff") as im: - color1 = im.getpalette()[0:3] - im.seek(0) - color2 = im.getpalette()[0:3] - assert color1 == color2 - def test_all_frames(self): - # Test a single image - with Image.open("Tests/images/iss634.gif") as im: - ims = ImageSequence.all_frames(im) +def test_tiff(): + _test_multipage_tiff() - assert len(ims) == 42 - for i, im_frame in enumerate(ims): - assert im_frame is not im - im.seek(i) - assert_image_equal(im, im_frame) +@skip_unless_feature("libtiff") +def test_libtiff(): + TiffImagePlugin.READ_LIBTIFF = True + _test_multipage_tiff() + TiffImagePlugin.READ_LIBTIFF = False - # Test a series of images - ims = ImageSequence.all_frames([im, hopper(), im]) - assert len(ims) == 85 - # Test an operation - ims = ImageSequence.all_frames(im, lambda im_frame: im_frame.rotate(90)) - for i, im_frame in enumerate(ims): - im.seek(i) - assert_image_equal(im.rotate(90), im_frame) +def test_consecutive(): + with Image.open("Tests/images/multipage.tiff") as im: + firstFrame = None + for frame in ImageSequence.Iterator(im): + if firstFrame is None: + firstFrame = frame.copy() + for frame in ImageSequence.Iterator(im): + assert_image_equal(frame, firstFrame) + break + + +def test_palette_mmap(): + # Using mmap in ImageFile can require to reload the palette. + with Image.open("Tests/images/multipage-mmap.tiff") as im: + color1 = im.getpalette()[0:3] + im.seek(0) + color2 = im.getpalette()[0:3] + assert color1 == color2 + + +def test_all_frames(): + # Test a single image + with Image.open("Tests/images/iss634.gif") as im: + ims = ImageSequence.all_frames(im) + + assert len(ims) == 42 + for i, im_frame in enumerate(ims): + assert im_frame is not im + + im.seek(i) + assert_image_equal(im, im_frame) + + # Test a series of images + ims = ImageSequence.all_frames([im, hopper(), im]) + assert len(ims) == 85 + + # Test an operation + ims = ImageSequence.all_frames(im, lambda im_frame: im_frame.rotate(90)) + for i, im_frame in enumerate(ims): + im.seek(i) + assert_image_equal(im.rotate(90), im_frame) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index d6e63d44f..b1ddc75e9 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,11 +1,10 @@ -import unittest - +import pytest from PIL import ImageWin -from .helper import PillowTestCase, hopper, is_win32 +from .helper import hopper, is_win32 -class TestImageWin(PillowTestCase): +class TestImageWin: def test_sanity(self): dir(ImageWin) @@ -32,8 +31,8 @@ class TestImageWin(PillowTestCase): assert wnd2 == 50 -@unittest.skipUnless(is_win32(), "Windows only") -class TestImageWinDib(PillowTestCase): +@pytest.mark.skipif(not is_win32(), reason="Windows only") +class TestImageWinDib: def test_dib_image(self): # Arrange im = hopper() diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 179bd17ac..45c60fa10 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,15 +1,9 @@ import shutil +import pytest from PIL import GifImagePlugin, Image, JpegImagePlugin -from .helper import ( - PillowTestCase, - cjpeg_available, - djpeg_available, - is_win32, - netpbm_available, - unittest, -) +from .helper import cjpeg_available, djpeg_available, is_win32, netpbm_available TEST_JPG = "Tests/images/hopper.jpg" TEST_GIF = "Tests/images/hopper.gif" @@ -17,38 +11,38 @@ TEST_GIF = "Tests/images/hopper.gif" test_filenames = ("temp_';", 'temp_";', "temp_'\"|", "temp_'\"||", "temp_'\"&&") -@unittest.skipIf(is_win32(), "requires Unix or macOS") -class TestShellInjection(PillowTestCase): - def assert_save_filename_check(self, src_img, save_func): +@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") +class TestShellInjection: + def assert_save_filename_check(self, tmp_path, src_img, save_func): for filename in test_filenames: - dest_file = self.tempfile(filename) + dest_file = str(tmp_path / filename) save_func(src_img, 0, dest_file) # If file can't be opened, shell injection probably occurred with Image.open(dest_file) as im: im.load() - @unittest.skipUnless(djpeg_available(), "djpeg not available") - def test_load_djpeg_filename(self): + @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") + def test_load_djpeg_filename(self, tmp_path): for filename in test_filenames: - src_file = self.tempfile(filename) + src_file = str(tmp_path / filename) shutil.copy(TEST_JPG, src_file) with Image.open(src_file) as im: im.load_djpeg() - @unittest.skipUnless(cjpeg_available(), "cjpeg not available") - def test_save_cjpeg_filename(self): + @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") + def test_save_cjpeg_filename(self, tmp_path): with Image.open(TEST_JPG) as im: - self.assert_save_filename_check(im, JpegImagePlugin._save_cjpeg) + self.assert_save_filename_check(tmp_path, im, JpegImagePlugin._save_cjpeg) - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_filename_bmp_mode(self): + @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") + def test_save_netpbm_filename_bmp_mode(self, tmp_path): with Image.open(TEST_GIF) as im: im = im.convert("RGB") - self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) + self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_filename_l_mode(self): + @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") + def test_save_netpbm_filename_l_mode(self, tmp_path): with Image.open(TEST_GIF) as im: im = im.convert("L") - self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) + self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index a044d04b3..707284d7b 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -3,56 +3,58 @@ from fractions import Fraction from PIL import Image, TiffImagePlugin, features from PIL.TiffImagePlugin import IFDRational -from .helper import PillowTestCase, hopper +from .helper import hopper -class Test_IFDRational(PillowTestCase): - def _test_equal(self, num, denom, target): +def _test_equal(num, denom, target): - t = IFDRational(num, denom) + t = IFDRational(num, denom) - assert target == t - assert t == target + assert target == t + assert t == target - def test_sanity(self): - self._test_equal(1, 1, 1) - self._test_equal(1, 1, Fraction(1, 1)) +def test_sanity(): - self._test_equal(2, 2, 1) - self._test_equal(1.0, 1, Fraction(1, 1)) + _test_equal(1, 1, 1) + _test_equal(1, 1, Fraction(1, 1)) - self._test_equal(Fraction(1, 1), 1, Fraction(1, 1)) - self._test_equal(IFDRational(1, 1), 1, 1) + _test_equal(2, 2, 1) + _test_equal(1.0, 1, Fraction(1, 1)) - self._test_equal(1, 2, Fraction(1, 2)) - self._test_equal(1, 2, IFDRational(1, 2)) + _test_equal(Fraction(1, 1), 1, Fraction(1, 1)) + _test_equal(IFDRational(1, 1), 1, 1) - def test_nonetype(self): - # Fails if the _delegate function doesn't return a valid function + _test_equal(1, 2, Fraction(1, 2)) + _test_equal(1, 2, IFDRational(1, 2)) - xres = IFDRational(72) - yres = IFDRational(72) - assert xres._val is not None - assert xres.numerator is not None - assert xres.denominator is not None - assert yres._val is not None - assert xres and 1 - assert xres and yres +def test_nonetype(): + # Fails if the _delegate function doesn't return a valid function - def test_ifd_rational_save(self): - methods = (True, False) - if not features.check("libtiff"): - methods = (False,) + xres = IFDRational(72) + yres = IFDRational(72) + assert xres._val is not None + assert xres.numerator is not None + assert xres.denominator is not None + assert yres._val is not None - for libtiff in methods: - TiffImagePlugin.WRITE_LIBTIFF = libtiff + assert xres and 1 + assert xres and yres - im = hopper() - out = self.tempfile("temp.tiff") - res = IFDRational(301, 1) - im.save(out, dpi=(res, res), compression="raw") - with Image.open(out) as reloaded: - assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) +def test_ifd_rational_save(tmp_path): + methods = (True, False) + if not features.check("libtiff"): + methods = (False,) + + for libtiff in methods: + TiffImagePlugin.WRITE_LIBTIFF = libtiff + + im = hopper() + out = str(tmp_path / "temp.tiff") + res = IFDRational(301, 1) + im.save(out, dpi=(res, res), compression="raw") + + with Image.open(out) as reloaded: + assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py index 24e7bebc8..720926e53 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -1,13 +1,13 @@ -from .helper import PillowTestCase, assert_image_equal, assert_image_similar, hopper +from .helper import assert_image_equal, assert_image_similar, hopper -class TestUploader(PillowTestCase): - def check_upload_equal(self): - result = hopper("P").convert("RGB") - target = hopper("RGB") - assert_image_equal(result, target) +def check_upload_equal(): + result = hopper("P").convert("RGB") + target = hopper("RGB") + assert_image_equal(result, target) - def check_upload_similar(self): - result = hopper("P").convert("RGB") - target = hopper("RGB") - assert_image_similar(result, target, 0) + +def check_upload_similar(): + result = hopper("P").convert("RGB") + target = hopper("RGB") + assert_image_similar(result, target, 0) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 53dac2b92..a12de82e2 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -298,6 +298,11 @@ The :py:meth:`~PIL.Image.Image.open` method may set the following **exif** Raw EXIF data from the image. +**comment** + A comment about the image. + + .. versionadded:: 7.1.0 + The :py:meth:`~PIL.Image.Image.save` method supports the following options: diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index acb61291c..868402615 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -74,7 +74,8 @@ Convert files to JPEG outfile = f + ".jpg" if infile != outfile: try: - Image.open(infile).save(outfile) + with Image.open(infile) as im: + im.save(outfile) except IOError: print("cannot convert", infile) diff --git a/docs/installation.rst b/docs/installation.rst index 062881411..ca3ae61ab 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -47,8 +47,8 @@ Basic Installation Install Pillow with :command:`pip`:: - python -m pip install --upgrade pip - python -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow Windows Installation @@ -59,8 +59,8 @@ supported Pythons in both 32 and 64-bit versions in wheel, egg, and executable installers. These binaries have all of the optional libraries included except for raqm and libimagequant:: - python -m pip install --upgrade pip - python -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow macOS Installation @@ -71,8 +71,8 @@ versions in the wheel format. These include support for all optional libraries except libimagequant. Raqm support requires libraqm, fribidi, and harfbuzz to be installed separately:: - python -m pip install --upgrade pip - python -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow Linux Installation ^^^^^^^^^^^^^^^^^^ @@ -82,8 +82,8 @@ versions in the manylinux wheel format. These include support for all optional libraries except libimagequant. Raqm support requires libraqm, fribidi, and harfbuzz to be installed separately:: - python -m pip install --upgrade pip - python -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow Most major Linux distributions, including Fedora, Debian/Ubuntu and ArchLinux also include Pillow in packages that previously contained @@ -195,8 +195,8 @@ Many of Pillow's features require external libraries: Once you have installed the prerequisites, run:: - python -m pip install --upgrade pip - python -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow If the prerequisites are installed in the standard library locations for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no @@ -206,7 +206,7 @@ those locations by editing :file:`setup.py` or :file:`setup.cfg`, or by adding environment variables on the command line:: - CFLAGS="-I/usr/pkg/include" python -m pip install --upgrade Pillow + CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow If Pillow has been previously built without the required prerequisites, it may be necessary to manually clear the pip cache or @@ -254,7 +254,7 @@ Sample usage:: or using pip:: - python -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" + python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" Building on macOS @@ -280,8 +280,8 @@ Then see ``depends/install_raqm_cmake.sh`` to install libraqm. Now install Pillow with:: - python -m pip install --upgrade pip - python -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow or from within the uncompressed source directory:: diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 6700deab4..d888913c3 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -20,14 +20,14 @@ Example: Draw a gray cross over an image from PIL import Image, ImageDraw - im = Image.open("hopper.jpg") + with Image.open("hopper.jpg") as im: - draw = ImageDraw.Draw(im) - draw.line((0, 0) + im.size, fill=128) - draw.line((0, im.size[1], im.size[0], 0), fill=128) + draw = ImageDraw.Draw(im) + draw.line((0, 0) + im.size, fill=128) + draw.line((0, im.size[1], im.size[0], 0), fill=128) - # write to stdout - im.save(sys.stdout, "PNG") + # write to stdout + im.save(sys.stdout, "PNG") Concepts diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index 251ea3a93..353e8099e 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -14,12 +14,11 @@ Extracting frames from an animation from PIL import Image, ImageSequence - im = Image.open("animation.fli") - - index = 1 - for frame in ImageSequence.Iterator(im): - frame.save("frame%d.png" % index) - index += 1 + with Image.open("animation.fli") as im: + index = 1 + for frame in ImageSequence.Iterator(im): + frame.save("frame%d.png" % index) + index += 1 The :py:class:`~PIL.ImageSequence.Iterator` class ------------------------------------------------- diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 8a8569922..f28e58f86 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -17,8 +17,8 @@ changes it. .. code-block:: python from PIL import Image - im = Image.open('hopper.jpg') - px = im.load() + with Image.open('hopper.jpg') as im: + px = im.load() print (px[4,4]) px[4,4] = (0,0,0) print (px[4,4]) diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index 6a492cd86..e00741c43 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -18,8 +18,8 @@ The following script loads an image, accesses one pixel from it, then changes it .. code-block:: python from PIL import Image - im = Image.open('hopper.jpg') - px = im.load() + with Image.open('hopper.jpg') as im: + px = im.load() print (px[4,4]) px[4,4] = (0,0,0) print (px[4,4]) diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index 2e3df6447..e99d233d2 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -18,6 +18,27 @@ been resolved. im = Image.open("hopper.jpg") im.save("out.jpg", quality=0) +API Additions +============= + +Reading JPEG comments +^^^^^^^^^^^^^^^^^^^^^ + +When opening a JPEG image, the comment may now be read into +:py:attr:`~PIL.Image.Image.info`. + +Other Changes +============= + +If present, only use alpha channel for bounding box +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When the :py:meth:`~PIL.Image.Image.getbbox` method calculates the bounding +box, for an RGB image it trims black pixels. Similarly, for an RGBA image it +would trim black transparent pixels. This is now changed so that if an image +has an alpha channel (RGBA, RGBa, PA, LA, La), any transparent pixels are +trimmed. + Improved APNG support ^^^^^^^^^^^^^^^^^^^^^ @@ -25,4 +46,4 @@ Added support for reading and writing Animated Portable Network Graphics (APNG) The PNG plugin now supports using the :py:meth:`~PIL.Image.Image.seek` method and the :py:class:`~PIL.ImageSequence.Iterator` class to read APNG frame sequences. The PNG plugin also now supports using the ``append_images`` argument to write APNG frame -sequences. See :ref:`apng-sequences` for further details. +sequences. See :ref:`apng-sequences` for further details. \ No newline at end of file diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 2fff26a79..e27a57671 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -141,8 +141,8 @@ def Ghostscript(tile, size, fp, scale=1): startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW subprocess.check_call(command, startupinfo=startupinfo) - im = Image.open(outfile) - im.load() + out_im = Image.open(outfile) + out_im.load() finally: try: os.unlink(outfile) @@ -151,7 +151,9 @@ def Ghostscript(tile, size, fp, scale=1): except OSError: pass - return im.im.copy() + im = out_im.im.copy() + out_im.close() + return im class PSFile: diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 6f26b90fa..c00392615 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -370,7 +370,7 @@ if __name__ == "__main__": for size in imf.info["sizes"]: imf.size = size imf.save("out-%s-%s-%s.png" % size) - im = Image.open(sys.argv[1]) - im.save("out.png") + with Image.open(sys.argv[1]) as im: + im.save("out.png") if sys.platform == "windows": os.startfile("out.png") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index dec94f2d0..b1e8ad3ea 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2236,20 +2236,22 @@ class Image: :returns: None """ - # preserve aspect ratio - x, y = self.size - if x > size[0]: - y = max(round(y * size[0] / x), 1) - x = round(size[0]) - if y > size[1]: - x = max(round(x * size[1] / y), 1) - y = round(size[1]) - size = x, y - box = None - - if size == self.size: + x, y = map(math.floor, size) + if x >= self.width and y >= self.height: return + def round_aspect(number, key): + return max(min(math.floor(number), math.ceil(number), key=key), 1) + + # preserve aspect ratio + aspect = self.width / self.height + if x / y >= aspect: + x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) + else: + y = round_aspect(x / aspect, key=lambda n: abs(aspect - x / n)) + size = (x, y) + + box = None if reducing_gap is not None: res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) if res is not None: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 619800829..027e4c42e 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -71,7 +71,10 @@ class ImageFont: def _load_pilfont(self, filename): with open(filename, "rb") as fp: + image = None for ext in (".png", ".gif", ".pbm"): + if image: + image.close() try: fullname = os.path.splitext(filename)[0] + ext image = Image.open(fullname) @@ -81,11 +84,14 @@ class ImageFont: if image and image.mode in ("1", "L"): break else: + if image: + image.close() raise OSError("cannot find glyph data file") self.file = fullname - return self._load_pilfont_data(fp, image) + self._load_pilfont_data(fp, image) + image.close() def _load_pilfont_data(self, file, image): diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index e587d942d..059a71fe7 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -35,7 +35,9 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False): im.load() os.unlink(filepath) if bbox: - im = im.crop(bbox) + im_cropped = im.crop(bbox) + im.close() + return im_cropped else: offset, size, data = Image.core.grabscreen(include_layered_windows, all_screens) im = Image.frombytes( diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index f7e809279..fc5089423 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -203,4 +203,5 @@ if __name__ == "__main__": print("Syntax: python ImageShow.py imagefile [title]") sys.exit() - print(show(Image.open(sys.argv[1]), *sys.argv[2:])) + with Image.open(sys.argv[1]) as im: + print(show(im, *sys.argv[2:])) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 86a7ee8cb..b2f976dda 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -158,9 +158,9 @@ class IptcImageFile(ImageFile.ImageFile): o.close() try: - _im = Image.open(outfile) - _im.load() - self.im = _im.im + with Image.open(outfile) as _im: + _im.load() + self.im = _im.im finally: try: os.unlink(outfile) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index b5371db73..2aa029efb 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -176,6 +176,7 @@ def COM(self, marker): n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) + self.info["comment"] = s self.app["COM"] = s # compatibility self.applist.append(("COM", s)) @@ -448,9 +449,9 @@ class JpegImageFile(ImageFile.ImageFile): raise ValueError("Invalid Filename") try: - _im = Image.open(path) - _im.load() - self.im = _im.im + with Image.open(path) as _im: + _im.load() + self.im = _im.im finally: try: os.unlink(path) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index dd0620c14..cbd31cf82 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -304,21 +304,21 @@ if __name__ == "__main__": print("input image must be in Spider format") sys.exit() - im = Image.open(filename) - print("image: " + str(im)) - print("format: " + str(im.format)) - print("size: " + str(im.size)) - print("mode: " + str(im.mode)) - print("max, min: ", end=" ") - print(im.getextrema()) + with Image.open(filename) as im: + print("image: " + str(im)) + print("format: " + str(im.format)) + print("size: " + str(im.size)) + print("mode: " + str(im.mode)) + print("max, min: ", end=" ") + print(im.getextrema()) - if len(sys.argv) > 2: - outfile = sys.argv[2] + if len(sys.argv) > 2: + outfile = sys.argv[2] - # perform some image operation - im = im.transpose(Image.FLIP_LEFT_RIGHT) - print( - "saving a flipped version of %s as %s " - % (os.path.basename(filename), outfile) - ) - im.save(outfile, SpiderImageFile.format) + # perform some image operation + im = im.transpose(Image.FLIP_LEFT_RIGHT) + print( + "saving a flipped version of %s as %s " + % (os.path.basename(filename), outfile) + ) + im.save(outfile, SpiderImageFile.format) diff --git a/src/PIL/features.py b/src/PIL/features.py index 5822febab..0a1d5d611 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -1,6 +1,7 @@ import collections import os import sys +import warnings import PIL @@ -76,14 +77,14 @@ def get_supported_features(): def check(feature): - return ( - feature in modules - and check_module(feature) - or feature in codecs - and check_codec(feature) - or feature in features - and check_feature(feature) - ) + if feature in modules: + return check_module(feature) + if feature in codecs: + return check_codec(feature) + if feature in features: + return check_feature(feature) + warnings.warn("Unknown feature '%s'." % feature, stacklevel=2) + return False def get_supported(): diff --git a/src/_imagingft.c b/src/_imagingft.c index 62a4c283e..a96fc8739 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -782,9 +782,6 @@ font_render(FontObject* self, PyObject* args) im = (Imaging) id; /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ load_flags = FT_LOAD_NO_BITMAP; - if (stroker == NULL) { - load_flags |= FT_LOAD_RENDER; - } if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } @@ -792,7 +789,7 @@ font_render(FontObject* self, PyObject* args) ascender = 0; for (i = 0; i < count; i++) { index = glyph_info[i].index; - error = FT_Load_Glyph(self->face, index, load_flags); + error = FT_Load_Glyph(self->face, index, load_flags | FT_LOAD_RENDER); if (error) { return geterror(error); } @@ -806,6 +803,10 @@ font_render(FontObject* self, PyObject* args) ascender = temp; } + if (stroker == NULL) { + load_flags |= FT_LOAD_RENDER; + } + x = y = 0; horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; for (i = 0; i < count; i++) { @@ -908,7 +909,7 @@ font_render(FontObject* self, PyObject* args) (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) ||\ (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) static PyObject* - font_getvarnames(FontObject* self, PyObject* args) + font_getvarnames(FontObject* self) { int error; FT_UInt i, j, num_namedstyles, name_count; @@ -947,7 +948,7 @@ font_render(FontObject* self, PyObject* args) } static PyObject* - font_getvaraxes(FontObject* self, PyObject* args) + font_getvaraxes(FontObject* self) { int error; FT_UInt i, j, num_axis, name_count; @@ -1077,8 +1078,8 @@ static PyMethodDef font_methods[] = { #if FREETYPE_MAJOR > 2 ||\ (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) ||\ (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) - {"getvarnames", (PyCFunction) font_getvarnames, METH_VARARGS }, - {"getvaraxes", (PyCFunction) font_getvaraxes, METH_VARARGS }, + {"getvarnames", (PyCFunction) font_getvarnames, METH_NOARGS }, + {"getvaraxes", (PyCFunction) font_getvaraxes, METH_NOARGS }, {"setvarname", (PyCFunction) font_setvarname, METH_VARARGS}, {"setvaraxes", (PyCFunction) font_setvaraxes, METH_VARARGS}, #endif diff --git a/src/_webp.c b/src/_webp.c index 4581ef89d..0a2b7e24e 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -369,7 +369,7 @@ PyObject* _anim_decoder_dealloc(PyObject* self) Py_RETURN_NONE; } -PyObject* _anim_decoder_get_info(PyObject* self, PyObject* args) +PyObject* _anim_decoder_get_info(PyObject* self) { WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self; WebPAnimInfo* info = &(decp->info); @@ -406,7 +406,7 @@ PyObject* _anim_decoder_get_chunk(PyObject* self, PyObject* args) return ret; } -PyObject* _anim_decoder_get_next(PyObject* self, PyObject* args) +PyObject* _anim_decoder_get_next(PyObject* self) { uint8_t* buf; int timestamp; @@ -428,13 +428,7 @@ PyObject* _anim_decoder_get_next(PyObject* self, PyObject* args) return ret; } -PyObject* _anim_decoder_has_more_frames(PyObject* self, PyObject* args) -{ - WebPAnimDecoderObject* decp = (WebPAnimDecoderObject*)self; - return Py_BuildValue("i", WebPAnimDecoderHasMoreFrames(decp->dec)); -} - -PyObject* _anim_decoder_reset(PyObject* self, PyObject* args) +PyObject* _anim_decoder_reset(PyObject* self) { WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self; WebPAnimDecoderReset(decp->dec); @@ -489,11 +483,10 @@ static PyTypeObject WebPAnimEncoder_Type = { // WebPAnimDecoder methods static struct PyMethodDef _anim_decoder_methods[] = { - {"get_info", (PyCFunction)_anim_decoder_get_info, METH_VARARGS, "get_info"}, + {"get_info", (PyCFunction)_anim_decoder_get_info, METH_NOARGS, "get_info"}, {"get_chunk", (PyCFunction)_anim_decoder_get_chunk, METH_VARARGS, "get_chunk"}, - {"get_next", (PyCFunction)_anim_decoder_get_next, METH_VARARGS, "get_next"}, - {"has_more_frames", (PyCFunction)_anim_decoder_has_more_frames, METH_VARARGS, "has_more_frames"}, - {"reset", (PyCFunction)_anim_decoder_reset, METH_VARARGS, "reset"}, + {"get_next", (PyCFunction)_anim_decoder_get_next, METH_NOARGS, "get_next"}, + {"reset", (PyCFunction)_anim_decoder_reset, METH_NOARGS, "reset"}, {NULL, NULL} /* sentinel */ }; @@ -775,7 +768,7 @@ end: // Return the decoder's version number, packed in hexadecimal using 8bits for // each of major/minor/revision. E.g: v2.5.7 is 0x020507. -PyObject* WebPDecoderVersion_wrapper(PyObject* self, PyObject* args){ +PyObject* WebPDecoderVersion_wrapper() { return Py_BuildValue("i", WebPGetDecoderVersion()); } @@ -787,7 +780,7 @@ int WebPDecoderBuggyAlpha(void) { return WebPGetDecoderVersion()==0x0103; } -PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){ +PyObject* WebPDecoderBuggyAlpha_wrapper() { return Py_BuildValue("i", WebPDecoderBuggyAlpha()); } @@ -803,8 +796,8 @@ static PyMethodDef webpMethods[] = #endif {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, - {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"}, - {"WebPDecoderBuggyAlpha", WebPDecoderBuggyAlpha_wrapper, METH_VARARGS, "WebPDecoderBuggyAlpha"}, + {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_NOARGS, "WebPVersion"}, + {"WebPDecoderBuggyAlpha", WebPDecoderBuggyAlpha_wrapper, METH_NOARGS, "WebPDecoderBuggyAlpha"}, {NULL, NULL} }; diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index b63888f87..bf5d61dd3 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -55,8 +55,19 @@ ImagingGetBBox(Imaging im, int bbox[4]) GETBBOX(image8, 0xff); } else { INT32 mask = 0xffffffff; - if (im->bands == 3) + if (im->bands == 3) { ((UINT8*) &mask)[3] = 0; + } else if (strcmp(im->mode, "RGBa") == 0 || + strcmp(im->mode, "RGBA") == 0 || + strcmp(im->mode, "La") == 0 || + strcmp(im->mode, "LA") == 0 || + strcmp(im->mode, "PA") == 0) { +#ifdef WORDS_BIGENDIAN + mask = 0x000000ff; +#else + mask = 0xff000000; +#endif + } GETBBOX(image32, mask); }