Pillow

Форк
0
/
test_file_png.py 
828 строк · 27.9 Кб
1
from __future__ import annotations
2

3
import re
4
import sys
5
import warnings
6
import zlib
7
from io import BytesIO
8
from pathlib import Path
9
from types import ModuleType
10
from typing import Any, cast
11

12
import pytest
13

14
from PIL import Image, ImageFile, PngImagePlugin, features
15

16
from .helper import (
17
    PillowLeakTestCase,
18
    assert_image,
19
    assert_image_equal,
20
    assert_image_equal_tofile,
21
    hopper,
22
    is_win32,
23
    mark_if_feature_version,
24
    skip_unless_feature,
25
)
26

27
ElementTree: ModuleType | None
28
try:
29
    from defusedxml import ElementTree
30
except ImportError:
31
    ElementTree = None
32

33
# sample png stream
34

35
TEST_PNG_FILE = "Tests/images/hopper.png"
36

37
# stuff to create inline PNG images
38

39
MAGIC = PngImagePlugin._MAGIC
40

41

42
def chunk(cid: bytes, *data: bytes) -> bytes:
43
    test_file = BytesIO()
44
    PngImagePlugin.putchunk(test_file, cid, *data)
45
    return test_file.getvalue()
46

47

48
o32 = PngImagePlugin.o32
49

50
IHDR = chunk(b"IHDR", o32(1), o32(1), b"\x08\x02", b"\0\0\0")
51
IDAT = chunk(b"IDAT")
52
IEND = chunk(b"IEND")
53

54
HEAD = MAGIC + IHDR
55
TAIL = IDAT + IEND
56

57

58
def load(data: bytes) -> Image.Image:
59
    return Image.open(BytesIO(data))
60

61

62
def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile:
63
    out = BytesIO()
64
    im.save(out, "PNG", **options)
65
    out.seek(0)
66
    return cast(PngImagePlugin.PngImageFile, Image.open(out))
67

68

69
@skip_unless_feature("zlib")
70
class TestFilePng:
71
    def get_chunks(self, filename: str) -> list[bytes]:
72
        chunks = []
73
        with open(filename, "rb") as fp:
74
            fp.read(8)
75
            with PngImagePlugin.PngStream(fp) as png:
76
                while True:
77
                    cid, pos, length = png.read()
78
                    chunks.append(cid)
79
                    try:
80
                        s = png.call(cid, pos, length)
81
                    except EOFError:
82
                        break
83
                    png.crc(cid, s)
84
        return chunks
85

86
    def test_sanity(self, tmp_path: Path) -> None:
87
        # internal version number
88
        version = features.version_codec("zlib")
89
        assert version is not None
90
        assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version)
91

92
        test_file = str(tmp_path / "temp.png")
93

94
        hopper("RGB").save(test_file)
95

96
        with Image.open(test_file) as im:
97
            im.load()
98
            assert im.mode == "RGB"
99
            assert im.size == (128, 128)
100
            assert im.format == "PNG"
101
            assert im.get_format_mimetype() == "image/png"
102

103
        for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]:
104
            im = hopper(mode)
105
            im.save(test_file)
106
            with Image.open(test_file) as reloaded:
107
                if mode in ("I", "I;16B"):
108
                    reloaded = reloaded.convert(mode)
109
                assert_image_equal(reloaded, im)
110

111
    def test_invalid_file(self) -> None:
112
        invalid_file = "Tests/images/flower.jpg"
113

114
        with pytest.raises(SyntaxError):
115
            PngImagePlugin.PngImageFile(invalid_file)
116

117
    def test_broken(self) -> None:
118
        # Check reading of totally broken files.  In this case, the test
119
        # file was checked into Subversion as a text file.
120

121
        test_file = "Tests/images/broken.png"
122
        with pytest.raises(OSError):
123
            with Image.open(test_file):
124
                pass
125

126
    def test_bad_text(self) -> None:
127
        # Make sure PIL can read malformed tEXt chunks (@PIL152)
128

129
        im = load(HEAD + chunk(b"tEXt") + TAIL)
130
        assert im.info == {}
131

