EpsImagePlugin.py 16 KB

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