Commits

Mikhail Korobov committed 0b3bda3

initial import

Comments (0)

Files changed (19)

+#projects
+\.idea
+
+#temp files
+\.pyc
+\.orig
+
+#os files
+\.DS_Store
+Thumbs.db
+
+#project-specific files
+\.tox
+stuff
+MANIFEST$
+^build
+\.ipynb$
+^dist
+Copyright (c) 2012 Mikhail Korobov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+include AUTHORS.rst
+include README.rst
+include CHANGES.rst
+include docs/Makefile
+include docs/make.bat
+include docs/conf.py
+
+recursive-include docs *.rst
+recursive-include benchmarks *.py
+psd-tools
+=========
+
+``psd-tools`` is a package for reading Adobe Photoshop PSD files
+(as described in specification_) to Python data structures.
+
+.. _specification: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/PhotoshopFileFormats.htm
+
+Why yet another PSD reader?
+---------------------------
+
+There are existing PSD readers for Python:
+
+* `psdparse <https://github.com/jerem/psdparse>`;
+* `pypsd <https://code.google.com/p/pypsd>`_;
+* there is a PSD reader in PIL library.
+
+PSD reader in PIL is incomplete, and contributing to PIL is somehow
+complicated because of the slow release process.
+
+I also considered contributing to pypsd or psdparse, but they are
+GPL and I was not totally satisfied with the interface and the code
+(they are really fine, it's me having specific style requirements).
+
+So I finally decided to roll out yet another implementation
+that should be MIT-licensed, systematically based on the specification_
+and implemented as a set of functions; it should also support both
+Python 2.x and Python 3.x.
+
+Design overview
+---------------
+
+The process of handling a PSD file is splitted into 3 stages:
+
+1) "PSD reading": the file is read and parsed to low-level data
+   structures that closely match the specification. No PIL images
+   are constructed; image resources blocks and additional layer
+   information are extracted but not parsed (they remain just keys
+   with a binary data). The goal is to extract all necessary
+   information from a PSD file.
+
+2) "Detailed parsing": image resource blocks and additional layer
+   information blocks are parsed to a more detailed data structures
+   (that are still based on a specification). There are a lot of PSD
+   data types and the library currently doesn't handle them all, but
+   it should be easy to add the parsing code for the missing PSD data
+   structures if needed.
+
+3) "User-facing API": PIL images of the PSD layers are created and
+   combined to a user-friendly data structure.
+
+.. note::
+
+    Currently only (1) is partially implemented.
+
+Contributing
+------------
+
+Development happens at github and bitbucket:
+
+* https://github.com/kmike/psd-tools
+* https://bitbucket.org/kmike/psd-tools
+
+The main issue tracker is at github: https://github.com/kmike/psd-tools/issues
+
+Feel free to submit ideas, bugs, pull requests (git or hg) or regular patches.
+
+The license is MIT.
+#!/usr/bin/env python
+import sys
+import psd_tools.cli
+sys.exit(psd_tools.cli.main())
+

psd_tools/__init__.py

Empty file added.
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+import logging
+import docopt
+import pprint
+
+import psd_tools.reader
+
+logger = logging.getLogger('psd_tools')
+logger.addHandler(logging.StreamHandler())
+
+def main():
+    """
+    psd-tools.py
+
+    Usage:
+        psd-tools.py <filename> [--encoding <encoding>] [--verbose]
+        psd-tools.py -h | --help
+        psd-tools.py --version
+
+    Options:
+        -v --verbose                Be more verbose.
+        --encoding <encoding>       Text encoding [default: latin1].
+
+    """
+    args = docopt.docopt(main.__doc__)
+
+    if args['--verbose']:
+        logger.setLevel(logging.DEBUG)
+    else:
+        logger.setLevel(logging.INFO)
+
+    with open(args['<filename>'], 'rb') as f:
+        res = psd_tools.reader.parse(f, args['--encoding'])
+        pprint.pprint(res)
+

