diff --git a/.travis.yml b/.travis.yml index 1d0f5c191..472f8a9fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,10 @@ python: - 3.2 - 3.3 -install: "sudo apt-get -qq install libfreetype6-dev liblcms2-dev libwebp-dev python-qt4 ghostscript" +install: + - "sudo apt-get -qq install libfreetype6-dev liblcms2-dev libwebp-dev python-qt4 ghostscript libffi-dev" + - "pip install cffi" + script: - python setup.py clean diff --git a/PIL/Image.py b/PIL/Image.py index 0ab0367f7..e71e2ad5f 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -100,6 +100,13 @@ import os, sys import collections import numbers +# works everywhere, win for pypy, not cpython +USE_CFFI_ACCESS = hasattr(sys, 'pypy_version_info') +try: + import cffi + HAS_CFFI=True +except: + HAS_CFFI=False def isImageType(t): """ @@ -468,6 +475,7 @@ class Image: self.info = {} self.category = NORMAL self.readonly = 0 + self.pyaccess = None def _new(self, im): new = Image() @@ -492,6 +500,7 @@ class Image: def _copy(self): self.load() self.im = self.im.copy() + self.pyaccess = None self.readonly = 0 def _dump(self, file=None, format=None): @@ -645,6 +654,13 @@ class Image: self.palette.mode = "RGBA" if self.im: + if HAS_CFFI and USE_CFFI_ACCESS: + if self.pyaccess: + return self.pyaccess + from PIL import PyAccess + self.pyaccess = PyAccess.new(self, self.readonly) + if self.pyaccess: + return self.pyaccess return self.im.pixel_access(self.readonly) def verify(self): @@ -976,6 +992,8 @@ class Image: """ self.load() + if self.pyaccess: + return self.pyaccess.getpixel(xy) return self.im.getpixel(xy) def getprojection(self): @@ -1186,12 +1204,14 @@ class Image: mode = getmodebase(self.mode) + "A" try: self.im.setmode(mode) + self.pyaccess = None except (AttributeError, ValueError): # do things the hard way im = self.im.convert(mode) if im.mode not in ("LA", "RGBA"): raise ValueError # sanity check self.im = im + self.pyaccess = None self.mode = self.im.mode except (KeyError, ValueError): raise ValueError("illegal image mode") @@ -1292,7 +1312,11 @@ class Image: self.load() if self.readonly: self._copy() - + self.pyaccess = None + self.load() + + if self.pyaccess: + return self.pyaccess.putpixel(xy,value) return self.im.putpixel(xy, value) def resize(self, size, resample=NEAREST): @@ -1593,6 +1617,7 @@ class Image: self.size = size self.readonly = 0 + self.pyaccess = None # FIXME: the different tranform methods need further explanation # instead of bloating the method docs, add a separate chapter. diff --git a/PIL/PyAccess.py b/PIL/PyAccess.py new file mode 100644 index 000000000..f76beb820 --- /dev/null +++ b/PIL/PyAccess.py @@ -0,0 +1,297 @@ +# +# The Python Imaging Library +# Pillow fork +# +# Python implementation of the PixelAccess Object +# +# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved. +# Copyright (c) 1995-2009 by Fredrik Lundh. +# Copyright (c) 2013 Eric Soroos +# +# See the README file for information on usage and redistribution +# + +# Notes: +# +# * Implements the pixel access object following Access. +# * Does not implement the line functions, as they don't appear to be used +# * Taking only the tuple form, which is used from python. +# * Fill.c uses the integer form, but it's still going to use the old Access.c implementation. +# + +from __future__ import print_function + +from cffi import FFI +import sys + +DEBUG = 0 + +defs = """ +struct Pixel_RGBA { + unsigned char r,g,b,a; +}; +struct Pixel_I16 { + unsigned char l,r; +}; +""" +ffi = FFI() +ffi.cdef(defs) + + +class PyAccess(object): + + def __init__(self, img, readonly = False): + vals = dict(img.im.unsafe_ptrs) + self.readonly = readonly + self.image8 = ffi.cast('unsigned char **', vals['image8']) + self.image32 = ffi.cast('int **', vals['image32']) + self.image = ffi.cast('unsigned char **', vals['image']) + self.xsize = vals['xsize'] + self.ysize = vals['ysize'] + + if DEBUG: + print (vals) + self._post_init() + + def _post_init(): pass + + def __setitem__(self, xy, color): + """ + Modifies the pixel at x,y. The color is given as a single + numerical value for single band images, and a tuple for + multi-band images + + :param xy: The pixel coordinate, given as (x, y). + :param value: The pixel value. + """ + if self.readonly: raise ValueError('Attempt to putpixel a read only image') + (x,y) = self.check_xy(xy) + return self.set_pixel(x,y,color) + + def __getitem__(self, xy): + """ + Returns the pixel at x,y. The pixel is returned as a single + value for single band images or a tuple for multiple band + images + + :param xy: The pixel coordinate, given as (x, y). + """ + + (x,y) = self.check_xy(xy) + return self.get_pixel(x,y) + + putpixel = __setitem__ + getpixel = __getitem__ + + def check_xy(self, xy): + (x,y) = xy + if not (0 <= x < self.xsize and 0 <= y < self.ysize): + raise ValueError('pixel location out of range') + return xy + +class _PyAccess32_2(PyAccess): + """ PA, LA, stored in first and last bytes of a 32 bit word """ + def _post_init(self, *args, **kwargs): + self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) + + def get_pixel(self, x,y): + pixel = self.pixels[y][x] + return (pixel.r, pixel.a) + + def set_pixel(self, x,y, color): + pixel = self.pixels[y][x] + # tuple + pixel.r = min(color[0],255) + pixel.a = min(color[1],255) + +class _PyAccess32_3(PyAccess): + """ RGB and friends, stored in the first three bytes of a 32 bit word """ + + def _post_init(self, *args, **kwargs): + self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) + + def get_pixel(self, x,y): + pixel = self.pixels[y][x] + return (pixel.r, pixel.g, pixel.b) + + def set_pixel(self, x,y, color): + pixel = self.pixels[y][x] + # tuple + pixel.r = min(color[0],255) + pixel.g = min(color[1],255) + pixel.b = min(color[2],255) + +class _PyAccess32_4(PyAccess): + """ RGBA etc, all 4 bytes of a 32 bit word """ + def _post_init(self, *args, **kwargs): + self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) + + def get_pixel(self, x,y): + pixel = self.pixels[y][x] + return (pixel.r, pixel.g, pixel.b, pixel.a) + + def set_pixel(self, x,y, color): + pixel = self.pixels[y][x] + # tuple + pixel.r = min(color[0],255) + pixel.g = min(color[1],255) + pixel.b = min(color[2],255) + pixel.a = min(color[3],255) + + +class _PyAccess8(PyAccess): + """ 1, L, P, 8 bit images stored as uint8 """ + def _post_init(self, *args, **kwargs): + self.pixels = self.image8 + + def get_pixel(self, x,y): + return self.pixels[y][x] + + def set_pixel(self, x,y, color): + try: + # integer + self.pixels[y][x] = min(color,255) + except: + # tuple + self.pixels[y][x] = min(color[0],255) + +class _PyAccessI16_N(PyAccess): + """ I;16 access, native bitendian without conversion """ + def _post_init(self, *args, **kwargs): + self.pixels = ffi.cast('unsigned short **', self.image) + + def get_pixel(self, x,y): + return self.pixels[y][x] + + def set_pixel(self, x,y, color): + try: + # integer + self.pixels[y][x] = min(color, 65535) + except: + # tuple + self.pixels[y][x] = min(color[0], 65535) + +class _PyAccessI16_L(PyAccess): + """ I;16L access, with conversion """ + def _post_init(self, *args, **kwargs): + self.pixels = ffi.cast('struct Pixel_I16 **', self.image) + + def get_pixel(self, x,y): + pixel = self.pixels[y][x] + return pixel.l + pixel.r * 256 + + def set_pixel(self, x,y, color): + pixel = self.pixels[y][x] + try: + color = min(color, 65535) + except: + color = min(color[0], 65535) + + pixel.l = color & 0xFF + pixel.r = color >> 8 + +class _PyAccessI16_B(PyAccess): + """ I;16B access, with conversion """ + def _post_init(self, *args, **kwargs): + self.pixels = ffi.cast('struct Pixel_I16 **', self.image) + + def get_pixel(self, x,y): + pixel = self.pixels[y][x] + return pixel.l *256 + pixel.r + + def set_pixel(self, x,y, color): + pixel = self.pixels[y][x] + try: + color = min(color, 65535) + except: + color = min(color[0], 65535) + + pixel.l = color >> 8 + pixel.r = color & 0xFF + +class _PyAccessI32_N(PyAccess): + """ Signed Int32 access, native endian """ + def _post_init(self, *args, **kwargs): + self.pixels = self.image32 + + def get_pixel(self, x,y): + return self.pixels[y][x] + + def set_pixel(self, x,y, color): + self.pixels[y][x] = color + +class _PyAccessI32_Swap(PyAccess): + """ I;32L/B access, with byteswapping conversion """ + def _post_init(self, *args, **kwargs): + self.pixels = self.image32 + + def reverse(self, i): + orig = ffi.new('int *', i) + chars = ffi.cast('unsigned char *', orig) + chars[0],chars[1],chars[2],chars[3] = chars[3], chars[2],chars[1],chars[0] + return ffi.cast('int *', chars)[0] + + def get_pixel(self, x,y): + return self.reverse(self.pixels[y][x]) + + def set_pixel(self, x,y, color): + self.pixels[y][x] = self.reverse(color) + +class _PyAccessF(PyAccess): + """ 32 bit float access """ + def _post_init(self, *args, **kwargs): + self.pixels = ffi.cast('float **', self.image32) + + def get_pixel(self, x,y): + return self.pixels[y][x] + + def set_pixel(self, x,y, color): + try: + # not a tuple + self.pixels[y][x] = color + except: + # tuple + self.pixels[y][x] = color[0] + + +mode_map = {'1': _PyAccess8, + 'L': _PyAccess8, + 'P': _PyAccess8, + 'LA': _PyAccess32_2, + 'PA': _PyAccess32_2, + 'RGB': _PyAccess32_3, + 'LAB': _PyAccess32_3, + 'YCbCr': _PyAccess32_3, + 'RGBA': _PyAccess32_4, + 'RGBa': _PyAccess32_4, + 'RGBX': _PyAccess32_4, + 'CMYK': _PyAccess32_4, + 'F': _PyAccessF, + 'I': _PyAccessI32_N, + } + +if sys.byteorder == 'little': + mode_map['I;16'] = _PyAccessI16_N + mode_map['I;16L'] = _PyAccessI16_N + mode_map['I;16B'] = _PyAccessI16_B + + mode_map['I;32L'] = _PyAccessI32_N + mode_map['I;32B'] = _PyAccessI32_Swap +else: + mode_map['I;16'] = _PyAccessI16_L + mode_map['I;16L'] = _PyAccessI16_L + mode_map['I;16B'] = _PyAccessI16_N + + mode_map['I;32L'] = _PyAccessI32_Swap + mode_map['I;32B'] = _PyAccessI32_N + +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) + return None + if DEBUG: print ("New PyAccess: %s" % img.mode) + return access_type(img, readonly) + + diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py new file mode 100644 index 000000000..8f8ef937a --- /dev/null +++ b/Tests/bench_cffi_access.py @@ -0,0 +1,51 @@ +from tester import * + +# not running this test by default. No DOS against travis. + +from PIL import PyAccess +from PIL import Image + +import time + +def iterate_get(size, access): + (w,h) = size + for x in range(w): + for y in range(h): + access[(x,y)] + +def iterate_set(size, access): + (w,h) = size + for x in range(w): + for y in range(h): + access[(x,y)] = (x %256,y%256,0) + +def timer(func, label, *args): + iterations = 5000 + starttime = time.time() + for x in range(iterations): + func(*args) + if time.time()-starttime > 10: + print ("%s: breaking at %s iterations, %.6f per iteration"%(label, x+1, (time.time()-starttime)/(x+1.0))) + break + if x == iterations-1: + endtime = time.time() + print ("%s: %.4f s %.6f per iteration" %(label, endtime-starttime, (endtime-starttime)/(x+1.0))) + +def test_direct(): + im = lena() + im.load() + #im = Image.new( "RGB", (2000,2000), (1,3,2)) + caccess = im.im.pixel_access(False) + access = PyAccess.new(im, False) + + assert_equal(caccess[(0,0)], access[(0,0)]) + + print ("Size: %sx%s" % im.size) + timer(iterate_get, 'PyAccess - get', im.size, access) + timer(iterate_set, 'PyAccess - set', im.size, access) + timer(iterate_get, 'C-api - get', im.size, caccess) + timer(iterate_set, 'C-api - set', im.size, caccess) + + + + diff --git a/Tests/test_cffi.py b/Tests/test_cffi.py new file mode 100644 index 000000000..4065a9e53 --- /dev/null +++ b/Tests/test_cffi.py @@ -0,0 +1,100 @@ +from tester import * + +from PIL import Image, PyAccess + +import test_image_putpixel as put +import test_image_getpixel as get + + + +try: + import cffi +except: + skip() + +Image.USE_CFFI_ACCESS = True + +def test_put(): + put.test_sanity() + +def test_get(): + get.test_basic() + get.test_signedness() + +def _test_get_access(im): + """ Do we get the same thing as the old pixel access """ + + """ Using private interfaces, forcing a capi access and a pyaccess for the same image """ + caccess = im.im.pixel_access(False) + access = PyAccess.new(im, False) + + w,h = im.size + for x in range(0,w,10): + for y in range(0,h,10): + assert_equal(access[(x,y)], caccess[(x,y)]) + +def test_get_vs_c(): + _test_get_access(lena('RGB')) + _test_get_access(lena('RGBA')) + _test_get_access(lena('L')) + _test_get_access(lena('LA')) + _test_get_access(lena('1')) + _test_get_access(lena('P')) + #_test_get_access(lena('PA')) # PA -- how do I make a PA image??? + _test_get_access(lena('F')) + + im = Image.new('I;16', (10,10), 40000) + _test_get_access(im) + im = Image.new('I;16L', (10,10), 40000) + _test_get_access(im) + im = Image.new('I;16B', (10,10), 40000) + _test_get_access(im) + + im = Image.new('I', (10,10), 40000) + _test_get_access(im) + # These don't actually appear to be modes that I can actually make, + # as unpack sets them directly into the I mode. + #im = Image.new('I;32L', (10,10), -2**10) + #_test_get_access(im) + #im = Image.new('I;32B', (10,10), 2**10) + #_test_get_access(im) + + + +def _test_set_access(im, color): + """ Are we writing the correct bits into the image? """ + + """ Using private interfaces, forcing a capi access and a pyaccess for the same image """ + caccess = im.im.pixel_access(False) + access = PyAccess.new(im, False) + + w,h = im.size + for x in range(0,w,10): + for y in range(0,h,10): + access[(x,y)] = color + assert_equal(color, caccess[(x,y)]) + +def test_set_vs_c(): + _test_set_access(lena('RGB'), (255, 128,0) ) + _test_set_access(lena('RGBA'), (255, 192, 128, 0)) + _test_set_access(lena('L'), 128) + _test_set_access(lena('LA'), (128,128)) + _test_set_access(lena('1'), 255) + _test_set_access(lena('P') , 128) + ##_test_set_access(i, (128,128)) #PA -- undone how to make + _test_set_access(lena('F'), 1024.0) + + im = Image.new('I;16', (10,10), 40000) + _test_set_access(im, 45000) + im = Image.new('I;16L', (10,10), 40000) + _test_set_access(im, 45000) + im = Image.new('I;16B', (10,10), 40000) + _test_set_access(im, 45000) + + + im = Image.new('I', (10,10), 40000) + _test_set_access(im, 45000) +# im = Image.new('I;32L', (10,10), -(2**10)) +# _test_set_access(im, -(2**13)+1) + #im = Image.new('I;32B', (10,10), 2**10) + #_test_set_access(im, 2**13-1) diff --git a/Tests/test_image_getpixel.py b/Tests/test_image_getpixel.py index 6c5e8b084..67f5904a2 100644 --- a/Tests/test_image_getpixel.py +++ b/Tests/test_image_getpixel.py @@ -2,6 +2,8 @@ from tester import * from PIL import Image +Image.USE_CFFI_ACCESS=False + def color(mode): bands = Image.getmodebands(mode) if bands == 1: @@ -9,49 +11,39 @@ def color(mode): else: return tuple(range(1, bands+1)) -def test_pixel(): - def pixel(mode): + +def check(mode, c=None): + if not c: c = color(mode) - im = Image.new(mode, (1, 1), None) - im.putpixel((0, 0), c) - return im.getpixel((0, 0)) + + #check putpixel + im = Image.new(mode, (1, 1), None) + im.putpixel((0, 0), c) + assert_equal(im.getpixel((0, 0)), c, + "put/getpixel roundtrip failed for mode %s, color %s" % + (mode, c)) + + # check inital color + im = Image.new(mode, (1, 1), c) + assert_equal(im.getpixel((0, 0)), c, + "initial color failed for mode %s, color %s " % + (mode, color)) - assert_equal(pixel("1"), 1) - assert_equal(pixel("L"), 1) - assert_equal(pixel("LA"), (1, 2)) - assert_equal(pixel("I"), 1) - assert_equal(pixel("I;16"), 1) - assert_equal(pixel("I;16B"), 1) - assert_equal(pixel("F"), 1.0) - assert_equal(pixel("P"), 1) - assert_equal(pixel("PA"), (1, 2)) - assert_equal(pixel("RGB"), (1, 2, 3)) - assert_equal(pixel("RGBA"), (1, 2, 3, 4)) - assert_equal(pixel("RGBX"), (1, 2, 3, 4)) - assert_equal(pixel("CMYK"), (1, 2, 3, 4)) - assert_equal(pixel("YCbCr"), (1, 2, 3)) +def test_basic(): + for mode in ("1", "L", "LA", "I", "I;16", "I;16B", "F", + "P", "PA", "RGB", "RGBA", "RGBX", "CMYK","YCbCr"): + check(mode) -def test_image(): - - def pixel(mode): - im = Image.new(mode, (1, 1), color(mode)) - return im.getpixel((0, 0)) - - assert_equal(pixel("1"), 1) - assert_equal(pixel("L"), 1) - assert_equal(pixel("LA"), (1, 2)) - assert_equal(pixel("I"), 1) - assert_equal(pixel("I;16"), 1) - assert_equal(pixel("I;16B"), 1) - assert_equal(pixel("F"), 1.0) - assert_equal(pixel("P"), 1) - assert_equal(pixel("PA"), (1, 2)) - assert_equal(pixel("RGB"), (1, 2, 3)) - assert_equal(pixel("RGBA"), (1, 2, 3, 4)) - assert_equal(pixel("RGBX"), (1, 2, 3, 4)) - assert_equal(pixel("CMYK"), (1, 2, 3, 4)) - assert_equal(pixel("YCbCr"), (1, 2, 3)) +def test_signedness(): + # see https://github.com/python-imaging/Pillow/issues/452 + # pixelaccess is using signed int* instead of uint* + for mode in ("I;16", "I;16B"): + check(mode, 2**15-1) + check(mode, 2**15) + check(mode, 2**15+1) + check(mode, 2**16-1) + diff --git a/Tests/test_image_putpixel.py b/Tests/test_image_putpixel.py index 2b60bbd97..5f19237cb 100644 --- a/Tests/test_image_putpixel.py +++ b/Tests/test_image_putpixel.py @@ -2,6 +2,8 @@ from tester import * from PIL import Image +Image.USE_CFFI_ACCESS=False + def test_sanity(): im1 = lena() diff --git a/_imaging.c b/_imaging.c index c62f0257d..078961da4 100644 --- a/_imaging.c +++ b/_imaging.c @@ -463,7 +463,7 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) { union { UINT8 b[4]; - INT16 h; + UINT16 h; INT32 i; FLOAT32 f; } pixel; @@ -3072,12 +3072,35 @@ _getattr_ptr(ImagingObject* self, void* closure) #endif } +static PyObject* +_getattr_unsafe_ptrs(ImagingObject* self, void* closure) +{ + return Py_BuildValue("(ss)(si)(si)(si)(si)(si)(sn)(sn)(sn)(sn)(sn)(si)(si)(sn)", + "mode", self->image->mode, + "type", self->image->type, + "depth", self->image->depth, + "bands", self->image->bands, + "xsize", self->image->xsize, + "ysize", self->image->ysize, + "palette", self->image->palette, + "image8", self->image->image8, + "image32", self->image->image32, + "image", self->image->image, + "block", self->image->block, + "pixelsize", self->image->pixelsize, + "linesize", self->image->linesize, + "destroy", self->image->destroy + ); +}; + + static struct PyGetSetDef getsetters[] = { { "mode", (getter) _getattr_mode }, { "size", (getter) _getattr_size }, { "bands", (getter) _getattr_bands }, { "id", (getter) _getattr_id }, { "ptr", (getter) _getattr_ptr }, + { "unsafe_ptrs", (getter) _getattr_unsafe_ptrs }, { NULL } }; diff --git a/libImaging/Access.c b/libImaging/Access.c index 70eb1af4c..62c97f3a3 100644 --- a/libImaging/Access.c +++ b/libImaging/Access.c @@ -94,11 +94,11 @@ static void get_pixel_16L(Imaging im, int x, int y, void* color) { UINT8* in = (UINT8*) &im->image[y][x+x]; - INT16* out = color; + UINT16* out = color; #ifdef WORDS_BIGENDIAN out[0] = in[0] + (in[1]<<8); #else - out[0] = *(INT16*) in; + out[0] = *(UINT16*) in; #endif } @@ -106,9 +106,9 @@ static void get_pixel_16B(Imaging im, int x, int y, void* color) { UINT8* in = (UINT8*) &im->image[y][x+x]; - INT16* out = color; + UINT16* out = color; #ifdef WORDS_BIGENDIAN - out[0] = *(INT16*) in; + out[0] = *(UINT16*) in; #else out[0] = in[1] + (in[0]<<8); #endif