Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

EpsImagePlugin.py 15KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  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_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. msg = "Unable to locate Ghostscript on paths"
  119. raise OSError(msg)
  120. command[0] = gs_windows_binary
  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):
  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):
  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):
  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_comments = True
  189. def check_required_header_comments():
  190. if "PS-Adobe" not in self.info:
  191. msg = 'EPS header missing "%!PS-Adobe" comment'
  192. raise SyntaxError(msg)
  193. if "BoundingBox" not in self.info:
  194. msg = 'EPS header missing "%%BoundingBox" comment'
  195. raise SyntaxError(msg)
  196. while True:
  197. byte = self.fp.read(1)
  198. if byte == b"":
  199. # if we didn't read a byte we must be at the end of the file
  200. if bytes_read == 0:
  201. break
  202. elif byte in b"\r\n":
  203. # if we read a line ending character, ignore it and parse what
  204. # we have already read. if we haven't read any other characters,
  205. # continue reading
  206. if bytes_read == 0:
  207. continue
  208. else:
  209. # ASCII/hexadecimal lines in an EPS file must not exceed
  210. # 255 characters, not including line ending characters
  211. if bytes_read >= 255:
  212. # only enforce this for lines starting with a "%",
  213. # otherwise assume it's binary data
  214. if byte_arr[0] == ord("%"):
  215. msg = "not an EPS file"
  216. raise SyntaxError(msg)
  217. else:
  218. if reading_comments:
  219. check_required_header_comments()
  220. reading_comments = False
  221. # reset bytes_read so we can keep reading
  222. # data until the end of the line
  223. bytes_read = 0
  224. byte_arr[bytes_read] = byte[0]
  225. bytes_read += 1
  226. continue
  227. if reading_comments:
  228. # Load EPS header
  229. # if this line doesn't start with a "%",
  230. # or does start with "%%EndComments",
  231. # then we've reached the end of the header/comments
  232. if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
  233. check_required_header_comments()
  234. reading_comments = False
  235. continue
  236. s = str(bytes_mv[:bytes_read], "latin-1")
  237. try:
  238. m = split.match(s)
  239. except re.error as e:
  240. msg = "not an EPS file"
  241. raise SyntaxError(msg) from e
  242. if m:
  243. k, v = m.group(1, 2)
  244. self.info[k] = v
  245. if k == "BoundingBox":
  246. try:
  247. # Note: The DSC spec says that BoundingBox
  248. # fields should be integers, but some drivers
  249. # put floating point values there anyway.
  250. box = [int(float(i)) for i in v.split()]
  251. self._size = box[2] - box[0], box[3] - box[1]
  252. self.tile = [
  253. ("eps", (0, 0) + self.size, offset, (length, box))
  254. ]
  255. except Exception:
  256. pass
  257. else:
  258. m = field.match(s)
  259. if m:
  260. k = m.group(1)
  261. if k[:8] == "PS-Adobe":
  262. self.info["PS-Adobe"] = k[9:]
  263. else:
  264. self.info[k] = ""
  265. elif s[0] == "%":
  266. # handle non-DSC PostScript comments that some
  267. # tools mistakenly put in the Comments section
  268. pass
  269. else:
  270. msg = "bad EPS header"
  271. raise OSError(msg)
  272. elif bytes_mv[:11] == b"%ImageData:":
  273. # Check for an "ImageData" descriptor
  274. # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096
  275. # Values:
  276. # columns
  277. # rows
  278. # bit depth (1 or 8)
  279. # mode (1: L, 2: LAB, 3: RGB, 4: CMYK)
  280. # number of padding channels
  281. # block size (number of bytes per row per channel)
  282. # binary/ascii (1: binary, 2: ascii)
  283. # data start identifier (the image data follows after a single line
  284. # consisting only of this quoted value)
  285. image_data_values = byte_arr[11:bytes_read].split(None, 7)
  286. columns, rows, bit_depth, mode_id = [
  287. int(value) for value in image_data_values[:4]
  288. ]
  289. if bit_depth == 1:
  290. self.mode = "1"
  291. elif bit_depth == 8:
  292. try:
  293. self.mode = self.mode_map[mode_id]
  294. except ValueError:
  295. break
  296. else:
  297. break
  298. self._size = columns, rows
  299. return
  300. bytes_read = 0
  301. check_required_header_comments()
  302. if not self._size:
  303. self._size = 1, 1 # errors if this isn't set. why (1,1)?
  304. msg = "cannot determine EPS bounding box"
  305. raise OSError(msg)
  306. def _find_offset(self, fp):
  307. s = fp.read(4)
  308. if s == b"%!PS":
  309. # for HEAD without binary preview
  310. fp.seek(0, io.SEEK_END)
  311. length = fp.tell()
  312. offset = 0
  313. elif i32(s) == 0xC6D3D0C5:
  314. # FIX for: Some EPS file not handled correctly / issue #302
  315. # EPS can contain binary data
  316. # or start directly with latin coding
  317. # more info see:
  318. # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
  319. s = fp.read(8)
  320. offset = i32(s)
  321. length = i32(s, 4)
  322. else:
  323. msg = "not an EPS file"
  324. raise SyntaxError(msg)
  325. return length, offset
  326. def load(self, scale=1, transparency=False):
  327. # Load EPS via Ghostscript
  328. if self.tile:
  329. self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
  330. self.mode = self.im.mode
  331. self._size = self.im.size
  332. self.tile = []
  333. return Image.Image.load(self)
  334. def load_seek(self, *args, **kwargs):
  335. # we can't incrementally load, so force ImageFile.parser to
  336. # use our custom load method by defining this method.
  337. pass
  338. # --------------------------------------------------------------------
  339. def _save(im, fp, filename, eps=1):
  340. """EPS Writer for the Python Imaging Library."""
  341. # make sure image data is available
  342. im.load()
  343. # determine PostScript image mode
  344. if im.mode == "L":
  345. operator = (8, 1, b"image")
  346. elif im.mode == "RGB":
  347. operator = (8, 3, b"false 3 colorimage")
  348. elif im.mode == "CMYK":
  349. operator = (8, 4, b"false 4 colorimage")
  350. else:
  351. msg = "image mode is not supported"
  352. raise ValueError(msg)
  353. if eps:
  354. # write EPS header
  355. fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n")
  356. fp.write(b"%%Creator: PIL 0.1 EpsEncode\n")
  357. # fp.write("%%CreationDate: %s"...)
  358. fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size)
  359. fp.write(b"%%Pages: 1\n")
  360. fp.write(b"%%EndComments\n")
  361. fp.write(b"%%Page: 1 1\n")
  362. fp.write(b"%%ImageData: %d %d " % im.size)
  363. fp.write(b'%d %d 0 1 1 "%s"\n' % operator)
  364. # image header
  365. fp.write(b"gsave\n")
  366. fp.write(b"10 dict begin\n")
  367. fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1]))
  368. fp.write(b"%d %d scale\n" % im.size)
  369. fp.write(b"%d %d 8\n" % im.size) # <= bits
  370. fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1]))
  371. fp.write(b"{ currentfile buf readhexstring pop } bind\n")
  372. fp.write(operator[2] + b"\n")
  373. if hasattr(fp, "flush"):
  374. fp.flush()
  375. ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)])
  376. fp.write(b"\n%%%%EndBinary\n")
  377. fp.write(b"grestore end\n")
  378. if hasattr(fp, "flush"):
  379. fp.flush()
  380. # --------------------------------------------------------------------
  381. Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
  382. Image.register_save(EpsImageFile.format, _save)
  383. Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
  384. Image.register_mime(EpsImageFile.format, "application/postscript")