Pillow

Форк
0
/
test_file_pdf.py 
351 строка · 10.7 Кб
1
from __future__ import annotations
2

3
import io
4
import os
5
import os.path
6
import tempfile
7
import time
8
from collections.abc import Generator
9
from pathlib import Path
10
from typing import Any
11

12
import pytest
13

14
from PIL import Image, PdfParser, features
15

16
from .helper import hopper, mark_if_feature_version, skip_unless_feature
17

18

19
def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str:
20
    # Arrange
21
    im = hopper(mode)
22
    outfile = str(tmp_path / ("temp_" + mode + ".pdf"))
23

24
    # Act
25
    im.save(outfile, **kwargs)
26

27
    # Assert
28
    assert os.path.isfile(outfile)
29
    assert os.path.getsize(outfile) > 0
30
    with PdfParser.PdfParser(outfile) as pdf:
31
        if kwargs.get("append_images", False) or kwargs.get("append", False):
32
            assert len(pdf.pages) > 1
33
        else:
34
            assert len(pdf.pages) > 0
35
    with open(outfile, "rb") as fp:
36
        contents = fp.read()
37
    size = tuple(
38
        float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split()
39
    )
40
    assert im.size == size
41

42
    return outfile
43

44

45
@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK"))
46
def test_save(tmp_path: Path, mode: str) -> None:
47
    helper_save_as_pdf(tmp_path, mode)
48

49

50
@skip_unless_feature("jpg_2000")
51
@pytest.mark.parametrize("mode", ("LA", "RGBA"))
52
def test_save_alpha(tmp_path: Path, mode: str) -> None:
53
    helper_save_as_pdf(tmp_path, mode)
54

55

56
def test_p_alpha(tmp_path: Path) -> None:
57
    # Arrange
58
    outfile = str(tmp_path / "temp.pdf")
59
    with Image.open("Tests/images/pil123p.png") as im:
60
        assert im.mode == "P"
61
        assert isinstance(im.info["transparency"], bytes)
62

63
        # Act
64
        im.save(outfile)
65

66
    # Assert
67
    with open(outfile, "rb") as fp:
68
        contents = fp.read()
69
    assert b"\n/SMask " in contents
70

71

72
def test_monochrome(tmp_path: Path) -> None:
73
    # Arrange
74
    mode = "1"
75

76
    # Act / Assert
77
    outfile = helper_save_as_pdf(tmp_path, mode)
78
    assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000)
79

80

81
def test_unsupported_mode(tmp_path: Path) -> None:
82
    im = hopper("PA")
83
    outfile = str(tmp_path / "temp_PA.pdf")
84

85
    with pytest.raises(ValueError):
86
        im.save(outfile)
87

88

89
def test_resolution(tmp_path: Path) -> None:
90
    im = hopper()
91

92
    outfile = str(tmp_path / "temp.pdf")
93
    im.save(outfile, resolution=150)
94

95
    with open(outfile, "rb") as fp:
96
        contents = fp.read()
97

98
    size = tuple(
99
        float(d)
100
        for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ")
101
    )
102
    assert size == (61.44, 61.44)
103

104
    size = tuple(
105
        float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split()
106
    )
107
    assert size == (61.44, 61.44)
108

109

110
@pytest.mark.parametrize(
111
    "params",
112
    (
113
        {"dpi": (75, 150)},
114
        {"dpi": (75, 150), "resolution": 200},
115
    ),
116
)
117
def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
118
    im = hopper()
119

120
    outfile = str(tmp_path / "temp.pdf")
121
    im.save(outfile, "PDF", **params)
122

123
    with open(outfile, "rb") as fp:
124
        contents = fp.read()
125

126
    size = tuple(
127
        float(d)
128
        for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ")
129
    )
130
    assert size == (122.88, 61.44)
131

132
    size = tuple(
133
        float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split()
134
    )
135
    assert size == (122.88, 61.44)
136

137

138
@mark_if_feature_version(
139
    pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
140
)
141
def test_save_all(tmp_path: Path) -> None:
142
    # Single frame image
143
    helper_save_as_pdf(tmp_path, "RGB", save_all=True)
144

145
    # Multiframe image
146
    with Image.open("Tests/images/dispose_bgnd.gif") as im:
147
        outfile = str(tmp_path / "temp.pdf")
