diff --git a/.travis.yml b/.travis.yml index 88118b1b2..fdc920ac2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: - 3.2 - 3.3 -install: "sudo apt-get -qq install libfreetype6-dev liblcms2-dev libwebp-dev" +install: "sudo apt-get -qq install libfreetype6-dev liblcms2-dev libwebp-dev ghostscript" script: - python setup.py clean diff --git a/CHANGES.rst b/CHANGES.rst index 9a5006c03..86ed026cd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,30 @@ Changelog (Pillow) 2.3.0 (2014-01-01) ------------------ +- 2gigapix image fixes + [wiredfool] + +- Save arbitrary tags in Tiff image files + [wiredfool] + +- Quote filenames and title before using on command line + [tmccombs] + +- Fixed Viewer.show to return properly + [tmccombs] + +- Documentation fixes + [wiredfool] + +- Fixed memory leak saving images as webp when webpmux is available + [cezarsa] + +- Fix compiling with FreeType 2.5.1 + [stromnov] + +- Adds directories for NetBSD. + [deepy] + - Support RGBA TIFF with missing ExtraSamples tag [cgohlke] diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst deleted file mode 100644 index 56efba356..000000000 --- a/CONTRIBUTORS.rst +++ /dev/null @@ -1,36 +0,0 @@ -Contributors (Pillow) -===================== - -.. Note:: Contributors: please add your name here - -- Alex Po -- Anton Vlasenko -- Brian J. Crowell -- Bryant Mairs -- Christoph Gohlke -- Corey Richardson -- Daniel Hahler -- David Schmidt -- Eliot -- etienne -- Jannis Leidel -- Kyle MacFarlane -- Lars Yencken -- Liu Qishuai -- Manuel Ebert -- Marc Abramowitz -- Matti Picus -- Mikhail Korobov -- OCHIAI, Gouji -- Oliver Tonnhofer -- Phil Elson -- Sandro Mani -- Simon Law -- Stéphane Klein -- Steve Johnson -- Takeshi KOMIYA -- Tom Gross -- Tom Payne -- Tyler Garner -- tdesvenain -- wiredfool diff --git a/MANIFEST.in b/MANIFEST.in index bfad0f9ad..3769b645e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -27,6 +27,8 @@ recursive-include Sane README recursive-include Scripts *.py recursive-include Scripts README recursive-include Tests *.bin +recursive-include Tests *.eps +recursive-include Tests *.gnuplot recursive-include Tests *.icm recursive-include Tests *.jpg recursive-include Tests *.pcf @@ -42,8 +44,11 @@ recursive-include Tk *.c recursive-include Tk *.txt recursive-include docs *.bat recursive-include docs *.gitignore +recursive-include docs *.html recursive-include docs *.py recursive-include docs *.rst +recursive-include docs *.txt +recursive-include docs Guardfile recursive-include docs Makefile recursive-include docs BUILDME recursive-include docs COPYING 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 ec5ff548d..cf5c4f477 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -675,15 +675,18 @@ class Image: L = R * 299/1000 + G * 587/1000 + B * 114/1000 - When translating a greyscale image into a bilevel image (mode - "1"), all non-zero values are set to 255 (white). To use other - thresholds, use the :py:meth:`~PIL.Image.Image.point` method. + The default method of converting a greyscale ("L") or "RGB" + image into a bilevel (mode "1") image uses Floyd-Steinberg + dither to approximate the original image luminosity levels. If + dither is NONE, all non-zero values are set to 255 (white). To + use other thresholds, use the :py:meth:`~PIL.Image.Image.point` + method. :param mode: The requested mode. :param matrix: An optional conversion matrix. If given, this should be 4- or 16-tuple containing floating point values. :param dither: Dithering method, used when converting from - mode "RGB" to "P". + mode "RGB" to "P" or from "RGB" or "L" to "1". Available methods are NONE or FLOYDSTEINBERG (default). :param palette: Palette to use when converting from mode "RGB" to "P". Available palettes are WEB or ADAPTIVE. diff --git a/PIL/ImageShow.py b/PIL/ImageShow.py index 7e3d63ba3..6ed913c8d 100644 --- a/PIL/ImageShow.py +++ b/PIL/ImageShow.py @@ -17,6 +17,11 @@ from __future__ import print_function from PIL import Image import os, sys +if(sys.version_info >= (3, 3)): + from shlex import quote +else: + from pipes import quote + _viewers = [] def register(viewer, order=1): @@ -65,7 +70,7 @@ class Viewer: if base != image.mode and image.mode != "1": image = image.convert(base) - self.show_image(image, **options) + return self.show_image(image, **options) # hook methods @@ -99,7 +104,7 @@ if sys.platform == "win32": format = "BMP" def get_command(self, file, **options): return ("start /wait %s && ping -n 2 127.0.0.1 >NUL " - "&& del /f %s" % (file, file)) + "&& del /f %s" % (quote(file), quote(file))) register(WindowsViewer) @@ -111,7 +116,7 @@ elif sys.platform == "darwin": # on darwin open returns immediately resulting in the temp # file removal while app is opening command = "open -a /Applications/Preview.app" - command = "(%s %s; sleep 20; rm -f %s)&" % (command, file, file) + command = "(%s %s; sleep 20; rm -f %s)&" % (command, quote(file), quote(file)) return command register(MacViewer) @@ -134,7 +139,7 @@ else: class UnixViewer(Viewer): def show_file(self, file, **options): command, executable = self.get_command_ex(file, **options) - command = "(%s %s; rm -f %s)&" % (command, file, file) + command = "(%s %s; rm -f %s)&" % (command, quote(file), quote(file)) os.system(command) return 1 @@ -154,8 +159,7 @@ else: # imagemagick's display command instead. command = executable = "xv" if title: - # FIXME: do full escaping - command = command + " -name \"%s\"" % title + command = command + " -name %s" % quote(title) return command, executable if which("xv"): diff --git a/PIL/IptcImagePlugin.py b/PIL/IptcImagePlugin.py index 02e3360ff..157b73509 100644 --- a/PIL/IptcImagePlugin.py +++ b/PIL/IptcImagePlugin.py @@ -262,7 +262,7 @@ def getiptcinfo(im): # get raw data from the IPTC/NAA tag (PhotoShop tags the data # as 4-byte integers, so we cannot use the get method...) try: - type, data = im.tag.tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] + data = im.tag.tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] except (AttributeError, KeyError): pass diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 0cc03f833..137da3288 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -220,11 +220,45 @@ def _accept(prefix): # Wrapper for TIFF IFDs. class ImageFileDirectory(collections.MutableMapping): + """ This class represents a TIFF tag directory. To speed things + up, we don't decode tags unless they're asked for. - # represents a TIFF tag directory. to speed things up, - # we don't decode tags unless they're asked for. + Exposes a dictionary interface of the tags in the directory + ImageFileDirectory[key] = value + value = ImageFileDirectory[key] - def __init__(self, prefix): + Also contains a dictionary of tag types as read from the tiff + image file, 'ImageFileDirectory.tagtype' + + + Data Structures: + 'public' + * self.tagtype = {} Key: numerical tiff tag number + Value: integer corresponding to the data type from + `TiffTags.TYPES` + + 'internal' + * self.tags = {} Key: numerical tiff tag number + Value: Decoded data, Generally a tuple. + * If set from __setval__ -- always a tuple + * Numeric types -- always a tuple + * String type -- not a tuple, returned as string + * Undefined data -- not a tuple, returned as bytes + * Byte -- not a tuple, returned as byte. + * self.tagdata = {} Key: numerical tiff tag number + Value: undecoded byte string from file + + + Tags will be found in either self.tags or self.tagdata, but + not both. The union of the two should contain all the tags + from the Tiff image file. External classes shouldn't + reference these unless they're really sure what they're doing. + """ + + def __init__(self, prefix=II): + """ + :prefix: 'II'|'MM' tiff endianness + """ self.prefix = prefix[:2] if self.prefix == MM: self.i16, self.i32 = ib16, ib32 @@ -270,7 +304,8 @@ class ImageFileDirectory(collections.MutableMapping): try: return self.tags[tag] except KeyError: - type, data = self.tagdata[tag] # unpack on the fly + data = self.tagdata[tag] # unpack on the fly + type = self.tagtype[tag] size, handler = self.load_dispatch[type] self.tags[tag] = data = handler(self, data) del self.tagdata[tag] @@ -299,6 +334,9 @@ class ImageFileDirectory(collections.MutableMapping): return tag in self def __setitem__(self, tag, value): + # tags are tuples for integers + # tags are not tuples for byte, string, and undefined data. + # see load_* if not isinstance(value, tuple): value = (value,) self.tags[tag] = value @@ -413,7 +451,7 @@ class ImageFileDirectory(collections.MutableMapping): warnings.warn("Possibly corrupt EXIF data. Expecting to read %d bytes but only got %d. Skipping tag %s" % (size, len(data), tag)) continue - self.tagdata[tag] = typ, data + self.tagdata[tag] = data self.tagtype[tag] = typ if Image.DEBUG: @@ -450,25 +488,42 @@ class ImageFileDirectory(collections.MutableMapping): if tag in self.tagtype: typ = self.tagtype[tag] - + + if Image.DEBUG: + print ("Tag %s, Type: %s, Value: %s" % (tag, typ, value)) + if typ == 1: # byte data - data = value + if isinstance(value, tuple): + data = value = value[-1] + else: + data = value elif typ == 7: # untyped data data = value = b"".join(value) - elif isinstance(value[0], str): + elif isStringType(value[0]): # string data + if isinstance(value, tuple): + value = value[-1] typ = 2 - data = value = b"\0".join(value.encode('ascii', 'replace')) + b"\0" + # was b'\0'.join(str), which led to \x00a\x00b sorts + # of strings which I don't see in in the wild tiffs + # and doesn't match the tiff spec: 8-bit byte that + # contains a 7-bit ASCII code; the last byte must be + # NUL (binary zero). Also, I don't think this was well + # excersized before. + data = value = b"" + value.encode('ascii', 'replace') + b"\0" else: # integer data if tag == STRIPOFFSETS: stripoffsets = len(directory) typ = 4 # to avoid catch-22 - elif tag in (X_RESOLUTION, Y_RESOLUTION): + elif tag in (X_RESOLUTION, Y_RESOLUTION) or typ==5: # identify rational data fields typ = 5 + if isinstance(value[0], tuple): + # long name for flatten + value = tuple(itertools.chain.from_iterable(value)) elif not typ: typ = 3 for v in value: @@ -500,6 +555,7 @@ class ImageFileDirectory(collections.MutableMapping): count = len(value) if typ == 5: count = count // 2 # adjust for rational data field + append((tag, typ, count, o32(offset), data)) offset = offset + len(data) if offset & 1: @@ -937,23 +993,34 @@ def _save(im, fp, filename): ifd[IMAGEWIDTH] = im.size[0] ifd[IMAGELENGTH] = im.size[1] + # write any arbitrary tags passed in as an ImageFileDirectory + info = im.encoderinfo.get("tiffinfo",{}) + if Image.DEBUG: + print ("Tiffinfo Keys: %s"% info.keys) + keys = list(info.keys()) + for key in keys: + ifd[key] = info.get(key) + try: + ifd.tagtype[key] = info.tagtype[key] + except: + pass # might not be an IFD, Might not have populated type + + # additions written by Greg Couch, gregc@cgl.ucsf.edu # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com if hasattr(im, 'tag'): # preserve tags from original TIFF image file - for key in (RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION): - if key in im.tag.tagdata: - ifd[key] = im.tag.tagdata.get(key) - # preserve some more tags from original TIFF image file - # -- 2008-06-06 Florian Hoech - ifd.tagtype = im.tag.tagtype - for key in (IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP): + for key in (RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION, + IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP): if key in im.tag: ifd[key] = im.tag[key] + ifd.tagtype[key] = im.tag.tagtype.get(key, None) + # preserve ICC profile (should also work when saving other formats # which support profiles as TIFF) -- 2008-06-06 Florian Hoech if "icc_profile" in im.info: ifd[ICCPROFILE] = im.info["icc_profile"] + if "description" in im.encoderinfo: ifd[IMAGEDESCRIPTION] = im.encoderinfo["description"] if "resolution" in im.encoderinfo: diff --git a/PIL/__init__.py b/PIL/__init__.py index 6ebe41ff6..18bd42a5f 100644 --- a/PIL/__init__.py +++ b/PIL/__init__.py @@ -12,7 +12,7 @@ # ;-) VERSION = '1.1.7' # PIL version -PILLOW_VERSION = '2.2.1' # Pillow +PILLOW_VERSION = '2.3.0' # Pillow _plugins = ['ArgImagePlugin', 'BmpImagePlugin', 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/lena.tif b/Tests/images/lena.tif new file mode 100644 index 000000000..fead980d4 Binary files /dev/null and b/Tests/images/lena.tif differ 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/large_memory_test.py b/Tests/large_memory_test.py new file mode 100644 index 000000000..148841ec2 --- /dev/null +++ b/Tests/large_memory_test.py @@ -0,0 +1,27 @@ +from tester import * + +# This test is not run automatically. +# +# It requires > 2gb memory for the >2 gigapixel image generated in the +# second test. Running this automatically would amount to a denial of +# service on our testing infrastructure. I expect this test to fail +# on any 32 bit machine, as well as any smallish things (like +# raspberrypis). It does succeed on a 3gb Ubuntu 12.04x64 VM on python +# 2.7 an 3.2 + +from PIL import Image +ydim = 32769 +xdim = 48000 +f = tempfile('temp.png') + +def _write_png(xdim,ydim): + im = Image.new('L',(xdim,ydim),(0)) + im.save(f) + success() + +def test_large(): + """ succeeded prepatch""" + _write_png(xdim,ydim) +def test_2gpx(): + """failed prepatch""" + _write_png(xdim,xdim) 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_libtiff.py b/Tests/test_file_libtiff.py index 476010c36..102819982 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -107,6 +107,29 @@ def test_adobe_deflate_tiff(): assert_equal(im.tile[0][:3], ('tiff_adobe_deflate', (0, 0, 278, 374), 0)) assert_no_exception(lambda: im.load()) +def test_write_metadata(): + """ Test metadata writing through libtiff """ + img = Image.open('Tests/images/lena_g4.tif') + f = tempfile('temp.tiff') + + img.save(f, tiffinfo = img.tag) + + loaded = Image.open(f) + + original = img.tag.named() + reloaded = loaded.tag.named() + + # PhotometricInterpretation is set from SAVE_INFO, not the original image. + ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber', 'PhotometricInterpretation'] + + for tag, value in reloaded.items(): + if tag not in ignored: + assert_equal(original[tag], value, "%s didn't roundtrip" % tag) + + for tag, value in original.items(): + if tag not in ignored: + assert_equal(value, reloaded[tag], "%s didn't roundtrip" % tag) + def test_g3_compression(): i = Image.open('Tests/images/lena_g4_500.tif') @@ -116,7 +139,7 @@ def test_g3_compression(): reread = Image.open(out) assert_equal(reread.info['compression'], 'group3') assert_image_equal(reread, i) - + def test_little_endian(): im = Image.open('Tests/images/16bit.deflate.tif') assert_equal(im.getpixel((0,0)), 480) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py new file mode 100644 index 000000000..354eb972f --- /dev/null +++ b/Tests/test_file_tiff_metadata.py @@ -0,0 +1,80 @@ +from tester import * +from PIL import Image, TiffImagePlugin, TiffTags + +tag_ids = dict(zip(TiffTags.TAGS.values(), TiffTags.TAGS.keys())) + +def test_rt_metadata(): + """ Test writing arbitray metadata into the tiff image directory + Use case is ImageJ private tags, one numeric, one arbitrary + data. https://github.com/python-imaging/Pillow/issues/291 + """ + + img = lena() + + textdata = "This is some arbitrary metadata for a text field" + info = TiffImagePlugin.ImageFileDirectory() + + info[tag_ids['ImageJMetaDataByteCounts']] = len(textdata) + info[tag_ids['ImageJMetaData']] = textdata + + f = tempfile("temp.tif") + + img.save(f, tiffinfo=info) + + loaded = Image.open(f) + + assert_equal(loaded.tag[50838], (len(textdata),)) + assert_equal(loaded.tag[50839], textdata) + +def test_read_metadata(): + img = Image.open('Tests/images/lena_g4.tif') + + known = {'YResolution': ((1207959552, 16777216),), + 'PlanarConfiguration': (1,), + 'BitsPerSample': (1,), + 'ImageLength': (128,), + 'Compression': (4,), + 'FillOrder': (1,), + 'DocumentName': 'lena.g4.tif', + 'RowsPerStrip': (128,), + 'ResolutionUnit': (1,), + 'PhotometricInterpretation': (0,), + 'PageNumber': (0, 1), + 'XResolution': ((1207959552, 16777216),), + 'ImageWidth': (128,), + 'Orientation': (1,), + 'StripByteCounts': (1796,), + 'SamplesPerPixel': (1,), + 'StripOffsets': (8,), + 'Software': 'ImageMagick 6.5.7-8 2012-08-17 Q16 http://www.imagemagick.org'} + + # assert_equal is equivalent, but less helpful in telling what's wrong. + named = img.tag.named() + for tag, value in named.items(): + assert_equal(known[tag], value) + + for tag, value in known.items(): + assert_equal(value, named[tag]) + + +def test_write_metadata(): + """ Test metadata writing through the python code """ + img = Image.open('Tests/images/lena.tif') + + f = tempfile('temp.tiff') + img.save(f, tiffinfo = img.tag) + + loaded = Image.open(f) + + original = img.tag.named() + reloaded = loaded.tag.named() + + ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber', 'StripOffsets'] + + for tag, value in reloaded.items(): + if tag not in ignored: + assert_equal(original[tag], value, "%s didn't roundtrip" % tag) + + for tag, value in original.items(): + if tag not in ignored: + assert_equal(value, reloaded[tag], "%s didn't roundtrip" % tag) diff --git a/_imaging.c b/_imaging.c index ecd67d63c..a0dc79253 100644 --- a/_imaging.c +++ b/_imaging.c @@ -71,7 +71,7 @@ * See the README file for information on usage and redistribution. */ -#define PILLOW_VERSION "2.2.1" +#define PILLOW_VERSION "2.3.0" #include "Python.h" @@ -1234,7 +1234,8 @@ static PyObject* _putdata(ImagingObject* self, PyObject* args) { Imaging image; - int n, i, x, y; + // i & n are # pixels, require py_ssize_t. x can be as large as n. y, just because. + Py_ssize_t n, i, x, y; PyObject* data; double scale = 1.0; @@ -1244,16 +1245,16 @@ _putdata(ImagingObject* self, PyObject* args) return NULL; if (!PySequence_Check(data)) { - PyErr_SetString(PyExc_TypeError, must_be_sequence); - return NULL; + PyErr_SetString(PyExc_TypeError, must_be_sequence); + return NULL; } image = self->image; n = PyObject_Length(data); - if (n > (int) (image->xsize * image->ysize)) { - PyErr_SetString(PyExc_TypeError, "too many data entries"); - return NULL; + if (n > (Py_ssize_t) (image->xsize * image->ysize)) { + PyErr_SetString(PyExc_TypeError, "too many data entries"); + return NULL; } if (image->image8) { @@ -1648,7 +1649,7 @@ _stretch(ImagingObject* self, PyObject* args) imIn = self->image; /* two-pass resize: minimize size of intermediate image */ - if (imIn->xsize * ysize < xsize * imIn->ysize) + if ((Py_ssize_t) imIn->xsize * ysize < (Py_ssize_t) xsize * imIn->ysize) imTemp = ImagingNew(imIn->mode, imIn->xsize, ysize); else imTemp = ImagingNew(imIn->mode, xsize, imIn->ysize); @@ -3073,7 +3074,7 @@ image_length(ImagingObject *self) { Imaging im = self->image; - return im->xsize * im->ysize; + return (Py_ssize_t) im->xsize * im->ysize; } static PyObject * diff --git a/_imagingft.c b/_imagingft.c index 47d50bdca..f19555be2 100644 --- a/_imagingft.c +++ b/_imagingft.c @@ -59,7 +59,11 @@ struct { const char* message; } ft_errors[] = +#if defined(USE_FREETYPE_2_1) +#include FT_ERRORS_H +#else #include +#endif /* -------------------------------------------------------------------- */ /* font objects */ diff --git a/_webp.c b/_webp.c index 2dab29fde..6381e1a56 100644 --- a/_webp.c +++ b/_webp.c @@ -113,11 +113,11 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) WebPMuxAssemble(mux, &output_data); WebPMuxDelete(mux); + free(output); - output = (uint8_t*)output_data.bytes; ret_size = output_data.size; if (ret_size > 0) { - PyObject *ret = PyBytes_FromStringAndSize((char*)output, ret_size); + PyObject *ret = PyBytes_FromStringAndSize((char*)output_data.bytes, ret_size); WebPDataClear(&output_data); return ret; } diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 93edebf4e..a326cb56d 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 ^^^ @@ -265,6 +279,57 @@ dictionary of decoded TIFF fields. Values are stored as either strings or tuples. Note that only short, long and ASCII tags are correctly unpacked by this release. +Saving Tiff Images +~~~~~~~~~~~~~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**tiffinfo** + A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory` object or dict + object containing tiff tags and values. The TIFF field type is + autodetected for Numeric and string values, any other types + require using an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory` + object and setting the type in + :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory.tagtype` with + the appropriate numerical value from + ``TiffTags.TYPES``. + + .. versionadded:: 2.3.0 + +**compression** + A string containing the desired compression method for the + file. (valid only with libtiff installed) Valid compression + methods are: ``[None, "tiff_ccitt", "group3", "group4", + "tiff_jpeg", "tiff_adobe_deflate", "tiff_thunderscan", + "tiff_deflate", "tiff_sgilog", "tiff_sgilog24", "tiff_raw_16"]`` + +These arguments to set the tiff header fields are an alternative to using the general tags available through tiffinfo. + +**description** + +**software** + +**date time** + +**artist** + +**copyright** + Strings + +**resolution unit** + A string of "inch", "centimeter" or "cm" + +**resolution** + +**x resolution** + +**y resolution** + +**dpi** + Either a Float, Integer, or 2 tuple of (numerator, + denominator). Resolution implies an equal x and y resolution, dpi + also implies a unit of inches. + WebP ^^^^ @@ -273,19 +338,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/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-file-decoder.rst index caf9dc07e..10833a53e 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-file-decoder.rst @@ -1,10 +1,13 @@ Writing your own file decoder ============================= -The Python Imaging Library uses a plug-in model which allows you to add your -own decoders to the library, without any changes to the library itself. Such -plug-ins have names like :file:`XxxImagePlugin.py`, where ``Xxx`` is a unique -format name (usually an abbreviation). +The Python Imaging Library uses a plug-in model which allows you to +add your own decoders to the library, without any changes to the +library itself. Such plug-ins usually have names like +:file:`XxxImagePlugin.py`, where ``Xxx`` is a unique format name +(usually an abbreviation). + +.. warning:: Pillow >= 2.1.0 no longer automatically imports any file in the Python path with a name ending in :file:`ImagePlugin.py`. You will need to import your decoder manually. A decoder plug-in should contain a decoder class, based on the :py:class:`PIL.ImageFile.ImageFile` base class. This class should provide an diff --git a/docs/installation.rst b/docs/installation.rst index b28c74b56..4c035a071 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -60,9 +60,13 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management + * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and + above uses liblcms2. Tested with **1.19** and **2.2**. + * **libwebp** provides the Webp format. - * Pillow has been tested with version **0.1.3**, which does not read transparent webp files. Version **0.3.0** supports transparency. + * Pillow has been tested with version **0.1.3**, which does not read + transparent webp files. Version **0.3.0** supports transparency. * **tcl/tk** provides support for tkinter bitmap and photo images. @@ -101,13 +105,13 @@ Or for Python 3:: Prerequisites are installed on **Ubuntu 10.04 LTS** with:: $ sudo apt-get install libtiff4-dev libjpeg62-dev zlib1g-dev \ - libfreetype6-dev liblcms1-dev tcl8.5-dev tk8.5-dev + libfreetype6-dev tcl8.5-dev tk8.5-dev Prerequisites are installed with on **Ubuntu 12.04 LTS** or **Raspian Wheezy 7.0** with:: $ sudo apt-get install libtiff4-dev libjpeg8-dev zlib1g-dev \ - libfreetype6-dev liblcms1-dev libwebp-dev tcl8.5-dev tk8.5-dev + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev Mac OS X installation --------------------- diff --git a/docs/porting-pil-to-pillow.rst b/docs/porting-pil-to-pillow.rst index 93bc672af..a58baac39 100644 --- a/docs/porting-pil-to-pillow.rst +++ b/docs/porting-pil-to-pillow.rst @@ -15,3 +15,9 @@ to this:: The :py:mod:`_imaging` module has been moved. You can now import it like this:: from PIL.Image import core as _imaging + +The image plugin loading mechanisim has changed. Pillow no longer +automatically imports any file in the Python path with a name ending +in :file:`ImagePlugin.py`. You will need to import your image plugin +manually. + diff --git a/setup.py b/setup.py index 6c677bbd3..5f3d22e56 100644 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ except ImportError: NAME = 'Pillow' -VERSION = '2.2.1' +VERSION = '2.3.0' TCL_ROOT = None JPEG_ROOT = None ZLIB_ROOT = None @@ -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? @@ -583,8 +587,7 @@ setup( description='Python Imaging Library (Fork)', long_description=( _read('README.rst') + b'\n' + - _read('CHANGES.rst') + b'\n' + - _read('CONTRIBUTORS.rst')).decode('utf-8'), + _read('CHANGES.rst')), author='Alex Clark (fork author)', author_email='aclark@aclark.net', url='http://python-imaging.github.io/',