EpsImagePlugin.py 12 KB

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