148
        im.save(outfile, save_all=True)
149

150
        assert os.path.isfile(outfile)
151
        assert os.path.getsize(outfile) > 0
152

153
        # Append images
154
        ims = [hopper()]
155
        im.copy().save(outfile, save_all=True, append_images=ims)
156

157
        assert os.path.isfile(outfile)
158
        assert os.path.getsize(outfile) > 0
159

160
        # Test appending using a generator
161
        def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
162
            yield from ims
163

164
        im.save(outfile, save_all=True, append_images=im_generator(ims))
165

166
    assert os.path.isfile(outfile)
167
    assert os.path.getsize(outfile) > 0
168

169
    # Append JPEG images
170
    with Image.open("Tests/images/flower.jpg") as jpeg:
171
        jpeg.save(outfile, save_all=True, append_images=[jpeg.copy()])
172

173
    assert os.path.isfile(outfile)
174
    assert os.path.getsize(outfile) > 0
175

176

177
def test_multiframe_normal_save(tmp_path: Path) -> None:
178
    # Test saving a multiframe image without save_all
179
    with Image.open("Tests/images/dispose_bgnd.gif") as im:
180
        outfile = str(tmp_path / "temp.pdf")
181
        im.save(outfile)
182

183
    assert os.path.isfile(outfile)
184
    assert os.path.getsize(outfile) > 0
185

186

187
def test_pdf_open(tmp_path: Path) -> None:
188
    # fail on a buffer full of null bytes
189
    with pytest.raises(PdfParser.PdfFormatError):
190
        PdfParser.PdfParser(buf=bytearray(65536))
191

192
    # make an empty PDF object
193
    with PdfParser.PdfParser() as empty_pdf:
194
        assert len(empty_pdf.pages) == 0
195
        assert len(empty_pdf.info) == 0
196
        assert not empty_pdf.should_close_buf
197
        assert not empty_pdf.should_close_file
198

199
    # make a PDF file
200
    pdf_filename = helper_save_as_pdf(tmp_path, "RGB")
201

202
    # open the PDF file
203
    with PdfParser.PdfParser(filename=pdf_filename) as hopper_pdf:
204
        assert len(hopper_pdf.pages) == 1
205
        assert hopper_pdf.should_close_buf
206
        assert hopper_pdf.should_close_file
207

208
    # read a PDF file from a buffer with a non-zero offset
209
    with open(pdf_filename, "rb") as f:
210
        content = b"xyzzy" + f.read()
211
    with PdfParser.PdfParser(buf=content, start_offset=5) as hopper_pdf:
212
        assert len(hopper_pdf.pages) == 1
213
        assert not hopper_pdf.should_close_buf
214
        assert not hopper_pdf.should_close_file
215

216
    # read a PDF file from an already open file
217
    with open(pdf_filename, "rb") as f:
218
        with PdfParser.PdfParser(f=f) as hopper_pdf:
219
            assert len(hopper_pdf.pages) == 1
220
            assert hopper_pdf.should_close_buf
221
            assert not hopper_pdf.should_close_file
222

223

224
def test_pdf_append_fails_on_nonexistent_file() -> None:
225
    im = hopper("RGB")
226
    with tempfile.TemporaryDirectory() as temp_dir:
227
        with pytest.raises(OSError):
228
            im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True)
229

230

231
def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None:
232
    assert pdf.pages_ref is not None
233
    pages_info = pdf.read_indirect(pdf.pages_ref)
234
    assert b"Parent" not in pages_info
235
    assert b"Kids" in pages_info
236
    kids_not_used = pages_info[b"Kids"]
237
    for page_ref in pdf.pages:
238
        while True:
239
            if page_ref in kids_not_used:
240
                kids_not_used.remove(page_ref)
241
            page_info = pdf.read_indirect(page_ref)
242
            assert b"Parent" in page_info
243
            page_ref = page_info[b"Parent"]
244
            if page_ref == pdf.pages_ref:
245
                break
246
        assert pdf.pages_ref == page_info[b"Parent"]
247
    assert kids_not_used == []
248

249

250
def test_pdf_append(tmp_path: Path) -> None:
251
    # make a PDF file
252
    pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser")
253

254
    # open it, check pages and info
