Add APNG test cases
Includes tests for reading and writing APNG files. The tests for reading files are based on the APNG browser compatibility tests from https://philip.html5.org/tests/apng/tests.html (which is linked in the Tests section of https://wiki.mozilla.org/APNG_Specification)
BIN
Tests/images/apng/blend_op_over.png
Normal file
After Width: | Height: | Size: 579 B |
BIN
Tests/images/apng/blend_op_over_near_transparent.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
Tests/images/apng/blend_op_source_near_transparent.png
Normal file
After Width: | Height: | Size: 614 B |
BIN
Tests/images/apng/blend_op_source_solid.png
Normal file
After Width: | Height: | Size: 671 B |
BIN
Tests/images/apng/blend_op_source_transparent.png
Normal file
After Width: | Height: | Size: 534 B |
BIN
Tests/images/apng/chunk_actl_after_idat.png
Normal file
After Width: | Height: | Size: 470 B |
BIN
Tests/images/apng/chunk_multi_actl.png
Normal file
After Width: | Height: | Size: 490 B |
BIN
Tests/images/apng/chunk_no_actl.png
Normal file
After Width: | Height: | Size: 450 B |
BIN
Tests/images/apng/chunk_no_fctl.png
Normal file
After Width: | Height: | Size: 432 B |
BIN
Tests/images/apng/chunk_no_fdat.png
Normal file
After Width: | Height: | Size: 508 B |
BIN
Tests/images/apng/chunk_repeat_fctl.png
Normal file
After Width: | Height: | Size: 508 B |
BIN
Tests/images/apng/delay.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
Tests/images/apng/delay_round.png
Normal file
After Width: | Height: | Size: 646 B |
BIN
Tests/images/apng/delay_short_max.png
Normal file
After Width: | Height: | Size: 646 B |
BIN
Tests/images/apng/delay_zero_denom.png
Normal file
After Width: | Height: | Size: 646 B |
BIN
Tests/images/apng/delay_zero_numer.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
Tests/images/apng/dispose_op_background.png
Normal file
After Width: | Height: | Size: 572 B |
BIN
Tests/images/apng/dispose_op_background_before_region.png
Normal file
After Width: | Height: | Size: 327 B |
BIN
Tests/images/apng/dispose_op_background_final.png
Normal file
After Width: | Height: | Size: 508 B |
BIN
Tests/images/apng/dispose_op_background_region.png
Normal file
After Width: | Height: | Size: 492 B |
BIN
Tests/images/apng/dispose_op_none.png
Normal file
After Width: | Height: | Size: 617 B |
BIN
Tests/images/apng/dispose_op_none_region.png
Normal file
After Width: | Height: | Size: 613 B |
BIN
Tests/images/apng/dispose_op_previous.png
Normal file
After Width: | Height: | Size: 780 B |
BIN
Tests/images/apng/dispose_op_previous_final.png
Normal file
After Width: | Height: | Size: 508 B |
BIN
Tests/images/apng/dispose_op_previous_first.png
Normal file
After Width: | Height: | Size: 371 B |
BIN
Tests/images/apng/dispose_op_previous_region.png
Normal file
After Width: | Height: | Size: 677 B |
BIN
Tests/images/apng/fctl_actl.png
Normal file
After Width: | Height: | Size: 508 B |
BIN
Tests/images/apng/mode_16bit.png
Normal file
After Width: | Height: | Size: 915 B |
BIN
Tests/images/apng/mode_greyscale.png
Normal file
After Width: | Height: | Size: 331 B |
BIN
Tests/images/apng/mode_greyscale_alpha.png
Normal file
After Width: | Height: | Size: 668 B |
BIN
Tests/images/apng/mode_palette.png
Normal file
After Width: | Height: | Size: 262 B |
BIN
Tests/images/apng/mode_palette_1bit_alpha.png
Normal file
After Width: | Height: | Size: 276 B |
BIN
Tests/images/apng/mode_palette_alpha.png
Normal file
After Width: | Height: | Size: 308 B |
BIN
Tests/images/apng/num_plays.png
Normal file
After Width: | Height: | Size: 646 B |
BIN
Tests/images/apng/num_plays_1.png
Normal file
After Width: | Height: | Size: 646 B |
BIN
Tests/images/apng/sequence_fdat_fctl.png
Normal file
After Width: | Height: | Size: 671 B |
BIN
Tests/images/apng/sequence_gap.png
Normal file
After Width: | Height: | Size: 671 B |
BIN
Tests/images/apng/sequence_reorder.png
Normal file
After Width: | Height: | Size: 687 B |
BIN
Tests/images/apng/sequence_reorder_chunk.png
Normal file
After Width: | Height: | Size: 687 B |
BIN
Tests/images/apng/sequence_repeat.png
Normal file
After Width: | Height: | Size: 671 B |
BIN
Tests/images/apng/sequence_repeat_chunk.png
Normal file
After Width: | Height: | Size: 834 B |
BIN
Tests/images/apng/sequence_start.png
Normal file
After Width: | Height: | Size: 671 B |
BIN
Tests/images/apng/single_frame.png
Normal file
After Width: | Height: | Size: 307 B |
BIN
Tests/images/apng/single_frame_default.png
Normal file
After Width: | Height: | Size: 470 B |
BIN
Tests/images/apng/split_fdat.png
Normal file
After Width: | Height: | Size: 486 B |
BIN
Tests/images/apng/split_fdat_zero_chunk.png
Normal file
After Width: | Height: | Size: 502 B |
BIN
Tests/images/apng/syntax_num_frames_high.png
Normal file
After Width: | Height: | Size: 671 B |
BIN
Tests/images/apng/syntax_num_frames_invalid.png
Normal file
After Width: | Height: | Size: 470 B |
BIN
Tests/images/apng/syntax_num_frames_low.png
Normal file
After Width: | Height: | Size: 671 B |
BIN
Tests/images/apng/syntax_num_frames_zero.png
Normal file
After Width: | Height: | Size: 65 B |
BIN
Tests/images/apng/syntax_num_frames_zero_default.png
Normal file
After Width: | Height: | Size: 269 B |
|
@ -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")
|
||||
|
|