Spaces:
Running
Running
# | |
# 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 | |
# | |
# -------------------------------------------------------------------- | |
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") | |
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") | |
gs_windows_binary = None | |
if sys.platform.startswith("win"): | |
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 | |
def has_ghostscript(): | |
if gs_windows_binary: | |
return True | |
if not sys.platform.startswith("win"): | |
try: | |
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL) | |
return True | |
except OSError: | |
# No Ghostscript | |
pass | |
return False | |
def Ghostscript(tile, size, fp, scale=1, transparency=False): | |
"""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) | |
# 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", | |
"-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", | |
] | |
if gs_windows_binary is not None: | |
if not gs_windows_binary: | |
raise OSError("Unable to locate Ghostscript on paths") | |
command[0] = gs_windows_binary | |
# 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. | |
""" | |
def __init__(self, fp): | |
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) | |
# Rewrap the open file pointer in something that will | |
# convert line endings and decode to latin-1. | |
fp = PSFile(self.fp) | |
# go to offset - start of "%!PS" | |
fp.seek(offset) | |
box = None | |
self.mode = "RGB" | |
self._size = 1, 1 # FIXME: huh? | |
# | |
# Load EPS header | |
s_raw = fp.readline() | |
s = s_raw.strip("\r\n") | |
while s_raw: | |
if s: | |
if len(s) > 255: | |
raise SyntaxError("not an EPS file") | |
try: | |
m = split.match(s) | |
except re.error as e: | |
raise SyntaxError("not an EPS file") from e | |
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(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 | |
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] == "%": | |
# handle non-DSC PostScript comments that some | |
# tools mistakenly put in the Comments section | |
pass | |
else: | |
raise OSError("bad EPS header") | |
s_raw = fp.readline() | |
s = s_raw.strip("\r\n") | |
if s and s[:1] != "%": | |
break | |
# | |
# Scan for an "ImageData" descriptor | |
while s[:1] == "%": | |
if len(s) > 255: | |
raise SyntaxError("not an EPS file") | |
if s[:11] == "%ImageData:": | |
# Encoded bitmapped image. | |
x, y, bi, mo = s[11:].split(None, 7)[:4] | |
if int(bi) == 1: | |
self.mode = "1" | |
elif int(bi) == 8: | |
try: | |
self.mode = self.mode_map[int(mo)] | |
except ValueError: | |
break | |
else: | |
break | |
self._size = int(x), int(y) | |
return | |
s = fp.readline().strip("\r\n") | |
if not s: | |
break | |
if not box: | |
raise OSError("cannot determine EPS bounding box") | |
def _find_offset(self, fp): | |
s = fp.read(160) | |
if s[:4] == b"%!PS": | |
# for HEAD without binary preview | |
fp.seek(0, io.SEEK_END) | |
length = fp.tell() | |
offset = 0 | |
elif i32(s, 0) == 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 | |
offset = i32(s, 4) | |
length = i32(s, 8) | |
else: | |
raise SyntaxError("not an EPS file") | |
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: | |
raise ValueError("image mode is not supported") | |
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") | |