Pillow

Форк
0
/
test_file_apng.py 
726 строк · 23.7 Кб
1
from __future__ import annotations
2

3
from pathlib import Path
4

5
import pytest
6

7
from PIL import Image, ImageSequence, PngImagePlugin
8

9

10
# APNG browser support tests and fixtures via:
11
# https://philip.html5.org/tests/apng/tests.html
12
# (referenced from https://wiki.mozilla.org/APNG_Specification)
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)
21

22
    with Image.open("Tests/images/apng/single_frame_default.png") as im:
23
        assert im.is_animated
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)
29
        im.seek(1)
30
        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
31
        assert im.getpixel((64, 32)) == (0, 255, 0, 255)
32

33
        # test out of bounds seek
34
        with pytest.raises(EOFError):
35
            im.seek(2)
36

37
        # test rewind support
38
        im.seek(0)
39
        assert im.getpixel((0, 0)) == (255, 0, 0, 255)
40
        assert im.getpixel((64, 32)) == (255, 0, 0, 255)
41
        im.seek(1)
42
        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
43
        assert im.getpixel((64, 32)) == (0, 255, 0, 255)
44

45

46
@pytest.mark.parametrize(
47
    "filename",
48
    ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"),
49
)
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)
55

56

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

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

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

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

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

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

88

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

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

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

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

110

111
def test_apng_dispose_op_previous_frame() -> None:
112
    # Test that the dispose settings being used are from the previous frame
113
    #
114
    # Image created with:
115
    # red = Image.new("RGBA", (128, 64), (255, 0, 0, 255))
116
    # green = red.copy()
117
    # green.paste(Image.new("RGBA", (64, 32), (0, 255, 0, 255)))
118
    # blue = red.copy()
119
    # blue.paste(Image.new("RGBA", (64, 32), (0, 255, 0, 255)), (64, 32))
120
    #
121
    # red.save(
122
    #     "Tests/images/apng/dispose_op_previous_frame.png",
123
    #     save_all=True,
124
    #     append_images=[green, blue],
125
    #     disposal=[
126
    #         PngImagePlugin.Disposal.OP_NONE,
127
    #         PngImagePlugin.Disposal.OP_PREVIOUS,
128
    #         PngImagePlugin.Disposal.OP_PREVIOUS
129
    #     ],
130
    # )
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)
134

135

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:
138
        im.seek(1)
139
        im.load()
140
        assert im.size == (128, 64)
141

142

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

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

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

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

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

169

170
def test_apng_blend_transparency() -> None:
171
    with Image.open("Tests/images/blend_transparency.png") as im:
172
        im.seek(1)
173
        assert im.getpixel((0, 0)) == (255, 0, 0)
174

175

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

182

183
def test_apng_delay() -> None:
184
    with Image.open("Tests/images/apng/delay.png") as im:
185
        im.seek(1)
186
        assert im.info.get("duration") == 500.0
187
        im.seek(2)
188
        assert im.info.get("duration") == 1000.0
189
        im.seek(3)
190
        assert im.info.get("duration") == 500.0
191
        im.seek(4)
192
        assert im.info.get("duration") == 1000.0
193

194
    with Image.open("Tests/images/apng/delay_round.png") as im:
195
        im.seek(1)
196
        assert im.info.get("duration") == 500.0
197
        im.seek(2)
198
        assert im.info.get("duration") == 1000.0
199

200
    with Image.open("Tests/images/apng/delay_short_max.png") as im:
201
        im.seek(1)
202
        assert im.info.get("duration") == 500.0
203
        im.seek(2)
204
        assert im.info.get("duration") == 1000.0
205

206
    with Image.open("Tests/images/apng/delay_zero_denom.png") as im:
207
        im.seek(1)
208
        assert im.info.get("duration") == 500.0
209
        im.seek(2)
210
        assert im.info.get("duration") == 1000.0
211

212
    with Image.open("Tests/images/apng/delay_zero_numer.png") as im:
213
        im.seek(1)
214
        assert im.info.get("duration") == 0.0
215
        im.seek(2)
216
        assert im.info.get("duration") == 0.0
