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

137 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 

24__lazy_modules__ = {"io", "math"} 

25 

26import io 

27import math 

28import os 

29import time 

30 

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

32 

33TYPE_CHECKING = False 

34if TYPE_CHECKING: 

35 from typing import IO, Any 

36 

37# 

38# -------------------------------------------------------------------- 

39 

40# object ids: 

41# 1. catalogue 

42# 2. pages 

43# 3. image 

44# 4. page 

45# 5. page contents 

46 

47 

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

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

50 

51 

52## 

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

54 

55 

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

64 

65 params = None 

66 decode = None 

67 

68 # 

69 # Get image characteristics 

70 

71 width, height = im.size 

72 

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 

115 

116 if "transparency" in im.info: 

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

118 smask.encoderinfo = {} 

119 

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) 

138 

139 # 

140 # image 

141 

142 op = io.BytesIO() 

143 

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 

156 

157 JpegImagePlugin._save(im, op, filename) 

158 elif decode_filter == "JPXDecode": 

159 from . import Jpeg2KImagePlugin 

160 

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) 

166 

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) 

174 

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 ) 

187 

188 return image_ref, procset 

189 

190 

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

200 

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) 

207 

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 

224 

225 # 

226 # make sure image data is available 

227 im.load() 

228 

229 existing_pdf.start_writing() 

230 existing_pdf.write_header() 

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

232 

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

253 

254 # 

255 # catalog and list of pages 

256 existing_pdf.write_catalog() 

257 

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) 

265 

266 # 

267 # page 

268 

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 ) 

283 

284 # 

285 # page contents 

286 

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 ) 

291 

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

293 

294 page_number += 1 

295 

296 # 

297 # trailer 

298 existing_pdf.write_xref_and_trailer() 

299 if hasattr(fp, "flush"): 

300 fp.flush() 

301 existing_pdf.close() 

302 

303 

304# 

305# -------------------------------------------------------------------- 

306 

307 

308Image.register_save("PDF", _save) 

309Image.register_save_all("PDF", _save_all) 

310 

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

312 

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