From 44e661f25a55fd967090afcf247d2dd4082a21a1 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 22 Feb 2020 23:03:01 +0200 Subject: [PATCH] Convert to use pytest --- Tests/test_color_lut.py | 15 +- Tests/test_file_eps.py | 375 ++++----- Tests/test_file_gif.py | 1321 ++++++++++++++++---------------- Tests/test_file_icns.py | 201 ++--- Tests/test_file_ico.py | 160 ++-- Tests/test_file_msp.py | 121 +-- Tests/test_file_spider.py | 233 +++--- Tests/test_image.py | 43 +- Tests/test_image_resample.py | 21 +- Tests/test_imagefont_bitmap.py | 61 +- 10 files changed, 1310 insertions(+), 1241 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 6f8cb05b7..b34dbadb6 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,10 +1,9 @@ -import unittest from array import array import pytest from PIL import Image, ImageFilter -from .helper import PillowTestCase, assert_image_equal +from .helper import assert_image_equal try: import numpy @@ -12,7 +11,7 @@ except ImportError: numpy = None -class TestColorLut3DCoreAPI(PillowTestCase): +class TestColorLut3DCoreAPI: def generate_identity_table(self, channels, size): if isinstance(size, tuple): size1D, size2D, size3D = size @@ -271,7 +270,7 @@ class TestColorLut3DCoreAPI(PillowTestCase): assert transformed[205, 205] == (255, 255, 0) -class TestColorLut3DFilter(PillowTestCase): +class TestColorLut3DFilter: def test_wrong_args(self): with pytest.raises(ValueError, match="should be either an integer"): ImageFilter.Color3DLUT("small", [1]) @@ -317,7 +316,7 @@ class TestColorLut3DFilter(PillowTestCase): assert tuple(lut.size) == (2, 2, 2) assert lut.table == list(range(4)) * 8 - @unittest.skipIf(numpy is None, "Numpy is not installed") + @pytest.mark.skipif(numpy is None, reason="NumPy not installed") def test_numpy_sources(self): table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16) with pytest.raises(ValueError, match="should have either channels"): @@ -350,7 +349,7 @@ class TestColorLut3DFilter(PillowTestCase): table[0] = 33 assert lut.table[0] == 33 - @unittest.skipIf(numpy is None, "Numpy is not installed") + @pytest.mark.skipif(numpy is None, reason="NumPy not installed") def test_numpy_formats(self): g = Image.linear_gradient("L") im = Image.merge( @@ -402,7 +401,7 @@ class TestColorLut3DFilter(PillowTestCase): ) -class TestGenerateColorLut3D(PillowTestCase): +class TestGenerateColorLut3D: def test_wrong_channels_count(self): with pytest.raises(ValueError, match="3 or 4 output channels"): ImageFilter.Color3DLUT.generate( @@ -450,7 +449,7 @@ class TestGenerateColorLut3D(PillowTestCase): assert im == im.filter(lut) -class TestTransformColorLut3D(PillowTestCase): +class TestTransformColorLut3D: def test_wrong_args(self): source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index af980b94a..4729f4a9a 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,190 +1,207 @@ import io -import unittest import pytest from PIL import EpsImagePlugin, Image, features -from .helper import PillowTestCase, assert_image_similar, hopper, skip_unless_feature +from .helper import assert_image_similar, hopper, skip_unless_feature HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() # Our two EPS test files (they are identical except for their bounding boxes) -file1 = "Tests/images/zero_bb.eps" -file2 = "Tests/images/non_zero_bb.eps" +FILE1 = "Tests/images/zero_bb.eps" +FILE2 = "Tests/images/non_zero_bb.eps" # Due to palletization, we'll need to convert these to RGB after load -file1_compare = "Tests/images/zero_bb.png" -file1_compare_scale2 = "Tests/images/zero_bb_scale2.png" +FILE1_COMPARE = "Tests/images/zero_bb.png" +FILE1_COMPARE_SCALE2 = "Tests/images/zero_bb_scale2.png" -file2_compare = "Tests/images/non_zero_bb.png" -file2_compare_scale2 = "Tests/images/non_zero_bb_scale2.png" +FILE2_COMPARE = "Tests/images/non_zero_bb.png" +FILE2_COMPARE_SCALE2 = "Tests/images/non_zero_bb_scale2.png" # EPS test files with binary preview -file3 = "Tests/images/binary_preview_map.eps" +FILE3 = "Tests/images/binary_preview_map.eps" -class TestFileEps(PillowTestCase): - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_sanity(self): - # Regular scale - with Image.open(file1) as image1: - image1.load() - assert image1.mode == "RGB" - assert image1.size == (460, 352) - assert image1.format == "EPS" +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_sanity(): + # Regular scale + with Image.open(FILE1) as image1: + image1.load() + assert image1.mode == "RGB" + assert image1.size == (460, 352) + assert image1.format == "EPS" - with Image.open(file2) as image2: - image2.load() - assert image2.mode == "RGB" - assert image2.size == (360, 252) - assert image2.format == "EPS" + with Image.open(FILE2) as image2: + image2.load() + assert image2.mode == "RGB" + assert image2.size == (360, 252) + assert image2.format == "EPS" - # Double scale - with Image.open(file1) as image1_scale2: - image1_scale2.load(scale=2) - assert image1_scale2.mode == "RGB" - assert image1_scale2.size == (920, 704) - assert image1_scale2.format == "EPS" + # Double scale + with Image.open(FILE1) as image1_scale2: + image1_scale2.load(scale=2) + assert image1_scale2.mode == "RGB" + assert image1_scale2.size == (920, 704) + assert image1_scale2.format == "EPS" - with Image.open(file2) as image2_scale2: - image2_scale2.load(scale=2) - assert image2_scale2.mode == "RGB" - assert image2_scale2.size == (720, 504) - assert image2_scale2.format == "EPS" + with Image.open(FILE2) as image2_scale2: + image2_scale2.load(scale=2) + assert image2_scale2.mode == "RGB" + assert image2_scale2.size == (720, 504) + assert image2_scale2.format == "EPS" - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - with pytest.raises(SyntaxError): - EpsImagePlugin.EpsImageFile(invalid_file) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_cmyk(self): - with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: + with pytest.raises(SyntaxError): + EpsImagePlugin.EpsImageFile(invalid_file) - assert cmyk_image.mode == "CMYK" - assert cmyk_image.size == (100, 100) - assert cmyk_image.format == "EPS" - cmyk_image.load() - assert cmyk_image.mode == "RGB" +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_cmyk(): + with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: - if features.check("jpg"): - with Image.open("Tests/images/pil_sample_rgb.jpg") as target: - assert_image_similar(cmyk_image, target, 10) + assert cmyk_image.mode == "CMYK" + assert cmyk_image.size == (100, 100) + assert cmyk_image.format == "EPS" - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_showpage(self): - # See https://github.com/python-pillow/Pillow/issues/2615 - with Image.open("Tests/images/reqd_showpage.eps") as plot_image: - with Image.open("Tests/images/reqd_showpage.png") as target: - # should not crash/hang - plot_image.load() - # fonts could be slightly different - assert_image_similar(plot_image, target, 6) + cmyk_image.load() + assert cmyk_image.mode == "RGB" - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_file_object(self): - # issue 479 - with Image.open(file1) as image1: - with open(self.tempfile("temp_file.eps"), "wb") as fh: - image1.save(fh, "EPS") + if features.check("jpg"): + with Image.open("Tests/images/pil_sample_rgb.jpg") as target: + assert_image_similar(cmyk_image, target, 10) - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_iobase_object(self): - # issue 479 - with Image.open(file1) as image1: - with open(self.tempfile("temp_iobase.eps"), "wb") as fh: - image1.save(fh, "EPS") - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_bytesio_object(self): - with open(file1, "rb") as f: - img_bytes = io.BytesIO(f.read()) +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_showpage(): + # See https://github.com/python-pillow/Pillow/issues/2615 + with Image.open("Tests/images/reqd_showpage.eps") as plot_image: + with Image.open("Tests/images/reqd_showpage.png") as target: + # should not crash/hang + plot_image.load() + # fonts could be slightly different + assert_image_similar(plot_image, target, 6) - with Image.open(img_bytes) as img: - img.load() - with Image.open(file1_compare) as image1_scale1_compare: - image1_scale1_compare = image1_scale1_compare.convert("RGB") - image1_scale1_compare.load() - assert_image_similar(img, image1_scale1_compare, 5) +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_file_object(tmp_path): + # issue 479 + with Image.open(FILE1) as image1: + with open(str(tmp_path / "temp.eps"), "wb") as fh: + image1.save(fh, "EPS") - def test_image_mode_not_supported(self): - im = hopper("RGBA") - tmpfile = self.tempfile("temp.eps") - with pytest.raises(ValueError): - im.save(tmpfile) - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - @skip_unless_feature("zlib") - def test_render_scale1(self): - # We need png support for these render test +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_iobase_object(tmp_path): + # issue 479 + with Image.open(FILE1) as image1: + with open(str(tmp_path / "temp_iobase.eps"), "wb") as fh: + image1.save(fh, "EPS") - # Zero bounding box - with Image.open(file1) as image1_scale1: - image1_scale1.load() - with Image.open(file1_compare) as image1_scale1_compare: - image1_scale1_compare = image1_scale1_compare.convert("RGB") - image1_scale1_compare.load() - assert_image_similar(image1_scale1, image1_scale1_compare, 5) - # Non-Zero bounding box - with Image.open(file2) as image2_scale1: - image2_scale1.load() - with Image.open(file2_compare) as image2_scale1_compare: - image2_scale1_compare = image2_scale1_compare.convert("RGB") - image2_scale1_compare.load() - assert_image_similar(image2_scale1, image2_scale1_compare, 10) +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_bytesio_object(): + with open(FILE1, "rb") as f: + img_bytes = io.BytesIO(f.read()) - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - @skip_unless_feature("zlib") - def test_render_scale2(self): - # We need png support for these render test + with Image.open(img_bytes) as img: + img.load() - # Zero bounding box - with Image.open(file1) as image1_scale2: - image1_scale2.load(scale=2) - with Image.open(file1_compare_scale2) as image1_scale2_compare: - image1_scale2_compare = image1_scale2_compare.convert("RGB") - image1_scale2_compare.load() - assert_image_similar(image1_scale2, image1_scale2_compare, 5) + with Image.open(FILE1_COMPARE) as image1_scale1_compare: + image1_scale1_compare = image1_scale1_compare.convert("RGB") + image1_scale1_compare.load() + assert_image_similar(img, image1_scale1_compare, 5) - # Non-Zero bounding box - with Image.open(file2) as image2_scale2: - image2_scale2.load(scale=2) - with Image.open(file2_compare_scale2) as image2_scale2_compare: - image2_scale2_compare = image2_scale2_compare.convert("RGB") - image2_scale2_compare.load() - assert_image_similar(image2_scale2, image2_scale2_compare, 10) - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_resize(self): - files = [file1, file2, "Tests/images/illu10_preview.eps"] - for fn in files: - with Image.open(fn) as im: - new_size = (100, 100) - im = im.resize(new_size) - assert im.size == new_size +def test_image_mode_not_supported(tmp_path): + im = hopper("RGBA") + tmpfile = str(tmp_path / "temp.eps") + with pytest.raises(ValueError): + im.save(tmpfile) - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_thumbnail(self): - # Issue #619 - # Arrange - files = [file1, file2] - for fn in files: - with Image.open(file1) as im: - new_size = (100, 100) - im.thumbnail(new_size) - assert max(im.size) == max(new_size) - def test_read_binary_preview(self): - # Issue 302 - # open image with binary preview - with Image.open(file3): - pass +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@skip_unless_feature("zlib") +def test_render_scale1(): + # We need png support for these render test - def _test_readline(self, t, ending): + # Zero bounding box + with Image.open(FILE1) as image1_scale1: + image1_scale1.load() + with Image.open(FILE1_COMPARE) as image1_scale1_compare: + image1_scale1_compare = image1_scale1_compare.convert("RGB") + image1_scale1_compare.load() + assert_image_similar(image1_scale1, image1_scale1_compare, 5) + + # Non-Zero bounding box + with Image.open(FILE2) as image2_scale1: + image2_scale1.load() + with Image.open(FILE2_COMPARE) as image2_scale1_compare: + image2_scale1_compare = image2_scale1_compare.convert("RGB") + image2_scale1_compare.load() + assert_image_similar(image2_scale1, image2_scale1_compare, 10) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@skip_unless_feature("zlib") +def test_render_scale2(): + # We need png support for these render test + + # Zero bounding box + with Image.open(FILE1) as image1_scale2: + image1_scale2.load(scale=2) + with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: + image1_scale2_compare = image1_scale2_compare.convert("RGB") + image1_scale2_compare.load() + assert_image_similar(image1_scale2, image1_scale2_compare, 5) + + # Non-Zero bounding box + with Image.open(FILE2) as image2_scale2: + image2_scale2.load(scale=2) + with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: + image2_scale2_compare = image2_scale2_compare.convert("RGB") + image2_scale2_compare.load() + assert_image_similar(image2_scale2, image2_scale2_compare, 10) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_resize(): + files = [FILE1, FILE2, "Tests/images/illu10_preview.eps"] + for fn in files: + with Image.open(fn) as im: + new_size = (100, 100) + im = im.resize(new_size) + assert im.size == new_size + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_thumbnail(): + # Issue #619 + # Arrange + files = [FILE1, FILE2] + for fn in files: + with Image.open(FILE1) as im: + new_size = (100, 100) + im.thumbnail(new_size) + assert max(im.size) == max(new_size) + + +def test_read_binary_preview(): + # Issue 302 + # open image with binary preview + with Image.open(FILE3): + pass + + +def test_readline(tmp_path): + # check all the freaking line endings possible from the spec + # test_string = u'something\r\nelse\n\rbaz\rbif\n' + line_endings = ["\r\n", "\n", "\n\r", "\r"] + strings = ["something", "else", "baz", "bif"] + + def _test_readline(t, ending): ending = "Failure with line ending: %s" % ( "".join("%s" % ord(s) for s in ending) ) @@ -193,53 +210,49 @@ class TestFileEps(PillowTestCase): assert t.readline().strip("\r\n") == "baz", ending assert t.readline().strip("\r\n") == "bif", ending - def _test_readline_io_psfile(self, test_string, ending): + def _test_readline_io_psfile(test_string, ending): f = io.BytesIO(test_string.encode("latin-1")) t = EpsImagePlugin.PSFile(f) - self._test_readline(t, ending) + _test_readline(t, ending) - def _test_readline_file_psfile(self, test_string, ending): - f = self.tempfile("temp.txt") + def _test_readline_file_psfile(test_string, ending): + f = str(tmp_path / "temp.bufr") with open(f, "wb") as w: w.write(test_string.encode("latin-1")) with open(f, "rb") as r: t = EpsImagePlugin.PSFile(r) - self._test_readline(t, ending) + _test_readline(t, ending) - def test_readline(self): - # check all the freaking line endings possible from the spec - # test_string = u'something\r\nelse\n\rbaz\rbif\n' - line_endings = ["\r\n", "\n", "\n\r", "\r"] - strings = ["something", "else", "baz", "bif"] + for ending in line_endings: + s = ending.join(strings) + _test_readline_io_psfile(s, ending) + _test_readline_file_psfile(s, ending) - for ending in line_endings: - s = ending.join(strings) - self._test_readline_io_psfile(s, ending) - self._test_readline_file_psfile(s, ending) - def test_open_eps(self): - # https://github.com/python-pillow/Pillow/issues/1104 - # Arrange - FILES = [ - "Tests/images/illu10_no_preview.eps", - "Tests/images/illu10_preview.eps", - "Tests/images/illuCS6_no_preview.eps", - "Tests/images/illuCS6_preview.eps", - ] +def test_open_eps(): + # https://github.com/python-pillow/Pillow/issues/1104 + # Arrange + FILES = [ + "Tests/images/illu10_no_preview.eps", + "Tests/images/illu10_preview.eps", + "Tests/images/illuCS6_no_preview.eps", + "Tests/images/illuCS6_preview.eps", + ] - # Act / Assert - for filename in FILES: - with Image.open(filename) as img: - assert img.mode == "RGB" + # Act / Assert + for filename in FILES: + with Image.open(filename) as img: + assert img.mode == "RGB" - @unittest.skipUnless(HAS_GHOSTSCRIPT, "Ghostscript not available") - def test_emptyline(self): - # Test file includes an empty line in the header data - emptyline_file = "Tests/images/zero_bb_emptyline.eps" - with Image.open(emptyline_file) as image: - image.load() - assert image.mode == "RGB" - assert image.size == (460, 352) - assert image.format == "EPS" +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_emptyline(): + # Test file includes an empty line in the header data + emptyline_file = "Tests/images/zero_bb_emptyline.eps" + + with Image.open(emptyline_file) as image: + image.load() + assert image.mode == "RGB" + assert image.size == (460, 352) + assert image.format == "EPS" diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 096aa872c..455e30f71 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,11 +1,9 @@ -import unittest from io import BytesIO import pytest from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, features from .helper import ( - PillowTestCase, assert_image_equal, assert_image_similar, hopper, @@ -20,772 +18,811 @@ with open(TEST_GIF, "rb") as f: data = f.read() -class TestFileGif(PillowTestCase): - def test_sanity(self): +def test_sanity(): + with Image.open(TEST_GIF) as im: + im.load() + assert im.mode == "P" + assert im.size == (128, 128) + assert im.format == "GIF" + assert im.info["version"] == b"GIF89a" + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + im = Image.open(TEST_GIF) + im.load() + + pytest.warns(ResourceWarning, open) + + +def test_closed_file(): + def open(): + im = Image.open(TEST_GIF) + im.load() + im.close() + + pytest.warns(None, open) + + +def test_context_manager(): + def open(): with Image.open(TEST_GIF) as im: im.load() - assert im.mode == "P" - assert im.size == (128, 128) - assert im.format == "GIF" - assert im.info["version"] == b"GIF89a" - @unittest.skipIf(is_pypy(), "Requires CPython") - def test_unclosed_file(self): - def open(): - im = Image.open(TEST_GIF) - im.load() + pytest.warns(None, open) - pytest.warns(ResourceWarning, open) - def test_closed_file(self): - def open(): - im = Image.open(TEST_GIF) - im.load() - im.close() +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - pytest.warns(None, open) + with pytest.raises(SyntaxError): + GifImagePlugin.GifImageFile(invalid_file) - def test_context_manager(self): - def open(): - with Image.open(TEST_GIF) as im: - im.load() - pytest.warns(None, open) +def test_optimize(): + def test_grayscale(optimize): + im = Image.new("L", (1, 1), 0) + filename = BytesIO() + im.save(filename, "GIF", optimize=optimize) + return len(filename.getvalue()) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - GifImagePlugin.GifImageFile(invalid_file) - - def test_optimize(self): - def test_grayscale(optimize): - im = Image.new("L", (1, 1), 0) - filename = BytesIO() - im.save(filename, "GIF", optimize=optimize) - return len(filename.getvalue()) - - def test_bilevel(optimize): - im = Image.new("1", (1, 1), 0) - test_file = BytesIO() - im.save(test_file, "GIF", optimize=optimize) - return len(test_file.getvalue()) - - assert test_grayscale(0) == 800 - assert test_grayscale(1) == 44 - assert test_bilevel(0) == 800 - assert test_bilevel(1) == 800 - - def test_optimize_correctness(self): - # 256 color Palette image, posterize to > 128 and < 128 levels - # Size bigger and smaller than 512x512 - # Check the palette for number of colors allocated. - # Check for correctness after conversion back to RGB - def check(colors, size, expected_palette_length): - # make an image with empty colors in the start of the palette range - im = Image.frombytes( - "P", (colors, colors), bytes(range(256 - colors, 256)) * colors - ) - im = im.resize((size, size)) - outfile = BytesIO() - im.save(outfile, "GIF") - outfile.seek(0) - with Image.open(outfile) as reloaded: - # check palette length - palette_length = max( - i + 1 for i, v in enumerate(reloaded.histogram()) if v - ) - assert expected_palette_length == palette_length - - assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) - - # These do optimize the palette - check(128, 511, 128) - check(64, 511, 64) - check(4, 511, 4) - - # These don't optimize the palette - check(128, 513, 256) - check(64, 513, 256) - check(4, 513, 256) - - # other limits that don't optimize the palette - check(129, 511, 256) - check(255, 511, 256) - check(256, 511, 256) - - def test_optimize_full_l(self): - im = Image.frombytes("L", (16, 16), bytes(range(256))) + def test_bilevel(optimize): + im = Image.new("1", (1, 1), 0) test_file = BytesIO() - im.save(test_file, "GIF", optimize=True) - assert im.mode == "L" + im.save(test_file, "GIF", optimize=optimize) + return len(test_file.getvalue()) - def test_roundtrip(self): - out = self.tempfile("temp.gif") - im = hopper() - im.save(out) - with Image.open(out) as reread: + assert test_grayscale(0) == 800 + assert test_grayscale(1) == 44 + assert test_bilevel(0) == 800 + assert test_bilevel(1) == 800 - assert_image_similar(reread.convert("RGB"), im, 50) - def test_roundtrip2(self): - # see https://github.com/python-pillow/Pillow/issues/403 - out = self.tempfile("temp.gif") - with Image.open(TEST_GIF) as im: - im2 = im.copy() - im2.save(out) - with Image.open(out) as reread: +def test_optimize_correctness(): + # 256 color Palette image, posterize to > 128 and < 128 levels + # Size bigger and smaller than 512x512 + # Check the palette for number of colors allocated. + # Check for correctness after conversion back to RGB + def check(colors, size, expected_palette_length): + # make an image with empty colors in the start of the palette range + im = Image.frombytes( + "P", (colors, colors), bytes(range(256 - colors, 256)) * colors + ) + im = im.resize((size, size)) + outfile = BytesIO() + im.save(outfile, "GIF") + outfile.seek(0) + with Image.open(outfile) as reloaded: + # check palette length + palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v) + assert expected_palette_length == palette_length - assert_image_similar(reread.convert("RGB"), hopper(), 50) + assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) - def test_roundtrip_save_all(self): - # Single frame image - out = self.tempfile("temp.gif") - im = hopper() + # These do optimize the palette + check(128, 511, 128) + check(64, 511, 64) + check(4, 511, 4) + + # These don't optimize the palette + check(128, 513, 256) + check(64, 513, 256) + check(4, 513, 256) + + # Other limits that don't optimize the palette + check(129, 511, 256) + check(255, 511, 256) + check(256, 511, 256) + + +def test_optimize_full_l(): + im = Image.frombytes("L", (16, 16), bytes(range(256))) + test_file = BytesIO() + im.save(test_file, "GIF", optimize=True) + assert im.mode == "L" + + +def test_roundtrip(tmp_path): + out = str(tmp_path / "temp.gif") + im = hopper() + im.save(out) + with Image.open(out) as reread: + + assert_image_similar(reread.convert("RGB"), im, 50) + + +def test_roundtrip2(tmp_path): + # see https://github.com/python-pillow/Pillow/issues/403 + out = str(tmp_path / "temp.gif") + with Image.open(TEST_GIF) as im: + im2 = im.copy() + im2.save(out) + with Image.open(out) as reread: + + assert_image_similar(reread.convert("RGB"), hopper(), 50) + + +def test_roundtrip_save_all(tmp_path): + # Single frame image + out = str(tmp_path / "temp.gif") + im = hopper() + im.save(out, save_all=True) + with Image.open(out) as reread: + + assert_image_similar(reread.convert("RGB"), im, 50) + + # Multiframe image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + out = str(tmp_path / "temp.gif") im.save(out, save_all=True) - with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), im, 50) + with Image.open(out) as reread: + assert reread.n_frames == 5 - # Multiframe image - with Image.open("Tests/images/dispose_bgnd.gif") as im: - out = self.tempfile("temp.gif") - im.save(out, save_all=True) - with Image.open(out) as reread: +def test_headers_saving_for_animated_gifs(tmp_path): + important_headers = ["background", "version", "duration", "loop"] + # Multiframe image + with Image.open("Tests/images/dispose_bgnd.gif") as im: - assert reread.n_frames == 5 + info = im.info.copy() - def test_headers_saving_for_animated_gifs(self): - important_headers = ["background", "version", "duration", "loop"] - # Multiframe image - with Image.open("Tests/images/dispose_bgnd.gif") as im: + out = str(tmp_path / "temp.gif") + im.save(out, save_all=True) + with Image.open(out) as reread: - info = im.info.copy() + for header in important_headers: + assert info[header] == reread.info[header] - out = self.tempfile("temp.gif") - im.save(out, save_all=True) - with Image.open(out) as reread: - for header in important_headers: - assert info[header] == reread.info[header] +def test_palette_handling(tmp_path): + # see https://github.com/python-pillow/Pillow/issues/513 - def test_palette_handling(self): - # see https://github.com/python-pillow/Pillow/issues/513 + with Image.open(TEST_GIF) as im: + im = im.convert("RGB") - with Image.open(TEST_GIF) as im: - im = im.convert("RGB") + im = im.resize((100, 100), Image.LANCZOS) + im2 = im.convert("P", palette=Image.ADAPTIVE, colors=256) - im = im.resize((100, 100), Image.LANCZOS) - im2 = im.convert("P", palette=Image.ADAPTIVE, colors=256) + f = str(tmp_path / "temp.gif") + im2.save(f, optimize=True) - f = self.tempfile("temp.gif") - im2.save(f, optimize=True) + with Image.open(f) as reloaded: - with Image.open(f) as reloaded: + assert_image_similar(im, reloaded.convert("RGB"), 10) - assert_image_similar(im, reloaded.convert("RGB"), 10) - def test_palette_434(self): - # see https://github.com/python-pillow/Pillow/issues/434 +def test_palette_434(tmp_path): + # see https://github.com/python-pillow/Pillow/issues/434 - def roundtrip(im, *args, **kwargs): - out = self.tempfile("temp.gif") - im.copy().save(out, *args, **kwargs) - reloaded = Image.open(out) + def roundtrip(im, *args, **kwargs): + out = str(tmp_path / "temp.gif") + im.copy().save(out, *args, **kwargs) + reloaded = Image.open(out) - return reloaded + return reloaded - orig = "Tests/images/test.colors.gif" - with Image.open(orig) as im: + orig = "Tests/images/test.colors.gif" + with Image.open(orig) as im: - with roundtrip(im) as reloaded: - assert_image_similar(im, reloaded, 1) - with roundtrip(im, optimize=True) as reloaded: - assert_image_similar(im, reloaded, 1) + with roundtrip(im) as reloaded: + assert_image_similar(im, reloaded, 1) + with roundtrip(im, optimize=True) as reloaded: + assert_image_similar(im, reloaded, 1) - im = im.convert("RGB") - # check automatic P conversion - with roundtrip(im) as reloaded: - reloaded = reloaded.convert("RGB") - assert_image_equal(im, reloaded) + im = im.convert("RGB") + # check automatic P conversion + with roundtrip(im) as reloaded: + reloaded = reloaded.convert("RGB") + assert_image_equal(im, reloaded) - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_bmp_mode(self): - with Image.open(TEST_GIF) as img: - img = img.convert("RGB") - tempfile = self.tempfile("temp.gif") - GifImagePlugin._save_netpbm(img, 0, tempfile) - with Image.open(tempfile) as reloaded: - assert_image_similar(img, reloaded.convert("RGB"), 0) +@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") +def test_save_netpbm_bmp_mode(tmp_path): + with Image.open(TEST_GIF) as img: + img = img.convert("RGB") - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_l_mode(self): - with Image.open(TEST_GIF) as img: - img = img.convert("L") + tempfile = str(tmp_path / "temp.gif") + GifImagePlugin._save_netpbm(img, 0, tempfile) + with Image.open(tempfile) as reloaded: + assert_image_similar(img, reloaded.convert("RGB"), 0) - tempfile = self.tempfile("temp.gif") - GifImagePlugin._save_netpbm(img, 0, tempfile) - with Image.open(tempfile) as reloaded: - assert_image_similar(img, reloaded.convert("L"), 0) - def test_seek(self): - with Image.open("Tests/images/dispose_none.gif") as img: - framecount = 0 - try: - while True: - framecount += 1 - img.seek(img.tell() + 1) - except EOFError: - assert framecount == 5 +@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") +def test_save_netpbm_l_mode(tmp_path): + with Image.open(TEST_GIF) as img: + img = img.convert("L") - def test_seek_info(self): - with Image.open("Tests/images/iss634.gif") as im: - info = im.info.copy() + tempfile = str(tmp_path / "temp.gif") + GifImagePlugin._save_netpbm(img, 0, tempfile) + with Image.open(tempfile) as reloaded: + assert_image_similar(img, reloaded.convert("L"), 0) - im.seek(1) - im.seek(0) - assert im.info == info - - def test_seek_rewind(self): - with Image.open("Tests/images/iss634.gif") as im: - im.seek(2) - im.seek(1) - - with Image.open("Tests/images/iss634.gif") as expected: - expected.seek(1) - assert_image_equal(im, expected) - - def test_n_frames(self): - for path, n_frames in [[TEST_GIF, 1], ["Tests/images/iss634.gif", 42]]: - # Test is_animated before n_frames - with Image.open(path) as im: - assert im.is_animated == (n_frames != 1) - - # Test is_animated after n_frames - with Image.open(path) as im: - assert im.n_frames == n_frames - assert im.is_animated == (n_frames != 1) - - def test_eoferror(self): - with Image.open(TEST_GIF) as im: - n_frames = im.n_frames - - # Test seeking past the last frame - with pytest.raises(EOFError): - im.seek(n_frames) - assert im.tell() < n_frames - - # Test that seeking to the last frame does not raise an error - im.seek(n_frames - 1) - - def test_dispose_none(self): - with Image.open("Tests/images/dispose_none.gif") as img: - try: - while True: - img.seek(img.tell() + 1) - assert img.disposal_method == 1 - except EOFError: - pass - - def test_dispose_background(self): - with Image.open("Tests/images/dispose_bgnd.gif") as img: - try: - while True: - img.seek(img.tell() + 1) - assert img.disposal_method == 2 - except EOFError: - pass - - def test_dispose_previous(self): - with Image.open("Tests/images/dispose_prev.gif") as img: - try: - while True: - img.seek(img.tell() + 1) - assert img.disposal_method == 3 - except EOFError: - pass - - def test_save_dispose(self): - out = self.tempfile("temp.gif") - im_list = [ - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#111"), - Image.new("L", (100, 100), "#222"), - ] - for method in range(0, 4): - im_list[0].save( - out, save_all=True, append_images=im_list[1:], disposal=method - ) - with Image.open(out) as img: - for _ in range(2): - img.seek(img.tell() + 1) - assert img.disposal_method == method - - # check per frame disposal - im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - disposal=tuple(range(len(im_list))), - ) - - with Image.open(out) as img: - - for i in range(2): +def test_seek(): + with Image.open("Tests/images/dispose_none.gif") as img: + frame_count = 0 + try: + while True: + frame_count += 1 img.seek(img.tell() + 1) - assert img.disposal_method == i + 1 + except EOFError: + assert frame_count == 5 - def test_dispose2_palette(self): - out = self.tempfile("temp.gif") - # 4 backgrounds: White, Grey, Black, Red - circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] +def test_seek_info(): + with Image.open("Tests/images/iss634.gif") as im: + info = im.info.copy() - im_list = [] - for circle in circles: - img = Image.new("RGB", (100, 100), (255, 0, 0)) + im.seek(1) + im.seek(0) - # Red circle in center of each frame - d = ImageDraw.Draw(img) - d.ellipse([(40, 40), (60, 60)], fill=circle) + assert im.info == info - im_list.append(img) - im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) +def test_seek_rewind(): + with Image.open("Tests/images/iss634.gif") as im: + im.seek(2) + im.seek(1) + with Image.open("Tests/images/iss634.gif") as expected: + expected.seek(1) + assert_image_equal(im, expected) + + +def test_n_frames(): + for path, n_frames in [[TEST_GIF, 1], ["Tests/images/iss634.gif", 42]]: + # Test is_animated before n_frames + with Image.open(path) as im: + assert im.is_animated == (n_frames != 1) + + # Test is_animated after n_frames + with Image.open(path) as im: + assert im.n_frames == n_frames + assert im.is_animated == (n_frames != 1) + + +def test_eoferror(): + with Image.open(TEST_GIF) as im: + n_frames = im.n_frames + + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames + + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) + + +def test_dispose_none(): + with Image.open("Tests/images/dispose_none.gif") as img: + try: + while True: + img.seek(img.tell() + 1) + assert img.disposal_method == 1 + except EOFError: + pass + + +def test_dispose_background(): + with Image.open("Tests/images/dispose_bgnd.gif") as img: + try: + while True: + img.seek(img.tell() + 1) + assert img.disposal_method == 2 + except EOFError: + pass + + +def test_dispose_previous(): + with Image.open("Tests/images/dispose_prev.gif") as img: + try: + while True: + img.seek(img.tell() + 1) + assert img.disposal_method == 3 + except EOFError: + pass + + +def test_save_dispose(tmp_path): + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), + Image.new("L", (100, 100), "#222"), + ] + for method in range(0, 4): + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) with Image.open(out) as img: - for i, circle in enumerate(circles): - img.seek(i) - rgb_img = img.convert("RGB") + for _ in range(2): + img.seek(img.tell() + 1) + assert img.disposal_method == method - # Check top left pixel matches background - assert rgb_img.getpixel((0, 0)) == (255, 0, 0) + # Check per frame disposal + im_list[0].save( + out, + save_all=True, + append_images=im_list[1:], + disposal=tuple(range(len(im_list))), + ) - # Center remains red every frame - assert rgb_img.getpixel((50, 50)) == circle + with Image.open(out) as img: - def test_dispose2_diff(self): - out = self.tempfile("temp.gif") - - # 4 frames: red/blue, red/red, blue/blue, red/blue - circles = [ - ((255, 0, 0, 255), (0, 0, 255, 255)), - ((255, 0, 0, 255), (255, 0, 0, 255)), - ((0, 0, 255, 255), (0, 0, 255, 255)), - ((255, 0, 0, 255), (0, 0, 255, 255)), - ] - - im_list = [] - for i in range(len(circles)): - # Transparent BG - img = Image.new("RGBA", (100, 100), (255, 255, 255, 0)) - - # Two circles per frame - d = ImageDraw.Draw(img) - d.ellipse([(0, 30), (40, 70)], fill=circles[i][0]) - d.ellipse([(60, 30), (100, 70)], fill=circles[i][1]) - - im_list.append(img) - - im_list[0].save( - out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0 - ) - - with Image.open(out) as img: - for i, colours in enumerate(circles): - img.seek(i) - rgb_img = img.convert("RGBA") - - # Check left circle is correct colour - assert rgb_img.getpixel((20, 50)) == colours[0] - - # Check right circle is correct colour - assert rgb_img.getpixel((80, 50)) == colours[1] - - # Check BG is correct colour - assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0) - - def test_dispose2_background(self): - out = self.tempfile("temp.gif") - - im_list = [] - - im = Image.new("P", (100, 100)) - d = ImageDraw.Draw(im) - d.rectangle([(50, 0), (100, 100)], fill="#f00") - d.rectangle([(0, 0), (50, 100)], fill="#0f0") - im_list.append(im) - - im = Image.new("P", (100, 100)) - d = ImageDraw.Draw(im) - d.rectangle([(0, 0), (100, 50)], fill="#f00") - d.rectangle([(0, 50), (100, 100)], fill="#0f0") - im_list.append(im) - - im_list[0].save( - out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1 - ) - - with Image.open(out) as im: - im.seek(1) - assert im.getpixel((0, 0)) == 0 - - def test_iss634(self): - with Image.open("Tests/images/iss634.gif") as img: - # seek to the second frame + for i in range(2): img.seek(img.tell() + 1) - # all transparent pixels should be replaced with the color from the - # first frame - assert img.histogram()[img.info["transparency"]] == 0 + assert img.disposal_method == i + 1 - def test_duration(self): - duration = 1000 - out = self.tempfile("temp.gif") - im = Image.new("L", (100, 100), "#000") +def test_dispose2_palette(tmp_path): + out = str(tmp_path / "temp.gif") - # Check that the argument has priority over the info settings - im.info["duration"] = 100 - im.save(out, duration=duration) + # 4 backgrounds: White, Grey, Black, Red + circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] - with Image.open(out) as reread: + im_list = [] + for circle in circles: + img = Image.new("RGB", (100, 100), (255, 0, 0)) + + # Red circle in center of each frame + d = ImageDraw.Draw(img) + d.ellipse([(40, 40), (60, 60)], fill=circle) + + im_list.append(img) + + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) + + with Image.open(out) as img: + for i, circle in enumerate(circles): + img.seek(i) + rgb_img = img.convert("RGB") + + # Check top left pixel matches background + assert rgb_img.getpixel((0, 0)) == (255, 0, 0) + + # Center remains red every frame + assert rgb_img.getpixel((50, 50)) == circle + + +def test_dispose2_diff(tmp_path): + out = str(tmp_path / "temp.gif") + + # 4 frames: red/blue, red/red, blue/blue, red/blue + circles = [ + ((255, 0, 0, 255), (0, 0, 255, 255)), + ((255, 0, 0, 255), (255, 0, 0, 255)), + ((0, 0, 255, 255), (0, 0, 255, 255)), + ((255, 0, 0, 255), (0, 0, 255, 255)), + ] + + im_list = [] + for i in range(len(circles)): + # Transparent BG + img = Image.new("RGBA", (100, 100), (255, 255, 255, 0)) + + # Two circles per frame + d = ImageDraw.Draw(img) + d.ellipse([(0, 30), (40, 70)], fill=circles[i][0]) + d.ellipse([(60, 30), (100, 70)], fill=circles[i][1]) + + im_list.append(img) + + im_list[0].save( + out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0 + ) + + with Image.open(out) as img: + for i, colours in enumerate(circles): + img.seek(i) + rgb_img = img.convert("RGBA") + + # Check left circle is correct colour + assert rgb_img.getpixel((20, 50)) == colours[0] + + # Check right circle is correct colour + assert rgb_img.getpixel((80, 50)) == colours[1] + + # Check BG is correct colour + assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0) + + +def test_dispose2_background(tmp_path): + out = str(tmp_path / "temp.gif") + + im_list = [] + + im = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im) + d.rectangle([(50, 0), (100, 100)], fill="#f00") + d.rectangle([(0, 0), (50, 100)], fill="#0f0") + im_list.append(im) + + im = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im) + d.rectangle([(0, 0), (100, 50)], fill="#f00") + d.rectangle([(0, 50), (100, 100)], fill="#0f0") + im_list.append(im) + + im_list[0].save( + out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1 + ) + + with Image.open(out) as im: + im.seek(1) + assert im.getpixel((0, 0)) == 0 + + +def test_iss634(): + with Image.open("Tests/images/iss634.gif") as img: + # Seek to the second frame + img.seek(img.tell() + 1) + # All transparent pixels should be replaced with the color from the first frame + assert img.histogram()[img.info["transparency"]] == 0 + + +def test_duration(tmp_path): + duration = 1000 + + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + + # Check that the argument has priority over the info settings + im.info["duration"] = 100 + im.save(out, duration=duration) + + with Image.open(out) as reread: + assert reread.info["duration"] == duration + + +def test_multiple_duration(tmp_path): + duration_list = [1000, 2000, 3000] + + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), + Image.new("L", (100, 100), "#222"), + ] + + # Duration as list + im_list[0].save( + out, save_all=True, append_images=im_list[1:], duration=duration_list + ) + with Image.open(out) as reread: + + for duration in duration_list: assert reread.info["duration"] == duration + try: + reread.seek(reread.tell() + 1) + except EOFError: + pass - def test_multiple_duration(self): - duration_list = [1000, 2000, 3000] + # Duration as tuple + im_list[0].save( + out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list) + ) + with Image.open(out) as reread: - out = self.tempfile("temp.gif") - im_list = [ - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#111"), - Image.new("L", (100, 100), "#222"), - ] + for duration in duration_list: + assert reread.info["duration"] == duration + try: + reread.seek(reread.tell() + 1) + except EOFError: + pass - # duration as list - im_list[0].save( - out, save_all=True, append_images=im_list[1:], duration=duration_list - ) - with Image.open(out) as reread: - for duration in duration_list: - assert reread.info["duration"] == duration - try: - reread.seek(reread.tell() + 1) - except EOFError: - pass +def test_identical_frames(tmp_path): + duration_list = [1000, 1500, 2000, 4000] - # duration as tuple - im_list[0].save( - out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list) - ) - with Image.open(out) as reread: + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), + ] - for duration in duration_list: - assert reread.info["duration"] == duration - try: - reread.seek(reread.tell() + 1) - except EOFError: - pass + # Duration as list + im_list[0].save( + out, save_all=True, append_images=im_list[1:], duration=duration_list + ) + with Image.open(out) as reread: - def test_identical_frames(self): - duration_list = [1000, 1500, 2000, 4000] + # Assert that the first three frames were combined + assert reread.n_frames == 2 - out = self.tempfile("temp.gif") + # Assert that the new duration is the total of the identical frames + assert reread.info["duration"] == 4500 + + +def test_identical_frames_to_single_frame(tmp_path): + for duration in ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500): + out = str(tmp_path / "temp.gif") im_list = [ Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#111"), ] - # duration as list im_list[0].save( - out, save_all=True, append_images=im_list[1:], duration=duration_list + out, save_all=True, append_images=im_list[1:], duration=duration ) with Image.open(out) as reread: - - # Assert that the first three frames were combined - assert reread.n_frames == 2 + # Assert that all frames were combined + assert reread.n_frames == 1 # Assert that the new duration is the total of the identical frames - assert reread.info["duration"] == 4500 + assert reread.info["duration"] == 8500 - def test_identical_frames_to_single_frame(self): - for duration in ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500): - out = self.tempfile("temp.gif") - im_list = [ - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#000"), - ] - im_list[0].save( - out, save_all=True, append_images=im_list[1:], duration=duration - ) - with Image.open(out) as reread: - # Assert that all frames were combined - assert reread.n_frames == 1 +def test_number_of_loops(tmp_path): + number_of_loops = 2 - # Assert that the new duration is the total of the identical frames - assert reread.info["duration"] == 8500 + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.save(out, loop=number_of_loops) + with Image.open(out) as reread: - def test_number_of_loops(self): - number_of_loops = 2 + assert reread.info["loop"] == number_of_loops - out = self.tempfile("temp.gif") - im = Image.new("L", (100, 100), "#000") - im.save(out, loop=number_of_loops) - with Image.open(out) as reread: - assert reread.info["loop"] == number_of_loops +def test_background(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.info["background"] = 1 + im.save(out) + with Image.open(out) as reread: - def test_background(self): - out = self.tempfile("temp.gif") - im = Image.new("L", (100, 100), "#000") - im.info["background"] = 1 - im.save(out) - with Image.open(out) as reread: + assert reread.info["background"] == im.info["background"] - assert reread.info["background"] == im.info["background"] - - if features.check("webp") and features.check("webp_anim"): - with Image.open("Tests/images/hopper.webp") as im: - assert isinstance(im.info["background"], tuple) - im.save(out) - - def test_comment(self): - with Image.open(TEST_GIF) as im: - assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" - - out = self.tempfile("temp.gif") - im = Image.new("L", (100, 100), "#000") - im.info["comment"] = b"Test comment text" - im.save(out) - with Image.open(out) as reread: - assert reread.info["comment"] == im.info["comment"] - - im.info["comment"] = "Test comment text" - im.save(out) - with Image.open(out) as reread: - assert reread.info["comment"] == im.info["comment"].encode() - - def test_comment_over_255(self): - out = self.tempfile("temp.gif") - im = Image.new("L", (100, 100), "#000") - comment = b"Test comment text" - while len(comment) < 256: - comment += comment - im.info["comment"] = comment - im.save(out) - with Image.open(out) as reread: - - assert reread.info["comment"] == comment - - def test_zero_comment_subblocks(self): - with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: - with Image.open(TEST_GIF) as expected: - assert_image_equal(im, expected) - - def test_version(self): - out = self.tempfile("temp.gif") - - def assertVersionAfterSave(im, version): + if features.check("webp") and features.check("webp_anim"): + with Image.open("Tests/images/hopper.webp") as im: + assert isinstance(im.info["background"], tuple) im.save(out) - with Image.open(out) as reread: - assert reread.info["version"] == version - # Test that GIF87a is used by default - im = Image.new("L", (100, 100), "#000") + +def test_comment(tmp_path): + with Image.open(TEST_GIF) as im: + assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" + + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.info["comment"] = b"Test comment text" + im.save(out) + with Image.open(out) as reread: + assert reread.info["comment"] == im.info["comment"] + + im.info["comment"] = "Test comment text" + im.save(out) + with Image.open(out) as reread: + assert reread.info["comment"] == im.info["comment"].encode() + + +def test_comment_over_255(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + comment = b"Test comment text" + while len(comment) < 256: + comment += comment + im.info["comment"] = comment + im.save(out) + with Image.open(out) as reread: + + assert reread.info["comment"] == comment + + +def test_zero_comment_subblocks(): + with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: + with Image.open(TEST_GIF) as expected: + assert_image_equal(im, expected) + + +def test_version(tmp_path): + out = str(tmp_path / "temp.gif") + + def assertVersionAfterSave(im, version): + im.save(out) + with Image.open(out) as reread: + assert reread.info["version"] == version + + # Test that GIF87a is used by default + im = Image.new("L", (100, 100), "#000") + assertVersionAfterSave(im, b"GIF87a") + + # Test setting the version to 89a + im = Image.new("L", (100, 100), "#000") + im.info["version"] = b"89a" + assertVersionAfterSave(im, b"GIF89a") + + # Test that adding a GIF89a feature changes the version + im.info["transparency"] = 1 + assertVersionAfterSave(im, b"GIF89a") + + # Test that a GIF87a image is also saved in that format + with Image.open("Tests/images/test.colors.gif") as im: assertVersionAfterSave(im, b"GIF87a") - # Test setting the version to 89a - im = Image.new("L", (100, 100), "#000") - im.info["version"] = b"89a" - assertVersionAfterSave(im, b"GIF89a") + # Test that a GIF89a image is also saved in that format + im.info["version"] = b"GIF89a" + assertVersionAfterSave(im, b"GIF87a") - # Test that adding a GIF89a feature changes the version - im.info["transparency"] = 1 - assertVersionAfterSave(im, b"GIF89a") - # Test that a GIF87a image is also saved in that format - with Image.open("Tests/images/test.colors.gif") as im: - assertVersionAfterSave(im, b"GIF87a") +def test_append_images(tmp_path): + out = str(tmp_path / "temp.gif") - # Test that a GIF89a image is also saved in that format - im.info["version"] = b"GIF89a" - assertVersionAfterSave(im, b"GIF87a") + # Test appending single frame images + im = Image.new("RGB", (100, 100), "#f00") + ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] + im.copy().save(out, save_all=True, append_images=ims) - def test_append_images(self): - out = self.tempfile("temp.gif") + with Image.open(out) as reread: + assert reread.n_frames == 3 - # Test appending single frame images - im = Image.new("RGB", (100, 100), "#f00") - ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] - im.copy().save(out, save_all=True, append_images=ims) + # Tests appending using a generator + def imGenerator(ims): + yield from ims - with Image.open(out) as reread: - assert reread.n_frames == 3 + im.save(out, save_all=True, append_images=imGenerator(ims)) - # Tests appending using a generator - def imGenerator(ims): - yield from ims + with Image.open(out) as reread: + assert reread.n_frames == 3 - im.save(out, save_all=True, append_images=imGenerator(ims)) + # Tests appending single and multiple frame images + with Image.open("Tests/images/dispose_none.gif") as im: + with Image.open("Tests/images/dispose_prev.gif") as im2: + im.save(out, save_all=True, append_images=[im2]) - with Image.open(out) as reread: - assert reread.n_frames == 3 + with Image.open(out) as reread: + assert reread.n_frames == 10 - # Tests appending single and multiple frame images - with Image.open("Tests/images/dispose_none.gif") as im: - with Image.open("Tests/images/dispose_prev.gif") as im2: - im.save(out, save_all=True, append_images=[im2]) - with Image.open(out) as reread: - assert reread.n_frames == 10 +def test_transparent_optimize(tmp_path): + # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses + # transparency. + # Need a palette that isn't using the 0 color, and one that's > 128 items where the + # transparent color is actually the top palette entry to trigger the bug. - def test_transparent_optimize(self): - # from issue #2195, if the transparent color is incorrectly - # optimized out, gif loses transparency - # Need a palette that isn't using the 0 color, and one - # that's > 128 items where the transparent color is actually - # the top palette entry to trigger the bug. + data = bytes(range(1, 254)) + palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - data = bytes(range(1, 254)) - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) + im = Image.new("L", (253, 1)) + im.frombytes(data) + im.putpalette(palette) - im = Image.new("L", (253, 1)) - im.frombytes(data) + out = str(tmp_path / "temp.gif") + im.save(out, transparency=253) + with Image.open(out) as reloaded: + + assert reloaded.info["transparency"] == 253 + + +def test_rgb_transparency(tmp_path): + out = str(tmp_path / "temp.gif") + + # Single frame + im = Image.new("RGB", (1, 1)) + im.info["transparency"] = (255, 0, 0) + pytest.warns(UserWarning, im.save, out) + + with Image.open(out) as reloaded: + assert "transparency" not in reloaded.info + + # Multiple frames + im = Image.new("RGB", (1, 1)) + im.info["transparency"] = b"" + ims = [Image.new("RGB", (1, 1))] + pytest.warns(UserWarning, im.save, out, save_all=True, append_images=ims) + + with Image.open(out) as reloaded: + assert "transparency" not in reloaded.info + + +def test_bbox(tmp_path): + out = str(tmp_path / "temp.gif") + + im = Image.new("RGB", (100, 100), "#fff") + ims = [Image.new("RGB", (100, 100), "#000")] + im.save(out, save_all=True, append_images=ims) + + with Image.open(out) as reread: + assert reread.n_frames == 2 + + +def test_palette_save_L(tmp_path): + # Generate an L mode image with a separate palette + + im = hopper("P") + im_l = Image.frombytes("L", im.size, im.tobytes()) + palette = bytes(im.getpalette()) + + out = str(tmp_path / "temp.gif") + im_l.save(out, palette=palette) + + with Image.open(out) as reloaded: + assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) + + +def test_palette_save_P(tmp_path): + # Pass in a different palette, then construct what the image would look like. + # Forcing a non-straight grayscale palette. + + im = hopper("P") + palette = bytes([255 - i // 3 for i in range(768)]) + + out = str(tmp_path / "temp.gif") + im.save(out, palette=palette) + + with Image.open(out) as reloaded: im.putpalette(palette) + assert_image_equal(reloaded, im) - out = self.tempfile("temp.gif") - im.save(out, transparency=253) - with Image.open(out) as reloaded: - assert reloaded.info["transparency"] == 253 +def test_palette_save_ImagePalette(tmp_path): + # Pass in a different palette, as an ImagePalette.ImagePalette + # effectively the same as test_palette_save_P - def test_rgb_transparency(self): - out = self.tempfile("temp.gif") + im = hopper("P") + palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) - # Single frame - im = Image.new("RGB", (1, 1)) - im.info["transparency"] = (255, 0, 0) - pytest.warns(UserWarning, im.save, out) + out = str(tmp_path / "temp.gif") + im.save(out, palette=palette) - with Image.open(out) as reloaded: - assert "transparency" not in reloaded.info + with Image.open(out) as reloaded: + im.putpalette(palette) + assert_image_equal(reloaded, im) - # Multiple frames - im = Image.new("RGB", (1, 1)) - im.info["transparency"] = b"" - ims = [Image.new("RGB", (1, 1))] - pytest.warns(UserWarning, im.save, out, save_all=True, append_images=ims) - with Image.open(out) as reloaded: - assert "transparency" not in reloaded.info +def test_save_I(tmp_path): + # Test saving something that would trigger the auto-convert to 'L' - def test_bbox(self): - out = self.tempfile("temp.gif") + im = hopper("I") - im = Image.new("RGB", (100, 100), "#fff") - ims = [Image.new("RGB", (100, 100), "#000")] - im.save(out, save_all=True, append_images=ims) + out = str(tmp_path / "temp.gif") + im.save(out) - with Image.open(out) as reread: - assert reread.n_frames == 2 + with Image.open(out) as reloaded: + assert_image_equal(reloaded.convert("L"), im.convert("L")) - def test_palette_save_L(self): - # generate an L mode image with a separate palette - im = hopper("P") - im_l = Image.frombytes("L", im.size, im.tobytes()) - palette = bytes(im.getpalette()) +def test_getdata(): + # Test getheader/getdata against legacy values. + # Create a 'P' image with holes in the palette. + im = Image._wedge().resize((16, 16), Image.NEAREST) + im.putpalette(ImagePalette.ImagePalette("RGB")) + im.info = {"background": 0} - out = self.tempfile("temp.gif") - im_l.save(out, palette=palette) + passed_palette = bytes([255 - i // 3 for i in range(768)]) - with Image.open(out) as reloaded: + GifImagePlugin._FORCE_OPTIMIZE = True + try: + h = GifImagePlugin.getheader(im, passed_palette) + d = GifImagePlugin.getdata(im) - assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) + import pickle - def test_palette_save_P(self): - # pass in a different palette, then construct what the image - # would look like. - # Forcing a non-straight grayscale palette. + # Enable to get target values on pre-refactor version + # with open('Tests/images/gif_header_data.pkl', 'wb') as f: + # pickle.dump((h, d), f, 1) + with open("Tests/images/gif_header_data.pkl", "rb") as f: + (h_target, d_target) = pickle.load(f) - im = hopper("P") - palette = bytes([255 - i // 3 for i in range(768)]) + assert h == h_target + assert d == d_target + finally: + GifImagePlugin._FORCE_OPTIMIZE = False - out = self.tempfile("temp.gif") - im.save(out, palette=palette) - with Image.open(out) as reloaded: - im.putpalette(palette) - assert_image_equal(reloaded, im) +def test_lzw_bits(): + # see https://github.com/python-pillow/Pillow/issues/2811 + with Image.open("Tests/images/issue_2811.gif") as im: + assert im.tile[0][3][0] == 11 # LZW bits + # codec error prepatch + im.load() - def test_palette_save_ImagePalette(self): - # pass in a different palette, as an ImagePalette.ImagePalette - # effectively the same as test_palette_save_P - im = hopper("P") - palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) - - out = self.tempfile("temp.gif") - im.save(out, palette=palette) - - with Image.open(out) as reloaded: - im.putpalette(palette) - assert_image_equal(reloaded, im) - - def test_save_I(self): - # Test saving something that would trigger the auto-convert to 'L' - - im = hopper("I") - - out = self.tempfile("temp.gif") - im.save(out) - - with Image.open(out) as reloaded: - assert_image_equal(reloaded.convert("L"), im.convert("L")) - - def test_getdata(self): - # test getheader/getdata against legacy values - # Create a 'P' image with holes in the palette - im = Image._wedge().resize((16, 16), Image.NEAREST) - im.putpalette(ImagePalette.ImagePalette("RGB")) - im.info = {"background": 0} - - passed_palette = bytes([255 - i // 3 for i in range(768)]) - - GifImagePlugin._FORCE_OPTIMIZE = True - try: - h = GifImagePlugin.getheader(im, passed_palette) - d = GifImagePlugin.getdata(im) - - import pickle - - # Enable to get target values on pre-refactor version - # with open('Tests/images/gif_header_data.pkl', 'wb') as f: - # pickle.dump((h, d), f, 1) - with open("Tests/images/gif_header_data.pkl", "rb") as f: - (h_target, d_target) = pickle.load(f) - - assert h == h_target - assert d == d_target - finally: - GifImagePlugin._FORCE_OPTIMIZE = False - - def test_lzw_bits(self): - # see https://github.com/python-pillow/Pillow/issues/2811 - with Image.open("Tests/images/issue_2811.gif") as im: - assert im.tile[0][3][0] == 11 # LZW bits - # codec error prepatch - im.load() - - def test_extents(self): - with Image.open("Tests/images/test_extents.gif") as im: - assert im.size == (100, 100) - im.seek(1) - assert im.size == (150, 150) +def test_extents(): + with Image.open("Tests/images/test_extents.gif") as im: + assert im.size == (100, 100) + im.seek(1) + assert im.size == (150, 150) diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 855cc9e08..aeb146f7e 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,122 +1,127 @@ import io import sys -import unittest import pytest from PIL import IcnsImagePlugin, Image -from .helper import PillowTestCase, assert_image_equal, assert_image_similar +from .helper import assert_image_equal, assert_image_similar # sample icon file TEST_FILE = "Tests/images/pillow.icns" -enable_jpeg2k = hasattr(Image.core, "jp2klib_version") +ENABLE_JPEG2K = hasattr(Image.core, "jp2klib_version") -class TestFileIcns(PillowTestCase): - def test_sanity(self): - # Loading this icon by default should result in the largest size - # (512x512@2x) being loaded - with Image.open(TEST_FILE) as im: +def test_sanity(): + # Loading this icon by default should result in the largest size + # (512x512@2x) being loaded + with Image.open(TEST_FILE) as im: - # Assert that there is no unclosed file warning - pytest.warns(None, im.load) + # Assert that there is no unclosed file warning + pytest.warns(None, im.load) - assert im.mode == "RGBA" - assert im.size == (1024, 1024) - assert im.format == "ICNS" + assert im.mode == "RGBA" + assert im.size == (1024, 1024) + assert im.format == "ICNS" - @unittest.skipIf(sys.platform != "darwin", "requires macOS") - def test_save(self): - temp_file = self.tempfile("temp.icns") - with Image.open(TEST_FILE) as im: - im.save(temp_file) +@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS") +def test_save(tmp_path): + temp_file = str(tmp_path / "temp.icns") + + with Image.open(TEST_FILE) as im: + im.save(temp_file) + + with Image.open(temp_file) as reread: + assert reread.mode == "RGBA" + assert reread.size == (1024, 1024) + assert reread.format == "ICNS" + + +@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS") +def test_save_append_images(tmp_path): + temp_file = str(tmp_path / "temp.icns") + provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) + + with Image.open(TEST_FILE) as im: + im.save(temp_file, append_images=[provided_im]) with Image.open(temp_file) as reread: - assert reread.mode == "RGBA" - assert reread.size == (1024, 1024) - assert reread.format == "ICNS" + assert_image_similar(reread, im, 1) - @unittest.skipIf(sys.platform != "darwin", "requires macOS") - def test_save_append_images(self): - temp_file = self.tempfile("temp.icns") - provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) + with Image.open(temp_file) as reread: + reread.size = (16, 16, 2) + reread.load() + assert_image_equal(reread, provided_im) - with Image.open(TEST_FILE) as im: - im.save(temp_file, append_images=[provided_im]) - with Image.open(temp_file) as reread: - assert_image_similar(reread, im, 1) - - with Image.open(temp_file) as reread: - reread.size = (16, 16, 2) - reread.load() - assert_image_equal(reread, provided_im) - - def test_sizes(self): - # Check that we can load all of the sizes, and that the final pixel - # dimensions are as expected - with Image.open(TEST_FILE) as im: - for w, h, r in im.info["sizes"]: - wr = w * r - hr = h * r - im.size = (w, h, r) - im.load() - assert im.mode == "RGBA" - assert im.size == (wr, hr) - - # Check that we cannot load an incorrect size - with pytest.raises(ValueError): - im.size = (1, 1) - - def test_older_icon(self): - # This icon was made with Icon Composer rather than iconutil; it still - # uses PNG rather than JP2, however (since it was made on 10.9). - with Image.open("Tests/images/pillow2.icns") as im: - for w, h, r in im.info["sizes"]: - wr = w * r - hr = h * r - with Image.open("Tests/images/pillow2.icns") as im2: - im2.size = (w, h, r) - im2.load() - assert im2.mode == "RGBA" - assert im2.size == (wr, hr) - - def test_jp2_icon(self): - # This icon was made by using Uli Kusterer's oldiconutil to replace - # the PNG images with JPEG 2000 ones. The advantage of doing this is - # that OS X 10.5 supports JPEG 2000 but not PNG; some commercial - # software therefore does just this. - - # (oldiconutil is here: https://github.com/uliwitness/oldiconutil) - - if not enable_jpeg2k: - return - - with Image.open("Tests/images/pillow3.icns") as im: - for w, h, r in im.info["sizes"]: - wr = w * r - hr = h * r - with Image.open("Tests/images/pillow3.icns") as im2: - im2.size = (w, h, r) - im2.load() - assert im2.mode == "RGBA" - assert im2.size == (wr, hr) - - def test_getimage(self): - with open(TEST_FILE, "rb") as fp: - icns_file = IcnsImagePlugin.IcnsFile(fp) - - im = icns_file.getimage() +def test_sizes(): + # Check that we can load all of the sizes, and that the final pixel + # dimensions are as expected + with Image.open(TEST_FILE) as im: + for w, h, r in im.info["sizes"]: + wr = w * r + hr = h * r + im.size = (w, h, r) + im.load() assert im.mode == "RGBA" - assert im.size == (1024, 1024) + assert im.size == (wr, hr) - im = icns_file.getimage((512, 512)) - assert im.mode == "RGBA" - assert im.size == (512, 512) + # Check that we cannot load an incorrect size + with pytest.raises(ValueError): + im.size = (1, 1) - def test_not_an_icns_file(self): - with io.BytesIO(b"invalid\n") as fp: - with pytest.raises(SyntaxError): - IcnsImagePlugin.IcnsFile(fp) + +def test_older_icon(): + # This icon was made with Icon Composer rather than iconutil; it still + # uses PNG rather than JP2, however (since it was made on 10.9). + with Image.open("Tests/images/pillow2.icns") as im: + for w, h, r in im.info["sizes"]: + wr = w * r + hr = h * r + with Image.open("Tests/images/pillow2.icns") as im2: + im2.size = (w, h, r) + im2.load() + assert im2.mode == "RGBA" + assert im2.size == (wr, hr) + + +def test_jp2_icon(): + # This icon was made by using Uli Kusterer's oldiconutil to replace + # the PNG images with JPEG 2000 ones. The advantage of doing this is + # that OS X 10.5 supports JPEG 2000 but not PNG; some commercial + # software therefore does just this. + + # (oldiconutil is here: https://github.com/uliwitness/oldiconutil) + + if not ENABLE_JPEG2K: + return + + with Image.open("Tests/images/pillow3.icns") as im: + for w, h, r in im.info["sizes"]: + wr = w * r + hr = h * r + with Image.open("Tests/images/pillow3.icns") as im2: + im2.size = (w, h, r) + im2.load() + assert im2.mode == "RGBA" + assert im2.size == (wr, hr) + + +def test_getimage(): + with open(TEST_FILE, "rb") as fp: + icns_file = IcnsImagePlugin.IcnsFile(fp) + + im = icns_file.getimage() + assert im.mode == "RGBA" + assert im.size == (1024, 1024) + + im = icns_file.getimage((512, 512)) + assert im.mode == "RGBA" + assert im.size == (512, 512) + + +def test_not_an_icns_file(): + with io.BytesIO(b"invalid\n") as fp: + with pytest.raises(SyntaxError): + IcnsImagePlugin.IcnsFile(fp) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 822b07532..9ed1ffcb7 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -3,101 +3,107 @@ import io import pytest from PIL import IcoImagePlugin, Image, ImageDraw -from .helper import PillowTestCase, assert_image_equal, hopper +from .helper import assert_image_equal, hopper TEST_ICO_FILE = "Tests/images/hopper.ico" -class TestFileIco(PillowTestCase): - def test_sanity(self): - with Image.open(TEST_ICO_FILE) as im: - im.load() - assert im.mode == "RGBA" - assert im.size == (16, 16) - assert im.format == "ICO" - assert im.get_format_mimetype() == "image/x-icon" +def test_sanity(): + with Image.open(TEST_ICO_FILE) as im: + im.load() + assert im.mode == "RGBA" + assert im.size == (16, 16) + assert im.format == "ICO" + assert im.get_format_mimetype() == "image/x-icon" - def test_invalid_file(self): - with open("Tests/images/flower.jpg", "rb") as fp: - with pytest.raises(SyntaxError): - IcoImagePlugin.IcoImageFile(fp) - def test_save_to_bytes(self): - output = io.BytesIO() - im = hopper() - im.save(output, "ico", sizes=[(32, 32), (64, 64)]) +def test_invalid_file(): + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + IcoImagePlugin.IcoImageFile(fp) - # the default image - output.seek(0) - with Image.open(output) as reloaded: - assert reloaded.info["sizes"] == {(32, 32), (64, 64)} - assert im.mode == reloaded.mode - assert (64, 64) == reloaded.size - assert reloaded.format == "ICO" - assert_image_equal(reloaded, hopper().resize((64, 64), Image.LANCZOS)) +def test_save_to_bytes(): + output = io.BytesIO() + im = hopper() + im.save(output, "ico", sizes=[(32, 32), (64, 64)]) - # the other one - output.seek(0) - with Image.open(output) as reloaded: - reloaded.size = (32, 32) + # The default image + output.seek(0) + with Image.open(output) as reloaded: + assert reloaded.info["sizes"] == {(32, 32), (64, 64)} - assert im.mode == reloaded.mode - assert (32, 32) == reloaded.size - assert reloaded.format == "ICO" - assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) + assert im.mode == reloaded.mode + assert (64, 64) == reloaded.size + assert reloaded.format == "ICO" + assert_image_equal(reloaded, hopper().resize((64, 64), Image.LANCZOS)) - def test_incorrect_size(self): - with Image.open(TEST_ICO_FILE) as im: - with pytest.raises(ValueError): - im.size = (1, 1) + # The other one + output.seek(0) + with Image.open(output) as reloaded: + reloaded.size = (32, 32) - def test_save_256x256(self): - """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" - # Arrange - with Image.open("Tests/images/hopper_256x256.ico") as im: - outfile = self.tempfile("temp_saved_hopper_256x256.ico") + assert im.mode == reloaded.mode + assert (32, 32) == reloaded.size + assert reloaded.format == "ICO" + assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) - # Act - im.save(outfile) - with Image.open(outfile) as im_saved: - # Assert - assert im_saved.size == (256, 256) +def test_incorrect_size(): + with Image.open(TEST_ICO_FILE) as im: + with pytest.raises(ValueError): + im.size = (1, 1) - def test_only_save_relevant_sizes(self): - """Issue #2266 https://github.com/python-pillow/Pillow/issues/2266 - Should save in 16x16, 24x24, 32x32, 48x48 sizes - and not in 16x16, 24x24, 32x32, 48x48, 48x48, 48x48, 48x48 sizes - """ - # Arrange - with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48 - outfile = self.tempfile("temp_saved_python.ico") - # Act - im.save(outfile) - with Image.open(outfile) as im_saved: - # Assert - assert im_saved.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)} +def test_save_256x256(tmp_path): + """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" + # Arrange + with Image.open("Tests/images/hopper_256x256.ico") as im: + outfile = str(tmp_path / "temp_saved_hopper_256x256.ico") - def test_unexpected_size(self): - # This image has been manually hexedited to state that it is 16x32 - # while the image within is still 16x16 - def open(): - with Image.open("Tests/images/hopper_unexpected.ico") as im: - assert im.size == (16, 16) + # Act + im.save(outfile) + with Image.open(outfile) as im_saved: - pytest.warns(UserWarning, open) + # Assert + assert im_saved.size == (256, 256) - def test_draw_reloaded(self): - with Image.open(TEST_ICO_FILE) as im: - outfile = self.tempfile("temp_saved_hopper_draw.ico") - draw = ImageDraw.Draw(im) - draw.line((0, 0) + im.size, "#f00") - im.save(outfile) +def test_only_save_relevant_sizes(tmp_path): + """Issue #2266 https://github.com/python-pillow/Pillow/issues/2266 + Should save in 16x16, 24x24, 32x32, 48x48 sizes + and not in 16x16, 24x24, 32x32, 48x48, 48x48, 48x48, 48x48 sizes + """ + # Arrange + with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48 + outfile = str(tmp_path / "temp_saved_python.ico") + # Act + im.save(outfile) - with Image.open(outfile) as im: - im.save("Tests/images/hopper_draw.ico") - with Image.open("Tests/images/hopper_draw.ico") as reloaded: - assert_image_equal(im, reloaded) + with Image.open(outfile) as im_saved: + # Assert + assert im_saved.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)} + + +def test_unexpected_size(): + # This image has been manually hexedited to state that it is 16x32 + # while the image within is still 16x16 + def open(): + with Image.open("Tests/images/hopper_unexpected.ico") as im: + assert im.size == (16, 16) + + pytest.warns(UserWarning, open) + + +def test_draw_reloaded(tmp_path): + with Image.open(TEST_ICO_FILE) as im: + outfile = str(tmp_path / "temp_saved_hopper_draw.ico") + + draw = ImageDraw.Draw(im) + draw.line((0, 0) + im.size, "#f00") + im.save(outfile) + + with Image.open(outfile) as im: + im.save("Tests/images/hopper_draw.ico") + with Image.open("Tests/images/hopper_draw.ico") as reloaded: + assert_image_equal(im, reloaded) diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 84fd4a0e2..8f261388e 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,83 +1,90 @@ import os -import unittest import pytest from PIL import Image, MspImagePlugin -from .helper import PillowTestCase, assert_image_equal, hopper +from .helper import assert_image_equal, hopper TEST_FILE = "Tests/images/hopper.msp" EXTRA_DIR = "Tests/images/picins" YA_EXTRA_DIR = "Tests/images/msp" -class TestFileMsp(PillowTestCase): - def test_sanity(self): - test_file = self.tempfile("temp.msp") +def test_sanity(tmp_path): + test_file = str(tmp_path / "temp.msp") - hopper("1").save(test_file) + hopper("1").save(test_file) - with Image.open(test_file) as im: - im.load() - assert im.mode == "1" - assert im.size == (128, 128) - assert im.format == "MSP" + with Image.open(test_file) as im: + im.load() + assert im.mode == "1" + assert im.size == (128, 128) + assert im.format == "MSP" - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - with pytest.raises(SyntaxError): - MspImagePlugin.MspImageFile(invalid_file) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - def test_bad_checksum(self): - # Arrange - # This was created by forcing Pillow to save with checksum=0 - bad_checksum = "Tests/images/hopper_bad_checksum.msp" + with pytest.raises(SyntaxError): + MspImagePlugin.MspImageFile(invalid_file) - # Act / Assert - with pytest.raises(SyntaxError): - MspImagePlugin.MspImageFile(bad_checksum) - def test_open_windows_v1(self): - # Arrange - # Act - with Image.open(TEST_FILE) as im: +def test_bad_checksum(): + # Arrange + # This was created by forcing Pillow to save with checksum=0 + bad_checksum = "Tests/images/hopper_bad_checksum.msp" - # Assert - assert_image_equal(im, hopper("1")) - assert isinstance(im, MspImagePlugin.MspImageFile) + # Act / Assert + with pytest.raises(SyntaxError): + MspImagePlugin.MspImageFile(bad_checksum) - def _assert_file_image_equal(self, source_path, target_path): - with Image.open(source_path) as im: - with Image.open(target_path) as target: - assert_image_equal(im, target) - @unittest.skipUnless(os.path.exists(EXTRA_DIR), "Extra image files not installed") - def test_open_windows_v2(self): +def test_open_windows_v1(): + # Arrange + # Act + with Image.open(TEST_FILE) as im: - files = ( - os.path.join(EXTRA_DIR, f) - for f in os.listdir(EXTRA_DIR) - if os.path.splitext(f)[1] == ".msp" - ) - for path in files: - self._assert_file_image_equal(path, path.replace(".msp", ".png")) + # Assert + assert_image_equal(im, hopper("1")) + assert isinstance(im, MspImagePlugin.MspImageFile) - @unittest.skipIf( - not os.path.exists(YA_EXTRA_DIR), "Even More Extra image files not installed" + +def _assert_file_image_equal(source_path, target_path): + with Image.open(source_path) as im: + with Image.open(target_path) as target: + assert_image_equal(im, target) + + +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +def test_open_windows_v2(): + + files = ( + os.path.join(EXTRA_DIR, f) + for f in os.listdir(EXTRA_DIR) + if os.path.splitext(f)[1] == ".msp" ) - def test_msp_v2(self): - for f in os.listdir(YA_EXTRA_DIR): - if ".MSP" not in f: - continue - path = os.path.join(YA_EXTRA_DIR, f) - self._assert_file_image_equal(path, path.replace(".MSP", ".png")) + for path in files: + _assert_file_image_equal(path, path.replace(".msp", ".png")) - def test_cannot_save_wrong_mode(self): - # Arrange - im = hopper() - filename = self.tempfile("temp.msp") - # Act/Assert - with pytest.raises(IOError): - im.save(filename) +@pytest.mark.skipif( + not os.path.exists(YA_EXTRA_DIR), reason="Even More Extra image files not installed" +) +def test_msp_v2(): + for f in os.listdir(YA_EXTRA_DIR): + if ".MSP" not in f: + continue + path = os.path.join(YA_EXTRA_DIR, f) + _assert_file_image_equal(path, path.replace(".MSP", ".png")) + + +def test_cannot_save_wrong_mode(tmp_path): + # Arrange + im = hopper() + filename = str(tmp_path / "temp.msp") + + # Act/Assert + with pytest.raises(IOError): + im.save(filename) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index fa299dad5..c7446e161 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,149 +1,162 @@ import tempfile -import unittest from io import BytesIO import pytest from PIL import Image, ImageSequence, SpiderImagePlugin -from .helper import PillowTestCase, assert_image_equal, hopper, is_pypy +from .helper import assert_image_equal, hopper, is_pypy TEST_FILE = "Tests/images/hopper.spider" -class TestImageSpider(PillowTestCase): - def test_sanity(self): +def test_sanity(): + with Image.open(TEST_FILE) as im: + im.load() + assert im.mode == "F" + assert im.size == (128, 128) + assert im.format == "SPIDER" + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + im = Image.open(TEST_FILE) + im.load() + + pytest.warns(ResourceWarning, open) + + +def test_closed_file(): + def open(): + im = Image.open(TEST_FILE) + im.load() + im.close() + + pytest.warns(None, open) + + +def test_context_manager(): + def open(): with Image.open(TEST_FILE) as im: im.load() - assert im.mode == "F" - assert im.size == (128, 128) - assert im.format == "SPIDER" - @unittest.skipIf(is_pypy(), "Requires CPython") - def test_unclosed_file(self): - def open(): - im = Image.open(TEST_FILE) - im.load() + pytest.warns(None, open) - pytest.warns(ResourceWarning, open) - def test_closed_file(self): - def open(): - im = Image.open(TEST_FILE) - im.load() - im.close() +def test_save(tmp_path): + # Arrange + temp = str(tmp_path / "temp.spider") + im = hopper() - pytest.warns(None, open) + # Act + im.save(temp, "SPIDER") - def test_context_manager(self): - def open(): - with Image.open(TEST_FILE) as im: - im.load() + # Assert + with Image.open(temp) as im2: + assert im2.mode == "F" + assert im2.size == (128, 128) + assert im2.format == "SPIDER" - pytest.warns(None, open) - def test_save(self): - # Arrange - temp = self.tempfile("temp.spider") - im = hopper() +def test_tempfile(): + # Arrange + im = hopper() - # Act - im.save(temp, "SPIDER") + # Act + with tempfile.TemporaryFile() as fp: + im.save(fp, "SPIDER") # Assert - with Image.open(temp) as im2: - assert im2.mode == "F" - assert im2.size == (128, 128) - assert im2.format == "SPIDER" + fp.seek(0) + with Image.open(fp) as reloaded: + assert reloaded.mode == "F" + assert reloaded.size == (128, 128) + assert reloaded.format == "SPIDER" - def test_tempfile(self): - # Arrange - im = hopper() + +def test_is_spider_image(): + assert SpiderImagePlugin.isSpiderImage(TEST_FILE) + + +def test_tell(): + # Arrange + with Image.open(TEST_FILE) as im: # Act - with tempfile.TemporaryFile() as fp: - im.save(fp, "SPIDER") - - # Assert - fp.seek(0) - with Image.open(fp) as reloaded: - assert reloaded.mode == "F" - assert reloaded.size == (128, 128) - assert reloaded.format == "SPIDER" - - def test_isSpiderImage(self): - assert SpiderImagePlugin.isSpiderImage(TEST_FILE) - - def test_tell(self): - # Arrange - with Image.open(TEST_FILE) as im: - - # Act - index = im.tell() - - # Assert - assert index == 0 - - def test_n_frames(self): - with Image.open(TEST_FILE) as im: - assert im.n_frames == 1 - assert not im.is_animated - - def test_loadImageSeries(self): - # Arrange - not_spider_file = "Tests/images/hopper.ppm" - file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] - - # Act - img_list = SpiderImagePlugin.loadImageSeries(file_list) + index = im.tell() # Assert - assert len(img_list) == 1 - assert isinstance(img_list[0], Image.Image) - assert img_list[0].size == (128, 128) + assert index == 0 - def test_loadImageSeries_no_input(self): - # Arrange - file_list = None - # Act - img_list = SpiderImagePlugin.loadImageSeries(file_list) +def test_n_frames(): + with Image.open(TEST_FILE) as im: + assert im.n_frames == 1 + assert not im.is_animated - # Assert - assert img_list is None - def test_isInt_not_a_number(self): - # Arrange - not_a_number = "a" +def test_load_image_series(): + # Arrange + not_spider_file = "Tests/images/hopper.ppm" + file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] - # Act - ret = SpiderImagePlugin.isInt(not_a_number) + # Act + img_list = SpiderImagePlugin.loadImageSeries(file_list) - # Assert - assert ret == 0 + # Assert + assert len(img_list) == 1 + assert isinstance(img_list[0], Image.Image) + assert img_list[0].size == (128, 128) - def test_invalid_file(self): - invalid_file = "Tests/images/invalid.spider" - with pytest.raises(IOError): - Image.open(invalid_file) +def test_load_image_series_no_input(): + # Arrange + file_list = None - def test_nonstack_file(self): - with Image.open(TEST_FILE) as im: - with pytest.raises(EOFError): - im.seek(0) + # Act + img_list = SpiderImagePlugin.loadImageSeries(file_list) - def test_nonstack_dos(self): - with Image.open(TEST_FILE) as im: - for i, frame in enumerate(ImageSequence.Iterator(im)): - assert i <= 1, "Non-stack DOS file test failed" + # Assert + assert img_list is None - # for issue #4093 - def test_odd_size(self): - data = BytesIO() - width = 100 - im = Image.new("F", (width, 64)) - im.save(data, format="SPIDER") - data.seek(0) - with Image.open(data) as im2: - assert_image_equal(im, im2) +def test_is_int_not_a_number(): + # Arrange + not_a_number = "a" + + # Act + ret = SpiderImagePlugin.isInt(not_a_number) + + # Assert + assert ret == 0 + + +def test_invalid_file(): + invalid_file = "Tests/images/invalid.spider" + + with pytest.raises(IOError): + Image.open(invalid_file) + + +def test_nonstack_file(): + with Image.open(TEST_FILE) as im: + with pytest.raises(EOFError): + im.seek(0) + + +def test_nonstack_dos(): + with Image.open(TEST_FILE) as im: + for i, frame in enumerate(ImageSequence.Iterator(im)): + assert i <= 1, "Non-stack DOS file test failed" + + +# for issue #4093 +def test_odd_size(): + data = BytesIO() + width = 100 + im = Image.new("F", (width, 64)) + im.save(data, format="SPIDER") + + data.seek(0) + with Image.open(data) as im2: + assert_image_equal(im, im2) diff --git a/Tests/test_image.py b/Tests/test_image.py index 55ee1a9fe..55e70a326 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -2,13 +2,11 @@ import io import os import shutil import tempfile -import unittest import pytest from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError from .helper import ( - PillowTestCase, assert_image_equal, assert_image_similar, assert_not_all_same, @@ -17,7 +15,7 @@ from .helper import ( ) -class TestImage(PillowTestCase): +class TestImage: def test_image_modes_success(self): for mode in [ "1", @@ -109,7 +107,7 @@ class TestImage(PillowTestCase): with pytest.raises(ValueError): Image.open(io.StringIO()) - def test_pathlib(self): + def test_pathlib(self, tmp_path): from PIL.Image import Path with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: @@ -120,13 +118,13 @@ class TestImage(PillowTestCase): assert im.mode == "RGB" assert im.size == (128, 128) - temp_file = self.tempfile("temp.jpg") + temp_file = str(tmp_path / "temp.jpg") if os.path.exists(temp_file): os.remove(temp_file) im.save(Path(temp_file)) - def test_fp_name(self): - temp_file = self.tempfile("temp.jpg") + def test_fp_name(self, tmp_path): + temp_file = str(tmp_path / "temp.jpg") class FP: def write(a, b): @@ -148,9 +146,9 @@ class TestImage(PillowTestCase): with Image.open(fp) as reloaded: assert_image_similar(im, reloaded, 20) - def test_unknown_extension(self): + def test_unknown_extension(self, tmp_path): im = hopper() - temp_file = self.tempfile("temp.unknown") + temp_file = str(tmp_path / "temp.unknown") with pytest.raises(ValueError): im.save(temp_file) @@ -164,25 +162,25 @@ class TestImage(PillowTestCase): im.paste(0, (0, 0, 100, 100)) assert not im.readonly - @unittest.skipIf(is_win32(), "Test requires opening tempfile twice") - def test_readonly_save(self): - temp_file = self.tempfile("temp.bmp") + @pytest.mark.skipif(is_win32(), reason="Test requires opening tempfile twice") + def test_readonly_save(self, tmp_path): + temp_file = str(tmp_path / "temp.bmp") shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) with Image.open(temp_file) as im: assert im.readonly im.save(temp_file) - def test_dump(self): + def test_dump(self, tmp_path): im = Image.new("L", (10, 10)) - im._dump(self.tempfile("temp_L.ppm")) + im._dump(str(tmp_path / "temp_L.ppm")) im = Image.new("RGB", (10, 10)) - im._dump(self.tempfile("temp_RGB.ppm")) + im._dump(str(tmp_path / "temp_RGB.ppm")) im = Image.new("HSV", (10, 10)) with pytest.raises(ValueError): - im._dump(self.tempfile("temp_HSV.ppm")) + im._dump(str(tmp_path / "temp_HSV.ppm")) def test_comparison_with_other_type(self): # Arrange @@ -434,8 +432,7 @@ class TestImage(PillowTestCase): assert_image_similar(im2, im3, 110) def test_check_size(self): - # Checking that the _check_size function throws value errors - # when we want it to. + # Checking that the _check_size function throws value errors when we want it to with pytest.raises(ValueError): Image.new("RGB", 0) # not a tuple with pytest.raises(ValueError): @@ -587,11 +584,11 @@ class TestImage(PillowTestCase): expected = Image.new(mode, (100, 100), color) assert_image_equal(im.convert(mode), expected) - def test_no_resource_warning_on_save(self): + def test_no_resource_warning_on_save(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/835 # Arrange test_file = "Tests/images/hopper.png" - temp_file = self.tempfile("temp.jpg") + temp_file = str(tmp_path / "temp.jpg") # Act/Assert with Image.open(test_file) as im: @@ -623,14 +620,14 @@ class TestImage(PillowTestCase): with Image.open(os.path.join("Tests/images", file)) as im: try: im.load() - self.assertFail() + assert False except OSError as e: assert str(e) == "buffer overrun when reading image file" with Image.open("Tests/images/fli_overrun2.bin") as im: try: im.seek(1) - self.assertFail() + assert False except OSError as e: assert str(e) == "buffer overrun when reading image file" @@ -645,7 +642,7 @@ def mock_encode(*args): return encoder -class TestRegistry(PillowTestCase): +class TestRegistry: def test_encode_registry(self): Image.register_encoder("MOCK", mock_encode) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index d3813b90a..764a3ca49 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,13 +1,12 @@ -import unittest from contextlib import contextmanager import pytest from PIL import Image, ImageDraw -from .helper import PillowTestCase, assert_image_equal, assert_image_similar, hopper +from .helper import assert_image_equal, assert_image_similar, hopper -class TestImagingResampleVulnerability(PillowTestCase): +class TestImagingResampleVulnerability: # see https://github.com/python-pillow/Pillow/issues/1710 def test_overflow(self): im = hopper("L") @@ -43,7 +42,7 @@ class TestImagingResampleVulnerability(PillowTestCase): assert im.tobytes() != copy.tobytes() -class TestImagingCoreResampleAccuracy(PillowTestCase): +class TestImagingCoreResampleAccuracy: def make_case(self, mode, size, color): """Makes a sample image with two dark and two bright squares. For example: @@ -219,7 +218,7 @@ class TestImagingCoreResampleAccuracy(PillowTestCase): assert_image_equal(im, ref) -class CoreResampleConsistencyTest(PillowTestCase): +class CoreResampleConsistencyTest: def make_case(self, mode, fill): im = Image.new(mode, (512, 9), fill) return im.resize((9, 512), Image.LANCZOS), im.load()[0, 0] @@ -254,7 +253,7 @@ class CoreResampleConsistencyTest(PillowTestCase): self.run_case(self.make_case("F", 1.192093e-07)) -class CoreResampleAlphaCorrectTest(PillowTestCase): +class CoreResampleAlphaCorrectTest: def make_levels_case(self, mode): i = Image.new(mode, (256, 16)) px = i.load() @@ -275,7 +274,7 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): len(used_colors), y ) - @unittest.skip("current implementation isn't precise enough") + @pytest.mark.skip("Current implementation isn't precise enough") def test_levels_rgba(self): case = self.make_levels_case("RGBA") self.run_levels_case(case.resize((512, 32), Image.BOX)) @@ -284,7 +283,7 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) - @unittest.skip("current implementation isn't precise enough") + @pytest.mark.skip("Current implementation isn't precise enough") def test_levels_la(self): case = self.make_levels_case("LA") self.run_levels_case(case.resize((512, 32), Image.BOX)) @@ -330,7 +329,7 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255,)) -class CoreResamplePassesTest(PillowTestCase): +class CoreResamplePassesTest: @contextmanager def count(self, diff): count = Image.core.get_stats()["new_count"] @@ -373,7 +372,7 @@ class CoreResamplePassesTest(PillowTestCase): assert_image_similar(with_box, cropped, 0.1) -class CoreResampleCoefficientsTest(PillowTestCase): +class CoreResampleCoefficientsTest: def test_reduce(self): test_color = 254 @@ -402,7 +401,7 @@ class CoreResampleCoefficientsTest(PillowTestCase): assert histogram[0x100 * 3 + 0xFF] == 0x10000 -class CoreResampleBoxTest(PillowTestCase): +class CoreResampleBoxTest: def test_wrong_arguments(self): im = hopper() for resample in ( diff --git a/Tests/test_imagefont_bitmap.py b/Tests/test_imagefont_bitmap.py index 437bd4bf6..c4032d55d 100644 --- a/Tests/test_imagefont_bitmap.py +++ b/Tests/test_imagefont_bitmap.py @@ -1,8 +1,7 @@ -import unittest - +import pytest from PIL import Image, ImageDraw, ImageFont -from .helper import PillowTestCase, assert_image_similar +from .helper import assert_image_similar image_font_installed = True try: @@ -11,35 +10,29 @@ except ImportError: image_font_installed = False -@unittest.skipUnless(image_font_installed, "image font not installed") -class TestImageFontBitmap(PillowTestCase): - def test_similar(self): - text = "EmbeddedBitmap" - font_outline = ImageFont.truetype(font="Tests/fonts/DejaVuSans.ttf", size=24) - font_bitmap = ImageFont.truetype( - font="Tests/fonts/DejaVuSans-bitmap.ttf", size=24 - ) - size_outline = font_outline.getsize(text) - size_bitmap = font_bitmap.getsize(text) - size_final = ( - max(size_outline[0], size_bitmap[0]), - max(size_outline[1], size_bitmap[1]), - ) - im_bitmap = Image.new("RGB", size_final, (255, 255, 255)) - im_outline = im_bitmap.copy() - draw_bitmap = ImageDraw.Draw(im_bitmap) - draw_outline = ImageDraw.Draw(im_outline) +@pytest.mark.skipif(not image_font_installed, reason="Image font not installed") +def test_similar(): + text = "EmbeddedBitmap" + font_outline = ImageFont.truetype(font="Tests/fonts/DejaVuSans.ttf", size=24) + font_bitmap = ImageFont.truetype(font="Tests/fonts/DejaVuSans-bitmap.ttf", size=24) + size_outline = font_outline.getsize(text) + size_bitmap = font_bitmap.getsize(text) + size_final = ( + max(size_outline[0], size_bitmap[0]), + max(size_outline[1], size_bitmap[1]), + ) + im_bitmap = Image.new("RGB", size_final, (255, 255, 255)) + im_outline = im_bitmap.copy() + draw_bitmap = ImageDraw.Draw(im_bitmap) + draw_outline = ImageDraw.Draw(im_outline) - # Metrics are different on the bitmap and ttf fonts, - # more so on some platforms and versions of freetype than others. - # Mac has a 1px difference, linux doesn't. - draw_bitmap.text( - (0, size_final[1] - size_bitmap[1]), text, fill=(0, 0, 0), font=font_bitmap - ) - draw_outline.text( - (0, size_final[1] - size_outline[1]), - text, - fill=(0, 0, 0), - font=font_outline, - ) - assert_image_similar(im_bitmap, im_outline, 20) + # Metrics are different on the bitmap and TTF fonts, + # more so on some platforms and versions of FreeType than others. + # Mac has a 1px difference, Linux doesn't. + draw_bitmap.text( + (0, size_final[1] - size_bitmap[1]), text, fill=(0, 0, 0), font=font_bitmap + ) + draw_outline.text( + (0, size_final[1] - size_outline[1]), text, fill=(0, 0, 0), font=font_outline, + ) + assert_image_similar(im_bitmap, im_outline, 20)