diff --git a/.travis.yml b/.travis.yml index bafcd7d14..1eecc9c13 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ python: - 3.2 - 3.3 -install: "sudo apt-get -qq install libfreetype6-dev liblcms2-dev libwebp-dev python-qt4" +install: "sudo apt-get -qq install libfreetype6-dev liblcms2-dev libwebp-dev python-qt4 ghostscript"" script: - python setup.py clean diff --git a/CHANGES.rst b/CHANGES.rst index 9a5006c03..f6a166922 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ Changelog (Pillow) 2.3.0 (2014-01-01) ------------------ +- Adds directories for NetBSD. + [deepy] + - Support RGBA TIFF with missing ExtraSamples tag [cgohlke] diff --git a/PIL/EpsImagePlugin.py b/PIL/EpsImagePlugin.py index bc0ed43c5..a8706b05f 100644 --- a/PIL/EpsImagePlugin.py +++ b/PIL/EpsImagePlugin.py @@ -50,14 +50,22 @@ if sys.platform.startswith('win'): else: gs_windows_binary = False -def Ghostscript(tile, size, fp): +def Ghostscript(tile, size, fp, scale=1): """Render an image using Ghostscript""" # Unpack decoder tile decoder, tile, offset, data = tile[0] length, bbox = data - import tempfile, os + #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, subprocess file = tempfile.mktemp() @@ -65,33 +73,32 @@ def Ghostscript(tile, size, fp): 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") + gs = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE) # adjust for image origin if bbox[0] != 0 or bbox[1] != 0: - gs.write("%d %d translate\n" % (-bbox[0], -bbox[1])) + gs.stdin.write(("%d %d translate\n" % (-bbox[0], -bbox[1])).encode('ascii')) fp.seek(offset) while length > 0: s = fp.read(8192) if not s: break length = length - len(s) - gs.write(s) - status = gs.close() + gs.stdin.write(s) + gs.stdin.close() + status = gs.wait() if status: raise IOError("gs failed (status %d)" % status) im = Image.core.open_ppm(file) @@ -304,11 +311,11 @@ class EpsImageFile(ImageFile.ImageFile): if not box: raise IOError("cannot determine EPS bounding box") - def load(self): + def load(self, scale=1): # Load EPS via Ghostscript if not self.tile: return - self.im = Ghostscript(self.tile, self.size, self.fp) + self.im = Ghostscript(self.tile, self.size, self.fp, scale) self.mode = self.im.mode self.size = self.im.size self.tile = [] diff --git a/PIL/Image.py b/PIL/Image.py index e1d88fe59..ec5ff548d 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -475,7 +475,7 @@ class Image: new.mode = im.mode new.size = im.size new.palette = self.palette - if im.mode == "P": + if im.mode == "P" and not new.palette: from PIL import ImagePalette new.palette = ImagePalette.ImagePalette() try: diff --git a/Tests/images/create_eps.gnuplot b/Tests/images/create_eps.gnuplot new file mode 100644 index 000000000..4d7e29877 --- /dev/null +++ b/Tests/images/create_eps.gnuplot @@ -0,0 +1,30 @@ +#!/usr/bin/gnuplot + +#This is the script that was used to create our sample EPS files +#We used the following version of the gnuplot program +#G N U P L O T +#Version 4.6 patchlevel 3 last modified 2013-04-12 +#Build System: Darwin x86_64 + +#This file will generate the non_zero_bb.eps variant, in order to get the +#zero_bb.eps variant you will need to edit line6 in the result file to +#be "%%BoundingBox: 0 0 460 352" instead of "%%BoundingBox: 50 50 410 302" + +set t postscript eps color +set o "sample.eps" +set dummy u,v +set key bmargin center horizontal Right noreverse enhanced autotitles nobox +set parametric +set view 50, 30, 1, 1 +set isosamples 10, 10 +set hidden3d back offset 1 trianglepattern 3 undefined 1 altdiagonal bentover +set ticslevel 0 +set title "Interlocking Tori" + +set style line 1 lt 1 lw 1 pt 3 lc rgb "red" +set style line 2 lt 1 lw 1 pt 3 lc rgb "blue" + +set urange [ -3.14159 : 3.14159 ] noreverse nowriteback +set vrange [ -3.14159 : 3.14159 ] noreverse nowriteback +splot cos(u)+.5*cos(u)*cos(v),sin(u)+.5*sin(u)*cos(v),.5*sin(v) ls 1,\ + 1+cos(u)+.5*cos(u)*cos(v),.5*sin(v),sin(u)+.5*sin(u)*cos(v) ls 2 diff --git a/Tests/images/non_zero_bb.eps b/Tests/images/non_zero_bb.eps new file mode 100644 index 000000000..750a44b38 Binary files /dev/null and b/Tests/images/non_zero_bb.eps differ diff --git a/Tests/images/non_zero_bb.png b/Tests/images/non_zero_bb.png new file mode 100644 index 000000000..156c9a091 Binary files /dev/null and b/Tests/images/non_zero_bb.png differ diff --git a/Tests/images/non_zero_bb_scale2.png b/Tests/images/non_zero_bb_scale2.png new file mode 100644 index 000000000..2600580b3 Binary files /dev/null and b/Tests/images/non_zero_bb_scale2.png differ diff --git a/Tests/images/zero_bb.eps b/Tests/images/zero_bb.eps new file mode 100644 index 000000000..e931bf833 Binary files /dev/null and b/Tests/images/zero_bb.eps differ diff --git a/Tests/images/zero_bb.png b/Tests/images/zero_bb.png new file mode 100644 index 000000000..7d02a5814 Binary files /dev/null and b/Tests/images/zero_bb.png differ diff --git a/Tests/images/zero_bb_scale2.png b/Tests/images/zero_bb_scale2.png new file mode 100644 index 000000000..81c9d056d Binary files /dev/null and b/Tests/images/zero_bb_scale2.png differ diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py new file mode 100644 index 000000000..75570f944 --- /dev/null +++ b/Tests/test_file_eps.py @@ -0,0 +1,82 @@ +from tester import * + +from PIL import Image + +#Our two EPS test files (they are identical except for their bounding boxes) +file1 = "Tests/images/zero_bb.eps" +file2 = "Tests/images/non_zero_bb.eps" + +#Due to palletization, we'll need to convert these to RGB after load +file1_compare = "Tests/images/zero_bb.png" +file1_compare_scale2 = "Tests/images/zero_bb_scale2.png" + +file2_compare = "Tests/images/non_zero_bb.png" +file2_compare_scale2 = "Tests/images/non_zero_bb_scale2.png" + +def test_sanity(): + #Regular scale + image1 = Image.open(file1) + image1.load() + assert_equal(image1.mode, "RGB") + assert_equal(image1.size, (460, 352)) + assert_equal(image1.format, "EPS") + + image2 = Image.open(file2) + image2.load() + assert_equal(image2.mode, "RGB") + assert_equal(image2.size, (360, 252)) + assert_equal(image2.format, "EPS") + + #Double scale + image1_scale2 = Image.open(file1) + image1_scale2.load(scale=2) + assert_equal(image1_scale2.mode, "RGB") + assert_equal(image1_scale2.size, (920, 704)) + assert_equal(image1_scale2.format, "EPS") + + image2_scale2 = Image.open(file2) + image2_scale2.load(scale=2) + assert_equal(image2_scale2.mode, "RGB") + assert_equal(image2_scale2.size, (720, 504)) + assert_equal(image2_scale2.format, "EPS") + +def test_render_scale1(): + #We need png support for these render test + codecs = dir(Image.core) + if "zip_encoder" not in codecs or "zip_decoder" not in codecs: + skip("zip/deflate support not available") + + #Zero bounding box + image1_scale1 = Image.open(file1) + image1_scale1.load() + image1_scale1_compare = Image.open(file1_compare).convert("RGB") + image1_scale1_compare.load() + assert_image_similar(image1_scale1, image1_scale1_compare, 5) + + #Non-Zero bounding box + image2_scale1 = Image.open(file2) + image2_scale1.load() + image2_scale1_compare = Image.open(file2_compare).convert("RGB") + image2_scale1_compare.load() + assert_image_similar(image2_scale1, image2_scale1_compare, 10) + +def test_render_scale2(): + #We need png support for these render test + codecs = dir(Image.core) + if "zip_encoder" not in codecs or "zip_decoder" not in codecs: + skip("zip/deflate support not available") + + #Zero bounding box + image1_scale2 = Image.open(file1) + image1_scale2.load(scale=2) + image1_scale2_compare = Image.open(file1_compare_scale2).convert("RGB") + image1_scale2_compare.load() + assert_image_similar(image1_scale2, image1_scale2_compare, 5) + + #Non-Zero bounding box + image2_scale2 = Image.open(file2) + image2_scale2.load(scale=2) + image2_scale2_compare = Image.open(file2_compare_scale2).convert("RGB") + image2_scale2_compare.load() + assert_image_similar(image2_scale2, image2_scale2_compare, 10) + diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 0c27865cd..3a6478e2a 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -27,3 +27,22 @@ def test_optimize(): return len(file.getvalue()) assert_equal(test(0), 800) assert_equal(test(1), 38) + +def test_roundtrip(): + out = tempfile('temp.gif') + im = lena() + im.save(out) + reread = Image.open(out) + + assert_image_similar(reread.convert('RGB'), im, 50) + +def test_roundtrip2(): + #see https://github.com/python-imaging/Pillow/issues/403 + out = 'temp.gif'#tempfile('temp.gif') + im = Image.open('Images/lena.gif') + im2 = im.copy() + im2.save(out) + reread = Image.open(out) + + assert_image_similar(reread.convert('RGB'), lena(), 50) + diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 93edebf4e..d0595711e 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -36,6 +36,20 @@ PIL identifies EPS files containing image data, and can read files that contain embedded raster images (ImageData descriptors). If Ghostscript is available, other EPS files can be read as well. The EPS driver can also write EPS images. +If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load` +method with the following parameter to affect how Ghostscript renders the EPS + +**scale** + Affects the scale of the resultant rasterized image. If the EPS suggests + that the image be rendered at 100px x 100px, setting this parameter to + 2 will make the Ghostscript render a 200px x 200px image instead. The + relative position of the bounding box is maintained:: + + im = Image.open(...) + im.size #(100,100) + im.load(scale=2) + im.size #(200,200) + GIF ^^^ @@ -273,19 +287,19 @@ format are currently undocumented. The :py:meth:`~PIL.Image.Image.save` method supports the following options: -**lossless** +**lossless** If present, instructs the WEBP writer to use lossless compression. -**quality** +**quality** Integer, 1-100, Defaults to 80. Sets the quality level for lossy compression. -**icc_procfile** +**icc_procfile** The ICC Profile to include in the saved file. Only supported if the system webp library was built with webpmux support. -**exif** +**exif** The exif data to include in the saved file. Only supported if the system webp library was built with webpmux support. diff --git a/setup.py b/setup.py index 6c677bbd3..2bc5ca3f6 100644 --- a/setup.py +++ b/setup.py @@ -211,6 +211,10 @@ class pil_build_ext(build_ext): # work ;-) self.add_multiarch_paths() + elif sys.platform.startswith("netbsd"): + _add_directory(library_dirs, "/usr/pkg/lib") + _add_directory(include_dirs, "/usr/pkg/include") + _add_directory(library_dirs, "/usr/local/lib") # FIXME: check /opt/stuff directories here?