This commit is contained in:
Karsten Wolf 2014-08-05 09:58:08 +00:00
commit abdf466d84
2 changed files with 107 additions and 57 deletions

156
PIL/EpsImagePlugin.py Normal file → Executable file
View File

@ -23,6 +23,8 @@ __version__ = "0.5"
import re import re
import io import io
import unicodedata
from PIL import Image, ImageFile, _binary from PIL import Image, ImageFile, _binary
# #
@ -34,6 +36,8 @@ o32 = _binary.o32le
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
defaultDPI = 600.0
gs_windows_binary = None gs_windows_binary = None
import sys import sys
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
@ -64,23 +68,29 @@ def has_ghostscript():
# no ghostscript # no ghostscript
pass pass
return False return False
def Ghostscript(tile, size, fp, scale=1): def makeunicode( s, enc="latin-1", normalizer='NFC'):
"""return a normalized unicode string"""
try:
if type(s) != unicode:
s = unicode(s, enc)
except:
pass
return unicodedata.normalize(normalizer, s)
def Ghostscript(tile, size, fp):
"""Render an image using Ghostscript""" """Render an image using Ghostscript"""
# Unpack decoder tile # Unpack decoder tile
decoder, tile, offset, data = tile[0] decoder, tile, offset, data = tile[0]
length, bbox = data length, bbox = data
#Hack to support hi-res rendering # bbox is in points == 1/72 inch
scale = int(scale) or 1 # size is in pixels, a device unit
orig_size = size xpointsize = bbox[2] - bbox[0]
orig_bbox = bbox ypointsize = bbox[3] - bbox[1]
size = (size[0] * scale, size[1] * scale) xdpi = size[0] / (xpointsize / 72.0)
# resolution is dependend on bbox and size ydpi = size[1] / (ypointsize / 72.0)
res = ( float((72.0 * size[0]) / (bbox[2]-bbox[0])), float((72.0 * size[1]) / (bbox[3]-bbox[1])) )
#print("Ghostscript", scale, size, orig_size, bbox, orig_bbox, res)
import tempfile, os, subprocess import tempfile, os, subprocess
@ -90,28 +100,27 @@ def Ghostscript(tile, size, fp, scale=1):
os.close(in_fd) os.close(in_fd)
# ignore length and offset! # ignore length and offset!
# ghostscript can read it # ghostscript can read it
# copy whole file to read in ghostscript # copy whole file to read in ghostscript
with open(infile, 'wb') as f: with open(infile, 'wb') as f:
# fetch length of fp # fetch length of fp
fp.seek(0, 2) fp.seek(0, 2)
fsize = fp.tell() lengthfile = fp.tell()
# ensure start position # ensure start position
# go back # go back
fp.seek(0) fp.seek(0)
lengthfile = fsize
while lengthfile > 0: while lengthfile > 0:
s = fp.read(min(lengthfile, 100*1024)) s = fp.read( 4*1024*1024 )
if not s: if not s:
break break
length -= len(s) lengthfile -= len(s)
f.write(s) f.write(s)
# Build ghostscript command # Build ghostscript command
command = ["gs", command = ["gs",
"-q", # quiet mode "-q", # quiet mode
"-g%dx%d" % size, # set output geometry (pixels) "-g%dx%d" % size, # set output geometry (pixels)
"-r%fx%f" % res, # set input DPI (dots per inch) "-r%fx%f" % (xdpi,ydpi), # set input DPI (dots per inch)
"-dNOPAUSE -dSAFER", # don't pause between pages, safe mode "-dNOPAUSE -dSAFER", # don't pause between pages, safe mode
"-sDEVICE=ppmraw", # ppm driver "-sDEVICE=ppmraw", # ppm driver
"-sOutputFile=%s" % outfile, # output file "-sOutputFile=%s" % outfile, # output file
@ -119,7 +128,7 @@ def Ghostscript(tile, size, fp, scale=1):
# adjust for image origin # adjust for image origin
"-f", infile, # input file "-f", infile, # input file
] ]
if gs_windows_binary is not None: if gs_windows_binary is not None:
if not gs_windows_binary: if not gs_windows_binary:
raise WindowsError('Unable to locate Ghostscript on paths') raise WindowsError('Unable to locate Ghostscript on paths')
@ -210,10 +219,10 @@ class EpsImageFile(ImageFile.ImageFile):
fp.seek(0) fp.seek(0)
sb = fp.readbinary(160) sb = fp.readbinary(160)
offset = 0
if s[:4] == "%!PS": if s[:4] == "%!PS":
fp.seek(0, 2) fp.seek(0, 2)
length = fp.tell() length = fp.tell()
offset = 0
elif i32(sb[0:4]) == 0xC6D3D0C5: elif i32(sb[0:4]) == 0xC6D3D0C5:
offset = i32(sb[4:8]) offset = i32(sb[4:8])
length = i32(sb[8:12]) length = i32(sb[8:12])
@ -222,27 +231,26 @@ class EpsImageFile(ImageFile.ImageFile):
# go to offset - start of "%!PS" # go to offset - start of "%!PS"
fp.seek(offset) fp.seek(offset)
box = None box = None
self.mode = "RGB" self.mode = "RGB"
self.size = 1, 1 # FIXME: huh? self.size = 1, 1 # FIXME: huh?
self.pixelsize = False
self.pointsize = False
self.bbox = False
# #
# Load EPS header # Load EPS header
s = fp.readline() s = fp.readline()
s = s.rstrip("\r\n")
s = makeunicode(s)
while s: while s:
if len(s) > 255: if len(s) > 255:
raise SyntaxError("not an EPS file") raise SyntaxError("not an EPS file")
if s[-2:] == '\r\n':
s = s[:-2]
elif s[-1:] == '\n':
s = s[:-1]
try: try:
m = split.match(s) m = split.match(s)
except re.error as v: except re.error as v:
@ -256,10 +264,31 @@ class EpsImageFile(ImageFile.ImageFile):
# Note: The DSC spec says that BoundingBox # Note: The DSC spec says that BoundingBox
# fields should be integers, but some drivers # fields should be integers, but some drivers
# put floating point values there anyway. # put floating point values there anyway.
box = [int(float(s)) for s in v.split()]
# if self.pointsize is not False, we already have a hiresbbox
if not self.pointsize:
box = [int(float(s)) for s in v.split()]
self.bbox = box
pointsize = box[2] - box[0], box[3] - box[1]
self.size = box[2] - box[0], box[3] - box[1]
self.tile = [ ("eps",
(0,0) + self.size,
offset,
(length, box))]
self.pointsize = pointsize
except:
pass
if k == "HiResBoundingBox":
try:
box = [float(s) for s in v.split()]
self.bbox = box
pointsize = box[2] - box[0], box[3] - box[1]
self.size = box[2] - box[0], box[3] - box[1] self.size = box[2] - box[0], box[3] - box[1]
self.tile = [("eps", (0,0) + self.size, offset, self.tile = [ ("eps",
(0,0) + self.size,
offset,
(length, box))] (length, box))]
self.pointsize = pointsize
except: except:
pass pass
@ -284,11 +313,12 @@ class EpsImageFile(ImageFile.ImageFile):
raise IOError("bad EPS header") raise IOError("bad EPS header")
s = fp.readline() s = fp.readline()
s = s.rstrip("\r\n")
s = makeunicode(s)
if s[:1] != "%": if s[:1] != "%":
break break
# #
# Scan for an "ImageData" descriptor # Scan for an "ImageData" descriptor
@ -303,12 +333,12 @@ class EpsImageFile(ImageFile.ImageFile):
s = s[:-1] s = s[:-1]
if s[:11] == "%ImageData:": if s[:11] == "%ImageData:":
[x, y, bi, mo, z3, z4, en, starttag] = s[11:].split(None, 7)
[x, y, bi, mo, z3, z4, en, id] =\ x = int(x)
s[11:].split(None, 7) y = int(y)
self.pixelsize = (x,y)
x = int(x); y = int(y)
bi = int(bi) bi = int(bi)
mo = int(mo) mo = int(mo)
@ -319,46 +349,66 @@ class EpsImageFile(ImageFile.ImageFile):
elif en == 2: elif en == 2:
decoder = "eps_hex" decoder = "eps_hex"
else: else:
break pass
if bi != 8: if bi != 8:
break break
if mo == 1: if mo == 1:
self.mode = "L" self.mode = "L"
elif mo == 2: elif mo == 2:
self.mode = "LAB" self.mode = "LAB"
elif mo == 3: elif mo == 3:
self.mode = "RGB" self.mode = "RGB"
elif mo == 4:
self.mode = "CMYK"
else: else:
break pass
if id[:1] == id[-1:] == '"': bbox = (0, 0, x, y)
id = id[1:-1] if self.bbox:
bbox = self.bbox
# Scan forward to the actual image data self.tile = [("eps",
while True: (0, 0, x, y),
s = fp.readline() 0,
if not s: (length, bbox))]
break xdpi = round(x / (self.size[0] / 72.0))
if s[:len(id)] == id: ydpi = round(y / (self.size[1] / 72.0))
self.size = x, y self.info["dpi"] = (xdpi,ydpi)
self.tile2 = [(decoder, return
(0, 0, x, y),
fp.tell(),
0)]
return
s = fp.readline() s = fp.readline()
s = s.rstrip("\r\n")
s = makeunicode(s)
if not s: if not s:
break break
if not box: if not box:
raise IOError("cannot determine EPS bounding box") raise IOError("cannot determine EPS bounding box")
def load(self, scale=1): if not self.pixelsize:
self.info["dpi"] = (defaultDPI, defaultDPI)
def load(self, scale=None):
# Load EPS via Ghostscript # Load EPS via Ghostscript
if not self.tile: if not self.tile:
return return
self.im = Ghostscript(self.tile, self.size, self.fp, scale)
size = self.size
if self.pixelsize:
# pixel based eps
# size is imagesize in pixels
size = self.pixelsize
else:
# generic eps
# size is in points (== 72dpi uglyness)
if not scale:
scale = (defaultDPI/72.0)
size = ( size[0] * scale, size[1] * scale)
self.im = Ghostscript(self.tile, size, self.fp)
self.mode = self.im.mode self.mode = self.im.mode
self.size = self.im.size self.size = self.im.size
self.tile = [] self.tile = []