132
        im = load(HEAD + chunk(b"tEXt", b"spam") + TAIL)
133
        assert im.info == {"spam": ""}
134

135
        im = load(HEAD + chunk(b"tEXt", b"spam\0") + TAIL)
136
        assert im.info == {"spam": ""}
137

138
        im = load(HEAD + chunk(b"tEXt", b"spam\0egg") + TAIL)
139
        assert im.info == {"spam": "egg"}
140

141
        im = load(HEAD + chunk(b"tEXt", b"spam\0egg\0") + TAIL)
142
        assert im.info == {"spam": "egg\x00"}
143

144
    def test_bad_ztxt(self) -> None:
145
        # Test reading malformed zTXt chunks (python-pillow/Pillow#318)
146

147
        im = load(HEAD + chunk(b"zTXt") + TAIL)
148
        assert im.info == {}
149

150
        im = load(HEAD + chunk(b"zTXt", b"spam") + TAIL)
151
        assert im.info == {"spam": ""}
152

153
        im = load(HEAD + chunk(b"zTXt", b"spam\0") + TAIL)
154
        assert im.info == {"spam": ""}
155

156
        im = load(HEAD + chunk(b"zTXt", b"spam\0\0") + TAIL)
157
        assert im.info == {"spam": ""}
158

159
        im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")[:1]) + TAIL)
160
        assert im.info == {"spam": ""}
161

162
        im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")) + TAIL)
163
        assert im.info == {"spam": "egg"}
164

165
    def test_bad_itxt(self) -> None:
166
        im = load(HEAD + chunk(b"iTXt") + TAIL)
167
        assert im.info == {}
168

169
        im = load(HEAD + chunk(b"iTXt", b"spam") + TAIL)
170
        assert im.info == {}
171

172
        im = load(HEAD + chunk(b"iTXt", b"spam\0") + TAIL)
173
        assert im.info == {}
174

175
        im = load(HEAD + chunk(b"iTXt", b"spam\0\x02") + TAIL)
176
        assert im.info == {}
177

178
        im = load(HEAD + chunk(b"iTXt", b"spam\0\0\0foo\0") + TAIL)
179
        assert im.info == {}
180

181
        im = load(HEAD + chunk(b"iTXt", b"spam\0\0\0en\0Spam\0egg") + TAIL)
182
        assert im.info == {"spam": "egg"}
183
        assert im.info["spam"].lang == "en"
184
        assert im.info["spam"].tkey == "Spam"
185

186
        im = load(
187
            HEAD
188
            + chunk(b"iTXt", b"spam\0\1\0en\0Spam\0" + zlib.compress(b"egg")[:1])
189
            + TAIL
190
        )
191
        assert im.info == {"spam": ""}
192

193
        im = load(
194
            HEAD
195
            + chunk(b"iTXt", b"spam\0\1\1en\0Spam\0" + zlib.compress(b"egg"))
196
            + TAIL
197
        )
198
        assert im.info == {}
199

200
        im = load(
201
            HEAD
202
            + chunk(b"iTXt", b"spam\0\1\0en\0Spam\0" + zlib.compress(b"egg"))
203
            + TAIL
204
        )
205
        assert im.info == {"spam": "egg"}
206
        assert im.info["spam"].lang == "en"
207
        assert im.info["spam"].tkey == "Spam"
208

209
    def test_interlace(self) -> None:
210
        test_file = "Tests/images/pil123p.png"
211
        with Image.open(test_file) as im:
212
            assert_image(im, "P", (162, 150))
213
            assert im.info.get("interlace")
214

215
            im.load()
216

217
        test_file = "Tests/images/pil123rgba.png"
218
        with Image.open(test_file) as im:
219
            assert_image(im, "RGBA", (162, 150))
220
            assert im.info.get("interlace")
221

222
            im.load()
223

224
    def test_load_transparent_p(self) -> None:
225
        test_file = "Tests/images/pil123p.png"
226
        with Image.open(test_file) as im:
227
            assert_image(im, "P", (162, 150))
228
            im = im.convert("RGBA")
229
        assert_image(im, "RGBA", (162, 150))
230

