EpsImagePlugin.py 16 KB

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