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

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

258 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 assert self.fp is not None 

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

194 

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

196 self.fp.seek(offset) 

197 

198 self._mode = "RGB" 

199 

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

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

202 bounding_box: list[int] | None = None 

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

204 

205 byte_arr = bytearray(255) 

206 bytes_mv = memoryview(byte_arr) 

207 bytes_read = 0 

208 reading_header_comments = True 

209 reading_trailer_comments = False 

210 trailer_reached = False 

211 

212 def check_required_header_comments() -> None: 

213 """ 

214 The EPS specification requires that some headers exist. 

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

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

217 """ 

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

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

220 raise SyntaxError(msg) 

221 if "BoundingBox" not in self.info: 

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

223 raise SyntaxError(msg) 

224 

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

226 nonlocal bounding_box, reading_trailer_comments 

227 try: 

228 m = split.match(s) 

229 except re.error as e: 

230 msg = "not an EPS file" 

231 raise SyntaxError(msg) from e 

232 

233 if not m: 

234 return False 

235 

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

237 self.info[k] = v 

238 if k == "BoundingBox": 

239 if v == "(atend)": 

240 reading_trailer_comments = True 

241 elif not bounding_box or (trailer_reached and reading_trailer_comments): 

242 try: 

243 # Note: The DSC spec says that BoundingBox 

244 # fields should be integers, but some drivers 

245 # put floating point values there anyway. 

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

247 except Exception: 

248 pass 

249 return True 

250 

251 while True: 

252 byte = self.fp.read(1) 

253 if byte == b"": 

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

255 if bytes_read == 0: 

256 if reading_header_comments: 

257 check_required_header_comments() 

258 break 

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

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

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

262 # continue reading 

263 if bytes_read == 0: 

264 continue 

265 else: 

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

267 # 255 characters, not including line ending characters 

268 if bytes_read >= 255: 

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

270 # otherwise assume it's binary data 

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

272 msg = "not an EPS file" 

273 raise SyntaxError(msg) 

274 else: 

275 if reading_header_comments: 

276 check_required_header_comments() 

277 reading_header_comments = False 

278 # reset bytes_read so we can keep reading 

279 # data until the end of the line 

280 bytes_read = 0 

281 byte_arr[bytes_read] = byte[0] 

282 bytes_read += 1 

283 continue 

284 

285 if reading_header_comments: 

286 # Load EPS header 

287 

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

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

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

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

292 check_required_header_comments() 

293 reading_header_comments = False 

294 continue 

295 

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

297 if not read_comment(s): 

298 m = field.match(s) 

299 if m: 

300 k = m.group(1) 

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

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

303 else: 

304 self.info[k] = "" 

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

306 # handle non-DSC PostScript comments that some 

307 # tools mistakenly put in the Comments section 

308 pass 

309 else: 

310 msg = "bad EPS header" 

311 raise OSError(msg) 

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

313 # Check for an "ImageData" descriptor 

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

315 

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

317 # don't read another one. 

318 if imagedata_size: 

319 bytes_read = 0 

320 continue 

321 

322 # Values: 

323 # columns 

324 # rows 

325 # bit depth (1 or 8) 

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

327 # number of padding channels 

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

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

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

331 # consisting only of this quoted value) 

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

333 columns, rows, bit_depth, mode_id = ( 

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

335 ) 

336 

337 if bit_depth == 1: 

338 self._mode = "1" 

339 elif bit_depth == 8: 

340 try: 

341 self._mode = self.mode_map[mode_id] 

342 except ValueError: 

343 break 

344 else: 

345 break 

346 

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

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

349 imagedata_size = columns, rows 

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

351 break 

352 elif trailer_reached and reading_trailer_comments: 

353 # Load EPS trailer 

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

355 read_comment(s) 

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

357 trailer_reached = True 

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

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

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

361 bytes_read = 0 

362 

363 # A "BoundingBox" is always required, 

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

365 if not bounding_box: 

366 msg = "cannot determine EPS bounding box" 

367 raise OSError(msg) 

368 

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

370 self._size = imagedata_size or ( 

371 bounding_box[2] - bounding_box[0], 

372 bounding_box[3] - bounding_box[1], 

373 ) 

374 

375 self.tile = [ 

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

377 ] 

378 

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

380 s = fp.read(4) 

381 

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

383 # for HEAD without binary preview 

384 fp.seek(0, io.SEEK_END) 

385 length = fp.tell() 

386 offset = 0 

387 elif i32(s) == 0xC6D3D0C5: 

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

389 # EPS can contain binary data 

390 # or start directly with latin coding 

391 # more info see: 

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

393 s = fp.read(8) 

394 offset = i32(s) 

395 length = i32(s, 4) 

396 else: 

397 msg = "not an EPS file" 

398 raise SyntaxError(msg) 

399 

400 return length, offset 

401 

402 def load( 

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

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

405 # Load EPS via Ghostscript 

406 if self.tile: 

407 assert self.fp is not None 

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

409 self._mode = self.im.mode 

410 self._size = self.im.size 

411 self.tile = [] 

412 return Image.Image.load(self) 

413 

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

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

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

417 pass 

418 

419 

420# -------------------------------------------------------------------- 

421 

422 

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

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

425 

426 # make sure image data is available 

427 im.load() 

428 

429 # determine PostScript image mode 

430 if im.mode == "L": 

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

432 elif im.mode == "RGB": 

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

434 elif im.mode == "CMYK": 

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

436 else: 

437 msg = "image mode is not supported" 

438 raise ValueError(msg) 

439 

440 if eps: 

441 # write EPS header 

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

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

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

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

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

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

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

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

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

451 

452 # image header 

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

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

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

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

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

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

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

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

461 if hasattr(fp, "flush"): 

462 fp.flush() 

463 

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

465 

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

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

468 if hasattr(fp, "flush"): 

469 fp.flush() 

470 

471 

472# -------------------------------------------------------------------- 

473 

474 

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

476 

477Image.register_save(EpsImageFile.format, _save) 

478 

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

480 

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