Pillow/src/PIL/EpsImagePlugin.py

415 lines
12 KiB
Python
Raw Normal View History

2010-07-31 06:52:47 +04:00
#
# 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-08-26 17:47:10 +04:00
# 2014-05-07 e Handling of EPS with binary preview and fixed resolution
# resizing
2010-07-31 06:52:47 +04:00
#
# 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
2015-08-25 15:27:18 +03:00
import sys
import tempfile
from . import Image, ImageFile
2017-03-03 13:32:31 +03:00
from ._binary import i32le as i32
2010-07-31 06:52:47 +04:00
#
# --------------------------------------------------------------------
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
2013-03-06 21:36:22 +04:00
gs_windows_binary = None
2019-03-21 16:28:20 +03:00
if sys.platform.startswith("win"):
2013-03-06 21:36:22 +04:00
import shutil
2019-03-21 16:28:20 +03:00
for binary in ("gswin32c", "gswin64c", "gs"):
2019-09-26 15:12:28 +03:00
if shutil.which(binary) is not None:
2013-03-06 21:36:22 +04:00
gs_windows_binary = binary
break
else:
gs_windows_binary = False
2014-08-26 17:47:10 +04:00
def has_ghostscript():
if gs_windows_binary:
return True
2019-03-21 16:28:20 +03:00
if not sys.platform.startswith("win"):
try:
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
return True
except OSError:
2018-09-26 14:38:44 +03:00
# No Ghostscript
pass
return False
2014-08-26 17:47:10 +04:00
def Ghostscript(tile, size, fp, scale=1, transparency=False):
2013-03-06 21:36:22 +04:00
"""Render an image using Ghostscript"""
2010-07-31 06:52:47 +04:00
# Unpack decoder tile
decoder, tile, offset, data = tile[0]
length, bbox = data
2014-08-26 17:47:10 +04:00
# Hack to support hi-res rendering
scale = int(scale) or 1
2014-08-26 17:47:10 +04:00
# orig_size = size
# orig_bbox = bbox
size = (size[0] * scale, size[1] * scale)
2014-08-26 17:47:10 +04:00
# resolution is dependent on bbox and size
2019-03-21 16:28:20 +03:00
res = (
72.0 * size[0] / (bbox[2] - bbox[0]),
72.0 * size[1] / (bbox[3] - bbox[1]),
2019-03-21 16:28:20 +03:00
)
out_fd, outfile = tempfile.mkstemp()
os.close(out_fd)
infile_temp = None
2019-03-21 16:28:20 +03:00
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
2018-09-26 14:38:44 +03:00
# Ignore length and offset!
# Ghostscript can read it
# Copy whole file to read in Ghostscript
2019-03-21 16:28:20 +03:00
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:
2019-03-21 16:28:20 +03:00
s = fp.read(min(lengthfile, 100 * 1024))
if not s:
break
lengthfile -= len(s)
f.write(s)
2010-07-31 06:52:47 +04:00
device = "pngalpha" if transparency else "ppmraw"
2018-09-26 14:38:44 +03:00
# Build Ghostscript command
2019-03-21 16:28:20 +03:00
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
2019-03-21 16:28:20 +03:00
# adjust for image origin
"-c",
f"{-bbox[0]} {-bbox[1]} translate",
2019-03-21 16:28:20 +03:00
"-f",
infile, # input file
# showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272)
"-c",
"showpage",
]
2014-08-26 17:47:10 +04:00
2013-03-06 21:36:22 +04:00
if gs_windows_binary is not None:
2014-01-09 07:07:35 +04:00
if not gs_windows_binary:
raise OSError("Unable to locate Ghostscript on paths")
2013-03-06 21:36:22 +04:00
command[0] = gs_windows_binary
2010-07-31 06:52:47 +04:00
2018-09-26 14:38:44 +03:00
# push data through Ghostscript
2010-07-31 06:52:47 +04:00
try:
startupinfo = None
2019-03-21 16:28:20 +03:00
if sys.platform.startswith("win"):
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
subprocess.check_call(command, startupinfo=startupinfo)
2020-02-18 12:49:05 +03:00
out_im = Image.open(outfile)
out_im.load()
2010-07-31 06:52:47 +04:00
finally:
try:
os.unlink(outfile)
if infile_temp:
os.unlink(infile_temp)
2015-12-02 08:23:49 +03:00
except OSError:
pass
2020-02-18 12:49:05 +03:00
im = out_im.im.copy()
out_im.close()
return im
2010-07-31 06:52:47 +04:00
class PSFile:
"""
Wrapper for bytesio object that treats either CR or LF as end of line.
"""
2019-03-21 16:28:20 +03:00
2010-07-31 06:52:47 +04:00
def __init__(self, fp):
self.fp = fp
self.char = None
def seek(self, offset, whence=io.SEEK_SET):
2010-07-31 06:52:47 +04:00
self.char = None
self.fp.seek(offset, whence)
2010-07-31 06:52:47 +04:00
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)
2010-07-31 06:52:47 +04:00
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")
2010-07-31 06:52:47 +04:00
def _accept(prefix):
2019-03-21 16:28:20 +03:00
return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
2010-07-31 06:52:47 +04:00
##
2020-07-10 11:48:02 +03:00
# Image plugin for Encapsulated PostScript. This plugin supports only
2010-07-31 06:52:47 +04:00
# a few variants of this format.
2014-08-26 17:47:10 +04:00
2010-07-31 06:52:47 +04:00
class EpsImageFile(ImageFile.ImageFile):
"""EPS File Parser for the Python Imaging Library"""
format = "EPS"
format_description = "Encapsulated Postscript"
2016-04-13 11:27:46 +03:00
mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
2010-07-31 06:52:47 +04:00
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)
2010-07-31 06:52:47 +04:00
2014-08-26 17:47:10 +04:00
# go to offset - start of "%!PS"
2010-07-31 06:52:47 +04:00
fp.seek(offset)
2014-08-26 17:47:10 +04:00
2010-07-31 06:52:47 +04:00
box = None
self.mode = "RGB"
self._size = 1, 1 # FIXME: huh?
2010-07-31 06:52:47 +04:00
#
# Load EPS header
s_raw = fp.readline()
2019-03-21 16:28:20 +03:00
s = s_raw.strip("\r\n")
while s_raw:
if s:
if len(s) > 255:
raise SyntaxError("not an EPS file")
2010-07-31 06:52:47 +04:00
try:
m = split.match(s)
except re.error as e:
raise SyntaxError("not an EPS file") from e
2010-07-31 06:52:47 +04:00
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]
2019-03-21 16:28:20 +03:00
self.tile = [
("eps", (0, 0) + self.size, offset, (length, box))
]
2018-09-26 14:38:44 +03:00
except Exception:
pass
2010-07-31 06:52:47 +04:00
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] = ""
2019-03-21 16:28:20 +03:00
elif s[0] == "%":
2020-07-10 11:48:02 +03:00
# handle non-DSC PostScript comments that some
# tools mistakenly put in the Comments section
pass
else:
raise OSError("bad EPS header")
2010-07-31 06:52:47 +04:00
s_raw = fp.readline()
2019-03-21 16:28:20 +03:00
s = s_raw.strip("\r\n")
2010-07-31 06:52:47 +04:00
if s and s[:1] != "%":
2010-07-31 06:52:47 +04:00
break
#
# Scan for an "ImageData" descriptor
2015-03-26 15:06:54 +03:00
while s[:1] == "%":
2010-07-31 06:52:47 +04:00
if len(s) > 255:
raise SyntaxError("not an EPS file")
2010-07-31 06:52:47 +04:00
if s[:11] == "%ImageData:":
# Encoded bitmapped image.
2015-04-24 11:24:52 +03:00
x, y, bi, mo = s[11:].split(None, 7)[:4]
2010-07-31 06:52:47 +04:00
2022-08-13 11:32:29 +03:00
if int(bi) == 1:
self.mode = "1"
elif int(bi) == 8:
try:
self.mode = self.mode_map[int(mo)]
except ValueError:
break
else:
2010-07-31 06:52:47 +04:00
break
self._size = int(x), int(y)
return
2019-03-21 16:28:20 +03:00
s = fp.readline().strip("\r\n")
2010-07-31 06:52:47 +04:00
if not s:
break
if not box:
raise OSError("cannot determine EPS bounding box")
2010-07-31 06:52:47 +04:00
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")
2022-04-10 19:25:40 +03:00
return length, offset
def load(self, scale=1, transparency=False):
2010-07-31 06:52:47 +04:00
# 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)
2010-07-31 06:52:47 +04:00
2014-08-26 17:47:10 +04:00
def load_seek(self, *args, **kwargs):
# we can't incrementally load, so force ImageFile.parser to
2014-08-26 17:47:10 +04:00
# use our custom load method by defining this method.
pass
2014-08-26 17:47:10 +04:00
2010-07-31 06:52:47 +04:00
#
# --------------------------------------------------------------------
2019-03-21 16:28:20 +03:00
2010-07-31 06:52:47 +04:00
def _save(im, fp, filename, eps=1):
"""EPS Writer for the Python Imaging Library."""
#
# make sure image data is available
im.load()
#
2020-07-10 11:48:02 +03:00
# determine PostScript image mode
2010-07-31 06:52:47 +04:00
if im.mode == "L":
operator = (8, 1, b"image")
2010-07-31 06:52:47 +04:00
elif im.mode == "RGB":
operator = (8, 3, b"false 3 colorimage")
2010-07-31 06:52:47 +04:00
elif im.mode == "CMYK":
operator = (8, 4, b"false 4 colorimage")
2010-07-31 06:52:47 +04:00
else:
raise ValueError("image mode is not supported")
2010-07-31 06:52:47 +04:00
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()
2010-07-31 06:52:47 +04:00
2019-03-21 16:28:20 +03:00
2010-07-31 06:52:47 +04:00
#
# --------------------------------------------------------------------
2018-03-03 12:54:00 +03:00
2010-07-31 06:52:47 +04:00
Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
Image.register_save(EpsImageFile.format, _save)
2016-04-25 07:59:02 +03:00
Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
2010-07-31 06:52:47 +04:00
Image.register_mime(EpsImageFile.format, "application/postscript")