255
    with PdfParser.PdfParser(pdf_filename, mode="r+b") as pdf:
256
        assert len(pdf.pages) == 1
257
        assert len(pdf.info) == 4
258
        assert pdf.info.Title == os.path.splitext(os.path.basename(pdf_filename))[0]
259
        assert pdf.info.Producer == "PdfParser"
260
        assert b"CreationDate" in pdf.info
261
        assert b"ModDate" in pdf.info
262
        check_pdf_pages_consistency(pdf)
263

264
        # append some info
265
        pdf.info.Title = "abc"
266
        pdf.info.Author = "def"
267
        pdf.info.Subject = "ghi\uABCD"
268
        pdf.info.Keywords = "qw)e\\r(ty"
269
        pdf.info.Creator = "hopper()"
270
        pdf.start_writing()
271
        pdf.write_xref_and_trailer()
272

273
    # open it again, check pages and info again
274
    with PdfParser.PdfParser(pdf_filename) as pdf:
275
        assert len(pdf.pages) == 1
276
        assert len(pdf.info) == 8
277
        assert pdf.info.Title == "abc"
278
        assert b"CreationDate" in pdf.info
279
        assert b"ModDate" in pdf.info
280
        check_pdf_pages_consistency(pdf)
281

282
    # append two images
283
    mode_cmyk = hopper("CMYK")
284
    mode_p = hopper("P")
285
    mode_cmyk.save(pdf_filename, append=True, save_all=True, append_images=[mode_p])
286

287
    # open the PDF again, check pages and info again
288
    with PdfParser.PdfParser(pdf_filename) as pdf:
289
        assert len(pdf.pages) == 3
290
        assert len(pdf.info) == 8
291
        assert PdfParser.decode_text(pdf.info[b"Title"]) == "abc"
292
        assert pdf.info.Title == "abc"
293
        assert pdf.info.Producer == "PdfParser"
294
        assert pdf.info.Keywords == "qw)e\\r(ty"
295
        assert pdf.info.Subject == "ghi\uABCD"
296
        assert b"CreationDate" in pdf.info
297
        assert b"ModDate" in pdf.info
298
        check_pdf_pages_consistency(pdf)
299

300

301
def test_pdf_info(tmp_path: Path) -> None:
302
    # make a PDF file
303
    pdf_filename = helper_save_as_pdf(
304
        tmp_path,
305
        "RGB",
306
        title="title",
307
        author="author",
308
        subject="subject",
309
        keywords="keywords",
310
        creator="creator",
311
        producer="producer",
312
        creationDate=time.strptime("2000", "%Y"),
313
        modDate=time.strptime("2001", "%Y"),
314
    )
315

316
    # open it, check pages and info
317
    with PdfParser.PdfParser(pdf_filename) as pdf:
318
        assert len(pdf.info) == 8
319
        assert pdf.info.Title == "title"
320
        assert pdf.info.Author == "author"
321
        assert pdf.info.Subject == "subject"
322
        assert pdf.info.Keywords == "keywords"
323
        assert pdf.info.Creator == "creator"
324
        assert pdf.info.Producer == "producer"
325
        assert pdf.info.CreationDate == time.strptime("2000", "%Y")
326
        assert pdf.info.ModDate == time.strptime("2001", "%Y")
327
        check_pdf_pages_consistency(pdf)
328

329

330
def test_pdf_append_to_bytesio() -> None:
331
    im = hopper("RGB")
332
    f = io.BytesIO()
333
    im.save(f, format="PDF")
334
    initial_size = len(f.getvalue())
335
    assert initial_size > 0
336
    im = hopper("P")
337
    f = io.BytesIO(f.getvalue())
338
    im.save(f, format="PDF", append=True)
339
    assert len(f.getvalue()) > initial_size
340

341

342
@pytest.mark.timeout(1)
343
@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
344
@pytest.mark.parametrize("newline", (b"\r", b"\n"))
345
def test_redos(newline: bytes) -> None:
346
    malicious = b" trailer<<>>" + newline * 3456
347

348
    # This particular exception isn't relevant here.
349
    # The important thing is it doesn't timeout, cause a ReDoS (CVE-2021-25292).
350
    with pytest.raises(PdfParser.PdfFormatError):
351
        PdfParser.PdfParser(buf=malicious)
352

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

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

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

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