Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pillow-10.4.0-py3.8-linux-x86_64.egg/PIL/EpsImagePlugin.py: 14%

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

266 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 

34from ._deprecate import deprecate 

35 

36# -------------------------------------------------------------------- 

37 

38 

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

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

41 

42gs_binary: str | bool | None = None 

43gs_windows_binary = None 

44 

45 

46def has_ghostscript() -> bool: 

47 global gs_binary, gs_windows_binary 

48 if gs_binary is None: 

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

50 if gs_windows_binary is None: 

51 import shutil 

52 

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

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

55 gs_windows_binary = binary 

56 break 

57 else: 

58 gs_windows_binary = False 

59 gs_binary = gs_windows_binary 

60 else: 

61 try: 

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

63 gs_binary = "gs" 

64 except OSError: 

65 gs_binary = False 

66 return gs_binary is not False 

67 

68 

69def Ghostscript(tile, size, fp, scale=1, transparency=False): 

70 """Render an image using Ghostscript""" 

71 global gs_binary 

72 if not has_ghostscript(): 

73 msg = "Unable to locate Ghostscript on paths" 

74 raise OSError(msg) 

75 

76 # Unpack decoder tile 

77 decoder, tile, offset, data = tile[0] 

78 length, bbox = data 

79 

80 # Hack to support hi-res rendering 

81 scale = int(scale) or 1 

82 width = size[0] * scale 

83 height = size[1] * scale 

84 # resolution is dependent on bbox and size 

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

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

87 

88 out_fd, outfile = tempfile.mkstemp() 

89 os.close(out_fd) 

90 

91 infile_temp = None 

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

93 infile = fp.name 

94 else: 

95 in_fd, infile_temp = tempfile.mkstemp() 

96 os.close(in_fd) 

97 infile = infile_temp 

98 

99 # Ignore length and offset! 

100 # Ghostscript can read it 

101 # Copy whole file to read in Ghostscript 

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

103 # fetch length of fp 

104 fp.seek(0, io.SEEK_END) 

105 fsize = fp.tell() 

106 # ensure start position 

107 # go back 

108 fp.seek(0) 

109 lengthfile = fsize 

110 while lengthfile > 0: 

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

112 if not s: 

113 break 

114 lengthfile -= len(s) 

115 f.write(s) 

116 

117 device = "pngalpha" if transparency else "ppmraw" 

118 

119 # Build Ghostscript command 

120 command = [ 

121 gs_binary, 

122 "-q", # quiet mode 

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

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

125 "-dBATCH", # exit after processing 

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

127 "-dSAFER", # safe mode 

128 f"-sDEVICE={device}", 

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

130 # adjust for image origin 

131 "-c", 

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

133 "-f", 

134 infile, # input file 

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

136 "-c", 

137 "showpage", 

138 ] 

139 

140 # push data through Ghostscript 

141 try: 

142 startupinfo = None 

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

144 startupinfo = subprocess.STARTUPINFO() 

145 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 

146 subprocess.check_call(command, startupinfo=startupinfo) 

147 out_im = Image.open(outfile) 

148 out_im.load() 

149 finally: 

150 try: 

151 os.unlink(outfile) 

152 if infile_temp: 

153 os.unlink(infile_temp) 

154 except OSError: 

155 pass 

156 

157 im = out_im.im.copy() 

158 out_im.close() 

159 return im 

160 

161 

162class PSFile: 

163 """ 

164 Wrapper for bytesio object that treats either CR or LF as end of line. 

165 This class is no longer used internally, but kept for backwards compatibility. 

166 """ 

167 

168 def __init__(self, fp): 

169 deprecate( 

170 "PSFile", 

171 11, 

172 action="If you need the functionality of this class " 

173 "you will need to implement it yourself.", 

174 ) 

175 self.fp = fp 

176 self.char = None 

177 

178 def seek(self, offset, whence=io.SEEK_SET): 

179 self.char = None 

180 self.fp.seek(offset, whence) 

181 

182 def readline(self) -> str: 

183 s = [self.char or b""] 

184 self.char = None 

185 

186 c = self.fp.read(1) 

187 while (c not in b"\r\n") and len(c): 

188 s.append(c) 

189 c = self.fp.read(1) 

190 

191 self.char = self.fp.read(1) 

192 # line endings can be 1 or 2 of \r \n, in either order 

193 if self.char in b"\r\n": 

194 self.char = None 

195 

196 return b"".join(s).decode("latin-1") 

197 

198 

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

200 return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) 

201 

202 

203## 

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

205# a few variants of this format. 

206 

207 

208class EpsImageFile(ImageFile.ImageFile): 

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

210 

211 format = "EPS" 

212 format_description = "Encapsulated Postscript" 

213 

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

215 

216 def _open(self) -> None: 

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

218 

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

220 self.fp.seek(offset) 

221 

222 self._mode = "RGB" 

223 self._size = None 

224 

225 byte_arr = bytearray(255) 

226 bytes_mv = memoryview(byte_arr) 