231
        # image has 124 unique alpha values
232
        assert len(im.getchannel("A").getcolors()) == 124
233

234
    def test_load_transparent_rgb(self) -> None:
235
        test_file = "Tests/images/rgb_trns.png"
236
        with Image.open(test_file) as im:
237
            assert im.info["transparency"] == (0, 255, 52)
238

239
            assert_image(im, "RGB", (64, 64))
240
            im = im.convert("RGBA")
241
        assert_image(im, "RGBA", (64, 64))
242

243
        # image has 876 transparent pixels
244
        assert im.getchannel("A").getcolors()[0][0] == 876
245

246
    def test_save_p_transparent_palette(self, tmp_path: Path) -> None:
247
        in_file = "Tests/images/pil123p.png"
248
        with Image.open(in_file) as im:
249
            # 'transparency' contains a byte string with the opacity for
250
            # each palette entry
251
            assert len(im.info["transparency"]) == 256
252

253
            test_file = str(tmp_path / "temp.png")
254
            im.save(test_file)
255

256
        # check if saved image contains same transparency
257
        with Image.open(test_file) as im:
258
            assert len(im.info["transparency"]) == 256
259

260
            assert_image(im, "P", (162, 150))
261
            im = im.convert("RGBA")
262
        assert_image(im, "RGBA", (162, 150))
263

264
        # image has 124 unique alpha values
265
        assert len(im.getchannel("A").getcolors()) == 124
266

267
    def test_save_p_single_transparency(self, tmp_path: Path) -> None:
268
        in_file = "Tests/images/p_trns_single.png"
269
        with Image.open(in_file) as im:
270
            # pixel value 164 is full transparent
271
            assert im.info["transparency"] == 164
272
            assert im.getpixel((31, 31)) == 164
273

274
            test_file = str(tmp_path / "temp.png")
275
            im.save(test_file)
276

277
        # check if saved image contains same transparency
278
        with Image.open(test_file) as im:
279
            assert im.info["transparency"] == 164
280
            assert im.getpixel((31, 31)) == 164
281
            assert_image(im, "P", (64, 64))
282
            im = im.convert("RGBA")
283
        assert_image(im, "RGBA", (64, 64))
284

285
        assert im.getpixel((31, 31)) == (0, 255, 52, 0)
286

287
        # image has 876 transparent pixels
288
        assert im.getchannel("A").getcolors()[0][0] == 876
289

290
    def test_save_p_transparent_black(self, tmp_path: Path) -> None:
291
        # check if solid black image with full transparency
292
        # is supported (check for #1838)
293
        im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
294
        assert im.getcolors() == [(100, (0, 0, 0, 0))]
295

296
        im = im.convert("P")
297
        test_file = str(tmp_path / "temp.png")
298
        im.save(test_file)
299

300
        # check if saved image contains same transparency
301
        with Image.open(test_file) as im:
302
            assert len(im.info["transparency"]) == 256
303
            assert_image(im, "P", (10, 10))
304
            im = im.convert("RGBA")
305
        assert_image(im, "RGBA", (10, 10))
306
        assert im.getcolors() == [(100, (0, 0, 0, 0))]
307

308
    def test_save_grayscale_transparency(self, tmp_path: Path) -> None:
309
        for mode, num_transparent in {"1": 1994, "L": 559, "I;16": 559}.items():
310
            in_file = "Tests/images/" + mode.split(";")[0].lower() + "_trns.png"
311
            with Image.open(in_file) as im:
312
                assert im.mode == mode
313
                assert im.info["transparency"] == 255
314

315
                im_rgba = im.convert("RGBA")
316
            assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent
317

318
            test_file = str(tmp_path / "temp.png")
319
            im.save(test_file)
320

321
            with Image.open(test_file) as test_im:
322
                assert test_im.mode == mode
323
                assert test_im.info["transparency"] == 255
324
                assert_image_equal(im, test_im)
325

326
            test_im_rgba = test_im.convert("RGBA")
327
            assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent
328

329
    def test_save_rgb_single_transparency(self, tmp_path: Path) -> None:
330
        in_file = "Tests/images/caption_6_33_22.png"
