EpsImagePlugin.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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. import io
  23. import os
  24. import re
  25. import subprocess
  26. import sys
  27. import tempfile
  28. from . import Image, ImageFile
  29. from ._binary import i32le as i32
  30. #
  31. # --------------------------------------------------------------------
  32. split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
  33. field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
  34. gs_windows_binary = None
  35. if sys.platform.startswith("win"):
  36. import shutil
  37. for binary in ("gswin32c", "gswin64c", "gs"):
  38. if shutil.which(binary) is not None:
  39. gs_windows_binary = binary
  40. break
  41. else:
  42. gs_windows_binary = False
  43. def has_ghostscript():
  44. if gs_windows_binary:
  45. return True
  46. if not sys.platform.startswith("win"):
  47. try:
  48. subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
  49. return True
  50. except OSError:
  51. # No Ghostscript
  52. pass
  53. return False
  54. def Ghostscript(tile, size, fp, scale=1):
  55. """Render an image using Ghostscript"""
  56. # Unpack decoder tile
  57. decoder, tile, offset, data = tile[0]
  58. length, bbox = data
  59. # Hack to support hi-res rendering
  60. scale = int(scale) or 1
  61. # orig_size = size
  62. # orig_bbox = bbox
  63. size = (size[0] * scale, size[1] * scale)
  64. # resolution is dependent on bbox and size
  65. res = (
  66. 72.0 * size[0] / (bbox[2] - bbox[0]),
  67. 72.0 * size[1] / (bbox[3] - bbox[1]),
  68. )
  69. out_fd, outfile = tempfile.mkstemp()
  70. os.close(out_fd)
  71. infile_temp = None
  72. if hasattr(fp, "name") and os.path.exists(fp.name):
  73. infile = fp.name
  74. else:
  75. in_fd, infile_temp = tempfile.mkstemp()
  76. os.close(in_fd)
  77. infile = infile_temp
  78. # Ignore length and offset!
  79. # Ghostscript can read it
  80. # Copy whole file to read in Ghostscript
  81. with open(infile_temp, "wb") as f:
  82. # fetch length of fp
  83. fp.seek(0, io.SEEK_END)
  84. fsize = fp.tell()
  85. # ensure start position
  86. # go back
  87. fp.seek(0)
  88. lengthfile = fsize
  89. while lengthfile > 0:
  90. s = fp.read(min(lengthfile, 100 * 1024))
  91. if not s:
  92. break
  93. lengthfile -= len(s)
  94. f.write(s)
  95. # Build Ghostscript command
  96. command = [
  97. "gs",
  98. "-q", # quiet mode
  99. "-g%dx%d" % size, # set output geometry (pixels)
  100. "-r%fx%f" % res, # set input DPI (dots per inch)
  101. "-dBATCH", # exit after processing
  102. "-dNOPAUSE", # don't pause between pages
  103. "-dSAFER", # safe mode
  104. "-sDEVICE=ppmraw", # ppm driver
  105. f"-sOutputFile={outfile}", # output file
  106. # adjust for image origin
  107. "-c",
  108. f"{-bbox[0]} {-bbox[1]} translate",
  109. "-f",
  110. infile, # input file
  111. # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272)
  112. "-c",
  113. "showpage",
  114. ]
  115. if gs_windows_binary is not None:
  116. if not gs_windows_binary:
  117. raise OSError("Unable to locate Ghostscript on paths")
  118. command[0] = gs_windows_binary
  119. # push data through Ghostscript
  120. try:
  121. startupinfo = None
  122. if sys.platform.startswith("win"):
  123. startupinfo = subprocess.STARTUPINFO()
  124. startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
  125. subprocess.check_call(command, startupinfo=startupinfo)
  126. out_im = Image.open(outfile)
  127. out_im.load()
  128. finally:
  129. try:
  130. os.unlink(outfile)
  131. if infile_temp:
  132. os.unlink(infile_temp)
  133. except OSError:
  134. pass
  135. im = out_im.im.copy()
  136. out_im.close()
  137. return im
  138. class PSFile:
  139. """
  140. Wrapper for bytesio object that treats either CR or LF as end of line.
  141. """
  142. def __init__(self, fp):
  143. self.fp = fp
  144. self.char = None
  145. def seek(self, offset, whence=io.SEEK_SET):
  146. self.char = None
  147. self.fp.seek(offset, whence)
  148. def readline(self):
  149. s = self.char or b""
  150. self.char = None
  151. c = self.fp.read(1)
  152. while c not in b"\r\n":
  153. s = s + c
  154. c = self.fp.read(1)
  155. self.char = self.fp.read(1)
  156. # line endings can be 1 or 2 of \r \n, in either order
  157. if self.char in b"\r\n":
  158. self.char = None
  159. return s.decode("latin-1")
  160. def _accept(prefix):
  161. return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
  162. ##
  163. # Image plugin for Encapsulated PostScript. This plugin supports only
  164. # a few variants of this format.
  165. class EpsImageFile(ImageFile.ImageFile):
  166. """EPS File Parser for the Python Imaging Library"""
  167. format = "EPS"
  168. format_description = "Encapsulated Postscript"
  169. mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
  170. def _open(self):
  171. (length, offset) = self._find_offset(self.fp)
  172. # Rewrap the open file pointer in something that will
  173. # convert line endings and decode to latin-1.
  174. fp = PSFile(self.fp)
  175. # go to offset - start of "%!PS"
  176. fp.seek(offset)
  177. box = None
  178. self.mode = "RGB"
  179. self._size = 1, 1 # FIXME: huh?
  180. #
  181. # Load EPS header
  182. s_raw = fp.readline()
  183. s = s_raw.strip("\r\n")
  184. while s_raw:
  185. if s:
  186. if len(s) > 255:
  187. raise SyntaxError("not an EPS file")
  188. try:
  189. m = split.match(s)
  190. except re.error as e:
  191. raise SyntaxError("not an EPS file") from e
  192. if m:
  193. k, v = m.group(1, 2)
  194. self.info[k] = v
  195. if k == "BoundingBox":
  196. try:
  197. # Note: The DSC spec says that BoundingBox
  198. # fields should be integers, but some drivers
  199. # put floating point values there anyway.
  200. box = [int(float(i)) for i in v.split()]
  201. self._size = box[2] - box[0], box[3] - box[1]
  202. self.tile = [
  203. ("eps", (0, 0) + self.size, offset, (length, box))
  204. ]
  205. except Exception:
  206. pass
  207. else:
  208. m = field.match(s)
  209. if m:
  210. k = m.group(1)
  211. if k == "EndComments":
  212. break
  213. if k[:8] == "PS-Adobe":
  214. self.info[k[:8]] = k[9:]
  215. else:
  216. self.info[k] = ""
  217. elif s[0] == "%":
  218. # handle non-DSC PostScript comments that some
  219. # tools mistakenly put in the Comments section
  220. pass
  221. else:
  222. raise OSError("bad EPS header")
  223. s_raw = fp.readline()
  224. s = s_raw.strip("\r\n")
  225. if s and s[:1] != "%":
  226. break
  227. #
  228. # Scan for an "ImageData" descriptor
  229. while s[:1] == "%":
  230. if len(s) > 255:
  231. raise SyntaxError("not an EPS file")
  232. if s[:11] == "%ImageData:":
  233. # Encoded bitmapped image.
  234. x, y, bi, mo = s[11:].split(None, 7)[:4]
  235. if int(bi) != 8:
  236. break
  237. try:
  238. self.mode = self.mode_map[int(mo)]
  239. except ValueError:
  240. break
  241. self._size = int(x), int(y)
  242. return
  243. s = fp.readline().strip("\r\n")
  244. if not s:
  245. break
  246. if not box:
  247. raise OSError("cannot determine EPS bounding box")
  248. def _find_offset(self, fp):
  249. s = fp.read(160)
  250. if s[:4] == b"%!PS":
  251. # for HEAD without binary preview
  252. fp.seek(0, io.SEEK_END)
  253. length = fp.tell()
  254. offset = 0
  255. elif i32(s[0:4]) == 0xC6D3D0C5:
  256. # FIX for: Some EPS file not handled correctly / issue #302
  257. # EPS can contain binary data
  258. # or start directly with latin coding
  259. # more info see:
  260. # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
  261. offset = i32(s[4:8])
  262. length = i32(s[8:12])
  263. else:
  264. raise SyntaxError("not an EPS file")
  265. return (length, offset)
  266. def load(self, scale=1):
  267. # Load EPS via Ghostscript
  268. if not self.tile:
  269. return
  270. self.im = Ghostscript(self.tile, self.size, self.fp, scale)
  271. self.mode = self.im.mode
  272. self._size = self.im.size
  273. self.tile = []
  274. def load_seek(self, *args, **kwargs):
  275. # we can't incrementally load, so force ImageFile.parser to
  276. # use our custom load method by defining this method.
  277. pass
  278. #
  279. # --------------------------------------------------------------------
  280. def _save(im, fp, filename, eps=1):
  281. """EPS Writer for the Python Imaging Library."""
  282. #
  283. # make sure image data is available
  284. im.load()
  285. #
  286. # determine PostScript image mode
  287. if im.mode == "L":
  288. operator = (8, 1, "image")
  289. elif im.mode == "RGB":
  290. operator = (8, 3, "false 3 colorimage")
  291. elif im.mode == "CMYK":
  292. operator = (8, 4, "false 4 colorimage")
  293. else:
  294. raise ValueError("image mode is not supported")
  295. base_fp = fp
  296. wrapped_fp = False
  297. if fp != sys.stdout:
  298. fp = io.TextIOWrapper(fp, encoding="latin-1")
  299. wrapped_fp = True
  300. try:
  301. if eps:
  302. #
  303. # write EPS header
  304. fp.write("%!PS-Adobe-3.0 EPSF-3.0\n")
  305. fp.write("%%Creator: PIL 0.1 EpsEncode\n")
  306. # fp.write("%%CreationDate: %s"...)
  307. fp.write("%%%%BoundingBox: 0 0 %d %d\n" % im.size)
  308. fp.write("%%Pages: 1\n")
  309. fp.write("%%EndComments\n")
  310. fp.write("%%Page: 1 1\n")
  311. fp.write("%%ImageData: %d %d " % im.size)
  312. fp.write('%d %d 0 1 1 "%s"\n' % operator)
  313. #
  314. # image header
  315. fp.write("gsave\n")
  316. fp.write("10 dict begin\n")
  317. fp.write(f"/buf {im.size[0] * operator[1]} string def\n")
  318. fp.write("%d %d scale\n" % im.size)
  319. fp.write("%d %d 8\n" % im.size) # <= bits
  320. fp.write(f"[{im.size[0]} 0 0 -{im.size[1]} 0 {im.size[1]}]\n")
  321. fp.write("{ currentfile buf readhexstring pop } bind\n")
  322. fp.write(operator[2] + "\n")
  323. if hasattr(fp, "flush"):
  324. fp.flush()
  325. ImageFile._save(im, base_fp, [("eps", (0, 0) + im.size, 0, None)])
  326. fp.write("\n%%%%EndBinary\n")
  327. fp.write("grestore end\n")
  328. if hasattr(fp, "flush"):
  329. fp.flush()
  330. finally:
  331. if wrapped_fp:
  332. fp.detach()
  333. #
  334. # --------------------------------------------------------------------
  335. Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
  336. Image.register_save(EpsImageFile.format, _save)
  337. Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
  338. Image.register_mime(EpsImageFile.format, "application/postscript")