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

139 statements  

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# 

18 

19## 

20# Image plugin for PDF images (output only). 

21## 

22from __future__ import annotations 

23 

24import io 

25import math 

26import os 

27import time 

28from typing import IO, Any 

29 

30from . import Image, ImageFile, ImageSequence, PdfParser, features 

31 

32# 

33# -------------------------------------------------------------------- 

34 

35# object ids: 

36# 1. catalogue 

37# 2. pages 

38# 3. image 

39# 4. page 

40# 5. page contents 

41 

42 

43def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 

44 _save(im, fp, filename, save_all=True) 

45 

46 

47## 

48# (Internal) Image save plugin for the PDF format. 

49 

50 

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). 

60 

61 params = None 

62 decode = None 

63 

64 # 

65 # Get image characteristics 

66 

67 width, height = im.size 

68 

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 

111 

112 if "transparency" in im.info: 

113 smask = im.convert("LA").getchannel("A") 

114 smask.encoderinfo = {} 

115 

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) 

134 

135 # 

136 # image 

137 

138 op = io.BytesIO() 

139 

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 

152 

153 JpegImagePlugin._save(im, op, filename) 

154 elif decode_filter == "JPXDecode": 

155 from . import Jpeg2KImagePlugin 

156 

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) 

162 

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) 

170 

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 ) 

184 

185 return image_ref, procset 

186 

187 

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") 

197 

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) 

204 

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 

221 

222 # 

223 # make sure image data is available 

224 im.load() 

225 

226 existing_pdf.start_writing() 

227 existing_pdf.write_header() 

228 existing_pdf.write_comment("created by Pillow PDF driver") 

229 

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)) 

251 

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]) 

255 

256 # 

257 # catalog and list of pages 

258 existing_pdf.write_catalog() 

259 

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) 

267 

268 # 

269 # page 

270 

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 ) 

285 

286 # 

287 # page contents 

288 

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 ) 

293 

294 existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) 

295 

296 page_number += 1 

297 

298 # 

299 # trailer 

300 existing_pdf.write_xref_and_trailer() 

301 if hasattr(fp, "flush"): 

302 fp.flush() 

303 existing_pdf.close() 

304 

305 

306# 

307# -------------------------------------------------------------------- 

308 

309 

310Image.register_save("PDF", _save) 

311Image.register_save_all("PDF", _save_all) 

312 

313Image.register_extension("PDF", ".pdf") 

314 

315Image.register_mime("PDF", "application/pdf")