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