331
        with Image.open(in_file) as im:
332
            test_file = str(tmp_path / "temp.png")
333
            im.save(test_file)
334

335
    def test_load_verify(self) -> None:
336
        # Check open/load/verify exception (@PIL150)
337

338
        with Image.open(TEST_PNG_FILE) as im:
339
            # Assert that there is no unclosed file warning
340
            with warnings.catch_warnings():
341
                im.verify()
342

343
        with Image.open(TEST_PNG_FILE) as im:
344
            im.load()
345
            with pytest.raises(RuntimeError):
346
                im.verify()
347

348
    def test_verify_struct_error(self) -> None:
349
        # Check open/load/verify exception (#1755)
350

351
        # offsets to test, -10: breaks in i32() in read. (OSError)
352
        #                  -13: breaks in crc, txt chunk.
353
        #                  -14: malformed chunk
354

355
        for offset in (-10, -13, -14):
356
            with open(TEST_PNG_FILE, "rb") as f:
357
                test_file = f.read()[:offset]
358

359
            with Image.open(BytesIO(test_file)) as im:
360
                assert im.fp is not None
361
                with pytest.raises((OSError, SyntaxError)):
362
                    im.verify()
363

364
    def test_verify_ignores_crc_error(self) -> None:
365
        # check ignores crc errors in ancillary chunks
366

367
        chunk_data = chunk(b"tEXt", b"spam")
368
        broken_crc_chunk_data = chunk_data[:-1] + b"q"  # break CRC
369

370
        image_data = HEAD + broken_crc_chunk_data + TAIL
371
        with pytest.raises(SyntaxError):
372
            PngImagePlugin.PngImageFile(BytesIO(image_data))
373

374
        ImageFile.LOAD_TRUNCATED_IMAGES = True
375
        try:
376
            im = load(image_data)
377
            assert im is not None
378
        finally:
379
            ImageFile.LOAD_TRUNCATED_IMAGES = False
380

381
    def test_verify_not_ignores_crc_error_in_required_chunk(self) -> None:
382
        # check does not ignore crc errors in required chunks
383

384
        image_data = MAGIC + IHDR[:-1] + b"q" + TAIL
385

386
        ImageFile.LOAD_TRUNCATED_IMAGES = True
387
        try:
388
            with pytest.raises(SyntaxError):
389
                PngImagePlugin.PngImageFile(BytesIO(image_data))
390
        finally:
391
            ImageFile.LOAD_TRUNCATED_IMAGES = False
392

393
    def test_roundtrip_dpi(self) -> None:
394
        # Check dpi roundtripping
395

396
        with Image.open(TEST_PNG_FILE) as im:
397
            im = roundtrip(im, dpi=(100.33, 100.33))
398
        assert im.info["dpi"] == (100.33, 100.33)
399

400
    def test_load_float_dpi(self) -> None:
401
        with Image.open(TEST_PNG_FILE) as im:
402
            assert im.info["dpi"] == (95.9866, 95.9866)
403

404
    def test_roundtrip_text(self) -> None:
405
        # Check text roundtripping
406

407
        with Image.open(TEST_PNG_FILE) as im:
408
            info = PngImagePlugin.PngInfo()
409
            info.add_text("TXT", "VALUE")
410
            info.add_text("ZIP", "VALUE", zip=True)
411

412
            im = roundtrip(im, pnginfo=info)
413
        assert im.info == {"TXT": "VALUE", "ZIP": "VALUE"}
414
        assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
415

416
    def test_roundtrip_itxt(self) -> None:
417
        # Check iTXt roundtripping
418

419
        im = Image.new("RGB", (32, 32))
420
        info = PngImagePlugin.PngInfo()
421
        info.add_itxt("spam", "Eggs", "en", "Spam")
422
        info.add_text("eggs", PngImagePlugin.iTXt("Spam", "en", "Eggs"), zip=True)
423

424
        im = roundtrip(im, pnginfo=info)
425
        assert im.info == {"spam": "Eggs", "eggs": "Spam"}
426
        assert im.text == {"spam": "Eggs", "eggs": "Spam"}
