1
from __future__ import annotations
3
from pathlib import Path
7
from PIL import Image, ImageSequence, PngImagePlugin
13
def test_apng_basic() -> None:
14
with Image.open("Tests/images/apng/single_frame.png") as im:
15
assert not im.is_animated
16
assert im.n_frames == 1
17
assert im.get_format_mimetype() == "image/apng"
18
assert im.info.get("default_image") is None
19
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
20
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
22
with Image.open("Tests/images/apng/single_frame_default.png") as im:
24
assert im.n_frames == 2
25
assert im.get_format_mimetype() == "image/apng"
26
assert im.info.get("default_image")
27
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
28
assert im.getpixel((64, 32)) == (255, 0, 0, 255)
30
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
31
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
34
with pytest.raises(EOFError):
39
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
40
assert im.getpixel((64, 32)) == (255, 0, 0, 255)
42
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
43
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
46
@pytest.mark.parametrize(
48
("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"),
50
def test_apng_fdat(filename: str) -> None:
51
with Image.open(filename) as im:
52
im.seek(im.n_frames - 1)
53
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
54
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
57
def test_apng_dispose() -> None:
58
with Image.open("Tests/images/apng/dispose_op_none.png") as im:
59
im.seek(im.n_frames - 1)
60
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
61
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
63
with Image.open("Tests/images/apng/dispose_op_background.png") as im:
64
im.seek(im.n_frames - 1)
65
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
66
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
68
with Image.open("Tests/images/apng/dispose_op_background_final.png") as im:
69
im.seek(im.n_frames - 1)
70
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
71
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
73
with Image.open("Tests/images/apng/dispose_op_previous.png") as im:
74
im.seek(im.n_frames - 1)
75
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
76
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
78
with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im:
79
im.seek(im.n_frames - 1)
80
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
81
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
83
with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im:
84
im.seek(im.n_frames - 1)
85
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
86
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
89
def test_apng_dispose_region() -> None:
90
with Image.open("Tests/images/apng/dispose_op_none_region.png") as im:
91
im.seek(im.n_frames - 1)
92
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
93
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
95
with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im:
96
im.seek(im.n_frames - 1)
97
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
98
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
100
with Image.open("Tests/images/apng/dispose_op_background_region.png") as im:
101
im.seek(im.n_frames - 1)
102
assert im.getpixel((0, 0)) == (0, 0, 255, 255)
103
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
105
with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im:
106
im.seek(im.n_frames - 1)
107
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
108
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
111
def test_apng_dispose_op_previous_frame() -> None:
131
with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im:
132
im.seek(im.n_frames - 1)
133
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
136
def test_apng_dispose_op_background_p_mode() -> None:
137
with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im:
140
assert im.size == (128, 64)
143
def test_apng_blend() -> None:
144
with Image.open("Tests/images/apng/blend_op_source_solid.png") as im:
145
im.seek(im.n_frames - 1)
146
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
147
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
149
with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im:
150
im.seek(im.n_frames - 1)
151
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
152
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
154
with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im:
155
im.seek(im.n_frames - 1)
156
assert im.getpixel((0, 0)) == (0, 255, 0, 2)
157
assert im.getpixel((64, 32)) == (0, 255, 0, 2)
159
with Image.open("Tests/images/apng/blend_op_over.png") as im:
160
im.seek(im.n_frames - 1)
161
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
162
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
164
with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im:
165
im.seek(im.n_frames - 1)
166
assert im.getpixel((0, 0)) == (0, 255, 0, 97)
167
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
170
def test_apng_blend_transparency() -> None:
171
with Image.open("Tests/images/blend_transparency.png") as im:
173
assert im.getpixel((0, 0)) == (255, 0, 0)
176
def test_apng_chunk_order() -> None:
177
with Image.open("Tests/images/apng/fctl_actl.png") as im:
178
im.seek(im.n_frames - 1)
179
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
180
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
183
def test_apng_delay() -> None:
184
with Image.open("Tests/images/apng/delay.png") as im:
186
assert im.info.get("duration") == 500.0
188
assert im.info.get("duration") == 1000.0
190
assert im.info.get("duration") == 500.0
192
assert im.info.get("duration") == 1000.0
194
with Image.open("Tests/images/apng/delay_round.png") as im:
196
assert im.info.get("duration") == 500.0
198
assert im.info.get("duration") == 1000.0
200
with Image.open("Tests/images/apng/delay_short_max.png") as im:
202
assert im.info.get("duration") == 500.0
204
assert im.info.get("duration") == 1000.0
206
with Image.open("Tests/images/apng/delay_zero_denom.png") as im:
208
assert im.info.get("duration") == 500.0
210
assert im.info.get("duration") == 1000.0
212
with Image.open("Tests/images/apng/delay_zero_numer.png") as im:
214
assert im.info.get("duration") == 0.0
216
assert im.info.get("duration") == 0.0
218
assert im.info.get("duration") == 500.0
220
assert im.info.get("duration") == 1000.0
223
def test_apng_num_plays() -> None:
224
with Image.open("Tests/images/apng/num_plays.png") as im:
225
assert im.info.get("loop") == 0
227
with Image.open("Tests/images/apng/num_plays_1.png") as im:
228
assert im.info.get("loop") == 1
231
def test_apng_mode() -> None:
232
with Image.open("Tests/images/apng/mode_16bit.png") as im:
233
assert im.mode == "RGBA"
234
im.seek(im.n_frames - 1)
235
assert im.getpixel((0, 0)) == (0, 0, 128, 191)
236
assert im.getpixel((64, 32)) == (0, 0, 128, 191)
238
with Image.open("Tests/images/apng/mode_grayscale.png") as im:
239
assert im.mode == "L"
240
im.seek(im.n_frames - 1)
241
assert im.getpixel((0, 0)) == 128
242
assert im.getpixel((64, 32)) == 255
244
with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im:
245
assert im.mode == "LA"
246
im.seek(im.n_frames - 1)
247
assert im.getpixel((0, 0)) == (128, 191)
248
assert im.getpixel((64, 32)) == (128, 191)
250
with Image.open("Tests/images/apng/mode_palette.png") as im:
251
assert im.mode == "P"
252
im.seek(im.n_frames - 1)
253
im = im.convert("RGB")
254
assert im.getpixel((0, 0)) == (0, 255, 0)
255
assert im.getpixel((64, 32)) == (0, 255, 0)
257
with Image.open("Tests/images/apng/mode_palette_alpha.png") as im:
258
assert im.mode == "P"
259
im.seek(im.n_frames - 1)
260
im = im.convert("RGBA")
261
assert im.getpixel((0, 0)) == (255, 0, 0, 0)
262
assert im.getpixel((64, 32)) == (255, 0, 0, 0)
264
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
265
assert im.mode == "P"
266
im.seek(im.n_frames - 1)
267
im = im.convert("RGBA")
268
assert im.getpixel((0, 0)) == (0, 0, 255, 128)
269
assert im.getpixel((64, 32)) == (0, 0, 255, 128)
272
def test_apng_chunk_errors() -> None:
273
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
274
assert not im.is_animated
276
with pytest.warns(UserWarning):
277
with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
279
assert not im.is_animated
281
with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im:
282
assert not im.is_animated
284
with Image.open("Tests/images/apng/chunk_no_fctl.png") as im:
285
with pytest.raises(SyntaxError):
286
im.seek(im.n_frames - 1)
288
with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im:
289
with pytest.raises(SyntaxError):
290
im.seek(im.n_frames - 1)
292
with Image.open("Tests/images/apng/chunk_no_fdat.png") as im:
293
with pytest.raises(SyntaxError):
294
im.seek(im.n_frames - 1)
297
def test_apng_syntax_errors() -> None:
298
with pytest.warns(UserWarning):
299
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
300
assert not im.is_animated
301
with pytest.raises(OSError):
304
with pytest.warns(UserWarning):
305
with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im:
306
assert not im.is_animated
311
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
313
im.seek(im.n_frames - 1)
314
except Exception as e:
316
assert exception is None
318
with pytest.raises(OSError):
319
with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im:
320
im.seek(im.n_frames - 1)
323
with pytest.warns(UserWarning):
324
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
325
assert not im.is_animated
329
@pytest.mark.parametrize(
332
"sequence_start.png",
334
"sequence_repeat.png",
335
"sequence_repeat_chunk.png",
336
"sequence_reorder.png",
337
"sequence_reorder_chunk.png",
338
"sequence_fdat_fctl.png",
341
def test_apng_sequence_errors(test_file: str) -> None:
342
with pytest.raises(SyntaxError):
343
with Image.open(f"Tests/images/apng/{test_file}") as im:
344
im.seek(im.n_frames - 1)
348
def test_apng_save(tmp_path: Path) -> None:
349
with Image.open("Tests/images/apng/single_frame.png") as im:
350
test_file = str(tmp_path / "temp.png")
351
im.save(test_file, save_all=True)
353
with Image.open(test_file) as im:
355
assert not im.is_animated
356
assert im.n_frames == 1
357
assert im.get_format_mimetype() == "image/png"
358
assert im.info.get("default_image") is None
359
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
360
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
362
with Image.open("Tests/images/apng/single_frame_default.png") as im:
363
frames = [frame_im.copy() for frame_im in ImageSequence.Iterator(im)]
365
test_file, save_all=True, default_image=True, append_images=frames[1:]
368
with Image.open(test_file) as im:
370
assert im.is_animated
371
assert im.n_frames == 2
372
assert im.get_format_mimetype() == "image/apng"
373
assert im.info.get("default_image")
375
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
376
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
379
def test_apng_save_alpha(tmp_path: Path) -> None:
380
test_file = str(tmp_path / "temp.png")
382
im = Image.new("RGBA", (1, 1), (255, 0, 0, 255))
383
im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127))
384
im.save(test_file, save_all=True, append_images=[im2])
386
with Image.open(test_file) as reloaded:
387
assert reloaded.getpixel((0, 0)) == (255, 0, 0, 255)
390
assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127)
393
def test_apng_save_split_fdat(tmp_path: Path) -> None:
398
test_file = str(tmp_path / "temp.png")
399
with Image.open("Tests/images/old-style-jpeg-compression.png") as im:
400
frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))]
405
append_images=frames,
407
with Image.open(test_file) as im:
410
im.seek(im.n_frames - 1)
412
except Exception as e:
414
assert exception is None
417
def test_apng_save_duration_loop(tmp_path: Path) -> None:
418
test_file = str(tmp_path / "temp.png")
419
with Image.open("Tests/images/apng/delay.png") as im:
422
loop = im.info.get("loop")
423
default_image = im.info.get("default_image")
424
for i, frame_im in enumerate(ImageSequence.Iterator(im)):
425
frames.append(frame_im.copy())
426
if i != 0 or not default_image:
427
durations.append(frame_im.info.get("duration", 0))
431
default_image=default_image,
432
append_images=frames[1:],
437
with Image.open(test_file) as im:
439
assert im.info.get("loop") == loop
441
assert im.info.get("duration") == 500.0
443
assert im.info.get("duration") == 1000.0
445
assert im.info.get("duration") == 500.0
447
assert im.info.get("duration") == 1000.0
450
frame = Image.new("RGBA", (128, 64), (255, 0, 0, 255))
452
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
454
with Image.open(test_file) as im:
455
assert im.n_frames == 1
456
assert "duration" not in im.info
458
different_frame = Image.new("RGBA", (128, 64))
462
append_images=[frame, different_frame],
463
duration=[500, 100, 150],
465
with Image.open(test_file) as im:
466
assert im.n_frames == 2
467
assert im.info["duration"] == 600
470
assert im.info["duration"] == 150
473
frame.info["duration"] = 300
474
frame.save(test_file, save_all=True, append_images=[frame, different_frame])
475
with Image.open(test_file) as im:
476
assert im.n_frames == 2
477
assert im.info["duration"] == 600
480
def test_apng_save_disposal(tmp_path: Path) -> None:
481
test_file = str(tmp_path / "temp.png")
483
red = Image.new("RGBA", size, (255, 0, 0, 255))
484
green = Image.new("RGBA", size, (0, 255, 0, 255))
485
transparent = Image.new("RGBA", size, (0, 0, 0, 0))
491
append_images=[green, transparent],
492
disposal=PngImagePlugin.Disposal.OP_NONE,
493
blend=PngImagePlugin.Blend.OP_OVER,
495
with Image.open(test_file) as im:
497
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
498
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
502
PngImagePlugin.Disposal.OP_NONE,
503
PngImagePlugin.Disposal.OP_BACKGROUND,
504
PngImagePlugin.Disposal.OP_NONE,
509
append_images=[red, transparent],
511
blend=PngImagePlugin.Blend.OP_OVER,
513
with Image.open(test_file) as im:
515
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
516
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
519
PngImagePlugin.Disposal.OP_NONE,
520
PngImagePlugin.Disposal.OP_BACKGROUND,
525
append_images=[green],
527
blend=PngImagePlugin.Blend.OP_OVER,
529
with Image.open(test_file) as im:
531
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
532
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
536
PngImagePlugin.Disposal.OP_NONE,
537
PngImagePlugin.Disposal.OP_PREVIOUS,
538
PngImagePlugin.Disposal.OP_NONE,
543
append_images=[green, red, transparent],
546
blend=PngImagePlugin.Blend.OP_OVER,
548
with Image.open(test_file) as im:
550
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
551
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
554
PngImagePlugin.Disposal.OP_NONE,
555
PngImagePlugin.Disposal.OP_PREVIOUS,
560
append_images=[green],
562
blend=PngImagePlugin.Blend.OP_OVER,
564
with Image.open(test_file) as im:
566
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
567
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
570
red.info["disposal"] = PngImagePlugin.Disposal.OP_BACKGROUND
574
append_images=[Image.new("RGBA", (10, 10), (0, 255, 0, 255))],
576
with Image.open(test_file) as im:
578
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
581
def test_apng_save_disposal_previous(tmp_path: Path) -> None:
582
test_file = str(tmp_path / "temp.png")
584
blue = Image.new("RGBA", size, (0, 0, 255, 255))
585
red = Image.new("RGBA", size, (255, 0, 0, 255))
586
green = Image.new("RGBA", size, (0, 255, 0, 255))
592
append_images=[red, green],
593
disposal=PngImagePlugin.Disposal.OP_PREVIOUS,
595
with Image.open(test_file) as im:
596
assert im.getpixel((0, 0)) == (0, 0, 255, 255)
599
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
600
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
603
def test_apng_save_blend(tmp_path: Path) -> None:
604
test_file = str(tmp_path / "temp.png")
606
red = Image.new("RGBA", size, (255, 0, 0, 255))
607
green = Image.new("RGBA", size, (0, 255, 0, 255))
608
transparent = Image.new("RGBA", size, (0, 0, 0, 0))
612
PngImagePlugin.Blend.OP_OVER,
613
PngImagePlugin.Blend.OP_SOURCE,
618
append_images=[red, green],
620
disposal=PngImagePlugin.Disposal.OP_NONE,
623
with Image.open(test_file) as im:
625
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
626
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
630
PngImagePlugin.Blend.OP_OVER,
631
PngImagePlugin.Blend.OP_SOURCE,
636
append_images=[red, transparent],
638
disposal=PngImagePlugin.Disposal.OP_NONE,
641
with Image.open(test_file) as im:
643
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
644
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
650
append_images=[green, transparent],
652
disposal=PngImagePlugin.Disposal.OP_NONE,
653
blend=PngImagePlugin.Blend.OP_OVER,
655
with Image.open(test_file) as im:
657
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
658
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
660
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
661
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
664
red.info["blend"] = PngImagePlugin.Blend.OP_OVER
665
red.save(test_file, save_all=True, append_images=[green, transparent])
666
with Image.open(test_file) as im:
668
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
671
def test_apng_save_size(tmp_path: Path) -> None:
672
test_file = str(tmp_path / "temp.png")
674
im = Image.new("L", (100, 100))
675
im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))])
677
with Image.open(test_file) as reloaded:
678
assert reloaded.size == (200, 200)
681
def test_seek_after_close() -> None:
682
im = Image.open("Tests/images/apng/delay.png")
686
with pytest.raises(ValueError):
690
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
691
@pytest.mark.parametrize("default_image", (True, False))
692
@pytest.mark.parametrize("duplicate", (True, False))
693
def test_different_modes_in_later_frames(
694
mode: str, default_image: bool, duplicate: bool, tmp_path: Path
696
test_file = str(tmp_path / "temp.png")
698
im = Image.new("L", (1, 1))
702
default_image=default_image,
703
append_images=[im.convert(mode) if duplicate else Image.new(mode, (1, 1), 1)],
705
with Image.open(test_file) as reloaded:
706
assert reloaded.mode == mode
709
def test_different_durations(tmp_path: Path) -> None:
710
test_file = str(tmp_path / "temp.png")
712
with Image.open("Tests/images/apng/different_durations.png") as im:
715
assert im.info["duration"] == 4000
718
assert im.info["duration"] == 1000
720
im.save(test_file, save_all=True)
722
with Image.open(test_file) as reloaded:
723
assert reloaded.info["duration"] == 4000
726
assert reloaded.info["duration"] == 1000