diff --git a/.landscape.yaml b/.landscape.yaml index c869da5b4..ddd9cef32 100644 --- a/.landscape.yaml +++ b/.landscape.yaml @@ -1,2 +1,3 @@ strictness: medium test-warnings: yes +max-line-length: 80 diff --git a/.travis.yml b/.travis.yml index f94f8376a..2397376fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ install: - "travis_retry sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick" - "travis_retry pip install cffi" - "travis_retry pip install coverage nose" + - "travis_retry pip install check-manifest" # Pyroma tests sometimes hang on PyPy; skip for PyPy - if [ $TRAVIS_PYTHON_VERSION != "pypy" ]; then travis_retry pip install pyroma; fi @@ -38,6 +39,7 @@ script: - coverage run --append --include=PIL/* selftest.py - coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py + - check-manifest --ignore "depends/*" after_success: # gather the coverage data diff --git a/CHANGES.rst b/CHANGES.rst index a91e4be5c..c3f7ebad8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,39 @@ Changelog (Pillow) ================== -2.9.0 (Unreleased) +3.0.0 (Unreleased) ------------------ + +- Treat MPO with unknown header as base JPEG file #1350 + [hugovk, radarhere] + +- Added various tests #1330, #1344 + [radarhere] + +- More ImageFont tests #1327 + [hugovk] + +- Use logging instead of print #1207 + [anntzer] + +2.9.0 (2015-07-01) +------------------ + +- Added test for GimpPaletteFile #1324 + [radarhere] + +- Fixed ValueError in Python 2.6 #1315 #1316 + [cgohlke, radarhere] + +- Fixed tox test script path #1308 + [radarhere] + +- Added width and height properties #1304 + [radarhere] + +- Update tiff and tk tcl 8.5 versions #1303 + [radarhere, wiredfool] + - Add functions to convert: Image <-> QImage; Image <-> QPixmap #1217 [radarhere, rominf] diff --git a/MANIFEST.in b/MANIFEST.in index 30e73c075..70ca05b01 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,14 +1,16 @@ + include *.c include *.h +include *.in include *.md include *.py -include *.sh include *.rst +include *.sh include *.txt include *.yaml +include *.yml include .coveragerc include .gitattributes -include .travis.yml include LICENSE include Makefile include tox.ini @@ -16,7 +18,6 @@ recursive-include PIL *.md recursive-include Scripts *.py recursive-include Scripts *.rst recursive-include Scripts *.sh -recursive-include Scripts README.rst recursive-include Tests *.bdf recursive-include Tests *.bin recursive-include Tests *.bmp @@ -28,16 +29,19 @@ recursive-include Tests *.eps recursive-include Tests *.fli recursive-include Tests *.ggr recursive-include Tests *.gif +recursive-include Tests *.gpl recursive-include Tests *.gnuplot recursive-include Tests *.html recursive-include Tests *.icc recursive-include Tests *.icns recursive-include Tests *.ico +recursive-include Tests *.im recursive-include Tests *.j2k recursive-include Tests *.jp2 recursive-include Tests *.jpg recursive-include Tests *.lut recursive-include Tests *.mpo +recursive-include Tests *.msp recursive-include Tests *.pbm recursive-include Tests *.pcf recursive-include Tests *.pcx @@ -59,8 +63,8 @@ recursive-include Tests *.tiff recursive-include Tests *.ttf recursive-include Tests *.txt recursive-include Tests *.webp +recursive-include Tests *.xbm recursive-include Tests *.xpm -recursive-include Tests *.msp recursive-include Tk *.c recursive-include Tk *.rst recursive-include depends *.rst @@ -71,9 +75,13 @@ recursive-include docs *.html recursive-include docs *.py recursive-include docs *.rst recursive-include docs *.txt -recursive-include docs BUILDME -recursive-include docs COPYING -recursive-include docs Guardfile recursive-include docs Makefile +recursive-include docs Guardfile +recursive-include docs COPYING +recursive-include docs BUILDME recursive-include libImaging *.c recursive-include libImaging *.h +recursive-include winbuild *.gitignore +recursive-include winbuild *.md +recursive-include winbuild *.opt +recursive-include winbuild *.py diff --git a/Makefile b/Makefile index 4d96c497d..e123a74c7 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ # https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html .PHONY: clean coverage doc docserve help inplace install install-req release-test sdist test upload upload-test +.DEFAULT_GOAL := release-test clean: python setup.py clean diff --git a/PIL/BmpImagePlugin.py b/PIL/BmpImagePlugin.py index 793bd8edd..b109d2696 100644 --- a/PIL/BmpImagePlugin.py +++ b/PIL/BmpImagePlugin.py @@ -103,7 +103,9 @@ class BmpImageFile(ImageFile.ImageFile): file_info['pixels_per_meter'] = (i32(header_data[20:24]), i32(header_data[24:28])) file_info['colors'] = i32(header_data[28:32]) file_info['palette_padding'] = 4 - self.info["dpi"] = tuple(map(lambda x: math.ceil(x / 39.3701), file_info['pixels_per_meter'])) + self.info["dpi"] = tuple( + map(lambda x: int(math.ceil(x / 39.3701)), + file_info['pixels_per_meter'])) if file_info['compression'] == self.BITFIELDS: if len(header_data) >= 52: for idx, mask in enumerate(['r_mask', 'g_mask', 'b_mask', 'a_mask']): diff --git a/PIL/DcxImagePlugin.py b/PIL/DcxImagePlugin.py index b3f43b4cf..c6c9b8773 100644 --- a/PIL/DcxImagePlugin.py +++ b/PIL/DcxImagePlugin.py @@ -66,6 +66,10 @@ class DcxImageFile(PcxImageFile): def n_frames(self): return len(self._offset) + @property + def is_animated(self): + return len(self._offset) > 1 + def seek(self, frame): if frame >= len(self._offset): raise EOFError("attempt to seek outside DCX directory") diff --git a/PIL/FliImagePlugin.py b/PIL/FliImagePlugin.py index df6a4eb8e..e3eaff970 100644 --- a/PIL/FliImagePlugin.py +++ b/PIL/FliImagePlugin.py @@ -90,6 +90,7 @@ class FliImageFile(ImageFile.ImageFile): self.__fp = self.fp self.__rewind = self.fp.tell() self._n_frames = None + self._is_animated = None self.seek(0) def _palette(self, palette, shift): @@ -122,13 +123,33 @@ class FliImageFile(ImageFile.ImageFile): self.seek(current) return self._n_frames + @property + def is_animated(self): + if self._is_animated is None: + current = self.tell() + + try: + self.seek(1) + self._is_animated = True + except EOFError: + self._is_animated = False + + self.seek(current) + return self._is_animated + def seek(self, frame): if frame == self.__frame: return if frame < self.__frame: self._seek(0) + + last_frame = self.__frame for f in range(self.__frame + 1, frame + 1): - self._seek(f) + try: + self._seek(f) + except EOFError: + self.seek(last_frame) + raise EOFError("no more images in FLI file") def _seek(self, frame): if frame == 0: diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index bd974b651..08567fcd0 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -24,7 +24,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile, ImagePalette, _binary +from PIL import Image, ImageFile, ImagePalette, ImageChops, ImageSequence, _binary __version__ = "0.9" @@ -88,6 +88,7 @@ class GifImageFile(ImageFile.ImageFile): self.__fp = self.fp # FIXME: hack self.__rewind = self.fp.tell() self._n_frames = None + self._is_animated = None self._seek(0) # get ready to read first frame @property @@ -102,13 +103,33 @@ class GifImageFile(ImageFile.ImageFile): self.seek(current) return self._n_frames + @property + def is_animated(self): + if self._is_animated is None: + current = self.tell() + + try: + self.seek(1) + self._is_animated = True + except EOFError: + self._is_animated = False + + self.seek(current) + return self._is_animated + def seek(self, frame): if frame == self.__frame: return if frame < self.__frame: self._seek(0) + + last_frame = self.__frame for f in range(self.__frame + 1, frame + 1): - self._seek(f) + try: + self._seek(f) + except EOFError: + self.seek(last_frame) + raise EOFError("no more images in GIF file") def _seek(self, frame): @@ -241,7 +262,7 @@ class GifImageFile(ImageFile.ImageFile): if not self.tile: # self.__fp = None - raise EOFError("no more images in GIF file") + raise EOFError self.mode = "L" if self.palette: @@ -279,7 +300,22 @@ RAWMODE = { } -def _save(im, fp, filename): +def _convert_mode(im): + # convert on the fly (EXPERIMENTAL -- I'm not sure PIL + # should automatically convert images on save...) + if Image.getmodebase(im.mode) == "RGB": + palette_size = 256 + if im.palette: + palette_size = len(im.palette.getdata()[1]) // 3 + return im.convert("P", palette=1, colors=palette_size) + return im.convert("L") + + +def _save_all(im, fp, filename): + _save(im, fp, filename, save_all=True) + + +def _save(im, fp, filename, save_all=False): if _imaging_gif: # call external driver @@ -292,15 +328,7 @@ def _save(im, fp, filename): if im.mode in RAWMODE: im_out = im.copy() else: - # convert on the fly (EXPERIMENTAL -- I'm not sure PIL - # should automatically convert images on save...) - if Image.getmodebase(im.mode) == "RGB": - palette_size = 256 - if im.palette: - palette_size = len(im.palette.getdata()[1]) // 3 - im_out = im.convert("P", palette=1, colors=palette_size) - else: - im_out = im.convert("L") + im_out = _convert_mode(im) # header try: @@ -309,23 +337,49 @@ def _save(im, fp, filename): palette = None im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True) - header, used_palette_colors = getheader(im_out, palette, im.encoderinfo) - for s in header: - fp.write(s) + if save_all: + previous = None - flags = 0 + for im_frame in ImageSequence.Iterator(im): + im_frame = _convert_mode(im_frame) - if get_interlace(im): - flags = flags | 64 + # To specify duration, add the time in milliseconds to getdata(), + # e.g. getdata(im_frame, duration=1000) + if not previous: + # global header + for s in getheader(im_frame, palette, im.encoderinfo)[0] + getdata(im_frame): + fp.write(s) + else: + # delta frame + delta = ImageChops.subtract_modulo(im_frame, previous) + bbox = delta.getbbox() - # local image header - get_local_header(fp, im, (0, 0), flags) + if bbox: + # compress difference + for s in getdata(im_frame.crop(bbox), offset=bbox[:2]): + fp.write(s) + else: + # FIXME: what should we do in this case? + pass + previous = im_frame.copy() + else: + header = getheader(im_out, palette, im.encoderinfo)[0] + for s in header: + fp.write(s) - im_out.encoderconfig = (8, get_interlace(im)) - ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0, - RAWMODE[im_out.mode])]) + flags = 0 - fp.write(b"\0") # end of image data + if get_interlace(im): + flags = flags | 64 + + # local image header + _get_local_header(fp, im, (0, 0), flags) + + im_out.encoderconfig = (8, get_interlace(im)) + ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0, + RAWMODE[im_out.mode])]) + + fp.write(b"\0") # end of image data fp.write(b";") # end of file @@ -348,7 +402,7 @@ def get_interlace(im): return interlace -def get_local_header(fp, im, offset, flags): +def _get_local_header(fp, im, offset, flags): transparent_color_exists = False try: transparency = im.encoderinfo["transparency"] @@ -571,7 +625,7 @@ def getdata(im, offset=(0, 0), **params): im.encoderinfo = params # local image header - get_local_header(fp, im, offset, 0) + _get_local_header(fp, im, offset, 0) ImageFile._save(im, fp, [("gif", (0, 0)+im.size, 0, RAWMODE[im.mode])]) @@ -588,6 +642,7 @@ def getdata(im, offset=(0, 0), **params): Image.register_open(GifImageFile.format, GifImageFile, _accept) Image.register_save(GifImageFile.format, _save) +Image.register_save_all(GifImageFile.format, _save_all) Image.register_extension(GifImageFile.format, ".gif") Image.register_mime(GifImageFile.format, "image/gif") diff --git a/PIL/ImImagePlugin.py b/PIL/ImImagePlugin.py index 589928d0e..0a0a666ce 100644 --- a/PIL/ImImagePlugin.py +++ b/PIL/ImImagePlugin.py @@ -264,6 +264,10 @@ class ImImageFile(ImageFile.ImageFile): def n_frames(self): return self.info[FRAMES] + @property + def is_animated(self): + return self.info[FRAMES] > 1 + def seek(self, frame): if frame < 0 or frame >= self.info[FRAMES]: diff --git a/PIL/Image.py b/PIL/Image.py index a6b08d196..861599bf7 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -28,8 +28,11 @@ from __future__ import print_function from PIL import VERSION, PILLOW_VERSION, _plugins +import logging import warnings +logger = logging.getLogger(__name__) + class DecompressionBombWarning(RuntimeWarning): pass @@ -138,11 +141,6 @@ def isImageType(t): """ return hasattr(t, "im") -# -# Debug level - -DEBUG = 0 - # # Constants (also defined in _imagingmodule.c!) @@ -204,6 +202,7 @@ ID = [] OPEN = {} MIME = {} SAVE = {} +SAVE_ALL = {} EXTENSION = {} # -------------------------------------------------------------------- @@ -386,13 +385,10 @@ def init(): for plugin in _plugins: try: - if DEBUG: - print("Importing %s" % plugin) + logger.debug("Importing %s", plugin) __import__("PIL.%s" % plugin, globals(), locals(), []) - except ImportError: - if DEBUG: - print("Image: failed to import", end=' ') - print(plugin, ":", sys.exc_info()[1]) + except ImportError as e: + logger.debug("Image: failed to import %s: %s", plugin, e) if OPEN or SAVE: _initialized = 2 @@ -504,6 +500,14 @@ class Image(object): self.readonly = 0 self.pyaccess = None + @property + def width(self): + return self.size[0] + + @property + def height(self): + return self.size[1] + def _new(self, im): new = Image() new.im = im @@ -546,8 +550,7 @@ class Image(object): try: self.fp.close() except Exception as msg: - if DEBUG: - print("Error closing: %s" % msg) + logger.debug("Error closing: %s" % msg) # Instead of simply setting to None, we're setting up a # deferred error that will better explain that the core image @@ -1661,6 +1664,10 @@ class Image(object): # may mutate self! self.load() + save_all = False + if 'save_all' in params: + save_all = params['save_all'] + del params['save_all'] self.encoderinfo = params self.encoderconfig = () @@ -1669,20 +1676,16 @@ class Image(object): ext = os.path.splitext(filename)[1].lower() if not format: - try: - format = EXTENSION[ext] - except KeyError: + if ext not in EXTENSION: init() - try: - format = EXTENSION[ext] - except KeyError: - raise KeyError(ext) # unknown extension + format = EXTENSION[ext] - try: - save_handler = SAVE[format.upper()] - except KeyError: + if format.upper() not in SAVE: init() - save_handler = SAVE[format.upper()] # unknown format + if save_all: + save_handler = SAVE_ALL[format.upper()] + else: + save_handler = SAVE[format.upper()] if isPath(fp): fp = builtins.open(fp, "wb") @@ -2262,7 +2265,7 @@ def open(fp, mode="r"): :py:meth:`~PIL.Image.Image.load` method). See :py:func:`~PIL.Image.new`. - :param file: A filename (string) or a file object. The file object + :param fp: A filename (string) or a file object. The file object must implement :py:meth:`~file.read`, :py:meth:`~file.seek`, and :py:meth:`~file.tell` methods, and be opened in binary mode. :param mode: The mode. If given, this argument must be "r". @@ -2298,9 +2301,7 @@ def open(fp, mode="r"): _decompression_bomb_check(im.size) return im except (SyntaxError, IndexError, TypeError, struct.error): - # import traceback - # traceback.print_exc() - pass + logger.debug("", exc_info=True) if init(): @@ -2313,9 +2314,7 @@ def open(fp, mode="r"): _decompression_bomb_check(im.size) return im except (SyntaxError, IndexError, TypeError, struct.error): - # import traceback - # traceback.print_exc() - pass + logger.debug("", exc_info=True) raise IOError("cannot identify image file %r" % (filename if filename else fp)) @@ -2461,6 +2460,18 @@ def register_save(id, driver): SAVE[id.upper()] = driver +def register_save_all(id, driver): + """ + Registers an image function to save all the frames + of a multiframe format. This function should not be + used in application code. + + :param id: An image format identifier. + :param driver: A function to save images in this format. + """ + SAVE_ALL[id.upper()] = driver + + def register_extension(id, extension): """ Registers an image extension. This function should not be diff --git a/PIL/ImageFile.py b/PIL/ImageFile.py index cc1d73f09..52d21e1e8 100644 --- a/PIL/ImageFile.py +++ b/PIL/ImageFile.py @@ -30,10 +30,13 @@ from PIL import Image from PIL._util import isPath import io +import logging import os import sys import traceback +logger = logging.getLogger(__name__) + MAXBLOCK = 65536 SAFEBLOCK = 1024*1024 @@ -95,21 +98,11 @@ class ImageFile(Image.Image): try: self._open() - except IndexError as v: # end of data - if Image.DEBUG > 1: - traceback.print_exc() - raise SyntaxError(v) - except TypeError as v: # end of data (ord) - if Image.DEBUG > 1: - traceback.print_exc() - raise SyntaxError(v) - except KeyError as v: # unsupported mode - if Image.DEBUG > 1: - traceback.print_exc() - raise SyntaxError(v) - except EOFError as v: # got header but not the first frame - if Image.DEBUG > 1: - traceback.print_exc() + except (IndexError, # end of data + TypeError, # end of data (ord) + KeyError, # unsupported mode + EOFError) as v: # got header but not the first frame + logger.exception("%s") raise SyntaxError(v) if not self.mode or self.size[0] <= 0: diff --git a/PIL/JpegImagePlugin.py b/PIL/JpegImagePlugin.py index 5cae90073..7cb280764 100644 --- a/PIL/JpegImagePlugin.py +++ b/PIL/JpegImagePlugin.py @@ -37,6 +37,7 @@ __version__ = "0.6" import array import struct import io +import warnings from struct import unpack from PIL import Image, ImageFile, TiffImagePlugin, _binary from PIL.JpegPresets import presets @@ -713,8 +714,8 @@ def _save_cjpeg(im, fp, filename): # Factory for making JPEG and MPO instances def jpeg_factory(fp=None, filename=None): im = JpegImageFile(fp, filename) - mpheader = im._getmp() try: + mpheader = im._getmp() if mpheader[45057] > 1: # It's actually an MPO from .MpoImagePlugin import MpoImageFile @@ -722,6 +723,10 @@ def jpeg_factory(fp=None, filename=None): except (TypeError, IndexError): # It is really a JPEG pass + except SyntaxError: + warnings.warn("Image appears to be a malformed MPO file, it will be " + "interpreted as a base JPEG file") + pass return im diff --git a/PIL/MicImagePlugin.py b/PIL/MicImagePlugin.py index aa41bf359..8e3e1b11d 100644 --- a/PIL/MicImagePlugin.py +++ b/PIL/MicImagePlugin.py @@ -75,6 +75,10 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): def n_frames(self): return len(self.images) + @property + def is_animated(self): + return len(self.images) > 1 + def seek(self, frame): try: diff --git a/PIL/MpoImagePlugin.py b/PIL/MpoImagePlugin.py index 9d21728b9..b7e6c5756 100644 --- a/PIL/MpoImagePlugin.py +++ b/PIL/MpoImagePlugin.py @@ -66,6 +66,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): def n_frames(self): return self.__framecount + @property + def is_animated(self): + return self.__framecount > 1 + def seek(self, frame): if frame < 0 or frame >= self.__framecount: raise EOFError("no more images in MPO file") diff --git a/PIL/PcxImagePlugin.py b/PIL/PcxImagePlugin.py index 40fafa0dc..906c38a35 100644 --- a/PIL/PcxImagePlugin.py +++ b/PIL/PcxImagePlugin.py @@ -27,8 +27,11 @@ from __future__ import print_function +import logging from PIL import Image, ImageFile, ImagePalette, _binary +logger = logging.getLogger(__name__) + i8 = _binary.i8 i16 = _binary.i16le o8 = _binary.o8 @@ -59,17 +62,15 @@ class PcxImageFile(ImageFile.ImageFile): bbox = i16(s, 4), i16(s, 6), i16(s, 8)+1, i16(s, 10)+1 if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]: raise SyntaxError("bad PCX image size") - if Image.DEBUG: - print("BBox: %s %s %s %s" % bbox) + logger.debug("BBox: %s %s %s %s", *bbox) # format version = i8(s[1]) bits = i8(s[3]) planes = i8(s[65]) stride = i16(s, 66) - if Image.DEBUG: - print("PCX version %s, bits %s, planes %s, stride %s" % - (version, bits, planes, stride)) + logger.debug("PCX version %s, bits %s, planes %s, stride %s", + version, bits, planes, stride) self.info["dpi"] = i16(s, 12), i16(s, 14) @@ -107,8 +108,7 @@ class PcxImageFile(ImageFile.ImageFile): self.size = bbox[2]-bbox[0], bbox[3]-bbox[1] bbox = (0, 0) + self.size - if Image.DEBUG: - print("size: %sx%s" % self.size) + logger.debug("size: %sx%s", *self.size) self.tile = [("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))] @@ -144,9 +144,8 @@ def _save(im, fp, filename, check=0): # Ideally it should be passed in in the state, but the bytes value # gets overwritten. - if Image.DEBUG: - print("PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d" % ( - im.size[0], bits, stride)) + logger.debug("PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d", + im.size[0], bits, stride) # under windows, we could determine the current screen size with # "Image.core.display_mode()[1]", but I think that's overkill... diff --git a/PIL/PngImagePlugin.py b/PIL/PngImagePlugin.py index 214ff9385..9fcc9fe4e 100644 --- a/PIL/PngImagePlugin.py +++ b/PIL/PngImagePlugin.py @@ -35,10 +35,13 @@ from __future__ import print_function __version__ = "0.9" +import logging import re +import zlib from PIL import Image, ImageFile, ImagePalette, _binary -import zlib + +logger = logging.getLogger(__name__) i8 = _binary.i8 i16 = _binary.i16be @@ -129,8 +132,7 @@ class ChunkStream(object): def call(self, cid, pos, length): "Call the appropriate chunk handler" - if Image.DEBUG: - print("STREAM", cid, pos, length) + logger.debug("STREAM %s %s %s", cid, pos, length) return getattr(self, "chunk_" + cid.decode('ascii'))(pos, length) def crc(self, cid, data): @@ -293,9 +295,8 @@ class PngStream(ChunkStream): # Compression method 1 byte (0) # Compressed profile n bytes (zlib with deflate compression) i = s.find(b"\0") - if Image.DEBUG: - print("iCCP profile name", s[:i]) - print("Compression method", i8(s[i])) + logger.debug("iCCP profile name %s", s[:i]) + logger.debug("Compression method %s", i8(s[i])) comp_method = i8(s[i]) if comp_method != 0: raise SyntaxError("Unknown compression method %s in iCCP chunk" % @@ -507,8 +508,7 @@ class PngImageFile(ImageFile.ImageFile): except EOFError: break except AttributeError: - if Image.DEBUG: - print(cid, pos, length, "(unknown)") + logger.debug("%s %s %s (unknown)", cid, pos, length) s = ImageFile._safe_read(self.fp, length) self.png.crc(cid, s) diff --git a/PIL/PsdImagePlugin.py b/PIL/PsdImagePlugin.py index d30695adb..030f5144c 100644 --- a/PIL/PsdImagePlugin.py +++ b/PIL/PsdImagePlugin.py @@ -136,6 +136,10 @@ class PsdImageFile(ImageFile.ImageFile): def n_frames(self): return len(self.layers) + @property + def is_animated(self): + return len(self.layers) > 1 + def seek(self, layer): # seek to given layer (1..max) if layer == self.frame: diff --git a/PIL/PyAccess.py b/PIL/PyAccess.py index 4924facd5..cb4f00cad 100644 --- a/PIL/PyAccess.py +++ b/PIL/PyAccess.py @@ -22,10 +22,14 @@ from __future__ import print_function -from cffi import FFI +import logging import sys -DEBUG = 0 +from cffi import FFI + + +logger = logging.getLogger(__name__) + defs = """ struct Pixel_RGBA { @@ -50,8 +54,7 @@ class PyAccess(object): self.xsize = vals['xsize'] self.ysize = vals['ysize'] - if DEBUG: - print(vals) + logger.debug("%s", vals) self._post_init() def _post_init(self): @@ -305,11 +308,9 @@ else: def new(img, readonly=False): access_type = mode_map.get(img.mode, None) if not access_type: - if DEBUG: - print("PyAccess Not Implemented: %s" % img.mode) + logger.debug("PyAccess Not Implemented: %s", img.mode) return None - if DEBUG: - print("New PyAccess: %s" % img.mode) + logger.debug("New PyAccess: %s", img.mode) return access_type(img, readonly) # End of file diff --git a/PIL/SpiderImagePlugin.py b/PIL/SpiderImagePlugin.py index 7de5156b1..f306538ae 100644 --- a/PIL/SpiderImagePlugin.py +++ b/PIL/SpiderImagePlugin.py @@ -158,6 +158,10 @@ class SpiderImageFile(ImageFile.ImageFile): def n_frames(self): return self._nimages + @property + def is_animated(self): + return self._nimages > 1 + # 1st image index is zero (although SPIDER imgnumber starts at 1) def tell(self): if self.imgnumber < 1: diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 372dad4c9..85d943df2 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -42,6 +42,7 @@ from __future__ import print_function __version__ = "1.3.5" +DEBUG = False # Needs to be merged with the new logging approach. from PIL import Image, ImageFile from PIL import ImagePalette @@ -434,7 +435,7 @@ class ImageFileDirectory(collections.MutableMapping): tag, typ = i16(ifd), i16(ifd, 2) - if Image.DEBUG: + if DEBUG: from PIL import TiffTags tagname = TiffTags.TAGS.get(tag, "unknown") typname = TiffTags.TYPES.get(typ, "unknown") @@ -444,7 +445,7 @@ class ImageFileDirectory(collections.MutableMapping): try: dispatch = self.load_dispatch[typ] except KeyError: - if Image.DEBUG: + if DEBUG: print("- unsupported type", typ) continue # ignore unsupported type @@ -455,10 +456,10 @@ class ImageFileDirectory(collections.MutableMapping): # Get and expand tag value if size > 4: here = fp.tell() - if Image.DEBUG: + if DEBUG: print("Tag Location: %s" % here) fp.seek(i32(ifd, 8)) - if Image.DEBUG: + if DEBUG: print("Data Location: %s" % fp.tell()) data = ImageFile._safe_read(fp, size) fp.seek(here) @@ -474,7 +475,7 @@ class ImageFileDirectory(collections.MutableMapping): self.tagdata[tag] = data self.tagtype[tag] = typ - if Image.DEBUG: + if DEBUG: if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, ICCPROFILE, XMP): print("- value: " % size) @@ -517,8 +518,8 @@ 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 DEBUG: + print ("Tag %s, Type: %s, Value: %s" % (tag, typ, value)) if typ == 1: # byte data @@ -573,7 +574,7 @@ class ImageFileDirectory(collections.MutableMapping): else: data = b"".join(map(o32, value)) - if Image.DEBUG: + if DEBUG: from PIL import TiffTags tagname = TiffTags.TAGS.get(tag, "unknown") typname = TiffTags.TYPES.get(typ, "unknown") @@ -610,7 +611,7 @@ class ImageFileDirectory(collections.MutableMapping): # pass 2: write directory to file for tag, typ, count, value, data in directory: - if Image.DEBUG > 1: + if DEBUG > 1: print(tag, typ, count, repr(value), repr(data)) fp.write(o16(tag) + o16(typ) + o32(count) + value) @@ -652,11 +653,12 @@ class TiffImageFile(ImageFile.ImageFile): self.__fp = self.fp self._frame_pos = [] self._n_frames = None + self._is_animated = None - if Image.DEBUG: - print("*** TiffImageFile._open ***") - print("- __first:", self.__first) - print("- ifh: ", ifh) + if DEBUG: + print ("*** TiffImageFile._open ***") + print ("- __first:", self.__first) + print ("- ifh: ", ifh) # and load the first frame self._seek(0) @@ -673,6 +675,20 @@ class TiffImageFile(ImageFile.ImageFile): self.seek(current) return self._n_frames + @property + def is_animated(self): + if self._is_animated is None: + current = self.tell() + + try: + self.seek(1) + self._is_animated = True + except EOFError: + self._is_animated = False + + self.seek(current) + return self._is_animated + def seek(self, frame): "Select a given frame as current image" self._seek(max(frame, 0)) # Questionable backwards compatibility. @@ -687,7 +703,7 @@ class TiffImageFile(ImageFile.ImageFile): while len(self._frame_pos) <= frame: if not self.__next: raise EOFError("no more images in TIFF file") - if Image.DEBUG: + if DEBUG: print("Seeking to frame %s, on frame %s, __next %s, location: %s" % (frame, self.__frame, self.__next, self.fp.tell())) # reset python3 buffered io handle in case fp @@ -695,7 +711,7 @@ class TiffImageFile(ImageFile.ImageFile): self.fp.tell() self.fp.seek(self.__next) self._frame_pos.append(self.__next) - if Image.DEBUG: + if DEBUG: print("Loading tags, location: %s" % self.fp.tell()) self.tag.load(self.fp) self.__next = self.tag.next @@ -773,20 +789,20 @@ class TiffImageFile(ImageFile.ImageFile): # Rearranging for supporting byteio items, since they have a fileno # that returns an IOError if there's no underlying fp. Easier to # deal with here by reordering. - if Image.DEBUG: - print("have getvalue. just sending in a string from getvalue") + if DEBUG: + print ("have getvalue. just sending in a string from getvalue") n, err = decoder.decode(self.fp.getvalue()) elif hasattr(self.fp, "fileno"): # we've got a actual file on disk, pass in the fp. - if Image.DEBUG: - print("have fileno, calling fileno version of the decoder.") + if DEBUG: + print ("have fileno, calling fileno version of the decoder.") self.fp.seek(0) # 4 bytes, otherwise the trace might error out n, err = decoder.decode(b"fpfp") else: # we have something else. - if Image.DEBUG: - print("don't have fileno or getvalue. just reading") + if DEBUG: + print ("don't have fileno or getvalue. just reading") # UNDONE -- so much for that buffer size thing. n, err = decoder.decode(self.fp.read()) @@ -823,7 +839,7 @@ class TiffImageFile(ImageFile.ImageFile): fillorder = getscalar(FILLORDER, 1) - if Image.DEBUG: + if DEBUG: print("*** Summary ***") print("- compression:", self._compression) print("- photometric_interpretation:", photo) @@ -835,7 +851,7 @@ class TiffImageFile(ImageFile.ImageFile): ysize = getscalar(IMAGELENGTH) self.size = xsize, ysize - if Image.DEBUG: + if DEBUG: print("- size:", self.size) format = getscalar(SAMPLEFORMAT, 1) @@ -846,16 +862,16 @@ class TiffImageFile(ImageFile.ImageFile): self.tag.get(BITSPERSAMPLE, (1,)), self.tag.get(EXTRASAMPLES, ()) ) - if Image.DEBUG: + if DEBUG: print("format key:", key) try: self.mode, rawmode = OPEN_INFO[key] except KeyError: - if Image.DEBUG: + if DEBUG: print("- unsupported format") raise SyntaxError("unknown pixel mode") - if Image.DEBUG: + if DEBUG: print("- raw mode:", rawmode) print("- pil mode:", self.mode) @@ -895,7 +911,7 @@ class TiffImageFile(ImageFile.ImageFile): "tiff_sgilog", "tiff_sgilog24", "tiff_raw_16"]: - # if Image.DEBUG: + # if DEBUG: # print "Activating g4 compression for whole file" # Decoder expects entire file as one tile. @@ -936,7 +952,7 @@ class TiffImageFile(ImageFile.ImageFile): self.tag.get(BITSPERSAMPLE, (1,)), self.tag.get(EXTRASAMPLES, ()) ) - if Image.DEBUG: + if DEBUG: print("format key:", key) # this should always work, since all the # fillorder==2 modes have a corresponding @@ -965,8 +981,8 @@ class TiffImageFile(ImageFile.ImageFile): (self._compression, (0, min(y, ysize), w, min(y+h, ysize)), offsets[i], a)) - if Image.DEBUG: - print("tiles: ", self.tile) + if DEBUG: + print ("tiles: ", self.tile) y = y + h if y >= self.size[1]: x = y = 0 @@ -994,7 +1010,7 @@ class TiffImageFile(ImageFile.ImageFile): l += 1 a = None else: - if Image.DEBUG: + if DEBUG: print("- unsupported data organization") raise SyntaxError("unknown data organization") @@ -1075,7 +1091,7 @@ def _save(im, fp, filename): # write any arbitrary tags passed in as an ImageFileDirectory info = im.encoderinfo.get("tiffinfo", {}) - if Image.DEBUG: + if DEBUG: print("Tiffinfo Keys: %s" % info.keys) keys = list(info.keys()) for key in keys: @@ -1150,9 +1166,9 @@ def _save(im, fp, filename): ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) if libtiff: - if Image.DEBUG: - print("Saving using libtiff encoder") - print(ifd.items()) + if DEBUG: + print ("Saving using libtiff encoder") + print (ifd.items()) _fp = 0 if hasattr(fp, "fileno"): try: @@ -1208,8 +1224,8 @@ def _save(im, fp, filename): # int or similar atts[k] = v - if Image.DEBUG: - print(atts) + if DEBUG: + print (atts) # libtiff always expects the bytes in native order. # we're storing image byte order. So, if the rawmode diff --git a/PIL/__init__.py b/PIL/__init__.py index 6d51a5dcb..665474f9b 100644 --- a/PIL/__init__.py +++ b/PIL/__init__.py @@ -12,7 +12,7 @@ # ;-) VERSION = '1.1.7' # PIL version -PILLOW_VERSION = '2.9.0.dev0' # Pillow +PILLOW_VERSION = '3.0.0.dev0' # Pillow _plugins = ['BmpImagePlugin', 'BufrStubImagePlugin', diff --git a/README.rst b/README.rst index d39070bbe..169547be1 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ Pillow is the "friendly PIL fork" by `Alex Clark and Contributors -# sequence.append(im) -# -# # write GIF animation -# fp = open("out.gif", "wb") -# gifmaker.makedelta(fp, sequence) -# fp.close() -# -# Alternatively, use an iterator to generate the sequence, and -# write data directly to a socket. Or something... -# - from __future__ import print_function -from PIL import Image, ImageChops, ImageSequence - -from PIL.GifImagePlugin import getheader, getdata - -# -------------------------------------------------------------------- -# straightforward delta encoding - - -def makedelta(fp, sequence): - """Convert list of image frames to a GIF animation file""" - - frames = 0 - - previous = None - - for im in sequence: - - # To specify duration, add the time in milliseconds to getdata(), - # e.g. getdata(im, duration=1000) - - if not previous: - - # global header - for s in getheader(im)[0] + getdata(im): - fp.write(s) - - else: - - # delta frame - delta = ImageChops.subtract_modulo(im, previous) - - bbox = delta.getbbox() - - if bbox: - - # compress difference - for s in getdata(im.crop(bbox), offset=bbox[:2]): - fp.write(s) - - else: - # FIXME: what should we do in this case? - pass - - previous = im.copy() - - frames += 1 - - fp.write(b";") - - return frames - -# -------------------------------------------------------------------- -# main hack - - -def compress(infile, outfile): - - # open input image, and force loading of first frame - im = Image.open(infile) - im.load() - - # open output file - fp = open(outfile, "wb") - - seq = ImageSequence.Iterator(im) - - makedelta(fp, seq) - - fp.close() - +from PIL import Image if __name__ == "__main__": @@ -122,4 +27,5 @@ if __name__ == "__main__": print("Usage: gifmaker infile outfile") sys.exit(1) - compress(sys.argv[1], sys.argv[2]) + im = Image.open(sys.argv[1]) + im.save(sys.argv[2], save_all=True) diff --git a/Scripts/pilfile.py b/Scripts/pilfile.py index b954114ac..dab240e2f 100644 --- a/Scripts/pilfile.py +++ b/Scripts/pilfile.py @@ -21,6 +21,7 @@ from __future__ import print_function import getopt import glob +import logging import sys from PIL import Image @@ -42,6 +43,7 @@ except getopt.error as v: sys.exit(1) verbose = quiet = verify = 0 +logging_level = "WARNING" for o, a in opt: if o == "-f": @@ -58,7 +60,9 @@ for o, a in opt: elif o == "-v": verify = 1 elif o == "-D": - Image.DEBUG += 1 + logging_level = "DEBUG" + +logging.basicConfig(level=logging_level) def globfix(files): diff --git a/Scripts/player.py b/Scripts/player.py index 43877415a..ac9eb817f 100644 --- a/Scripts/player.py +++ b/Scripts/player.py @@ -15,9 +15,6 @@ from PIL import Image, ImageTk import sys -Image.DEBUG = 0 - - # -------------------------------------------------------------------- # an image animation player diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index c6d99b8d1..a31cd2180 100644 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -3,7 +3,7 @@ from __future__ import division from helper import unittest, PillowTestCase import sys -from PIL import Image, ImageFilter +from PIL import Image min_iterations = 100 max_iterations = 10000 @@ -31,7 +31,8 @@ class TestImagingLeaks(PillowTestCase): def test_leak_putdata(self): im = Image.new('RGB', (25, 25)) - self._test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) + self._test_leak(min_iterations, max_iterations, + im.putdata, im.getdata()) def test_leak_getlist(self): im = Image.new('P', (25, 25)) diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 762c9607a..c24eeb359 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -42,7 +42,8 @@ class TestPngDos(PillowTestCase): total_len = 0 for txt in im2.text.values(): total_len += len(txt) - self.assertLess(total_len, 64*1024*1024, "Total text chunks greater than 64M") + self.assertLess(total_len, 64*1024*1024, + "Total text chunks greater than 64M") if __name__ == '__main__': unittest.main() diff --git a/Tests/images/bad_palette_entry.gpl b/Tests/images/bad_palette_entry.gpl new file mode 100644 index 000000000..162037184 --- /dev/null +++ b/Tests/images/bad_palette_entry.gpl @@ -0,0 +1,12 @@ +GIMP Palette +Name: badpaletteentry +Columns: 4 +# + 0 0 0 Index 3 + 65 38 +103 62 49 Index 6 + 79 73 72 Index 7 +114 101 97 Index 8 +208 127 100 Index 9 +151 144 142 Index 10 +221 207 199 Index 11 diff --git a/Tests/images/bad_palette_file.gpl b/Tests/images/bad_palette_file.gpl new file mode 100644 index 000000000..c366cc8db --- /dev/null +++ b/Tests/images/bad_palette_file.gpl @@ -0,0 +1,12 @@ +GIMP Palette +Name: badpalettefile +Columns: 4 +# + 0 0 0 Index 3 +01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 +103 62 49 Index 6 + 79 73 72 Index 7 +114 101 97 Index 8 +208 127 100 Index 9 +151 144 142 Index 10 +221 207 199 Index 11 diff --git a/Tests/images/custom_gimp_palette.gpl b/Tests/images/custom_gimp_palette.gpl new file mode 100644 index 000000000..08ea70028 --- /dev/null +++ b/Tests/images/custom_gimp_palette.gpl @@ -0,0 +1,12 @@ +GIMP Palette +Name: custompalette +Columns: 4 +# + 0 0 0 Index 3 + 65 38 30 Index 4 +103 62 49 Index 6 + 79 73 72 Index 7 +114 101 97 Index 8 +208 127 100 Index 9 +151 144 142 Index 10 +221 207 199 Index 11 diff --git a/Tests/images/hopper.bmp b/Tests/images/hopper.bmp new file mode 100644 index 000000000..785700d22 Binary files /dev/null and b/Tests/images/hopper.bmp differ diff --git a/Tests/images/sugarshack_bad_mpo_header.jpg b/Tests/images/sugarshack_bad_mpo_header.jpg new file mode 100644 index 000000000..6e2d75ab6 Binary files /dev/null and b/Tests/images/sugarshack_bad_mpo_header.jpg differ diff --git a/Tests/images/test.gpl b/Tests/images/test.gpl new file mode 100644 index 000000000..7436a3099 --- /dev/null +++ b/Tests/images/test.gpl @@ -0,0 +1,4 @@ +GIMP Palette +Name: Test +Columns: 0 +# diff --git a/Tests/test_cffi.py b/Tests/test_cffi.py index cea0db093..02d1ff7d3 100644 --- a/Tests/test_cffi.py +++ b/Tests/test_cffi.py @@ -61,6 +61,10 @@ class TestCffi(PillowTestCase): for y in range(0, h, 10): self.assertEqual(access[(x, y)], caccess[(x, y)]) + # Access an out-of-range pixel + self.assertRaises(ValueError, + lambda: access[(access.xsize+1, access.ysize+1)]) + def test_get_vs_c(self): rgb = hopper('RGB') rgb.load() @@ -70,7 +74,7 @@ class TestCffi(PillowTestCase): self._test_get_access(hopper('LA')) self._test_get_access(hopper('1')) self._test_get_access(hopper('P')) - # self._test_get_access(hopper('PA')) # PA -- how do I make a PA image? + # self._test_get_access(hopper('PA')) # PA -- how do I make a PA image? self._test_get_access(hopper('F')) im = Image.new('I;16', (10, 10), 40000) @@ -103,6 +107,14 @@ class TestCffi(PillowTestCase): access[(x, y)] = color self.assertEqual(color, caccess[(x, y)]) + # Attempt to set the value on a read-only image + access = PyAccess.new(im, True) + try: + access[(0, 0)] = color + except ValueError: + return + self.fail("Putpixel did not fail on a read-only image") + def test_set_vs_c(self): rgb = hopper('RGB') rgb.load() diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 69792fe12..25f70139d 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase, hopper -from PIL import Image +from PIL import Image, BmpImagePlugin import io @@ -25,6 +25,11 @@ class TestFileBmp(PillowTestCase): self.roundtrip(hopper("P")) self.roundtrip(hopper("RGB")) + def test_invalid_file(self): + with open("Tests/images/flower.jpg", "rb") as fp: + self.assertRaises(SyntaxError, + lambda: BmpImagePlugin.BmpImageFile(fp)) + def test_save_to_bytes(self): output = io.BytesIO() im = hopper() @@ -49,6 +54,22 @@ class TestFileBmp(PillowTestCase): self.assertEqual(reloaded.info["dpi"], dpi) + def test_save_bmp_with_dpi(self): + # Test for #1301 + # Arrange + outfile = self.tempfile("temp.jpg") + im = Image.open("Tests/images/hopper.bmp") + + # Act + im.save(outfile, 'JPEG', dpi=im.info['dpi']) + + # Assert + reloaded = Image.open(outfile) + reloaded.load() + self.assertEqual(im.info['dpi'], reloaded.info['dpi']) + self.assertEqual(im.size, reloaded.size) + self.assertEqual(reloaded.format, "JPEG") + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py new file mode 100644 index 000000000..05266e9fd --- /dev/null +++ b/Tests/test_file_bufrstub.py @@ -0,0 +1,19 @@ +from helper import unittest, PillowTestCase + +from PIL import BufrStubImagePlugin + + +class TestFileBufrStub(PillowTestCase): + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: + BufrStubImagePlugin.BufrStubImageFile(invalid_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 07bf3a750..fa4242629 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -20,6 +20,12 @@ class TestFileCur(PillowTestCase): self.assertEqual(im.getpixel((11, 1)), (253, 254, 254, 1)) self.assertEqual(im.getpixel((16, 16)), (84, 87, 86, 255)) + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: CurImagePlugin.CurImageFile(invalid_file)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 7d2ae32d8..2c0f90c1f 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -20,6 +20,11 @@ class TestFileDcx(PillowTestCase): orig = hopper() self.assert_image_equal(im, orig) + def test_invalid_file(self): + with open("Tests/images/flower.jpg", "rb") as fp: + self.assertRaises(SyntaxError, + lambda: DcxImagePlugin.DcxImageFile(fp)) + def test_tell(self): # Arrange im = Image.open(TEST_FILE) @@ -33,6 +38,19 @@ class TestFileDcx(PillowTestCase): def test_n_frames(self): im = Image.open(TEST_FILE) self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) + + def test_eoferror(self): + im = Image.open(TEST_FILE) + + n_frames = im.n_frames + while True: + n_frames -= 1 + try: + im.seek(n_frames) + break + except EOFError: + self.assertTrue(im.tell() < n_frames) def test_seek_too_far(self): # Arrange diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 08d2d875a..6e4c63e4f 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -51,6 +51,12 @@ class TestFileEps(PillowTestCase): self.assertEqual(image2_scale2.size, (720, 504)) self.assertEqual(image2_scale2.format, "EPS") + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: EpsImagePlugin.EpsImageFile(invalid_file)) + def test_file_object(self): # issue 479 image1 = Image.open(file1) @@ -149,7 +155,9 @@ class TestFileEps(PillowTestCase): Image.open(file3) def _test_readline(self, t, ending): - ending = "Failure with line ending: %s" % ("".join("%s" % ord(s) for s in ending)) + ending = "Failure with line ending: %s" % ("".join( + "%s" % ord(s) + for s in ending)) self.assertEqual(t.readline().strip('\r\n'), 'something', ending) self.assertEqual(t.readline().strip('\r\n'), 'else', ending) self.assertEqual(t.readline().strip('\r\n'), 'baz', ending) diff --git a/Tests/test_file_fitsstub.py b/Tests/test_file_fitsstub.py new file mode 100644 index 000000000..cc38b4a3a --- /dev/null +++ b/Tests/test_file_fitsstub.py @@ -0,0 +1,19 @@ +from helper import unittest, PillowTestCase + +from PIL import FitsStubImagePlugin + + +class TestFileFitsStub(PillowTestCase): + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: + FitsStubImagePlugin.FITSStubImageFile(invalid_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 04c2006c9..aa2fa5ae5 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import Image +from PIL import Image, FliImagePlugin # sample ppm stream # created as an export of a palette image from Gimp2.6 @@ -18,9 +18,28 @@ class TestFileFli(PillowTestCase): self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "FLI") + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: FliImagePlugin.FliImageFile(invalid_file)) + def test_n_frames(self): im = Image.open(test_file) - self.assertEqual(im.n_frames, 2) + self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) + + def test_eoferror(self): + im = Image.open(test_file) + + n_frames = im.n_frames + while True: + n_frames -= 1 + try: + im.seek(n_frames) + break + except EOFError: + self.assertTrue(im.tell() < n_frames) if __name__ == '__main__': diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py new file mode 100644 index 000000000..35119a612 --- /dev/null +++ b/Tests/test_file_fpx.py @@ -0,0 +1,18 @@ +from helper import unittest, PillowTestCase + +from PIL import FpxImagePlugin + + +class TestFileFpx(PillowTestCase): + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: FpxImagePlugin.FpxImageFile(invalid_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py new file mode 100644 index 000000000..57b301ada --- /dev/null +++ b/Tests/test_file_gbr.py @@ -0,0 +1,18 @@ +from helper import unittest, PillowTestCase + +from PIL import GbrImagePlugin + + +class TestFileGbr(PillowTestCase): + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: GbrImagePlugin.GbrImageFile(invalid_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index b15d32097..70438eb03 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -25,6 +25,12 @@ class TestFileGif(PillowTestCase): self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "GIF") + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: GifImagePlugin.GifImageFile(invalid_file)) + def test_optimize(self): from io import BytesIO @@ -71,6 +77,24 @@ class TestFileGif(PillowTestCase): self.assert_image_similar(reread.convert('RGB'), hopper(), 50) + def test_roundtrip_save_all(self): + # Single frame image + out = self.tempfile('temp.gif') + im = hopper() + im.save(out, save_all=True) + reread = Image.open(out) + + self.assert_image_similar(reread.convert('RGB'), im, 50) + + # Multiframe image + im = Image.open("Tests/images/dispose_bgnd.gif") + + out = self.tempfile('temp.gif') + im.save(out, save_all=True) + reread = Image.open(out) + + self.assertEqual(reread.n_frames, 5) + def test_palette_handling(self): # see https://github.com/python-pillow/Pillow/issues/513 @@ -135,8 +159,25 @@ class TestFileGif(PillowTestCase): self.assertEqual(framecount, 5) def test_n_frames(self): + im = Image.open(TEST_GIF) + self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) + im = Image.open("Tests/images/iss634.gif") - self.assertEqual(im.n_frames, 43) + self.assertEqual(im.n_frames, 42) + self.assertTrue(im.is_animated) + + def test_eoferror(self): + im = Image.open(TEST_GIF) + + n_frames = im.n_frames + while True: + n_frames -= 1 + try: + im.seek(n_frames) + break + except EOFError: + self.assertTrue(im.tell() < n_frames) def test_dispose_none(self): img = Image.open("Tests/images/dispose_none.gif") diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py new file mode 100644 index 000000000..dfa2845ac --- /dev/null +++ b/Tests/test_file_gimppalette.py @@ -0,0 +1,36 @@ +from helper import unittest, PillowTestCase + +from PIL.GimpPaletteFile import GimpPaletteFile + + +class TestImage(PillowTestCase): + + def test_sanity(self): + with open('Tests/images/test.gpl', 'rb') as fp: + GimpPaletteFile(fp) + + with open('Tests/images/hopper.jpg', 'rb') as fp: + self.assertRaises(SyntaxError, lambda: GimpPaletteFile(fp)) + + with open('Tests/images/bad_palette_file.gpl', 'rb') as fp: + self.assertRaises(SyntaxError, lambda: GimpPaletteFile(fp)) + + with open('Tests/images/bad_palette_entry.gpl', 'rb') as fp: + self.assertRaises(ValueError, lambda: GimpPaletteFile(fp)) + + def test_get_palette(self): + # Arrange + with open('Tests/images/custom_gimp_palette.gpl', 'rb') as fp: + palette_file = GimpPaletteFile(fp) + + # Act + palette, mode = palette_file.getpalette() + + # Assert + self.assertEqual(mode, "RGB") + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py new file mode 100644 index 000000000..dd4ee84ff --- /dev/null +++ b/Tests/test_file_gribstub.py @@ -0,0 +1,19 @@ +from helper import unittest, PillowTestCase + +from PIL import GribStubImagePlugin + + +class TestFileGribStub(PillowTestCase): + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: + GribStubImagePlugin.GribStubImageFile(invalid_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py new file mode 100644 index 000000000..485931b36 --- /dev/null +++ b/Tests/test_file_hdf5stub.py @@ -0,0 +1,19 @@ +from helper import unittest, PillowTestCase + +from PIL import Hdf5StubImagePlugin + + +class TestFileHdf5Stub(PillowTestCase): + + def test_invalid_file(self): + test_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: + Hdf5StubImagePlugin.HDF5StubImageFile(test_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index f7b52b124..70e00c083 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,7 +1,7 @@ from helper import unittest, PillowTestCase, hopper import io -from PIL import Image +from PIL import Image, IcoImagePlugin # sample ppm stream TEST_ICO_FILE = "Tests/images/hopper.ico" @@ -17,6 +17,11 @@ class TestFileIco(PillowTestCase): self.assertEqual(im.size, (16, 16)) self.assertEqual(im.format, "ICO") + def test_invalid_file(self): + with open("Tests/images/flower.jpg", "rb") as fp: + self.assertRaises(SyntaxError, + lambda: IcoImagePlugin.IcoImageFile(fp)) + def test_save_to_bytes(self): output = io.BytesIO() im = hopper() @@ -30,7 +35,8 @@ class TestFileIco(PillowTestCase): self.assertEqual(im.mode, reloaded.mode) self.assertEqual((64, 64), reloaded.size) self.assertEqual(reloaded.format, "ICO") - self.assert_image_equal(reloaded, hopper().resize((64, 64), Image.LANCZOS)) + self.assert_image_equal(reloaded, + hopper().resize((64, 64), Image.LANCZOS)) # the other one output.seek(0) @@ -40,7 +46,8 @@ class TestFileIco(PillowTestCase): self.assertEqual(im.mode, reloaded.mode) self.assertEqual((32, 32), reloaded.size) self.assertEqual(reloaded.format, "ICO") - self.assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) + self.assert_image_equal(reloaded, + hopper().resize((32, 32), Image.LANCZOS)) if __name__ == '__main__': diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 24e00b2f0..e3d92d1d5 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase, hopper -from PIL import Image +from PIL import Image, ImImagePlugin # sample im TEST_IM = "Tests/images/hopper.im" @@ -18,6 +18,19 @@ class TestFileIm(PillowTestCase): def test_n_frames(self): im = Image.open(TEST_IM) self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) + + def test_eoferror(self): + im = Image.open(TEST_IM) + + n_frames = im.n_frames + while True: + n_frames -= 1 + try: + im.seek(n_frames) + break + except EOFError: + self.assertTrue(im.tell() < n_frames) def test_roundtrip(self): out = self.tempfile('temp.im') @@ -27,6 +40,13 @@ class TestFileIm(PillowTestCase): self.assert_image_equal(reread, im) + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: ImImagePlugin.ImImageFile(invalid_file)) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index d4929dd58..25c599a17 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -291,22 +291,24 @@ class TestFileJpeg(PillowTestCase): # dict of qtable lists self.assert_image_similar(im, - self.roundtrip(im, - qtables={0: standard_l_qtable, - 1: standard_chrominance_qtable}), - 30) + self.roundtrip(im, qtables={ + 0: standard_l_qtable, + 1: standard_chrominance_qtable + }), 30) # not a sequence self.assertRaises(Exception, lambda: self.roundtrip(im, qtables='a')) # sequence wrong length self.assertRaises(Exception, lambda: self.roundtrip(im, qtables=[])) # sequence wrong length - self.assertRaises(Exception, lambda: self.roundtrip(im, qtables=[1, 2, 3, 4, 5])) + self.assertRaises(Exception, + lambda: self.roundtrip(im, qtables=[1, 2, 3, 4, 5])) # qtable entry not a sequence self.assertRaises(Exception, lambda: self.roundtrip(im, qtables=[1])) # qtable entry has wrong number of items - self.assertRaises(Exception, lambda: self.roundtrip(im, qtables=[[1, 2, 3, 4]])) + self.assertRaises(Exception, + lambda: self.roundtrip(im, qtables=[[1, 2, 3, 4]])) @unittest.skipUnless(djpeg_available(), "djpeg not available") def test_load_djpeg(self): @@ -337,7 +339,8 @@ class TestFileJpeg(PillowTestCase): """ Generates a very hard to compress file :param size: tuple """ - return Image.frombytes('RGB', size, os.urandom(size[0]*size[1] * 3)) + return Image.frombytes('RGB', + size, os.urandom(size[0]*size[1] * 3)) im = gen_random_image((512, 512)) f = self.tempfile("temp.jpeg") @@ -350,6 +353,17 @@ class TestFileJpeg(PillowTestCase): reloaded.save(f, quality='keep', progressive=True) reloaded.save(f, quality='keep', optimize=True) + def test_bad_mpo_header(self): + """ Treat unknown MPO as JPEG """ + # Arrange + + # Act + # Shouldn't raise error + im = Image.open("Tests/images/sugarshack_bad_mpo_header.jpg") + + # Assert + self.assertEqual(im.format, "JPEG") + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 9768a881d..4b7286a20 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import Image +from PIL import Image, Jpeg2KImagePlugin from io import BytesIO codecs = dir(Image.core) @@ -39,6 +39,13 @@ class TestFileJpeg2k(PillowTestCase): self.assertEqual(im.size, (640, 480)) self.assertEqual(im.format, 'JPEG2000') + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: + Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file)) + def test_bytesio(self): with open('Tests/images/test-card-lossless.jp2', 'rb') as f: data = BytesIO(f.read()) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 896038b9d..8d5b383a9 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,11 +1,14 @@ from __future__ import print_function from helper import unittest, PillowTestCase, hopper, py3 -import os import io +import logging +import os from PIL import Image, TiffImagePlugin +logger = logging.getLogger(__name__) + class LibTiffTestCase(PillowTestCase): @@ -231,7 +234,6 @@ class TestFileLibTiff(LibTiffTestCase): """ Are we generating the same interpretation of the image as Imagemagick is? """ TiffImagePlugin.READ_LIBTIFF = True - # Image.DEBUG = True im = Image.open('Tests/images/12bit.cropped.tif') im.load() TiffImagePlugin.READ_LIBTIFF = False @@ -243,14 +245,8 @@ class TestFileLibTiff(LibTiffTestCase): im2 = Image.open('Tests/images/12in16bit.tif') - if Image.DEBUG: - print(im.getpixel((0, 0))) - print(im.getpixel((0, 1))) - print(im.getpixel((0, 2))) - - print(im2.getpixel((0, 0))) - print(im2.getpixel((0, 1))) - print(im2.getpixel((0, 2))) + logger.debug("%s", [img.getpixel((0, idx)) + for img in [im, im2] for idx in range(3)]) self.assert_image_equal(im, im2) diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py new file mode 100644 index 000000000..d547011e2 --- /dev/null +++ b/Tests/test_file_mcidas.py @@ -0,0 +1,19 @@ +from helper import unittest, PillowTestCase + +from PIL import McIdasImagePlugin + + +class TestFileMcIdas(PillowTestCase): + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: + McIdasImagePlugin.McIdasImageFile(invalid_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py new file mode 100644 index 000000000..0a574422a --- /dev/null +++ b/Tests/test_file_mic.py @@ -0,0 +1,18 @@ +from helper import unittest, PillowTestCase + +from PIL import MicImagePlugin + + +class TestFileMic(PillowTestCase): + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: MicImagePlugin.MicImageFile(invalid_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 1a0ebc453..b1d3b7413 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -98,6 +98,19 @@ class TestFileMpo(PillowTestCase): def test_n_frames(self): im = Image.open("Tests/images/sugarshack.mpo") self.assertEqual(im.n_frames, 2) + self.assertTrue(im.is_animated) + + def test_eoferror(self): + im = Image.open("Tests/images/sugarshack.mpo") + + n_frames = im.n_frames + while True: + n_frames -= 1 + try: + im.seek(n_frames) + break + except EOFError: + self.assertTrue(im.tell() < n_frames) def test_image_grab(self): for test_file in test_files: diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index f4b1af75e..3dbca6e60 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase, hopper -from PIL import Image +from PIL import Image, MspImagePlugin TEST_FILE = "Tests/images/hopper.msp" @@ -18,6 +18,12 @@ class TestFileMsp(PillowTestCase): self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "MSP") + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: MspImagePlugin.MspImageFile(invalid_file)) + def test_open(self): # Arrange # Act diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 10d17d349..f6342bed9 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase, hopper -from PIL import Image +from PIL import Image, PcxImagePlugin class TestFilePcx(PillowTestCase): @@ -19,6 +19,12 @@ class TestFilePcx(PillowTestCase): for mode in ('1', 'L', 'P', 'RGB'): self._roundtrip(hopper(mode)) + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: PcxImagePlugin.PcxImageFile(invalid_file)) + def test_odd(self): # see issue #523, odd sized images should have a stride that's even. # not that imagemagick or gimp write pcx that way. diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 9424bc09d..8dad9822c 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -52,6 +52,12 @@ class TestFilePdf(PillowTestCase): # Act / Assert self.helper_save_as_pdf(mode) + def test_unsupported_mode(self): + im = hopper("LA") + outfile = self.tempfile("temp_LA.pdf") + + self.assertRaises(ValueError, lambda: im.save(outfile)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index f438e24cc..cb72b2d73 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -81,6 +81,12 @@ class TestFilePng(PillowTestCase): hopper("I").save(test_file) im = Image.open(test_file) + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: PngImagePlugin.PngImageFile(invalid_file)) + def test_broken(self): # Check reading of totally broken files. In this case, the test # file was checked into Subversion as a text file. diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index dca3601b2..4890839f1 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import Image +from PIL import Image, PsdImagePlugin # sample ppm stream test_file = "Tests/images/hopper.psd" @@ -16,12 +16,33 @@ class TestImagePsd(PillowTestCase): self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "PSD") + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: PsdImagePlugin.PsdImageFile(invalid_file)) + def test_n_frames(self): im = Image.open("Tests/images/hopper_merged.psd") self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) im = Image.open(test_file) self.assertEqual(im.n_frames, 2) + self.assertTrue(im.is_animated) + + def test_eoferror(self): + im = Image.open(test_file) + + n_frames = im.n_frames + while True: + n_frames -= 1 + try: + # PSD seek index starts at 1 rather than 0 + im.seek(n_frames+1) + break + except EOFError: + self.assertTrue(im.tell() < n_frames) if __name__ == '__main__': diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index d49086c51..e78488913 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import Image +from PIL import Image, SgiImagePlugin class TestFileSgi(PillowTestCase): @@ -32,6 +32,13 @@ class TestFileSgi(PillowTestCase): # Act / Assert self.assertRaises(ValueError, lambda: Image.open(test_file)) + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(ValueError, + lambda: + SgiImagePlugin.SgiImageFile(invalid_file)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 7d24b2fe5..1ddecd365 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -45,6 +45,7 @@ class TestImageSpider(PillowTestCase): def test_n_frames(self): im = Image.open(TEST_FILE) self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) def test_loadImageSeries(self): # Arrange diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 332104062..16c43b921 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import Image +from PIL import Image, SunImagePlugin class TestFileSun(PillowTestCase): @@ -16,6 +16,10 @@ class TestFileSun(PillowTestCase): # Assert self.assertEqual(im.size, (128, 128)) + invalid_file = "Tests/images/flower.jpg" + self.assertRaises(SyntaxError, + lambda: SunImagePlugin.SunImageFile(invalid_file)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index ea94dee64..459e766d5 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -15,6 +15,25 @@ class TestFileTga(PillowTestCase): # Assert self.assertEqual(im.size, (100, 100)) + def test_save(self): + test_file = "Tests/images/tga_id_field.tga" + im = Image.open(test_file) + + test_file = self.tempfile("temp.tga") + + # Save + im.save(test_file) + test_im = Image.open(test_file) + self.assertEqual(test_im.size, (100, 100)) + + # RGBA save + im.convert("RGBA").save(test_file) + test_im = Image.open(test_file) + self.assertEqual(test_im.size, (100, 100)) + + # Unsupported mode save + self.assertRaises(IOError, lambda: im.convert("LA").save(test_file)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 02a63586c..08f1f1880 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,9 +1,12 @@ from __future__ import print_function +import logging +import struct + from helper import unittest, PillowTestCase, hopper, py3 from PIL import Image, TiffImagePlugin -import struct +logger = logging.getLogger(__name__) class TestFileTiff(PillowTestCase): @@ -79,11 +82,25 @@ class TestFileTiff(PillowTestCase): im._setup() self.assertEqual(im.info['dpi'], (72., 72.)) + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: TiffImagePlugin.TiffImageFile(invalid_file)) + + def test_bad_exif(self): try: Image.open('Tests/images/hopper_bad_exif.jpg')._getexif() except struct.error: - self.fail("Bad EXIF data should not pass incorrect values to _binary unpack") + self.fail( + "Bad EXIF data passed incorrect values to _binary unpack") + + def test_save_unsupported_mode(self): + im = hopper("HSV") + outfile = self.tempfile("temp.tif") + + self.assertRaises(IOError, lambda: im.save(outfile)) def test_little_endian(self): im = Image.open('Tests/images/16bit.cropped.tif') @@ -118,7 +135,6 @@ class TestFileTiff(PillowTestCase): """ Are we generating the same interpretation of the image as Imagemagick is? """ - # Image.DEBUG = True im = Image.open('Tests/images/12bit.cropped.tif') # to make the target -- @@ -129,14 +145,8 @@ class TestFileTiff(PillowTestCase): im2 = Image.open('Tests/images/12in16bit.tif') - if Image.DEBUG: - print(im.getpixel((0, 0))) - print(im.getpixel((0, 1))) - print(im.getpixel((0, 2))) - - print(im2.getpixel((0, 0))) - print(im2.getpixel((0, 1))) - print(im2.getpixel((0, 2))) + logger.debug("%s", [img.getpixel((0, idx)) + for img in [im, im2] for idx in range(3)]) self.assert_image_equal(im, im2) @@ -153,9 +163,23 @@ class TestFileTiff(PillowTestCase): def test_n_frames(self): im = Image.open('Tests/images/multipage-lastframe.tif') self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) im = Image.open('Tests/images/multipage.tiff') self.assertEqual(im.n_frames, 3) + self.assertTrue(im.is_animated) + + def test_eoferror(self): + im = Image.open('Tests/images/multipage-lastframe.tif') + + n_frames = im.n_frames + while True: + n_frames -= 1 + try: + im.seek(n_frames) + break + except EOFError: + self.assertTrue(im.tell() < n_frames) def test_multipage(self): # issue #862 diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 8c8313dd9..d1c8e580e 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -72,6 +72,11 @@ class TestFileWebp(PillowTestCase): target = hopper("RGB") self.assert_image_similar(image, target, 12) + def test_write_unsupported_mode(self): + temp_file = self.tempfile("temp.webp") + + self.assertRaises(IOError, lambda: hopper("L").save(temp_file)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index f316b71e1..e7df62ee6 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -83,7 +83,8 @@ class TestFileWebpAlpha(PillowTestCase): image.load() image.getdata() - # early versions of webp are known to produce higher deviations: deal with it + # early versions of webp are known to produce higher deviations: + # deal with it if _webp.WebPDecoderVersion(self) <= 0x201: self.assert_image_similar(image, pil_image, 3.0) else: diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 6a6817048..e589a8d26 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase, hopper -from PIL import Image +from PIL import Image, XpmImagePlugin # sample ppm stream TEST_FILE = "Tests/images/hopper.xpm" @@ -18,6 +18,12 @@ class TestFileXpm(PillowTestCase): # large error due to quantization->44 colors. self.assert_image_similar(im.convert('RGB'), hopper('RGB'), 60) + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: XpmImagePlugin.XpmImageFile(invalid_file)) + def test_load_read(self): # Arrange im = Image.open(TEST_FILE) diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index b844f1228..4d88ae46f 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -15,6 +15,10 @@ class TestFontBdf(PillowTestCase): self.assertIsInstance(font, FontFile.FontFile) self.assertEqual(len([_f for _f in font.glyph if _f]), 190) + def test_invalid_file(self): + with open("Tests/images/flower.jpg", "rb") as fp: + self.assertRaises(SyntaxError, lambda: BdfFontFile.BdfFontFile(fp)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 3cc6afa64..f6dd9265e 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -30,6 +30,10 @@ class TestFontPcf(PillowTestCase): def test_sanity(self): self.save_font() + def test_invalid_file(self): + with open("Tests/images/flower.jpg", "rb") as fp: + self.assertRaises(SyntaxError, lambda: PcfFontFile.PcfFontFile(fp)) + def xtest_draw(self): tempname = self.save_font() diff --git a/Tests/test_image.py b/Tests/test_image.py index 469045909..bd5fd3522 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -30,6 +30,15 @@ class TestImage(PillowTestCase): # self.assertRaises( # MemoryError, lambda: Image.new("L", (1000000, 1000000))) + def test_width_height(self): + im = Image.new("RGB", (1, 2)) + self.assertEqual(im.width, 1) + self.assertEqual(im.height, 2) + + im.size = (3, 4) + self.assertEqual(im.width, 3) + self.assertEqual(im.height, 4) + def test_invalid_image(self): if str is bytes: import StringIO diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 6a694b3ca..f75a1891d 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -88,6 +88,12 @@ class TestImageFilter(PillowTestCase): self.assertEqual(rankfilter("I"), (0, 4, 8)) self.assertEqual(rankfilter("F"), (0.0, 4.0, 8.0)) + def test_rankfilter_properties(self): + rankfilter = ImageFilter.RankFilter(1, 2) + + self.assertEqual(rankfilter.size, 1) + self.assertEqual(rankfilter.rank, 2) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 6441c7d1b..c20db50aa 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -21,12 +21,14 @@ class TestImageMode(PillowTestCase): m = ImageMode.getmode("1") self.assertEqual(m.mode, "1") + self.assertEqual(str(m), "1") self.assertEqual(m.bands, ("1",)) self.assertEqual(m.basemode, "L") self.assertEqual(m.basetype, "L") m = ImageMode.getmode("RGB") self.assertEqual(m.mode, "RGB") + self.assertEqual(str(m), "RGB") self.assertEqual(m.bands, ("R", "G", "B")) self.assertEqual(m.basemode, "RGB") self.assertEqual(m.basetype, "L") diff --git a/Tests/test_image_toqimage.py b/Tests/test_image_toqimage.py index 38b83c023..6c79b4c6d 100644 --- a/Tests/test_image_toqimage.py +++ b/Tests/test_image_toqimage.py @@ -19,7 +19,7 @@ class TestToQImage(PillowQtTestCase, PillowTestCase): self.assertFalse(data.isNull()) # Test saving the file - tempfile = self.tempfile('temp_{}.png'.format(mode)) + tempfile = self.tempfile('temp_{0}.png'.format(mode)) data.save(tempfile) diff --git a/Tests/test_image_toqpixmap.py b/Tests/test_image_toqpixmap.py index 95db2e8f7..8fabab13d 100644 --- a/Tests/test_image_toqpixmap.py +++ b/Tests/test_image_toqpixmap.py @@ -19,7 +19,7 @@ class TestToQPixmap(PillowQPixmapTestCase, PillowTestCase): self.assertFalse(data.isNull()) # Test saving the file - tempfile = self.tempfile('temp_{}.png'.format(mode)) + tempfile = self.tempfile('temp_{0}.png'.format(mode)) data.save(tempfile) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index 5d8944852..a38c8b070 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -18,6 +18,9 @@ class TestImageColor(PillowTestCase): (255, 0, 0, 0), ImageColor.getrgb("rgba(255, 0, 0, 0)")) self.assertEqual((255, 0, 0), ImageColor.getrgb("red")) + self.assertRaises(ValueError, + lambda: ImageColor.getrgb("invalid color")) + # look for rounding errors (based on code by Tim Hatch) def test_rounding_errors(self): @@ -43,8 +46,8 @@ class TestImageColor(PillowTestCase): self.assertEqual(0, ImageColor.getcolor("black", "L")) self.assertEqual(255, ImageColor.getcolor("white", "L")) - self.assertEqual( - 162, ImageColor.getcolor("rgba(0, 255, 115, 33)", "L")) + self.assertEqual(162, + ImageColor.getcolor("rgba(0, 255, 115, 33)", "L")) Image.new("L", (1, 1), "white") self.assertEqual(0, ImageColor.getcolor("black", "1")) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index ffefa6504..803677616 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -52,6 +52,12 @@ class TestImageDraw(PillowTestCase): self.assert_warning(DeprecationWarning, lambda: draw.setink(0)) self.assert_warning(DeprecationWarning, lambda: draw.setfill(0)) + def test_mode_mismatch(self): + im = hopper("RGB").copy() + + self.assertRaises(ValueError, + lambda: ImageDraw.ImageDraw(im, mode="L")) + def helper_arc(self, bbox): # Arrange im = Image.new("RGB", (W, H)) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index bc81575b6..fbd10d47b 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -93,6 +93,9 @@ class TestImageFile(PillowTestCase): self.assert_image_equal(im1, im2) + def test_raise_ioerror(self): + self.assertRaises(IOError, lambda: ImageFile.raise_ioerror(1)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 8414584f9..1fd70b3d8 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -182,7 +182,10 @@ try: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) # Act/Assert - self.assertRaises(AssertionError, lambda: draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align="unknown")) + self.assertRaises(AssertionError, + lambda: draw.multiline_text((0, 0), TEST_TEXT, + font=ttf, + align="unknown")) def test_multiline_size(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -199,7 +202,8 @@ try: draw = ImageDraw.Draw(im) self.assertEqual(draw.textsize("longest line", font=ttf)[0], - draw.multiline_textsize("longest line\nline", font=ttf)[0]) + draw.multiline_textsize("longest line\nline", + font=ttf)[0]) def test_multiline_spacing(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -256,6 +260,34 @@ try: # Check boxes a and b are same size self.assertEqual(box_size_a, box_size_b) + def test_rotated_transposed_font_get_mask(self): + # Arrange + text = "mask this" + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + orientation = Image.ROTATE_90 + transposed_font = ImageFont.TransposedFont( + font, orientation=orientation) + + # Act + mask = transposed_font.getmask(text) + + # Assert + self.assertEqual(mask.size, (13, 108)) + + def test_unrotated_transposed_font_get_mask(self): + # Arrange + text = "mask this" + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + orientation = None + transposed_font = ImageFont.TransposedFont( + font, orientation=orientation) + + # Act + mask = transposed_font.getmask(text) + + # Assert + self.assertEqual(mask.size, (108, 13)) + def test_free_type_font_get_name(self): # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -278,6 +310,28 @@ try: self.assertIsInstance(descent, int) self.assertEqual((ascent, descent), (16, 4)) # too exact check? + def test_free_type_font_get_offset(self): + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + text = "offset this" + + # Act + offset = font.getoffset(text) + + # Assert + self.assertEqual(offset, (0, 3)) + + def test_free_type_font_get_mask(self): + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + text = "mask this" + + # Act + mask = font.getmask(text) + + # Assert + self.assertEqual(mask.size, (108, 13)) + def test_load_path_not_found(self): # Arrange filename = "somefilenamethatdoesntexist.ttf" diff --git a/Tests/test_imagefont_bitmap.py b/Tests/test_imagefont_bitmap.py index 27141f4b3..a9d745b22 100644 --- a/Tests/test_imagefont_bitmap.py +++ b/Tests/test_imagefont_bitmap.py @@ -5,16 +5,19 @@ from PIL import Image, ImageFont, ImageDraw class TestImageFontBitmap(PillowTestCase): def test_similar(self): text = 'EmbeddedBitmap' - font_outline = ImageFont.truetype(font='Tests/fonts/DejaVuSans.ttf', size=24) - font_bitmap = ImageFont.truetype(font='Tests/fonts/DejaVuSans-bitmap.ttf', size=24) + font_outline = ImageFont.truetype( + font='Tests/fonts/DejaVuSans.ttf', size=24) + font_bitmap = ImageFont.truetype( + font='Tests/fonts/DejaVuSans-bitmap.ttf', size=24) size_outline, size_bitmap = font_outline.getsize(text), font_bitmap.getsize(text) size_final = max(size_outline[0], size_bitmap[0]), max(size_outline[1], size_bitmap[1]) im_bitmap = Image.new('RGB', size_final, (255, 255, 255)) im_outline = im_bitmap.copy() draw_bitmap, draw_outline = ImageDraw.Draw(im_bitmap), ImageDraw.Draw(im_outline) - # Metrics are different on the bitmap and ttf fonts, more so on some platforms - # and versions of freetype than others. Mac has a 1px difference, linux doesn't. + # Metrics are different on the bitmap and ttf fonts, + # more so on some platforms and versions of freetype than others. + # Mac has a 1px difference, linux doesn't. draw_bitmap.text((0, size_final[1] - size_bitmap[1]), text, fill=(0, 0, 0), font=font_bitmap) draw_outline.text((0, size_final[1] - size_outline[1]), diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index f6eae640b..89c63c774 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -75,7 +75,9 @@ class TestImageOpsUsm(PillowTestCase): (4, 3, 2), (4, 2, 2)]: self.assertGreaterEqual(i.im.getpixel((x, y))[c], 250) # Fuzzy match. - gp = lambda x, y: i.im.getpixel((x, y)) + + def gp(x, y): + return i.im.getpixel((x, y)) self.assertTrue(236 <= gp(7, 4)[0] <= 239) self.assertTrue(236 <= gp(7, 5)[2] <= 239) self.assertTrue(236 <= gp(7, 6)[2] <= 239) diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 1b4bb3c02..3bd6dc582 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -22,15 +22,14 @@ class TestImageSequence(PillowTestCase): self.assertEqual(index, 1) + self.assertRaises(AttributeError, lambda: ImageSequence.Iterator(0)) + def _test_multipage_tiff(self, dbg=False): - # debug had side effect of calling fp.tell. - Image.DEBUG = dbg im = Image.open('Tests/images/multipage.tiff') for index, frame in enumerate(ImageSequence.Iterator(im)): frame.load() self.assertEqual(index, im.tell()) frame.convert('RGB') - Image.DEBUG = False def test_tiff(self): # self._test_multipage_tiff(True) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 7ceea86ee..8a5bc125f 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -115,7 +115,8 @@ class TestImageWinDib(PillowTestCase): # Act/Assert self.assert_warning(DeprecationWarning, dib.tostring) - self.assert_warning(DeprecationWarning, lambda: dib.fromstring(test_buffer)) + self.assert_warning(DeprecationWarning, + lambda: dib.fromstring(test_buffer)) if __name__ == '__main__': diff --git a/Tests/test_olefileio.py b/Tests/test_olefileio.py index 1cff273a1..d3842e977 100644 --- a/Tests/test_olefileio.py +++ b/Tests/test_olefileio.py @@ -7,25 +7,20 @@ import PIL.OleFileIO as OleFileIO class TestOleFileIo(PillowTestCase): - def test_isOleFile_false(self): - # Arrange - non_ole_file = "Tests/images/flower.jpg" - - # Act - is_ole = OleFileIO.isOleFile(non_ole_file) - - # Assert - self.assertFalse(is_ole) - - def test_isOleFile_true(self): - # Arrange + def test_isOleFile(self): ole_file = "Tests/images/test-ole-file.doc" - # Act - is_ole = OleFileIO.isOleFile(ole_file) + self.assertTrue(OleFileIO.isOleFile(ole_file)) + with open(ole_file, 'rb') as fp: + self.assertTrue(OleFileIO.isOleFile(fp)) + self.assertTrue(OleFileIO.isOleFile(fp.read())) - # Assert - self.assertTrue(is_ole) + non_ole_file = "Tests/images/flower.jpg" + + self.assertFalse(OleFileIO.isOleFile(non_ole_file)) + with open(non_ole_file, 'rb') as fp: + self.assertFalse(OleFileIO.isOleFile(fp)) + self.assertFalse(OleFileIO.isOleFile(fp.read())) def test_exists_worddocument(self): # Arrange diff --git a/Tests/test_scipy.py b/Tests/test_scipy.py index 1632d9475..1d7568148 100644 --- a/Tests/test_scipy.py +++ b/Tests/test_scipy.py @@ -30,10 +30,10 @@ class Test_scipy_resize(PillowTestCase): def test_imresize4(self): im = np.array([[1, 2], [3, 4]]) - res = np.array([[1. , 1.25, 1.75, 2. ], + res = np.array([[1., 1.25, 1.75, 2.], [1.5, 1.75, 2.25, 2.5], [2.5, 2.75, 3.25, 3.5], - [3. , 3.25, 3.75, 4. ]], dtype=np.float32) + [3., 3.25, 3.75, 4.]], dtype=np.float32) # Check that resizing by target size, float and int are the same im2 = misc.imresize(im, (4, 4), mode='F') # output size im3 = misc.imresize(im, 2., mode='F') # fraction diff --git a/_imaging.c b/_imaging.c index 09345c0dd..895faac54 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.9.0.dev0" +#define PILLOW_VERSION "3.0.0.dev0" #include "Python.h" diff --git a/appveyor.yml b/appveyor.yml index 868f34f7e..6e953c46e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,4 @@ -version: 2.9.pre.{build} -shallow_clone: true +version: 3.0.pre.{build} clone_folder: c:\pillow init: - ECHO %PYTHON% @@ -25,3 +24,5 @@ test_script: - cd c:\pillow - '%PYTHON%\Scripts\pip.exe install nose' - '%PYTHON%\python.exe test-installed.py' +matrix: + fast_finish: true diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index ed0c3ae5b..11ec60401 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -77,8 +77,8 @@ Reading sequences ~~~~~~~~~~~~~~~~~ The GIF loader supports the :py:meth:`~file.seek` and :py:meth:`~file.tell` -methods. You can seek to the next frame (``im.seek(im.tell() + 1``), or rewind -the file by seeking to the first frame. Random access is not supported. +methods. You can seek to the next frame (``im.seek(im.tell() + 1)``), or rewind +the file by seeking to the first frame. Random access is not supported. ``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame. Reading local images ~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 974d84a6e..ac8b6f506 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -181,6 +181,18 @@ Instances of the :py:class:`Image` class have the following attributes: :type: ``(width, height)`` +.. py:attribute:: width + + Image width, in pixels. + + :type: :py:class:`int` + +.. py:attribute:: height + + Image height, in pixels. + + :type: :py:class:`int` + .. py:attribute:: palette Colour palette table, if any. If mode is ā€œPā€, this should be an instance of diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index e030147e9..1c9c242c7 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -234,16 +234,17 @@ Methods :param xy: Top left corner of the text. :param text: Text to be drawn. If it contains any newline characters, the text is passed on to mulitiline_text() - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param fill: Color to use for the text. + :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + .. py:method:: PIL.ImageDraw.Draw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left") Draws the string at the given position. :param xy: Top left corner of the text. - :param text: Text to be drawn. If it contains any newline characters, - the text is split and passed on to mulitiline_text() + :param text: Text to be drawn. + :param fill: Color to use for the text. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param spacing: The number of pixels between lines. :param align: "left", "center" or "right". @@ -260,8 +261,7 @@ Methods Return the size of the given string, in pixels. - :param text: Text to be measured. If it contains any newline characters, - the text is split and passed on to mulitiline_textsize() + :param text: Text to be measured. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param spacing: The number of pixels between lines. diff --git a/setup.py b/setup.py index f669873d6..edfd24c41 100644 --- a/setup.py +++ b/setup.py @@ -90,7 +90,7 @@ except (ImportError, OSError): NAME = 'Pillow' -PILLOW_VERSION = '2.9.0.dev0' +PILLOW_VERSION = '3.0.0.dev0' TCL_ROOT = None JPEG_ROOT = None JPEG2K_ROOT = None @@ -464,11 +464,11 @@ class pil_build_ext(build_ext): if _find_library_file(self, "lcms2"): feature.lcms = "lcms2" elif _find_library_file(self, "lcms2_static"): - #alternate Windows name. + # alternate Windows name. feature.lcms = "lcms2_static" if _tkinter and _find_include_file(self, "tk.h"): - # the library names may vary somewhat (e.g. tcl84 or tcl8.4) + # the library names may vary somewhat (e.g. tcl85 or tcl8.5) version = TCL_VERSION[0] + TCL_VERSION[2] if feature.want('tcl'): if _find_library_file(self, "tcl" + version): @@ -571,32 +571,33 @@ class pil_build_ext(build_ext): exts.append(Extension( "PIL._webp", ["_webp.c"], libraries=libs, define_macros=defs)) - if sys.platform == "darwin": - # locate Tcl/Tk frameworks - frameworks = [] - framework_roots = [ - "/Library/Frameworks", - "/System/Library/Frameworks"] - for root in framework_roots: - if ( - os.path.exists(os.path.join(root, "Tcl.framework")) and - os.path.exists(os.path.join(root, "Tk.framework"))): - print("--- using frameworks at %s" % root) - frameworks = ["-framework", "Tcl", "-framework", "Tk"] - dir = os.path.join(root, "Tcl.framework", "Headers") - _add_directory(self.compiler.include_dirs, dir, 0) - dir = os.path.join(root, "Tk.framework", "Headers") - _add_directory(self.compiler.include_dirs, dir, 1) - break - if frameworks: + if feature.tcl and feature.tk: + if sys.platform == "darwin": + # locate Tcl/Tk frameworks + frameworks = [] + framework_roots = [ + "/Library/Frameworks", + "/System/Library/Frameworks"] + for root in framework_roots: + root_tcl = os.path.join(root, "Tcl.framework") + root_tk = os.path.join(root, "Tk.framework") + if (os.path.exists(root_tcl) and os.path.exists(root_tk)): + print("--- using frameworks at %s" % root) + frameworks = ["-framework", "Tcl", "-framework", "Tk"] + dir = os.path.join(root_tcl, "Headers") + _add_directory(self.compiler.include_dirs, dir, 0) + dir = os.path.join(root_tk, "Headers") + _add_directory(self.compiler.include_dirs, dir, 1) + break + if frameworks: + exts.append(Extension( + "PIL._imagingtk", ["_imagingtk.c", "Tk/tkImaging.c"], + extra_compile_args=frameworks, + extra_link_args=frameworks)) + else: exts.append(Extension( "PIL._imagingtk", ["_imagingtk.c", "Tk/tkImaging.c"], - extra_compile_args=frameworks, extra_link_args=frameworks)) - feature.tcl = feature.tk = 1 # mark as present - elif feature.tcl and feature.tk: - exts.append(Extension( - "PIL._imagingtk", ["_imagingtk.c", "Tk/tkImaging.c"], - libraries=[feature.tcl, feature.tk])) + libraries=[feature.tcl, feature.tk])) if os.path.isfile("_imagingmath.c"): exts.append(Extension("PIL._imagingmath", ["_imagingmath.c"])) diff --git a/tox.ini b/tox.ini index 80f7edef4..ebc63d85a 100644 --- a/tox.ini +++ b/tox.ini @@ -11,4 +11,4 @@ commands = {envpython} setup.py clean {envpython} setup.py build_ext --inplace {envpython} selftest.py - {envpython} Tests/run.py --installed + {envpython} test-installed.py --installed diff --git a/winbuild/build.py b/winbuild/build.py index 9e5e0ea1c..bdede470a 100644 --- a/winbuild/build.py +++ b/winbuild/build.py @@ -13,8 +13,8 @@ def setup_vms(): ret = [] for py in pythons.keys(): for arch in ('', X64_EXT): - ret.append("virtualenv -p c:/Python%s%s/python.exe --clear %s%s%s" % - (py, arch, VIRT_BASE, py, arch)) + ret.append("virtualenv -p c:/Python%s%s/python.exe --clear %s%s%s" + % (py, arch, VIRT_BASE, py, arch)) ret.append("%s%s%s\Scripts\pip.exe install nose" % (VIRT_BASE, py, arch)) if py == '26': @@ -109,13 +109,15 @@ def main(op): scripts.append((py_version, "\n".join([header(op), build_one(py_version, - compilers[(compiler_version, 32)]), + compilers[(compiler_version, + 32)]), footer()]))) scripts.append(("%s%s" % (py_version, X64_EXT), "\n".join([header(op), build_one("%sx64" % py_version, - compilers[(compiler_version, 64)]), + compilers[(compiler_version, + 64)]), footer()]))) results = map(run_script, scripts) diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index a837b31f5..09587c26c 100644 --- a/winbuild/build_dep.py +++ b/winbuild/build_dep.py @@ -4,7 +4,7 @@ from untar import untar import os import hashlib -from config import * +from config import bin_libs, compilers, compiler_fromEnv, libs def _relpath(*args): @@ -64,10 +64,12 @@ def fetch_libs(): if name == 'openjpeg': filename = check_hash(fetch(lib['url']), lib['hash']) for compiler in compilers.values(): - if not os.path.exists(os.path.join(build_dir, lib['dir']+compiler['inc_dir'])): + if not os.path.exists(os.path.join( + build_dir, lib['dir']+compiler['inc_dir'])): extract(filename, build_dir) os.rename(os.path.join(build_dir, lib['dir']), - os.path.join(build_dir, lib['dir']+compiler['inc_dir'])) + os.path.join( + build_dir, lib['dir']+compiler['inc_dir'])) else: extract(check_hash(fetch(lib['url']), lib['hash']), build_dir) @@ -91,18 +93,19 @@ endlocal """ % compiler -def cp_tk(): +def cp_tk(ver_85, ver_86): + versions = {'ver_85':ver_85, 'ver_86':ver_86} return r""" -mkdir %INCLIB%\tcl85\include\X11 -copy /Y /B %BUILD%\tcl8.5.13\generic\*.h %INCLIB%\tcl85\include\ -copy /Y /B %BUILD%\tk8.5.13\generic\*.h %INCLIB%\tcl85\include\ -copy /Y /B %BUILD%\tk8.5.13\xlib\X11\* %INCLIB%\tcl85\include\X11\ +mkdir %%INCLIB%%\tcl85\include\X11 +copy /Y /B %%BUILD%%\tcl%(ver_85)s\generic\*.h %%INCLIB%%\tcl85\include\ +copy /Y /B %%BUILD%%\tk%(ver_85)s\generic\*.h %%INCLIB%%\tcl85\include\ +copy /Y /B %%BUILD%%\tk%(ver_85)s\xlib\X11\* %%INCLIB%%\tcl85\include\X11\ -mkdir %INCLIB%\tcl86\include\X11 -copy /Y /B %BUILD%\tcl8.6.4\generic\*.h %INCLIB%\tcl86\include\ -copy /Y /B %BUILD%\tk8.6.4\generic\*.h %INCLIB%\tcl86\include\ -copy /Y /B %BUILD%\tk8.6.4\xlib\X11\* %INCLIB%\tcl86\include\X11\ -""" +mkdir %%INCLIB%%\tcl86\include\X11 +copy /Y /B %%BUILD%%\tcl%(ver_86)s\generic\*.h %%INCLIB%%\tcl86\include\ +copy /Y /B %%BUILD%%\tk%(ver_86)s\generic\*.h %%INCLIB%%\tcl86\include\ +copy /Y /B %%BUILD%%\tk%(ver_86)s\xlib\X11\* %%INCLIB%%\tcl86\include\X11\ +""" % versions def header(): @@ -305,7 +308,7 @@ def add_compiler(compiler): mkdirs() fetch_libs() # extract_binlib() -script = [header(), cp_tk()] +script = [header(), cp_tk(libs['tk-8.5']['version'],libs['tk-8.6']['version'] )] if 'PYTHON' in os.environ: diff --git a/winbuild/config.py b/winbuild/config.py index ffd8079fe..d3d2f97d7 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -11,108 +11,108 @@ pythons = {#'26': 7, VIRT_BASE = "c:/vp/" X64_EXT = os.environ.get('X64_EXT', "x64") -libs = {'zlib': { - 'url': 'http://zlib.net/zlib128.zip', - 'hash': 'md5:126f8676442ffbd97884eb4d6f32afb4', - 'dir': 'zlib-1.2.8', +libs = { + 'zlib': { + 'url': 'http://zlib.net/zlib128.zip', + 'hash': 'md5:126f8676442ffbd97884eb4d6f32afb4', + 'dir': 'zlib-1.2.8', }, - 'jpeg': { - 'url': 'http://www.ijg.org/files/jpegsr9a.zip', - 'hash': 'md5:a34f3c82760270ee1e1885b15b90a72e', # not found - generated by wiredfool - 'dir': 'jpeg-9a', + 'jpeg': { + 'url': 'http://www.ijg.org/files/jpegsr9a.zip', + 'hash': 'md5:a34f3c82760270ee1e1885b15b90a72e', # not found - generated by wiredfool + 'dir': 'jpeg-9a', }, - 'tiff': { - 'url': 'ftp://ftp.remotesensing.org/pub/libtiff/tiff-4.0.3.zip', - 'hash': 'md5:dd70349cedb3981371686e1c9b89a7f9', # not found - generated by wiredfool - 'dir': 'tiff-4.0.3', - }, - 'freetype': { - 'url': 'http://download.savannah.gnu.org/releases/freetype/freetype-2.6.tar.gz', - 'hash': 'md5:1d733ea6c1b7b3df38169fbdbec47d2b', - 'dir': 'freetype-2.6', + 'tiff': { + 'url': 'ftp://ftp.remotesensing.org/pub/libtiff/tiff-4.0.4.zip', + 'hash': 'md5:8f538a34156188f9a8dcddb679c65d1e', + 'dir': 'tiff-4.0.4', }, - 'lcms': { - 'url': SF_MIRROR+'/project/lcms/lcms/2.7/lcms2-2.7.zip', - 'hash': 'sha1:7ff1a5b721ca719760ba6eb4ec6f38d5e65381cf', - 'dir': 'lcms2-2.7', - }, - 'tcl-8.5': { - 'url': SF_MIRROR+'/project/tcl/Tcl/8.5.13/tcl8513-src.zip', - 'hash': 'sha1:3e01585c91293c532a3cd594ec59deca92153a5e', - 'dir': '', - }, - 'tk-8.5': { - 'url': SF_MIRROR+'/project/tcl/Tcl/8.5.13/tk8513-src.zip', - 'hash': 'sha1:23a1d7ddd416e11e06dfdb9f86111d4bab9420b4', - 'dir': '', + 'freetype': { + 'url': 'http://download.savannah.gnu.org/releases/freetype/freetype-2.6.tar.gz', + 'hash': 'md5:1d733ea6c1b7b3df38169fbdbec47d2b', + 'dir': 'freetype-2.6', }, - 'tcl-8.6': { - 'url': SF_MIRROR+'/project/tcl/Tcl/8.6.4/tcl864-src.zip', - 'hash': 'md5:35748d2fc61e08a2fdb23b85c6f8c4a0', - 'dir': '', - }, - 'tk-8.6': { - 'url': SF_MIRROR+'/project/tcl/Tcl/8.6.4/tk864-src.zip', - 'hash': 'md5:111d45061a69e7f5250b6ec8ca7c4f35', - 'dir': '', + 'lcms': { + 'url': SF_MIRROR+'/project/lcms/lcms/2.7/lcms2-2.7.zip', + 'hash': 'sha1:7ff1a5b721ca719760ba6eb4ec6f38d5e65381cf', + 'dir': 'lcms2-2.7', }, - 'webp': { - 'url': 'http://downloads.webmproject.org/releases/webp/libwebp-0.4.3.tar.gz', - 'hash': 'sha1:1c307a61c4d0018620b4ba9a58e8f48a8d6640ef', - 'dir': 'libwebp-0.4.3', - + 'tcl-8.5': { + 'url': SF_MIRROR+'/project/tcl/Tcl/8.5.18/tcl8518-src.zip', + 'hash': 'sha1:4c2aed9043088c630a4c795265e2738ef1b4db3b', + 'dir': '', }, - 'openjpeg': { - 'url': SF_MIRROR+'/project/openjpeg/openjpeg/2.1.0/openjpeg-2.1.0.tar.gz', - 'hash': 'md5:f6419fcc233df84f9a81eb36633c6db6', - 'dir': 'openjpeg-2.1.0', + 'tk-8.5': { + 'url': SF_MIRROR+'/project/tcl/Tcl/8.5.18/tk8518-src.zip', + 'hash': 'sha1:273f55148777413774aa722ecad25cabda1e31ae', + 'dir': '', + 'version':'8.5.18', }, - - } + 'tcl-8.6': { + 'url': SF_MIRROR+'/project/tcl/Tcl/8.6.4/tcl864-src.zip', + 'hash': 'md5:35748d2fc61e08a2fdb23b85c6f8c4a0', + 'dir': '', + }, + 'tk-8.6': { + 'url': SF_MIRROR+'/project/tcl/Tcl/8.6.4/tk864-src.zip', + 'hash': 'md5:111d45061a69e7f5250b6ec8ca7c4f35', + 'dir': '', + 'version':'8.6.4', + }, + 'webp': { + 'url': 'http://downloads.webmproject.org/releases/webp/libwebp-0.4.3.tar.gz', + 'hash': 'sha1:1c307a61c4d0018620b4ba9a58e8f48a8d6640ef', + 'dir': 'libwebp-0.4.3', + }, + 'openjpeg': { + 'url': SF_MIRROR+'/project/openjpeg/openjpeg/2.1.0/openjpeg-2.1.0.tar.gz', + 'hash': 'md5:f6419fcc233df84f9a81eb36633c6db6', + 'dir': 'openjpeg-2.1.0', + }, +} bin_libs = { - 'openjpeg': { - 'filename': 'openjpeg-2.0.0-win32-x86.zip', - 'hash': 'sha1:xxx', - 'version': '2.0' - }, - } - -compilers = {(7, 64): { - 'env_version': 'v7.0', - 'vc_version': '2008', - 'env_flags': '/x64 /xp', - 'inc_dir': 'msvcr90-x64', - 'platform': 'x64', - 'webp_platform': 'x64', + 'openjpeg': { + 'filename': 'openjpeg-2.0.0-win32-x86.zip', + 'hash': 'sha1:xxx', + 'version': '2.0' }, - (7, 32): { - 'env_version': 'v7.0', - 'vc_version': '2008', - 'env_flags': '/x86 /xp', - 'inc_dir': 'msvcr90-x32', - 'platform': 'Win32', - 'webp_platform': 'x86', - }, +} - (7.1, 64): { - 'env_version': 'v7.1', - 'vc_version': '2010', - 'env_flags': '/x64 /vista', - 'inc_dir': 'msvcr10-x64', - 'platform': 'x64', - 'webp_platform': 'x64', - }, - (7.1, 32): { - 'env_version': 'v7.1', - 'vc_version': '2010', - 'env_flags': '/x86 /vista', - 'inc_dir': 'msvcr10-x32', - 'platform': 'Win32', - 'webp_platform': 'x86', +compilers = { + (7, 64): { + 'env_version': 'v7.0', + 'vc_version': '2008', + 'env_flags': '/x64 /xp', + 'inc_dir': 'msvcr90-x64', + 'platform': 'x64', + 'webp_platform': 'x64', }, - - } + (7, 32): { + 'env_version': 'v7.0', + 'vc_version': '2008', + 'env_flags': '/x86 /xp', + 'inc_dir': 'msvcr90-x32', + 'platform': 'Win32', + 'webp_platform': 'x86', + }, + (7.1, 64): { + 'env_version': 'v7.1', + 'vc_version': '2010', + 'env_flags': '/x64 /vista', + 'inc_dir': 'msvcr10-x64', + 'platform': 'x64', + 'webp_platform': 'x64', + }, + (7.1, 32): { + 'env_version': 'v7.1', + 'vc_version': '2010', + 'env_flags': '/x86 /vista', + 'inc_dir': 'msvcr10-x32', + 'platform': 'Win32', + 'webp_platform': 'x86', + }, +} def pyversion_fromEnv(): diff --git a/winbuild/get_pythons.py b/winbuild/get_pythons.py index 04d8591ac..8ac3b1d4a 100644 --- a/winbuild/get_pythons.py +++ b/winbuild/get_pythons.py @@ -5,8 +5,8 @@ if __name__ == '__main__': for version in ['2.6.5', '2.7.6', '3.2.5', '3.3.5', '3.4.3']: for platform in ['', '.amd64']: for extension in ['', '.asc']: - fetch('https://www.python.org/ftp/python/%s/python-%s%s.msi%s' % ( - version, version, platform, extension)) + fetch('https://www.python.org/ftp/python/%s/python-%s%s.msi%s' + % (version, version, platform, extension)) # find pip, if it's not in the path! os.system('pip install virtualenv') diff --git a/winbuild/test.py b/winbuild/test.py index 3310fb556..84e071308 100644 --- a/winbuild/test.py +++ b/winbuild/test.py @@ -5,14 +5,15 @@ import os import glob import sys -from config import * +from config import pythons, VIRT_BASE, X64_EXT def test_one(params): python, architecture = params try: print("Running: %s, %s" % params) - command = [r'%s\%s%s\Scripts\python.exe' % (VIRT_BASE, python, architecture), + command = [r'%s\%s%s\Scripts\python.exe' % + (VIRT_BASE, python, architecture), 'test-installed.py', '--processes=-0', '--process-timeout=30',