View File

@ -27,13 +27,13 @@ class TestFileEps(PillowTestCase):
def test_sanity(self): def test_sanity(self):
# Regular scale # Regular scale
image1 = Image.open(file1) image1 = Image.open(file1)
image1.load() image1.load(scale=1.0)
self.assertEqual(image1.mode, "RGB") self.assertEqual(image1.mode, "RGB")
self.assertEqual(image1.size, (460, 352)) self.assertEqual(image1.size, (460, 352))
self.assertEqual(image1.format, "EPS") self.assertEqual(image1.format, "EPS")
image2 = Image.open(file2) image2 = Image.open(file2)
image2.load() image2.load(scale=1.0)
self.assertEqual(image2.mode, "RGB") self.assertEqual(image2.mode, "RGB")
self.assertEqual(image2.size, (360, 252)) self.assertEqual(image2.size, (360, 252))
self.assertEqual(image2.format, "EPS") self.assertEqual(image2.format, "EPS")
@ -71,14 +71,14 @@ class TestFileEps(PillowTestCase):
# Zero bounding box # Zero bounding box
image1_scale1 = Image.open(file1) image1_scale1 = Image.open(file1)
image1_scale1.load() image1_scale1.load(scale=1.0)
image1_scale1_compare = Image.open(file1_compare).convert("RGB") image1_scale1_compare = Image.open(file1_compare).convert("RGB")
image1_scale1_compare.load() image1_scale1_compare.load()
self.assert_image_similar(image1_scale1, image1_scale1_compare, 5) self.assert_image_similar(image1_scale1, image1_scale1_compare, 5)
# Non-Zero bounding box # Non-Zero bounding box
image2_scale1 = Image.open(file2) image2_scale1 = Image.open(file2)
image2_scale1.load() image2_scale1.load(scale=1.0)
image2_scale1_compare = Image.open(file2_compare).convert("RGB") image2_scale1_compare = Image.open(file2_compare).convert("RGB")
image2_scale1_compare.load() image2_scale1_compare.load()
self.assert_image_similar(image2_scale1, image2_scale1_compare, 10) self.assert_image_similar(image2_scale1, image2_scale1_compare, 10)