psd_tools/constants.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
+class Enum(object):
+
+    @classmethod
+    def _attributes(cls):
+        return [name for name in dir(cls) if name.isupper()]
+
+    @classmethod
+    def is_known(cls, value):
+        for name in cls._attributes():
+            if getattr(cls, name) == value:
+                return True
+        return False
+
+class ColorMode(Enum):
+    BITMAP = 0
+    GRAYSCALE = 1
+    INDEXED = 2
+    RGB = 3
+    CMYK = 4
+    MULTICHANNEL = 7
+    DUOTONE = 8
+    LAB = 9
+
+class ChannelID(Enum):
+    RED = 0
+    GREEN = 1
+    BLUE = 2
+    TRANSPARENCY_MASK = -1
+    USER_LAYER_MASK = -2
+    REAL_USER_LAYER_MASK = -3
+
+class ImageResourceID(Enum):
+    OBSOLETE1 = 1000
+    MAC_PRINT_MANAGER_INFO = 1001
+    OBSOLETE2 = 1003
+    RESOLUTION_INFO = 1005
+    ALPHA_NAMES_PASCAL = 1006
+    DISPLAY_INFO_OBSOLETE = 1007
+    CAPTION_PASCAL = 1008
+    BORDER_INFO = 1009
+    BACKGROUND_COLOR = 1010
+    PRINT_FLAGS = 1011
+    GRAYSCALE_HALFTONING_INFO = 1012
+    COLOR_HALFTONING_INFO = 1013
+    DUOTONE_HALFTONING_INFO = 1014
+    GRAYSCALE_TRANSFER_FUNCTION = 1015
+    COLOR_TRANSFER_FUNCTION = 1016
+    DUOTONE_TRANSFER_FUNCTION = 1017
+    DUOTONE_IMAGE_INFO = 1018
+    EFFECTIVE_BW = 1019
+    OBSOLETE3 = 1020
+    EPS_OPTIONS = 1021
+    QUICK_MASK_INFO = 1022
+    OBSOLETE4 = 1023
+    LAYER_STATE_INFO = 1024
+    WORKING_PATH = 1025
+    LAYER_GROUP_INFO = 1026
+    OBSOLETE5 = 1027
+    IPTC_NAA = 1028
+    IMAGE_MODE_RAW = 1029
+    JPEG_QUALITY = 1030
+    GRID_AND_GUIDES_INFO = 1032
+    THUMBNAIL_RESOURCE_PS4 = 1033
+    COPYRIGHT_FLAG = 1034
+    URL = 1035
+    THUMBNAIL_RESOURCE = 1036
+    GLOBAL_ANGLE_OBSOLETE = 1037
+    COLOR_SAMPLERS_RESOURCE_OBSOLETE = 1038
+    ICC_PROFILE = 1039
+    WATERMARK = 1040
+    ICC_UNTAGGED_PROFILE = 1041
+    EFFECTS_VISIBLE = 1042
+    SPOT_HALFTONE = 1043
+    IDS_SEED_NUMBER = 1044
+    ALPHA_NAMES_UNICODE = 1045
+    INDEXED_COLOR_TABLE_COUNT = 1046
+    TRANSPARENCY_INDEX = 1047
+    GLOBAL_ALTITUD = 1049
+    SLICES = 1050
+    WORKFLOW_URL = 1051
+    JUMP_TO_XPEP = 1052
+    ALPHA_IDENTIFIERS = 1053
+    URL_LIST = 1054
+    VERSION_INFO = 1057
+    EXIF_DATA_1 = 1058
+    EXIF_DATA_3 = 1059
+    XMP_METADATA = 1060
+    CAPTION_DIGEST = 1061
+    PRINT_SCALE = 1062
+    PIXEL_ASPECT_RATIO = 1064
+    LAYER_COMPS = 1065
+    ALTERNATE_DUOTONE_COLORS = 1066
+    ALTERNATE_SPOT_COLORS = 1067
+    LAYER_SELECTION_IDS = 1069
+    HDR_TONING_INFO = 1070
+    PRINT_INFO_CS2 = 1071
+    LAYER_GROUPS_ENABLED_ID = 1072
+    COLOR_SAMPLERS_RESOURCE = 1073
+    MEASURMENT_SCALE = 1074
+    TIMELINE_INFO = 1075
+    SHEET_DISCLOSURE = 1076
+    DISPLAY_INFO = 1077
+    ONION_SKINS = 1078
+    COUNT_INFO = 1080
+    PRINT_INFO_CS5 = 1082
+    PRINT_STYLE = 1083
+    MAC_NSPRINTINFO = 1084
+    WINDOWS_DEVMODE = 1085
+    AUTO_SAVE_FILE_PATH = 1086
+    AUTO_SAVE_FORMAT = 1087
+
+    # PATH_INFO = 2000...2997
+    PATH_INFO_FIRST = 2000
+    PATH_INFO_LAST = 2997
+    CLIPPING_PATH_NAME = 2999
+
+    # PLUGIN_RESOURCES = 4000..4999
+    PLUGIN_RESOURCES_FIRST = 4000
+    PLUGIN_RESOURCES_LAST = 4999
+
+    IMAGE_READY_VARIABLES = 7000
+    IMAGE_READY_DATA_SETS = 7001
+    LIGHTROOM_WORKFLOW = 8000
+    PRINT_FLAGS_INFO = 10000
+
+class BlendMode(Enum):
+    PASS_THROUGH = 'pass'
+    NORMAL = 'norm'
+    DISSOLVE = 'diss'
+    DARKEN = 'dark'
+    MULTIPLY = 'mul '
+    COLOR_BURN = 'idiv'
+    LINEAR_BURN = 'lbrn'
+    DARKER_COLOR = 'dkCl'
+    LIGHTEN = 'lite'
+    SCREEN = 'scrn'
+    COLOR_DODGE = 'div '
+    LINEAR_DODGE = 'lddg'
+    LIGHTER_COLOR = 'lgCl'
+    OVERLAY = 'over'
+    SOFT_LIGHT = 'sLit'
+    HARD_LIGHT = 'hLit'
+    VIVID_LIGHT = 'vLit'
+    LINEAR_LIGHT = 'lLit'
+    PIN_LIGHT = 'pLit'
+    HARD_MIX = 'hMix'
+    DIFFERENCE = 'diff'
+    EXCLUSION = 'smud'
+    SUBTRACT = 'fsub'
+    DIVIDE = 'fdiv'
+    HUE = 'hue '
+    SATURATION = 'sat '
+    COLOR = 'colr'
+    LUMINOSITY = 'lum '
+
+class Clipping(Enum):
+    BASE = 0
+    NON_BASE = 1
+
+class GlobalLayerMaskKind(Enum):
+    COLOR_SELECTED = 0
+    COLOR_PROTECTED = 1
+    PER_LAYER = 128
+    # others options are possible in beta versions.
+
+class Compression(Enum):
+    RAW = 0
+    PACK_BITS = 1
+    ZIP = 2
+    ZIP_WITH_PREDICTION = 3

