diff --git a/Tests/images/apng/blend_op_over.png b/Tests/images/apng/blend_op_over.png new file mode 100644 index 000000000..3fe0f4ca7 Binary files /dev/null and b/Tests/images/apng/blend_op_over.png differ diff --git a/Tests/images/apng/blend_op_over_near_transparent.png b/Tests/images/apng/blend_op_over_near_transparent.png new file mode 100644 index 000000000..3ee5fe3bf Binary files /dev/null and b/Tests/images/apng/blend_op_over_near_transparent.png differ diff --git a/Tests/images/apng/blend_op_source_near_transparent.png b/Tests/images/apng/blend_op_source_near_transparent.png new file mode 100644 index 000000000..1af30f81f Binary files /dev/null and b/Tests/images/apng/blend_op_source_near_transparent.png differ diff --git a/Tests/images/apng/blend_op_source_solid.png b/Tests/images/apng/blend_op_source_solid.png new file mode 100644 index 000000000..d90c54967 Binary files /dev/null and b/Tests/images/apng/blend_op_source_solid.png differ diff --git a/Tests/images/apng/blend_op_source_transparent.png b/Tests/images/apng/blend_op_source_transparent.png new file mode 100644 index 000000000..0f290fd7f Binary files /dev/null and b/Tests/images/apng/blend_op_source_transparent.png differ diff --git a/Tests/images/apng/chunk_actl_after_idat.png b/Tests/images/apng/chunk_actl_after_idat.png new file mode 100644 index 000000000..296a29d4c Binary files /dev/null and b/Tests/images/apng/chunk_actl_after_idat.png differ diff --git a/Tests/images/apng/chunk_multi_actl.png b/Tests/images/apng/chunk_multi_actl.png new file mode 100644 index 000000000..213f88549 Binary files /dev/null and b/Tests/images/apng/chunk_multi_actl.png differ diff --git a/Tests/images/apng/chunk_no_actl.png b/Tests/images/apng/chunk_no_actl.png new file mode 100644 index 000000000..5b68c7b44 Binary files /dev/null and b/Tests/images/apng/chunk_no_actl.png differ diff --git a/Tests/images/apng/chunk_no_fctl.png b/Tests/images/apng/chunk_no_fctl.png new file mode 100644 index 000000000..58ca904ab Binary files /dev/null and b/Tests/images/apng/chunk_no_fctl.png differ diff --git a/Tests/images/apng/chunk_no_fdat.png b/Tests/images/apng/chunk_no_fdat.png new file mode 100644 index 000000000..af42766b5 Binary files /dev/null and b/Tests/images/apng/chunk_no_fdat.png differ diff --git a/Tests/images/apng/chunk_repeat_fctl.png b/Tests/images/apng/chunk_repeat_fctl.png new file mode 100644 index 000000000..a5779855f Binary files /dev/null and b/Tests/images/apng/chunk_repeat_fctl.png differ diff --git a/Tests/images/apng/delay.png b/Tests/images/apng/delay.png new file mode 100644 index 000000000..64cceaae8 Binary files /dev/null and b/Tests/images/apng/delay.png differ diff --git a/Tests/images/apng/delay_round.png b/Tests/images/apng/delay_round.png new file mode 100644 index 000000000..3f082665c Binary files /dev/null and b/Tests/images/apng/delay_round.png differ diff --git a/Tests/images/apng/delay_short_max.png b/Tests/images/apng/delay_short_max.png new file mode 100644 index 000000000..99d53b718 Binary files /dev/null and b/Tests/images/apng/delay_short_max.png differ diff --git a/Tests/images/apng/delay_zero_denom.png b/Tests/images/apng/delay_zero_denom.png new file mode 100644 index 000000000..bad60c767 Binary files /dev/null and b/Tests/images/apng/delay_zero_denom.png differ diff --git a/Tests/images/apng/delay_zero_numer.png b/Tests/images/apng/delay_zero_numer.png new file mode 100644 index 000000000..a029a959b Binary files /dev/null and b/Tests/images/apng/delay_zero_numer.png differ diff --git a/Tests/images/apng/dispose_op_background.png b/Tests/images/apng/dispose_op_background.png new file mode 100644 index 000000000..b63ebc0b3 Binary files /dev/null and b/Tests/images/apng/dispose_op_background.png differ diff --git a/Tests/images/apng/dispose_op_background_before_region.png b/Tests/images/apng/dispose_op_background_before_region.png new file mode 100644 index 000000000..427b829a0 Binary files /dev/null and b/Tests/images/apng/dispose_op_background_before_region.png differ diff --git a/Tests/images/apng/dispose_op_background_final.png b/Tests/images/apng/dispose_op_background_final.png new file mode 100644 index 000000000..77694ff1d Binary files /dev/null and b/Tests/images/apng/dispose_op_background_final.png differ diff --git a/Tests/images/apng/dispose_op_background_region.png b/Tests/images/apng/dispose_op_background_region.png new file mode 100644 index 000000000..05948d44a Binary files /dev/null and b/Tests/images/apng/dispose_op_background_region.png differ diff --git a/Tests/images/apng/dispose_op_none.png b/Tests/images/apng/dispose_op_none.png new file mode 100644 index 000000000..3094c1d23 Binary files /dev/null and b/Tests/images/apng/dispose_op_none.png differ diff --git a/Tests/images/apng/dispose_op_none_region.png b/Tests/images/apng/dispose_op_none_region.png new file mode 100644 index 000000000..4e1dbf77e Binary files /dev/null and b/Tests/images/apng/dispose_op_none_region.png differ diff --git a/Tests/images/apng/dispose_op_previous.png b/Tests/images/apng/dispose_op_previous.png new file mode 100644 index 000000000..1c15f132f Binary files /dev/null and b/Tests/images/apng/dispose_op_previous.png differ diff --git a/Tests/images/apng/dispose_op_previous_final.png b/Tests/images/apng/dispose_op_previous_final.png new file mode 100644 index 000000000..858f6f038 Binary files /dev/null and b/Tests/images/apng/dispose_op_previous_final.png differ diff --git a/Tests/images/apng/dispose_op_previous_first.png b/Tests/images/apng/dispose_op_previous_first.png new file mode 100644 index 000000000..3f9b3cfae Binary files /dev/null and b/Tests/images/apng/dispose_op_previous_first.png differ diff --git a/Tests/images/apng/dispose_op_previous_region.png b/Tests/images/apng/dispose_op_previous_region.png new file mode 100644 index 000000000..f326afa5c Binary files /dev/null and b/Tests/images/apng/dispose_op_previous_region.png differ diff --git a/Tests/images/apng/fctl_actl.png b/Tests/images/apng/fctl_actl.png new file mode 100644 index 000000000..d0418ddd7 Binary files /dev/null and b/Tests/images/apng/fctl_actl.png differ diff --git a/Tests/images/apng/mode_16bit.png b/Tests/images/apng/mode_16bit.png new file mode 100644 index 000000000..1210e3737 Binary files /dev/null and b/Tests/images/apng/mode_16bit.png differ diff --git a/Tests/images/apng/mode_greyscale.png b/Tests/images/apng/mode_greyscale.png new file mode 100644 index 000000000..29ed7d1ea Binary files /dev/null and b/Tests/images/apng/mode_greyscale.png differ diff --git a/Tests/images/apng/mode_greyscale_alpha.png b/Tests/images/apng/mode_greyscale_alpha.png new file mode 100644 index 000000000..f9307f635 Binary files /dev/null and b/Tests/images/apng/mode_greyscale_alpha.png differ diff --git a/Tests/images/apng/mode_palette.png b/Tests/images/apng/mode_palette.png new file mode 100644 index 000000000..11ccfb6cb Binary files /dev/null and b/Tests/images/apng/mode_palette.png differ diff --git a/Tests/images/apng/mode_palette_1bit_alpha.png b/Tests/images/apng/mode_palette_1bit_alpha.png new file mode 100644 index 000000000..e95425ac1 Binary files /dev/null and b/Tests/images/apng/mode_palette_1bit_alpha.png differ diff --git a/Tests/images/apng/mode_palette_alpha.png b/Tests/images/apng/mode_palette_alpha.png new file mode 100644 index 000000000..f3c4c9f9e Binary files /dev/null and b/Tests/images/apng/mode_palette_alpha.png differ diff --git a/Tests/images/apng/num_plays.png b/Tests/images/apng/num_plays.png new file mode 100644 index 000000000..4d76802e4 Binary files /dev/null and b/Tests/images/apng/num_plays.png differ diff --git a/Tests/images/apng/num_plays_1.png b/Tests/images/apng/num_plays_1.png new file mode 100644 index 000000000..fb2539430 Binary files /dev/null and b/Tests/images/apng/num_plays_1.png differ diff --git a/Tests/images/apng/sequence_fdat_fctl.png b/Tests/images/apng/sequence_fdat_fctl.png new file mode 100644 index 000000000..29ac75e16 Binary files /dev/null and b/Tests/images/apng/sequence_fdat_fctl.png differ diff --git a/Tests/images/apng/sequence_gap.png b/Tests/images/apng/sequence_gap.png new file mode 100644 index 000000000..25dd9bcd8 Binary files /dev/null and b/Tests/images/apng/sequence_gap.png differ diff --git a/Tests/images/apng/sequence_reorder.png b/Tests/images/apng/sequence_reorder.png new file mode 100644 index 000000000..dc78e9bb1 Binary files /dev/null and b/Tests/images/apng/sequence_reorder.png differ diff --git a/Tests/images/apng/sequence_reorder_chunk.png b/Tests/images/apng/sequence_reorder_chunk.png new file mode 100644 index 000000000..5d951ffe2 Binary files /dev/null and b/Tests/images/apng/sequence_reorder_chunk.png differ diff --git a/Tests/images/apng/sequence_repeat.png b/Tests/images/apng/sequence_repeat.png new file mode 100644 index 000000000..d5cf83f9f Binary files /dev/null and b/Tests/images/apng/sequence_repeat.png differ diff --git a/Tests/images/apng/sequence_repeat_chunk.png b/Tests/images/apng/sequence_repeat_chunk.png new file mode 100644 index 000000000..27d1d3eb5 Binary files /dev/null and b/Tests/images/apng/sequence_repeat_chunk.png differ diff --git a/Tests/images/apng/sequence_start.png b/Tests/images/apng/sequence_start.png new file mode 100644 index 000000000..5e040743a Binary files /dev/null and b/Tests/images/apng/sequence_start.png differ diff --git a/Tests/images/apng/single_frame.png b/Tests/images/apng/single_frame.png new file mode 100644 index 000000000..0cd5bea85 Binary files /dev/null and b/Tests/images/apng/single_frame.png differ diff --git a/Tests/images/apng/single_frame_default.png b/Tests/images/apng/single_frame_default.png new file mode 100644 index 000000000..db7581fbd Binary files /dev/null and b/Tests/images/apng/single_frame_default.png differ diff --git a/Tests/images/apng/split_fdat.png b/Tests/images/apng/split_fdat.png new file mode 100644 index 000000000..2dc58b929 Binary files /dev/null and b/Tests/images/apng/split_fdat.png differ diff --git a/Tests/images/apng/split_fdat_zero_chunk.png b/Tests/images/apng/split_fdat_zero_chunk.png new file mode 100644 index 000000000..14a76d9d6 Binary files /dev/null and b/Tests/images/apng/split_fdat_zero_chunk.png differ diff --git a/Tests/images/apng/syntax_num_frames_high.png b/Tests/images/apng/syntax_num_frames_high.png new file mode 100644 index 000000000..bba9cdfd5 Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_high.png differ diff --git a/Tests/images/apng/syntax_num_frames_invalid.png b/Tests/images/apng/syntax_num_frames_invalid.png new file mode 100644 index 000000000..ca7b13ab8 Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_invalid.png differ diff --git a/Tests/images/apng/syntax_num_frames_low.png b/Tests/images/apng/syntax_num_frames_low.png new file mode 100644 index 000000000..6f895f91d Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_low.png differ diff --git a/Tests/images/apng/syntax_num_frames_zero.png b/Tests/images/apng/syntax_num_frames_zero.png new file mode 100644 index 000000000..0cb7ea36e Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_zero.png differ diff --git a/Tests/images/apng/syntax_num_frames_zero_default.png b/Tests/images/apng/syntax_num_frames_zero_default.png new file mode 100644 index 000000000..89f2b75e2 Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_zero_default.png differ diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index c434d836c..8e7ba8e12 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, PngImagePlugin +from PIL import Image, ImageFile, ImageSequence, PngImagePlugin from .helper import ( PillowLeakTestCase, @@ -18,6 +18,7 @@ from .helper import ( skip_unless_feature, ) + # sample png stream TEST_PNG_FILE = "Tests/images/hopper.png" @@ -624,16 +625,521 @@ class TestFilePng(PillowTestCase): with Image.open(test_file) as reloaded: assert reloaded.info["exif"] == b"Exif\x00\x00exifstring" - @skip_unless_feature("webp") - @skip_unless_feature("webp_anim") - def test_apng(self): - with Image.open("Tests/images/iss634.apng") as im: - assert im.get_format_mimetype() == "image/apng" + # 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_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")