diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py new file mode 100644 index 000000000..8c5ea90ff --- /dev/null +++ b/Tests/test_file_apng.py @@ -0,0 +1,539 @@ +from PIL import Image, ImageSequence, PngImagePlugin + +import pytest + +from .helper import PillowTestCase + + +class TestFilePng(PillowTestCase): + + # APNG browser support tests and fixtures via: + # https://philip.html5.org/tests/apng/tests.html + # (referenced from https://wiki.mozilla.org/APNG_Specification) + def test_apng_basic(self): + im = Image.open("Tests/images/apng/single_frame.png") + self.assertTrue(im.is_animated) + self.assertEqual(im.get_format_mimetype(), "image/apng") + self.assertIsNone(im.info.get("default_image")) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + im = Image.open("Tests/images/apng/single_frame_default.png") + self.assertTrue(im.is_animated) + self.assertEqual(im.get_format_mimetype(), "image/apng") + self.assertTrue(im.info.get("default_image")) + self.assertEqual(im.getpixel((0, 0)), (255, 0, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (255, 0, 0, 255)) + im.seek(1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + # test out of bounds seek + with self.assertRaises(EOFError): + im.seek(2) + + # test rewind support + im.seek(0) + self.assertEqual(im.getpixel((0, 0)), (255, 0, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (255, 0, 0, 255)) + im.seek(1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + def test_apng_fdat(self): + im = Image.open("Tests/images/apng/split_fdat.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + im = Image.open("Tests/images/apng/split_fdat_zero_chunk.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + def test_apng_dispose(self): + im = Image.open("Tests/images/apng/dispose_op_none.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + im = Image.open("Tests/images/apng/dispose_op_background.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 0, 0, 0)) + self.assertEqual(im.getpixel((64, 32)), (0, 0, 0, 0)) + + im = Image.open("Tests/images/apng/dispose_op_background_final.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + im = Image.open("Tests/images/apng/dispose_op_previous.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + im = Image.open("Tests/images/apng/dispose_op_previous_final.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + im = Image.open("Tests/images/apng/dispose_op_previous_first.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 0, 0, 0)) + self.assertEqual(im.getpixel((64, 32)), (0, 0, 0, 0)) + + def test_apng_dispose_region(self): + im = Image.open("Tests/images/apng/dispose_op_none_region.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + im = Image.open("Tests/images/apng/dispose_op_background_before_region.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 0, 0, 0)) + self.assertEqual(im.getpixel((64, 32)), (0, 0, 0, 0)) + + im = Image.open("Tests/images/apng/dispose_op_background_region.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 0, 255, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 0, 0, 0)) + + im = Image.open("Tests/images/apng/dispose_op_previous_region.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + def test_apng_blend(self): + im = Image.open("Tests/images/apng/blend_op_source_solid.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + im = Image.open("Tests/images/apng/blend_op_source_transparent.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 0, 0, 0)) + self.assertEqual(im.getpixel((64, 32)), (0, 0, 0, 0)) + + im = Image.open("Tests/images/apng/blend_op_source_near_transparent.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 2)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 2)) + + im = Image.open("Tests/images/apng/blend_op_over.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + im = Image.open("Tests/images/apng/blend_op_over_near_transparent.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 97)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + def test_apng_chunk_order(self): + im = Image.open("Tests/images/apng/fctl_actl.png") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + def test_apng_delay(self): + im = Image.open("Tests/images/apng/delay.png") + im.seek(1) + self.assertEqual(im.info.get("duration"), 500.0) + im.seek(2) + self.assertEqual(im.info.get("duration"), 1000.0) + im.seek(3) + self.assertEqual(im.info.get("duration"), 500.0) + im.seek(4) + self.assertEqual(im.info.get("duration"), 1000.0) + + im = Image.open("Tests/images/apng/delay_round.png") + im.seek(1) + self.assertEqual(im.info.get("duration"), 500.0) + im.seek(2) + self.assertEqual(im.info.get("duration"), 1000.0) + + im = Image.open("Tests/images/apng/delay_short_max.png") + im.seek(1) + self.assertEqual(im.info.get("duration"), 500.0) + im.seek(2) + self.assertEqual(im.info.get("duration"), 1000.0) + + im = Image.open("Tests/images/apng/delay_zero_denom.png") + im.seek(1) + self.assertEqual(im.info.get("duration"), 500.0) + im.seek(2) + self.assertEqual(im.info.get("duration"), 1000.0) + + im = Image.open("Tests/images/apng/delay_zero_numer.png") + im.seek(1) + self.assertEqual(im.info.get("duration"), 0.0) + im.seek(2) + self.assertEqual(im.info.get("duration"), 0.0) + im.seek(3) + self.assertEqual(im.info.get("duration"), 500.0) + im.seek(4) + self.assertEqual(im.info.get("duration"), 1000.0) + + def test_apng_num_plays(self): + im = Image.open("Tests/images/apng/num_plays.png") + self.assertEqual(im.info.get("loop"), 0) + + im = Image.open("Tests/images/apng/num_plays_1.png") + self.assertEqual(im.info.get("loop"), 1) + + def test_apng_mode(self): + im = Image.open("Tests/images/apng/mode_16bit.png") + self.assertEqual(im.mode, "RGBA") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (0, 0, 128, 191)) + self.assertEqual(im.getpixel((64, 32)), (0, 0, 128, 191)) + + im = Image.open("Tests/images/apng/mode_greyscale.png") + self.assertEqual(im.mode, "L") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), 128) + self.assertEqual(im.getpixel((64, 32)), 255) + + im = Image.open("Tests/images/apng/mode_greyscale_alpha.png") + self.assertEqual(im.mode, "LA") + im.seek(im.n_frames - 1) + self.assertEqual(im.getpixel((0, 0)), (128, 191)) + self.assertEqual(im.getpixel((64, 32)), (128, 191)) + + im = Image.open("Tests/images/apng/mode_palette.png") + self.assertEqual(im.mode, "P") + im.seek(im.n_frames - 1) + im = im.convert("RGB") + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0)) + + im = Image.open("Tests/images/apng/mode_palette_alpha.png") + self.assertEqual(im.mode, "P") + im.seek(im.n_frames - 1) + im = im.convert("RGBA") + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + im = Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") + self.assertEqual(im.mode, "P") + im.seek(im.n_frames - 1) + im = im.convert("RGBA") + self.assertEqual(im.getpixel((0, 0)), (0, 0, 255, 128)) + self.assertEqual(im.getpixel((64, 32)), (0, 0, 255, 128)) + + def test_apng_chunk_errors(self): + im = Image.open("Tests/images/apng/chunk_no_actl.png") + self.assertFalse(im.is_animated) + + def open(): + im = Image.open("Tests/images/apng/chunk_multi_actl.png") + im.load() + + pytest.warns(UserWarning, open) + self.assertFalse(im.is_animated) + + im = Image.open("Tests/images/apng/chunk_actl_after_idat.png") + self.assertFalse(im.is_animated) + + im = Image.open("Tests/images/apng/chunk_no_fctl.png") + with self.assertRaises(SyntaxError): + im.seek(im.n_frames - 1) + + im = Image.open("Tests/images/apng/chunk_repeat_fctl.png") + with self.assertRaises(SyntaxError): + im.seek(im.n_frames - 1) + + im = Image.open("Tests/images/apng/chunk_no_fdat.png") + with self.assertRaises(SyntaxError): + im.seek(im.n_frames - 1) + + def test_apng_syntax_errors(self): + def open(): + im = Image.open("Tests/images/apng/syntax_num_frames_zero.png") + self.assertFalse(im.is_animated) + with self.assertRaises(OSError): + im.load() + + pytest.warns(UserWarning, open) + + def open(): + im = Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") + self.assertFalse(im.is_animated) + im.load() + + pytest.warns(UserWarning, open) + + # we can handle this case gracefully + exception = None + im = Image.open("Tests/images/apng/syntax_num_frames_low.png") + try: + im.seek(im.n_frames - 1) + except Exception as e: + exception = e + self.assertIsNone(exception) + + with self.assertRaises(SyntaxError): + im = Image.open("Tests/images/apng/syntax_num_frames_high.png") + im.seek(im.n_frames - 1) + im.load() + + def open(): + im = Image.open("Tests/images/apng/syntax_num_frames_invalid.png") + self.assertFalse(im.is_animated) + im.load() + + pytest.warns(UserWarning, open) + + def test_apng_sequence_errors(self): + test_files = [ + "sequence_start.png", + "sequence_gap.png", + "sequence_repeat.png", + "sequence_repeat_chunk.png", + "sequence_reorder.png", + "sequence_reorder_chunk.png", + "sequence_fdat_fctl.png", + ] + for f in test_files: + with self.assertRaises(SyntaxError): + im = Image.open("Tests/images/apng/{0}".format(f)) + im.seek(im.n_frames - 1) + im.load() + + def test_apng_save(self): + im = Image.open("Tests/images/apng/single_frame.png") + test_file = self.tempfile("temp.png") + im.save(test_file, save_all=True) + + with Image.open(test_file) as im: + im.load() + self.assertTrue(im.is_animated) + self.assertEqual(im.get_format_mimetype(), "image/apng") + self.assertIsNone(im.info.get("default_image")) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + im = Image.open("Tests/images/apng/single_frame_default.png") + frames = [] + for im in ImageSequence.Iterator(im): + frames.append(im.copy()) + frames[0].save( + test_file, save_all=True, default_image=True, append_images=frames[1:] + ) + + with Image.open(test_file) as im: + im.load() + self.assertTrue(im.is_animated) + self.assertEqual(im.get_format_mimetype(), "image/apng") + self.assertTrue(im.info.get("default_image")) + im.seek(1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + def test_apng_save_split_fdat(self): + # 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) + test_file = self.tempfile("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, + ) + with Image.open(test_file) as im: + exception = None + try: + im.seek(im.n_frames - 1) + im.load() + except Exception as e: + exception = e + self.assertIsNone(exception) + + def test_apng_save_duration_loop(self): + test_file = self.tempfile("temp.png") + im = Image.open("Tests/images/apng/delay.png") + frames = [] + durations = [] + loop = im.info.get("loop") + default_image = im.info.get("default_image") + for i, im in enumerate(ImageSequence.Iterator(im)): + frames.append(im.copy()) + if i != 0 or not default_image: + durations.append(im.info.get("duration", 0)) + frames[0].save( + test_file, + save_all=True, + default_image=default_image, + append_images=frames[1:], + duration=durations, + loop=loop, + ) + with Image.open(test_file) as im: + im.load() + self.assertEqual(im.info.get("loop"), loop) + im.seek(1) + self.assertEqual(im.info.get("duration"), 500.0) + im.seek(2) + self.assertEqual(im.info.get("duration"), 1000.0) + im.seek(3) + self.assertEqual(im.info.get("duration"), 500.0) + im.seek(4) + self.assertEqual(im.info.get("duration"), 1000.0) + + # test removal of duplicated frames + frame = Image.new("RGBA", (128, 64), (255, 0, 0, 255)) + frame.save(test_file, save_all=True, append_images=[frame], duration=[500, 250]) + with Image.open(test_file) as im: + im.load() + self.assertEqual(im.n_frames, 1) + self.assertEqual(im.info.get("duration"), 750) + + def test_apng_save_disposal(self): + test_file = self.tempfile("temp.png") + size = (128, 64) + red = Image.new("RGBA", size, (255, 0, 0, 255)) + green = Image.new("RGBA", size, (0, 255, 0, 255)) + transparent = Image.new("RGBA", size, (0, 0, 0, 0)) + + # test APNG_DISPOSE_OP_NONE + red.save( + test_file, + save_all=True, + append_images=[green, transparent], + disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, + blend=PngImagePlugin.APNG_BLEND_OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(2) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + # test APNG_DISPOSE_OP_BACKGROUND + disposal = [ + PngImagePlugin.APNG_DISPOSE_OP_NONE, + PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND, + PngImagePlugin.APNG_DISPOSE_OP_NONE, + ] + red.save( + test_file, + save_all=True, + append_images=[red, transparent], + disposal=disposal, + blend=PngImagePlugin.APNG_BLEND_OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(2) + self.assertEqual(im.getpixel((0, 0)), (0, 0, 0, 0)) + self.assertEqual(im.getpixel((64, 32)), (0, 0, 0, 0)) + + disposal = [ + PngImagePlugin.APNG_DISPOSE_OP_NONE, + PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND, + ] + red.save( + test_file, + save_all=True, + append_images=[green], + disposal=disposal, + blend=PngImagePlugin.APNG_BLEND_OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + # test APNG_DISPOSE_OP_PREVIOUS + disposal = [ + PngImagePlugin.APNG_DISPOSE_OP_NONE, + PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, + PngImagePlugin.APNG_DISPOSE_OP_NONE, + ] + red.save( + test_file, + save_all=True, + append_images=[green, red, transparent], + default_image=True, + disposal=disposal, + blend=PngImagePlugin.APNG_BLEND_OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(3) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + disposal = [ + PngImagePlugin.APNG_DISPOSE_OP_NONE, + PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, + ] + red.save( + test_file, + save_all=True, + append_images=[green], + disposal=disposal, + blend=PngImagePlugin.APNG_BLEND_OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + def test_apng_save_blend(self): + test_file = self.tempfile("temp.png") + size = (128, 64) + red = Image.new("RGBA", size, (255, 0, 0, 255)) + green = Image.new("RGBA", size, (0, 255, 0, 255)) + transparent = Image.new("RGBA", size, (0, 0, 0, 0)) + + # test APNG_BLEND_OP_SOURCE on solid color + blend = [ + PngImagePlugin.APNG_BLEND_OP_OVER, + PngImagePlugin.APNG_BLEND_OP_SOURCE, + ] + red.save( + test_file, + save_all=True, + append_images=[red, green], + default_image=True, + disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, + blend=blend, + ) + with Image.open(test_file) as im: + im.seek(2) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + + # test APNG_BLEND_OP_SOURCE on transparent color + blend = [ + PngImagePlugin.APNG_BLEND_OP_OVER, + PngImagePlugin.APNG_BLEND_OP_SOURCE, + ] + red.save( + test_file, + save_all=True, + append_images=[red, transparent], + default_image=True, + disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, + blend=blend, + ) + with Image.open(test_file) as im: + im.seek(2) + self.assertEqual(im.getpixel((0, 0)), (0, 0, 0, 0)) + self.assertEqual(im.getpixel((64, 32)), (0, 0, 0, 0)) + + # test APNG_BLEND_OP_OVER + red.save( + test_file, + save_all=True, + append_images=[green, transparent], + default_image=True, + disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, + blend=PngImagePlugin.APNG_BLEND_OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(1) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) + im.seek(2) + self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) + self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 09a9963b8..47ac40f8f 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -3,7 +3,7 @@ import zlib from io import BytesIO import pytest -from PIL import Image, ImageFile, ImageSequence, PngImagePlugin +from PIL import Image, ImageFile, PngImagePlugin from .helper import ( PillowLeakTestCase, @@ -625,541 +625,6 @@ class TestFilePng(PillowTestCase): with Image.open(test_file) as reloaded: assert reloaded.info["exif"] == b"Exif\x00\x00exifstring" - # APNG browser support tests and fixtures via: - # https://philip.html5.org/tests/apng/tests.html - # (referenced from https://wiki.mozilla.org/APNG_Specification) - def test_apng_basic(self): - im = Image.open("Tests/images/apng/single_frame.png") - self.assertTrue(im.is_animated) - self.assertEqual(im.get_format_mimetype(), "image/apng") - self.assertIsNone(im.info.get("default_image")) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - im = Image.open("Tests/images/apng/single_frame_default.png") - self.assertTrue(im.is_animated) - self.assertEqual(im.get_format_mimetype(), "image/apng") - self.assertTrue(im.info.get("default_image")) - self.assertEqual(im.getpixel((0, 0)), (255, 0, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (255, 0, 0, 255)) - im.seek(1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - # test out of bounds seek - with self.assertRaises(EOFError): - im.seek(2) - - # test rewind support - im.seek(0) - self.assertEqual(im.getpixel((0, 0)), (255, 0, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (255, 0, 0, 255)) - im.seek(1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - def test_apng_fdat(self): - im = Image.open("Tests/images/apng/split_fdat.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - im = Image.open("Tests/images/apng/split_fdat_zero_chunk.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - def test_apng_dispose(self): - im = Image.open("Tests/images/apng/dispose_op_none.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - im = Image.open("Tests/images/apng/dispose_op_background.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 0, 0, 0)) - self.assertEqual(im.getpixel((64, 32)), (0, 0, 0, 0)) - - im = Image.open("Tests/images/apng/dispose_op_background_final.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - im = Image.open("Tests/images/apng/dispose_op_previous.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - im = Image.open("Tests/images/apng/dispose_op_previous_final.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - im = Image.open("Tests/images/apng/dispose_op_previous_first.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 0, 0, 0)) - self.assertEqual(im.getpixel((64, 32)), (0, 0, 0, 0)) - - def test_apng_dispose_region(self): - im = Image.open("Tests/images/apng/dispose_op_none_region.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - im = Image.open("Tests/images/apng/dispose_op_background_before_region.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 0, 0, 0)) - self.assertEqual(im.getpixel((64, 32)), (0, 0, 0, 0)) - - im = Image.open("Tests/images/apng/dispose_op_background_region.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 0, 255, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 0, 0, 0)) - - im = Image.open("Tests/images/apng/dispose_op_previous_region.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - def test_apng_blend(self): - im = Image.open("Tests/images/apng/blend_op_source_solid.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - im = Image.open("Tests/images/apng/blend_op_source_transparent.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 0, 0, 0)) - self.assertEqual(im.getpixel((64, 32)), (0, 0, 0, 0)) - - im = Image.open("Tests/images/apng/blend_op_source_near_transparent.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 2)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 2)) - - im = Image.open("Tests/images/apng/blend_op_over.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - im = Image.open("Tests/images/apng/blend_op_over_near_transparent.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 97)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - def test_apng_chunk_order(self): - im = Image.open("Tests/images/apng/fctl_actl.png") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - def test_apng_delay(self): - im = Image.open("Tests/images/apng/delay.png") - im.seek(1) - self.assertEqual(im.info.get("duration"), 500.0) - im.seek(2) - self.assertEqual(im.info.get("duration"), 1000.0) - im.seek(3) - self.assertEqual(im.info.get("duration"), 500.0) - im.seek(4) - self.assertEqual(im.info.get("duration"), 1000.0) - - im = Image.open("Tests/images/apng/delay_round.png") - im.seek(1) - self.assertEqual(im.info.get("duration"), 500.0) - im.seek(2) - self.assertEqual(im.info.get("duration"), 1000.0) - - im = Image.open("Tests/images/apng/delay_short_max.png") - im.seek(1) - self.assertEqual(im.info.get("duration"), 500.0) - im.seek(2) - self.assertEqual(im.info.get("duration"), 1000.0) - - im = Image.open("Tests/images/apng/delay_zero_denom.png") - im.seek(1) - self.assertEqual(im.info.get("duration"), 500.0) - im.seek(2) - self.assertEqual(im.info.get("duration"), 1000.0) - - im = Image.open("Tests/images/apng/delay_zero_numer.png") - im.seek(1) - self.assertEqual(im.info.get("duration"), 0.0) - im.seek(2) - self.assertEqual(im.info.get("duration"), 0.0) - im.seek(3) - self.assertEqual(im.info.get("duration"), 500.0) - im.seek(4) - self.assertEqual(im.info.get("duration"), 1000.0) - - def test_apng_num_plays(self): - im = Image.open("Tests/images/apng/num_plays.png") - self.assertEqual(im.info.get("loop"), 0) - - im = Image.open("Tests/images/apng/num_plays_1.png") - self.assertEqual(im.info.get("loop"), 1) - - def test_apng_mode(self): - im = Image.open("Tests/images/apng/mode_16bit.png") - self.assertEqual(im.mode, "RGBA") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (0, 0, 128, 191)) - self.assertEqual(im.getpixel((64, 32)), (0, 0, 128, 191)) - - im = Image.open("Tests/images/apng/mode_greyscale.png") - self.assertEqual(im.mode, "L") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), 128) - self.assertEqual(im.getpixel((64, 32)), 255) - - im = Image.open("Tests/images/apng/mode_greyscale_alpha.png") - self.assertEqual(im.mode, "LA") - im.seek(im.n_frames - 1) - self.assertEqual(im.getpixel((0, 0)), (128, 191)) - self.assertEqual(im.getpixel((64, 32)), (128, 191)) - - im = Image.open("Tests/images/apng/mode_palette.png") - self.assertEqual(im.mode, "P") - im.seek(im.n_frames - 1) - im = im.convert("RGB") - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0)) - - im = Image.open("Tests/images/apng/mode_palette_alpha.png") - self.assertEqual(im.mode, "P") - im.seek(im.n_frames - 1) - im = im.convert("RGBA") - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - im = Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") - self.assertEqual(im.mode, "P") - im.seek(im.n_frames - 1) - im = im.convert("RGBA") - self.assertEqual(im.getpixel((0, 0)), (0, 0, 255, 128)) - self.assertEqual(im.getpixel((64, 32)), (0, 0, 255, 128)) - - def test_apng_chunk_errors(self): - im = Image.open("Tests/images/apng/chunk_no_actl.png") - self.assertFalse(im.is_animated) - - def open(): - im = Image.open("Tests/images/apng/chunk_multi_actl.png") - im.load() - - pytest.warns(UserWarning, open) - self.assertFalse(im.is_animated) - - im = Image.open("Tests/images/apng/chunk_actl_after_idat.png") - self.assertFalse(im.is_animated) - - im = Image.open("Tests/images/apng/chunk_no_fctl.png") - with self.assertRaises(SyntaxError): - im.seek(im.n_frames - 1) - - im = Image.open("Tests/images/apng/chunk_repeat_fctl.png") - with self.assertRaises(SyntaxError): - im.seek(im.n_frames - 1) - - im = Image.open("Tests/images/apng/chunk_no_fdat.png") - with self.assertRaises(SyntaxError): - im.seek(im.n_frames - 1) - - def test_apng_syntax_errors(self): - def open(): - im = Image.open("Tests/images/apng/syntax_num_frames_zero.png") - self.assertFalse(im.is_animated) - with self.assertRaises(OSError): - im.load() - - pytest.warns(UserWarning, open) - - def open(): - im = Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") - self.assertFalse(im.is_animated) - im.load() - - pytest.warns(UserWarning, open) - - # we can handle this case gracefully - exception = None - im = Image.open("Tests/images/apng/syntax_num_frames_low.png") - try: - im.seek(im.n_frames - 1) - except Exception as e: - exception = e - self.assertIsNone(exception) - - with self.assertRaises(SyntaxError): - im = Image.open("Tests/images/apng/syntax_num_frames_high.png") - im.seek(im.n_frames - 1) - im.load() - - def open(): - im = Image.open("Tests/images/apng/syntax_num_frames_invalid.png") - self.assertFalse(im.is_animated) - im.load() - - pytest.warns(UserWarning, open) - - def test_apng_sequence_errors(self): - test_files = [ - "sequence_start.png", - "sequence_gap.png", - "sequence_repeat.png", - "sequence_repeat_chunk.png", - "sequence_reorder.png", - "sequence_reorder_chunk.png", - "sequence_fdat_fctl.png", - ] - for f in test_files: - with self.assertRaises(SyntaxError): - im = Image.open("Tests/images/apng/{0}".format(f)) - im.seek(im.n_frames - 1) - im.load() - - def test_apng_save(self): - im = Image.open("Tests/images/apng/single_frame.png") - test_file = self.tempfile("temp.png") - im.save(test_file, save_all=True) - - with Image.open(test_file) as im: - im.load() - self.assertTrue(im.is_animated) - self.assertEqual(im.get_format_mimetype(), "image/apng") - self.assertIsNone(im.info.get("default_image")) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - im = Image.open("Tests/images/apng/single_frame_default.png") - frames = [] - for im in ImageSequence.Iterator(im): - frames.append(im.copy()) - frames[0].save( - test_file, save_all=True, default_image=True, append_images=frames[1:] - ) - - with Image.open(test_file) as im: - im.load() - self.assertTrue(im.is_animated) - self.assertEqual(im.get_format_mimetype(), "image/apng") - self.assertTrue(im.info.get("default_image")) - im.seek(1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - def test_apng_save_split_fdat(self): - # 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) - test_file = self.tempfile("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, - ) - with Image.open(test_file) as im: - exception = None - try: - im.seek(im.n_frames - 1) - im.load() - except Exception as e: - exception = e - self.assertIsNone(exception) - - def test_apng_save_duration_loop(self): - test_file = self.tempfile("temp.png") - im = Image.open("Tests/images/apng/delay.png") - frames = [] - durations = [] - loop = im.info.get("loop") - default_image = im.info.get("default_image") - for i, im in enumerate(ImageSequence.Iterator(im)): - frames.append(im.copy()) - if i != 0 or not default_image: - durations.append(im.info.get("duration", 0)) - frames[0].save( - test_file, - save_all=True, - default_image=default_image, - append_images=frames[1:], - duration=durations, - loop=loop, - ) - with Image.open(test_file) as im: - im.load() - self.assertEqual(im.info.get("loop"), loop) - im.seek(1) - self.assertEqual(im.info.get("duration"), 500.0) - im.seek(2) - self.assertEqual(im.info.get("duration"), 1000.0) - im.seek(3) - self.assertEqual(im.info.get("duration"), 500.0) - im.seek(4) - self.assertEqual(im.info.get("duration"), 1000.0) - - # test removal of duplicated frames - frame = Image.new("RGBA", (128, 64), (255, 0, 0, 255)) - frame.save(test_file, save_all=True, append_images=[frame], duration=[500, 250]) - with Image.open(test_file) as im: - im.load() - self.assertEqual(im.n_frames, 1) - self.assertEqual(im.info.get("duration"), 750) - - # This also tests reading unknown PNG chunks (fcTL and fdAT) in load_end - with Image.open("Tests/images/iss634.webp") as expected: - assert_image_similar(im, expected, 0.23) - - def test_apng_save_disposal(self): - test_file = self.tempfile("temp.png") - size = (128, 64) - red = Image.new("RGBA", size, (255, 0, 0, 255)) - green = Image.new("RGBA", size, (0, 255, 0, 255)) - transparent = Image.new("RGBA", size, (0, 0, 0, 0)) - - # test APNG_DISPOSE_OP_NONE - red.save( - test_file, - save_all=True, - append_images=[green, transparent], - disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, - ) - with Image.open(test_file) as im: - im.seek(2) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - # test APNG_DISPOSE_OP_BACKGROUND - disposal = [ - PngImagePlugin.APNG_DISPOSE_OP_NONE, - PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND, - PngImagePlugin.APNG_DISPOSE_OP_NONE, - ] - red.save( - test_file, - save_all=True, - append_images=[red, transparent], - disposal=disposal, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, - ) - with Image.open(test_file) as im: - im.seek(2) - self.assertEqual(im.getpixel((0, 0)), (0, 0, 0, 0)) - self.assertEqual(im.getpixel((64, 32)), (0, 0, 0, 0)) - - disposal = [ - PngImagePlugin.APNG_DISPOSE_OP_NONE, - PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND, - ] - red.save( - test_file, - save_all=True, - append_images=[green], - disposal=disposal, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, - ) - with Image.open(test_file) as im: - im.seek(1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - # test APNG_DISPOSE_OP_PREVIOUS - disposal = [ - PngImagePlugin.APNG_DISPOSE_OP_NONE, - PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, - PngImagePlugin.APNG_DISPOSE_OP_NONE, - ] - red.save( - test_file, - save_all=True, - append_images=[green, red, transparent], - default_image=True, - disposal=disposal, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, - ) - with Image.open(test_file) as im: - im.seek(3) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - disposal = [ - PngImagePlugin.APNG_DISPOSE_OP_NONE, - PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, - ] - red.save( - test_file, - save_all=True, - append_images=[green], - disposal=disposal, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, - ) - with Image.open(test_file) as im: - im.seek(1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - def test_apng_save_blend(self): - test_file = self.tempfile("temp.png") - size = (128, 64) - red = Image.new("RGBA", size, (255, 0, 0, 255)) - green = Image.new("RGBA", size, (0, 255, 0, 255)) - transparent = Image.new("RGBA", size, (0, 0, 0, 0)) - - # test APNG_BLEND_OP_SOURCE on solid color - blend = [ - PngImagePlugin.APNG_BLEND_OP_OVER, - PngImagePlugin.APNG_BLEND_OP_SOURCE, - ] - red.save( - test_file, - save_all=True, - append_images=[red, green], - default_image=True, - disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, - blend=blend, - ) - with Image.open(test_file) as im: - im.seek(2) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - - # test APNG_BLEND_OP_SOURCE on transparent color - blend = [ - PngImagePlugin.APNG_BLEND_OP_OVER, - PngImagePlugin.APNG_BLEND_OP_SOURCE, - ] - red.save( - test_file, - save_all=True, - append_images=[red, transparent], - default_image=True, - disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, - blend=blend, - ) - with Image.open(test_file) as im: - im.seek(2) - self.assertEqual(im.getpixel((0, 0)), (0, 0, 0, 0)) - self.assertEqual(im.getpixel((64, 32)), (0, 0, 0, 0)) - - # test APNG_BLEND_OP_OVER - red.save( - test_file, - save_all=True, - append_images=[green, transparent], - default_image=True, - disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, - ) - with Image.open(test_file) as im: - im.seek(1) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - im.seek(2) - self.assertEqual(im.getpixel((0, 0)), (0, 255, 0, 255)) - self.assertEqual(im.getpixel((64, 32)), (0, 255, 0, 255)) - @unittest.skipIf(is_win32(), "requires Unix or macOS") @skip_unless_feature("zlib")