Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/EpsImagePlugin.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

256 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 

24import io 

25import os 

26import re 

27import subprocess 

28import sys 

29import tempfile 

30from typing import IO 

31 

32from . import Image, ImageFile 

33from ._binary import i32le as i32 

34 

35# -------------------------------------------------------------------- 

36 

37 

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

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

40 

41gs_binary: str | bool | None = None 

42gs_windows_binary = None 

43 

44 

45def has_ghostscript() -> bool: 

46 global gs_binary, gs_windows_binary 

47 if gs_binary is None: 

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

49 if gs_windows_binary is None: 

50 import shutil 

51 

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

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

54 gs_windows_binary = binary 

55 break 

56 else: 

57 gs_windows_binary = False 

58 gs_binary = gs_windows_binary 

59 else: 

60 try: 

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

62 gs_binary = "gs" 

63 except OSError: 

64 gs_binary = False 

65 return gs_binary is not False 

66 

67 

68def Ghostscript( 

69 tile: list[ImageFile._Tile], 

70 size: tuple[int, int], 

71 fp: IO[bytes], 

72 scale: int = 1, 

73 transparency: bool = False, 

74) -> Image.core.ImagingCore: 

75 """Render an image using Ghostscript""" 

76 global gs_binary 

77 if not has_ghostscript(): 

78 msg = "Unable to locate Ghostscript on paths" 

79 raise OSError(msg) 

80 assert isinstance(gs_binary, str) 

81 

82 # Unpack decoder tile 

83 args = tile[0].args 

84 assert isinstance(args, tuple) 

85 length, bbox = args 

86 

87 # Hack to support hi-res rendering 

88 scale = int(scale) or 1 

89 width = size[0] * scale 

90 height = size[1] * scale 

91 # resolution is dependent on bbox and size 

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

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

94 

95 out_fd, outfile = tempfile.mkstemp() 

96 os.close(out_fd) 

97 

98 infile_temp = None 

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

100 infile = fp.name 

101 else: 

102 in_fd, infile_temp = tempfile.mkstemp() 

103 os.close(in_fd) 

104 infile = infile_temp 

105 

106 # Ignore length and offset! 

107 # Ghostscript can read it 

108 # Copy whole file to read in Ghostscript 

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

110 # fetch length of fp 

111 fp.seek(0, io.SEEK_END) 

112 fsize = fp.tell() 

113 # ensure start position 

114 # go back 

115 fp.seek(0) 

116 lengthfile = fsize 

117 while lengthfile > 0: 

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

119 if not s: 

120 break 

121 lengthfile -= len(s) 

122 f.write(s) 

123 

124 if transparency: 

125 # "RGBA" 

126 device = "pngalpha" 

127 else: 

128 # "pnmraw" automatically chooses between 

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

130 device = "pnmraw" 

131 

132 # Build Ghostscript command 

133 command = [ 

134 gs_binary, 

135 "-q", # quiet mode 

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

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

138 "-dBATCH", # exit after processing 

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

140 "-dSAFER", # safe mode 

141 f"-sDEVICE={device}", 

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

143 # adjust for image origin 

144 "-c", 

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

146 "-f", 

147 infile, # input file 

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

149 "-c", 

150 "showpage", 

151 ] 

152 

153 # push data through Ghostscript 

154 try: 

155 startupinfo = None 

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

157 startupinfo = subprocess.STARTUPINFO() 

158 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 

159 subprocess.check_call(command, startupinfo=startupinfo) 

160 with Image.open(outfile) as out_im: 

161 out_im.load() 

162 return out_im.im.copy() 

163 finally: 

164 try: 

165 os.unlink(outfile) 

166 if infile_temp: 

167 os.unlink(infile_temp) 

168 except OSError: 

169 pass 

170 

171 

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

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

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

175 ) 

176 

177 

178## 

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

180# a few variants of this format. 

181 

182 

183class EpsImageFile(ImageFile.ImageFile): 

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

185 

186 format = "EPS" 

187 format_description = "Encapsulated Postscript" 

188 

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

190 

191 def _open(self) -> None: 

192 (length, offset) = self._find_offset(self.fp) 

193 

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

195 self.fp.seek(offset) 

196 

197 self._mode = "RGB" 

198 

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

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

201 bounding_box: list[int] | None = None 

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

203 

204 byte_arr = bytearray(255) 

205 bytes_mv = memoryview(byte_arr) 

206 bytes_read = 0 

207 reading_header_comments = True 

208 reading_trailer_comments = False 

209 trailer_reached = False 

210 

211 def check_required_header_comments() -> None: 

212 """ 

213 The EPS specification requires that some headers exist. 

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

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

216 """ 

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

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

219 raise SyntaxError(msg) 

220 if "BoundingBox" not in self.info: 

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

222 raise SyntaxError(msg) 

223 

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

225 nonlocal bounding_box, reading_trailer_comments 

226 try: 

227 m = split.match(s) 

228 except re.error as e: 

229 msg = "not an EPS file" 

230 raise SyntaxError(msg) from e 

231 

232 if not m: 