427
        assert isinstance(im.text["spam"], PngImagePlugin.iTXt)
428
        assert im.text["spam"].lang == "en"
429
        assert im.text["spam"].tkey == "Spam"
430
        assert isinstance(im.text["eggs"], PngImagePlugin.iTXt)
431
        assert im.text["eggs"].lang == "en"
432
        assert im.text["eggs"].tkey == "Eggs"
433

434
    def test_nonunicode_text(self) -> None:
435
        # Check so that non-Unicode text is saved as a tEXt rather than iTXt
436

437
        im = Image.new("RGB", (32, 32))
438
        info = PngImagePlugin.PngInfo()
439
        info.add_text("Text", "Ascii")
440
        im = roundtrip(im, pnginfo=info)
441
        assert isinstance(im.info["Text"], str)
442

443
    def test_unicode_text(self) -> None:
444
        # Check preservation of non-ASCII characters
445

446
        def rt_text(value: str) -> None:
447
            im = Image.new("RGB", (32, 32))
448
            info = PngImagePlugin.PngInfo()
449
            info.add_text("Text", value)
450
            im = roundtrip(im, pnginfo=info)
451
            assert im.info == {"Text": value}
452

453
        rt_text(" Aa" + chr(0xA0) + chr(0xC4) + chr(0xFF))  # Latin1
454
        rt_text(chr(0x400) + chr(0x472) + chr(0x4FF))  # Cyrillic
455
        # CJK:
456
        rt_text(chr(0x4E00) + chr(0x66F0) + chr(0x9FBA) + chr(0x3042) + chr(0xAC00))
457
        rt_text("A" + chr(0xC4) + chr(0x472) + chr(0x3042))  # Combined
458

459
    def test_scary(self) -> None:
460
        # Check reading of evil PNG file.  For information, see:
461
        # http://scary.beasts.org/security/CESA-2004-001.txt
462
        # The first byte is removed from pngtest_bad.png
463
        # to avoid classification as malware.
464

465
        with open("Tests/images/pngtest_bad.png.bin", "rb") as fd:
466
            data = b"\x89" + fd.read()
467

468
        pngfile = BytesIO(data)
469
        with pytest.raises(OSError):
470
            with Image.open(pngfile):
471
                pass
472

473
    def test_trns_rgb(self) -> None:
474
        # Check writing and reading of tRNS chunks for RGB images.
475
        # Independent file sample provided by Sebastian Spaeth.
476

477
        test_file = "Tests/images/caption_6_33_22.png"
478
        with Image.open(test_file) as im:
479
            assert im.info["transparency"] == (248, 248, 248)
480

481
            # check saving transparency by default
482
            im = roundtrip(im)
483
        assert im.info["transparency"] == (248, 248, 248)
484

485
        im = roundtrip(im, transparency=(0, 1, 2))
486
        assert im.info["transparency"] == (0, 1, 2)
487

488
    def test_trns_p(self, tmp_path: Path) -> None:
489
        # Check writing a transparency of 0, issue #528
490
        im = hopper("P")
491
        im.info["transparency"] = 0
492

493
        f = str(tmp_path / "temp.png")
494
        im.save(f)
495

496
        with Image.open(f) as im2:
497
            assert "transparency" in im2.info
498

499
            assert_image_equal(im2.convert("RGBA"), im.convert("RGBA"))
500

501
    def test_trns_null(self) -> None:
502
        # Check reading images with null tRNS value, issue #1239
503
        test_file = "Tests/images/tRNS_null_1x1.png"
504
        with Image.open(test_file) as im:
505
            assert im.info["transparency"] == 0
506

507
    def test_save_icc_profile(self) -> None:
508
        with Image.open("Tests/images/icc_profile_none.png") as im:
509
            assert im.info["icc_profile"] is None
510

511
            with Image.open("Tests/images/icc_profile.png") as with_icc:
512
                expected_icc = with_icc.info["icc_profile"]
513

514
                im = roundtrip(im, icc_profile=expected_icc)
515
                assert im.info["icc_profile"] == expected_icc
516

517
    def test_discard_icc_profile(self) -> None:
