Move apng tests into test_file_apng.py

This commit is contained in:
Peter Rowlands 2020-01-10 14:28:24 +09:00 committed by Andrew Murray
parent 0b536fb599
commit 0f84fa7707
2 changed files with 540 additions and 536 deletions

539
Tests/test_file_apng.py Normal file
View File

@ -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))

View File

@ -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")