From adfbe8323aa9daf453872fd60561a865b95beb24 Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 22 Apr 2014 08:54:16 +0300 Subject: [PATCH 01/10] Tests and partial implementation of pickling (#629) --- PIL/Image.py | 47 ++++++++++++++++++++------------- Tests/test_pickle.py | 63 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 19 deletions(-) create mode 100644 Tests/test_pickle.py diff --git a/PIL/Image.py b/PIL/Image.py index 333397701..c74971b1d 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -101,7 +101,7 @@ import collections import numbers # works everywhere, win for pypy, not cpython -USE_CFFI_ACCESS = hasattr(sys, 'pypy_version_info') +USE_CFFI_ACCESS = hasattr(sys, 'pypy_version_info') try: import cffi HAS_CFFI=True @@ -233,7 +233,7 @@ _MODE_CONV = { "CMYK": ('|u1', 4), "YCbCr": ('|u1', 3), "LAB": ('|u1', 3), # UNDONE - unsigned |u1i1i1 - # I;16 == I;16L, and I;32 == I;32L + # I;16 == I;16L, and I;32 == I;32L "I;16": ('u2', None), "I;16L": (' 8bit images. + # a gamma function point transform on > 8bit images. scale, offset = _getscaleoffset(lut) return self._new(self.im.point_transform(scale, offset)) # for other modes, convert the function to a table @@ -1420,8 +1429,8 @@ class Image: self._copy() self.pyaccess = None self.load() - - if self.pyaccess: + + if self.pyaccess: return self.pyaccess.putpixel(xy,value) return self.im.putpixel(xy, value) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py new file mode 100644 index 000000000..a40d26088 --- /dev/null +++ b/Tests/test_pickle.py @@ -0,0 +1,63 @@ +from tester import * + +from PIL import Image + + +def test_frombytes_tobytes(): + # Arrange + im = Image.open('Images/lena.jpg') + + # Act + data = im.tobytes() + new_im = Image.frombytes(im.mode, im.size, data) + + # Assert + assert_image_equal(im, new_im) + + +def helper_test_pickle_file(pickle, protocol=0): + im = Image.open('Images/lena.jpg') + filename = tempfile('temp.pkl') + + # Act + with open(filename, 'wb') as f: + pickle.dump(im, f, protocol) + with open(filename, 'rb') as f: + loaded_im = pickle.load(f) + + # Assert + assert_image_equal(im, loaded_im) + + +def helper_test_pickle_string(pickle, protocol=0): + im = Image.open('Images/lena.jpg') + + # Act + dumped_string = pickle.dumps(im, protocol) + loaded_im = pickle.loads(dumped_string) + + # Assert + assert_image_equal(im, loaded_im) + + +def test_pickle_image(): + # Arrange + import pickle + + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + helper_test_pickle_string(pickle, protocol) + helper_test_pickle_file(pickle, protocol) + + +def test_cpickle_image(): + # Arrange + import cPickle + + # Act / Assert + for protocol in range(0, cPickle.HIGHEST_PROTOCOL + 1): + helper_test_pickle_string(cPickle, protocol) + helper_test_pickle_file(cPickle, protocol) + + +# End of file From a3edb45f08560f5661034b62a53678270b7465bf Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 22 Apr 2014 09:23:34 +0300 Subject: [PATCH 02/10] pep8 --- PIL/Image.py | 166 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 103 insertions(+), 63 deletions(-) diff --git a/PIL/Image.py b/PIL/Image.py index c74971b1d..d7db1fe1e 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -30,6 +30,7 @@ from PIL import VERSION, PILLOW_VERSION, _plugins import warnings + class _imaging_not_installed: # module placeholder def __getattr__(self, id): @@ -52,8 +53,8 @@ try: # directly; import Image and use the Image.core variable instead. from PIL import _imaging as core if PILLOW_VERSION != getattr(core, 'PILLOW_VERSION', None): - raise ImportError("The _imaging extension was built for another " - " version of Pillow or PIL") + raise ImportError("The _imaging extension was built for another " + " version of Pillow or PIL") except ImportError as v: core = _imaging_not_installed() @@ -94,7 +95,8 @@ from PIL import ImageMode from PIL._binary import i8, o8 from PIL._util import isPath, isStringType, deferred_error -import os, sys +import os +import sys # type stuff import collections @@ -104,9 +106,10 @@ import numbers USE_CFFI_ACCESS = hasattr(sys, 'pypy_version_info') try: import cffi - HAS_CFFI=True + HAS_CFFI = True except: - HAS_CFFI=False + HAS_CFFI = False + def isImageType(t): """ @@ -148,16 +151,16 @@ MESH = 4 # resampling filters NONE = 0 NEAREST = 0 -ANTIALIAS = 1 # 3-lobed lanczos +ANTIALIAS = 1 # 3-lobed lanczos LINEAR = BILINEAR = 2 CUBIC = BICUBIC = 3 # dithers NONE = 0 NEAREST = 0 -ORDERED = 1 # Not yet implemented -RASTERIZE = 2 # Not yet implemented -FLOYDSTEINBERG = 3 # default +ORDERED = 1 # Not yet implemented +RASTERIZE = 2 # Not yet implemented +FLOYDSTEINBERG = 3 # default # palettes/quantizers WEB = 0 @@ -222,7 +225,7 @@ else: _MODE_CONV = { # official modes - "1": ('|b1', None), # broken + "1": ('|b1', None), # broken "L": ('|u1', None), "I": (_ENDIAN + 'i4', None), "F": (_ENDIAN + 'f4', None), @@ -232,8 +235,8 @@ _MODE_CONV = { "RGBA": ('|u1', 4), "CMYK": ('|u1', 4), "YCbCr": ('|u1', 3), - "LAB": ('|u1', 3), # UNDONE - unsigned |u1i1i1 - # I;16 == I;16L, and I;32 == I;32L + "LAB": ('|u1', 3), # UNDONE - unsigned |u1i1i1 + # I;16 == I;16L, and I;32 == I;32L "I;16": ('u2', None), "I;16L": (' size[0]: y = int(max(y * size[0] / x, 1)); x = int(size[0]) - if y > size[1]: x = int(max(x * size[1] / y, 1)); y = int(size[1]) + if x > size[0]: + y = int(max(y * size[0] / x, 1)) + x = int(size[0]) + if y > size[1]: + x = int(max(x * size[1] / y, 1)) + y = int(size[1]) size = x, y if size == self.size: @@ -1725,7 +1751,7 @@ class Image: except ValueError: if resample != ANTIALIAS: raise - im = self.resize(size, NEAREST) # fallback + im = self.resize(size, NEAREST) # fallback self.im = im.im self.mode = im.mode @@ -1761,7 +1787,8 @@ class Image: """ if self.mode == 'RGBA': - return self.convert('RGBa').transform(size, method, data, resample, fill).convert('RGBA') + return self.convert('RGBa').transform( + size, method, data, resample, fill).convert('RGBA') if isinstance(method, ImageTransformHandler): return method.transform(size, self, resample=resample, fill=fill) @@ -1808,8 +1835,13 @@ class Image: elif method == QUAD: # quadrilateral warp. data specifies the four corners # given as NW, SW, SE, and NE. - nw = data[0:2]; sw = data[2:4]; se = data[4:6]; ne = data[6:8] - x0, y0 = nw; As = 1.0 / w; At = 1.0 / h + nw = data[0:2] + sw = data[2:4] + se = data[4:6] + ne = data[6:8] + x0, y0 = nw + As = 1.0 / w + At = 1.0 / h data = (x0, (ne[0]-x0)*As, (sw[0]-x0)*At, (se[0]-sw[0]-ne[0]+x0)*As*At, y0, (ne[1]-y0)*As, (sw[1]-y0)*At, @@ -1843,6 +1875,7 @@ class Image: im = self.im.transpose(method) return self._new(im) + # -------------------------------------------------------------------- # Lazy operations @@ -1878,6 +1911,7 @@ class _ImageCrop(Image): # FIXME: future versions should optimize crop/paste # sequences! + # -------------------------------------------------------------------- # Abstract handlers. @@ -1885,10 +1919,12 @@ class ImagePointHandler: # used as a mixin by point transforms (for use with im.point) pass + class ImageTransformHandler: # used as a mixin by geometry transforms (for use with im.transform) pass + # -------------------------------------------------------------------- # Factories @@ -1964,6 +2000,7 @@ def frombytes(mode, size, data, decoder_name="raw", *args): im.frombytes(data, decoder_name, args) return im + def fromstring(*args, **kw): """Deprecated alias to frombytes. @@ -2026,9 +2063,9 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): " frombuffer(mode, size, data, 'raw', mode, 0, 1)", RuntimeWarning, stacklevel=2 ) - args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 + args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 if args[0] in _MAPMODES: - im = new(mode, (1,1)) + im = new(mode, (1, 1)) im = im._new( core.map_buffer(data, size, decoder_name, None, 0, args) ) @@ -2168,6 +2205,7 @@ def open(fp, mode="r"): raise IOError("cannot identify image file %r" % (filename if filename else fp)) + # # Image processing. @@ -2266,6 +2304,7 @@ def merge(mode, bands): im.putband(bands[i].im, i) return bands[0]._new(im) + # -------------------------------------------------------------------- # Plugin registry @@ -2324,6 +2363,7 @@ def _show(image, **options): # override me, as necessary _showxv(image, **options) + def _showxv(image, title=None, **options): from PIL import ImageShow ImageShow.show(image, title, **options) From e4185694a2d900fed994b0fede818d98683c3021 Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 22 Apr 2014 09:30:15 +0300 Subject: [PATCH 03/10] P3 will skip explicit cPickle tests --- Tests/test_pickle.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index a40d26088..b52932757 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -52,12 +52,14 @@ def test_pickle_image(): def test_cpickle_image(): # Arrange - import cPickle + try: + import cPickle + except ImportError: + skip() # Act / Assert for protocol in range(0, cPickle.HIGHEST_PROTOCOL + 1): helper_test_pickle_string(cPickle, protocol) helper_test_pickle_file(cPickle, protocol) - # End of file From 6802c12f89a7cea013febe4fd4d53327d1c94ed9 Mon Sep 17 00:00:00 2001 From: hugovk Date: Fri, 25 Apr 2014 09:01:16 +0300 Subject: [PATCH 04/10] Initialise object when unpickling --- PIL/Image.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/PIL/Image.py b/PIL/Image.py index d7db1fe1e..40caedbe5 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -585,9 +585,16 @@ class Image: return [self.mode, self.size, self.tobytes()] def __setstate__(self, state): + self.category = NORMAL + self.info = {} + self.palette = None + self.pyaccess = None + self.readonly = 0 + self.tile = [] mode, size, data = state self.mode = mode self.size = size + self.im = core.new(mode, size) self.frombytes(data) def tobytes(self, encoder_name="raw", *args): From 6c938b784b55fd4858384172e8a8d4bb98191e6d Mon Sep 17 00:00:00 2001 From: hugovk Date: Sat, 26 Apr 2014 17:18:29 +0300 Subject: [PATCH 05/10] Remove duplication by calling __init__() (Suggested by @ulope: https://github.com/hugovk/Pillow/commit/6802c12f89a7cea013febe4fd4d53327d1c94ed9#commitcomment-6125853) --- PIL/Image.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/PIL/Image.py b/PIL/Image.py index 40caedbe5..889e92303 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -585,11 +585,7 @@ class Image: return [self.mode, self.size, self.tobytes()] def __setstate__(self, state): - self.category = NORMAL - self.info = {} - self.palette = None - self.pyaccess = None - self.readonly = 0 + Image.__init__(self) self.tile = [] mode, size, data = state self.mode = mode From 2a6f2c5442d1095ed755d4fa699169f68bfceb1e Mon Sep 17 00:00:00 2001 From: hugovk Date: Sat, 26 Apr 2014 19:43:53 +0300 Subject: [PATCH 06/10] Add __eq__ and __ne__ to Image to be able to test image equality when pickling. Pickle more data. --- PIL/Image.py | 37 ++++++++++++++++++++++++++++++------- Tests/test_pickle.py | 33 +++++++++++++++++++-------------- Tests/tester.py | 3 ++- 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/PIL/Image.py b/PIL/Image.py index 889e92303..8d97e1221 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -563,6 +563,21 @@ class Image: self.save(file, format) return file + def __eq__(self, other): + a = (self.mode == other.mode) + b = (self.size == other.size) + c = (self.getpalette() == other.getpalette()) + d = (self.info == other.info) + e = (self.category == other.category) + f = (self.readonly == other.readonly) + g = (self.pyaccess == other.pyaccess) + h = (self.tobytes() == other.tobytes()) + return a and b and c and d and e and f and g and h + + def __ne__(self, other): + eq = (self == other) + return not eq + def __repr__(self): return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( self.__class__.__module__, self.__class__.__name__, @@ -582,15 +597,23 @@ class Image: raise AttributeError(name) def __getstate__(self): - return [self.mode, self.size, self.tobytes()] + return [ + self.info, + self.mode, + self.size, + self.getpalette(), + self.tobytes()] def __setstate__(self, state): Image.__init__(self) self.tile = [] - mode, size, data = state + info, mode, size, palette, data = state + self.info = info self.mode = mode self.size = size self.im = core.new(mode, size) + if mode in ("L", "P"): + self.putpalette(palette) self.frombytes(data) def tobytes(self, encoder_name="raw", *args): @@ -870,7 +893,7 @@ class Image: new_im = self._new(im) if delete_trns: - #crash fail if we leave a bytes transparency in an rgb/l mode. + # crash fail if we leave a bytes transparency in an rgb/l mode. del(new_im.info['transparency']) if trns is not None: if new_im.mode == 'P': @@ -2188,8 +2211,8 @@ def open(fp, mode="r"): fp.seek(0) return factory(fp, filename) except (SyntaxError, IndexError, TypeError): - #import traceback - #traceback.print_exc() + # import traceback + # traceback.print_exc() pass if init(): @@ -2201,8 +2224,8 @@ def open(fp, mode="r"): fp.seek(0) return factory(fp, filename) except (SyntaxError, IndexError, TypeError): - #import traceback - #traceback.print_exc() + # import traceback + # traceback.print_exc() pass raise IOError("cannot identify image file %r" diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index b52932757..7da385624 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -3,18 +3,6 @@ from tester import * from PIL import Image -def test_frombytes_tobytes(): - # Arrange - im = Image.open('Images/lena.jpg') - - # Act - data = im.tobytes() - new_im = Image.frombytes(im.mode, im.size, data) - - # Assert - assert_image_equal(im, new_im) - - def helper_test_pickle_file(pickle, protocol=0): im = Image.open('Images/lena.jpg') filename = tempfile('temp.pkl') @@ -29,8 +17,8 @@ def helper_test_pickle_file(pickle, protocol=0): assert_image_equal(im, loaded_im) -def helper_test_pickle_string(pickle, protocol=0): - im = Image.open('Images/lena.jpg') +def helper_test_pickle_string(pickle, protocol=0, file='Images/lena.jpg'): + im = Image.open(file) # Act dumped_string = pickle.dumps(im, protocol) @@ -62,4 +50,21 @@ def test_cpickle_image(): helper_test_pickle_string(cPickle, protocol) helper_test_pickle_file(cPickle, protocol) + +def test_pickle_p_mode(): + # Arrange + import pickle + + # Act / Assert + for file in [ + "Tests/images/test-card.png", + "Tests/images/zero_bb.png", + "Tests/images/zero_bb_scale2.png", + "Tests/images/non_zero_bb.png", + "Tests/images/non_zero_bb_scale2.png", + "Tests/images/p_trns_single.png", + "Tests/images/pil123p.png" + ]: + helper_test_pickle_string(pickle, file=file) + # End of file diff --git a/Tests/tester.py b/Tests/tester.py index 32da48e98..a58872e2c 100644 --- a/Tests/tester.py +++ b/Tests/tester.py @@ -242,7 +242,8 @@ def assert_image_equal(a, b, msg=None): failure(msg or "got size %r, expected %r" % (a.size, b.size)) elif a.tobytes() != b.tobytes(): failure(msg or "got different content") - # generate better diff? + elif a != b: + failure(msg or "images different") else: success() From 8794c463181b94dc39cab8bd368efe809cc9adde Mon Sep 17 00:00:00 2001 From: hugovk Date: Sat, 26 Apr 2014 20:05:02 +0300 Subject: [PATCH 07/10] For pickling, test with Image's __eq__. Everything else can use the old assert_image_equal. --- Tests/test_pickle.py | 4 ++-- Tests/tester.py | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 7da385624..ac8e0545d 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -14,7 +14,7 @@ def helper_test_pickle_file(pickle, protocol=0): loaded_im = pickle.load(f) # Assert - assert_image_equal(im, loaded_im) + assert_image_completely_equal(im, loaded_im) def helper_test_pickle_string(pickle, protocol=0, file='Images/lena.jpg'): @@ -25,7 +25,7 @@ def helper_test_pickle_string(pickle, protocol=0, file='Images/lena.jpg'): loaded_im = pickle.loads(dumped_string) # Assert - assert_image_equal(im, loaded_im) + assert_image_completely_equal(im, loaded_im) def test_pickle_image(): diff --git a/Tests/tester.py b/Tests/tester.py index a58872e2c..c1e8404d7 100644 --- a/Tests/tester.py +++ b/Tests/tester.py @@ -242,7 +242,12 @@ def assert_image_equal(a, b, msg=None): failure(msg or "got size %r, expected %r" % (a.size, b.size)) elif a.tobytes() != b.tobytes(): failure(msg or "got different content") - elif a != b: + else: + success() + + +def assert_image_completely_equal(a, b, msg=None): + if a != b: failure(msg or "images different") else: success() From 7d1cdc54c433855cf560b6305db5b38baa278a08 Mon Sep 17 00:00:00 2001 From: hugovk Date: Sat, 26 Apr 2014 20:20:42 +0300 Subject: [PATCH 08/10] Don't skip all the tests on Python 3 when there's no cPickle, just the irrelevant test --- Tests/test_pickle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index ac8e0545d..34240f19a 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -41,9 +41,9 @@ def test_pickle_image(): def test_cpickle_image(): # Arrange try: - import cPickle + import ABCcPickle except ImportError: - skip() + return # Act / Assert for protocol in range(0, cPickle.HIGHEST_PROTOCOL + 1): From a7d21dec154e146ba20d16f3b9efc32363a5708d Mon Sep 17 00:00:00 2001 From: hugovk Date: Sat, 26 Apr 2014 20:41:01 +0300 Subject: [PATCH 09/10] Remove temp test code --- Tests/test_pickle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 34240f19a..d36f476d0 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -41,7 +41,7 @@ def test_pickle_image(): def test_cpickle_image(): # Arrange try: - import ABCcPickle + import cPickle except ImportError: return From f1cc19495570d2f557349c023d740f29b7943033 Mon Sep 17 00:00:00 2001 From: hugovk Date: Sat, 26 Apr 2014 21:23:45 +0300 Subject: [PATCH 10/10] Don't compare pyaccess in __eq__ --- PIL/Image.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/PIL/Image.py b/PIL/Image.py index 8d97e1221..9424f4bd2 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -570,9 +570,8 @@ class Image: d = (self.info == other.info) e = (self.category == other.category) f = (self.readonly == other.readonly) - g = (self.pyaccess == other.pyaccess) - h = (self.tobytes() == other.tobytes()) - return a and b and c and d and e and f and g and h + g = (self.tobytes() == other.tobytes()) + return a and b and c and d and e and f and g def __ne__(self, other): eq = (self == other)