diff --git a/Tests/helper.py b/Tests/helper.py index acd553d6f61..a0034edc19c 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -16,7 +16,7 @@ import pytest from packaging.version import parse as parse_version -from PIL import Image, ImageFile, ImageMath, features +from PIL import Image, ImageChops, ImageFile, ImageMath, features TYPE_CHECKING = False if TYPE_CHECKING: @@ -60,10 +60,8 @@ def convert_to_comparable( ) -> tuple[Image.Image, Image.Image]: new_a, new_b = a, b if a.mode == "P": - new_a = Image.new("L", a.size) - new_b = Image.new("L", b.size) - new_a.putdata(a.get_flattened_data()) - new_b.putdata(b.get_flattened_data()) + new_a = Image.frombytes("L", a.size, a.tobytes()) + new_b = Image.frombytes("L", b.size, b.tobytes()) elif a.mode == "I;16": new_a = a.convert("I") new_b = b.convert("I") @@ -124,12 +122,16 @@ def assert_image_similar( a, b = convert_to_comparable(a, b) - diff = 0 - for ach, bch in zip(a.split(), b.split()): - chdiff = ImageMath.lambda_eval( - lambda args: abs(args["a"] - args["b"]), a=ach, b=bch + if a.mode in ("I", "F"): + # ImageChops.difference only supports 8bpc images, + # so this needs to be done by hand. + diff_im = ImageMath.lambda_eval( + lambda args: abs(args["a"] - args["b"]), a=a, b=b ).convert("L") - diff += sum(i * num for i, num in enumerate(chdiff.histogram())) + else: + diff_im = ImageChops.difference(a, b) + + diff = sum((i % 256) * count for i, count in enumerate(diff_im.histogram())) ave_diff = diff / (a.size[0] * a.size[1]) try: diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 15689476c91..27854ec83ee 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -4,7 +4,9 @@ import pytest -from PIL import Image, ImageSequence, PngImagePlugin +from PIL import Image, ImageFile, ImageSequence, PngImagePlugin + +from .helper import hopper TYPE_CHECKING = False if TYPE_CHECKING: @@ -459,20 +461,23 @@ def test_apng_save_alpha(tmp_path: Path) -> None: assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127) -def test_apng_save_split_fdat(tmp_path: Path) -> None: +def test_apng_save_split_fdat(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: # test to make sure we do not generate sequence errors when writing # frames with image data spanning multiple fdAT chunks (in this case # both the default image and first animation frame will span multiple - # data chunks) + # data chunks). A small block size lets a small image satisfy that premise, + # rather than needing a large image and the time spent compressing it. + monkeypatch.setattr(ImageFile, "MAXBLOCK", 1024) test_file = tmp_path / "temp.png" - with Image.open("Tests/images/old-style-jpeg-compression.png") as im: - frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))] - im.save( - test_file, - save_all=True, - default_image=True, - append_images=frames, - ) + im = hopper("RGBA") + frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))] + im.save( + test_file, + save_all=True, + default_image=True, + append_images=frames, + ) + assert test_file.read_bytes().count(b"fdAT") > 2 with Image.open(test_file) as im: assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 5e7add81c72..4c923ca86c0 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -7,82 +7,22 @@ from PIL import ( BmpImagePlugin, - EpsImagePlugin, Image, ImageFile, UnidentifiedImageError, _binary, - features, ) from .helper import ( - assert_image, assert_image_equal, - assert_image_similar, fromstring, hopper, skip_unless_feature, tostring, ) -# save original block sizes -MAXBLOCK = ImageFile.MAXBLOCK -SAFEBLOCK = ImageFile.SAFEBLOCK - class TestImageFile: - def test_parser(self, monkeypatch: pytest.MonkeyPatch) -> None: - def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: - im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) - if format in ("MSP", "XBM"): - im = im.convert("1") - - test_file = BytesIO() - - im.copy().save(test_file, format) - - data = test_file.getvalue() - - parser = ImageFile.Parser() - parser.feed(data) - im_out = parser.close() - - return im, im_out - - assert_image_equal(*roundtrip("BMP")) - im1, im2 = roundtrip("GIF") - assert_image_similar(im1.convert("P"), im2, 1) - with pytest.warns(DeprecationWarning, match="IM image format"): - assert_image_equal(*roundtrip("IM")) - assert_image_equal(*roundtrip("MSP")) - if features.check("zlib"): - # force multiple blocks in PNG driver - monkeypatch.setattr(ImageFile, "MAXBLOCK", 8192) - assert_image_equal(*roundtrip("PNG")) - assert_image_equal(*roundtrip("PPM")) - assert_image_equal(*roundtrip("TIFF")) - assert_image_equal(*roundtrip("XBM")) - assert_image_equal(*roundtrip("TGA")) - assert_image_equal(*roundtrip("PCX")) - - if EpsImagePlugin.has_ghostscript(): - im1, im2 = roundtrip("EPS") - # This test fails on Ubuntu 12.04, PPC (Bigendian) It - # appears to be a ghostscript 9.05 bug, since the - # ghostscript rendering is wonky and the file is identical - # to that written on ubuntu 12.04 x64 - # md5sum: ba974835ff2d6f3f2fd0053a23521d4a - - # EPS comes back in RGB: - assert_image_similar(im1, im2.convert("L"), 20) - - if features.check("jpg"): - im1, im2 = roundtrip("JPEG") # lossy compression - assert_image(im1, im2.mode, im2.size) - - with pytest.raises(OSError): - roundtrip("PDF") - def test_ico(self) -> None: with open("Tests/images/python.ico", "rb") as f: data = f.read() diff --git a/Tests/test_imagefile_parser.py b/Tests/test_imagefile_parser.py new file mode 100644 index 00000000000..ab4e7c69f16 --- /dev/null +++ b/Tests/test_imagefile_parser.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from io import BytesIO + +import pytest + +from PIL import EpsImagePlugin, Image, ImageFile +from Tests.helper import ( + assert_image, + assert_image_equal, + assert_image_similar, + hopper, + skip_unless_feature, +) + + +@pytest.fixture(scope="module") +def hopper_l_1k() -> Image.Image: + return hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) + + +def roundtrip(im: Image.Image, format: str) -> tuple[Image.Image, Image.Image]: + if format in ("MSP", "XBM"): + im = im.convert("1") + + test_file = BytesIO() + + im.copy().save(test_file, format) + + data = test_file.getvalue() + + parser = ImageFile.Parser() + parser.feed(data) + im_out = parser.close() + + return im, im_out + + +@pytest.mark.parametrize( + "format", + [ + "BMP", + pytest.param( + "EPS", + marks=pytest.mark.skipif( + not EpsImagePlugin.has_ghostscript(), + reason="Ghostscript not available", + ), + ), + "GIF", + "IM", + pytest.param("JPEG", marks=skip_unless_feature("jpg")), + "MSP", + "PCX", + pytest.param("PNG", marks=skip_unless_feature("zlib")), + "PPM", + "TGA", + "TIFF", + "XBM", + ], +) +def test_parser( + monkeypatch: pytest.MonkeyPatch, hopper_l_1k: Image.Image, format: str +) -> None: + # force multiple blocks in PNG driver + monkeypatch.setattr(ImageFile, "MAXBLOCK", 8192) + + if format == "IM": + with pytest.warns(DeprecationWarning, match="IM image format"): + im1, im2 = roundtrip(hopper_l_1k, format) + else: + im1, im2 = roundtrip(hopper_l_1k, format) + + if format == "GIF": + assert_image_similar(im1.convert("P"), im2, 1) + elif format == "EPS": + # This test fails on Ubuntu 12.04, PPC (Bigendian) It + # appears to be a ghostscript 9.05 bug, since the + # ghostscript rendering is wonky and the file is identical + # to that written on ubuntu 12.04 x64 + # md5sum: ba974835ff2d6f3f2fd0053a23521d4a + + # EPS comes back in RGB: + assert_image_similar(im1, im2.convert("L"), 20) + elif format == "JPEG": # Lossy compression + assert_image(im1, im2.mode, im2.size) + else: + assert_image_equal(im1, im2) + + +def test_parser_pdf_roundtrip_error(hopper_l_1k: Image.Image) -> None: + # See https://github.com/python-pillow/Pillow/issues/78 + with pytest.raises(OSError, match="cannot parse this image"): + roundtrip(hopper_l_1k, "PDF")