Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/EpsImagePlugin.py: 44%

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

265 statements  

1# 

2# The Python Imaging Library. 

3# $Id$ 

4# 

5# EPS file handling 

6# 

7# History: 

8# 1995-09-01 fl Created (0.1) 

9# 1996-05-18 fl Don't choke on "atend" fields, Ghostscript interface (0.2) 

10# 1996-08-22 fl Don't choke on floating point BoundingBox values 

11# 1996-08-23 fl Handle files from Macintosh (0.3) 

12# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) 

13# 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5) 

14# 2014-05-07 e Handling of EPS with binary preview and fixed resolution 

15# resizing 

16# 

17# Copyright (c) 1997-2003 by Secret Labs AB. 

18# Copyright (c) 1995-2003 by Fredrik Lundh 

19# 

20# See the README file for information on usage and redistribution. 

21# 

22from __future__ import annotations 

23 

24__lazy_modules__ = {"PIL._binary", "io", "subprocess"} 

25 

26import io 

27import os 

28import re 

29import subprocess 

30import sys 

31 

32from . import Image, ImageFile 

33from ._binary import i32le as i32 

34 

35TYPE_CHECKING = False 

36if TYPE_CHECKING: 

37 from typing import IO 

38 

39# -------------------------------------------------------------------- 

40 

41 

42split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") 

43field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") 

44 

45gs_binary: str | bool | None = None 

46gs_windows_binary = None 

47 

48 

49def has_ghostscript() -> bool: 

50 global gs_binary, gs_windows_binary 

51 if gs_binary is None: 

52 if sys.platform.startswith("win"): 

53 if gs_windows_binary is None: 

54 import shutil 

55 

56 for binary in ("gswin32c", "gswin64c", "gs"): 

57 if shutil.which(binary) is not None: 

58 gs_windows_binary = binary 

59 break 

60 else: 

61 gs_windows_binary = False 

62 gs_binary = gs_windows_binary 

63 else: 

64 try: 

65 subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL) 

66 gs_binary = "gs" 

67 except OSError: 

68 gs_binary = False 

69 return gs_binary is not False 

70 

71 

72def Ghostscript( 

73 tile: list[ImageFile._Tile], 

74 size: tuple[int, int], 

75 fp: IO[bytes], 

76 scale: int = 1, 

77 transparency: bool = False, 

78) -> Image.core.ImagingCore: 

79 """Render an image using Ghostscript""" 

80 global gs_binary 

81 if not has_ghostscript(): 

82 msg = "Unable to locate Ghostscript on paths" 

83 raise OSError(msg) 

84 assert isinstance(gs_binary, str) 

85 

86 # Unpack decoder tile 

87 args = tile[0].args 

88 assert isinstance(args, tuple) 

89 length, bbox = args 

90 

91 # Hack to support hi-res rendering 

92 scale = int(scale) or 1 

93 width = size[0] * scale 

94 height = size[1] * scale 

95 Image._decompression_bomb_check((width, height)) 

96 # resolution is dependent on bbox and size 

97 res_x = 72.0 * width / (bbox[2] - bbox[0]) 

98 res_y = 72.0 * height / (bbox[3] - bbox[1]) 

99 

100 import tempfile 

101 

102 out_fd, outfile = tempfile.mkstemp() 

103 os.close(out_fd) 

104 

105 infile_temp = None 

106 if hasattr(fp, "name") and os.path.exists(fp.name): 

107 # Make the path absolute so that a filename beginning with "-" or "@" 

108 # can never be interpreted by Ghostscript's argument parser 

109 # as an option or an @-file token 

110 infile = os.path.abspath(fp.name) 

111 else: 

112 in_fd, infile_temp = tempfile.mkstemp() 

113 os.close(in_fd) 

114 infile = infile_temp 

115 

116 # Ignore length and offset! 

117 # Ghostscript can read it 

118 # Copy whole file to read in Ghostscript 

119 with open(infile_temp, "wb") as f: 

120 # fetch length of fp 

121 fp.seek(0, io.SEEK_END) 

122 fsize = fp.tell() 

123 # ensure start position 

124 # go back 

125 fp.seek(0) 

126 lengthfile = fsize 

127 while lengthfile > 0: 