518
        with Image.open("Tests/images/icc_profile.png") as im:
519
            assert "icc_profile" in im.info
520

521
            im = roundtrip(im, icc_profile=None)
522
        assert "icc_profile" not in im.info
523

524
    def test_roundtrip_icc_profile(self) -> None:
525
        with Image.open("Tests/images/icc_profile.png") as im:
526
            expected_icc = im.info["icc_profile"]
527

528
            im = roundtrip(im)
529
        assert im.info["icc_profile"] == expected_icc
530

531
    def test_roundtrip_no_icc_profile(self) -> None:
532
        with Image.open("Tests/images/icc_profile_none.png") as im:
533
            assert im.info["icc_profile"] is None
534

535
            im = roundtrip(im)
536
        assert "icc_profile" not in im.info
537

538
    def test_repr_png(self) -> None:
539
        im = hopper()
540
        b = im._repr_png_()
541
        assert b is not None
542

543
        with Image.open(BytesIO(b)) as repr_png:
544
            assert repr_png.format == "PNG"
545
            assert_image_equal(im, repr_png)
546

547
    def test_repr_png_error_returns_none(self) -> None:
548
        im = hopper("F")
549

550
        assert im._repr_png_() is None
551

552
    def test_chunk_order(self, tmp_path: Path) -> None:
553
        with Image.open("Tests/images/icc_profile.png") as im:
554
            test_file = str(tmp_path / "temp.png")
555
            im.convert("P").save(test_file, dpi=(100, 100))
556

557
        chunks = self.get_chunks(test_file)
558

559
        # https://www.w3.org/TR/PNG/#5ChunkOrdering
560
        # IHDR - shall be first
561
        assert chunks.index(b"IHDR") == 0
562
        # PLTE - before first IDAT
563
        assert chunks.index(b"PLTE") < chunks.index(b"IDAT")
564
        # iCCP - before PLTE and IDAT
565
        assert chunks.index(b"iCCP") < chunks.index(b"PLTE")
566
        assert chunks.index(b"iCCP") < chunks.index(b"IDAT")
567
        # tRNS - after PLTE, before IDAT
568
        assert chunks.index(b"tRNS") > chunks.index(b"PLTE")
569
        assert chunks.index(b"tRNS") < chunks.index(b"IDAT")
570
        # pHYs - before IDAT
571
        assert chunks.index(b"pHYs") < chunks.index(b"IDAT")
572

573
    def test_getchunks(self) -> None:
574
        im = hopper()
575

576
        chunks = PngImagePlugin.getchunks(im)
577
        assert len(chunks) == 3
578

579
    def test_read_private_chunks(self) -> None:
580
        with Image.open("Tests/images/exif.png") as im:
581
            assert im.private_chunks == [(b"orNT", b"\x01")]
582

583
    def test_roundtrip_private_chunk(self) -> None:
584
        # Check private chunk roundtripping
585

586
        with Image.open(TEST_PNG_FILE) as im:
587
            info = PngImagePlugin.PngInfo()
588
            info.add(b"prIV", b"VALUE")
589
            info.add(b"atEC", b"VALUE2")
590
            info.add(b"prIV", b"VALUE3", True)
591

592
            im = roundtrip(im, pnginfo=info)
593
        assert im.private_chunks == [(b"prIV", b"VALUE"), (b"atEC", b"VALUE2")]
594
        im.load()
595
        assert im.private_chunks == [
596
            (b"prIV", b"VALUE"),
597
            (b"atEC", b"VALUE2"),
598
            (b"prIV", b"VALUE3", True),
599
        ]
600

601
    def test_textual_chunks_after_idat(self) -> None:
602
        with Image.open("Tests/images/hopper.png") as im:
603
            assert "comment" in im.text
604
            for k, v in {
605
                "date:create": "2014-09-04T09:37:08+03:00",
606
                "date:modify": "2014-09-04T09:37:08+03:00",
607
            }.items():
608
                assert im.text[k] == v
609

610
        # Raises a SyntaxError in load_end
611
        with Image.open("Tests/images/broken_data_stream.png") as im:
612
            with pytest.raises(OSError):
