EpsImagePlugin.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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. #
  22. from __future__ import annotations
  23. import io
  24. import os
  25. import re
  26. import subprocess
  27. import sys
  28. import tempfile
  29. from typing import IO
  30. from . import Image, ImageFile
  31. from ._binary import i32le as i32
  32. # --------------------------------------------------------------------
  33. split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
  34. field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
  35. gs_binary: str | bool | None = None
  36. gs_windows_binary = None
  37. def has_ghostscript() -> bool:
  38. global gs_binary, gs_windows_binary
  39. if gs_binary is None:
  40. if sys.platform.startswith("win"):
  41. if gs_windows_binary is None:
  42. import shutil
  43. for binary in ("gswin32c", "gswin64c", "gs"):
  44. if shutil.which(binary) is not None:
  45. gs_windows_binary = binary
  46. break
  47. else:
  48. gs_windows_binary = False
  49. gs_binary = gs_windows_binary
  50. else:
  51. try:
  52. subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
  53. gs_binary = "gs"
  54. except OSError:
  55. gs_binary = False
  56. return gs_binary is not False
  57. def Ghostscript(
  58. tile: list[ImageFile._Tile],
  59. size: tuple[int, int],
  60. fp: IO[bytes],
  61. scale: int = 1,
  62. transparency: bool = False,
  63. ) -> Image.core.ImagingCore:
  64. """Render an image using Ghostscript"""
  65. global gs_binary
  66. if not has_ghostscript():
  67. msg = "Unable to locate Ghostscript on paths"
  68. raise OSError(msg)
  69. assert isinstance(gs_binary, str)
  70. # Unpack decoder tile
  71. args = tile[0].args
  72. assert isinstance(args, tuple)
  73. length, bbox = args
  74. # Hack to support hi-res rendering
  75. scale = int(scale) or 1
  76. width = size[0] * scale
  77. height = size[1] * scale
  78. # resolution is dependent on bbox and size
  79. res_x = 72.0 * width / (bbox[2] - bbox[0])
  80. res_y = 72.0 * height / (bbox[3] - bbox[1])
  81. out_fd, outfile = tempfile.mkstemp()
  82. os.close(out_fd)
  83. infile_temp = None
  84. if hasattr(fp, "name") and os.path.exists(fp.name):
  85. infile = fp.name
  86. else:
  87. in_fd, infile_temp = tempfile.mkstemp()
  88. os.close(in_fd)
  89. infile = infile_temp
  90. # Ignore length and offset!
  91. # Ghostscript can read it
  92. # Copy whole file to read in Ghostscript
  93. with open(infile_temp, "wb") as f:
  94. # fetch length of fp
  95. fp.seek(0, io.SEEK_END)
  96. fsize = fp.tell()
  97. # ensure start position
  98. # go back
  99. fp.seek(0)
  100. lengthfile = fsize
  101. while lengthfile > 0:
  102. s = fp.read(min(lengthfile, 100 * 1024))
  103. if not s:
  104. break
  105. lengthfile -= len(s)
  106. f.write(s)
  107. if transparency:
  108. # "RGBA"
  109. device = "pngalpha"
  110. else:
  111. # "pnmraw" automatically chooses between
  112. # PBM ("1"), PGM ("L"), and PPM ("RGB").
  113. device = "pnmraw"
  114. # Build Ghostscript command
  115. command = [
  116. gs_binary,
  117. "-q", # quiet mode
  118. f"-g{width:d}x{height:d}", # set output geometry (pixels)
  119. f"-r{res_x:f}x{res_y:f}", # set input DPI (dots per inch)
  120. "-dBATCH", # exit after processing
  121. "-dNOPAUSE", # don't pause between pages
  122. "-dSAFER", # safe mode
  123. f"-sDEVICE={device}",
  124. f"-sOutputFile={outfile}", # output file
  125. # adjust for image origin
  126. "-c",
  127. f"{-bbox[0]} {-bbox[1]} translate",
  128. "-f",
  129. infile, # input file
  130. # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272)
  131. "-c",
  132. "showpage",
  133. ]
  134. # push data through Ghostscript
  135. try:
  136. startupinfo = None
  137. if sys.platform.startswith("win"):
  138. startupinfo = subprocess.STARTUPINFO()
  139. startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
  140. subprocess.check_call(command, startupinfo=startupinfo)
  141. with Image.open(outfile) as out_im:
  142. out_im.load()
  143. return out_im.im.copy()
  144. finally:
  145. try:
  146. os.unlink(outfile)
  147. if infile_temp:
  148. os.unlink(infile_temp)
  149. except OSError:
  150. pass
  151. def _accept(prefix: bytes) -> bool:
  152. return prefix.startswith(b"%!PS") or (
  153. len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5
  154. )
  155. ##
  156. # Image plugin for Encapsulated PostScript. This plugin supports only
  157. # a few variants of this format.
  158. class EpsImageFile(ImageFile.ImageFile):
  159. """EPS File Parser for the Python Imaging Library"""
  160. format = "EPS"
  161. format_description = "Encapsulated Postscript"
  162. mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
  163. def _open(self) -> None:
  164. assert self.fp is not None
  165. (length, offset) = self._find_offset(self.fp)
  166. # go to offset - start of "%!PS"
  167. self.fp.seek(offset)
  168. self._mode = "RGB"
  169. # When reading header comments, the first comment is used.
  170. # When reading trailer comments, the last comment is used.
  171. bounding_box: list[int] | None = None
  172. imagedata_size: tuple[int, int] | None = None
  173. byte_arr = bytearray(255)
  174. bytes_mv = memoryview(byte_arr)
  175. bytes_read = 0
  176. reading_header_comments = True
  177. reading_trailer_comments = False
  178. trailer_reached = False
  179. def check_required_header_comments() -> None:
  180. """
  181. The EPS specification requires that some headers exist.
  182. This should be checked when the header comments formally end,
  183. when image data starts, or when the file ends, whichever comes first.
  184. """
  185. if "PS-Adobe" not in self.info:
  186. msg = 'EPS header missing "%!PS-Adobe" comment'
  187. raise SyntaxError(msg)
  188. if "BoundingBox" not in self.info:
  189. msg = 'EPS header missing "%%BoundingBox" comment'
  190. raise SyntaxError(msg)
  191. def read_comment(s: str) -> bool:
  192. nonlocal bounding_box, reading_trailer_comments
  193. try:
  194. m = split.match(s)
  195. except re.error as e:
  196. msg = "not an EPS file"
  197. raise SyntaxError(msg) from e
  198. if not m:
  199. return False
  200. k, v = m.group(1, 2)
  201. self.info[k] = v
  202. if k == "BoundingBox":
  203. if v == "(atend)":
  204. reading_trailer_comments = True
  205. elif not bounding_box or (trailer_reached and reading_trailer_comments):
  206. try:
  207. # Note: The DSC spec says that BoundingBox
  208. # fields should be integers, but some drivers
  209. # put floating point values there anyway.
  210. bounding_box = [int(float(i)) for i in v.split()]
  211. except Exception:
  212. pass
  213. return True
  214. while True:
  215. byte = self.fp.read(1)
  216. if byte == b"":
  217. # if we didn't read a byte we must be at the end of the file
  218. if bytes_read == 0:
  219. if reading_header_comments:
  220. check_required_header_comments()
  221. break
  222. elif byte in b"\r\n":
  223. # if we read a line ending character, ignore it and parse what
  224. # we have already read. if we haven't read any other characters,
  225. # continue reading
  226. if bytes_read == 0:
  227. continue
  228. else:
  229. # ASCII/hexadecimal lines in an EPS file must not exceed
  230. # 255 characters, not including line ending characters
  231. if bytes_read >= 255:
  232. # only enforce this for lines starting with a "%",
  233. # otherwise assume it's binary data
  234. if byte_arr[0] == ord("%"):
  235. msg = "not an EPS file"
  236. raise SyntaxError(msg)
  237. else:
  238. if reading_header_comments:
  239. check_required_header_comments()
  240. reading_header_comments = False
  241. # reset bytes_read so we can keep reading
  242. # data until the end of the line
  243. bytes_read = 0
  244. byte_arr[bytes_read] = byte[0]
  245. bytes_read += 1
  246. continue
  247. if reading_header_comments:
  248. # Load EPS header
  249. # if this line doesn't start with a "%",
  250. # or does start with "%%EndComments",
  251. # then we've reached the end of the header/comments
  252. if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
  253. check_required_header_comments()
  254. reading_header_comments = False
  255. continue
  256. s = str(bytes_mv[:bytes_read], "latin-1")
  257. if not read_comment(s):
  258. m = field.match(s)
  259. if m:
  260. k = m.group(1)
  261. if k.startswith("PS-Adobe"):
  262. self.info["PS-Adobe"] = k[9:]
  263. else:
  264. self.info[k] = ""
  265. elif s[0] == "%":
  266. # handle non-DSC PostScript comments that some
  267. # tools mistakenly put in the Comments section
  268. pass
  269. else:
  270. msg = "bad EPS header"
  271. raise OSError(msg)
  272. elif bytes_mv[:11] == b"%ImageData:":
  273. # Check for an "ImageData" descriptor
  274. # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096
  275. # If we've already read an "ImageData" descriptor,
  276. # don't read another one.
  277. if imagedata_size:
  278. bytes_read = 0
  279. continue
  280. # Values:
  281. # columns
  282. # rows
  283. # bit depth (1 or 8)
  284. # mode (1: L, 2: LAB, 3: RGB, 4: CMYK)
  285. # number of padding channels
  286. # block size (number of bytes per row per channel)
  287. # binary/ascii (1: binary, 2: ascii)
  288. # data start identifier (the image data follows after a single line
  289. # consisting only of this quoted value)
  290. image_data_values = byte_arr[11:bytes_read].split(None, 7)
  291. columns, rows, bit_depth, mode_id = (
  292. int(value) for value in image_data_values[:4]
  293. )
  294. if bit_depth == 1:
  295. self._mode = "1"
  296. elif bit_depth == 8:
  297. try:
  298. self._mode = self.mode_map[mode_id]
  299. except ValueError:
  300. break
  301. else:
  302. break
  303. # Parse the columns and rows after checking the bit depth and mode
  304. # in case the bit depth and/or mode are invalid.
  305. imagedata_size = columns, rows
  306. elif bytes_mv[:5] == b"%%EOF":
  307. break
  308. elif trailer_reached and reading_trailer_comments:
  309. # Load EPS trailer
  310. s = str(bytes_mv[:bytes_read], "latin-1")
  311. read_comment(s)
  312. elif bytes_mv[:9] == b"%%Trailer":
  313. trailer_reached = True
  314. elif bytes_mv[:14] == b"%%BeginBinary:":
  315. bytecount = int(byte_arr[14:bytes_read])
  316. self.fp.seek(bytecount, os.SEEK_CUR)
  317. bytes_read = 0
  318. # A "BoundingBox" is always required,
  319. # even if an "ImageData" descriptor size exists.
  320. if not bounding_box:
  321. msg = "cannot determine EPS bounding box"
  322. raise OSError(msg)
  323. # An "ImageData" size takes precedence over the "BoundingBox".
  324. self._size = imagedata_size or (
  325. bounding_box[2] - bounding_box[0],
  326. bounding_box[3] - bounding_box[1],
  327. )
  328. self.tile = [
  329. ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box))
  330. ]
  331. def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]:
  332. s = fp.read(4)
  333. if s == b"%!PS":
  334. # for HEAD without binary preview
  335. fp.seek(0, io.SEEK_END)
  336. length = fp.tell()
  337. offset = 0
  338. elif i32(s) == 0xC6D3D0C5:
  339. # FIX for: Some EPS file not handled correctly / issue #302
  340. # EPS can contain binary data
  341. # or start directly with latin coding
  342. # more info see:
  343. # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
  344. s = fp.read(8)
  345. offset = i32(s)
  346. length = i32(s, 4)
  347. else:
  348. msg = "not an EPS file"
  349. raise SyntaxError(msg)
  350. return length, offset
  351. def load(
  352. self, scale: int = 1, transparency: bool = False
  353. ) -> Image.core.PixelAccess | None:
  354. # Load EPS via Ghostscript
  355. if self.tile:
  356. assert self.fp is not None
  357. self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
  358. self._mode = self.im.mode
  359. self._size = self.im.size
  360. self.tile = []
  361. return Image.Image.load(self)
  362. def load_seek(self, pos: int) -> None:
  363. # we can't incrementally load, so force ImageFile.parser to
  364. # use our custom load method by defining this method.
  365. pass
  366. # --------------------------------------------------------------------
  367. def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None:
  368. """EPS Writer for the Python Imaging Library."""
  369. # make sure image data is available
  370. im.load()
  371. # determine PostScript image mode
  372. if im.mode == "L":
  373. operator = (8, 1, b"image")
  374. elif im.mode == "RGB":
  375. operator = (8, 3, b"false 3 colorimage")
  376. elif im.mode == "CMYK":
  377. operator = (8, 4, b"false 4 colorimage")
  378. else:
  379. msg = "image mode is not supported"
  380. raise ValueError(msg)
  381. if eps:
  382. # write EPS header
  383. fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n")
  384. fp.write(b"%%Creator: PIL 0.1 EpsEncode\n")
  385. # fp.write("%%CreationDate: %s"...)
  386. fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size)
  387. fp.write(b"%%Pages: 1\n")
  388. fp.write(b"%%EndComments\n")
  389. fp.write(b"%%Page: 1 1\n")
  390. fp.write(b"%%ImageData: %d %d " % im.size)
  391. fp.write(b'%d %d 0 1 1 "%s"\n' % operator)
  392. # image header
  393. fp.write(b"gsave\n")
  394. fp.write(b"10 dict begin\n")
  395. fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1]))
  396. fp.write(b"%d %d scale\n" % im.size)
  397. fp.write(b"%d %d 8\n" % im.size) # <= bits
  398. fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1]))
  399. fp.write(b"{ currentfile buf readhexstring pop } bind\n")
  400. fp.write(operator[2] + b"\n")
  401. if hasattr(fp, "flush"):
  402. fp.flush()
  403. ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)])
  404. fp.write(b"\n%%%%EndBinary\n")
  405. fp.write(b"grestore end\n")
  406. if hasattr(fp, "flush"):
  407. fp.flush()
  408. # --------------------------------------------------------------------
  409. Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
  410. Image.register_save(EpsImageFile.format, _save)
  411. Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
  412. Image.register_mime(EpsImageFile.format, "application/postscript")