233 return False 

234 

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

236 self.info[k] = v 

237 if k == "BoundingBox": 

238 if v == "(atend)": 

239 reading_trailer_comments = True 

240 elif not bounding_box or (trailer_reached and reading_trailer_comments): 

241 try: 

242 # Note: The DSC spec says that BoundingBox 

243 # fields should be integers, but some drivers 

244 # put floating point values there anyway. 

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

246 except Exception: 

247 pass 

248 return True 

249 

250 while True: 

251 byte = self.fp.read(1) 

252 if byte == b"": 

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

254 if bytes_read == 0: 

255 if reading_header_comments: 

256 check_required_header_comments() 

257 break 

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

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

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

261 # continue reading 

262 if bytes_read == 0: 

263 continue 

264 else: 

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

266 # 255 characters, not including line ending characters 

267 if bytes_read >= 255: 

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

269 # otherwise assume it's binary data 

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

271 msg = "not an EPS file" 

272 raise SyntaxError(msg) 

273 else: 

274 if reading_header_comments: 

275 check_required_header_comments() 

276 reading_header_comments = False 

277 # reset bytes_read so we can keep reading 

278 # data until the end of the line 

279 bytes_read = 0 

280 byte_arr[bytes_read] = byte[0] 

281 bytes_read += 1 

282 continue 

283 

284 if reading_header_comments: 

285 # Load EPS header 

286 

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

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

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

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

291 check_required_header_comments() 

292 reading_header_comments = False 

293 continue 

294 

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

296 if not read_comment(s): 

297 m = field.match(s) 

298 if m: 

299 k = m.group(1) 

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

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

302 else: 

303 self.info[k] = "" 

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

305 # handle non-DSC PostScript comments that some 

306 # tools mistakenly put in the Comments section 

307 pass 

308 else: 

309 msg = "bad EPS header" 

310 raise OSError(msg) 

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

312 # Check for an "ImageData" descriptor 

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

314 

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

316 # don't read another one. 

317 if imagedata_size: 

318 bytes_read = 0 

319 continue 

320 

321 # Values: 

322 # columns 

323 # rows 

324 # bit depth (1 or 8) 

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

326 # number of padding channels 

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

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

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

330 # consisting only of this quoted value) 

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

332 columns, rows, bit_depth, mode_id = ( 

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

334 ) 

335 

336 if bit_depth == 1: 

337 self._mode = "1" 

338 elif bit_depth == 8: 

339 try: 

340 self._mode = self.mode_map[mode_id] 

341 except ValueError: 

342 break 

343 else: 

344 break 

345 

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

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

348 imagedata_size = columns, rows 

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

350 break 

351 elif trailer_reached and reading_trailer_comments: 

352 # Load EPS trailer 

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

354 read_comment(s) 

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

356 trailer_reached = True 

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

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

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

360 bytes_read = 0 

361 

362 # A "BoundingBox" is always required, 

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

364 if not bounding_box: 

365 msg = "cannot determine EPS bounding box" 

366 raise OSError(msg) 

367 

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

369 self._size = imagedata_size or ( 

370 bounding_box[2] - bounding_box[0], 

371 bounding_box[3] - bounding_box[1], 

372 ) 

373 

374 self.tile = [ 

375 ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box)) 

376 ] 

377 

378 def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]: 

379 s = fp.read(4) 

380 

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

382 # for HEAD without binary preview 

383 fp.seek(0, io.SEEK_END) 

384 length = fp.tell() 

385 offset = 0 

386 elif i32(s) == 0xC6D3D0C5: 

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

388 # EPS can contain binary data 

389 # or start directly with latin coding 

390 # more info see: 

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

392 s = fp.read(8) 

393 offset = i32(s) 

394 length = i32(s, 4) 

395 else: 

396 msg = "not an EPS file" 

397 raise SyntaxError(msg) 

398 

399 return length, offset 

400 

401 def load( 

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

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

404 # Load EPS via Ghostscript 

405 if self.tile: 

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

407 self._mode = self.im.mode 

408 self._size = self.im.size 

409 self.tile = [] 

410 return Image.Image.load(self) 

411 

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

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

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

415 pass 

416 

417 

418# -------------------------------------------------------------------- 

419 

420 

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

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

423 

424 # make sure image data is available 

425 im.load() 

426 

427 # determine PostScript image mode 

428 if im.mode == "L": 

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

430 elif im.mode == "RGB": 

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

432 elif im.mode == "CMYK": 

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

434 else: 

435 msg = "image mode is not supported" 

436 raise ValueError(msg) 

437 

438 if eps: 

439 # write EPS header 

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

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

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

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

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

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

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

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

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

449 

450 # image header 

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

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

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

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

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

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

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

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

459 if hasattr(fp, "flush"): 

460 fp.flush() 

461 

462 ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)]) 

463 

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

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

466 if hasattr(fp, "flush"): 

467 fp.flush() 

468 

469 

470# -------------------------------------------------------------------- 

471 

472 

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

474 

475Image.register_save(EpsImageFile.format, _save) 

476 

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

478 

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