# # 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) # # 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. # __version__ = "0.5" import re import io from PIL import Image, ImageFile, _binary # # -------------------------------------------------------------------- i32 = _binary.i32le o32 = _binary.o32le split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") gs_windows_binary = None import sys if sys.platform.startswith('win'): import shutil if hasattr(shutil, 'which'): which = shutil.which else: # Python < 3.3 import distutils.spawn which = distutils.spawn.find_executable for binary in ('gswin32c', 'gswin64c', 'gs'): if which(binary) is not None: gs_windows_binary = binary break else: gs_windows_binary = False def Ghostscript(tile, size, fp, scale=1): """Render an image using Ghostscript""" # 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) bbox = [bbox[0], bbox[1], bbox[2] * scale, bbox[3] * scale] #print("Ghostscript", scale, size, orig_size, bbox, orig_bbox) import tempfile, os file = tempfile.mktemp() # Build ghostscript command command = ["gs", "-q", # quite mode "-g%dx%d" % size, # set output geometry (pixels) "-r%d" % (72*scale), # set input DPI (dots per inch) "-dNOPAUSE -dSAFER", # don't pause between pages, safe mode "-sDEVICE=ppmraw", # ppm driver "-sOutputFile=%s" % file,# output file "- >/dev/null 2>/dev/null"] if gs_windows_binary is not None: if gs_windows_binary is False: raise WindowsError('Unable to locate Ghostscript on paths') command[0] = gs_windows_binary command[-1] = '- >nul 2>nul' command = " ".join(command) # push data through ghostscript try: gs = os.popen(command, "w") # adjust for image origin if bbox[0] != 0 or bbox[1] != 0: gs.write("%d %d translate\n" % (-bbox[0], -bbox[1])) fp.seek(offset) while length > 0: s = fp.read(8192) if not s: break length = length - len(s) gs.write(s) status = gs.close() if status: raise IOError("gs failed (status %d)" % status) im = Image.core.open_ppm(file) finally: try: os.unlink(file) except: pass return im class PSFile: """Wrapper that treats either CR or LF as end of line.""" def __init__(self, fp): self.fp = fp self.char = None def __getattr__(self, id): v = getattr(self.fp, id) setattr(self, id, v) return v def seek(self, offset, whence=0): self.char = None self.fp.seek(offset, whence) def read(self, count): return self.fp.read(count).decode('latin-1') def tell(self): pos = self.fp.tell() if self.char: pos = pos - 1 return pos def readline(self): s = b"" if self.char: c = self.char self.char = None else: c = self.fp.read(1) while c not in b"\r\n": s = s + c c = self.fp.read(1) if c == b"\r": self.char = self.fp.read(1) if self.char == b"\n": self.char = None return s.decode('latin-1') + "\n" def _accept(prefix): return prefix[:4] == b"%!PS" or 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" def _open(self): # FIXME: should check the first 512 bytes to see if this # really is necessary (platform-dependent, though...) fp = PSFile(self.fp) # HEAD s = fp.read(512) if s[:4] == "%!PS": offset = 0 fp.seek(0, 2) length = fp.tell() elif i32(s) == 0xC6D3D0C5: offset = i32(s[4:]) length = i32(s[8:]) fp.seek(offset) else: raise SyntaxError("not an EPS file") fp.seek(offset) box = None self.mode = "RGB" self.size = 1, 1 # FIXME: huh? # # Load EPS header s = fp.readline() while s: if len(s) > 255: raise SyntaxError("not an EPS file") if s[-2:] == '\r\n': s = s[:-2] elif s[-1:] == '\n': s = s[:-1] try: m = split.match(s) except re.error as v: raise SyntaxError("not an EPS file") if m: k, v = m.group(1, 2) self.info[k] = v if k == "BoundingBox": try: # Note: The DSC spec says that BoundingBox # fields should be integers, but some drivers # put floating point values there anyway. box = [int(float(s)) for s in v.split()] self.size = box[2] - box[0], box[3] - box[1] self.tile = [("eps", (0,0) + self.size, offset, (length, box))] except: pass else: m = field.match(s) if m: k = m.group(1) if k == "EndComments": break if k[:8] == "PS-Adobe": self.info[k[:8]] = k[9:] else: self.info[k] = "" elif s[0:1] == '%': # handle non-DSC Postscript comments that some # tools mistakenly put in the Comments section pass else: raise IOError("bad EPS header") s = fp.readline() if s[:1] != "%": break # # Scan for an "ImageData" descriptor while s[0] == "%": if len(s) > 255: raise SyntaxError("not an EPS file") if s[-2:] == '\r\n': s = s[:-2] elif s[-1:] == '\n': s = s[:-1] if s[:11] == "%ImageData:": [x, y, bi, mo, z3, z4, en, id] =\ s[11:].split(None, 7) x = int(x); y = int(y) bi = int(bi) mo = int(mo) en = int(en) if en == 1: decoder = "eps_binary" elif en == 2: decoder = "eps_hex" else: break if bi != 8: break if mo == 1: self.mode = "L" elif mo == 2: self.mode = "LAB" elif mo == 3: self.mode = "RGB" else: break if id[:1] == id[-1:] == '"': id = id[1:-1] # Scan forward to the actual image data while True: s = fp.readline() if not s: break if s[:len(id)] == id: self.size = x, y self.tile2 = [(decoder, (0, 0, x, y), fp.tell(), 0)] return s = fp.readline() if not s: break if not box: raise IOError("cannot determine EPS bounding box") def load(self, scale=1): # Load EPS via Ghostscript if not self.tile: return self.im = Ghostscript(self.tile, self.size, self.fp, scale) self.mode = self.im.mode self.size = self.im.size self.tile = [] # # -------------------------------------------------------------------- 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, "image") elif im.mode == "RGB": operator = (8, 3, "false 3 colorimage") elif im.mode == "CMYK": operator = (8, 4, "false 4 colorimage") else: raise ValueError("image mode is not supported") class NoCloseStream: def __init__(self, fp): self.fp = fp def __getattr__(self, name): return getattr(self.fp, name) def close(self): pass base_fp = fp fp = io.TextIOWrapper(NoCloseStream(fp), encoding='latin-1') if eps: # # write EPS header fp.write("%!PS-Adobe-3.0 EPSF-3.0\n") fp.write("%%Creator: PIL 0.1 EpsEncode\n") #fp.write("%%CreationDate: %s"...) fp.write("%%%%BoundingBox: 0 0 %d %d\n" % im.size) fp.write("%%Pages: 1\n") fp.write("%%EndComments\n") fp.write("%%Page: 1 1\n") fp.write("%%ImageData: %d %d " % im.size) fp.write("%d %d 0 1 1 \"%s\"\n" % operator) # # image header fp.write("gsave\n") fp.write("10 dict begin\n") fp.write("/buf %d string def\n" % (im.size[0] * operator[1])) fp.write("%d %d scale\n" % im.size) fp.write("%d %d 8\n" % im.size) # <= bits fp.write("[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1])) fp.write("{ currentfile buf readhexstring pop } bind\n") fp.write(operator[2] + "\n") fp.flush() ImageFile._save(im, base_fp, [("eps", (0,0)+im.size, 0, None)]) fp.write("\n%%%%EndBinary\n") fp.write("grestore end\n") fp.flush() # # -------------------------------------------------------------------- Image.register_open(EpsImageFile.format, EpsImageFile, _accept) Image.register_save(EpsImageFile.format, _save) Image.register_extension(EpsImageFile.format, ".ps") Image.register_extension(EpsImageFile.format, ".eps") Image.register_mime(EpsImageFile.format, "application/postscript")