1
from __future__ import annotations
8
from collections.abc import Generator
9
from pathlib import Path
14
from PIL import Image, PdfParser, features
16
from .helper import hopper, mark_if_feature_version, skip_unless_feature
19
def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str:
22
outfile = str(tmp_path / ("temp_" + mode + ".pdf"))
25
im.save(outfile, **kwargs)
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
34
assert len(pdf.pages) > 0
35
with open(outfile, "rb") as fp:
38
float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split()
40
assert im.size == size
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)
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)
56
def test_p_alpha(tmp_path: Path) -> None:
58
outfile = str(tmp_path / "temp.pdf")
59
with Image.open("Tests/images/pil123p.png") as im:
61
assert isinstance(im.info["transparency"], bytes)
67
with open(outfile, "rb") as fp:
69
assert b"\n/SMask " in contents
72
def test_monochrome(tmp_path: Path) -> None:
77
outfile = helper_save_as_pdf(tmp_path, mode)
78
assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000)
81
def test_unsupported_mode(tmp_path: Path) -> None:
83
outfile = str(tmp_path / "temp_PA.pdf")
85
with pytest.raises(ValueError):
89
def test_resolution(tmp_path: Path) -> None:
92
outfile = str(tmp_path / "temp.pdf")
93
im.save(outfile, resolution=150)
95
with open(outfile, "rb") as fp:
100
for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ")
102
assert size == (61.44, 61.44)
105
float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split()
107
assert size == (61.44, 61.44)
110
@pytest.mark.parametrize(
114
{"dpi": (75, 150), "resolution": 200},
117
def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
120
outfile = str(tmp_path / "temp.pdf")
121
im.save(outfile, "PDF", **params)
123
with open(outfile, "rb") as fp:
128
for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ")
130
assert size == (122.88, 61.44)
133
float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split()
135
assert size == (122.88, 61.44)
138
@mark_if_feature_version(
139
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
141
def test_save_all(tmp_path: Path) -> None:
143
helper_save_as_pdf(tmp_path, "RGB", save_all=True)
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)
150
assert os.path.isfile(outfile)
151
assert os.path.getsize(outfile) > 0
155
im.copy().save(outfile, save_all=True, append_images=ims)
157
assert os.path.isfile(outfile)
158
assert os.path.getsize(outfile) > 0
161
def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
164
im.save(outfile, save_all=True, append_images=im_generator(ims))
166
assert os.path.isfile(outfile)
167
assert os.path.getsize(outfile) > 0
170
with Image.open("Tests/images/flower.jpg") as jpeg:
171
jpeg.save(outfile, save_all=True, append_images=[jpeg.copy()])
173
assert os.path.isfile(outfile)
174
assert os.path.getsize(outfile) > 0
177
def test_multiframe_normal_save(tmp_path: Path) -> None:
179
with Image.open("Tests/images/dispose_bgnd.gif") as im:
180
outfile = str(tmp_path / "temp.pdf")
183
assert os.path.isfile(outfile)
184
assert os.path.getsize(outfile) > 0
187
def test_pdf_open(tmp_path: Path) -> None:
189
with pytest.raises(PdfParser.PdfFormatError):
190
PdfParser.PdfParser(buf=bytearray(65536))
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
200
pdf_filename = helper_save_as_pdf(tmp_path, "RGB")
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
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
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
224
def test_pdf_append_fails_on_nonexistent_file() -> None:
226
with tempfile.TemporaryDirectory() as temp_dir:
227
with pytest.raises(OSError):
228
im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True)
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:
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:
246
assert pdf.pages_ref == page_info[b"Parent"]
247
assert kids_not_used == []
250
def test_pdf_append(tmp_path: Path) -> None:
252
pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser")
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)
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()"
271
pdf.write_xref_and_trailer()
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)
283
mode_cmyk = hopper("CMYK")
285
mode_cmyk.save(pdf_filename, append=True, save_all=True, append_images=[mode_p])
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)
301
def test_pdf_info(tmp_path: Path) -> None:
303
pdf_filename = helper_save_as_pdf(
312
creationDate=time.strptime("2000", "%Y"),
313
modDate=time.strptime("2001", "%Y"),
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)
330
def test_pdf_append_to_bytesio() -> None:
333
im.save(f, format="PDF")
334
initial_size = len(f.getvalue())
335
assert initial_size > 0
337
f = io.BytesIO(f.getvalue())
338
im.save(f, format="PDF", append=True)
339
assert len(f.getvalue()) > initial_size
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
350
with pytest.raises(PdfParser.PdfFormatError):
351
PdfParser.PdfParser(buf=malicious)