From 85ecfe9dafcc28a8a773cff949b622cab5731c48 Mon Sep 17 00:00:00 2001 From: Karsten Wolf Date: Mon, 4 Aug 2014 14:17:05 +0200 Subject: [PATCH 1/7] Replaced PSFile wrapper with standart Python open for universal line endings. DPI for non pixelbased eps files is set to 600. DPI for pixelbased eps files is calculated from file and scale is ignored. --- PIL/EpsImagePlugin.py | 241 +++++++++++++++++++----------------------- 1 file changed, 110 insertions(+), 131 deletions(-) diff --git a/PIL/EpsImagePlugin.py b/PIL/EpsImagePlugin.py index 9f963f7e6..ab68b11ac 100644 --- a/PIL/EpsImagePlugin.py +++ b/PIL/EpsImagePlugin.py @@ -23,6 +23,8 @@ __version__ = "0.5" import re import io +import unicodedata + from PIL import Image, ImageFile, _binary # @@ -34,6 +36,8 @@ o32 = _binary.o32le split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") +defaultDPI = 600.0 + gs_windows_binary = None import sys if sys.platform.startswith('win'): @@ -51,6 +55,7 @@ if sys.platform.startswith('win'): else: gs_windows_binary = False +# UNUSED def has_ghostscript(): if gs_windows_binary: return True @@ -64,62 +69,47 @@ def has_ghostscript(): # no ghostscript pass return False - -def Ghostscript(tile, size, fp, scale=1): +def makeunicode( s, enc="latin-1", normalizer='NFD'): + 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""" + # size is either pts or pixels + # 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 dependend on bbox and size - 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) + + xpointsize = bbox[2] - bbox[0] + ypointsize = bbox[3] - bbox[1] + xdpi = size[0] / (xpointsize / 72.0) + ydpi = size[1] / (ypointsize / 72.0) import tempfile, os, subprocess out_fd, outfile = tempfile.mkstemp() os.close(out_fd) - in_fd, infile = tempfile.mkstemp() - os.close(in_fd) - - # ignore length and offset! - # ghostscript can read it - # copy whole file to read in ghostscript - with open(infile, 'wb') as f: - # fetch length of fp - fp.seek(0, 2) - 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 - length -= len(s) - f.write(s) # 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) + "-r%fc%f" % (xdpi,ydpi), # set input DPI (dots per inch) "-dNOPAUSE -dSAFER", # don't pause between pages, safe mode "-sDEVICE=ppmraw", # ppm driver "-sOutputFile=%s" % outfile, # output file "-c", "%d %d translate" % (-bbox[0], -bbox[1]), # adjust for image origin - "-f", infile, # input file + "-f", fp.name ] - + if gs_windows_binary is not None: if not gs_windows_binary: raise WindowsError('Unable to locate Ghostscript on paths') @@ -136,50 +126,11 @@ def Ghostscript(tile, size, fp, scale=1): finally: try: os.unlink(outfile) - os.unlink(infile) - except: pass - + 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 readbinary(self, count): - return self.fp.read(count) - def tell(self): - pos = self.fp.tell() - if self.char: - 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 @@ -194,55 +145,42 @@ class EpsImageFile(ImageFile.ImageFile): format_description = "Encapsulated Postscript" def _open(self): + fp = open(self.fp.name, "Ur") - fp = PSFile(self.fp) - - # FIX for: Some EPS file not handled correctly / issue #302 - # EPS can contain binary data - # or start directly with latin coding - # read header in both ways to handle both - # file types - # more info see http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf - - # for HEAD without binary preview - s = fp.read(4) - # for HEAD with binary preview - fp.seek(0) - sb = fp.readbinary(160) - + # HEAD + s = fp.read(512) if s[:4] == "%!PS": + offset = 0 fp.seek(0, 2) length = fp.tell() - offset = 0 - elif i32(sb[0:4]) == 0xC6D3D0C5: - offset = i32(sb[4:8]) - length = i32(sb[8:12]) + elif i32(s) == 0xC6D3D0C5: + offset = i32(s[4:]) + length = i32(s[8:]) + fp.seek(offset) else: raise SyntaxError("not an EPS file") - # go to offset - start of "%!PS" fp.seek(offset) - + box = None self.mode = "RGB" self.size = 1, 1 # FIXME: huh? + self.pixelsize = False + self.pointsize = False + self.bbox = False # # Load EPS header - s = fp.readline() - + s = s.rstrip("\r\n") + s = makeunicode(s) + 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: @@ -256,10 +194,31 @@ class EpsImageFile(ImageFile.ImageFile): # 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()] + + # 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.tile = [("eps", (0,0) + self.size, offset, + self.tile = [ ("eps", + (0,0) + self.size, + offset, (length, box))] + self.pointsize = pointsize except: pass @@ -284,14 +243,14 @@ class EpsImageFile(ImageFile.ImageFile): raise IOError("bad EPS header") s = fp.readline() + s = s.rstrip("\r\n") + s = makeunicode(s) if s[:1] != "%": break - # # Scan for an "ImageData" descriptor - while s[0] == "%": if len(s) > 255: @@ -303,12 +262,12 @@ class EpsImageFile(ImageFile.ImageFile): s = s[:-1] 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] =\ - s[11:].split(None, 7) - - x = int(x); y = int(y) - + x = int(x) + y = int(y) + self.pixelsize = (x,y) + bi = int(bi) mo = int(mo) @@ -319,46 +278,66 @@ class EpsImageFile(ImageFile.ImageFile): elif en == 2: decoder = "eps_hex" else: - break + pass + if bi != 8: break + if mo == 1: self.mode = "L" elif mo == 2: self.mode = "LAB" elif mo == 3: self.mode = "RGB" + elif mo == 4: + self.mode = "CMYK" else: - break + pass - if id[:1] == id[-1:] == '"': - id = id[1:-1] + bbox = (0, 0, x, y) + if self.bbox: + bbox = self.bbox - # 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 + self.tile = [("eps", + (0, 0, x, y), + 0, + (length, bbox))] + xdpi = round(x / (self.size[0] / 72.0)) + ydpi = round(y / (self.size[1] / 72.0)) + self.info["dpi"] = (xdpi,ydpi) + return s = fp.readline() + s = s.rstrip("\r\n") + s = makeunicode(s) if not s: break if not 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 if not self.tile: 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.size = self.im.size self.tile = [] From b682b7a34f57433c68d1b36cee3349fd10392e67 Mon Sep 17 00:00:00 2001 From: Karsten Wolf Date: Mon, 4 Aug 2014 14:17:39 +0200 Subject: [PATCH 2/7] Adapted test to work with 600 dpi default by setting scale=1.0 (==72dpi). --- Tests/test_file_eps.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 0ca4249a3..e0099b168 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -27,13 +27,13 @@ class TestFileEps(PillowTestCase): def test_sanity(self): # Regular scale image1 = Image.open(file1) - image1.load() + image1.load(scale=1.0) self.assertEqual(image1.mode, "RGB") self.assertEqual(image1.size, (460, 352)) self.assertEqual(image1.format, "EPS") image2 = Image.open(file2) - image2.load() + image2.load(scale=1.0) self.assertEqual(image2.mode, "RGB") self.assertEqual(image2.size, (360, 252)) self.assertEqual(image2.format, "EPS") @@ -71,14 +71,14 @@ class TestFileEps(PillowTestCase): # Zero bounding box 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.load() self.assert_image_similar(image1_scale1, image1_scale1_compare, 5) # Non-Zero bounding box 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.load() self.assert_image_similar(image2_scale1, image2_scale1_compare, 10) From f0ec4b7cb61b4d369c925818823fe3224a201d6c Mon Sep 17 00:00:00 2001 From: Karsten Wolf Date: Mon, 4 Aug 2014 14:37:18 +0200 Subject: [PATCH 3/7] Corrected unicode normalizer default to 'NFC' --- PIL/EpsImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PIL/EpsImagePlugin.py b/PIL/EpsImagePlugin.py index ab68b11ac..e1b09cafa 100644 --- a/PIL/EpsImagePlugin.py +++ b/PIL/EpsImagePlugin.py @@ -55,7 +55,6 @@ if sys.platform.startswith('win'): else: gs_windows_binary = False -# UNUSED def has_ghostscript(): if gs_windows_binary: return True @@ -70,7 +69,8 @@ def has_ghostscript(): pass return False -def makeunicode( s, enc="latin-1", normalizer='NFD'): +def makeunicode( s, enc="latin-1", normalizer='NFC'): + """return a normalized unicode string""" try: if type(s) != unicode: s = unicode(s, enc) From eee4f887293e0561539d12751ee19dabad17dc29 Mon Sep 17 00:00:00 2001 From: Karsten Wolf Date: Mon, 4 Aug 2014 15:32:57 +0200 Subject: [PATCH 4/7] Brought back some of the deleted code to make things stream safe. --- PIL/EpsImagePlugin.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/PIL/EpsImagePlugin.py b/PIL/EpsImagePlugin.py index e1b09cafa..27cfbb513 100644 --- a/PIL/EpsImagePlugin.py +++ b/PIL/EpsImagePlugin.py @@ -96,6 +96,27 @@ def Ghostscript(tile, size, fp): out_fd, outfile = tempfile.mkstemp() os.close(out_fd) + in_fd, infile = tempfile.mkstemp() + os.close(in_fd) + + # ignore length and offset! + # ghostscript can read it + # copy whole file to read in ghostscript + with open(infile, 'wb') as f: + # fetch length of fp + fp.seek(0, 2) + fsize = fp.tell() + # ensure start position + # go back + fp.seek(0) + lengthfile = fsize + while lengthfile > 0: + s = fp.read(min(lengthfile, 4*1024*1024)) + if not s: + break + length -= len(s) + f.write(s) + # Build ghostscript command command = ["gs", @@ -107,7 +128,7 @@ def Ghostscript(tile, size, fp): "-sOutputFile=%s" % outfile, # output file "-c", "%d %d translate" % (-bbox[0], -bbox[1]), # adjust for image origin - "-f", fp.name + "-f", infile #fp.name ] if gs_windows_binary is not None: @@ -145,7 +166,12 @@ class EpsImageFile(ImageFile.ImageFile): format_description = "Encapsulated Postscript" def _open(self): - fp = open(self.fp.name, "Ur") + try: + fp = open(self.fp.name, "Ur") + except Exception, err: + print err + fp = self.fp + fp.seek(0) # HEAD s = fp.read(512) From 482914f467cc3aa8e553442f91545543052aa22b Mon Sep 17 00:00:00 2001 From: Karsten Wolf Date: Mon, 4 Aug 2014 16:31:22 +0200 Subject: [PATCH 5/7] travis py2 debug exception error --- PIL/EpsImagePlugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PIL/EpsImagePlugin.py b/PIL/EpsImagePlugin.py index 27cfbb513..878bf9128 100644 --- a/PIL/EpsImagePlugin.py +++ b/PIL/EpsImagePlugin.py @@ -168,8 +168,7 @@ class EpsImageFile(ImageFile.ImageFile): def _open(self): try: fp = open(self.fp.name, "Ur") - except Exception, err: - print err + except: fp = self.fp fp.seek(0) From 8a8f57e568f95e1f1a0d1257b904541e0a073db7 Mon Sep 17 00:00:00 2001 From: Karsten Wolf Date: Tue, 5 Aug 2014 08:43:36 +0200 Subject: [PATCH 6/7] Reverted & redone without replacing PSFile. --- PIL/EpsImagePlugin.py | 84 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 17 deletions(-) mode change 100644 => 100755 PIL/EpsImagePlugin.py diff --git a/PIL/EpsImagePlugin.py b/PIL/EpsImagePlugin.py old mode 100644 new mode 100755 index 878bf9128..bd38d1c80 --- a/PIL/EpsImagePlugin.py +++ b/PIL/EpsImagePlugin.py @@ -27,6 +27,8 @@ import unicodedata from PIL import Image, ImageFile, _binary +import pdb + # # -------------------------------------------------------------------- @@ -117,18 +119,17 @@ def Ghostscript(tile, size, fp): length -= len(s) f.write(s) - # Build ghostscript command command = ["gs", "-q", # quiet mode "-g%dx%d" % size, # set output geometry (pixels) - "-r%fc%f" % (xdpi,ydpi), # 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 "-sDEVICE=ppmraw", # ppm driver "-sOutputFile=%s" % outfile, # output file "-c", "%d %d translate" % (-bbox[0], -bbox[1]), # adjust for image origin - "-f", infile #fp.name + "-f", infile, # input file ] if gs_windows_binary is not None: @@ -147,11 +148,50 @@ def Ghostscript(tile, size, fp): finally: try: os.unlink(outfile) - except: - pass - + os.unlink(infile) + 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 readbinary(self, count): + return self.fp.read(count) + def tell(self): + pos = self.fp.tell() + if self.char: + 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 @@ -166,25 +206,34 @@ class EpsImageFile(ImageFile.ImageFile): format_description = "Encapsulated Postscript" def _open(self): - try: - fp = open(self.fp.name, "Ur") - except: - fp = self.fp - fp.seek(0) - # HEAD - s = fp.read(512) + fp = PSFile(self.fp) + + # FIX for: Some EPS file not handled correctly / issue #302 + # EPS can contain binary data + # or start directly with latin coding + # read header in both ways to handle both + # file types + # more info see http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf + + # for HEAD without binary preview + s = fp.read(4) + # for HEAD with binary preview + fp.seek(0) + sb = fp.readbinary(160) + 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) + offset = 0 + elif i32(sb[0:4]) == 0xC6D3D0C5: + offset = i32(sb[4:8]) + length = i32(sb[8:12]) else: raise SyntaxError("not an EPS file") + # go to offset - start of "%!PS" fp.seek(offset) box = None @@ -276,6 +325,7 @@ class EpsImageFile(ImageFile.ImageFile): # # Scan for an "ImageData" descriptor + while s[0] == "%": if len(s) > 255: From 689fa83f8615a1608494587de9d8900098bcdcc8 Mon Sep 17 00:00:00 2001 From: Karsten Wolf Date: Tue, 5 Aug 2014 09:42:42 +0200 Subject: [PATCH 7/7] commentwork and minor corrections --- PIL/EpsImagePlugin.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/PIL/EpsImagePlugin.py b/PIL/EpsImagePlugin.py index bd38d1c80..00d948fe5 100755 --- a/PIL/EpsImagePlugin.py +++ b/PIL/EpsImagePlugin.py @@ -27,8 +27,6 @@ import unicodedata from PIL import Image, ImageFile, _binary -import pdb - # # -------------------------------------------------------------------- @@ -83,12 +81,12 @@ def makeunicode( s, enc="latin-1", normalizer='NFC'): def Ghostscript(tile, size, fp): """Render an image using Ghostscript""" - # size is either pts or pixels - # Unpack decoder tile decoder, tile, offset, data = tile[0] length, bbox = data + # bbox is in points == 1/72 inch + # size is in pixels, a device unit xpointsize = bbox[2] - bbox[0] ypointsize = bbox[3] - bbox[1] xdpi = size[0] / (xpointsize / 72.0) @@ -102,21 +100,20 @@ def Ghostscript(tile, size, fp): os.close(in_fd) # ignore length and offset! - # ghostscript can read it + # ghostscript can read it # copy whole file to read in ghostscript with open(infile, 'wb') as f: # fetch length of fp fp.seek(0, 2) - fsize = fp.tell() + lengthfile = fp.tell() # ensure start position # go back fp.seek(0) - lengthfile = fsize while lengthfile > 0: - s = fp.read(min(lengthfile, 4*1024*1024)) + s = fp.read( 4*1024*1024 ) if not s: break - length -= len(s) + lengthfile -= len(s) f.write(s) # Build ghostscript command @@ -222,11 +219,10 @@ class EpsImageFile(ImageFile.ImageFile): fp.seek(0) sb = fp.readbinary(160) + offset = 0 if s[:4] == "%!PS": - offset = 0 fp.seek(0, 2) length = fp.tell() - offset = 0 elif i32(sb[0:4]) == 0xC6D3D0C5: offset = i32(sb[4:8]) length = i32(sb[8:12])