Mikhail Korobov avatar Mikhail Korobov committed a1f54d8

optional cython extension for fast zip-with-prediction decompression; 'encoding' module is renamed to 'compression'

Comments (0)

Files changed (8)

 \.ipynb$
 ^dist
 ^psd_tools.egg-info
-^\.coverage$
+^\.coverage$
+\.html$
 include AUTHORS.rst
 include README.rst
 include CHANGES.rst
+
+recursive-include src *.c *.pxd *.pyx
 #!/usr/bin/env python
 from distutils.core import setup
+from distutils.extension import Extension
 
 import sys
 
     if cmd in sys.argv:
         from setuptools import setup
 
-setup(
+setup_args = dict(
     name = 'psd-tools',
     version = '0.2',
     author = 'Mikhail Korobov',
     package_dir = {'': 'src'},
     packages = ['psd_tools', 'psd_tools.reader', 'psd_tools.decoder', 'psd_tools.user_api'],
     scripts=['bin/psd-tools.py'],
-    requires=['docopt', 'Pillow'],
 
     classifiers=[
         'Development Status :: 3 - Alpha',
         'Topic :: Software Development :: Libraries :: Python Modules',
     ],
 )
+
+# ========== make extension optional (copied from coverage.py) =========
+
+compile_extension = True
+
+if sys.platform.startswith('java'):
+    # Jython can't compile C extensions
+    compile_extension = False
+
+if '__pypy__' in sys.builtin_module_names:
+    # Cython extensions are slow under PyPy
+    compile_extension = False
+
+if compile_extension:
+    setup_args.update(dict(
+        ext_modules = [
+            Extension("psd_tools._compression", sources=["src/psd_tools/_compression.c"])
+        ],
+    ))
+
+# For a variety of reasons, it might not be possible to install the C
+# extension.  Try it with, and if it fails, try it without.
+try:
+    setup(**setup_args)
+except:     # pylint: disable=W0702
+    # When setup() can't compile, it tries to exit.  We'll catch SystemExit
+    # here :-(, and try again.
+    if 'install' not in sys.argv or 'ext_modules' not in setup_args:
+        # We weren't trying to install an extension, so forget it.
+        raise
+    msg = "Couldn't install with extension module, trying without it..."
+    exc = sys.exc_info()[1]
+    exc_msg = "%s: %s" % (exc.__class__.__name__, exc)
+    print("**\n** %s\n** %s\n**" % (msg, exc_msg))
+
+    del setup_args['ext_modules']
+    setup(**setup_args)

src/psd_tools/_compression.pyx

+"""
+Cython extension with utilities for "zip-with-prediction"
+decompression method.
+"""
+cimport cpython.array
+
+def _delta_decode(arr, int mod, int w, int h):
+    if mod == 256:
+        _delta_decode_bytes(arr, w, h)
+        return arr
+    elif mod == 256*256:
+        _delta_decode_words(arr, w, h)
+        arr.byteswap()
+        return arr
+    else:
+        raise NotImplementedError
+
+
+cdef _delta_decode_bytes(unsigned char[:] arr, int w, int h):
+    cdef int x, y, pos, offset
+    for y in range(h):
+        offset = y*w
+        for x in range(w-1):
+            pos = offset + x
+            arr[pos+1] += arr[pos]
+
+cdef _delta_decode_words(unsigned short[:] arr, int w, int h):
+    cdef int x, y, pos, offset
+    for y in range(h):
+        offset = y*w
+        for x in range(w-1):
+            pos = offset + x
+            arr[pos+1] += arr[pos]
+
+
+def _restore_byte_order(bytes_array, int w, int h):
+    cdef bytes_copy = bytes_array[:]
+    cdef unsigned char [:] src = bytes_array, dst = bytes_copy
+    cdef int i = 0
+    cdef int b, x, y, row_start
+
+    for y in range(h):
+        row_start = y*w*4
+        for x in range(w):
+            for b in range(4):
+                dst[i+b] = src[row_start + w*b + x]
+            i += 4
+
+    return bytes_copy.tostring()

src/psd_tools/compression.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+import array
+from psd_tools.utils import be_array_from_bytes
+
+def decode_prediction(data, w, h, bytes_per_pixel):
+    if bytes_per_pixel == 1:
+        arr = be_array_from_bytes("B", data)
+        arr = _delta_decode(arr, 2**8, w, h)
+
+    elif bytes_per_pixel == 2:
+        arr = be_array_from_bytes("H", data)
+        arr = _delta_decode(arr, 2**16, w, h)
+
+    elif bytes_per_pixel == 4:
+
+        # 32bit channels are also encoded using delta encoding,
+        # but it make no sense to apply delta compression to bytes.
+        # It is possible to apply delta compression to 2-byte or 4-byte
+        # words, but it seems it is not the best way either.
+        # In PSD, each 4-byte item is split into 4 bytes and these
+        # bytes are packed together: "123412341234" becomes "111222333444";
+        # delta compression is applied to the packed data.
+        #
+        # So we have to (a) decompress data from the delta compression
+        # and (b) recombine data back to 4-byte values.
+
+        arr = array.array(str("B"), data)
+        arr = _delta_decode(arr, 2**8, w*4, h)
+        arr = _restore_byte_order(arr, w, h)
+        arr = array.array(str("f"), arr)
+    else:
+        return None
+
+    return arr.tostring()
+
+def _delta_decode(arr, mod, w, h):
+    for y in range(h):
+        offset = y*w
+        for x in range(w-1):
+            pos = offset + x
+            next_value = (arr[pos+1] + arr[pos]) % mod
+            arr[pos+1] = next_value
+    arr.byteswap()
+    return arr
+
+def _restore_byte_order(bytes_array, w, h):
+    arr = bytes_array[:]
+    i = 0
+    rng4 = range(4)
+    for y in range(h):
+        row_start = y*w*4
+        offsets = row_start, row_start+w, row_start+w*2, row_start+w*3
+        for x in range(w):
+            for bt in rng4:
+                arr[i] = bytes_array[offsets[bt] + x]
+                i += 1
+    return arr.tostring()
+
+# Replace _delta_decode and _restore_byte_order with faster versions (from
+# a compiled extension) if this is possible:
+try:
+    from ._compression import _delta_decode, _restore_byte_order
+except ImportError:
+    pass

src/psd_tools/encoding.py

-# -*- coding: utf-8 -*-
-from __future__ import absolute_import, unicode_literals
-import array
-from psd_tools.utils import be_array_from_bytes
-
-def decode_prediction(data, w, h, bytes_per_pixel):
-    if bytes_per_pixel == 1:
-        arr = be_array_from_bytes("B", data)
-        arr = _delta_decode(arr, 2**8, w, h)
-
-    elif bytes_per_pixel == 2:
-        arr = be_array_from_bytes("H", data)
-        arr = _delta_decode(arr, 2**16, w, h)
-
-    elif bytes_per_pixel == 4:
-
-        # 32bit channels are also encoded using delta encoding,
-        # but it make no sense to apply delta compression to bytes.
-        # It is possible to apply delta compression to 2-byte or 4-byte
-        # words, but it seems it is not the best way either.
-        # In PSD, each 4-byte item is split into 4 bytes and these
-        # bytes are packed together: "123412341234" becomes "111222333444";
-        # delta compression is applied to the packed data.
-        #
-        # So we have to (a) decompress data from the delta compression
-        # and (b) recombine data back to 4-byte values.
-
-        arr = array.array(str("B"), data)
-        arr = _delta_decode(arr, 2**8, w*4, h)
-        arr = _restore_byte_order(arr, w, h)
-        arr = array.array(str("f"), arr.tostring())
-    else:
-        return None
-
-    return arr.tostring()
-
-def _delta_decode(arr, mod, w, h):
-    for y in range(h):
-        offset = y*w
-        for x in range(w-1):
-            pos = offset + x
-            next_value = (arr[pos+1] + arr[pos]) % mod
-            arr[pos+1] = next_value
-    arr.byteswap()
-    return arr
-
-def _restore_byte_order(bytes_array, w, h):
-    arr = bytes_array[:]
-    i = 0
-    rng4 = range(4)
-    for y in range(h):
-        row_start = y*w*4
-        offsets = row_start, row_start+w, row_start+w*2, row_start+w*3
-        for x in range(w):
-            for bt in rng4:
-                arr[i] = bytes_array[offsets[bt] + x]
-                i += 1
-    return arr

src/psd_tools/reader/layers.py

 import array
 
 from psd_tools.utils import (read_fmt, read_pascal_string,
-                             read_be_array, be_array_from_bytes,
-                             trimmed_repr, pad, synchronize, debug_view)
+                             read_be_array, trimmed_repr, pad, synchronize,
+                             debug_view)
 from psd_tools.exceptions import Error
 from psd_tools.constants import (Compression, Clipping, BlendMode,
                                  ChannelID, TaggedBlock)