128 s = fp.read(min(lengthfile, 100 * 1024)) 

129 if not s: 

130 break 

131 lengthfile -= len(s) 

132 f.write(s) 

133 

134 if transparency: 

135 # "RGBA" 

136 device = "pngalpha" 

137 else: 

138 # "pnmraw" automatically chooses between 

139 # PBM ("1"), PGM ("L"), and PPM ("RGB"). 

140 device = "pnmraw" 

141 

142 # Build Ghostscript command 

143 command = [ 

144 gs_binary, 

145 "-q", # quiet mode 

146 f"-g{width:d}x{height:d}", # set output geometry (pixels) 

147 f"-r{res_x:f}x{res_y:f}", # set input DPI (dots per inch) 

148 "-dBATCH", # exit after processing 

149 "-dNOPAUSE", # don't pause between pages 

150 "-dSAFER", # safe mode 

151 f"-sDEVICE={device}", 

152 f"-sOutputFile={outfile}", # output file 

153 # adjust for image origin 

154 "-c", 

155 f"{-bbox[0]} {-bbox[1]} translate", 

156 "-f", 

157 infile, # input file 

158 # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272) 

159 "-c", 

160 "showpage", 

161 ] 

162 

163 # push data through Ghostscript 

164 try: 

165 startupinfo = None 

166 if sys.platform.startswith("win"): 

167 startupinfo = subprocess.STARTUPINFO() 

168 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 

169 subprocess.check_call(command, startupinfo=startupinfo) 

170 with Image.open(outfile) as out_im: 

171 out_im.load() 

172 return out_im.im.copy() 

173 finally: 

174 try: 

175 os.unlink(outfile) 

176 if infile_temp: 

177 os.unlink(infile_temp) 

178 except OSError: 

179 pass 

180 

181 

182def _accept(prefix: bytes) -> bool: 

183 return prefix.startswith(b"%!PS") or ( 

184 len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5 

185 ) 

186 

187 

188## 

189# Image plugin for Encapsulated PostScript. This plugin supports only 

190# a few variants of this format. 

191 

192 

193class EpsImageFile(ImageFile.ImageFile): 

194 """EPS File Parser for the Python Imaging Library""" 

195 

196 format = "EPS" 

197 format_description = "Encapsulated Postscript" 

198 

199 mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"} 

200 

201 def _open(self) -> None: 

202 length, offset = self._find_offset() 

203 

204 # go to offset - start of "%!PS" 

205 assert self.fp is not None 

206 self.fp.seek(offset) 

207 

208 self._mode = "RGB" 

209 

210 # When reading header comments, the first comment is used. 

211 # When reading trailer comments, the last comment is used. 

212 bounding_box: list[int] | None = None 

213 imagedata_size: tuple[int, int] | None = None 

214 

215 byte_arr = bytearray(255) 

216 bytes_mv = memoryview(byte_arr) 

217 bytes_read = 0 

218 reading_header_comments = True 

219 reading_trailer_comments = False 

220 trailer_reached = False 

221 

222 def check_required_header_comments() -> None: 

223 """ 

224 The EPS specification requires that some headers exist. 

225 This should be checked when the header comments formally end, 

226 when image data starts, or when the file ends, whichever comes first. 

227 """ 

228 if "PS-Adobe" not in self.info: 

229 msg = 'EPS header missing "%!PS-Adobe" comment' 

230 raise SyntaxError(msg) 

231 if "BoundingBox" not in self.info: 

232 msg = 'EPS header missing "%%BoundingBox" comment' 

233 raise SyntaxError(msg) 

234 

235 def read_comment(s: str) -> bool: 

236 nonlocal bounding_box, reading_trailer_comments 

237 try: 

238 m = split.match(s) 

239 except re.error as e: 

240 msg = "not an EPS file" 

241 raise SyntaxError(msg) from e 

242 

243 if not m: 

244 return False 

245 

246 k, v = m.group(1, 2) 

247 self.info[k] = v 

248 if k == "BoundingBox": 

249 if v == "(atend)": 

250 reading_trailer_comments = True 

251 elif not bounding_box or (trailer_reached and reading_trailer_comments): 

252 try: 

253 # Note: The DSC spec says that BoundingBox 

254 # fields should be integers, but some drivers 