217
        im.seek(3)
218
        assert im.info.get("duration") == 500.0
219
        im.seek(4)
220
        assert im.info.get("duration") == 1000.0
221

222

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
226

227
    with Image.open("Tests/images/apng/num_plays_1.png") as im:
228
        assert im.info.get("loop") == 1
229

230

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

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
243

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

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

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

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

271

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
275

276
    with pytest.warns(UserWarning):
277
        with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
278
            im.load()
279
        assert not im.is_animated
280

281
    with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im:
282
        assert not im.is_animated
283

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

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

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

296

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):
302
                im.load()
303

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
307
            im.load()
308

309
    # we can handle this case gracefully
310
    exception = None
311
    with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
312
        try:
313
            im.seek(im.n_frames - 1)
314
        except Exception as e:
315
            exception = e
316
        assert exception is None
317

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)
321
            im.load()
322

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
326
            im.load()
327

328

329
@pytest.mark.parametrize(
330
    "test_file",
331
    (
332
        "sequence_start.png",
333
        "sequence_gap.png",
334
        "sequence_repeat.png",
335
        "sequence_repeat_chunk.png",
336
        "sequence_reorder.png",
337
        "sequence_reorder_chunk.png",
338
        "sequence_fdat_fctl.png",
339
    ),
340
)
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)
345
            im.load()
346

347

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

353
    with Image.open(test_file) as im:
354
        im.load()
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)
361

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)]
364
        frames[0].save(
365
            test_file, save_all=True, default_image=True, append_images=frames[1:]
366
        )
367

368
    with Image.open(test_file) as im:
369
        im.load()
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")
374
        im.seek(1)
375
        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
376
        assert im.getpixel((64, 32)) == (0, 255, 0, 255)
377

378

379
def test_apng_save_alpha(tmp_path: Path) -> None:
380
    test_file = str(tmp_path / "temp.png")
381

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])
385

386
    with Image.open(test_file) as reloaded:
387
        assert reloaded.getpixel((0, 0)) == (255, 0, 0, 255)
388

389
        reloaded.seek(1)
390
        assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127)
391

392

393
def test_apng_save_split_fdat(tmp_path: Path) -> None:
394
    # test to make sure we do not generate sequence errors when writing
395
    # frames with image data spanning multiple fdAT chunks (in this case
396
    # both the default image and first animation frame will span multiple
397
    # data chunks)
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))]
401
        im.save(
402
            test_file,
403
            save_all=True,
404
            default_image=True,
405
            append_images=frames,
406
        )
407
    with Image.open(test_file) as im:
408
        exception = None
409
        try:
410
            im.seek(im.n_frames - 1)
411
            im.load()
412
        except Exception as e:
413
            exception = e
414
        assert exception is None
415

416

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:
420
        frames = []
421
        durations = []
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))
428
        frames[0].save(
429
            test_file,
430
            save_all=True,
431
            default_image=default_image,
432
            append_images=frames[1:],
433
            duration=durations,
434
            loop=loop,
435
        )
436

437
    with Image.open(test_file) as im:
438
        im.load()
439
        assert im.info.get("loop") == loop
440
        im.seek(1)
441
        assert im.info.get("duration") == 500.0
442
        im.seek(2)
443
        assert im.info.get("duration") == 1000.0
444
        im.seek(3)
445
        assert im.info.get("duration") == 500.0
446
        im.seek(4)
447
        assert im.info.get("duration") == 1000.0
448

449
    # test removal of duplicated frames
450
    frame = Image.new("RGBA", (128, 64), (255, 0, 0, 255))
451
    frame.save(
452
        test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
453
    )
454
    with Image.open(test_file) as im:
455
        assert im.n_frames == 1
456
        assert "duration" not in im.info
457

458
    different_frame = Image.new("RGBA", (128, 64))
459
    frame.save(
460
        test_file,
461
        save_all=True,
462
        append_images=[frame, different_frame],
463
        duration=[500, 100, 150],
464
    )
465
    with Image.open(test_file) as im:
466
        assert im.n_frames == 2
467
        assert im.info["duration"] == 600