613
                assert isinstance(im.text, dict)
614

615
        # Raises a UnicodeDecodeError in load_end
616
        with Image.open("Tests/images/truncated_image.png") as im:
617
            # The file is truncated
618
            with pytest.raises(OSError):
619
                im.text()
620
            ImageFile.LOAD_TRUNCATED_IMAGES = True
621
            assert isinstance(im.text, dict)
622
            ImageFile.LOAD_TRUNCATED_IMAGES = False
623

624
        # Raises an EOFError in load_end
625
        with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
626
            assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
627

628
    def test_unknown_compression_method(self) -> None:
629
        with pytest.raises(SyntaxError, match="Unknown compression method"):
630
            PngImagePlugin.PngImageFile("Tests/images/unknown_compression_method.png")
631

632
    def test_padded_idat(self) -> None:
633
        # This image has been manually hexedited
634
        # so that the IDAT chunk has padding at the end
635
        # Set MAXBLOCK to the length of the actual data
636
        # so that the decoder finishes reading before the chunk ends
637
        MAXBLOCK = ImageFile.MAXBLOCK
638
        ImageFile.MAXBLOCK = 45
639
        ImageFile.LOAD_TRUNCATED_IMAGES = True
640

641
        with Image.open("Tests/images/padded_idat.png") as im:
642
            im.load()
643

644
            ImageFile.MAXBLOCK = MAXBLOCK
645
            ImageFile.LOAD_TRUNCATED_IMAGES = False
646

647
            assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
648

649
    @pytest.mark.parametrize(
650
        "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT")
651
    )
652
    def test_truncated_chunks(self, cid: bytes) -> None:
653
        fp = BytesIO()
654
        with PngImagePlugin.PngStream(fp) as png:
655
            with pytest.raises(ValueError):
656
                png.call(cid, 0, 0)
657

658
            ImageFile.LOAD_TRUNCATED_IMAGES = True
659
            png.call(cid, 0, 0)
660
            ImageFile.LOAD_TRUNCATED_IMAGES = False
661

662
    @pytest.mark.parametrize("save_all", (True, False))
663
    def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
664
        im = hopper("P")
665

666
        out = str(tmp_path / "temp.png")
667
        im.save(out, bits=4, save_all=save_all)
668

669
        with Image.open(out) as reloaded:
670
            assert len(reloaded.png.im_palette[1]) == 48
671

672
    def test_plte_length(self, tmp_path: Path) -> None:
673
        im = Image.new("P", (1, 1))
674
        im.putpalette((1, 1, 1))
675

676
        out = str(tmp_path / "temp.png")
677
        im.save(str(tmp_path / "temp.png"))
678

679
        with Image.open(out) as reloaded:
680
            assert len(reloaded.png.im_palette[1]) == 3
681

682
    def test_getxmp(self) -> None:
683
        with Image.open("Tests/images/color_snakes.png") as im:
684
            if ElementTree is None:
685
                with pytest.warns(
686
                    UserWarning,
687
                    match="XMP data cannot be read without defusedxml dependency",
688
                ):
689
                    assert im.getxmp() == {}
690
            else:
691
                assert "xmp" in im.info
692
                xmp = im.getxmp()
693

694
                description = xmp["xmpmeta"]["RDF"]["Description"]
695
                assert description["PixelXDimension"] == "10"
696
                assert description["subject"]["Seq"] is None
697

698
    def test_exif(self) -> None:
699
        # With an EXIF chunk
700
        with Image.open("Tests/images/exif.png") as im:
701
            exif = im._getexif()
702
        assert exif[274] == 1
703

704
        # With an ImageMagick zTXt chunk
705
        with Image.open("Tests/images/exif_imagemagick.png") as im:
706
            exif = im._getexif()
707
            assert exif[274] == 1
708

709
            # Assert that info still can be extracted
710
            # when the image is no longer a PngImageFile instance
711
            exif = im.copy().getexif()
712
            assert exif[274] == 1
713

714
        # With a tEXt chunk
715
        with Image.open("Tests/images/exif_text.png") as im:
716
            exif = im._getexif()
717
        assert exif[274] == 1
