EpsImagePlugin.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  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. (length, offset) = self._find_offset(self.fp)
  165. # go to offset - start of "%!PS"
  166. self.fp.seek(offset)
  167. self._mode = "RGB"
  168. # When reading header comments, the first comment is used.
  169. # When reading trailer comments, the last comment is used.
  170. bounding_box: list[int] | None = None
  171. imagedata_size: tuple[int, int] | None = None
  172. byte_arr = bytearray(255)
  173. bytes_mv = memoryview(byte_arr)
  174. bytes_read = 0
  175. reading_header_comments = True
  176. reading_trailer_comments = False
  177. trailer_reached = False
  178. def check_required_header_comments() -> None:
  179. """
  180. The EPS specification requires that some headers exist.
  181. This should be checked when the header comments formally end,
  182. when image data starts, or when the file ends, whichever comes first.
  183. """
  184. if "PS-Adobe" not in self.info:
  185. msg = 'EPS header missing "%!PS-Adobe" comment'
  186. raise SyntaxError(msg)
  187. if "BoundingBox" not in self.info:
  188. msg = 'EPS header missing "%%BoundingBox" comment'
  189. raise SyntaxError(msg)
  190. def read_comment(s: str) -> bool:
  191. nonlocal bounding_box, reading_trailer_comments
  192. try:
  193. m = split.match(s)
  194. except re.error as e:
  195. msg = "not an EPS file"
  196. raise SyntaxError(msg) from e
  197. if not m:
  198. return False
  199. k, v = m.group(1, 2)
  200. self.info[k] = v
  201. if k == "BoundingBox":
  202. if v == "(atend)":
  203. reading_trailer_comments = True
  204. elif not bounding_box or (trailer_reached and reading_trailer_comments):
  205. try:
  206. # Note: The DSC spec says that BoundingBox
  207. # fields should be integers, but some drivers
  208. # put floating point values there anyway.
  209. bounding_box = [int(float(i)) for i in v.split()]
  210. except Exception:
  211. pass
  212. return True
  213. while True:
  214. byte = self.fp.read(1)
  215. if byte == b"":
  216. # if we didn't read a byte we must be at the end of the file
  217. if bytes_read == 0:
  218. if reading_header_comments:
  219. check_required_header_comments()
  220. break
  221. elif byte in b"\r\n":
  222. # if we read a line ending character, ignore it and parse what
  223. # we have already read. if we haven't read any other characters,
  224. # continue reading
  225. if bytes_read == 0:
  226. continue
  227. else:
  228. # ASCII/hexadecimal lines in an EPS file must not exceed
  229. # 255 characters, not including line ending characters
  230. if bytes_read >= 255:
  231. # only enforce this for lines starting with a "%",
  232. # otherwise assume it's binary data
  233. if byte_arr[0] == ord("%"):
  234. msg = "not an EPS file"
  235. raise SyntaxError(msg)
  236. else:
  237. if reading_header_comments:
  238. check_required_header_comments()
  239. reading_header_comments = False
  240. # reset bytes_read so we can keep reading
  241. # data until the end of the line
  242. bytes_read = 0
  243. byte_arr[bytes_read] = byte[0]
  244. bytes_read += 1
  245. continue
  246. if reading_header_comments:
  247. # Load EPS header
  248. # if this line doesn't start with a "%",
  249. # or does start with "%%EndComments",
  250. # then we've reached the end of the header/comments
  251. if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
  252. check_required_header_comments()
  253. reading_header_comments = False
  254. continue
  255. s = str(bytes_mv[:bytes_read], "latin-1")
  256. if not read_comment(s):
  257. m = field.match(s)
  258. if m:
  259. k = m.group(1)
  260. if k.startswith("PS-Adobe"):
  261. self.info["PS-Adobe"] = k[9:]
  262. else:
  263. self.info[k] = ""
  264. elif s[0] == "%":
  265. # handle non-DSC PostScript comments that some
  266. # tools mistakenly put in the Comments section
  267. pass
  268. else:
  269. msg = "bad EPS header"
  270. raise OSError(msg)
  271. elif bytes_mv[:11] == b"%ImageData:":
  272. # Check for an "ImageData" descriptor
  273. # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096
  274. # If we've already read an "ImageData" descriptor,
  275. # don't read another one.
  276. if imagedata_size:
  277. bytes_read = 0
  278. continue
  279. # Values:
  280. # columns
  281. # rows
  282. # bit depth (1 or 8)
  283. # mode (1: L, 2: LAB, 3: RGB, 4: CMYK)
  284. # number of padding channels
  285. # block size (number of bytes per row per channel)
  286. # binary/ascii (1: binary, 2: ascii)
  287. # data start identifier (the image data follows after a single line
  288. # consisting only of this quoted value)
  289. image_data_values = byte_arr[11:bytes_read].split(None, 7)
  290. columns, rows, bit_depth, mode_id = (
  291. int(value) for value in image_data_values[:4]
  292. )
  293. if bit_depth == 1:
  294. self._mode = "1"
  295. elif bit_depth == 8:
  296. try:
  297. self._mode = self.mode_map[mode_id]
  298. except ValueError:
  299. break
  300. else:
  301. break
  302. # Parse the columns and rows after checking the bit depth and mode
  303. # in case the bit depth and/or mode are invalid.
  304. imagedata_size = columns, rows
  305. elif bytes_mv[:5] == b"%%EOF":
  306. break
  307. elif trailer_reached and reading_trailer_comments:
  308. # Load EPS trailer
  309. s = str(bytes_mv[:bytes_read], "latin-1")
  310. read_comment(s)
  311. elif bytes_mv[:9] == b"%%Trailer":
  312. trailer_reached = True
  313. elif bytes_mv[:14] == b"%%BeginBinary:":
  314. bytecount = int(byte_arr[14:bytes_read])
  315. self.fp.seek(bytecount, os.SEEK_CUR)
  316. bytes_read = 0
  317. # A "BoundingBox" is always required,
  318. # even if an "ImageData" descriptor size exists.
  319. if not bounding_box:
  320. msg = "cannot determine EPS bounding box"
  321. raise OSError(msg)
  322. # An "ImageData" size takes precedence over the "BoundingBox".
  323. self._size = imagedata_size or (
  324. bounding_box[2] - bounding_box[0],
  325. bounding_box[3] - bounding_box[1],
  326. )
  327. self.tile = [
  328. ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box))
  329. ]
  330. def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]:
  331. s = fp.read(4)
  332. if s == b"%!PS":
  333. # for HEAD without binary preview
  334. fp.seek(0, io.SEEK_END)
  335. length = fp.tell()
  336. offset = 0
  337. elif i32(s) == 0xC6D3D0C5:
  338. # FIX for: Some EPS file not handled correctly / issue #302
  339. # EPS can contain binary data
  340. # or start directly with latin coding
  341. # more info see:
  342. # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
  343. s = fp.read(8)
  344. offset = i32(s)
  345. length = i32(s, 4)
  346. else:
  347. msg = "not an EPS file"
  348. raise SyntaxError(msg)
  349. return length, offset
  350. def load(
  351. self, scale: int = 1, transparency: bool = False
  352. ) -> Image.core.PixelAccess | None:
  353. # Load EPS via Ghostscript
  354. if self.tile:
  355. self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
  356. self._mode = self.im.mode
  357. self._size = self.im.size
  358. self.tile = []
  359. return Image.Image.load(self)
  360. def load_seek(self, pos: int) -> None:
  361. # we can't incrementally load, so force ImageFile.parser to
  362. # use our custom load method by defining this method.
  363. pass
  364. # --------------------------------------------------------------------
  365. def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None:
  366. """EPS Writer for the Python Imaging Library."""
  367. # make sure image data is available
  368. im.load()
  369. # determine PostScript image mode
  370. if im.mode == "L":
  371. operator = (8, 1, b"image")
  372. elif im.mode == "RGB":
  373. operator = (8, 3, b"false 3 colorimage")
  374. elif im.mode == "CMYK":
  375. operator = (8, 4, b"false 4 colorimage")
  376. else:
  377. msg = "image mode is not supported"
  378. raise ValueError(msg)
  379. if eps:
  380. # write EPS header
  381. fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n")
  382. fp.write(b"%%Creator: PIL 0.1 EpsEncode\n")
  383. # fp.write("%%CreationDate: %s"...)
  384. fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size)
  385. fp.write(b"%%Pages: 1\n")
  386. fp.write(b"%%EndComments\n")
  387. fp.write(b"%%Page: 1 1\n")
  388. fp.write(b"%%ImageData: %d %d " % im.size)
  389. fp.write(b'%d %d 0 1 1 "%s"\n' % operator)
  390. # image header
  391. fp.write(b"gsave\n")
  392. fp.write(b"10 dict begin\n")
  393. fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1]))
  394. fp.write(b"%d %d scale\n" % im.size)
  395. fp.write(b"%d %d 8\n" % im.size) # <= bits
  396. fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1]))
  397. fp.write(b"{ currentfile buf readhexstring pop } bind\n")
  398. fp.write(operator[2] + b"\n")
  399. if hasattr(fp, "flush"):
  400. fp.flush()
  401. ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)])
  402. fp.write(b"\n%%%%EndBinary\n")
  403. fp.write(b"grestore end\n")
  404. if hasattr(fp, "flush"):
  405. fp.flush()
  406. # --------------------------------------------------------------------
  407. Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
  408. Image.register_save(EpsImageFile.format, _save)
  409. Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
  410. Image.register_mime(EpsImageFile.format, "application/postscript")