-from psd_tools import encoding
+from psd_tools import compression
 
 logger = logging.getLogger(__name__)
 
             w, h = layer.width(), layer.height()
 
         start_pos = fp.tell()
-        compression = read_fmt("H", fp)[0]
+        compress_type = read_fmt("H", fp)[0]
 
         data = None
 
-        if compression == Compression.RAW:
+        if compress_type == Compression.RAW:
             data_size = w * h * bytes_per_pixel
             data = fp.read(data_size)
 
-        elif compression == Compression.PACK_BITS:
+        elif compress_type == Compression.PACK_BITS:
             byte_counts = read_be_array("H", h, fp)
             data_size = sum(byte_counts) * bytes_per_pixel
             data = fp.read(data_size)
 
-        elif compression == Compression.ZIP:
+        elif compress_type == Compression.ZIP:
             data = zlib.decompress(fp.read(channel.length - 2))
 
-        elif compression == Compression.ZIP_WITH_PREDICTION:
+        elif compress_type == Compression.ZIP_WITH_PREDICTION:
             decompressed = zlib.decompress(fp.read(channel.length - 2))
-            data = encoding.decode_prediction(decompressed, w, h, bytes_per_pixel)
+            data = compression.decode_prediction(decompressed, w, h, bytes_per_pixel)
 
         if data is None:
             return []
 