468

469
        im.seek(1)
470
        assert im.info["duration"] == 150
471

472
    # test info duration
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
478

479

480
def test_apng_save_disposal(tmp_path: Path) -> None:
481
    test_file = str(tmp_path / "temp.png")
482
    size = (128, 64)
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))
486

487
    # test OP_NONE
488
    red.save(
489
        test_file,
490
        save_all=True,
491
        append_images=[green, transparent],
492
        disposal=PngImagePlugin.Disposal.OP_NONE,
493
        blend=PngImagePlugin.Blend.OP_OVER,
494
    )
495
    with Image.open(test_file) as im:
496
        im.seek(2)
497
        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
498
        assert im.getpixel((64, 32)) == (0, 255, 0, 255)
499

500
    # test OP_BACKGROUND
501
    disposal = [
502
        PngImagePlugin.Disposal.OP_NONE,
503
        PngImagePlugin.Disposal.OP_BACKGROUND,
504
        PngImagePlugin.Disposal.OP_NONE,
505
    ]
506
    red.save(
507
        test_file,
508
        save_all=True,
509
        append_images=[red, transparent],
510
        disposal=disposal,
511
        blend=PngImagePlugin.Blend.OP_OVER,
512
    )
513
    with Image.open(test_file) as im:
514
        im.seek(2)
515
        assert im.getpixel((0, 0)) == (0, 0, 0, 0)
516
        assert im.getpixel((64, 32)) == (0, 0, 0, 0)
517

518
    disposal = [
519
        PngImagePlugin.Disposal.OP_NONE,
520
        PngImagePlugin.Disposal.OP_BACKGROUND,
521
    ]
522
    red.save(
523
        test_file,
524
        save_all=True,
525
        append_images=[green],
526
        disposal=disposal,
527
        blend=PngImagePlugin.Blend.OP_OVER,
528
    )
529
    with Image.open(test_file) as im:
530
        im.seek(1)
531
        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
532
        assert im.getpixel((64, 32)) == (0, 255, 0, 255)
533

534
    # test OP_PREVIOUS
535
    disposal = [
536
        PngImagePlugin.Disposal.OP_NONE,
537
        PngImagePlugin.Disposal.OP_PREVIOUS,
538
        PngImagePlugin.Disposal.OP_NONE,
539
    ]
540
    red.save(
541
        test_file,
542
        save_all=True,
543
        append_images=[green, red, transparent],
544
        default_image=True,
545
        disposal=disposal,
546
        blend=PngImagePlugin.Blend.OP_OVER,
547
    )
548
    with Image.open(test_file) as im:
549
        im.seek(3)
550
        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
551
        assert im.getpixel((64, 32)) == (0, 255, 0, 255)
552

553
    disposal = [
554
        PngImagePlugin.Disposal.OP_NONE,
555
        PngImagePlugin.Disposal.OP_PREVIOUS,
556
    ]
557
    red.save(
558
        test_file,
559
        save_all=True,
560
        append_images=[green],
561
        disposal=disposal,
562
        blend=PngImagePlugin.Blend.OP_OVER,
563
    )
564
    with Image.open(test_file) as im:
565
        im.seek(1)
566
        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
567
        assert im.getpixel((64, 32)) == (0, 255, 0, 255)
568

569
    # test info disposal
570
    red.info["disposal"] = PngImagePlugin.Disposal.OP_BACKGROUND
571
    red.save(
572
        test_file,
573
        save_all=True,
574
        append_images=[Image.new("RGBA", (10, 10), (0, 255, 0, 255))],
575
    )
576
    with Image.open(test_file) as im:
577
        im.seek(1)
578
        assert im.getpixel((64, 32)) == (0, 0, 0, 0)
579

580

581
def test_apng_save_disposal_previous(tmp_path: Path) -> None:
582
    test_file = str(tmp_path / "temp.png")
583
    size = (128, 64)
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))
587

588
    # test OP_NONE
589
    blue.save(
590
        test_file,
591
        save_all=True,
592
        append_images=[red, green],
593
        disposal=PngImagePlugin.Disposal.OP_PREVIOUS,
594
    )