psd_tools/exceptions.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+class Error(Exception):
+    pass

psd_tools/reader/__init__.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+from .reader import parse

psd_tools/reader/color_mode_data.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+import logging
+from psd_tools.utils import read_fmt
+
+logger = logging.getLogger(__name__)
+
+def read(fp):
+    """
+    Reads data from the color mode data section.
+
+    For indexed color images the data is the color table
+    for the image in a non-interleaved order.
+
+    Duotone images also have this data, but the data format is undocumented.
+    """
+    logger.debug("reading color mode data..")
+    length = read_fmt("I", fp)[0]
+    data = fp.read(length)
+    return data

psd_tools/reader/header.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+import logging
+import collections
+
+from psd_tools.exceptions import Error
+from psd_tools.utils import read_fmt
+from psd_tools.constants import ColorMode
+
+logger = logging.getLogger(__name__)
+
+Header = collections.namedtuple("PsdHeader", "number_of_channels, height, width, depth, color_mode")
+
+def read(fp):
+    """
+    Reads PSD file header.
+    """
+    logger.debug("reading header..")
+    signature = fp.read(4)
+    if signature != b'8BPS':
+        raise Error("This is not a PSD file")
+
+    version = read_fmt("H", fp)[0]
+    if version != 1:
+        raise Error("Unsupported PSD version (%s)" % version)
+
+    header = Header(*read_fmt("6x HIIHH", fp))
+
+    if not ColorMode.is_known(header.color_mode):
+        raise Error("Unknown color mode: %s" % header.color_mode)
+
+    logger.debug(header)
+    return header

