123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480 |
- #
- # The Python Imaging Library.
- # $Id$
- #
- # EPS file handling
- #
- # History:
- # 1995-09-01 fl Created (0.1)
- # 1996-05-18 fl Don't choke on "atend" fields, Ghostscript interface (0.2)
- # 1996-08-22 fl Don't choke on floating point BoundingBox values
- # 1996-08-23 fl Handle files from Macintosh (0.3)
- # 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4)
- # 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5)
- # 2014-05-07 e Handling of EPS with binary preview and fixed resolution
- # resizing
- #
- # Copyright (c) 1997-2003 by Secret Labs AB.
- # Copyright (c) 1995-2003 by Fredrik Lundh
- #
- # See the README file for information on usage and redistribution.
- #
- import io
- import os
- import re
- import subprocess
- import sys
- import tempfile
- from . import Image, ImageFile
- from ._binary import i32le as i32
- from ._deprecate import deprecate
- # --------------------------------------------------------------------
- split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
- field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
- gs_binary = None
- gs_windows_binary = None
- def has_ghostscript():
- global gs_binary, gs_windows_binary
- if gs_binary is None:
- if sys.platform.startswith("win"):
- if gs_windows_binary is None:
- import shutil
- for binary in ("gswin32c", "gswin64c", "gs"):
- if shutil.which(binary) is not None:
- gs_windows_binary = binary
- break
- else:
- gs_windows_binary = False
- gs_binary = gs_windows_binary
- else:
- try:
- subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
- gs_binary = "gs"
- except OSError:
- gs_binary = False
- return gs_binary is not False
- def Ghostscript(tile, size, fp, scale=1, transparency=False):
- """Render an image using Ghostscript"""
- global gs_binary
- if not has_ghostscript():
- msg = "Unable to locate Ghostscript on paths"
- raise OSError(msg)
- # Unpack decoder tile
- decoder, tile, offset, data = tile[0]
- length, bbox = data
- # Hack to support hi-res rendering
- scale = int(scale) or 1
- # orig_size = size
- # orig_bbox = bbox
- size = (size[0] * scale, size[1] * scale)
- # resolution is dependent on bbox and size
- res = (
- 72.0 * size[0] / (bbox[2] - bbox[0]),
- 72.0 * size[1] / (bbox[3] - bbox[1]),
- )
- out_fd, outfile = tempfile.mkstemp()
- os.close(out_fd)
- infile_temp = None
- if hasattr(fp, "name") and os.path.exists(fp.name):
- infile = fp.name
- else:
- in_fd, infile_temp = tempfile.mkstemp()
- os.close(in_fd)
- infile = infile_temp
- # Ignore length and offset!
- # Ghostscript can read it
- # Copy whole file to read in Ghostscript
- with open(infile_temp, "wb") as f:
- # fetch length of fp
- fp.seek(0, io.SEEK_END)
- fsize = fp.tell()
- # ensure start position
- # go back
- fp.seek(0)
- lengthfile = fsize
- while lengthfile > 0:
- s = fp.read(min(lengthfile, 100 * 1024))
- if not s:
- break
- lengthfile -= len(s)
- f.write(s)
- device = "pngalpha" if transparency else "ppmraw"
- # Build Ghostscript command
- command = [
- gs_binary,
- "-q", # quiet mode
- "-g%dx%d" % size, # set output geometry (pixels)
- "-r%fx%f" % res, # set input DPI (dots per inch)
- "-dBATCH", # exit after processing
- "-dNOPAUSE", # don't pause between pages
- "-dSAFER", # safe mode
- f"-sDEVICE={device}",
- f"-sOutputFile={outfile}", # output file
- # adjust for image origin
- "-c",
- f"{-bbox[0]} {-bbox[1]} translate",
- "-f",
- infile, # input file
- # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272)
- "-c",
- "showpage",
- ]
- # push data through Ghostscript
- try:
- startupinfo = None
- if sys.platform.startswith("win"):
- startupinfo = subprocess.STARTUPINFO()
- startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
- subprocess.check_call(command, startupinfo=startupinfo)
- out_im = Image.open(outfile)
- out_im.load()
- finally:
- try:
- os.unlink(outfile)
- if infile_temp:
- os.unlink(infile_temp)
- except OSError:
- pass
- im = out_im.im.copy()
- out_im.close()
- return im
- class PSFile:
- """
- Wrapper for bytesio object that treats either CR or LF as end of line.
- This class is no longer used internally, but kept for backwards compatibility.
- """
- def __init__(self, fp):
- deprecate(
- "PSFile",
- 11,
- action="If you need the functionality of this class "
- "you will need to implement it yourself.",
- )
- self.fp = fp
- self.char = None
- def seek(self, offset, whence=io.SEEK_SET):
- self.char = None
- self.fp.seek(offset, whence)
- def readline(self):
- s = [self.char or b""]
- self.char = None
- c = self.fp.read(1)
- while (c not in b"\r\n") and len(c):
- s.append(c)
- c = self.fp.read(1)
- self.char = self.fp.read(1)
- # line endings can be 1 or 2 of \r \n, in either order
- if self.char in b"\r\n":
- self.char = None
- return b"".join(s).decode("latin-1")
- def _accept(prefix):
- return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
- ##
- # Image plugin for Encapsulated PostScript. This plugin supports only
- # a few variants of this format.
- class EpsImageFile(ImageFile.ImageFile):
- """EPS File Parser for the Python Imaging Library"""
- format = "EPS"
- format_description = "Encapsulated Postscript"
- mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
- def _open(self):
- (length, offset) = self._find_offset(self.fp)
- # go to offset - start of "%!PS"
- self.fp.seek(offset)
- self._mode = "RGB"
- self._size = None
- byte_arr = bytearray(255)
- bytes_mv = memoryview(byte_arr)
- bytes_read = 0
- reading_header_comments = True
- reading_trailer_comments = False
- trailer_reached = False
- def check_required_header_comments():
- if "PS-Adobe" not in self.info:
- msg = 'EPS header missing "%!PS-Adobe" comment'
- raise SyntaxError(msg)
- if "BoundingBox" not in self.info:
- msg = 'EPS header missing "%%BoundingBox" comment'
- raise SyntaxError(msg)
- def _read_comment(s):
- nonlocal reading_trailer_comments
- try:
- m = split.match(s)
- except re.error as e:
- msg = "not an EPS file"
- raise SyntaxError(msg) from e
- if m:
- k, v = m.group(1, 2)
- self.info[k] = v
- if k == "BoundingBox":
- if v == "(atend)":
- reading_trailer_comments = True
- elif not self._size or (
- trailer_reached and reading_trailer_comments
- ):
- try:
- # Note: The DSC spec says that BoundingBox
- # fields should be integers, but some drivers
- # put floating point values there anyway.
- box = [int(float(i)) for i in v.split()]
- self._size = box[2] - box[0], box[3] - box[1]
- self.tile = [
- ("eps", (0, 0) + self.size, offset, (length, box))
- ]
- except Exception:
- pass
- return True
- while True:
- byte = self.fp.read(1)
- if byte == b"":
- # if we didn't read a byte we must be at the end of the file
- if bytes_read == 0:
- break
- elif byte in b"\r\n":
- # if we read a line ending character, ignore it and parse what
- # we have already read. if we haven't read any other characters,
- # continue reading
- if bytes_read == 0:
- continue
- else:
- # ASCII/hexadecimal lines in an EPS file must not exceed
- # 255 characters, not including line ending characters
- if bytes_read >= 255:
- # only enforce this for lines starting with a "%",
- # otherwise assume it's binary data
- if byte_arr[0] == ord("%"):
- msg = "not an EPS file"
- raise SyntaxError(msg)
- else:
- if reading_header_comments:
- check_required_header_comments()
- reading_header_comments = False
- # reset bytes_read so we can keep reading
- # data until the end of the line
- bytes_read = 0
- byte_arr[bytes_read] = byte[0]
- bytes_read += 1
- continue
- if reading_header_comments:
- # Load EPS header
- # if this line doesn't start with a "%",
- # or does start with "%%EndComments",
- # then we've reached the end of the header/comments
- if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
- check_required_header_comments()
- reading_header_comments = False
- continue
- s = str(bytes_mv[:bytes_read], "latin-1")
- if not _read_comment(s):
- m = field.match(s)
- if m:
- k = m.group(1)
- if k[:8] == "PS-Adobe":
- self.info["PS-Adobe"] = k[9:]
- else:
- self.info[k] = ""
- elif s[0] == "%":
- # handle non-DSC PostScript comments that some
- # tools mistakenly put in the Comments section
- pass
- else:
- msg = "bad EPS header"
- raise OSError(msg)
- elif bytes_mv[:11] == b"%ImageData:":
- # Check for an "ImageData" descriptor
- # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096
- # Values:
- # columns
- # rows
- # bit depth (1 or 8)
- # mode (1: L, 2: LAB, 3: RGB, 4: CMYK)
- # number of padding channels
- # block size (number of bytes per row per channel)
- # binary/ascii (1: binary, 2: ascii)
- # data start identifier (the image data follows after a single line
- # consisting only of this quoted value)
- image_data_values = byte_arr[11:bytes_read].split(None, 7)
- columns, rows, bit_depth, mode_id = (
- int(value) for value in image_data_values[:4]
- )
- if bit_depth == 1:
- self._mode = "1"
- elif bit_depth == 8:
- try:
- self._mode = self.mode_map[mode_id]
- except ValueError:
- break
- else:
- break
- self._size = columns, rows
- return
- elif trailer_reached and reading_trailer_comments:
- # Load EPS trailer
- # if this line starts with "%%EOF",
- # then we've reached the end of the file
- if bytes_mv[:5] == b"%%EOF":
- break
- s = str(bytes_mv[:bytes_read], "latin-1")
- _read_comment(s)
- elif bytes_mv[:9] == b"%%Trailer":
- trailer_reached = True
- bytes_read = 0
- check_required_header_comments()
- if not self._size:
- msg = "cannot determine EPS bounding box"
- raise OSError(msg)
- def _find_offset(self, fp):
- s = fp.read(4)
- if s == b"%!PS":
- # for HEAD without binary preview
- fp.seek(0, io.SEEK_END)
- length = fp.tell()
- offset = 0
- elif i32(s) == 0xC6D3D0C5:
- # FIX for: Some EPS file not handled correctly / issue #302
- # EPS can contain binary data
- # or start directly with latin coding
- # more info see:
- # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
- s = fp.read(8)
- offset = i32(s)
- length = i32(s, 4)
- else:
- msg = "not an EPS file"
- raise SyntaxError(msg)
- return length, offset
- def load(self, scale=1, transparency=False):
- # Load EPS via Ghostscript
- if self.tile:
- self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
- self._mode = self.im.mode
- self._size = self.im.size
- self.tile = []
- return Image.Image.load(self)
- def load_seek(self, *args, **kwargs):
- # we can't incrementally load, so force ImageFile.parser to
- # use our custom load method by defining this method.
- pass
- # --------------------------------------------------------------------
- def _save(im, fp, filename, eps=1):
- """EPS Writer for the Python Imaging Library."""
- # make sure image data is available
- im.load()
- # determine PostScript image mode
- if im.mode == "L":
- operator = (8, 1, b"image")
- elif im.mode == "RGB":
- operator = (8, 3, b"false 3 colorimage")
- elif im.mode == "CMYK":
- operator = (8, 4, b"false 4 colorimage")
- else:
- msg = "image mode is not supported"
- raise ValueError(msg)
- if eps:
- # write EPS header
- fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n")
- fp.write(b"%%Creator: PIL 0.1 EpsEncode\n")
- # fp.write("%%CreationDate: %s"...)
- fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size)
- fp.write(b"%%Pages: 1\n")
- fp.write(b"%%EndComments\n")
- fp.write(b"%%Page: 1 1\n")
- fp.write(b"%%ImageData: %d %d " % im.size)
- fp.write(b'%d %d 0 1 1 "%s"\n' % operator)
- # image header
- fp.write(b"gsave\n")
- fp.write(b"10 dict begin\n")
- fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1]))
- fp.write(b"%d %d scale\n" % im.size)
- fp.write(b"%d %d 8\n" % im.size) # <= bits
- fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1]))
- fp.write(b"{ currentfile buf readhexstring pop } bind\n")
- fp.write(operator[2] + b"\n")
- if hasattr(fp, "flush"):
- fp.flush()
- ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)])
- fp.write(b"\n%%%%EndBinary\n")
- fp.write(b"grestore end\n")
- if hasattr(fp, "flush"):
- fp.flush()
- # --------------------------------------------------------------------
- Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
- Image.register_save(EpsImageFile.format, _save)
- Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
- Image.register_mime(EpsImageFile.format, "application/postscript")
|