EpsImagePlugin.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  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. from ._deprecate import deprecate
  31. # --------------------------------------------------------------------
  32. split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
  33. field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
  34. gs_binary = None
  35. gs_windows_binary = None
  36. def has_ghostscript():
  37. global gs_binary, gs_windows_binary
  38. if gs_binary is None:
  39. if sys.platform.startswith("win"):
  40. if gs_windows_binary is None:
  41. import shutil
  42. for binary in ("gswin32c", "gswin64c", "gs"):
  43. if shutil.which(binary) is not None:
  44. gs_windows_binary = binary
  45. break
  46. else:
  47. gs_windows_binary = False
  48. gs_binary = gs_windows_binary
  49. else:
  50. try:
  51. subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
  52. gs_binary = "gs"
  53. except OSError:
  54. gs_binary = False
  55. return gs_binary is not False
  56. def Ghostscript(tile, size, fp, scale=1, transparency=False):
  57. """Render an image using Ghostscript"""
  58. global gs_binary
  59. if not has_ghostscript():
  60. msg = "Unable to locate Ghostscript on paths"
  61. raise OSError(msg)
  62. # Unpack decoder tile
  63. decoder, tile, offset, data = tile[0]
  64. length, bbox = data
  65. # Hack to support hi-res rendering
  66. scale = int(scale) or 1
  67. # orig_size = size
  68. # orig_bbox = bbox
  69. size = (size[0] * scale, size[1] * scale)
  70. # resolution is dependent on bbox and size
  71. res = (
  72. 72.0 * size[0] / (bbox[2] - bbox[0]),
  73. 72.0 * size[1] / (bbox[3] - bbox[1]),
  74. )
  75. out_fd, outfile = tempfile.mkstemp()
  76. os.close(out_fd)
  77. infile_temp = None
  78. if hasattr(fp, "name") and os.path.exists(fp.name):
  79. infile = fp.name
  80. else:
  81. in_fd, infile_temp = tempfile.mkstemp()
  82. os.close(in_fd)
  83. infile = infile_temp
  84. # Ignore length and offset!
  85. # Ghostscript can read it
  86. # Copy whole file to read in Ghostscript
  87. with open(infile_temp, "wb") as f:
  88. # fetch length of fp
  89. fp.seek(0, io.SEEK_END)
  90. fsize = fp.tell()
  91. # ensure start position
  92. # go back
  93. fp.seek(0)
  94. lengthfile = fsize
  95. while lengthfile > 0:
  96. s = fp.read(min(lengthfile, 100 * 1024))
  97. if not s:
  98. break
  99. lengthfile -= len(s)
  100. f.write(s)
  101. device = "pngalpha" if transparency else "ppmraw"
  102. # Build Ghostscript command
  103. command = [
  104. gs_binary,
  105. "-q", # quiet mode
  106. "-g%dx%d" % size, # set output geometry (pixels)
  107. "-r%fx%f" % res, # set input DPI (dots per inch)
  108. "-dBATCH", # exit after processing
  109. "-dNOPAUSE", # don't pause between pages
  110. "-dSAFER", # safe mode
  111. f"-sDEVICE={device}",
  112. f"-sOutputFile={outfile}", # output file
  113. # adjust for image origin
  114. "-c",
  115. f"{-bbox[0]} {-bbox[1]} translate",
  116. "-f",
  117. infile, # input file
  118. # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272)
  119. "-c",
  120. "showpage",
  121. ]
  122. # push data through Ghostscript
  123. try:
  124. startupinfo = None
  125. if sys.platform.startswith("win"):
  126. startupinfo = subprocess.STARTUPINFO()
  127. startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
  128. subprocess.check_call(command, startupinfo=startupinfo)
  129. out_im = Image.open(outfile)
  130. out_im.load()
  131. finally:
  132. try:
  133. os.unlink(outfile)
  134. if infile_temp:
  135. os.unlink(infile_temp)
  136. except OSError:
  137. pass
  138. im = out_im.im.copy()
  139. out_im.close()
  140. return im
  141. class PSFile:
  142. """
  143. Wrapper for bytesio object that treats either CR or LF as end of line.
  144. This class is no longer used internally, but kept for backwards compatibility.
  145. """
  146. def __init__(self, fp):
  147. deprecate(
  148. "PSFile",
  149. 11,
  150. action="If you need the functionality of this class "
  151. "you will need to implement it yourself.",
  152. )
  153. self.fp = fp
  154. self.char = None
  155. def seek(self, offset, whence=io.SEEK_SET):
  156. self.char = None
  157. self.fp.seek(offset, whence)
  158. def readline(self):
  159. s = [self.char or b""]
  160. self.char = None
  161. c = self.fp.read(1)
  162. while (c not in b"\r\n") and len(c):
  163. s.append(c)
  164. c = self.fp.read(1)
  165. self.char = self.fp.read(1)
  166. # line endings can be 1 or 2 of \r \n, in either order
  167. if self.char in b"\r\n":
  168. self.char = None
  169. return b"".join(s).decode("latin-1")
  170. def _accept(prefix):
  171. return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
  172. ##
  173. # Image plugin for Encapsulated PostScript. This plugin supports only
  174. # a few variants of this format.
  175. class EpsImageFile(ImageFile.ImageFile):
  176. """EPS File Parser for the Python Imaging Library"""
  177. format = "EPS"
  178. format_description = "Encapsulated Postscript"
  179. mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
  180. def _open(self):
  181. (length, offset) = self._find_offset(self.fp)
  182. # go to offset - start of "%!PS"
  183. self.fp.seek(offset)
  184. self._mode = "RGB"
  185. self._size = None
  186. byte_arr = bytearray(255)
  187. bytes_mv = memoryview(byte_arr)
  188. bytes_read = 0
  189. reading_header_comments = True
  190. reading_trailer_comments = False
  191. trailer_reached = False
  192. def check_required_header_comments():
  193. if "PS-Adobe" not in self.info:
  194. msg = 'EPS header missing "%!PS-Adobe" comment'
  195. raise SyntaxError(msg)
  196. if "BoundingBox" not in self.info:
  197. msg = 'EPS header missing "%%BoundingBox" comment'
  198. raise SyntaxError(msg)
  199. def _read_comment(s):
  200. nonlocal reading_trailer_comments
  201. try:
  202. m = split.match(s)
  203. except re.error as e:
  204. msg = "not an EPS file"
  205. raise SyntaxError(msg) from e
  206. if m:
  207. k, v = m.group(1, 2)
  208. self.info[k] = v
  209. if k == "BoundingBox":
  210. if v == "(atend)":
  211. reading_trailer_comments = True
  212. elif not self._size or (
  213. trailer_reached and reading_trailer_comments
  214. ):
  215. try:
  216. # Note: The DSC spec says that BoundingBox
  217. # fields should be integers, but some drivers
  218. # put floating point values there anyway.
  219. box = [int(float(i)) for i in v.split()]
  220. self._size = box[2] - box[0], box[3] - box[1]
  221. self.tile = [
  222. ("eps", (0, 0) + self.size, offset, (length, box))
  223. ]
  224. except Exception:
  225. pass
  226. return True
  227. while True:
  228. byte = self.fp.read(1)
  229. if byte == b"":
  230. # if we didn't read a byte we must be at the end of the file
  231. if bytes_read == 0:
  232. break
  233. elif byte in b"\r\n":
  234. # if we read a line ending character, ignore it and parse what
  235. # we have already read. if we haven't read any other characters,
  236. # continue reading
  237. if bytes_read == 0:
  238. continue
  239. else:
  240. # ASCII/hexadecimal lines in an EPS file must not exceed
  241. # 255 characters, not including line ending characters
  242. if bytes_read >= 255:
  243. # only enforce this for lines starting with a "%",
  244. # otherwise assume it's binary data
  245. if byte_arr[0] == ord("%"):
  246. msg = "not an EPS file"
  247. raise SyntaxError(msg)
  248. else:
  249. if reading_header_comments:
  250. check_required_header_comments()
  251. reading_header_comments = False
  252. # reset bytes_read so we can keep reading
  253. # data until the end of the line
  254. bytes_read = 0
  255. byte_arr[bytes_read] = byte[0]
  256. bytes_read += 1
  257. continue
  258. if reading_header_comments:
  259. # Load EPS header
  260. # if this line doesn't start with a "%",
  261. # or does start with "%%EndComments",
  262. # then we've reached the end of the header/comments
  263. if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
  264. check_required_header_comments()
  265. reading_header_comments = False
  266. continue
  267. s = str(bytes_mv[:bytes_read], "latin-1")
  268. if not _read_comment(s):
  269. m = field.match(s)
  270. if m:
  271. k = m.group(1)
  272. if k[:8] == "PS-Adobe":
  273. self.info["PS-Adobe"] = k[9:]
  274. else:
  275. self.info[k] = ""
  276. elif s[0] == "%":
  277. # handle non-DSC PostScript comments that some
  278. # tools mistakenly put in the Comments section
  279. pass
  280. else:
  281. msg = "bad EPS header"
  282. raise OSError(msg)
  283. elif bytes_mv[:11] == b"%ImageData:":
  284. # Check for an "ImageData" descriptor
  285. # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096
  286. # Values:
  287. # columns
  288. # rows
  289. # bit depth (1 or 8)
  290. # mode (1: L, 2: LAB, 3: RGB, 4: CMYK)
  291. # number of padding channels
  292. # block size (number of bytes per row per channel)
  293. # binary/ascii (1: binary, 2: ascii)
  294. # data start identifier (the image data follows after a single line
  295. # consisting only of this quoted value)
  296. image_data_values = byte_arr[11:bytes_read].split(None, 7)
  297. columns, rows, bit_depth, mode_id = (
  298. int(value) for value in image_data_values[:4]
  299. )
  300. if bit_depth == 1:
  301. self._mode = "1"
  302. elif bit_depth == 8:
  303. try:
  304. self._mode = self.mode_map[mode_id]
  305. except ValueError:
  306. break
  307. else:
  308. break
  309. self._size = columns, rows
  310. return
  311. elif trailer_reached and reading_trailer_comments:
  312. # Load EPS trailer
  313. # if this line starts with "%%EOF",
  314. # then we've reached the end of the file
  315. if bytes_mv[:5] == b"%%EOF":
  316. break
  317. s = str(bytes_mv[:bytes_read], "latin-1")
  318. _read_comment(s)
  319. elif bytes_mv[:9] == b"%%Trailer":
  320. trailer_reached = True
  321. bytes_read = 0
  322. check_required_header_comments()
  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, *args, **kwargs):
  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, fp, filename, eps=1):
  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")