From d3c740cdc8ba5b71b2440e69eda85ad9b3a4f913 Mon Sep 17 00:00:00 2001 From: Joaquin Cuenca Abela Date: Thu, 20 Sep 2012 14:38:14 +0000 Subject: [PATCH 1/8] Add tests to cover the filter and unfilter methods. --- code/png_test.py | 94 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 code/png_test.py diff --git a/code/png_test.py b/code/png_test.py new file mode 100644 index 00000000..733465e7 --- /dev/null +++ b/code/png_test.py @@ -0,0 +1,94 @@ +# Use py.test to run these tests +import png + + +def paeth(x, a, b, c): + """Returns the paeth predictor of the pixel x width neightbors a, b, c. + + +-+-+ + |c|b| + +-+-+ + |a|x| + +-+-+ + + See http://www.w3.org/TR/PNG/#9Filter-type-4-Paeth for more details. + """ + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: + pr = a + elif pb <= pc: + pr = b + else: + pr = c + return x - pr + + +def test_filter_scanline_first_line(): + fo = 3 # bytes per pixel + line = [30, 31, 32, 230, 231, 232] + out = png.filter_scanline(0, line, fo, None) # none + assert list(out) == [0, 30, 31, 32, 230, 231, 232] + out = png.filter_scanline(1, line, fo, None) # sub + assert list(out) == [1, 30, 31, 32, 200, 200, 200] + out = png.filter_scanline(2, line, fo, None) # up + # TODO: All filtered scanlines start with a byte indicating the filter + # algorithm, except "up". Is this a bug? + assert list(out) == [30, 31, 32, 230, 231, 232] + out = png.filter_scanline(3, line, fo, None) # average + assert list(out) == [3, 30, 31, 32, 215, 216, 216] + out = png.filter_scanline(4, line, fo, None) # paeth + assert list(out) == [ + 4, paeth(30, 0, 0, 0), paeth(31, 0, 0, 0), paeth(32, 0, 0, 0), + paeth(230, 30, 0, 0), paeth(231, 31, 0, 0), paeth(232, 32, 0, 0) + ] + + +def test_filter_scanline(): + prev = [20, 21, 22, 210, 211, 212] + line = [30, 32, 34, 230, 233, 236] + fo = 3 + out = png.filter_scanline(0, line, fo, prev) # none + assert list(out) == [0, 30, 32, 34, 230, 233, 236] + out = png.filter_scanline(1, line, fo, prev) # sub + assert list(out) == [1, 30, 32, 34, 200, 201, 202] + out = png.filter_scanline(2, line, fo, prev) # up + assert list(out) == [2, 10, 11, 12, 20, 22, 24] + out = png.filter_scanline(3, line, fo, prev) # average + assert list(out) == [3, 20, 22, 23, 110, 112, 113] + out = png.filter_scanline(4, line, fo, prev) # paeth + assert list(out) == [ + 4, paeth(30, 0, 20, 0), paeth(32, 0, 21, 0), paeth(34, 0, 22, 0), + paeth(230, 30, 210, 20), paeth(233, 32, 211, 21), paeth(236, 34, 212, 22) + ] + + +def test_unfilter_scanline(): + reader = png.Reader(bytes='') + reader.psize = 3 + scanprev = [20, 21, 22, 210, 211, 212] + scanline = [30, 32, 34, 230, 233, 236] + + out = reader.undo_filter(0, scanline, scanprev) + assert list(out) == scanline # none + out = reader.undo_filter(1, scanline, scanprev) + assert list(out) == [30, 32, 34, 4, 9, 14] # sub + out = reader.undo_filter(2, scanline, scanprev) + assert list(out) == [50, 53, 56, 184, 188, 192] # up + out = reader.undo_filter(3, scanline, scanprev) + assert list(out) == [40, 42, 45, 99, 103, 108] # average + out = reader.undo_filter(4, scanline, scanprev) + assert list(out) == [50, 53, 56, 184, 188, 192] # paeth + + +def test_unfilter_scanline_paeth(): + # This tests more edge cases in the paeth unfilter + reader = png.Reader(bytes='') + reader.psize = 3 + scanprev = [2, 0, 0, 0, 9, 11] + scanline = [6, 10, 9, 100, 101, 102] + + out = reader.undo_filter(4, scanline, scanprev) + assert list(out) == [8, 10, 9, 108, 111, 113] # paeth From 759bc2529175cb020a5f7682cb9e4eacd74728c8 Mon Sep 17 00:00:00 2001 From: Joaquin Cuenca Abela Date: Thu, 20 Sep 2012 17:52:19 +0000 Subject: [PATCH 2/8] Create a cython module to improve the speed of some key operations. --- code/cpngfilters.pyx | 89 +++++++++++++++++++++++++++++++++++++++++ code/png.py | 41 +++++++++---------- code/png_test.py | 11 ++--- code/pngfilters.py | 58 +++++++++++++++++++++++++++ code/pngfilters_test.py | 14 +++++++ 5 files changed, 187 insertions(+), 26 deletions(-) create mode 100644 code/cpngfilters.pyx create mode 100644 code/pngfilters.py create mode 100644 code/pngfilters_test.py diff --git a/code/cpngfilters.pyx b/code/cpngfilters.pyx new file mode 100644 index 00000000..006d123d --- /dev/null +++ b/code/cpngfilters.pyx @@ -0,0 +1,89 @@ +cimport cpython.array + +#cython: boundscheck=False +#cython: wraparound=False + +def undo_filter_sub(int filter_unit, unsigned char[:] scanline, + unsigned char[:] previous, unsigned char[:] result): + """Undo sub filter.""" + + cdef int l = len(result) + cdef int ai = 0 + cdef unsigned char x, a + + # Loops starts at index fu. Observe that the initial part + # of the result is already filled in correctly with + # scanline. + for i in range(filter_unit, l): + x = scanline[i] + a = result[ai] + result[i] = (x + a) & 0xff + ai += 1 + + +def undo_filter_paeth(int filter_unit, unsigned char[:] scanline, + unsigned char[:] previous, unsigned char[:] result): + """Undo Paeth filter.""" + + # Also used for ci. + cdef int ai = -filter_unit + cdef int l = len(result) + cdef int i, pa, pb, pc, p + cdef unsigned char x, a, b, c, pr + + for i in range(l): + x = scanline[i] + if ai < 0: + a = c = 0 + else: + a = result[ai] + c = previous[ai] + b = previous[i] + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: + pr = a + elif pb <= pc: + pr = b + else: + pr = c + result[i] = (x + pr) & 0xff + ai += 1 + + +def convert_rgb_to_rgba(unsigned char[:] row, unsigned char[:] result): + cdef int i, l, j, k + l = min(len(row) / 3, len(result) / 4) + for i in range(l): + j = i * 3 + k = i * 4 + result[k] = row[j] + result[k + 1] = row[j + 1] + result[k + 2] = row[j + 2] + + +def convert_l_to_rgba(unsigned char[:] row, unsigned char[:] result): + cdef int i, l, j, k, lum + l = min(len(row), len(result) / 4) + for i in range(l): + j = i + k = i * 4 + lum = row[j] + result[k] = lum + result[k + 1] = lum + result[k + 2] = lum + + +def convert_la_to_rgba(unsigned char[:] row, unsigned char[:] result): + cdef int i, l, j, k, lum + l = min(len(row) / 2, len(result) / 4) + for i in range(l): + j = i * 2 + k = i * 4 + lum = row[j] + result[k] = lum + result[k + 1] = lum + result[k + 2] = lum + result[k + 3] = row[j + 1] diff --git a/code/png.py b/code/png.py index 3ba2cefe..626359b0 100755 --- a/code/png.py +++ b/code/png.py @@ -180,6 +180,12 @@ import zlib # http://www.python.org/doc/2.4.4/lib/module-warnings.html import warnings +try: + import pyximport + pyximport.install() + import cpngfilters as pngfilters +except ImportError: + import pngfilters __all__ = ['Image', 'Reader', 'Writer', 'write_chunks', 'from_array'] @@ -1450,17 +1456,12 @@ def undo_filter(self, filter_type, scanline, previous): """ # :todo: Would it be better to update scanline in place? - - # Create the result byte array. It seems that the best way to - # create the array to be the right size is to copy from an - # existing sequence. *sigh* - # If we fill the result with scanline, then this allows a - # micro-optimisation in the "null" and "sub" cases. - result = array('B', scanline) + # Yes, with the Cython extension making the undo_filter fast, + # updating scanline inplace makes the code 3 times faster + # (reading 50 images of 800x800 went from 40s to 16s) + result = scanline if filter_type == 0: - # And here, we _rely_ on filling the result with scanline, - # above. return result if filter_type not in (1,2,3,4): @@ -1543,7 +1544,10 @@ def paeth(): # Call appropriate filter algorithm. Note that 0 has already # been dealt with. - (None, sub, up, average, paeth)[filter_type]() + if filter_type in (1, 4): + pngfilters.undo_filter_paeth(fu, scanline, previous, result) + else: + (None, sub, up, average, paeth)[filter_type]() return result def deinterlace(self, raw): @@ -2182,8 +2186,9 @@ def asRGBA(self): return width,height,pixels,meta typecode = 'BH'[meta['bitdepth'] > 8] maxval = 2**meta['bitdepth'] - 1 + maxbuffer = '\xff' * 4 * width def newarray(): - return array(typecode, [0]) * 4 * width + return array(typecode, maxbuffer) if meta['alpha'] and meta['greyscale']: # LA to RGBA def convert(): @@ -2192,18 +2197,14 @@ def convert(): # into first three target channels, and A channel # into fourth channel. a = newarray() - for i in range(3): - a[i::4] = row[0::2] - a[3::4] = row[1::2] + pngfilters.convert_la_to_rgba(row, a) yield a elif meta['greyscale']: # L to RGBA def convert(): for row in pixels: a = newarray() - for i in range(3): - a[i::4] = row - a[3::4] = array(typecode, [maxval]) * width + pngfilters.convert_l_to_rgba(row, a) yield a else: assert not meta['alpha'] and not meta['greyscale'] @@ -2211,9 +2212,7 @@ def convert(): def convert(): for row in pixels: a = newarray() - for i in range(3): - a[i::4] = row[i::3] - a[3::4] = array(typecode, [maxval]) * width + pngfilters.convert_rgb_to_rgba(row, a) yield a meta['alpha'] = True meta['greyscale'] = False @@ -2458,7 +2457,7 @@ def testP2(self): x,y,pixels,meta = r.asRGB8() self.assertEqual(x, 1) self.assertEqual(y, 4) - self.assertEqual(list(pixels), map(list, [a, b, b, c])) + self.assertEqual([list(row) for row in pixels], map(list, [a, b, b, c])) def testPtrns(self): "Test colour type 3 and tRNS chunk (and 4-bit palette)." a = (50,99,50,50) diff --git a/code/png_test.py b/code/png_test.py index 733465e7..a4398be3 100644 --- a/code/png_test.py +++ b/code/png_test.py @@ -1,4 +1,5 @@ # Use py.test to run these tests +import array import png @@ -68,11 +69,11 @@ def test_filter_scanline(): def test_unfilter_scanline(): reader = png.Reader(bytes='') reader.psize = 3 - scanprev = [20, 21, 22, 210, 211, 212] - scanline = [30, 32, 34, 230, 233, 236] + scanprev = array.array('B', [20, 21, 22, 210, 211, 212]) + scanline = array.array('B', [30, 32, 34, 230, 233, 236]) out = reader.undo_filter(0, scanline, scanprev) - assert list(out) == scanline # none + assert list(out) == list(scanline) # none out = reader.undo_filter(1, scanline, scanprev) assert list(out) == [30, 32, 34, 4, 9, 14] # sub out = reader.undo_filter(2, scanline, scanprev) @@ -87,8 +88,8 @@ def test_unfilter_scanline_paeth(): # This tests more edge cases in the paeth unfilter reader = png.Reader(bytes='') reader.psize = 3 - scanprev = [2, 0, 0, 0, 9, 11] - scanline = [6, 10, 9, 100, 101, 102] + scanprev = array.array('B', [2, 0, 0, 0, 9, 11]) + scanline = array.array('B', [6, 10, 9, 100, 101, 102]) out = reader.undo_filter(4, scanline, scanprev) assert list(out) == [8, 10, 9, 108, 111, 113] # paeth diff --git a/code/pngfilters.py b/code/pngfilters.py new file mode 100644 index 00000000..021396f4 --- /dev/null +++ b/code/pngfilters.py @@ -0,0 +1,58 @@ +import array + + +def undo_filter_sub(filter_unit, scanline, previous, result): + """Undo sub filter.""" + + ai = 0 + # Loops starts at index fu. Observe that the initial part + # of the result is already filled in correctly with + # scanline. + for i in range(fu, len(result)): + x = scanline[i] + a = result[ai] + result[i] = (x + a) & 0xff + ai += 1 + + +def undo_filter_paeth(filter_unit, scanline, previous, result): + """Undo Paeth filter.""" + + # Also used for ci. + ai = -filter_unit + for i in range(len(result)): + x = scanline[i] + if ai < 0: + a = c = 0 + else: + a = result[ai] + c = previous[ai] + b = previous[i] + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: + pr = a + elif pb <= pc: + pr = b + else: + pr = c + result[i] = (x + pr) & 0xff + ai += 1 + + +def convert_la_to_rgba(row, result): + for i in range(3): + result[i::4] = row[0::2] + result[3::4] = row[1::2] + + +def convert_l_to_rgba(row, result): + for i in range(3): + result[i::4] = row + + +def convert_rgb_to_rgba(row, result): + for i in range(3): + result[i::4] = row[i::3] diff --git a/code/pngfilters_test.py b/code/pngfilters_test.py new file mode 100644 index 00000000..8519d07d --- /dev/null +++ b/code/pngfilters_test.py @@ -0,0 +1,14 @@ +import array +import pyximport +pyximport.install() +import cpngfilters as pngfilters + + +def test_unfilter_scanline_paeth(): + # This tests more edge cases in the paeth unfilter + scanprev = array.array('B', [2, 0, 0, 0, 9, 11]) + scanline = array.array('B', [6, 10, 9, 100, 101, 102]) + + result = array.array('B', scanline) + pngfilters.undo_filter_paeth(3, scanline, scanprev, result) + assert list(result) == [8, 10, 9, 108, 111, 113] # paeth From fa7eacba39ad641858df18811e5fc1e0014b1826 Mon Sep 17 00:00:00 2001 From: Joaquin Cuenca Abela Date: Thu, 20 Sep 2012 18:14:49 +0000 Subject: [PATCH 3/8] Fix a few bugs discovered with png2 test suite. --- code/png.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/code/png.py b/code/png.py index 626359b0..fa06de9f 100755 --- a/code/png.py +++ b/code/png.py @@ -1544,7 +1544,9 @@ def paeth(): # Call appropriate filter algorithm. Note that 0 has already # been dealt with. - if filter_type in (1, 4): + if filter_type == 1: + pngfilters.undo_filter_sub(fu, scanline, previous, result) + elif filter_type == 4: pngfilters.undo_filter_paeth(fu, scanline, previous, result) else: (None, sub, up, average, paeth)[filter_type]() @@ -2186,9 +2188,10 @@ def asRGBA(self): return width,height,pixels,meta typecode = 'BH'[meta['bitdepth'] > 8] maxval = 2**meta['bitdepth'] - 1 - maxbuffer = '\xff' * 4 * width + maxbuffer = struct.pack('=' + typecode, maxval) * 4 * width def newarray(): return array(typecode, maxbuffer) + if meta['alpha'] and meta['greyscale']: # LA to RGBA def convert(): @@ -2485,7 +2488,7 @@ def testRGBtoRGBA(self): x,y,pixels,meta = r.asRGBA8() # Test the pixels at row 9 columns 0 and 1. row9 = list(pixels)[9] - self.assertEqual(row9[0:8], + self.assertEqual(list(row9[0:8]), [0xff, 0xdf, 0xff, 0xff, 0xff, 0xde, 0xff, 0xff]) def testLtoRGBA(self): "asRGBA() on grey source.""" From cf37891b243e8a3e0c6a20f22af0ec937d0d2c26 Mon Sep 17 00:00:00 2001 From: Joaquin Cuenca Abela Date: Fri, 21 Sep 2012 09:38:41 +0000 Subject: [PATCH 4/8] Complete the cython auxiliary functions --- code/cpngfilters.pyx | 71 +++++++++++++++++++++++++++++++++++--------- code/png.py | 11 ++++--- code/png_test.py | 33 ++++++++++++++++---- code/pngfilters.py | 27 ++++++++++++++++- 4 files changed, 116 insertions(+), 26 deletions(-) diff --git a/code/cpngfilters.pyx b/code/cpngfilters.pyx index 006d123d..53eb2376 100644 --- a/code/cpngfilters.pyx +++ b/code/cpngfilters.pyx @@ -1,13 +1,15 @@ -cimport cpython.array - #cython: boundscheck=False #cython: wraparound=False -def undo_filter_sub(int filter_unit, unsigned char[:] scanline, - unsigned char[:] previous, unsigned char[:] result): +cimport cpython.array + + +# TODO: I don't know how can I not return any value (void doesn't work) +cpdef int undo_filter_sub(int filter_unit, unsigned char[:] scanline, + unsigned char[:] previous, unsigned char[:] result): """Undo sub filter.""" - cdef int l = len(result) + cdef int l = result.shape[0] cdef int ai = 0 cdef unsigned char x, a @@ -19,15 +21,52 @@ def undo_filter_sub(int filter_unit, unsigned char[:] scanline, a = result[ai] result[i] = (x + a) & 0xff ai += 1 + return 0 + + +cpdef int undo_filter_up(int filter_unit, unsigned char[:] scanline, + unsigned char[:] previous, unsigned char[:] result): + """Undo up filter.""" + + cdef int i + cdef int l = result.shape[0] + cdef unsigned char x, b + + for i in range(l): + x = scanline[i] + b = previous[i] + result[i] = (x + b) & 0xff + return 0 + + +cpdef int undo_filter_average(int filter_unit, unsigned char[:] scanline, + unsigned char[:] previous, unsigned char[:] result): + """Undo up filter.""" + + cdef int i, ai + cdef int l = result.shape[0] + cdef unsigned char x, a, b + + ai = -filter_unit + for i in range(l): + x = scanline[i] + if ai < 0: + a = 0 + else: + a = result[ai] + b = previous[i] + result[i] = (x + ((a + b) >> 1)) & 0xff + ai += 1 + return 0 -def undo_filter_paeth(int filter_unit, unsigned char[:] scanline, - unsigned char[:] previous, unsigned char[:] result): +cpdef int undo_filter_paeth(int filter_unit, unsigned char[:] scanline, + unsigned char[:] previous, unsigned char[:] result): """Undo Paeth filter.""" # Also used for ci. cdef int ai = -filter_unit - cdef int l = len(result) + cdef int l = result.shape[0] cdef int i, pa, pb, pc, p cdef unsigned char x, a, b, c, pr @@ -51,22 +90,24 @@ def undo_filter_paeth(int filter_unit, unsigned char[:] scanline, pr = c result[i] = (x + pr) & 0xff ai += 1 + return 0 -def convert_rgb_to_rgba(unsigned char[:] row, unsigned char[:] result): +cpdef int convert_rgb_to_rgba(unsigned char[:] row, unsigned char[:] result): cdef int i, l, j, k - l = min(len(row) / 3, len(result) / 4) + l = min(row.shape[0] / 3, result.shape[0] / 4) for i in range(l): j = i * 3 k = i * 4 result[k] = row[j] result[k + 1] = row[j + 1] result[k + 2] = row[j + 2] + return 0 -def convert_l_to_rgba(unsigned char[:] row, unsigned char[:] result): +cpdef int convert_l_to_rgba(unsigned char[:] row, unsigned char[:] result): cdef int i, l, j, k, lum - l = min(len(row), len(result) / 4) + l = min(row.shape[0], result.shape[0] / 4) for i in range(l): j = i k = i * 4 @@ -74,11 +115,12 @@ def convert_l_to_rgba(unsigned char[:] row, unsigned char[:] result): result[k] = lum result[k + 1] = lum result[k + 2] = lum + return 0 -def convert_la_to_rgba(unsigned char[:] row, unsigned char[:] result): +cpdef int convert_la_to_rgba(unsigned char[:] row, unsigned char[:] result): cdef int i, l, j, k, lum - l = min(len(row) / 2, len(result) / 4) + l = min(row.shape[0] / 2, result.shape[0] / 4) for i in range(l): j = i * 2 k = i * 4 @@ -87,3 +129,4 @@ def convert_la_to_rgba(unsigned char[:] row, unsigned char[:] result): result[k + 1] = lum result[k + 2] = lum result[k + 3] = row[j + 1] + return 0 diff --git a/code/png.py b/code/png.py index fa06de9f..e69c0eea 100755 --- a/code/png.py +++ b/code/png.py @@ -1544,12 +1544,11 @@ def paeth(): # Call appropriate filter algorithm. Note that 0 has already # been dealt with. - if filter_type == 1: - pngfilters.undo_filter_sub(fu, scanline, previous, result) - elif filter_type == 4: - pngfilters.undo_filter_paeth(fu, scanline, previous, result) - else: - (None, sub, up, average, paeth)[filter_type]() + (None, + pngfilters.undo_filter_sub, + pngfilters.undo_filter_up, + pngfilters.undo_filter_average, + pngfilters.undo_filter_paeth)[filter_type](fu, scanline, previous, result) return result def deinterlace(self, raw): diff --git a/code/png_test.py b/code/png_test.py index a4398be3..0425b41f 100644 --- a/code/png_test.py +++ b/code/png_test.py @@ -71,16 +71,18 @@ def test_unfilter_scanline(): reader.psize = 3 scanprev = array.array('B', [20, 21, 22, 210, 211, 212]) scanline = array.array('B', [30, 32, 34, 230, 233, 236]) + def cp(a): + return array.array('B', a) - out = reader.undo_filter(0, scanline, scanprev) + out = reader.undo_filter(0, cp(scanline), cp(scanprev)) assert list(out) == list(scanline) # none - out = reader.undo_filter(1, scanline, scanprev) + out = reader.undo_filter(1, cp(scanline), cp(scanprev)) assert list(out) == [30, 32, 34, 4, 9, 14] # sub - out = reader.undo_filter(2, scanline, scanprev) + out = reader.undo_filter(2, cp(scanline), cp(scanprev)) assert list(out) == [50, 53, 56, 184, 188, 192] # up - out = reader.undo_filter(3, scanline, scanprev) + out = reader.undo_filter(3, cp(scanline), cp(scanprev)) assert list(out) == [40, 42, 45, 99, 103, 108] # average - out = reader.undo_filter(4, scanline, scanprev) + out = reader.undo_filter(4, cp(scanline), cp(scanprev)) assert list(out) == [50, 53, 56, 184, 188, 192] # paeth @@ -93,3 +95,24 @@ def test_unfilter_scanline_paeth(): out = reader.undo_filter(4, scanline, scanprev) assert list(out) == [8, 10, 9, 108, 111, 113] # paeth + + +def arraify(list_of_str): + return [array.array('B', s) for s in list_of_str] + + +def test_iterstraight(): + reader = png.Reader(bytes='') + reader.row_bytes = 6 + reader.psize = 3 + rows = reader.iterstraight(arraify(['\x00abcdef', '\x00ghijkl'])) + assert list(rows) == arraify(['abcdef', 'ghijkl']) + + rows = reader.iterstraight(arraify(['\x00abc', 'def\x00ghijkl'])) + assert list(rows) == arraify(['abcdef', 'ghijkl']) + + rows = reader.iterstraight(arraify(['\x00abcdef\x00ghijkl'])) + assert list(rows) == arraify(['abcdef', 'ghijkl']) + + rows = reader.iterstraight(arraify(['\x00abcdef\x00ghi', 'jkl'])) + assert list(rows) == arraify(['abcdef', 'ghijkl']) diff --git a/code/pngfilters.py b/code/pngfilters.py index 021396f4..bb31b50d 100644 --- a/code/pngfilters.py +++ b/code/pngfilters.py @@ -8,13 +8,38 @@ def undo_filter_sub(filter_unit, scanline, previous, result): # Loops starts at index fu. Observe that the initial part # of the result is already filled in correctly with # scanline. - for i in range(fu, len(result)): + for i in range(filter_unit, len(result)): x = scanline[i] a = result[ai] result[i] = (x + a) & 0xff ai += 1 +def undo_filter_up(filter_unit, scanline, previous, result): + """Undo up filter.""" + + for i in range(len(result)): + x = scanline[i] + b = previous[i] + result[i] = (x + b) & 0xff + print 'x: %d, b: %d, result: %d' % (x, b, result[i]) + + +def undo_filter_average(filter_unit, scanline, previous, result): + """Undo up filter.""" + + ai = -filter_unit + for i in range(len(result)): + x = scanline[i] + if ai < 0: + a = 0 + else: + a = result[ai] + b = previous[i] + result[i] = (x + ((a + b) >> 1)) & 0xff + ai += 1 + + def undo_filter_paeth(filter_unit, scanline, previous, result): """Undo Paeth filter.""" From 628949be5481ddff3af195afd570b5495cfc2d01 Mon Sep 17 00:00:00 2001 From: Joaquin Cuenca Abela Date: Fri, 21 Sep 2012 10:08:25 +0000 Subject: [PATCH 5/8] Remove lingering print, and make more cython functions nogil. --- code/cpngfilters.pyx | 22 ++++++++++++---------- code/pngfilters.py | 1 - 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/code/cpngfilters.pyx b/code/cpngfilters.pyx index 53eb2376..5872cd11 100644 --- a/code/cpngfilters.pyx +++ b/code/cpngfilters.pyx @@ -1,12 +1,14 @@ #cython: boundscheck=False #cython: wraparound=False +from libc.stdlib cimport abs as c_abs + cimport cpython.array # TODO: I don't know how can I not return any value (void doesn't work) cpdef int undo_filter_sub(int filter_unit, unsigned char[:] scanline, - unsigned char[:] previous, unsigned char[:] result): + unsigned char[:] previous, unsigned char[:] result) nogil: """Undo sub filter.""" cdef int l = result.shape[0] @@ -25,7 +27,7 @@ cpdef int undo_filter_sub(int filter_unit, unsigned char[:] scanline, cpdef int undo_filter_up(int filter_unit, unsigned char[:] scanline, - unsigned char[:] previous, unsigned char[:] result): + unsigned char[:] previous, unsigned char[:] result) nogil: """Undo up filter.""" cdef int i @@ -40,7 +42,7 @@ cpdef int undo_filter_up(int filter_unit, unsigned char[:] scanline, cpdef int undo_filter_average(int filter_unit, unsigned char[:] scanline, - unsigned char[:] previous, unsigned char[:] result): + unsigned char[:] previous, unsigned char[:] result) nogil: """Undo up filter.""" cdef int i, ai @@ -61,7 +63,7 @@ cpdef int undo_filter_average(int filter_unit, unsigned char[:] scanline, cpdef int undo_filter_paeth(int filter_unit, unsigned char[:] scanline, - unsigned char[:] previous, unsigned char[:] result): + unsigned char[:] previous, unsigned char[:] result) nogil: """Undo Paeth filter.""" # Also used for ci. @@ -79,9 +81,9 @@ cpdef int undo_filter_paeth(int filter_unit, unsigned char[:] scanline, c = previous[ai] b = previous[i] p = a + b - c - pa = abs(p - a) - pb = abs(p - b) - pc = abs(p - c) + pa = c_abs(p - a) + pb = c_abs(p - b) + pc = c_abs(p - c) if pa <= pb and pa <= pc: pr = a elif pb <= pc: @@ -93,7 +95,7 @@ cpdef int undo_filter_paeth(int filter_unit, unsigned char[:] scanline, return 0 -cpdef int convert_rgb_to_rgba(unsigned char[:] row, unsigned char[:] result): +cpdef int convert_rgb_to_rgba(unsigned char[:] row, unsigned char[:] result) nogil: cdef int i, l, j, k l = min(row.shape[0] / 3, result.shape[0] / 4) for i in range(l): @@ -105,7 +107,7 @@ cpdef int convert_rgb_to_rgba(unsigned char[:] row, unsigned char[:] result): return 0 -cpdef int convert_l_to_rgba(unsigned char[:] row, unsigned char[:] result): +cpdef int convert_l_to_rgba(unsigned char[:] row, unsigned char[:] result) nogil: cdef int i, l, j, k, lum l = min(row.shape[0], result.shape[0] / 4) for i in range(l): @@ -118,7 +120,7 @@ cpdef int convert_l_to_rgba(unsigned char[:] row, unsigned char[:] result): return 0 -cpdef int convert_la_to_rgba(unsigned char[:] row, unsigned char[:] result): +cpdef int convert_la_to_rgba(unsigned char[:] row, unsigned char[:] result) nogil: cdef int i, l, j, k, lum l = min(row.shape[0] / 2, result.shape[0] / 4) for i in range(l): diff --git a/code/pngfilters.py b/code/pngfilters.py index bb31b50d..081f625d 100644 --- a/code/pngfilters.py +++ b/code/pngfilters.py @@ -22,7 +22,6 @@ def undo_filter_up(filter_unit, scanline, previous, result): x = scanline[i] b = previous[i] result[i] = (x + b) & 0xff - print 'x: %d, b: %d, result: %d' % (x, b, result[i]) def undo_filter_average(filter_unit, scanline, previous, result): From 869941a4e584ac891c77d3e57a43b4200c3fd809 Mon Sep 17 00:00:00 2001 From: Joaquin Cuenca Abela Date: Fri, 21 Sep 2012 12:41:22 +0000 Subject: [PATCH 6/8] Integrate my unit tests into png unit tests. --- code/png.py | 97 ++++++++++++++++++++++++++++++++++++++ code/png_test.py | 118 ----------------------------------------------- 2 files changed, 97 insertions(+), 118 deletions(-) delete mode 100644 code/png_test.py diff --git a/code/png.py b/code/png.py index e69c0eea..2d37e3da 100755 --- a/code/png.py +++ b/code/png.py @@ -2833,6 +2833,103 @@ def testNumpyarray(self): img = from_array(pixels, 'L') img.save('testnumpyL16.png') + def paeth(self, x, a, b, c): + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: + pr = a + elif pb <= pc: + pr = b + else: + pr = c + return x - pr + + # test filters and unfilters + def testFilterScanlineFirstLine(self): + fo = 3 # bytes per pixel + line = [30, 31, 32, 230, 231, 232] + out = filter_scanline(0, line, fo, None) # none + self.assertEqual(list(out), [0, 30, 31, 32, 230, 231, 232]) + out = filter_scanline(1, line, fo, None) # sub + self.assertEqual(list(out), [1, 30, 31, 32, 200, 200, 200]) + out = filter_scanline(2, line, fo, None) # up + # TODO: All filtered scanlines start with a byte indicating the filter + # algorithm, except "up". Is this a bug? Should the expected output + # start with 2 here? + self.assertEqual(list(out), [30, 31, 32, 230, 231, 232]) + out = filter_scanline(3, line, fo, None) # average + self.assertEqual(list(out), [3, 30, 31, 32, 215, 216, 216]) + out = filter_scanline(4, line, fo, None) # paeth + self.assertEqual(list(out), [ + 4, self.paeth(30, 0, 0, 0), self.paeth(31, 0, 0, 0), + self.paeth(32, 0, 0, 0), self.paeth(230, 30, 0, 0), + self.paeth(231, 31, 0, 0), self.paeth(232, 32, 0, 0) + ]) + def testFilterScanline(self): + prev = [20, 21, 22, 210, 211, 212] + line = [30, 32, 34, 230, 233, 236] + fo = 3 + out = filter_scanline(0, line, fo, prev) # none + self.assertEqual(list(out), [0, 30, 32, 34, 230, 233, 236]) + out = filter_scanline(1, line, fo, prev) # sub + self.assertEqual(list(out), [1, 30, 32, 34, 200, 201, 202]) + out = filter_scanline(2, line, fo, prev) # up + self.assertEqual(list(out), [2, 10, 11, 12, 20, 22, 24]) + out = filter_scanline(3, line, fo, prev) # average + self.assertEqual(list(out), [3, 20, 22, 23, 110, 112, 113]) + out = filter_scanline(4, line, fo, prev) # paeth + self.assertEqual(list(out), [ + 4, self.paeth(30, 0, 20, 0), self.paeth(32, 0, 21, 0), + self.paeth(34, 0, 22, 0), self.paeth(230, 30, 210, 20), + self.paeth(233, 32, 211, 21), self.paeth(236, 34, 212, 22) + ]) + def testUnfilterScanline(self): + reader = Reader(bytes='') + reader.psize = 3 + scanprev = array('B', [20, 21, 22, 210, 211, 212]) + scanline = array('B', [30, 32, 34, 230, 233, 236]) + def cp(a): + return array('B', a) + + out = reader.undo_filter(0, cp(scanline), cp(scanprev)) + self.assertEqual(list(out), list(scanline)) # none + out = reader.undo_filter(1, cp(scanline), cp(scanprev)) + self.assertEqual(list(out), [30, 32, 34, 4, 9, 14]) # sub + out = reader.undo_filter(2, cp(scanline), cp(scanprev)) + self.assertEqual(list(out), [50, 53, 56, 184, 188, 192]) # up + out = reader.undo_filter(3, cp(scanline), cp(scanprev)) + self.assertEqual(list(out), [40, 42, 45, 99, 103, 108]) # average + out = reader.undo_filter(4, cp(scanline), cp(scanprev)) + self.assertEqual(list(out), [50, 53, 56, 184, 188, 192]) # paeth + def testUnfilterScanlinePaeth(self): + # This tests more edge cases in the paeth unfilter + reader = Reader(bytes='') + reader.psize = 3 + scanprev = array('B', [2, 0, 0, 0, 9, 11]) + scanline = array('B', [6, 10, 9, 100, 101, 102]) + + out = reader.undo_filter(4, scanline, scanprev) + self.assertEqual(list(out), [8, 10, 9, 108, 111, 113]) # paeth + def testIterstraight(self): + def arraify(list_of_str): + return [array('B', s) for s in list_of_str] + reader = Reader(bytes='') + reader.row_bytes = 6 + reader.psize = 3 + rows = reader.iterstraight(arraify(['\x00abcdef', '\x00ghijkl'])) + self.assertEqual(list(rows), arraify(['abcdef', 'ghijkl'])) + + rows = reader.iterstraight(arraify(['\x00abc', 'def\x00ghijkl'])) + self.assertEqual(list(rows), arraify(['abcdef', 'ghijkl'])) + + rows = reader.iterstraight(arraify(['\x00abcdef\x00ghijkl'])) + self.assertEqual(list(rows), arraify(['abcdef', 'ghijkl'])) + + rows = reader.iterstraight(arraify(['\x00abcdef\x00ghi', 'jkl'])) + self.assertEqual(list(rows), arraify(['abcdef', 'ghijkl'])) + # === Command Line Support === def _dehex(s): diff --git a/code/png_test.py b/code/png_test.py deleted file mode 100644 index 0425b41f..00000000 --- a/code/png_test.py +++ /dev/null @@ -1,118 +0,0 @@ -# Use py.test to run these tests -import array -import png - - -def paeth(x, a, b, c): - """Returns the paeth predictor of the pixel x width neightbors a, b, c. - - +-+-+ - |c|b| - +-+-+ - |a|x| - +-+-+ - - See http://www.w3.org/TR/PNG/#9Filter-type-4-Paeth for more details. - """ - p = a + b - c - pa = abs(p - a) - pb = abs(p - b) - pc = abs(p - c) - if pa <= pb and pa <= pc: - pr = a - elif pb <= pc: - pr = b - else: - pr = c - return x - pr - - -def test_filter_scanline_first_line(): - fo = 3 # bytes per pixel - line = [30, 31, 32, 230, 231, 232] - out = png.filter_scanline(0, line, fo, None) # none - assert list(out) == [0, 30, 31, 32, 230, 231, 232] - out = png.filter_scanline(1, line, fo, None) # sub - assert list(out) == [1, 30, 31, 32, 200, 200, 200] - out = png.filter_scanline(2, line, fo, None) # up - # TODO: All filtered scanlines start with a byte indicating the filter - # algorithm, except "up". Is this a bug? - assert list(out) == [30, 31, 32, 230, 231, 232] - out = png.filter_scanline(3, line, fo, None) # average - assert list(out) == [3, 30, 31, 32, 215, 216, 216] - out = png.filter_scanline(4, line, fo, None) # paeth - assert list(out) == [ - 4, paeth(30, 0, 0, 0), paeth(31, 0, 0, 0), paeth(32, 0, 0, 0), - paeth(230, 30, 0, 0), paeth(231, 31, 0, 0), paeth(232, 32, 0, 0) - ] - - -def test_filter_scanline(): - prev = [20, 21, 22, 210, 211, 212] - line = [30, 32, 34, 230, 233, 236] - fo = 3 - out = png.filter_scanline(0, line, fo, prev) # none - assert list(out) == [0, 30, 32, 34, 230, 233, 236] - out = png.filter_scanline(1, line, fo, prev) # sub - assert list(out) == [1, 30, 32, 34, 200, 201, 202] - out = png.filter_scanline(2, line, fo, prev) # up - assert list(out) == [2, 10, 11, 12, 20, 22, 24] - out = png.filter_scanline(3, line, fo, prev) # average - assert list(out) == [3, 20, 22, 23, 110, 112, 113] - out = png.filter_scanline(4, line, fo, prev) # paeth - assert list(out) == [ - 4, paeth(30, 0, 20, 0), paeth(32, 0, 21, 0), paeth(34, 0, 22, 0), - paeth(230, 30, 210, 20), paeth(233, 32, 211, 21), paeth(236, 34, 212, 22) - ] - - -def test_unfilter_scanline(): - reader = png.Reader(bytes='') - reader.psize = 3 - scanprev = array.array('B', [20, 21, 22, 210, 211, 212]) - scanline = array.array('B', [30, 32, 34, 230, 233, 236]) - def cp(a): - return array.array('B', a) - - out = reader.undo_filter(0, cp(scanline), cp(scanprev)) - assert list(out) == list(scanline) # none - out = reader.undo_filter(1, cp(scanline), cp(scanprev)) - assert list(out) == [30, 32, 34, 4, 9, 14] # sub - out = reader.undo_filter(2, cp(scanline), cp(scanprev)) - assert list(out) == [50, 53, 56, 184, 188, 192] # up - out = reader.undo_filter(3, cp(scanline), cp(scanprev)) - assert list(out) == [40, 42, 45, 99, 103, 108] # average - out = reader.undo_filter(4, cp(scanline), cp(scanprev)) - assert list(out) == [50, 53, 56, 184, 188, 192] # paeth - - -def test_unfilter_scanline_paeth(): - # This tests more edge cases in the paeth unfilter - reader = png.Reader(bytes='') - reader.psize = 3 - scanprev = array.array('B', [2, 0, 0, 0, 9, 11]) - scanline = array.array('B', [6, 10, 9, 100, 101, 102]) - - out = reader.undo_filter(4, scanline, scanprev) - assert list(out) == [8, 10, 9, 108, 111, 113] # paeth - - -def arraify(list_of_str): - return [array.array('B', s) for s in list_of_str] - - -def test_iterstraight(): - reader = png.Reader(bytes='') - reader.row_bytes = 6 - reader.psize = 3 - rows = reader.iterstraight(arraify(['\x00abcdef', '\x00ghijkl'])) - assert list(rows) == arraify(['abcdef', 'ghijkl']) - - rows = reader.iterstraight(arraify(['\x00abc', 'def\x00ghijkl'])) - assert list(rows) == arraify(['abcdef', 'ghijkl']) - - rows = reader.iterstraight(arraify(['\x00abcdef\x00ghijkl'])) - assert list(rows) == arraify(['abcdef', 'ghijkl']) - - rows = reader.iterstraight(arraify(['\x00abcdef\x00ghi', 'jkl'])) - assert list(rows) == arraify(['abcdef', 'ghijkl']) From 7c3a2aacff93ddbecd74e56c1bb64fddf7c66eb1 Mon Sep 17 00:00:00 2001 From: Joaquin Cuenca Abela Date: Fri, 21 Sep 2012 12:43:39 +0000 Subject: [PATCH 7/8] Remove obsolete test. --- code/pngfilters_test.py | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 code/pngfilters_test.py diff --git a/code/pngfilters_test.py b/code/pngfilters_test.py deleted file mode 100644 index 8519d07d..00000000 --- a/code/pngfilters_test.py +++ /dev/null @@ -1,14 +0,0 @@ -import array -import pyximport -pyximport.install() -import cpngfilters as pngfilters - - -def test_unfilter_scanline_paeth(): - # This tests more edge cases in the paeth unfilter - scanprev = array.array('B', [2, 0, 0, 0, 9, 11]) - scanline = array.array('B', [6, 10, 9, 100, 101, 102]) - - result = array.array('B', scanline) - pngfilters.undo_filter_paeth(3, scanline, scanprev, result) - assert list(result) == [8, 10, 9, 108, 111, 113] # paeth From db9f958bc41db904b3d2c921b964b4e03d5ee018 Mon Sep 17 00:00:00 2001 From: Joaquin Cuenca Abela Date: Fri, 21 Sep 2012 13:52:24 +0000 Subject: [PATCH 8/8] Integrate pngfilters in png.py. --- code/png.py | 95 +++++++++++++++++++++++++++++++++++++++++++++- code/pngfilters.py | 82 --------------------------------------- 2 files changed, 93 insertions(+), 84 deletions(-) delete mode 100644 code/pngfilters.py diff --git a/code/png.py b/code/png.py index 2d37e3da..b55dd3ab 100755 --- a/code/png.py +++ b/code/png.py @@ -185,7 +185,7 @@ pyximport.install() import cpngfilters as pngfilters except ImportError: - import pngfilters + pass __all__ = ['Image', 'Reader', 'Writer', 'write_chunks', 'from_array'] @@ -2314,6 +2314,97 @@ def _itertools_chain(*iterables): itertools.chain = _itertools_chain +# === Support for users without Cython === + +try: + pngfilters +except: + class pngfilters(object): + def undo_filter_sub(filter_unit, scanline, previous, result): + """Undo sub filter.""" + + ai = 0 + # Loops starts at index fu. Observe that the initial part + # of the result is already filled in correctly with + # scanline. + for i in range(filter_unit, len(result)): + x = scanline[i] + a = result[ai] + result[i] = (x + a) & 0xff + ai += 1 + undo_filter_sub = staticmethod(undo_filter_sub) + + def undo_filter_up(filter_unit, scanline, previous, result): + """Undo up filter.""" + + for i in range(len(result)): + x = scanline[i] + b = previous[i] + result[i] = (x + b) & 0xff + undo_filter_up = staticmethod(undo_filter_up) + + def undo_filter_average(filter_unit, scanline, previous, result): + """Undo up filter.""" + + ai = -filter_unit + for i in range(len(result)): + x = scanline[i] + if ai < 0: + a = 0 + else: + a = result[ai] + b = previous[i] + result[i] = (x + ((a + b) >> 1)) & 0xff + ai += 1 + undo_filter_average = staticmethod(undo_filter_average) + + def undo_filter_paeth(filter_unit, scanline, previous, result): + """Undo Paeth filter.""" + + # Also used for ci. + ai = -filter_unit + for i in range(len(result)): + x = scanline[i] + if ai < 0: + a = c = 0 + else: + a = result[ai] + c = previous[ai] + b = previous[i] + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: + pr = a + elif pb <= pc: + pr = b + else: + pr = c + result[i] = (x + pr) & 0xff + ai += 1 + undo_filter_paeth = staticmethod(undo_filter_paeth) + + def convert_la_to_rgba(row, result): + for i in range(3): + result[i::4] = row[0::2] + result[3::4] = row[1::2] + convert_la_to_rgba = staticmethod(convert_la_to_rgba) + + def convert_l_to_rgba(row, result): + """Convert a grayscale image to RGBA. This method assumes the alpha + channel in result is already correctly initialized.""" + for i in range(3): + result[i::4] = row + convert_l_to_rgba = staticmethod(convert_l_to_rgba) + + def convert_rgb_to_rgba(row, result): + """Convert an RGB image to RGBA. This method assumes the alpha + channel in result is already correctly initialized.""" + for i in range(3): + result[i::4] = row[i::3] + convert_rgb_to_rgba = staticmethod(convert_rgb_to_rgba) + # === Internal Test Support === @@ -2459,7 +2550,7 @@ def testP2(self): x,y,pixels,meta = r.asRGB8() self.assertEqual(x, 1) self.assertEqual(y, 4) - self.assertEqual([list(row) for row in pixels], map(list, [a, b, b, c])) + self.assertEqual(map(list, pixels), map(list, [a, b, b, c])) def testPtrns(self): "Test colour type 3 and tRNS chunk (and 4-bit palette)." a = (50,99,50,50) diff --git a/code/pngfilters.py b/code/pngfilters.py deleted file mode 100644 index 081f625d..00000000 --- a/code/pngfilters.py +++ /dev/null @@ -1,82 +0,0 @@ -import array - - -def undo_filter_sub(filter_unit, scanline, previous, result): - """Undo sub filter.""" - - ai = 0 - # Loops starts at index fu. Observe that the initial part - # of the result is already filled in correctly with - # scanline. - for i in range(filter_unit, len(result)): - x = scanline[i] - a = result[ai] - result[i] = (x + a) & 0xff - ai += 1 - - -def undo_filter_up(filter_unit, scanline, previous, result): - """Undo up filter.""" - - for i in range(len(result)): - x = scanline[i] - b = previous[i] - result[i] = (x + b) & 0xff - - -def undo_filter_average(filter_unit, scanline, previous, result): - """Undo up filter.""" - - ai = -filter_unit - for i in range(len(result)): - x = scanline[i] - if ai < 0: - a = 0 - else: - a = result[ai] - b = previous[i] - result[i] = (x + ((a + b) >> 1)) & 0xff - ai += 1 - - -def undo_filter_paeth(filter_unit, scanline, previous, result): - """Undo Paeth filter.""" - - # Also used for ci. - ai = -filter_unit - for i in range(len(result)): - x = scanline[i] - if ai < 0: - a = c = 0 - else: - a = result[ai] - c = previous[ai] - b = previous[i] - p = a + b - c - pa = abs(p - a) - pb = abs(p - b) - pc = abs(p - c) - if pa <= pb and pa <= pc: - pr = a - elif pb <= pc: - pr = b - else: - pr = c - result[i] = (x + pr) & 0xff - ai += 1 - - -def convert_la_to_rgba(row, result): - for i in range(3): - result[i::4] = row[0::2] - result[3::4] = row[1::2] - - -def convert_l_to_rgba(row, result): - for i in range(3): - result[i::4] = row - - -def convert_rgb_to_rgba(row, result): - for i in range(3): - result[i::4] = row[i::3]