255 # put floating point values there anyway. 

256 bounding_box = [int(float(i)) for i in v.split()] 

257 except Exception: 

258 pass 

259 return True 

260 

261 while True: 

262 byte = self.fp.read(1) 

263 if byte == b"": 

264 # if we didn't read a byte we must be at the end of the file 

265 if bytes_read == 0: 

266 if reading_header_comments: 

267 check_required_header_comments() 

268 break 

269 elif byte in b"\r\n": 

270 # if we read a line ending character, ignore it and parse what 

271 # we have already read. if we haven't read any other characters, 

272 # continue reading 

273 if bytes_read == 0: 

274 continue 

275 else: 

276 # ASCII/hexadecimal lines in an EPS file must not exceed 

277 # 255 characters, not including line ending characters 

278 if bytes_read >= 255: 

279 # only enforce this for lines starting with a "%", 

280 # otherwise assume it's binary data 

281 if byte_arr[0] == ord("%"): 

282 msg = "not an EPS file" 

283 raise SyntaxError(msg) 

284 else: 

285 if reading_header_comments: 

286 check_required_header_comments() 

287 reading_header_comments = False 

288 # reset bytes_read so we can keep reading 

289 # data until the end of the line 

290 bytes_read = 0 

291 byte_arr[bytes_read] = byte[0] 

292 bytes_read += 1 

293 continue 

294 

295 if reading_header_comments: 

296 # Load EPS header 

297 

298 # if this line doesn't start with a "%", 

299 # or does start with "%%EndComments", 

300 # then we've reached the end of the header/comments 

301 if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments": 

302 check_required_header_comments() 

303 reading_header_comments = False 

304 continue 

305 

306 s = str(bytes_mv[:bytes_read], "latin-1") 

307 if not read_comment(s): 

308 m = field.match(s) 

309 if m: 

310 k = m.group(1) 

311 if k.startswith("PS-Adobe"): 

312 self.info["PS-Adobe"] = k[9:] 

313 else: 

314 self.info[k] = "" 

315 elif s[0] == "%": 

316 # handle non-DSC PostScript comments that some 

317 # tools mistakenly put in the Comments section 

318 pass 

319 else: 

320 msg = "bad EPS header" 

321 raise OSError(msg) 

322 elif bytes_mv[:11] == b"%ImageData:": 

323 # Check for an "ImageData" descriptor 

324 # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 

325 

326 # If we've already read an "ImageData" descriptor, 

327 # don't read another one. 

328 if imagedata_size: 

329 bytes_read = 0 

330 continue 

331 

332 # Values: 

333 # columns 

334 # rows 

335 # bit depth (1 or 8) 

336 # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) 

337 # number of padding channels 

338 # block size (number of bytes per row per channel) 

339 # binary/ascii (1: binary, 2: ascii) 

340 # data start identifier (the image data follows after a single line 

341 # consisting only of this quoted value) 

342 image_data_values = byte_arr[11:bytes_read].split(None, 7) 

343 columns, rows, bit_depth, mode_id = ( 

344 int(value) for value in image_data_values[:4] 

345 ) 

346 

347 if bit_depth == 1: 

348 self._mode = "1" 

349 elif bit_depth == 8: 

350 try: 

351 self._mode = self.mode_map[mode_id] 

352 except ValueError: 

353 break 

354 else: 

355 break 

356 

357 # Parse the columns and rows after checking the bit depth and mode 

358 # in case the bit depth and/or mode are invalid. 

359 imagedata_size = columns, rows 

360 elif bytes_mv[:5] == b"%%EOF": 

361 break 

362 elif trailer_reached and reading_trailer_comments: 

363 # Load EPS trailer 

364 s = str(bytes_mv[:bytes_read], "latin-1") 

365 read_comment(s) 

366 elif bytes_mv[:9] == b"%%Trailer": 

367 trailer_reached = True 

368 elif bytes_mv[:14] == b"%%BeginBinary:": 

369 bytecount = int(byte_arr[14:bytes_read]) 

370 if bytecount < 0: 

371 msg = "BeginBinary bytecount cannot be negative" 

372 raise ValueError(msg) 

373 self.fp.seek(bytecount, os.SEEK_CUR) 

374 bytes_read = 0 