psd_tools/reader/image_resources.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals, division
+import collections
+import logging
+
+from psd_tools.utils import read_fmt, trimmed_repr, read_pascal_string, pad
+from psd_tools.exceptions import Error
+
+logger = logging.getLogger(__name__)
+
+class ImageResource(collections.namedtuple("ImageResource", "resource_id, name, data")):
+    def __repr__(self):
+        return "ImageResource(%r, %r, %s)" % (
+            self.resource_id, self.name, trimmed_repr(self.data, 20))
+
+
+def read(fp, encoding):
+    """ Reads image resources. """
+    logger.debug("reading image resources..")
+
+    resource_section_length = read_fmt("I", fp)[0]
+    position = fp.tell()
+    blocks = []
+    while fp.tell() < position + resource_section_length:
+        block = _read_block(fp, encoding)
+        logger.debug("%r", block)
+        blocks.append(block)
+    return blocks
+
+def _read_block(fp, encoding):
+    """
+    Reads single image resource block. Such blocks contain non-pixel data
+    for the images (e.g. pen tool paths).
+    """
+    sig = fp.read(4)
+    if sig != b'8BIM':
+        raise Error("Invalid resource signature (%r)" % sig)
+
+    resource_id = read_fmt("H", fp)[0]
+    name = read_pascal_string(fp, encoding, 2)
+    fp.seek(1, 1) # XXX: why is this needed??
+
+    data_size = read_fmt("I", fp)[0]
+    data = fp.read(pad(data_size, 2))
+
+    return ImageResource(resource_id, name, data)

