Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/PdfImagePlugin.py: 10%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# The Python Imaging Library.
3# $Id$
4#
5# PDF (Acrobat) file handling
6#
7# History:
8# 1996-07-16 fl Created
9# 1997-01-18 fl Fixed header
10# 2004-02-21 fl Fixes for 1/L/CMYK images, etc.
11# 2004-02-24 fl Fixes for 1 and P images.
12#
13# Copyright (c) 1997-2004 by Secret Labs AB. All rights reserved.
14# Copyright (c) 1996-1997 by Fredrik Lundh.
15#
16# See the README file for information on usage and redistribution.
17#
19##
20# Image plugin for PDF images (output only).
21##
22from __future__ import annotations
24import io
25import math
26import os
27import time
28from typing import IO, Any
30from . import Image, ImageFile, ImageSequence, PdfParser, features
32#
33# --------------------------------------------------------------------
35# object ids:
36# 1. catalogue
37# 2. pages
38# 3. image
39# 4. page
40# 5. page contents
43def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
44 _save(im, fp, filename, save_all=True)
47##
48# (Internal) Image save plugin for the PDF format.
51def _write_image(
52 im: Image.Image,
53 filename: str | bytes,
54 existing_pdf: PdfParser.PdfParser,
55 image_refs: list[PdfParser.IndirectReference],
56) -> tuple[PdfParser.IndirectReference, str]:
57 # FIXME: Should replace ASCIIHexDecode with RunLengthDecode
58 # (packbits) or LZWDecode (tiff/lzw compression). Note that
59 # PDF 1.2 also supports Flatedecode (zip compression).
61 params = None
62 decode = None
64 #
65 # Get image characteristics
67 width, height = im.size
69 dict_obj: dict[str, Any] = {"BitsPerComponent": 8}
70 if im.mode == "1":
71 if features.check("libtiff"):
72 decode_filter = "CCITTFaxDecode"
73 dict_obj["BitsPerComponent"] = 1
74 params = PdfParser.PdfArray(
75 [
76 PdfParser.PdfDict(
77 {
78 "K": -1,
79 "BlackIs1": True,
80 "Columns": width,
81 "Rows": height,
82 }
83 )
84 ]
85 )
86 else:
87 decode_filter = "DCTDecode"
88 dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
89 procset = "ImageB" # grayscale
90 elif im.mode == "L":
91 decode_filter = "DCTDecode"
92 # params = f"<< /Predictor 15 /Columns {width-2} >>"
93 dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
94 procset = "ImageB" # grayscale
95 elif im.mode == "LA":
96 decode_filter = "JPXDecode"
97 # params = f"<< /Predictor 15 /Columns {width-2} >>"
98 procset = "ImageB" # grayscale
99 dict_obj["SMaskInData"] = 1
100 elif im.mode == "P":
101 decode_filter = "ASCIIHexDecode"
102 palette = im.getpalette()
103 assert palette is not None
104 dict_obj["ColorSpace"] = [
105 PdfParser.PdfName("Indexed"),
106 PdfParser.PdfName("DeviceRGB"),
107 len(palette) // 3 - 1,
108 PdfParser.PdfBinary(palette),
109 ]
110 procset = "ImageI" # indexed color
112 if "transparency" in im.info:
113 smask = im.convert("LA").getchannel("A")
114 smask.encoderinfo = {}
116 image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0]
117 dict_obj["SMask"] = image_ref
118 elif im.mode == "RGB":
119 decode_filter = "DCTDecode"
120 dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB")
121 procset = "ImageC" # color images
122 elif im.mode == "RGBA":
123 decode_filter = "JPXDecode"
124 procset = "ImageC" # color images
125 dict_obj["SMaskInData"] = 1
126 elif im.mode == "CMYK":
127 decode_filter = "DCTDecode"
128 dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK")
129 procset = "ImageC" # color images
130 decode = [1, 0, 1, 0, 1, 0, 1, 0]
131 else:
132 msg = f"cannot save mode {im.mode}"
133 raise ValueError(msg)
135 #
136 # image
138 op = io.BytesIO()
140 if decode_filter == "ASCIIHexDecode":
141 ImageFile._save(im, op, [ImageFile._Tile("hex", (0, 0) + im.size, 0, im.mode)])
142 elif decode_filter == "CCITTFaxDecode":
143 im.save(
144 op,
145 "TIFF",
146 compression="group4",
147 # use a single strip
148 strip_size=math.ceil(width / 8) * height,
149 )
150 elif decode_filter == "DCTDecode":
151 from . import JpegImagePlugin
153 JpegImagePlugin._save(im, op, filename)
154 elif decode_filter == "JPXDecode":
155 from . import Jpeg2KImagePlugin
157 del dict_obj["BitsPerComponent"]
158 Jpeg2KImagePlugin._save(im, op, filename)
159 else:
160 msg = f"unsupported PDF filter ({decode_filter})"
161 raise ValueError(msg)
163 stream = op.getvalue()
164 filter: PdfParser.PdfArray | PdfParser.PdfName
165 if decode_filter == "CCITTFaxDecode":
166 stream = stream[8:]
167 filter = PdfParser.PdfArray([PdfParser.PdfName(decode_filter)])
168 else:
169 filter = PdfParser.PdfName(decode_filter)
171 image_ref = image_refs.pop(0)
172 existing_pdf.write_obj(
173 image_ref,
174 stream=stream,
175 Type=PdfParser.PdfName("XObject"),
176 Subtype=PdfParser.PdfName("Image"),
177 Width=width, # * 72.0 / x_resolution,
178 Height=height, # * 72.0 / y_resolution,
179 Filter=filter,
180 Decode=decode,
181 DecodeParms=params,
182 **dict_obj,
183 )
185 return image_ref, procset
188def _save(
189 im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
190) -> None:
191 is_appending = im.encoderinfo.get("append", False)
192 filename_str = filename.decode() if isinstance(filename, bytes) else filename
193 if is_appending:
194 existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="r+b")
195 else:
196 existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="w+b")
198 dpi = im.encoderinfo.get("dpi")
199 if dpi:
200 x_resolution = dpi[0]
201 y_resolution = dpi[1]
202 else:
203 x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0)
205 info = {
206 "title": (
207 None if is_appending else os.path.splitext(os.path.basename(filename))[0]
208 ),
209 "author": None,
210 "subject": None,
211 "keywords": None,
212 "creator": None,
213 "producer": None,
214 "creationDate": None if is_appending else time.gmtime(),
215 "modDate": None if is_appending else time.gmtime(),
216 }
217 for k, default in info.items():
218 v = im.encoderinfo.get(k) if k in im.encoderinfo else default
219 if v:
220 existing_pdf.info[k[0].upper() + k[1:]] = v
222 #
223 # make sure image data is available
224 im.load()
226 existing_pdf.start_writing()
227 existing_pdf.write_header()
228 existing_pdf.write_comment("created by Pillow PDF driver")
230 #
231 # pages
232 ims = [im]
233 if save_all:
234 append_images = im.encoderinfo.get("append_images", [])
235 for append_im in append_images:
236 append_im.encoderinfo = im.encoderinfo.copy()
237 ims.append(append_im)
238 number_of_pages = 0
239 image_refs = []
240 page_refs = []
241 contents_refs = []
242 for im in ims:
243 im_number_of_pages = 1
244 if save_all:
245 im_number_of_pages = getattr(im, "n_frames", 1)
246 number_of_pages += im_number_of_pages
247 for i in range(im_number_of_pages):
248 image_refs.append(existing_pdf.next_object_id(0))
249 if im.mode == "P" and "transparency" in im.info:
250 image_refs.append(existing_pdf.next_object_id(0))
252 page_refs.append(existing_pdf.next_object_id(0))
253 contents_refs.append(existing_pdf.next_object_id(0))
254 existing_pdf.pages.append(page_refs[-1])
256 #
257 # catalog and list of pages
258 existing_pdf.write_catalog()
260 page_number = 0
261 for im_sequence in ims:
262 im_pages: ImageSequence.Iterator | list[Image.Image] = (
263 ImageSequence.Iterator(im_sequence) if save_all else [im_sequence]
264 )
265 for im in im_pages:
266 image_ref, procset = _write_image(im, filename, existing_pdf, image_refs)
268 #
269 # page
271 existing_pdf.write_page(
272 page_refs[page_number],
273 Resources=PdfParser.PdfDict(
274 ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)],
275 XObject=PdfParser.PdfDict(image=image_ref),
276 ),
277 MediaBox=[
278 0,
279 0,
280 im.width * 72.0 / x_resolution,
281 im.height * 72.0 / y_resolution,
282 ],
283 Contents=contents_refs[page_number],
284 )
286 #
287 # page contents
289 page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % (
290 im.width * 72.0 / x_resolution,
291 im.height * 72.0 / y_resolution,
292 )
294 existing_pdf.write_obj(contents_refs[page_number], stream=page_contents)
296 page_number += 1
298 #
299 # trailer
300 existing_pdf.write_xref_and_trailer()
301 if hasattr(fp, "flush"):
302 fp.flush()
303 existing_pdf.close()
306#
307# --------------------------------------------------------------------
310Image.register_save("PDF", _save)
311Image.register_save_all("PDF", _save_all)
313Image.register_extension("PDF", ".pdf")
315Image.register_mime("PDF", "application/pdf")