718

719
        # With XMP tags
720
        with Image.open("Tests/images/xmp_tags_orientation.png") as im:
721
            exif = im.getexif()
722
        assert exif[274] == 3
723

724
    def test_exif_save(self, tmp_path: Path) -> None:
725
        # Test exif is not saved from info
726
        test_file = str(tmp_path / "temp.png")
727
        with Image.open("Tests/images/exif.png") as im:
728
            im.save(test_file)
729

730
        with Image.open(test_file) as reloaded:
731
            assert reloaded._getexif() is None
732

733
        # Test passing in exif
734
        with Image.open("Tests/images/exif.png") as im:
735
            im.save(test_file, exif=im.getexif())
736

737
        with Image.open(test_file) as reloaded:
738
            exif = reloaded._getexif()
739
        assert exif[274] == 1
740

741
    @mark_if_feature_version(
742
        pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
743
    )
744
    def test_exif_from_jpg(self, tmp_path: Path) -> None:
745
        with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
746
            test_file = str(tmp_path / "temp.png")
747
            im.save(test_file, exif=im.getexif())
748

749
        with Image.open(test_file) as reloaded:
750
            exif = reloaded._getexif()
751
        assert exif[305] == "Adobe Photoshop CS Macintosh"
752

753
    def test_exif_argument(self, tmp_path: Path) -> None:
754
        with Image.open(TEST_PNG_FILE) as im:
755
            test_file = str(tmp_path / "temp.png")
756
            im.save(test_file, exif=b"exifstring")
757

758
        with Image.open(test_file) as reloaded:
759
            assert reloaded.info["exif"] == b"Exif\x00\x00exifstring"
760

761
    def test_tell(self) -> None:
762
        with Image.open(TEST_PNG_FILE) as im:
763
            assert im.tell() == 0
764

765
    def test_seek(self) -> None:
766
        with Image.open(TEST_PNG_FILE) as im:
767
            im.seek(0)
768

769
            with pytest.raises(EOFError):
770
                im.seek(1)
771

772
    @pytest.mark.parametrize("buffer", (True, False))
773
    def test_save_stdout(self, buffer: bool) -> None:
774
        old_stdout = sys.stdout
775

776
        class MyStdOut:
777
            buffer = BytesIO()
778

779
        mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
780

781
        sys.stdout = mystdout
782

783
        with Image.open(TEST_PNG_FILE) as im:
784
            im.save(sys.stdout, "PNG")
785

786
        # Reset stdout
787
        sys.stdout = old_stdout
788

789
        if isinstance(mystdout, MyStdOut):
790
            mystdout = mystdout.buffer
791
        with Image.open(mystdout) as reloaded:
792
            assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
793

794
    def test_truncated_end_chunk(self) -> None:
795
        with Image.open("Tests/images/truncated_end_chunk.png") as im:
796
            with pytest.raises(OSError):
797
                im.load()
798

799
        ImageFile.LOAD_TRUNCATED_IMAGES = True
800
        try:
801
            with Image.open("Tests/images/truncated_end_chunk.png") as im:
802
                assert_image_equal_tofile(im, "Tests/images/hopper.png")
803
        finally:
804
            ImageFile.LOAD_TRUNCATED_IMAGES = False
805

806

807
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
808
@skip_unless_feature("zlib")
809
class TestTruncatedPngPLeaks(PillowLeakTestCase):
810
    mem_limit = 2 * 1024  # max increase in K
811
    iterations = 100  # Leak is 56k/iteration, this will leak 5.6megs
812

813
    def test_leak_load(self) -> None:
814
        with open("Tests/images/hopper.png", "rb") as f:
815
            DATA = BytesIO(f.read(16 * 1024))
816

817
        ImageFile.LOAD_TRUNCATED_IMAGES = True
818
        with Image.open(DATA) as im:
819
            im.load()
820

821
        def core() -> None:
822
            with Image.open(DATA) as im:
823
                im.load()
824

825
        try:
826
            self._test_leak(core)
827
        finally:
828
            ImageFile.LOAD_TRUNCATED_IMAGES = False
829

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

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

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

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