Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/PdfImagePlugin.py: 12%
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
24__lazy_modules__ = {"io", "math"}
26import io
27import math
28import os
29import time
31from . import Image, ImageFile, ImageSequence, PdfParser, features
33TYPE_CHECKING = False
34if TYPE_CHECKING:
35 from typing import IO, Any
37#
38# --------------------------------------------------------------------
40# object ids:
41# 1. catalogue
42# 2. pages
43# 3. image
44# 4. page
45# 5. page contents
48def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
49 _save(im, fp, filename, save_all=True)
52##
53# (Internal) Image save plugin for the PDF format.
56def _write_image(
57 im: Image.Image,
58 filename: str | bytes,
59 existing_pdf: PdfParser.PdfParser,
60) -> tuple[PdfParser.IndirectReference, str]:
61 # FIXME: Should replace ASCIIHexDecode with RunLengthDecode
62 # (packbits) or LZWDecode (tiff/lzw compression). Note that
63 # PDF 1.2 also supports Flatedecode (zip compression).
65 params = None
66 decode = None
68 #
69 # Get image characteristics
71 width, height = im.size
73 dict_obj: dict[str, Any] = {"BitsPerComponent": 8}
74 if im.mode == "1":
75 if features.check("libtiff"):
76 decode_filter = "CCITTFaxDecode"
77 dict_obj["BitsPerComponent"] = 1
78 params = PdfParser.PdfArray(
79 [
80 PdfParser.PdfDict(
81 {
82 "K": -1,
83 "BlackIs1": True,
84 "Columns": width,
85 "Rows": height,
86 }
87 )
88 ]
89 )
90 else:
91 decode_filter = "DCTDecode"
92 dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
93 procset = "ImageB" # grayscale
94 elif im.mode == "L":
95 decode_filter = "DCTDecode"
96 # params = f"<< /Predictor 15 /Columns {width-2} >>"
97 dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
98 procset = "ImageB" # grayscale
99 elif im.mode == "LA":
100 decode_filter = "JPXDecode"
101 # params = f"<< /Predictor 15 /Columns {width-2} >>"
102 procset = "ImageB" # grayscale
103 dict_obj["SMaskInData"] = 1
104 elif im.mode == "P":
105 decode_filter = "ASCIIHexDecode"
106 palette = im.getpalette()
107 assert palette is not None
108 dict_obj["ColorSpace"] = [
109 PdfParser.PdfName("Indexed"),
110 PdfParser.PdfName("DeviceRGB"),
111 len(palette) // 3 - 1,
112 PdfParser.PdfBinary(palette),
113 ]
114 procset = "ImageI" # indexed color
116 if "transparency" in im.info:
117 smask = im.convert("LA").getchannel("A")
118 smask.encoderinfo = {}
120 image_ref = _write_image(smask, filename, existing_pdf)[0]
121 dict_obj["SMask"] = image_ref
122 elif im.mode == "RGB":
123 decode_filter = "DCTDecode"
124 dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB")
125 procset = "ImageC" # color images
126 elif im.mode == "RGBA":
127 decode_filter = "JPXDecode"
128 procset = "ImageC" # color images
129 dict_obj["SMaskInData"] = 1
130 elif im.mode == "CMYK":
131 decode_filter = "DCTDecode"
132 dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK")
133 procset = "ImageC" # color images
134 decode = [1, 0, 1, 0, 1, 0, 1, 0]
135 else:
136 msg = f"cannot save mode {im.mode}"
137 raise ValueError(msg)
139 #
140 # image
142 op = io.BytesIO()
144 if decode_filter == "ASCIIHexDecode":
145 ImageFile._save(im, op, [ImageFile._Tile("hex", args=im.mode)])
146 elif decode_filter == "CCITTFaxDecode":
147 im.save(
148 op,
149 "TIFF",
150 compression="group4",
151 # use a single strip
152 strip_size=math.ceil(width / 8) * height,
153 )
154 elif decode_filter == "DCTDecode":
155 from . import JpegImagePlugin
157 JpegImagePlugin._save(im, op, filename)
158 elif decode_filter == "JPXDecode":
159 from . import Jpeg2KImagePlugin
161 del dict_obj["BitsPerComponent"]
162 Jpeg2KImagePlugin._save(im, op, filename)
163 else:
164 msg = f"unsupported PDF filter ({decode_filter})"
165 raise ValueError(msg)
167 stream = op.getvalue()
168 filter: PdfParser.PdfArray | PdfParser.PdfName
169 if decode_filter == "CCITTFaxDecode":
170 stream = stream[8:]
171 filter = PdfParser.PdfArray([PdfParser.PdfName(decode_filter)])
172 else:
173 filter = PdfParser.PdfName(decode_filter)
175 image_ref = existing_pdf.write_obj(
176 None,
177 stream=stream,
178 Type=PdfParser.PdfName("XObject"),
179 Subtype=PdfParser.PdfName("Image"),
180 Width=width, # * 72.0 / x_resolution,
181 Height=height, # * 72.0 / y_resolution,
182 Filter=filter,
183 Decode=decode,
184 DecodeParms=params,
185 **dict_obj,
186 )
188 return image_ref, procset
191def _save(
192 im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
193) -> None:
194 is_appending = im.encoderinfo.get("append", False)
195 filename_str = filename.decode() if isinstance(filename, bytes) else filename
196 if is_appending:
197 existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="r+b")
198 else:
199 existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="w+b")
201 dpi = im.encoderinfo.get("dpi")
202 if dpi:
203 x_resolution = dpi[0]
204 y_resolution = dpi[1]
205 else:
206 x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0)
208 info = {
209 "title": (
210 None if is_appending else os.path.splitext(os.path.basename(filename))[0]
211 ),
212 "author": None,
213 "subject": None,
214 "keywords": None,
215 "creator": None,
216 "producer": None,
217 "creationDate": None if is_appending else time.gmtime(),
218 "modDate": None if is_appending else time.gmtime(),
219 }
220 for k, default in info.items():
221 v = im.encoderinfo.get(k) if k in im.encoderinfo else default
222 if v:
223 existing_pdf.info[k[0].upper() + k[1:]] = v
225 #
226 # make sure image data is available
227 im.load()
229 existing_pdf.start_writing()
230 existing_pdf.write_header()
231 existing_pdf.write_comment("created by Pillow PDF driver")
233 #
234 # pages
235 ims = [im]
236 if save_all:
237 append_images = im.encoderinfo.get("append_images", [])
238 for append_im in append_images:
239 append_im.encoderinfo = im.encoderinfo.copy()
240 ims.append(append_im)
241 number_of_pages = 0
242 page_refs = []
243 contents_refs = []
244 for im in ims:
245 im_number_of_pages = 1
246 if save_all:
247 im_number_of_pages = getattr(im, "n_frames", 1)
248 number_of_pages += im_number_of_pages
249 for i in range(im_number_of_pages):
250 page_refs.append(existing_pdf.next_object_id(0))
251 contents_refs.append(existing_pdf.next_object_id(0))
252 existing_pdf.pages.append(page_refs[-1])
254 #
255 # catalog and list of pages
256 existing_pdf.write_catalog()
258 page_number = 0
259 for im_sequence in ims:
260 im_pages: ImageSequence.Iterator | list[Image.Image] = (
261 ImageSequence.Iterator(im_sequence) if save_all else [im_sequence]
262 )
263 for im in im_pages:
264 image_ref, procset = _write_image(im, filename, existing_pdf)
266 #
267 # page
269 existing_pdf.write_page(
270 page_refs[page_number],
271 Resources=PdfParser.PdfDict(
272 ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)],
273 XObject=PdfParser.PdfDict(image=image_ref),
274 ),
275 MediaBox=[
276 0,
277 0,
278 im.width * 72.0 / x_resolution,
279 im.height * 72.0 / y_resolution,
280 ],
281 Contents=contents_refs[page_number],
282 )
284 #
285 # page contents
287 page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % (
288 im.width * 72.0 / x_resolution,
289 im.height * 72.0 / y_resolution,
290 )
292 existing_pdf.write_obj(contents_refs[page_number], stream=page_contents)
294 page_number += 1
296 #
297 # trailer
298 existing_pdf.write_xref_and_trailer()
299 if hasattr(fp, "flush"):
300 fp.flush()
301 existing_pdf.close()
304#
305# --------------------------------------------------------------------
308Image.register_save("PDF", _save)
309Image.register_save_all("PDF", _save_all)
311Image.register_extension("PDF", ".pdf")
313Image.register_mime("PDF", "application/pdf")