375 

376 # A "BoundingBox" is always required, 

377 # even if an "ImageData" descriptor size exists. 

378 if not bounding_box: 

379 msg = "cannot determine EPS bounding box" 

380 raise OSError(msg) 

381 

382 # An "ImageData" size takes precedence over the "BoundingBox". 

383 self._size = imagedata_size or ( 

384 bounding_box[2] - bounding_box[0], 

385 bounding_box[3] - bounding_box[1], 

386 ) 

387 

388 self.tile = [ImageFile._Tile("eps", None, offset, (length, bounding_box))] 

389 

390 def _find_offset(self) -> tuple[int, int]: 

391 assert self.fp is not None 

392 s = self.fp.read(4) 

393 

394 if s == b"%!PS": 

395 # for HEAD without binary preview 

396 length = self._get_fp_length() 

397 offset = 0 

398 elif i32(s) == 0xC6D3D0C5: 

399 # FIX for: Some EPS file not handled correctly / issue #302 

400 # EPS can contain binary data 

401 # or start directly with latin coding 

402 # more info see: 

403 # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf 

404 s = self.fp.read(8) 

405 offset = i32(s) 

406 length = i32(s, 4) 

407 else: 

408 msg = "not an EPS file" 

409 raise SyntaxError(msg) 

410 

411 return length, offset 

412 

413 def load( 

414 self, scale: int = 1, transparency: bool = False 

415 ) -> Image.core.PixelAccess | None: 

416 # Load EPS via Ghostscript 

417 if self.tile: 

418 assert self.fp is not None 

419 self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) 

420 self._mode = self.im.mode 

421 self._size = self.im.size 

422 self.tile = [] 

423 return Image.Image.load(self) 

424 

425 def load_seek(self, pos: int) -> None: 

426 # we can't incrementally load, so force ImageFile.parser to 

427 # use our custom load method by defining this method. 

428 pass 

429 

430 

431# -------------------------------------------------------------------- 

432 

433 

434def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None: 

435 """EPS Writer for the Python Imaging Library.""" 

436 

437 # make sure image data is available 

438 im.load() 

439 

440 # determine PostScript image mode 

441 if im.mode == "L": 

442 operator = (8, 1, b"image") 

443 elif im.mode == "RGB": 

444 operator = (8, 3, b"false 3 colorimage") 

445 elif im.mode == "CMYK": 

446 operator = (8, 4, b"false 4 colorimage") 

447 else: 

448 msg = "image mode is not supported" 

449 raise ValueError(msg) 

450 

451 if eps: 

452 # write EPS header 

453 fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n") 

454 fp.write(b"%%Creator: PIL 0.1 EpsEncode\n") 

455 # fp.write("%%CreationDate: %s"...) 

456 fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size) 

457 fp.write(b"%%Pages: 1\n") 

458 fp.write(b"%%EndComments\n") 

459 fp.write(b"%%Page: 1 1\n") 

460 fp.write(b"%%ImageData: %d %d " % im.size) 

461 fp.write(b'%d %d 0 1 1 "%s"\n' % operator) 

462 

463 # image header 

464 fp.write(b"gsave\n") 

465 fp.write(b"10 dict begin\n") 

466 fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1])) 

467 fp.write(b"%d %d scale\n" % im.size) 

468 fp.write(b"%d %d 8\n" % im.size) # <= bits 

469 fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1])) 

470 fp.write(b"{ currentfile buf readhexstring pop } bind\n") 

471 fp.write(operator[2] + b"\n") 

472 if hasattr(fp, "flush"): 

473 fp.flush() 

474 

475 ImageFile._save(im, fp, [ImageFile._Tile("eps")]) 

476 

477 fp.write(b"\n%%%%EndBinary\n") 

478 fp.write(b"grestore end\n") 

479 if hasattr(fp, "flush"): 

480 fp.flush() 

481 

482 

483# -------------------------------------------------------------------- 

484 

485 

486Image.register_open(EpsImageFile.format, EpsImageFile, _accept) 

487 

488Image.register_save(EpsImageFile.format, _save) 

489 

490Image.register_extensions(EpsImageFile.format, [".ps", ".eps"]) 

491 

492Image.register_mime(EpsImageFile.format, "application/postscript")