595
    with Image.open(test_file) as im:
596
        assert im.getpixel((0, 0)) == (0, 0, 255, 255)
597

598
        im.seek(2)
599
        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
600
        assert im.getpixel((64, 32)) == (0, 255, 0, 255)
601

602

603
def test_apng_save_blend(tmp_path: Path) -> None:
604
    test_file = str(tmp_path / "temp.png")
605
    size = (128, 64)
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))
609

610
    # test OP_SOURCE on solid color
611
    blend = [
612
        PngImagePlugin.Blend.OP_OVER,
613
        PngImagePlugin.Blend.OP_SOURCE,
614
    ]
615
    red.save(
616
        test_file,
617
        save_all=True,
618
        append_images=[red, green],
619
        default_image=True,
620
        disposal=PngImagePlugin.Disposal.OP_NONE,
621
        blend=blend,
622
    )
623
    with Image.open(test_file) as im:
624
        im.seek(2)
625
        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
626
        assert im.getpixel((64, 32)) == (0, 255, 0, 255)
627

628
    # test OP_SOURCE on transparent color
629
    blend = [
630
        PngImagePlugin.Blend.OP_OVER,
631
        PngImagePlugin.Blend.OP_SOURCE,
632
    ]
633
    red.save(
634
        test_file,
635
        save_all=True,
636
        append_images=[red, transparent],
637
        default_image=True,
638
        disposal=PngImagePlugin.Disposal.OP_NONE,
639
        blend=blend,
640
    )
641
    with Image.open(test_file) as im:
642
        im.seek(2)
643
        assert im.getpixel((0, 0)) == (0, 0, 0, 0)
644
        assert im.getpixel((64, 32)) == (0, 0, 0, 0)
645

646
    # test OP_OVER
647
    red.save(
648
        test_file,
649
        save_all=True,
650
        append_images=[green, transparent],
651
        default_image=True,
652
        disposal=PngImagePlugin.Disposal.OP_NONE,
653
        blend=PngImagePlugin.Blend.OP_OVER,
654
    )
655
    with Image.open(test_file) as im:
656
        im.seek(1)
657
        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
658
        assert im.getpixel((64, 32)) == (0, 255, 0, 255)
659
        im.seek(2)
660
        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
661
        assert im.getpixel((64, 32)) == (0, 255, 0, 255)
662

663
    # test info blend
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:
667
        im.seek(2)
668
        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
669

670

671
def test_apng_save_size(tmp_path: Path) -> None:
672
    test_file = str(tmp_path / "temp.png")
673

674
    im = Image.new("L", (100, 100))
675
    im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))])
676

677
    with Image.open(test_file) as reloaded:
678
        assert reloaded.size == (200, 200)
679

680

681
def test_seek_after_close() -> None:
682
    im = Image.open("Tests/images/apng/delay.png")
683
    im.seek(1)
684
    im.close()
685

686
    with pytest.raises(ValueError):
687
        im.seek(0)
688

689

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
695
) -> None:
696
    test_file = str(tmp_path / "temp.png")
697

698
    im = Image.new("L", (1, 1))
699
    im.save(
700
        test_file,
701
        save_all=True,
702
        default_image=default_image,
703
        append_images=[im.convert(mode) if duplicate else Image.new(mode, (1, 1), 1)],
704
    )
705
    with Image.open(test_file) as reloaded:
706
        assert reloaded.mode == mode
707

708

709
def test_different_durations(tmp_path: Path) -> None:
710
    test_file = str(tmp_path / "temp.png")
711

712
    with Image.open("Tests/images/apng/different_durations.png") as im:
713
        for _ in range(3):
714
            im.seek(0)
715
            assert im.info["duration"] == 4000
716

717
            im.seek(1)
718
            assert im.info["duration"] == 1000
719

720
        im.save(test_file, save_all=True)
721

722
    with Image.open(test_file) as reloaded:
723
        assert reloaded.info["duration"] == 4000
724

725
        reloaded.seek(1)
726
        assert reloaded.info["duration"] == 1000
727

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.