psd_tools/reader/layers.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals, division
+import collections
+import logging
+
+from psd_tools.utils import read_fmt, read_pascal_string, read_be_array
+from psd_tools.exceptions import Error
+from psd_tools.constants import Compression, Clipping, BlendMode
+
+logger = logging.getLogger(__name__)
+
+LayerRecord = collections.namedtuple('LayerRecord', [
+    'top', 'left', 'bottom', 'right',
+    'num_channels', 'channels',
+    'blend_mode', 'opacity', 'cilpping', 'flags',
+    'mask_data', 'blending_ranges', 'name',
+    'tagged_blocks'
+])
+
+ChannelInfo = collections.namedtuple('ChannelInfo', 'id length')
+LayerMaskData = collections.namedtuple('LayerMaskData', 'top left bottom right default_color flags real_flags real_background')
+
+class ChannelData(collections.namedtuple('ChannelData', 'compression data')):
+    def __repr__(self):
+        return "ChannelData(compression=%r, size(data)=%r" % (
+            self.compression, len(self.data) if self.data is not None else None
+        )
+
+GlobalMaskInfo = collections.namedtuple('GlobalMaskInfo', 'overlay color_components opacity kind')
+
+def read(fp, encoding):
+    """
+    Reads layers and masks information.
+    """
+    length = read_fmt("I", fp)[0]
+    start_position = fp.tell()
+
+    layer_info = _read_layer(fp, encoding)
+
+    # XXX: are tagged blocks really after the layers?
+    # XXX: does global mask reading really work?
+    global_mask_info = _read_global_mask_info(fp)
+
+    consumed_bytes = fp.tell() - start_position
+    tagged_blocks = _read_layer_tagged_blocks(fp, length - consumed_bytes)
+
+    consumed_bytes = fp.tell() - start_position
+    fp.seek(length-consumed_bytes, 1)
+
+    return layer_info, global_mask_info, tagged_blocks
+
+def _read_layer(fp, encoding):
+    """
+    Reads info about layers.
+    """
+    length = read_fmt("I", fp)[0]
+    layer_count = read_fmt("h", fp)[0]
+
+    layers = []
+    for idx in range(abs(layer_count)):
+        layer = _read_layer_record(fp, encoding)
+        layers.append(layer)
+
+    channel_image_data = []
+    for layer in layers:
+
+        data = _read_channel_image_data(fp, layer)
+        channel_image_data.append(data)
+
+    return length, layer_count, layers, channel_image_data
+
+def _read_layer_record(fp, encoding):
+    """
+    Reads single layer record.
+    """
+    top, left, bottom, right = read_fmt("4i", fp)
+    num_channels = read_fmt("H", fp)[0]
+
+    channel_info = []
+    for channel_num in range(num_channels):
+        info = ChannelInfo(*read_fmt("hI", fp))
+        channel_info.append(info)
+
+    sig = fp.read(4)
+    if sig != b'8BIM':
+        raise Error("Error parsing layer: invalid signature (%r)" % sig)
+
+    blend_mode = fp.read(4).decode('ascii')
+    if not BlendMode.is_known(blend_mode):
+        raise Error("Unknown blend mode (%s)" % blend_mode)
+
+    opacity, clipping, flags, extra_length = read_fmt("BBBxI", fp)
+
+    if not Clipping.is_known(clipping):
+        raise Error("Unknown clipping: %s" % clipping)
+
+    start = fp.tell()
+    mask_data = _read_layer_mask_data(fp)
+    blending_ranges = _read_layer_blending_ranges(fp)
+    name = read_pascal_string(fp, encoding, 1) # XXX: spec says padding should be 4?
+
+    remaining_length = extra_length - (fp.tell()-start)
+    tagged_blocks = _read_layer_tagged_blocks(fp, remaining_length)
+
+    remaining_length = extra_length - (fp.tell()-start)
+    fp.seek(remaining_length, 1) # skip the reminder
+
+    return LayerRecord(
+        top, left, bottom, right,
+        num_channels, channel_info,
+        blend_mode, opacity, clipping, flags,
+        mask_data, blending_ranges, name,
+        tagged_blocks
+    )
+
+def _read_layer_tagged_blocks(fp, remaining_length):
+    """
+    Reads a section of tagged blocks with additional layer information.
+    """
+    blocks = []
+    start_pos = fp.tell()
+    read_bytes = 0
+    while read_bytes < remaining_length:
+        block = _read_additional_layer_info_block(fp)
+        if block is None:
+            break
+        blocks.append(block)
+        read_bytes = fp.tell() - start_pos
+    return blocks
+
+def _read_additional_layer_info_block(fp):
+    """
+    Reads a tagged block with additional layer information.
+    """
+    sig = fp.read(4)
+    if sig not in [b'8BIM', b'8B64']:
+        fp.seek(-4, 1)
+        return
+
+    key = fp.read(4)
+    length = read_fmt("I", fp)[0]
+    data = fp.read(length)
+    return key, data
+
+def _read_layer_mask_data(fp):
+    """ Reads layer mask or adjustment layer data. """
+    size = read_fmt("I", fp)[0]
+    if size not in [0, 20, 36]:
+        raise Error("Invalid layer data size: %d" % size)
+
+    if not size:
+        return
+
+    top, left, bottom, right, default_color, flags = read_fmt("4i 2B", fp)
+    if size == 20:
+        fp.seek(2, 1)
+        real_flags, real_background = None, None
+    else:
+        real_flags, real_background = read_fmt("2B", fp)
+        fp.seek(16, 1)
+
+    return LayerMaskData(top, left, bottom, right, default_color, flags, real_flags, real_background)
+
+def _read_layer_blending_ranges(fp):
+    """ Reads layer blending data. """
+    length = read_fmt("I", fp)[0]
+    fp.read(length) # skip; this is not implemented
+
+def _read_channel_image_data(fp, layer):
+    """
+    Reads image data for all channels in a layer.
+    """
+    w, h = (layer.right - layer.left), (layer.bottom - layer.top)
+
+    channel_data = []
+
+    for channel in layer.channels:
+        start_pos = fp.tell()
+        compression = read_fmt("H", fp)[0]
+
+        if compression == Compression.RAW:
+            data = fp.read(w*h)
+            channel_data.append(ChannelData(compression, data))
+
+        elif compression == Compression.PACK_BITS:
+            byte_counts = read_be_array(fp, "H", h)
+            data = fp.read(sum(byte_counts))
+            channel_data.append(ChannelData(compression, data))
+
+        elif Compression.is_known(compression):
+            raise Error("This compression type is not implemented (%d)" % compression)
+        else:
+            raise Error("Unknown compression type: %d" % compression)
+
+        remaining_bytes = channel.length - (fp.tell() - start_pos) - 2
+        if remaining_bytes > 0:
+            fp.seek(remaining_bytes, 1)
+
+    return channel_data
+
+
+def _read_global_mask_info(fp):
+    """
+    Reads global layer mask info.
+    """
+    # XXX: Does it really work properly? What is it for?
+    start_pos = fp.tell()
+    length, overlay_color_space, c1, c2, c3, c4, opacity, kind = read_fmt("IH 4H HB", fp)
+    filler_length = length - (fp.tell()-start_pos)
+    if filler_length > 0:
+        fp.seek(filler_length, 1)
+
+    return GlobalMaskInfo(overlay_color_space, (c1, c2, c3, c4), opacity, kind)
+
+def read_image_data(fp, header):
+    """
+    Reads merged image pixel data which is stored at the end of PSD file.
+    """
+    w, h = header.height, header.width
+    compression = read_fmt("H", fp)[0]
+
+    channel_byte_counts = []
+    if compression == Compression.PACK_BITS:
+        for ch in range(header.number_of_channels):
+            channel_byte_counts.append(read_be_array(fp, "H", h))
+
+    channel_data = []
+    for channel_id in range(header.number_of_channels):
+
+        if compression == Compression.RAW:
+            data = fp.read(w*h)
+            channel_data.append(ChannelData(compression, data))
+
+        elif compression == Compression.PACK_BITS:
+            byte_counts = channel_byte_counts[channel_id]
+            data = fp.read(sum(byte_counts))
+            channel_data.append(ChannelData(compression, data))
+
+        elif Compression.is_known(compression):
+            raise Error("This compression type is not implemented (%d)" % compression)
+        else:
+            raise Error("Unknown compression type: %d" % compression)
+
+    return channel_data