-        channel_data.append(ChannelData(compression, data))
+        channel_data.append(ChannelData(compress_type, data))
 
         remaining_bytes = channel.length - (fp.tell() - start_pos) - 2
         if remaining_bytes > 0:
     Reads merged image pixel data which is stored at the end of PSD file.
     """
     w, h = header.width, header.height
-    compression = read_fmt("H", fp)[0]
+    compress_type = read_fmt("H", fp)[0]
 
     bytes_per_pixel = header.depth // 8
 
     channel_byte_counts = []
-    if compression == Compression.PACK_BITS:
+    if compress_type == Compression.PACK_BITS:
         for ch in range(header.number_of_channels):
             channel_byte_counts.append(read_be_array("H", h, fp))
 
 
         data = None
 
-        if compression == Compression.RAW:
+        if compress_type == Compression.RAW:
             data_size = w * h * bytes_per_pixel
             data = fp.read(data_size)
 
-        elif compression == Compression.PACK_BITS:
+        elif compress_type == Compression.PACK_BITS:
             byte_counts = channel_byte_counts[channel_id]
             data_size = sum(byte_counts) * bytes_per_pixel
             data = fp.read(data_size)
 
-        elif compression == Compression.ZIP:
+        elif compress_type == Compression.ZIP:
             warnings.warn("ZIP compression of composite image is not supported.")
 
-        elif compression == Compression.ZIP_WITH_PREDICTION:
+        elif compress_type == Compression.ZIP_WITH_PREDICTION:
             warnings.warn("ZIP_WITH_PREDICTION compression of composite image is not supported.")
 
         if data is None:
             return []
-        channel_data.append(ChannelData(compression, data))
+        channel_data.append(ChannelData(compress_type, data))
 
     return channel_data
+#!/bin/sh
+cython src/psd_tools/*.pyx -a
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.