227 bytes_read = 0 

228 reading_header_comments = True 

229 reading_trailer_comments = False 

230 trailer_reached = False 

231 

232 def check_required_header_comments() -> None: 

233 """ 

234 The EPS specification requires that some headers exist. 

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

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

237 """ 

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

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

240 raise SyntaxError(msg) 

241 if "BoundingBox" not in self.info: 

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

243 raise SyntaxError(msg) 

244 

245 def _read_comment(s: str) -> bool: 

246 nonlocal reading_trailer_comments 

247 try: 

248 m = split.match(s) 

249 except re.error as e: 

250 msg = "not an EPS file" 

251 raise SyntaxError(msg) from e 

252 

253 if not m: 

254 return False 

255 

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

257 self.info[k] = v 

258 if k == "BoundingBox": 

259 if v == "(atend)": 

260 reading_trailer_comments = True 

261 elif not self._size or (trailer_reached and reading_trailer_comments): 

262 try: 

263 # Note: The DSC spec says that BoundingBox 

264 # fields should be integers, but some drivers 

265 # put floating point values there anyway. 

266 box = [int(float(i)) for i in v.split()] 

267 self._size = box[2] - box[0], box[3] - box[1] 

268 self.tile = [("eps", (0, 0) + self.size, offset, (length, box))] 

269 except Exception: 

270 pass 

271 return True 

272 

273 while True: 

274 byte = self.fp.read(1) 

275 if byte == b"": 

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

277 if bytes_read == 0: 

278 if reading_header_comments: 

279 check_required_header_comments() 

280 break 

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

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

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

284 # continue reading 

285 if bytes_read == 0: 

286 continue 

287 else: 

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

289 # 255 characters, not including line ending characters 

290 if bytes_read >= 255: 

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

292 # otherwise assume it's binary data 

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

294 msg = "not an EPS file" 

295 raise SyntaxError(msg) 

296 else: 

297 if reading_header_comments: 

298 check_required_header_comments() 

299 reading_header_comments = False 

300 # reset bytes_read so we can keep reading 

301 # data until the end of the line 

302 bytes_read = 0 

303 byte_arr[bytes_read] = byte[0] 

304 bytes_read += 1 

305 continue 

306 

307 if reading_header_comments: 

308 # Load EPS header 

309 

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

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

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

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

314 check_required_header_comments() 

315 reading_header_comments = False 

316 continue 

317 

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

319 if not _read_comment(s): 

320 m = field.match(s) 

321 if m: 

322 k = m.group(1) 

323 if k[:8] == "PS-Adobe": 

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

325 else: 

326 self.info[k] = "" 

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

328 # handle non-DSC PostScript comments that some 

329 # tools mistakenly put in the Comments section 

330 pass 

331 else: 

332 msg = "bad EPS header" 

333 raise OSError(msg) 

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

335 # Check for an "ImageData" descriptor 

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

337 

338 # Values: 

339 # columns 

340 # rows 

341 # bit depth (1 or 8) 

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

343 # number of padding channels 

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

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

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

347 # consisting only of this quoted value) 

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

349 columns, rows, bit_depth, mode_id = ( 

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

351 ) 

352 

353 if bit_depth == 1: 

354 self._mode = "1" 

355 elif bit_depth == 8: 

356 try: 

357 self._mode = self.mode_map[mode_id] 

358 except ValueError: 

359 break 

360 else: 

361 break 

362 

363 self._size = columns, rows 

364 return 

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

366 break 

367 elif trailer_reached and reading_trailer_comments: 

368 # Load EPS trailer 

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

370 _read_comment(s) 

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

372 trailer_reached = True 

373 bytes_read = 0 

374 

375 if not self._size: 

376 msg = "cannot determine EPS bounding box" 

377 raise OSError(msg) 

378 

379 def _find_offset(self, fp): 

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(self, scale=1, transparency=False): 

403 # Load EPS via Ghostscript 

404 if self.tile: 

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

406 self._mode = self.im.mode 

407 self._size = self.im.size 

408 self.tile = [] 

409 return Image.Image.load(self) 

410 

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

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

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

414 pass 

415 

416 

417# -------------------------------------------------------------------- 

418 

419 

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

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

422 

423 # make sure image data is available 

424 im.load() 

425 

426 # determine PostScript image mode 

427 if im.mode == "L": 

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

429 elif im.mode == "RGB": 

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

431 elif im.mode == "CMYK": 

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

433 else: 

434 msg = "image mode is not supported" 

435 raise ValueError(msg) 

436 

437 if eps: 

438 # write EPS header 

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

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

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

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

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

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

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

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

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

448 

449 # image header 

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

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

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

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

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

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

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

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

458 if hasattr(fp, "flush"): 

459 fp.flush() 

460 

461 ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)]) 

462 

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

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

465 if hasattr(fp, "flush"): 

466 fp.flush() 

467 

468 

469# -------------------------------------------------------------------- 

470 

471 

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

473 

474Image.register_save(EpsImageFile.format, _save) 

475 

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

477 

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