psd_tools/reader/reader.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals, division
+import logging
+
+import psd_tools.reader.header
+import psd_tools.reader.color_mode_data
+import psd_tools.reader.image_resources
+import psd_tools.reader.layers
+
+logger = logging.getLogger(__name__)
+
+def parse(fp, encoding='latin1'):
+    header = psd_tools.reader.header.read(fp)
+    color_data = psd_tools.reader.color_mode_data.read(fp)
+    image_resource_blocks = psd_tools.reader.image_resources.read(fp, encoding)
+    layer_and_mask_info = psd_tools.reader.layers.read(fp, encoding)
+    image_data = psd_tools.reader.layers.read_image_data(fp, header)
+
+    return header, color_data, image_resource_blocks, layer_and_mask_info, image_data
+

psd_tools/utils.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, division, unicode_literals
+
+import sys
+import struct
+import array
+
+def read_fmt(fmt, fp):
+    """
+    Reads data from ``fp`` according to ``fmt``.
+    """
+    fmt = str(">" + fmt)
+    fmt_size = struct.calcsize(fmt)
+    data = fp.read(fmt_size)
+    assert len(data) == fmt_size, (len(data), fmt_size)
+    return struct.unpack(fmt, data)
+
+def pad(number, divisor):
+    if number % divisor:
+        number = (number // divisor + 1) * divisor
+    return number
+
+def read_pascal_string(fp, encoding, padding=1):
+    length = pad(read_fmt("B", fp)[0], padding)
+    return fp.read(length).decode(encoding, errors='replace')
+
+def read_be_array(fp, fmt, count):
+    """
+    Reads an array from a file with big-endian data.
+    """
+    arr = array.array(str(fmt))
+    arr.fromfile(fp, count)
+    if sys.byteorder == 'little':
+        arr.byteswap()
+    return arr
+
+def trimmed_repr(data, trim_length):
+    if data is not None and len(data) > trim_length:
+        return repr(data[:trim_length] + b' ...')
+    else:
+        return repr(data)
+
+#!/usr/bin/env python
+from distutils.core import setup
+
+import sys
+
+for cmd in ('egg_info', 'develop'):
+    if cmd in sys.argv:
+        from setuptools import setup
+
+setup(
+    name = 'psd-tools',
+    version = '0.0.1',
+    author = 'Mikhail Korobov',
+    author_email = 'kmike84@gmail.com',
+    url = 'https://github.com/kmike/psd-tools',
+
+    description = 'PSD tools',
+    long_description = open('README.rst').read() + open('CHANGES.rst').read(),
+
+    license = 'MIT License',
+    packages = ['psd_tools', 'psd_tools.reader'],
+    scripts=['bin/psd-tools.py'],
+    requires=['docopt'],
+
+    classifiers=[
+        'Development Status :: 2 - Pre-Alpha',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: MIT License',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.2',
+        'Programming Language :: Python :: 3.3',
+        'Topic :: Multimedia :: Graphics',
+        'Topic :: Multimedia :: Graphics :: Viewers',
+        'Topic :: Multimedia :: Graphics :: Graphics Conversion',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+    ],
+)
+; this is a tox config for running "psd-reader.py" script
+; under different Python interpreters
+
+[tox]
+envlist = py26,py27,py32,py33,pypy
+
+[testenv]
+deps=
+    docopt >= 0.5
+
+commands=
+    psd-tools.py []