Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/pillow-11.0.0-py3.10-linux-x86_64.egg/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

252 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[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) 

174 

175 

176## 

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

178# a few variants of this format. 

179 

180 

181class EpsImageFile(ImageFile.ImageFile): 

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

183 

184 format = "EPS" 

185 format_description = "Encapsulated Postscript" 

186 

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

188 

189 def _open(self) -> None: 

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

191 

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

193 self.fp.seek(offset) 

194 

195 self._mode = "RGB" 

196 

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

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

199 bounding_box: list[int] | None = None 

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

201 

202 byte_arr = bytearray(255) 

203 bytes_mv = memoryview(byte_arr) 

204 bytes_read = 0 

205 reading_header_comments = True 

206 reading_trailer_comments = False 

207 trailer_reached = False 

208 

209 def check_required_header_comments() -> None: 

210 """ 

211 The EPS specification requires that some headers exist. 

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

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

214 """ 

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

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

217 raise SyntaxError(msg) 

218 if "BoundingBox" not in self.info: 

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

220 raise SyntaxError(msg) 

221 

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

223 nonlocal bounding_box, reading_trailer_comments 

224 try: 

225 m = split.match(s) 

226 except re.error as e: 

227 msg = "not an EPS file" 

228 raise SyntaxError(msg) from e 

229 

230 if not m: 

231 return False 

232 

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

234 self.info[k] = v 

235 if k == "BoundingBox": 

236 if v == "(atend)": 

237 reading_trailer_comments = True 

238 elif not bounding_box or (trailer_reached and reading_trailer_comments): 

239 try: 

240 # Note: The DSC spec says that BoundingBox 

241 # fields should be integers, but some drivers 

242 # put floating point values there anyway. 

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

244 except Exception: 

245 pass 

246 return True 

247 

248 while True: 

249 byte = self.fp.read(1) 

250 if byte == b"": 

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

252 if bytes_read == 0: 

253 if reading_header_comments: 

254 check_required_header_comments() 

255 break 

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

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

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

259 # continue reading 

260 if bytes_read == 0: 

261 continue 

262 else: 

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

264 # 255 characters, not including line ending characters 

265 if bytes_read >= 255: 

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

267 # otherwise assume it's binary data 

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

269 msg = "not an EPS file" 

270 raise SyntaxError(msg) 

271 else: 

272 if reading_header_comments: 

273 check_required_header_comments() 

274 reading_header_comments = False 

275 # reset bytes_read so we can keep reading 

276 # data until the end of the line 

277 bytes_read = 0 

278 byte_arr[bytes_read] = byte[0] 

279 bytes_read += 1 

280 continue 

281 

282 if reading_header_comments: 

283 # Load EPS header 

284 

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

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

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

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

289 check_required_header_comments() 

290 reading_header_comments = False 

291 continue 

292 

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

294 if not read_comment(s): 

295 m = field.match(s) 

296 if m: 

297 k = m.group(1) 

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

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

300 else: 

301 self.info[k] = "" 

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

303 # handle non-DSC PostScript comments that some 

304 # tools mistakenly put in the Comments section 

305 pass 

306 else: 

307 msg = "bad EPS header" 

308 raise OSError(msg) 

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

310 # Check for an "ImageData" descriptor 

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

312 

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

314 # don't read another one. 

315 if imagedata_size: 

316 bytes_read = 0 

317 continue 

318 

319 # Values: 

320 # columns 

321 # rows 

322 # bit depth (1 or 8) 

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

324 # number of padding channels 

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

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

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

328 # consisting only of this quoted value) 

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

330 columns, rows, bit_depth, mode_id = ( 

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

332 ) 

333 

334 if bit_depth == 1: 

335 self._mode = "1" 

336 elif bit_depth == 8: 

337 try: 

338 self._mode = self.mode_map[mode_id] 

339 except ValueError: 

340 break 

341 else: 

342 break 

343 

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

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

346 imagedata_size = columns, rows 

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

348 break 

349 elif trailer_reached and reading_trailer_comments: 

350 # Load EPS trailer 

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

352 read_comment(s) 

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

354 trailer_reached = True 

355 bytes_read = 0 

356 

357 # A "BoundingBox" is always required, 

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

359 if not bounding_box: 

360 msg = "cannot determine EPS bounding box" 

361 raise OSError(msg) 

362 

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

364 self._size = imagedata_size or ( 

365 bounding_box[2] - bounding_box[0], 

366 bounding_box[3] - bounding_box[1], 

367 ) 

368 

369 self.tile = [ 

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

371 ] 

372 

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

374 s = fp.read(4) 

375 

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

377 # for HEAD without binary preview 

378 fp.seek(0, io.SEEK_END) 

379 length = fp.tell() 

380 offset = 0 

381 elif i32(s) == 0xC6D3D0C5: 

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

383 # EPS can contain binary data 

384 # or start directly with latin coding 

385 # more info see: 

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

387 s = fp.read(8) 

388 offset = i32(s) 

389 length = i32(s, 4) 

390 else: 

391 msg = "not an EPS file" 

392 raise SyntaxError(msg) 

393 

394 return length, offset 

395 

396 def load( 

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

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

399 # Load EPS via Ghostscript 

400 if self.tile: 

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

402 self._mode = self.im.mode 

403 self._size = self.im.size 

404 self.tile = [] 

405 return Image.Image.load(self) 

406 

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

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

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

410 pass 

411 

412 

413# -------------------------------------------------------------------- 

414 

415 

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

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

418 

419 # make sure image data is available 

420 im.load() 

421 

422 # determine PostScript image mode 

423 if im.mode == "L": 

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

425 elif im.mode == "RGB": 

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

427 elif im.mode == "CMYK": 

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

429 else: 

430 msg = "image mode is not supported" 

431 raise ValueError(msg) 

432 

433 if eps: 

434 # write EPS header 

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

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

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

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

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

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

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

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

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

444 

445 # image header 

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

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

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

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

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

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

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

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

454 if hasattr(fp, "flush"): 

455 fp.flush() 

456 

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

458 

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

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

461 if hasattr(fp, "flush"): 

462 fp.flush() 

463 

464 

465# -------------------------------------------------------------------- 

466 

467 

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

469 

470Image.register_save(EpsImageFile.format, _save) 

471 

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

473 

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