From f9c679e728019ec3f58ee4425a21f0c4a56e5677 Mon Sep 17 00:00:00 2001 From: Kota Yamaguchi Date: Thu, 24 Jan 2019 16:42:15 +0900 Subject: [PATCH] Drop psd_tools packages --- AUTHORS.txt | 22 - CHANGES.rst | 11 + LICENSE | 4 +- MANIFEST.in | 1 - Makefile | 3 +- src/psd_tools/__init__.py | 4 - src/psd_tools/__main__.py | 67 - src/psd_tools/_compression.pyx | 53 - src/psd_tools/compression.py | 75 - src/psd_tools/constants.py | 547 ------- src/psd_tools/debug.py | 75 - src/psd_tools/decoder/__init__.py | 3 - src/psd_tools/decoder/actions.py | 312 ---- src/psd_tools/decoder/color.py | 45 - src/psd_tools/decoder/decoder.py | 81 - src/psd_tools/decoder/decoders.py | 43 - src/psd_tools/decoder/engine_data.py | 183 --- src/psd_tools/decoder/image_resources.py | 696 --------- src/psd_tools/decoder/layer_effects.py | 355 ----- src/psd_tools/decoder/linked_layer.py | 137 -- src/psd_tools/decoder/path.py | 54 - src/psd_tools/decoder/tagged_blocks.py | 1314 ----------------- src/psd_tools/exceptions.py | 6 - src/psd_tools/icc_profiles/Gray-CIE_L.icc | Bin 424 -> 0 bytes src/psd_tools/icc_profiles/Gray.icc | Bin 420 -> 0 bytes src/psd_tools/icc_profiles/__init__.py | 13 - src/psd_tools/reader/__init__.py | 3 - src/psd_tools/reader/color_mode_data.py | 35 - src/psd_tools/reader/header.py | 80 - src/psd_tools/reader/image_resources.py | 65 - src/psd_tools/reader/layers.py | 670 --------- src/psd_tools/reader/reader.py | 68 - src/psd_tools/user_api/__init__.py | 100 -- src/psd_tools/user_api/actions.py | 391 ----- src/psd_tools/user_api/adjustments.py | 501 ------- src/psd_tools/user_api/composer.py | 198 --- src/psd_tools/user_api/effects.py | 658 --------- src/psd_tools/user_api/layers.py | 809 ---------- src/psd_tools/user_api/mask.py | 158 -- src/psd_tools/user_api/pil_support.py | 471 ------ src/psd_tools/user_api/psd_image.py | 380 ----- src/psd_tools/user_api/pymaging_support.py | 112 -- src/psd_tools/user_api/shape.py | 338 ----- src/psd_tools/user_api/smart_object.py | 61 - src/psd_tools/utils.py | 132 -- src/psd_tools/version.py | 1 - tests/psd_tools/__init__.py | 2 - .../psd_tools/reader/test_color_mode_data.py | 12 - tests/psd_tools/reader/test_header.py | 14 - tests/psd_tools/test_adjustments.py | 203 --- tests/psd_tools/test_advanced_blending.py | 28 - tests/psd_tools/test_binary.py | 38 - tests/psd_tools/test_composer.py | 45 - tests/psd_tools/test_dimensions.py | 93 -- tests/psd_tools/test_enum.py | 24 - tests/psd_tools/test_grouping.py | 111 -- tests/psd_tools/test_image_resources.py | 41 - tests/psd_tools/test_info.py | 172 --- tests/psd_tools/test_layer_effects.py | 357 ----- tests/psd_tools/test_layer_masks.py | 189 --- tests/psd_tools/test_metadata.py | 25 - tests/psd_tools/test_patterns.py | 23 - tests/psd_tools/test_pixels.py | 225 --- tests/psd_tools/test_placed_layer.py | 62 - tests/psd_tools/test_text.py | 41 - tests/psd_tools/test_utils.py | 28 - tests/psd_tools/utils.py | 47 - 67 files changed, 14 insertions(+), 11101 deletions(-) delete mode 100644 AUTHORS.txt delete mode 100644 src/psd_tools/__init__.py delete mode 100644 src/psd_tools/__main__.py delete mode 100644 src/psd_tools/_compression.pyx delete mode 100644 src/psd_tools/compression.py delete mode 100644 src/psd_tools/constants.py delete mode 100644 src/psd_tools/debug.py delete mode 100644 src/psd_tools/decoder/__init__.py delete mode 100644 src/psd_tools/decoder/actions.py delete mode 100644 src/psd_tools/decoder/color.py delete mode 100644 src/psd_tools/decoder/decoder.py delete mode 100644 src/psd_tools/decoder/decoders.py delete mode 100644 src/psd_tools/decoder/engine_data.py delete mode 100644 src/psd_tools/decoder/image_resources.py delete mode 100644 src/psd_tools/decoder/layer_effects.py delete mode 100644 src/psd_tools/decoder/linked_layer.py delete mode 100644 src/psd_tools/decoder/path.py delete mode 100644 src/psd_tools/decoder/tagged_blocks.py delete mode 100644 src/psd_tools/exceptions.py delete mode 100644 src/psd_tools/icc_profiles/Gray-CIE_L.icc delete mode 100644 src/psd_tools/icc_profiles/Gray.icc delete mode 100644 src/psd_tools/icc_profiles/__init__.py delete mode 100644 src/psd_tools/reader/__init__.py delete mode 100644 src/psd_tools/reader/color_mode_data.py delete mode 100644 src/psd_tools/reader/header.py delete mode 100644 src/psd_tools/reader/image_resources.py delete mode 100644 src/psd_tools/reader/layers.py delete mode 100644 src/psd_tools/reader/reader.py delete mode 100644 src/psd_tools/user_api/__init__.py delete mode 100644 src/psd_tools/user_api/actions.py delete mode 100644 src/psd_tools/user_api/adjustments.py delete mode 100644 src/psd_tools/user_api/composer.py delete mode 100644 src/psd_tools/user_api/effects.py delete mode 100644 src/psd_tools/user_api/layers.py delete mode 100644 src/psd_tools/user_api/mask.py delete mode 100644 src/psd_tools/user_api/pil_support.py delete mode 100644 src/psd_tools/user_api/psd_image.py delete mode 100644 src/psd_tools/user_api/pymaging_support.py delete mode 100644 src/psd_tools/user_api/shape.py delete mode 100644 src/psd_tools/user_api/smart_object.py delete mode 100644 src/psd_tools/utils.py delete mode 100644 src/psd_tools/version.py delete mode 100644 tests/psd_tools/__init__.py delete mode 100644 tests/psd_tools/reader/test_color_mode_data.py delete mode 100644 tests/psd_tools/reader/test_header.py delete mode 100644 tests/psd_tools/test_adjustments.py delete mode 100644 tests/psd_tools/test_advanced_blending.py delete mode 100644 tests/psd_tools/test_binary.py delete mode 100644 tests/psd_tools/test_composer.py delete mode 100644 tests/psd_tools/test_dimensions.py delete mode 100644 tests/psd_tools/test_enum.py delete mode 100644 tests/psd_tools/test_grouping.py delete mode 100644 tests/psd_tools/test_image_resources.py delete mode 100644 tests/psd_tools/test_info.py delete mode 100644 tests/psd_tools/test_layer_effects.py delete mode 100644 tests/psd_tools/test_layer_masks.py delete mode 100644 tests/psd_tools/test_metadata.py delete mode 100644 tests/psd_tools/test_patterns.py delete mode 100644 tests/psd_tools/test_pixels.py delete mode 100644 tests/psd_tools/test_placed_layer.py delete mode 100644 tests/psd_tools/test_text.py delete mode 100644 tests/psd_tools/test_utils.py delete mode 100644 tests/psd_tools/utils.py diff --git a/AUTHORS.txt b/AUTHORS.txt deleted file mode 100644 index 58396bf6..00000000 --- a/AUTHORS.txt +++ /dev/null @@ -1,22 +0,0 @@ -Contributors -============ - -* Mikhail Korobov; -* Ivan Ivanov; -* Oliver Zheng; -* Pavel Zinovkin; -* Luke Petre; -* Doug Ellwanger; -* Ivan Maradzhyiski; -* Alexey Buzanov; -* Alex Martyn; -* Vladimir Timofeev; -* Evgeny Kopylov; -* Carlton P. Taylor; -* Joey Gentry; -* Volker Braun; -* Michael Wu; -* Josh Drake; -* Leendert Brouwer; -* Kota Yamaguchi; -* Ichiro Arai. diff --git a/CHANGES.rst b/CHANGES.rst index 1703dd67..a19fc299 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,14 @@ +1.8.0 (2019-01-24) +------------------ + +- major API changes; +- package name changed to `psd_tools2`; +- completely rewritten decoding subpackage `psd_tools2.psd`; +- improved composer functionality; +- file write support; +- drop cython compression module and makes the package pure-python; +- drop pymaging support. + 1.7.30 (2019-01-15) ------------------- diff --git a/LICENSE b/LICENSE index 9ca795d6..0ddde68b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012 Mikhail Korobov +Copyright (c) 2019 Kota Yamaguchi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -16,4 +16,4 @@ 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. \ No newline at end of file +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index d9134b43..4328f983 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ -include AUTHORS.txt include README.rst include CHANGES.rst diff --git a/Makefile b/Makefile index 590ccba3..11ad09f0 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,4 @@ BRANCH=master -VERSIONS=2.7 3.5 3.6 3.7 clean: rm -rf dist/ build/ @@ -7,7 +6,7 @@ clean: package: pip install wheel python setup.py sdist - for v in ${VERSIONS}; do python$$v setup.py bdist_wheel; done + python setup.py bdist_wheel --universal publish: package test -n "$(shell git branch | grep '* ${BRANCH}')" diff --git a/src/psd_tools/__init__.py b/src/psd_tools/__init__.py deleted file mode 100644 index 5749ccdb..00000000 --- a/src/psd_tools/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from __future__ import absolute_import -from psd_tools.user_api.psd_image import PSDImage -from psd_tools.user_api.composer import compose -from psd_tools.version import __version__ diff --git a/src/psd_tools/__main__.py b/src/psd_tools/__main__.py deleted file mode 100644 index b3aa5791..00000000 --- a/src/psd_tools/__main__.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, print_function -import logging -import docopt - -import psd_tools.reader -import psd_tools.decoder -from psd_tools import PSDImage -from psd_tools.debug import pprint -from psd_tools.version import __version__ - -logger = logging.getLogger(__name__) -logger.addHandler(logging.StreamHandler()) - - -def main(): - """ - psd-tools - - Usage: - psd-tools convert [options] - psd-tools export_layer \ -[options] - psd-tools debug [options] - psd-tools -h | --help - psd-tools --version - - Options: - -v --verbose Be more verbose. - --encoding Text encoding [default: utf8]. - - """ - args = docopt.docopt(main.__doc__, version=__version__) - - if args['--verbose']: - logger.setLevel(logging.DEBUG) - else: - logger.setLevel(logging.INFO) - encoding = args['--encoding'] - - if args['convert']: - psd = PSDImage.load(args[''], encoding=encoding) - im = psd.as_PIL() - im.save(args['']) - - elif args['export_layer']: - psd = PSDImage.load(args[''], encoding=encoding) - index = int(args['']) - im = psd.layers[index].as_PIL() - im.save(args['']) - print(psd.layers) - - psd.as_PIL() - - elif args['debug']: - psd = PSDImage.load(args[''], encoding=encoding) - - print("\nHeader\n------") - print(psd.decoded_data.header) - print("\nDecoded data\n-----------") - pprint(psd.decoded_data) - print("\nLayers\n------") - psd.print_tree() - - -if __name__ == "__main__": - main() diff --git a/src/psd_tools/_compression.pyx b/src/psd_tools/_compression.pyx deleted file mode 100644 index 7d9f362c..00000000 --- a/src/psd_tools/_compression.pyx +++ /dev/null @@ -1,53 +0,0 @@ -#!python -#cython: language_level=2 -""" -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 - if hasattr(bytes_copy, 'tobytes'): - return bytes_copy.tobytes() - else: - return bytes_copy.tostring() diff --git a/src/psd_tools/compression.py b/src/psd_tools/compression.py deleted file mode 100644 index 369fd59e..00000000 --- a/src/psd_tools/compression.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- 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 - - if hasattr(arr, 'tobytes'): - return arr.tobytes() - else: - 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 - if hasattr(arr, 'tobytes'): - return arr.tobytes() - else: - 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 diff --git a/src/psd_tools/constants.py b/src/psd_tools/constants.py deleted file mode 100644 index 973d54a0..00000000 --- a/src/psd_tools/constants.py +++ /dev/null @@ -1,547 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - - -class Enum(object): - """ - Base class for enumeration. - """ - - _attributes_cache = None - _values_dict_cache = None - - @classmethod - def _attributes(cls): - if cls._attributes_cache is None: - attrs = [name for name in dir(cls) - if name.isupper() and not name.startswith('_')] - cls._attributes_cache = attrs - return cls._attributes_cache - - @classmethod - def _values_dict(cls): - if cls._values_dict_cache is None: - cls._values_dict_cache = dict([ - (getattr(cls, name), name) - for name in cls._attributes() - ]) - return cls._values_dict_cache - - @classmethod - def is_known(cls, value): - return value in cls._values_dict() - - @classmethod - def name_of(cls, value): - return cls._values_dict().get(value, "".format(value)) - - @classmethod - def human_name_of(cls, value, separator="-"): - return cls.name_of(value).lower().replace("_", separator) - - -class ColorMode(Enum): - """ - Color mode. - """ - BITMAP = 0 - GRAYSCALE = 1 - INDEXED = 2 - RGB = 3 - CMYK = 4 - MULTICHANNEL = 7 - DUOTONE = 8 - LAB = 9 - - -class ChannelID(Enum): - """ - Channel types. - """ - TRANSPARENCY_MASK = -1 - USER_LAYER_MASK = -2 - REAL_USER_LAYER_MASK = -3 - - -class ImageResourceID(Enum): - """ - Image resource keys. - """ - 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 = 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_ALTITUDE = 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 - MEASUREMENT_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_SELECTION_STATE = 1088 - - # PATH_INFO = 2000...2997 - PATH_INFO_0 = 2000 - PATH_INFO_LAST = 2997 - CLIPPING_PATH_NAME = 2999 - ORIGIN_PATH_INFO = 3000 - - # PLUGIN_RESOURCES = 4000..4999 - PLUGIN_RESOURCES_0 = 4000 - PLUGIN_RESOURCES_LAST = 4999 - - IMAGE_READY_VARIABLES = 7000 - IMAGE_READY_DATA_SETS = 7001 - LIGHTROOM_WORKFLOW = 8000 - PRINT_FLAGS_INFO = 10000 - - @classmethod - def is_known(cls, value): - path_info = cls.PATH_INFO_0 <= value <= cls.PATH_INFO_LAST - plugin_resource = ( - cls.PLUGIN_RESOURCES_0 <= value <= cls.PLUGIN_RESOURCES_LAST - ) - return ( - super(ImageResourceID, cls).is_known(value) or - path_info or - plugin_resource - ) - - @classmethod - def name_of(cls, value): - if cls.PATH_INFO_0 < value < cls.PATH_INFO_LAST: - return "PATH_INFO_%d" % (value - cls.PATH_INFO_0) - if cls.PLUGIN_RESOURCES_0 < value < cls.PLUGIN_RESOURCES_LAST: - return "PLUGIN_RESOURCES_%d" % (value - cls.PLUGIN_RESOURCES_0) - return super(ImageResourceID, cls).name_of(value) - - -class ColorSpaceID(Enum): - """ - Color space types. - """ - RGB = 0 - HSB = 1 - CMYK = 2 - LAB = 7 - GRAYSCALE = 8 - - -class BlendMode(Enum): - """ - Blend modes. - """ - PASS_THROUGH = b'pass' - NORMAL = b'norm' - DISSOLVE = b'diss' - DARKEN = b'dark' - MULTIPLY = b'mul ' - COLOR_BURN = b'idiv' - LINEAR_BURN = b'lbrn' - DARKER_COLOR = b'dkCl' - LIGHTEN = b'lite' - SCREEN = b'scrn' - COLOR_DODGE = b'div ' - LINEAR_DODGE = b'lddg' - LIGHTER_COLOR = b'lgCl' - OVERLAY = b'over' - SOFT_LIGHT = b'sLit' - HARD_LIGHT = b'hLit' - VIVID_LIGHT = b'vLit' - LINEAR_LIGHT = b'lLit' - PIN_LIGHT = b'pLit' - HARD_MIX = b'hMix' - DIFFERENCE = b'diff' - EXCLUSION = b'smud' - SUBTRACT = b'fsub' - DIVIDE = b'fdiv' - HUE = b'hue ' - SATURATION = b'sat ' - COLOR = b'colr' - LUMINOSITY = b'lum ' - - -class BlendMode2(Enum): - """Blend mode in layer effect descriptor.""" - NORMAL = b'Nrml' - DISSOLVE = b'Dslv' - DARKEN = b'Drkn' - MULTIPLY = b'Mltp' - COLOR_BURN = b'CBrn' - LINEAR_BURN = b'linearBurn' - DARKER_COLOR = b'darkerColor' - LIGHTEN = b'Lghn' - SCREEN = b'Scrn' - COLOR_DODGE = b'CDdg' - LINEAR_DODGE = b'linearDodge' - LIGHTER_COLOR = b'lighterColor' - OVERLAY = b'Ovrl' - SOFT_LIGHT = b'SftL' - HARD_LIGHT = b'HrdL' - VIVID_LIGHT = b'vividLight' - LINEAR_LIGHT = b'linearLight' - PIN_LIGHT = b'pinLight' - HARD_MIX = b'hardMix' - DIFFERENCE = b'Dfrn' - EXCLUSION = b'Xclu' - SUBTRACT = b'blendSubtraction' - DIVIDE = b'blendDivide' - HUE = b'H ' - SATURATION = b'Strt' - COLOR = b'Clr ' - LUMINOSITY = b'Lmns' - - -class Clipping(Enum): - """Clipping.""" - BASE = 0 - NON_BASE = 1 - - -class GlobalLayerMaskKind(Enum): - """Global layer mask kind.""" - COLOR_SELECTED = 0 - COLOR_PROTECTED = 1 - PER_LAYER = 128 - # others options are possible in beta versions. - - -class Compression(Enum): - """Compression modes.""" - RAW = 0 - PACK_BITS = 1 - ZIP = 2 - ZIP_WITH_PREDICTION = 3 - - -class PrintScaleStyle(Enum): - """Print scale style.""" - CENTERED = 0 - SIZE_TO_FIT = 1 - USER_DEFINED = 2 - - -class TaggedBlock(Enum): - """Tagged blocks keys.""" - _FILL_KEYS = set([ - b'SoCo', b'GdFl', b'PtFl' - ]) - - _ADJUSTMENT_KEYS = set([ - b'brit', b'levl', b'curv', b'expA', b'vibA', b'hue ', b'hue2', - b'blnc', b'blwh', b'phfl', b'mixr', b'clrL', b'nvrt', b'post', - b'thrs', b'grdm', b'selc' - ]) - - SOLID_COLOR_SHEET_SETTING = b'SoCo' - GRADIENT_FILL_SETTING = b'GdFl' - PATTERN_FILL_SETTING = b'PtFl' - BRIGHTNESS_AND_CONTRAST = b'brit' - LEVELS = b'levl' - CURVES = b'curv' - EXPOSURE = b'expA' - VIBRANCE = b'vibA' - HUE_SATURATION_V4 = b'hue ' - HUE_SATURATION = b'hue2' - COLOR_BALANCE = b'blnc' - BLACK_AND_WHITE = b'blwh' - PHOTO_FILTER = b'phfl' - CHANNEL_MIXER = b'mixr' - COLOR_LOOKUP = b'clrL' - INVERT = b'nvrt' - POSTERIZE = b'post' - THRESHOLD = b'thrs' - GRADIENT_MAP_SETTING = b'grdm' - SELECTIVE_COLOR = b'selc' - - @classmethod - def is_adjustment_key(cls, key): - return key in cls._ADJUSTMENT_KEYS - - @classmethod - def is_fill_key(cls, key): - return key in cls._FILL_KEYS - - EFFECTS_LAYER = b'lrFX' - TYPE_TOOL_INFO = b'tySh' - UNICODE_LAYER_NAME = b'luni' - LAYER_ID = b'lyid' - OBJECT_BASED_EFFECTS_LAYER_INFO_V0 = b'lmfx' # Undocumented. - OBJECT_BASED_EFFECTS_LAYER_INFO_V1 = b'lfxs' # Undocumented. - OBJECT_BASED_EFFECTS_LAYER_INFO = b'lfx2' - - PATTERNS1 = b'Patt' - PATTERNS2 = b'Pat2' - PATTERNS3 = b'Pat3' - - ANNOTATIONS = b'Anno' - BLEND_CLIPPING_ELEMENTS = b'clbl' - BLEND_INTERIOR_ELEMENTS = b'infx' - BLEND_FILL_OPACITY = b'iOpa' # Undocumented. - - KNOCKOUT_SETTING = b'knko' - PROTECTED_SETTING = b'lspf' - SHEET_COLOR_SETTING = b'lclr' - REFERENCE_POINT = b'fxrp' - SECTION_DIVIDER_SETTING = b'lsct' - NESTED_SECTION_DIVIDER_SETTING = b'lsdk' - CHANNEL_BLENDING_RESTRICTIONS_SETTING = b'brst' - VECTOR_MASK_SETTING1 = b'vmsk' - VECTOR_MASK_SETTING2 = b'vsms' - TYPE_TOOL_OBJECT_SETTING = b'TySh' - FOREIGN_EFFECT_ID = b'ffxi' - LAYER_NAME_SOURCE_SETTING = b'lnsr' - PATTERN_DATA = b'shpa' - METADATA_SETTING = b'shmd' - LAYER_VERSION = b'lyvr' - TRANSPARENCY_SHAPES_LAYER = b'tsly' - LAYER_MASK_AS_GLOBAL_MASK = b'lmgm' - VECTOR_MASK_AS_GLOBAL_MASK = b'vmgm' - VECTOR_ORIGINATION_DATA = b'vogk' - PIXEL_SOURCE_DATA1 = b'PxSc' - PIXEL_SOURCE_DATA2 = b'PxSD' - ARTBOARD_DATA1 = b'artb' - ARTBOARD_DATA2 = b'artd' - ARTBOARD_DATA3 = b'abdd' - - PLACED_LAYER_OBSOLETE1 = b'plLd' - PLACED_LAYER_OBSOLETE2 = b'PlLd' - - LINKED_LAYER1 = b'lnkD' - LINKED_LAYER2 = b'lnk2' - LINKED_LAYER3 = b'lnk3' - LINKED_LAYER_EXTERNAL = b'lnkE' - CONTENT_GENERATOR_EXTRA_DATA = b'CgEd' - TEXT_ENGINE_DATA = b'Txt2' - UNICODE_PATH_NAME = b'pths' - ANIMATION_EFFECTS = b'anFX' - FILTER_MASK = b'FMsk' - PLACED_LAYER_DATA = b'SoLd' - SMART_OBJECT_PLACED_LAYER_DATA = b'SoLE' - EXPORT_SETTING1 = b'extd' # Undocumented. - EXPORT_SETTING2 = b'extn' # Undocumented. - - VECTOR_STROKE_DATA = b'vstk' - VECTOR_STROKE_CONTENT_DATA = b'vscg' - USING_ALIGNED_RENDERING = b'sn2P' - SAVING_MERGED_TRANSPARENCY = b'Mtrn' - SAVING_MERGED_TRANSPARENCY16 = b'Mt16' - SAVING_MERGED_TRANSPARENCY32 = b'Mt32' - USER_MASK = b'LMsk' - FILTER_EFFECTS1 = b'FXid' - FILTER_EFFECTS2 = b'FEid' - FILTER_EFFECTS3 = b'FELS' # Undocumented. - - LAYER_16 = b'Lr16' - LAYER_32 = b'Lr32' - LAYER = b'Layr' - - COMPUTER_INFO = b'cinf' - - -class OSType(Enum): - """ - Action descriptor types. - """ - REFERENCE = b'obj ' - DESCRIPTOR = b'Objc' - LIST = b'VlLs' - DOUBLE = b'doub' - UNIT_FLOAT = b'UntF' - UNIT_FLOATS = b'UnFl' - STRING = b'TEXT' - ENUMERATED = b'enum' - INTEGER = b'long' - LARGE_INTEGER = b'comp' - BOOLEAN = b'bool' - GLOBAL_OBJECT = b'GlbO' - CLASS1 = b'type' - CLASS2 = b'GlbC' - ALIAS = b'alis' - RAW_DATA = b'tdta' - OBJECT_ARRAY = b'ObAr' - PATH = b'Pth ' # Undocumented - - -class ReferenceOSType(Enum): - """ - OS Type keys for Reference Structure. - """ - PROPERTY = b'prop' - CLASS = b'Clss' - ENUMERATED_REFERENCE = b'Enmr' - OFFSET = b'rele' - IDENTIFIER = b'Idnt' - INDEX = b'indx' - NAME = b'name' - - -class EffectOSType(Enum): - """ - OS Type keys for Layer Effects. - """ - COMMON_STATE = b'cmnS' - DROP_SHADOW = b'dsdw' - INNER_SHADOW = b'isdw' - OUTER_GLOW = b'oglw' - INNER_GLOW = b'iglw' - BEVEL = b'bevl' - SOLID_FILL = b'sofi' - - -class UnitFloatType(Enum): - """ - Units the value is in (used in Unit float structure). - """ - ANGLE = b'#Ang' # base degrees - DENSITY = b'#Rsl' # base per inch - DISTANCE = b'#Rlt' # base 72ppi - NONE = b'#Nne' # coerced - PERCENT = b'#Prc' # unit value - PIXELS = b'#Pxl' # tagged unit value - POINTS = b'#Pnt' # points - MILLIMETERS = b'#Mlm' # millimeters - - -class SectionDivider(Enum): - OTHER = 0 - OPEN_FOLDER = 1 - CLOSED_FOLDER = 2 - BOUNDING_SECTION_DIVIDER = 3 - - -class DisplayResolutionUnit(Enum): - PIXELS_PER_INCH = 1 - PIXELS_PER_CM = 2 - - -class DimensionUnit(Enum): - INCH = 1 - CM = 2 - POINT = 3 # 72 points == 1 inch - PICA = 4 # 6 pica == 1 inch - COLUMN = 5 - - -class PlacedLayerProperty(Enum): - TRANSFORM = b'Trnf' - SIZE = b'Sz ' - ID = b'Idnt' - - -class SzProperty(Enum): - WIDTH = b'Wdth' - HEIGHT = b'Hght' - - -class TextProperty(Enum): - TXT = b'Txt ' - ORIENTATION = b'Ornt' - - -class TextOrientation(Enum): - HORIZONTAL = b'Hrzn' - - -class PathResource(Enum): - CLOSED_SUBPATH_LENGTH_RECORD = 0 - CLOSED_SUBPATH_BEZIER_KNOT_LINKED = 1 - CLOSED_SUBPATH_BEZIER_KNOT_UNLINKED = 2 - OPEN_SUBPATH_LENGTH_RECORD = 3 - OPEN_SUBPATH_BEZIER_KNOT_LINKED = 4 - OPEN_SUBPATH_BEZIER_KNOT_UNLINKED = 5 - PATH_FILL_RULE_RECORD = 6 - CLIPBOARD_RECORD = 7 - INITIAL_FILL_RULE_RECORD = 8 - - -class LinkedLayerType(Enum): - DATA = b'liFD' - EXTERNAL = b'liFE' - ALIAS = b'liFA' - - -class ObjectBasedEffects(Enum): - """Type of the object-based effects.""" - DROP_SHADOW_MULTI = b'dropShadowMulti' - DROP_SHADOW = b'DrSh' - INNER_SHADOW_MULTI = b'innerShadowMulti' - INNER_SHADOW = b'IrSh' - OUTER_GLOW = b'OrGl' - COLOR_OVERLAY_MULTI = b'solidFillMulti' - COLOR_OVERLAY = b'SoFi' - GRADIENT_OVERLAY_MULTI = b'gradientFillMulti' - GRADIENT_OVERLAY = b'GrFl' - PATTERN_OVERLAY = b'patternFill' - STROKE_MULTI = b'frameFXMulti' - STROKE = b'FrFX' - INNER_GLOW = b'IrGl' - BEVEL_EMBOSS = b'ebbl' - SATIN = b'ChFX' diff --git a/src/psd_tools/debug.py b/src/psd_tools/debug.py deleted file mode 100644 index 8e595b26..00000000 --- a/src/psd_tools/debug.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Assorted debug utilities -""" -from __future__ import absolute_import, print_function -import sys -from collections import namedtuple - - -def pprint(*args, **kwargs): - """ - Pretty-print a Python object using ``IPython.lib.pretty.pprint``. - Fallback to ``pprint.pprint`` if IPython is not available. - """ - try: - from IPython.lib.pretty import pprint - except ImportError: - from pprint import pprint - pprint(*args, **kwargs) - - -def debug_view(fp, txt="", max_back=20): - """ - Print file contents around current position for file pointer ``fp`` - """ - max_back = min(max_back, fp.tell()) - fp.seek(-max_back, 1) - pre = fp.read(max_back) - post = fp.read(100) - fp.seek(-100, 1) - print(txt, repr(pre), "--->.<---", repr(post)) - - -def pretty_namedtuple(typename, field_names): - """ - Return a namedtuple class that knows how to pretty-print itself - using IPython.lib.pretty library. - """ - cls = namedtuple(typename, field_names) - PrettyMixin = _get_pretty_mixin(typename) - cls = type(str(typename), (PrettyMixin, cls), {}) - - # For pickling to work, the __module__ variable needs to be set to the - # frame where the named tuple is created. Bypass this step in enviroments - # where sys._getframe is not defined (Jython for example) or sys._getframe - # is not defined for arguments greater than 0 (IronPython). - try: - cls.__module__ = sys._getframe(1).f_globals.get( - '__name__', '__main__') - except (AttributeError, ValueError): - pass - - return cls - - -def _get_pretty_mixin(typename): - """ - Return a mixin class for multiline pretty-printing - of namedtuple objects. - """ - class _PrettyNamedtupleMixin(object): - def _repr_pretty_(self, p, cycle): - if cycle: - return "{typename}(...)".format(name=typename) - - with p.group(1, '{name}('.format(name=typename), ')'): - p.breakable() - for idx, field in enumerate(self._fields): - if idx: - p.text(',') - p.breakable() - p.text('{field}='.format(field=field)) - p.pretty(getattr(self, field)) - - return _PrettyNamedtupleMixin diff --git a/src/psd_tools/decoder/__init__.py b/src/psd_tools/decoder/__init__.py deleted file mode 100644 index 42d2a19c..00000000 --- a/src/psd_tools/decoder/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from .decoder import parse diff --git a/src/psd_tools/decoder/actions.py b/src/psd_tools/decoder/actions.py deleted file mode 100644 index e0e9fbcf..00000000 --- a/src/psd_tools/decoder/actions.py +++ /dev/null @@ -1,312 +0,0 @@ -# -*- coding: utf-8 -*- -""" -A module for decoding "Actions" additional PSD data format. -""" -from __future__ import absolute_import, unicode_literals - -from psd_tools.utils import read_unicode_string, read_fmt -from psd_tools.constants import OSType, ReferenceOSType, UnitFloatType -from psd_tools.debug import pretty_namedtuple -from psd_tools.utils import trimmed_repr -import warnings - -Descriptor = pretty_namedtuple('Descriptor', 'name classID items') -Reference = pretty_namedtuple('Reference', 'items') -Property = pretty_namedtuple('Property', 'name classID keyID') -UnitFloat = pretty_namedtuple('UnitFloat', 'unit value') -Double = pretty_namedtuple('Double', 'value') -Class = pretty_namedtuple('Class', 'name classID') -String = pretty_namedtuple('String', 'value') -EnumReference = pretty_namedtuple('EnumReference', 'name classID typeID enum') -Boolean = pretty_namedtuple('Boolean', 'value') -Offset = pretty_namedtuple('Offset', 'name classID value') -Alias = pretty_namedtuple('Alias', 'value') -List = pretty_namedtuple('List', 'items') -Integer = pretty_namedtuple('Integer', 'value') -Enum = pretty_namedtuple('Enum', 'type value') -Identifier = pretty_namedtuple('Identifier', 'value') -Index = pretty_namedtuple('Index', 'value') -Name = pretty_namedtuple('Name', 'value') -ObjectArray = pretty_namedtuple('ObjectArray', 'classObj items') -ObjectArrayItem = pretty_namedtuple('ObjectArrayItem', 'keyID value') -_RawData = pretty_namedtuple('RawData', 'value') - - -class RawData(_RawData): - def __repr__(self): - return "RawData(value=%s)" % trimmed_repr(self.value) - - def _repr_pretty_(self, p, cycle): - if cycle: - p.text("RawData(...)") - else: - with p.group(1, "RawData(", ")"): - p.breakable() - p.text("value=") - if isinstance(self.value, bytes): - p.text(trimmed_repr(self.value)) - else: - p.pretty(self.value) - - -def get_ostype_decode_func(ostype): - return { - OSType.REFERENCE: decode_ref, - OSType.DESCRIPTOR: decode_descriptor, - OSType.LIST: decode_list, - OSType.DOUBLE: decode_double, - OSType.UNIT_FLOAT: decode_unit_float, - OSType.UNIT_FLOATS: decode_unit_floats, - OSType.STRING: decode_string, - OSType.ENUMERATED: decode_enum, - OSType.INTEGER: decode_integer, - OSType.LARGE_INTEGER: decode_large_integer, - OSType.BOOLEAN: decode_bool, - OSType.GLOBAL_OBJECT: decode_descriptor, - OSType.CLASS1: decode_class, - OSType.CLASS2: decode_class, - OSType.ALIAS: decode_alias, - OSType.RAW_DATA: decode_raw, - OSType.OBJECT_ARRAY: decode_object_array, - OSType.PATH: decode_raw, # Undocumented - }.get(ostype, None) - - -def get_reference_ostype_decode_func(ostype): - return { - ReferenceOSType.PROPERTY: decode_prop, - ReferenceOSType.CLASS: decode_class, - ReferenceOSType.OFFSET: decode_offset, - ReferenceOSType.IDENTIFIER: decode_identifier, - ReferenceOSType.INDEX: decode_index, - ReferenceOSType.NAME: decode_name, - ReferenceOSType.ENUMERATED_REFERENCE: decode_enum_ref, - }.get(ostype, None) - - -def decode_descriptor(_, fp): - name = read_unicode_string(fp)[:-1] - classID_length = read_fmt("I", fp)[0] - classID = fp.read(classID_length or 4) - - items = [] - item_count = read_fmt("I", fp)[0] - while len(items) < item_count: - item_length = read_fmt("I", fp)[0] - key = fp.read(item_length or 4) - ostype = fp.read(4) - - decode_ostype = get_ostype_decode_func(ostype) - if not decode_ostype: - # For some reason, name can appear in the middle of items... - if key == ReferenceOSType.NAME: - fp.seek(fp.tell() - 4) - name = decode_name(key, fp) - continue - - raise UnknownOSType('Unknown descriptor item of type %r' % ostype) - - value = decode_ostype(key, fp) - if value is None: - warnings.warn("%r (%r) is None" % (key, ostype)) - items.append((key, value)) - - return Descriptor(name, classID, items) - - -def decode_ref(key, fp): - item_count = read_fmt("I", fp)[0] - items = [] - - for _ in range(item_count): - ostype = fp.read(4) - - decode_ostype = get_reference_ostype_decode_func(ostype) - if not decode_ostype: - raise UnknownOSType('Unknown reference item of type %r' % ostype) - - value = decode_ostype(key, fp) - if value is not None: - items.append(value) - - return Reference(items) - - -def decode_prop(key, fp): - name = read_unicode_string(fp)[:-1] - classID_length = read_fmt("I", fp)[0] - classID = fp.read(classID_length or 4) - keyID_length = read_fmt("I", fp)[0] - keyID = fp.read(keyID_length or 4) - return Property(name, classID, keyID) - - -def decode_unit_float(key, fp): - unit_key = fp.read(4) - if not UnitFloatType.is_known(unit_key): - warnings.warn('Unknown UnitFloatType: %r' % unit_key) - - value = read_fmt("d", fp)[0] - return UnitFloat(UnitFloatType.name_of(unit_key), value) - - -def decode_unit_floats(key, fp): - unit_key = fp.read(4) - if not UnitFloatType.is_known(unit_key): - warnings.warn('Unknown UnitFloatType: %r' % unit_key) - - floats_count = read_fmt("I", fp)[0] - floats = [] - - for n in range(floats_count): - value = read_fmt("d", fp)[0] - floats.append(UnitFloat(UnitFloatType.name_of(unit_key), value)) - - return floats - - -def decode_double(key, fp): - return Double(read_fmt("d", fp)[0]) - - -def decode_class(key, fp): - name = read_unicode_string(fp)[:-1] - classID_length = read_fmt("I", fp)[0] - classID = fp.read(classID_length or 4) - return Class(name, classID) - - -def decode_string(key, fp): - value = read_unicode_string(fp)[:-1] - return String(value) - - -def decode_enum_ref(key, fp): - name = read_unicode_string(fp)[:-1] - classID_length = read_fmt("I", fp)[0] - classID = fp.read(classID_length or 4) - typeID_length = read_fmt("I", fp)[0] - typeID = fp.read(typeID_length or 4) - enum_length = read_fmt("I", fp)[0] - enum = fp.read(enum_length or 4) - return EnumReference(name, classID, typeID, enum) - - -def decode_offset(key, fp): - name = read_unicode_string(fp)[:-1] - classID_length = read_fmt("I", fp)[0] - classID = fp.read(classID_length or 4) - offset = read_fmt("I", fp)[0] - return Offset(name, classID, offset) - - -def decode_bool(key, fp): - return Boolean(read_fmt("?", fp)[0]) - - -def decode_alias(key, fp): - length = read_fmt("I", fp)[0] - value = fp.read(length) - return Alias(value) - - -def decode_list(key, fp): - items_count = read_fmt("I", fp)[0] - items = [] - for _ in range(items_count): - ostype = fp.read(4) - - decode_ostype = get_ostype_decode_func(ostype) - if not decode_ostype: - # It seems Path Selection State might have Enum fields. - if ostype == b'\x00\x00\x00\x00': - fp.seek(fp.tell() - 4) - items.append(_decode_enum_descriptor(key, fp)) - continue - raise UnknownOSType('Unknown list item of type %r' % ostype) - - value = decode_ostype(key, fp) - if value is not None: - items.append(value) - - return List(items) - - -def decode_integer(key, fp): - return Integer(read_fmt("i", fp)[0]) - - -def decode_large_integer(key, fp): - return Integer(read_fmt("q", fp)[0]) - - -def decode_enum(key, fp): - type_length = read_fmt("I", fp)[0] - type_ = fp.read(type_length or 4) - value_length = read_fmt("I", fp)[0] - value = fp.read(value_length or 4) - return Enum(type_, value) - - -def decode_identifier(key, fp): - return Identifier(read_fmt("I", fp)[0]) - - -def decode_index(key, fp): - return Index(read_fmt("I", fp)[0]) - - -def decode_name(key, fp): - value = read_unicode_string(fp)[:-1] - return Name(value) - - -def decode_raw(key, fp): - # This is the only thing we know about: - # The first unsigned int determines the size of the raw data. - size = read_fmt("I", fp)[0] - data = fp.read(size) - return RawData(data) - - -def decode_object_array(key, fp): - items_per_object_count = read_fmt("I", fp)[0] - classObj = decode_class(None, fp) - items_count = read_fmt("I", fp)[0] - items = [] - - for n in range(items_count): - object_array_item = decode_object_array_item(None, fp) - - if object_array_item is not None: - items.append(object_array_item) - - return ObjectArray(classObj, items) - - -def decode_object_array_item(key, fp): - keyID_length = read_fmt("I", fp)[0] - keyID = fp.read(keyID_length or 4) - - ostype = fp.read(4) - - decode_ostype = get_ostype_decode_func(ostype) - if not decode_ostype: - raise UnknownOSType('Unknown list item of type %r' % ostype) - - value = decode_ostype(key, fp) - - return ObjectArrayItem(keyID, value) - - -# There seems to be undocumented enum structure in (Photoshop CC) Path -# Selection State image resource. -def _decode_enum_descriptor(key, fp): - type_length = read_fmt("I", fp)[0] - type_ = fp.read(type_length or 4) - value = read_unicode_string(fp)[:-1] - return Enum(type_, value) - - -class UnknownOSType(ValueError): - pass diff --git a/src/psd_tools/decoder/color.py b/src/psd_tools/decoder/color.py deleted file mode 100644 index 2c5fd63f..00000000 --- a/src/psd_tools/decoder/color.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals -import warnings - -from psd_tools.utils import read_fmt -from psd_tools.constants import ColorSpaceID -from psd_tools.debug import pretty_namedtuple - -_Color = pretty_namedtuple('Color', 'color_space_id color_data') - - -class Color(_Color): - def __repr__(self): - return "Color(id=%s %s, %s)" % ( - self.color_space_id, - ColorSpaceID.name_of(self.color_space_id), - self.color_data - ) - - def _repr_pretty_(self, p, cycle): - # IS NOT TESTED!! - if cycle: - p.text('Color(...)') - else: - with p.group(1, 'Color(', ')'): - p.breakable() - p.text("id=%s %s," % ( - self.color_space_id, - ColorSpaceID.name_of(self.color_space_id) - )) - p.breakable() - p.pretty(self.color_data) - - -def decode_color(fp): - color_space_id = read_fmt("H", fp)[0] - - if not ColorSpaceID.is_known(color_space_id): - warnings.warn("Unknown color space (%s)" % color_space_id) - - if color_space_id == ColorSpaceID.LAB: - color_data = read_fmt("4h", fp) - else: - color_data = read_fmt("4H", fp) - return Color(color_space_id, color_data) diff --git a/src/psd_tools/decoder/decoder.py b/src/psd_tools/decoder/decoder.py deleted file mode 100644 index 14f36492..00000000 --- a/src/psd_tools/decoder/decoder.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -from . import image_resources, tagged_blocks, color -import io -from psd_tools.constants import TaggedBlock - - -def parse(reader_parse_result): - """ - Decode :py:class:`~psd_tools.reader.reader.ParseResult` into Python - structure:: - - import psd_tools.reader - import psd_tools.decoder - - with open('/path/to/input.psd', 'rb') as fp: - binary = psd_tools.reader.parse(fp) - decoded = psd_tools.decoder.parse(binary) - - :param reader_parse_result: - :py:class:`~psd_tools.reader.reader.ParseResult` - :rtype: :py:class:`~psd_tools.reader.reader.ParseResult` - - """ - image_resource_blocks = reader_parse_result.image_resource_blocks - image_resource_blocks = image_resources.decode(image_resource_blocks) - - layer_and_mask_data = reader_parse_result.layer_and_mask_data - _layers = decode_layers(layer_and_mask_data.layers, - reader_parse_result.header.version) - _global_mask_info = decode_global_mask_info( - layer_and_mask_data.global_mask_info) - _tagged_blocks = tagged_blocks.decode( - layer_and_mask_data.tagged_blocks, reader_parse_result.header.version) - - # 16 and 32 bit layers are stored in Lr16 and Lr32 tagged blocks - if _layers.layer_count == 0: - blocks_dict = dict(_tagged_blocks) - if reader_parse_result.header.depth == 16: - _layers = blocks_dict.get(TaggedBlock.LAYER_16, _layers) - elif reader_parse_result.header.depth == 32: - _layers = blocks_dict.get(TaggedBlock.LAYER_32, _layers) - - # XXX: this code is complicated because of the namedtuple abuse - layer_and_mask_data = layer_and_mask_data._replace( - layers=_layers, - global_mask_info=_global_mask_info, - tagged_blocks=_tagged_blocks - ) - - reader_parse_result = reader_parse_result._replace( - image_resource_blocks=image_resource_blocks, - layer_and_mask_data=layer_and_mask_data - ) - - return reader_parse_result - - -def decode_layers(layers, version): - if layers.layer_count == 0: - return layers - - _layer_records = [ - record._replace( - tagged_blocks=tagged_blocks.decode(record.tagged_blocks, version) - ) for record in layers.layer_records - ] - return layers._replace(layer_records=_layer_records) - - -def decode_global_mask_info(global_mask_info): - if global_mask_info is None: - return None - - fp = io.BytesIO(global_mask_info.overlay_color) - global_mask_info = global_mask_info._replace( - overlay_color=color.decode_color(fp) - ) - - return global_mask_info diff --git a/src/psd_tools/decoder/decoders.py b/src/psd_tools/decoder/decoders.py deleted file mode 100644 index cd647f29..00000000 --- a/src/psd_tools/decoder/decoders.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import io -import struct -import warnings -from psd_tools.utils import unpack, read_fmt, read_unicode_string - - -def single_value(fmt): - def decoder(data, **kwargs): - return unpack(fmt, data)[0] - return decoder - - -def unicode_string(data, **kwargs): - if len(data): - return read_unicode_string(io.BytesIO(data)) - else: - return u'' - - -def boolean(fmt="?"): - fmt_size = struct.calcsize(str(fmt)) - - def decoder(data, **kwargs): - return bool(single_value(fmt)(data[:fmt_size])) - - return decoder - - -def new_registry(): - """ - Returns an empty dict and a @register decorator - """ - decoders = {} - - def register(key): - def decorator(func): - decoders[key] = func - return func - return decorator - - return decoders, register diff --git a/src/psd_tools/decoder/engine_data.py b/src/psd_tools/decoder/engine_data.py deleted file mode 100644 index 0a86aa10..00000000 --- a/src/psd_tools/decoder/engine_data.py +++ /dev/null @@ -1,183 +0,0 @@ -# -*- coding: utf-8 -*- -""" -EngineData decoder. - -PSD file embeds text formatting data in its own markup language referred -EngineData. The format looks like the following:: - - << - /EngineDict - << - /Editor - << - /Text (˛ˇMake a change and save.) - >> - >> - /Font - << - /Name (˛ˇHelveticaNeue-Light) - /FillColor - << - /Type 1 - /Values [ 1.0 0.0 0.0 0.0 ] - >> - /StyleSheetSet [ - << - /Name (˛ˇNormal RGB) - >> - ] - >> - >> - -""" - -from __future__ import absolute_import -import re -import warnings -from psd_tools.decoder import decoders -from psd_tools.constants import Enum - - -class InvalidTokenError(ValueError): - pass - - -class EngineToken(Enum): - ARRAY_END = re.compile(b'^\]$') - ARRAY_START = re.compile(b'^\[$') - BOOLEAN = re.compile(b'^(true|false)$') - DICT_END = re.compile(b'^>>(\x00+)?$') - DICT_START = re.compile(b'^<<$') - NOOP = re.compile(b'^$') - NUMBER = re.compile(b'^(-?\d+)$') - NUMBER_WITH_DECIMAL = re.compile(b'^(-?\d*)\.(\d+)$') - PROPERTY = re.compile(b'^\/([a-zA-Z0-9]+)$') - STRING = re.compile(r'^\((\xfe\xff([^\)]|\\\)|\x00\))*)\)$'.encode( - 'utf-8'), re.M | re.DOTALL) - # Unknown tags: b'(hwid)', b'(fwid)', b'(aalt)' - UNKNOWN_TAG = re.compile(b'^\([a-zA-Z0-9]+\)$') - - -class EngineTokenizer(object): - """ - Engine data tokenizer. - """ - def __init__(self, divider): - self.divider = re.compile(divider, re.M | re.DOTALL) - - def tokenize(self, data): - current_token = None - while len(data) > 0: - match = self.divider.search(data) - if match is None: - token, data = data, b'' - elif data.startswith(b'(\xfe\xff'): - for index in range(len(data)): - if data[index:index+1] != b')': - continue - if index > 0 and ( - data[index-1:index+1] == b'\\)' or - data[index-1:index+1] == b'\x00)'): - continue - break - token, data = data[:index+1], data[index+1:] - else: - token, data = data[:match.start()], data[match.end():] - - yield token - - -class EngineDataDecoder(object): - """ - Engine data decoder. - """ - _decoders, register = decoders.new_registry() - - def __init__(self, data, divider=b'[ \n\t]+'): - self.node_stack = [{}] - self.prop_stack = [b'Root'] - self.data = data - self.tokenizer = EngineTokenizer(divider=divider) - - def parse(self): - for token in self.tokenizer.tokenize(self.data): - value = self._parse_token(token) - - if value is not None: - if isinstance(self.node_stack[-1], list): - self.node_stack[-1].append(value) - else: - self.node_stack[-1][self.prop_stack[-1]] = value - - return self.node_stack[0].get(b'Root', self.node_stack[0]) - - def _parse_token(self, token): - patterns = EngineToken._values_dict() - for pattern in patterns: - match = pattern.search(token) - if match: - return self._decoders[pattern](self, match) - raise InvalidTokenError("Unknown token: {}".format(token)) - - @register(EngineToken.ARRAY_END) - def _decode_array_end(self, match): - return self.node_stack.pop() - - @register(EngineToken.ARRAY_START) - def _decode_array_start(self, match): - self.node_stack.append([]) - - @register(EngineToken.BOOLEAN) - def _decode_boolean(self, match): - return True if match.group(1) == b'true' else False - - @register(EngineToken.DICT_END) - def _decode_dict_end(self, match): - self.prop_stack.pop() - return self.node_stack.pop() - - @register(EngineToken.DICT_START) - def _decode_dict_start(self, match): - self.prop_stack.append(None) - self.node_stack.append({}) - - @register(EngineToken.NOOP) - def _decode_noop(self, match): - pass - - @register(EngineToken.NUMBER) - def _decode_number(self, match): - return int(match.group(1)) - - @register(EngineToken.NUMBER_WITH_DECIMAL) - def _decode_number_with_decimal(self, match): - return float(match.group(0)) - - @register(EngineToken.PROPERTY) - def _decode_property(self, match): - self.prop_stack[-1] = match.group(1) - - @register(EngineToken.STRING) - def _decode_string(self, match): - # UTF-16 is backslash-escaped here. - return re.sub(b'\\\\(.)', b'\\1', match.group(1)).decode( - 'utf-16', 'replace') - - @register(EngineToken.UNKNOWN_TAG) - def _decode_unknown_tag(self, match): - return match - - -def decode(data, **kwargs): - """ - Decode EngineData. - - :param data: EngineData encoded in `bytes` - :rtype: `dict` - """ - try: - decoder = EngineDataDecoder(data, **kwargs) - return decoder.parse() - except InvalidTokenError as e: - warnings.warn("Failed to parse EngineData: %s" % e) - return data diff --git a/src/psd_tools/decoder/image_resources.py b/src/psd_tools/decoder/image_resources.py deleted file mode 100644 index 85cbb8b9..00000000 --- a/src/psd_tools/decoder/image_resources.py +++ /dev/null @@ -1,696 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Module for decoding image resources section. - -Image resources section holds key-value store of various resources that are -relevant to the photoshop document. The keys are specified by -:py:class:`~psd_tools.constants.ImageResourceID`. - -""" -from __future__ import absolute_import, unicode_literals, division -import io -import struct -import warnings -from collections import namedtuple -from psd_tools.utils import (read_pascal_string, unpack, read_fmt, - read_unicode_string, be_array_from_bytes, - decode_fixed_point_32bit) -from psd_tools.constants import (ImageResourceID, PrintScaleStyle, - DisplayResolutionUnit, DimensionUnit) -from psd_tools.decoder import decoders -from psd_tools.decoder.actions import decode_descriptor, UnknownOSType, RawData -from psd_tools.decoder.color import decode_color -from psd_tools.decoder.path import decode_path_resource - -_image_resource_decoders, register = decoders.new_registry() - -_image_resource_decoders.update({ - ImageResourceID.LAYER_STATE_INFO: decoders.single_value("H"), - ImageResourceID.WATERMARK: decoders.single_value("B"), - ImageResourceID.ICC_UNTAGGED_PROFILE: decoders.boolean(), - ImageResourceID.EFFECTS_VISIBLE: decoders.boolean(), - ImageResourceID.IDS_SEED_NUMBER: decoders.single_value("I"), - ImageResourceID.INDEXED_COLOR_TABLE_COUNT: decoders.single_value("H"), - ImageResourceID.TRANSPARENCY_INDEX: decoders.single_value("H"), - ImageResourceID.GLOBAL_ALTITUDE: decoders.single_value("i"), - ImageResourceID.GLOBAL_ANGLE: decoders.single_value("i"), - ImageResourceID.COPYRIGHT_FLAG: decoders.boolean("H"), - - ImageResourceID.ALPHA_NAMES_UNICODE: decoders.unicode_string, - ImageResourceID.WORKFLOW_URL: decoders.unicode_string, - ImageResourceID.AUTO_SAVE_FILE_PATH: decoders.unicode_string, - ImageResourceID.AUTO_SAVE_FORMAT: decoders.unicode_string, -}) - - -class HalftoneScreen(namedtuple( - 'HalftoneScreen', - 'ink_frequency units angle shape accurate_screen printer_default' -)): - """ - .. py:attribute:: ink_frequency - .. py:attribute:: units - .. py:attribute:: angle - .. py:attribute:: shape - .. py:attribute:: accurate_screen - .. py:attribute:: printer_default - """ - - -class TransferFunction(namedtuple('TransferFunction', 'curve override')): - """ - .. py:attribute:: curve - .. py:attribute:: override - """ - - -class PrintScale(namedtuple('PrintScale', 'style x y scale')): - """ - .. py:attribute:: style - .. py:attribute:: x - .. py:attribute:: y - .. py:attribute:: scale - """ - - -class PrintFlags(namedtuple( - 'PrintFlags', - 'labels crop_marks color_bars registration_marks negative flip ' - 'interpolate caption print_flags' -)): - """ - .. py:attribute:: labels - .. py:attribute:: crop_marks - .. py:attribute:: color_bars - .. py:attribute:: registration_marks - .. py:attribute:: negative - .. py:attribute:: flip - .. py:attribute:: interpolate - .. py:attribute:: caption - .. py:attribute:: print_flags - """ - - -class PrintFlagsInfo(namedtuple( - 'PrintFlagsInfo', - 'version center_crop_marks bleed_width_value bleed_width_scale' -)): - """ - .. py:attribute:: version - .. py:attribute:: center_crop_marks - .. py:attribute:: bleed_width_value - .. py:attribute:: bleed_width_scale - """ - - -class ThumbnailResource(namedtuple( - 'ThumbnailResource', - 'format width height widthbytes total_size size bits planes data' -)): - """ - """ - - -class SlicesHeader(namedtuple( - 'SlicesHeader', - 'descriptor_version descriptor' -)): - """ - .. py:attribute:: format - .. py:attribute:: width - .. py:attribute:: height - .. py:attribute:: widthbytes - .. py:attribute:: total_size - .. py:attribute:: size - .. py:attribute:: bits - .. py:attribute:: planes data - """ - - -class SlicesHeaderV6(namedtuple( - 'SlicesHeaderV6', - 'top left bottom right name count items' -)): - """ - .. py:attribute:: top - .. py:attribute:: left - .. py:attribute:: bottom - .. py:attribute:: right - .. py:attribute:: name - .. py:attribute:: count - .. py:attribute:: items - """ - - -class SlicesResourceBlock(namedtuple( - 'SlicesResourceBlock', - 'id group_id origin associated_id name type left top right bottom url ' - 'target message alt_tag cell_is_html cell_text horizontal_alignment ' - 'vertical_alignment alpha red green blue descriptor' -)): - """ - .. py:attribute:: id - .. py:attribute:: group_id - .. py:attribute:: origin - .. py:attribute:: associated_id - .. py:attribute:: name - .. py:attribute:: type - .. py:attribute:: left - .. py:attribute:: top - .. py:attribute:: right - .. py:attribute:: bottom - .. py:attribute:: url - .. py:attribute:: target - .. py:attribute:: message - .. py:attribute:: alt_tag - .. py:attribute:: cell_is_html - .. py:attribute:: cell_text - .. py:attribute:: horizontal_alignment - .. py:attribute:: vertical_alignment - .. py:attribute:: alpha - .. py:attribute:: red - .. py:attribute:: green - .. py:attribute:: blue - .. py:attribute:: descriptor - """ - - -class VersionInfo(namedtuple( - 'VersionInfo', - 'version has_real_merged_data writer_name reader_name file_version' -)): - """ - .. py:attribute:: version - .. py:attribute:: has_real_merged_data - .. py:attribute:: writer_name - .. py:attribute:: reader_name - .. py:attribute:: file_version - """ - - -class UrlListItem(namedtuple('UrlListItem', 'number id url')): - """ - .. py:attribute:: number - .. py:attribute:: id - .. py:attribute:: url - """ - - -class PixelAspectRatio(namedtuple('PixelAspectRatio', 'version aspect')): - """ - .. py:attribute:: version - .. py:attribute:: aspect - """ - - -class ResolutionInfo(namedtuple( - 'ResolutionInfo', - 'h_res h_res_unit width_unit v_res v_res_unit height_unit' -)): - """ - .. py:attribute:: h_res - .. py:attribute:: h_res_unit - .. py:attribute:: width_unit - .. py:attribute:: v_res - .. py:attribute:: v_res_unit - .. py:attribute:: height_unit - """ - def __repr__(self): - - return ("ResolutionInfo(h_res=%s, h_res_unit=%s, v_res=%s, " - "v_res_unit=%s, width_unit=%s, height_unit=%s)") % ( - self.h_res, - DisplayResolutionUnit.name_of(self.h_res_unit), - self.v_res, - DisplayResolutionUnit.name_of(self.v_res_unit), - DimensionUnit.name_of(self.width_unit), - DimensionUnit.name_of(self.height_unit), - ) - - -class LayerComps(namedtuple('LayerComps', 'descriptor_version descriptor')): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class MeasurementScale(namedtuple( - 'MeasurementScale', - 'descriptor_version descriptor' -)): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class TimelineInformation(namedtuple( - 'TimelineInformation', - 'descriptor_version descriptor' -)): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class SheetDisclosure(namedtuple( - 'SheetDisclosure', - 'descriptor_version descriptor' -)): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class OnionSkins(namedtuple('OnionSkins', 'descriptor_version descriptor')): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class CountInformation(namedtuple( - 'CountInformation', - 'descriptor_version descriptor' -)): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class PrintInformation(namedtuple( - 'PrintInformation', - 'descriptor_version descriptor' -)): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class PrintStyle(namedtuple( - 'PrintStyle', - 'descriptor_version descriptor' -)): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class PathSelectionState(namedtuple( - 'PathSelectionState', - 'descriptor_version descriptor' -)): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class GridGuideResource(namedtuple( - 'GridGuideResource', - 'version grid_horizontal grid_vertical guides' -)): - """ - .. py:attribute:: version - .. py:attribute:: grid_horizontal - .. py:attribute:: grid_vertical - .. py:attribute:: guides - """ - - -class GuideResourceBlock(namedtuple( - 'GuideResourceBlock', - 'location direction' -)): - """ - .. py:attribute:: location - .. py:attribute:: direction - """ - - -class OriginPathInfo(namedtuple( - 'OriginPathInfo', - 'descriptor_version descriptor' -)): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -def decode(image_resource_blocks): - """ - Replaces ``data`` of image resource blocks with parsed data structures. - """ - return [parse_image_resource(res) for res in image_resource_blocks] - - -def parse_image_resource(resource): - """ - Replaces ``data`` of image resource block with a parsed data structure. - """ - if not ImageResourceID.is_known(resource.resource_id): - warnings.warn("Unknown resource_id (%s)" % resource.resource_id) - - if (ImageResourceID.PATH_INFO_0 <= resource.resource_id and - ImageResourceID.PATH_INFO_LAST >= resource.resource_id): - decoder = decode_path_resource - else: - decoder = _image_resource_decoders.get(resource.resource_id, - lambda data: data) - return resource._replace(data=decoder(resource.data)) - - -def _decode_descriptor_resource(data, kls): - if isinstance(data, bytes): - fp = io.BytesIO(data) - version = read_fmt("I", fp)[0] - - try: - return kls(version, decode_descriptor(None, fp)) - except UnknownOSType as e: - warnings.warn("Ignoring image resource %s" % e) - return data - - -@register(ImageResourceID.GRAYSCALE_HALFTONING_INFO) -@register(ImageResourceID.COLOR_HALFTONING_INFO) -@register(ImageResourceID.DUOTONE_HALFTONING_INFO) -def _decode_halftone_screens(data): - if not len(data) == 72: - return data - fp = io.BytesIO(data) - descriptions = [] - for i in range(4): - # 16 bits + 16 bits fixed points. - ink_frequency = float(read_fmt("I", fp)[0]) / 0x10000 - units = read_fmt("h", fp)[0] - angle = read_fmt("I", fp)[0] / 0x10000 - shape = read_fmt("H", fp)[0] - padding = read_fmt("I", fp)[0] - if padding: - warnings.warn("Invalid halftone screens") - return data - accurate_screen, printer_default = read_fmt("2?", fp) - descriptions.append(HalftoneScreen( - ink_frequency, units, angle, shape, accurate_screen, - printer_default)) - return descriptions - - -@register(ImageResourceID.GRAYSCALE_TRANSFER_FUNCTION) -@register(ImageResourceID.COLOR_TRANSFER_FUNCTION) -@register(ImageResourceID.DUOTONE_TRANSFER_FUNCTION) -def _decode_transfer_function(data): - if not len(data) == 112: - return data - fp = io.BytesIO(data) - functions = [] - for i in range(4): - curve = read_fmt("13h", fp) - override = read_fmt("H", fp)[0] - functions.append(TransferFunction(curve, override)) - return functions - - -@register(ImageResourceID.LAYER_GROUP_INFO) -def _decode_layer_group_info(data): - return be_array_from_bytes("H", data) - - -@register(ImageResourceID.LAYER_SELECTION_IDS) -def _decode_layer_selection(data): - return be_array_from_bytes("I", data[2:]) - - -@register(ImageResourceID.LAYER_GROUPS_ENABLED_ID) -def _decode_layer_groups_enabled_id(data): - return be_array_from_bytes("B", data) - - -@register(ImageResourceID.THUMBNAIL_RESOURCE_PS4) -@register(ImageResourceID.THUMBNAIL_RESOURCE) -def _decode_thumbnail_resource(data): - fp = io.BytesIO(data) - fmt, width, height, widthbytes, total_size, size, bits, planes = read_fmt( - "6I2H", fp) - jfif = RawData(fp.read(size)) - return ThumbnailResource(fmt, width, height, widthbytes, total_size, size, - bits, planes, jfif) - - -@register(ImageResourceID.SLICES) -def _decode_slices(data): - fp = io.BytesIO(data) - version = read_fmt('I', fp)[0] - if version == 6: - return _decode_slices_v6(fp) - elif version in (7, 8): - return _decode_descriptor_resource(fp.read(-1), SlicesHeader) - else: - warnings.warn("Unsupported slices version %s. " - "Only version 7 or 8 slices supported." % version) - return data - - -def _decode_slices_v6(fp): - bbox = read_fmt('4I', fp) - name = read_unicode_string(fp) - count = read_fmt('I', fp)[0] - items = [] - for index in range(count): - items.append(_decode_slices_v6_block(fp)) - return SlicesHeaderV6(bbox[0], bbox[1], bbox[2], bbox[3], name, count, - items) - - -def _decode_slices_v6_block(fp): - slice_id, group_id, origin = read_fmt('3I', fp) - associated_id = read_fmt('I', fp)[0] if origin == 1 else None - name = read_unicode_string(fp) - slice_type, left, top, right, bottom = read_fmt('5I', fp) - url = read_unicode_string(fp) - target = read_unicode_string(fp) - message = read_unicode_string(fp) - alt_tag = read_unicode_string(fp) - cell_is_html = read_fmt('?', fp)[0] - cell_text = read_unicode_string(fp) - horizontal_alignment, vertical_alignment = read_fmt('2I', fp) - alpha, red, green, blue = read_fmt('4B', fp) - # Some version stores descriptor here, but the documentation unclear... - descriptor = None - return SlicesResourceBlock( - slice_id, group_id, origin, associated_id, name, slice_type, left, - top, right, bottom, url, target, message, alt_tag, cell_is_html, - cell_text, horizontal_alignment, vertical_alignment, alpha, red, - green, blue, descriptor) - - -@register(ImageResourceID.URL_LIST) -def _decode_url_list(data): - urls = [] - fp = io.BytesIO(data) - count = read_fmt("I", fp)[0] - - try: - for i in range(count): - number, id = read_fmt("2I", fp) - url = read_unicode_string(fp) - urls.append(UrlListItem(number, id, url)) - return urls - except UnknownOSType as e: - warnings.warn("Ignoring image resource %s" % e) - return data - - -@register(ImageResourceID.VERSION_INFO) -def _decode_version_info(data): - fp = io.BytesIO(data) - - return VersionInfo( - read_fmt("I", fp)[0], - read_fmt("?", fp)[0], - read_unicode_string(fp), - read_unicode_string(fp), - read_fmt("I", fp)[0], - ) - - -@register(ImageResourceID.EXIF_DATA_1) -@register(ImageResourceID.EXIF_DATA_3) -def _decode_exif_data(data): - try: - import exifread - except: - warnings.warn("EXIF data is ignored. Install exifread to decode.") - return data - - fp = io.BytesIO(data) - tags = exifread.process_file(fp) - exif = {} - for key in tags.keys(): - ifd = tags[key] - if isinstance(ifd, exifread.classes.IfdTag): - field_type = exifread.tags.FIELD_TYPES[ifd.field_type - 1] - if isinstance(ifd.printable, bytes): - try: - value = ifd.printable.decode('utf-8') - except UnicodeDecodeError: - value = ifd.printable.encode('string_escape') - else: - value = ifd.printable - if field_type[1] in ('A', 'B'): - exif[key] = value - else: - try: - exif[key] = int(value) - except ValueError: - exif[key] = value - else: - # Seems sometimes EXIF data is corrupt. - pass - return exif - - -@register(ImageResourceID.PIXEL_ASPECT_RATIO) -def _decode_pixel_aspect_ration(data): - version = unpack("I", data[:4])[0] - aspect = unpack("d", data[4:])[0] - return PixelAspectRatio(version, aspect) - - -@register(ImageResourceID.PRINT_FLAGS) -def _decode_print_flags(data): - try: - return PrintFlags(*(unpack("9?x", data))) - except struct.error as e: - warnings.warn("%s" % e) - return data - - -@register(ImageResourceID.PRINT_FLAGS_INFO) -def _decode_print_flags_info(data): - return PrintFlagsInfo(*(unpack("HBxIh", data))) - - -@register(ImageResourceID.PRINT_SCALE) -def _decode_print_scale(data): - style, x, y, scale = unpack("H3f", data) - - if not PrintScaleStyle.is_known(style): - warnings.warn("Unknown print scale style (%s)" % style) - - return PrintScale(style, x, y, scale) - - -@register(ImageResourceID.CAPTION_PASCAL) -def _decode_caption_pascal(data): - fp = io.BytesIO(data) - return read_pascal_string(fp, 'ascii') - - -@register(ImageResourceID.RESOLUTION_INFO) -def _decode_resolution(data): - h_res, h_res_unit, width_unit, v_res, v_res_unit, height_unit = unpack( - "4s HH 4s HH", data) - - h_res = decode_fixed_point_32bit(h_res) - v_res = decode_fixed_point_32bit(v_res) - - return ResolutionInfo( - h_res, h_res_unit, width_unit, v_res, v_res_unit, height_unit) - - -@register(ImageResourceID.GRID_AND_GUIDES_INFO) -def _decode_grid_and_guides_info(data): - fp = io.BytesIO(data) - version, grid_h, grid_v, guide_count = read_fmt("4I", fp) - - try: - guides = [] - for i in range(guide_count): - guides.append(GuideResourceBlock(*read_fmt("IB", fp))) - return GridGuideResource(version, grid_h, grid_v, guides) - except UnknownOSType as e: - warnings.warn("Ignoring image resource %s" % e) - return data - - -@register(ImageResourceID.ICC_PROFILE) -def _decode_icc(data): - try: - from PIL import ImageCms - return ImageCms.ImageCmsProfile(io.BytesIO(data)) - except ImportError: - warnings.warn( - "ICC profile is not handled; colors could be incorrect. " - "Please build PIL or Pillow with littlecms/littlecms2 support.") - return data - - -@register(ImageResourceID.BACKGROUND_COLOR) -def _decode_background_color(data): - fp = io.BytesIO(data) - return decode_color(fp) - - -@register(ImageResourceID.LAYER_COMPS) -def _decode_layer_comps(data): - return _decode_descriptor_resource(data, LayerComps) - - -@register(ImageResourceID.MEASUREMENT_SCALE) -def _decode_measurement_scale(data): - return _decode_descriptor_resource(data, MeasurementScale) - - -@register(ImageResourceID.TIMELINE_INFO) -def _decode_timeline_information(data): - return _decode_descriptor_resource(data, TimelineInformation) - - -@register(ImageResourceID.SHEET_DISCLOSURE) -def _decode_sheet_disclosure(data): - return _decode_descriptor_resource(data, SheetDisclosure) - - -@register(ImageResourceID.ONION_SKINS) -def _decode_onion_skins(data): - return _decode_descriptor_resource(data, OnionSkins) - - -@register(ImageResourceID.COUNT_INFO) -def _decode_count_information(data): - return _decode_descriptor_resource(data, CountInformation) - - -@register(ImageResourceID.PRINT_INFO_CS5) -def _decode_print_information_cs5(data): - return _decode_descriptor_resource(data, PrintInformation) - - -@register(ImageResourceID.PRINT_STYLE) -def _decode_print_style(data): - return _decode_descriptor_resource(data, PrintStyle) - - -@register(ImageResourceID.PATH_SELECTION_STATE) -def _decode_path_selection_state(data): - return _decode_descriptor_resource(data, PathSelectionState) - - -@register(ImageResourceID.CLIPPING_PATH_NAME) -def _decode_clipping_path_name(data): - fp = io.BytesIO(data) # TODO: flatness and fill rule decoding? - return read_pascal_string(fp, 'ascii') - - -@register(ImageResourceID.ORIGIN_PATH_INFO) -def _decode_origin_path_info(data): - return _decode_descriptor_resource(data, OriginPathInfo) diff --git a/src/psd_tools/decoder/layer_effects.py b/src/psd_tools/decoder/layer_effects.py deleted file mode 100644 index 978516f5..00000000 --- a/src/psd_tools/decoder/layer_effects.py +++ /dev/null @@ -1,355 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Module for decoding layer effects. -""" -from __future__ import absolute_import, unicode_literals, print_function -import warnings -import io - -from psd_tools.decoder import decoders -from psd_tools.decoder.actions import decode_descriptor, UnknownOSType -from psd_tools.decoder.color import decode_color -from psd_tools.exceptions import Error -from psd_tools.utils import read_fmt -from psd_tools.constants import EffectOSType, BlendMode -from psd_tools.debug import pretty_namedtuple - -_effect_info_decoders, register = decoders.new_registry() - - -class Effects(pretty_namedtuple( - 'Effects', - 'version effects_count effects_list' -)): - """ - .. py:attribute:: version - .. py:attribute:: effects_count - .. py:attribute:: effects_list - """ - - -class LayerEffect(pretty_namedtuple( - 'LayerEffect', - 'effect_type effect_info' -)): - """ - .. py:attribute:: effect_type - .. py:attribute:: effect_info - """ - - def __repr__(self): - return "LayerEffect(%s %s, %s)" % ( - self.effect_type, EffectOSType.name_of(self.effect_type), - self.effect_info - ) - - def _repr_pretty_(self, p, cycle): - # IS NOT TESTED!! - if cycle: - p.text('LayerEffect(...)') - else: - with p.group(1, 'LayerEffect(', ')'): - p.breakable() - p.text("%s %s," % ( - self.effect_type, EffectOSType.name_of(self.effect_type) - )) - p.breakable() - p.pretty(self.effect_info) - - -class ObjectBasedEffects(pretty_namedtuple( - 'ObjectBasedEffects', - 'version descriptor_version descriptor' -)): - """ - .. py:attribute:: version - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class CommonStateInfo(pretty_namedtuple( - 'CommonStateInfo', - 'version visible unused' -)): - """ - .. py:attribute:: version - .. py:attribute:: visible - .. py:attribute:: unused - """ - - -class ShadowInfo(pretty_namedtuple( - 'ShadowInfo', - 'version enabled blend_mode color opacity angle use_global_angle ' - 'distance intensity blur native_color' -)): - """ - .. py:attribute:: version - .. py:attribute:: enabled - .. py:attribute:: blend_mode - .. py:attribute:: color - .. py:attribute:: opacity - .. py:attribute:: angle - .. py:attribute:: use_global_angle - .. py:attribute:: distance - .. py:attribute:: intensity - .. py:attribute:: blur - .. py:attribute:: native_color - """ - - -class OuterGlowInfo(pretty_namedtuple( - 'OuterGlowInfo', - 'version enabled blend_mode opacity color intensity blur native_color' -)): - """ - .. py:attribute:: version - .. py:attribute:: enabled - .. py:attribute:: blend_mode - .. py:attribute:: opacity - .. py:attribute:: color - .. py:attribute:: intensity - .. py:attribute:: blur - .. py:attribute:: native_color - """ - - -class InnerGlowInfo(pretty_namedtuple( - 'InnerGlowInfo', - 'version enabled blend_mode opacity color intensity blur invert ' - 'native_color' -)): - """ - .. py:attribute:: version - .. py:attribute:: enabled - .. py:attribute:: blend_mode - .. py:attribute:: opacity - .. py:attribute:: color - .. py:attribute:: intensity - .. py:attribute:: blur - .. py:attribute:: invert - .. py:attribute:: native_color - """ - - -class BevelInfo(pretty_namedtuple( - 'BevelInfo', - 'version enabled bevel_style depth direction blur angle use_global_angle ' - 'highlight_blend_mode highlight_color highlight_opacity ' - 'shadow_blend_mode shadow_color shadow_opacity real_highlight_color ' - 'real_shadow_color' -)): - """ - .. py:attribute:: version - .. py:attribute:: enabled - .. py:attribute:: bevel_style - .. py:attribute:: depth - .. py:attribute:: direction - .. py:attribute:: blur - .. py:attribute:: angle - .. py:attribute:: use_global_angle - .. py:attribute:: highlight_blend_mode - .. py:attribute:: highlight_color - .. py:attribute:: highlight_opacity - .. py:attribute:: shadow_blend_mode - .. py:attribute:: shadow_color - .. py:attribute:: shadow_opacity - .. py:attribute:: real_highlight_color - .. py:attribute:: real_shadow_color - """ - - -class SolidFillInfo(pretty_namedtuple( - 'SolidFillInfo', - 'version enabled blend_mode color opacity native_color' -)): - """ - .. py:attribute:: version - .. py:attribute:: enabled - .. py:attribute:: blend_mode - .. py:attribute:: color - .. py:attribute:: opacity - .. py:attribute:: native_color - """ - - -def decode(effects, **kwargs): - """ - Reads and decodes info about layer effects. - """ - fp = io.BytesIO(effects) - - version, effects_count = read_fmt("HH", fp) - - effects_list = [] - for idx in range(effects_count): - sig = fp.read(4) - if sig != b'8BIM': - raise Error( - "Error parsing layer effect: invalid signature (%r)" % sig) - - effect_type = fp.read(4) - if not EffectOSType.is_known(effect_type): - warnings.warn("Unknown effect type (%s)" % effect_type) - - effect_info_length = read_fmt("I", fp)[0] - effect_info = fp.read(effect_info_length) - - decoder = _effect_info_decoders.get(effect_type, lambda data: data) - effects_list.append(LayerEffect(effect_type, decoder(effect_info))) - - return Effects(version, effects_count, effects_list) - - -def decode_object_based(effects, **kwargs): - """ - Reads and decodes info about object-based layer effects. - """ - fp = io.BytesIO(effects) - - version, descriptor_version = read_fmt("II", fp) - try: - descriptor = decode_descriptor(None, fp) - except UnknownOSType as e: - warnings.warn( - "Ignoring object-based layer effects tagged block (%s)" % e) - return effects - - return ObjectBasedEffects(version, descriptor_version, descriptor) - - -def _read_blend_mode(fp): - sig = fp.read(4) - if sig != b'8BIM': - raise Error( - "Error parsing layer effect: invalid signature (%r)" % sig) - - blend_mode = fp.read(4) - if not BlendMode.is_known(blend_mode): - warnings.warn("Unknown blend mode (%s)" % blend_mode) - - return blend_mode - - -@register(EffectOSType.COMMON_STATE) -def _decode_common_info(data): - version, visible, unused = read_fmt("IBH", io.BytesIO(data)) - return CommonStateInfo(version, bool(visible), unused) - - -@register(EffectOSType.DROP_SHADOW) -@register(EffectOSType.INNER_SHADOW) -def _decode_shadow_info(data): - fp = io.BytesIO(data) - - version, blur, intensity, angle, distance = read_fmt("IIIiI", fp) - color = decode_color(fp) - blend_mode = _read_blend_mode(fp) - enabled, use_global_angle, opacity = read_fmt("3B", fp) - - native_color = None - if version == 2: - native_color = decode_color(fp) - - return ShadowInfo( - version, bool(enabled), - blend_mode, color, opacity, - angle, bool(use_global_angle), - distance, intensity, blur, - native_color - ) - - -@register(EffectOSType.OUTER_GLOW) -def _decode_outer_glow_info(data): - fp = io.BytesIO(data) - - version, blur, intensity = read_fmt("3I", fp) - color = decode_color(fp) - blend_mode = _read_blend_mode(fp) - enabled, opacity = read_fmt("2B", fp) - - native_color = None - if version == 2: - native_color = decode_color(fp) - - return OuterGlowInfo( - version, bool(enabled), - blend_mode, opacity, color, - intensity, blur, - native_color - ) - - -@register(EffectOSType.INNER_GLOW) -def _decode_inner_glow_info(data): - fp = io.BytesIO(data) - - version, blur, intensity = read_fmt("3I", fp) - color = decode_color(fp) - blend_mode = _read_blend_mode(fp) - enabled, opacity = read_fmt("2B", fp) - - invert = None - native_color = None - if version == 2: - invert = bool(read_fmt("B", fp)[0]) - native_color = decode_color(fp) - - return InnerGlowInfo( - version, bool(enabled), - blend_mode, opacity, color, - intensity, blur, - invert, native_color - ) - - -@register(EffectOSType.BEVEL) -def _decode_bevel_info(data): - fp = io.BytesIO(data) - - version, angle, depth, blur = read_fmt("IiII", fp) - - highlight_blend_mode = _read_blend_mode(fp) - shadow_blend_mode = _read_blend_mode(fp) - - highlight_color = decode_color(fp) - shadow_color = decode_color(fp) - - bevel_style, highlight_opacity, shadow_opacity = read_fmt("3B", fp) - enabled, use_global_angle, direction = read_fmt("3B", fp) - - real_highlight_color = None - real_shadow_color = None - if version == 2: - real_highlight_color = decode_color(fp) - real_shadow_color = decode_color(fp) - - return BevelInfo( - version, bool(enabled), - bevel_style, - depth, direction, blur, - angle, bool(use_global_angle), - highlight_blend_mode, highlight_color, highlight_opacity, - shadow_blend_mode, shadow_color, shadow_opacity, - real_highlight_color, real_shadow_color - ) - - -@register(EffectOSType.SOLID_FILL) -def _decode_solid_fill_info(data): - fp = io.BytesIO(data) - - version = read_fmt("I", fp)[0] - blend_mode = _read_blend_mode(fp) - color = decode_color(fp) - opacity, enabled = read_fmt("2B", fp) - - native_color = decode_color(fp) - - return SolidFillInfo( - version, bool(enabled), - blend_mode, color, opacity, - native_color - ) diff --git a/src/psd_tools/decoder/linked_layer.py b/src/psd_tools/decoder/linked_layer.py deleted file mode 100644 index f94125b4..00000000 --- a/src/psd_tools/decoder/linked_layer.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, print_function -import warnings -import io -import struct - -from psd_tools.constants import LinkedLayerType -from psd_tools.utils import read_fmt, read_pascal_string, read_unicode_string -from psd_tools.debug import pretty_namedtuple -from psd_tools.decoder.actions import decode_descriptor - -LinkedLayerCollection = pretty_namedtuple('LinkedLayerCollection', - 'linked_list ') -_LinkedLayer = pretty_namedtuple( - 'LinkedLayer', - 'type version unique_id filename filetype creator filesize ' - 'file_open_descriptor linked_file_descriptor timestamp decoded ' - 'child_document_id asset_mod_time asset_lock_state') - - -class LinkedLayer(_LinkedLayer): - - def __repr__(self): - return "LinkedLayer(type=%s filename='%s', size=%s)" % ( - self.type, self.filename, self.filesize) - - def _repr_pretty_(self, p, cycle): - if cycle: - p.text("LinkedLayer(...)") - else: - with p.group(1, "LinkedLayer(", ")"): - p.breakable() - p.text("type='%s', " % self.type) - p.breakable() - p.text("version='%s', " % self.version) - p.breakable() - p.text("unique_id=%s, " % self.unique_id) - p.breakable() - p.text("filename='%s', " % self.filename) - p.breakable() - p.text("filetype='%s', " % self.filetype) - p.breakable() - p.text("creator='%s', " % self.creator) - p.breakable() - p.text("size=%s, " % self.filesize) - - -def decode(data): - """ - Reads and decodes info about linked layers. - - These are embedded files (embedded smart objects). But Adobe calls - them "linked layers", so we'll follow that nomenclature. - """ - fp = io.BytesIO(data) - layers = [] - while True: - start = fp.tell() - length_buf = fp.read(8) - if not length_buf: - break # end of file - length = struct.unpack(str('>Q'), length_buf)[0] - link_type, version = read_fmt('4s I', fp) - if link_type not in (b'liFD', b'liFE', b'liFA'): - warnings.warn('unknown layer type') - break - if version < 1 or 7 < version: - warnings.warn('unsupported linked layer version (%s)' % version) - break - - unique_id = read_pascal_string(fp, 'ascii') - filename = read_unicode_string(fp) - filetype, creator, datasize, have_file_open_descriptor = read_fmt( - '4s 4s Q B', fp) - if have_file_open_descriptor: - # Does not seem to contain any useful information - undocumented_integer = read_fmt("I", fp) - file_open_descriptor = decode_descriptor(None, fp) - else: - file_open_descriptor = None - - linked_file_descriptor = None - timestamp = None - decoded = None - file_size = None - child_document_id = None - asset_mod_time = None - asset_lock_state = None - - if link_type == LinkedLayerType.EXTERNAL: - fp.read(4) # Undocumented zero padding - linked_file_descriptor = decode_descriptor(None, fp) - if version > 3: - timestamp = read_fmt('I B B B B d', fp) - file_size = read_fmt('Q', fp)[0] # External file size. - if version > 2: - decoded = fp.read(datasize) - elif link_type == LinkedLayerType.ALIAS: - fp.seek(8, io.SEEK_CUR) - - if link_type == LinkedLayerType.DATA: - decoded = fp.read(datasize) - if len(decoded) != datasize: - warnings.warn('failed to read linked file data (%d vs %d)' % ( - len(decoded), datasize)) - - # The following are not well documented... - if version >= 5: - child_document_id = read_unicode_string(fp) - if version >= 6: - asset_mod_time = read_fmt('d', fp) - if version >= 7: - asset_lock_state = read_fmt('B', fp) - if link_type == LinkedLayerType.EXTERNAL and version == 2: - decoded = fp.read(datasize) - - layers.append( - LinkedLayer( - link_type, version, unique_id, filename, filetype, creator, - file_size if file_size else datasize, file_open_descriptor, - linked_file_descriptor, timestamp, decoded, child_document_id, - asset_mod_time, asset_lock_state) - ) - # Gobble up anything that we don't know how to decode - # first 8 bytes contained the length - expected_position = start + 8 + length - current_position = fp.tell() - if expected_position != current_position: - warnings.warn( - 'skipping over undocumented additional fields (%d vs %d)' % ( - current_position, expected_position)) - fp.read(expected_position - current_position) - # Each layer is padded to start and end at 4-byte boundary - pad = -fp.tell() % 4 - fp.read(pad) - - return LinkedLayerCollection(layers) diff --git a/src/psd_tools/decoder/path.py b/src/psd_tools/decoder/path.py deleted file mode 100644 index 812ee53d..00000000 --- a/src/psd_tools/decoder/path.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Module for decoding path resource. -""" -from __future__ import absolute_import, unicode_literals -import warnings - -import io -from psd_tools.utils import read_fmt -from psd_tools.constants import PathResource - - -# Path points are 8 bits + 24 bits fixed points. Convert to float here. -def _decode_fixed_point(fixed_point): - return tuple(float(x) / 0x01000000 for x in fixed_point) - - -def decode_path_resource(data): - fp = io.BytesIO(data) - path = [] - path_rec = len(data) // 26 - while path_rec > 0: - selector, = read_fmt("H", fp) - record = {"selector": selector} - if selector in ( - PathResource.CLOSED_SUBPATH_LENGTH_RECORD, - PathResource.OPEN_SUBPATH_LENGTH_RECORD - ): - record["num_knot_records"], = read_fmt("H", fp) - fp.seek(22, io.SEEK_CUR) - elif selector in ( - PathResource.CLOSED_SUBPATH_BEZIER_KNOT_LINKED, - PathResource.CLOSED_SUBPATH_BEZIER_KNOT_UNLINKED, - PathResource.OPEN_SUBPATH_BEZIER_KNOT_LINKED, - PathResource.OPEN_SUBPATH_BEZIER_KNOT_UNLINKED): - record["control_preceding_knot"] = _decode_fixed_point( - read_fmt("2i", fp)) - record["anchor"] = _decode_fixed_point(read_fmt("2i", fp)) - record["control_leaving_knot"] = _decode_fixed_point( - read_fmt("2i", fp)) - elif selector == PathResource.PATH_FILL_RULE_RECORD: - fp.seek(24, io.SEEK_CUR) - elif selector == PathResource.CLIPBOARD_RECORD: - record["top"], record["left"], record["bottom"], record["right"], - record["resolution"] = _decode_fixed_point(read_fmt("5i", fp)) - fp.seek(4, io.SEEK_CUR) - elif selector == PathResource.INITIAL_FILL_RULE_RECORD: - record["initial_fill_rule"], = read_fmt("H", fp) - fp.seek(22, io.SEEK_CUR) - else: - warnings.warn("Unknown path record found %s" % (selector)) - path.append(record) - path_rec -= 1 - return path diff --git a/src/psd_tools/decoder/tagged_blocks.py b/src/psd_tools/decoder/tagged_blocks.py deleted file mode 100644 index 89e27745..00000000 --- a/src/psd_tools/decoder/tagged_blocks.py +++ /dev/null @@ -1,1314 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Module for decoding tagged blocks. - -Tagged blocks are key-value configuration for individual layers or for a -photoshop document. The keys are specified by -:py:class:`~psd_tools.constants.TaggedBlock`. -""" -from __future__ import absolute_import, unicode_literals, print_function -import warnings -import collections -import io - -from psd_tools.constants import TaggedBlock, SectionDivider, ColorMode -from psd_tools.decoder.actions import ( - decode_descriptor, UnknownOSType, RawData -) -from psd_tools.utils import ( - read_fmt, unpack, read_unicode_string, read_pascal_string -) -from psd_tools.decoder import decoders, layer_effects -from psd_tools.decoder.color import decode_color -from psd_tools.decoder.path import decode_path_resource -from psd_tools.reader.layers import Block -from psd_tools.debug import pretty_namedtuple -from psd_tools.decoder import engine_data - -_tagged_block_decoders, register = decoders.new_registry() - -_tagged_block_decoders.update({ - TaggedBlock.BLEND_CLIPPING_ELEMENTS: decoders.boolean("I"), - TaggedBlock.BLEND_INTERIOR_ELEMENTS: decoders.boolean("I"), - TaggedBlock.BLEND_FILL_OPACITY: decoders.single_value("4B"), - TaggedBlock.KNOCKOUT_SETTING: decoders.boolean("I"), - TaggedBlock.UNICODE_LAYER_NAME: decoders.unicode_string, - # XXX: there are more fields in docs, but they seem to be incorrect - TaggedBlock.LAYER_ID: decoders.single_value("I"), - TaggedBlock.EFFECTS_LAYER: layer_effects.decode, - TaggedBlock.OBJECT_BASED_EFFECTS_LAYER_INFO: - layer_effects.decode_object_based, - TaggedBlock.OBJECT_BASED_EFFECTS_LAYER_INFO_V0: - layer_effects.decode_object_based, - TaggedBlock.OBJECT_BASED_EFFECTS_LAYER_INFO_V1: - layer_effects.decode_object_based, - TaggedBlock.USING_ALIGNED_RENDERING: decoders.boolean("I"), - TaggedBlock.LAYER_VERSION: decoders.single_value("I"), - TaggedBlock.TRANSPARENCY_SHAPES_LAYER: decoders.single_value("4B"), - TaggedBlock.LAYER_MASK_AS_GLOBAL_MASK: decoders.single_value("4B"), - TaggedBlock.VECTOR_MASK_AS_GLOBAL_MASK: decoders.single_value("4B"), -}) - - -class SolidColorSetting(pretty_namedtuple( - 'SolidColorSetting', - 'version data' -)): - """ - .. py:attribute:: version - .. py:attribute:: data - """ - - -class PatternFillSetting(pretty_namedtuple( - 'PatternFillSetting', - 'version data' -)): - """ - .. py:attribute:: version - .. py:attribute:: data - """ - - -class GradientFillSetting(pretty_namedtuple( - 'GradientFillSetting', - 'version data' -)): - """ - .. py:attribute:: version - .. py:attribute:: data - """ - - -class BrightnessContrast(pretty_namedtuple( - 'BrightnessContrast', - 'brightness contrast mean lab' -)): - """ - .. py:attribute:: brightness - .. py:attribute:: contrast - .. py:attribute:: mean - .. py:attribute:: lab - """ - - -class LevelsSettings(pretty_namedtuple('LevelsSettings', 'version data')): - """ - .. py:attribute:: version - .. py:attribute:: data - """ - - -class LevelRecord(pretty_namedtuple( - 'LevelRecord', - 'input_floor input_ceiling output_floor output_ceiling gamma' -)): - """ - .. py:attribute:: input_floor - .. py:attribute:: input_ceiling - .. py:attribute:: output_floor - .. py:attribute:: output_ceiling - .. py:attribute:: gamma - """ - - -class CurvesSettings(pretty_namedtuple( - 'CurvesSettings', - 'version count data extra' -)): - """ - .. py:attribute:: version - .. py:attribute:: count - .. py:attribute:: data - .. py:attribute:: extra - """ - - -class CurvesExtraMarker(pretty_namedtuple( - 'CurvesExtraMarker', - 'tag version count data' -)): - """ - .. py:attribute:: tag - .. py:attribute:: version - .. py:attribute:: count - .. py:attribute:: data - """ - - -class CurveData(pretty_namedtuple('CurveData', 'channel points')): - """ - .. py:attribute:: channel - .. py:attribute:: points - """ - - -class Exposure(pretty_namedtuple( - 'Exposure', - 'version exposure offset gamma' -)): - """ - .. py:attribute:: version - .. py:attribute:: exposure - .. py:attribute:: offset - .. py:attribute:: gamma - """ - - -class Vibrance(pretty_namedtuple( - 'Vibrance', - 'descriptor_version descriptor' -)): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class HueSaturation(pretty_namedtuple( - 'HueSaturation', - 'version enable_colorization colorization master items' -)): - """ - .. py:attribute:: version - .. py:attribute:: enable_colorization - .. py:attribute:: colorization - .. py:attribute:: master - .. py:attribute:: items - """ - - -class HueSaturationData(pretty_namedtuple( - 'HueSaturationData', - 'range settings' -)): - """ - .. py:attribute:: range - .. py:attribute:: settings - """ - - -class ColorBalance(pretty_namedtuple( - 'ColorBalance', - 'shadows midtones highlights preserve_luminosity' -)): - """ - .. py:attribute:: shadows - .. py:attribute:: midtones - .. py:attribute:: highlights - .. py:attribute:: preserve_luminosity - """ - - -class BlackWhite(pretty_namedtuple( - 'BlackWhite', - 'descriptor_version descriptor' -)): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class PhotoFilter(pretty_namedtuple( - 'PhotoFilter', - 'version xyz color_space color_components density preserve_luminosity' -)): - """ - .. py:attribute:: version - .. py:attribute:: xyz - .. py:attribute:: color_space - .. py:attribute:: color_components - .. py:attribute:: density - .. py:attribute:: preserve_luminosity - """ - - -class ChannelMixer(pretty_namedtuple( - 'ChannelMixer', - 'version monochrome mixer_settings' -)): - """ - .. py:attribute:: version - .. py:attribute:: monochrome - .. py:attribute:: mixer_settings - """ - - -class ColorLookup(pretty_namedtuple( - 'ColorLookup', - 'version descriptor_version descriptor' -)): - """ - .. py:attribute:: version - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class Invert(pretty_namedtuple('Invert', '')): - """ - """ - - -class Posterize(pretty_namedtuple('Posterize', 'value')): - """ - .. py:attribute:: value - """ - - -class Threshold(pretty_namedtuple('Threshold', 'value')): - """ - .. py:attribute:: value - """ - - -class SelectiveColor(pretty_namedtuple( - 'SelectiveColor', - 'version method items' -)): - """ - .. py:attribute:: version - .. py:attribute:: method - .. py:attribute:: items - """ - - -class Pattern(pretty_namedtuple( - 'Pattern', - 'version image_mode point name pattern_id color_table data' -)): - """ - .. py:attribute:: version - .. py:attribute:: image_mode - .. py:attribute:: point - .. py:attribute:: name - .. py:attribute:: pattern_id - .. py:attribute:: color_table data - """ - - -class VirtualMemoryArrayList(pretty_namedtuple( - 'VirtualMemoryArrayList', - 'version rectangle channels' -)): - """ - .. py:attribute:: version - .. py:attribute:: rectangle - .. py:attribute:: channels - """ - - -class VirtualMemoryArray(pretty_namedtuple( - 'VirtualMemoryArray', - 'is_written depth rectangle pixel_depth compression data' -)): - """ - .. py:attribute:: is_written - .. py:attribute:: depth - .. py:attribute:: rectangle - .. py:attribute:: pixel_depth - .. py:attribute:: compression data - """ - - -class GradientSettings(pretty_namedtuple( - 'GradientSettings', - 'version reversed dithered name color_stops transparency_stops expansion ' - 'interpolation length mode random_seed show_transparency ' - 'use_vector_color roughness color_model min_color max_color' -)): - """ - .. py:attribute:: version - .. py:attribute:: reversed - .. py:attribute:: dithered - .. py:attribute:: name - .. py:attribute:: color_stops - .. py:attribute:: transparency_stops - .. py:attribute:: expansion - .. py:attribute:: interpolation - .. py:attribute:: length - .. py:attribute:: mode - .. py:attribute:: random_seed - .. py:attribute:: show_transparency - .. py:attribute:: use_vector_color - .. py:attribute:: roughness - .. py:attribute:: color_model - .. py:attribute:: min_color - .. py:attribute:: max_color - """ - - -class ColorStop(pretty_namedtuple( - 'ColorStop', - 'location midpoint mode color' -)): - """ - .. py:attribute:: location - .. py:attribute:: midpoint - .. py:attribute:: mode - .. py:attribute:: color - """ - - -class TransparencyStop(pretty_namedtuple( - 'TransparencyStop', - 'location midpoint opacity expansion interpolation length mode' -)): - """ - .. py:attribute:: location - .. py:attribute:: midpoint - .. py:attribute:: opacity - .. py:attribute:: expansion - .. py:attribute:: interpolation - .. py:attribute:: length - .. py:attribute:: mode - """ - - -class ExportSetting(pretty_namedtuple('ExportSetting', 'version data')): - """ - .. py:attribute:: version - .. py:attribute:: data - """ - - -class VectorStrokeSetting(pretty_namedtuple( - 'VectorStrokeSetting', - 'version data' -)): - """ - .. py:attribute:: version - .. py:attribute:: data - """ - - -class VectorStrokeContentSetting(pretty_namedtuple( - 'VectorStrokeContentSetting', - 'key version data' -)): - """ - .. py:attribute:: key - .. py:attribute:: version - .. py:attribute:: data - """ - - -class MetadataItem(pretty_namedtuple( - 'MetadataItem', - 'key copy_on_sheet_duplication descriptor_version data' -)): - """ - .. py:attribute:: key - .. py:attribute:: copy_on_sheet_duplication - .. py:attribute:: descriptor_version - .. py:attribute:: data - """ - - -class ProtectedSetting(pretty_namedtuple( - 'ProtectedSetting', - 'transparency composite position' -)): - """ - .. py:attribute:: transparency - .. py:attribute:: composite - .. py:attribute:: position - """ - - -class TypeToolObjectSetting(pretty_namedtuple( - 'TypeToolObjectSetting', - 'version xx xy yx yy tx ty text_version descriptor1_version text_data ' - 'warp_version descriptor2_version warp_data left top right bottom' -)): - """ - .. py:attribute:: version - .. py:attribute:: xx - .. py:attribute:: xy - .. py:attribute:: yx - .. py:attribute:: yy - .. py:attribute:: tx - .. py:attribute:: ty - .. py:attribute:: text_version - .. py:attribute:: descriptor1_version - .. py:attribute:: text_data - .. py:attribute:: warp_version - .. py:attribute:: descriptor2_version - .. py:attribute:: warp_data - .. py:attribute:: left - .. py:attribute:: top - .. py:attribute:: right - .. py:attribute:: bottom - """ - - -class ContentGeneratorExtraData(pretty_namedtuple( - 'ContentGeneratorExtraData', - 'descriptor_version descriptor' -)): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class UnicodePathName(pretty_namedtuple( - 'UnicodePathName', - 'descriptor_version descriptor' -)): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class AnimationEffects(pretty_namedtuple( - 'AnimationEffects', - 'descriptor_version descriptor' -)): - """ - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class FilterMask(pretty_namedtuple('FilterMask', 'color opacity')): - """ - .. py:attribute:: color - .. py:attribute:: opacity - """ - - -class VectorOriginationData(pretty_namedtuple( - 'VectorOriginationData', - 'version descriptor_version data' -)): - """ - .. py:attribute:: version - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class VectorMaskSetting(pretty_namedtuple( - 'VectorMaskSetting', - 'version invert not_link disable path' -)): - """ - .. py:attribute:: version - .. py:attribute:: invert - .. py:attribute:: not_link - .. py:attribute:: disable path - """ - - -class PixelSourceData(pretty_namedtuple('PixelSourceData', 'version data')): - """ - .. py:attribute:: version - .. py:attribute:: data - """ - - -class ArtboardData(pretty_namedtuple('ArtboardData', 'version data')): - """ - .. py:attribute:: version - .. py:attribute:: data - """ - - -class UserMask(pretty_namedtuple('UserMask', 'color opacity flag')): - """ - .. py:attribute:: color - .. py:attribute:: opacity - .. py:attribute:: flag - """ - - -class FilterEffects(pretty_namedtuple( - 'FilterEffects', - 'uuid version rectangle depth max_channels channels extra_data' -)): - """ - .. py:attribute:: uuid - .. py:attribute:: version - .. py:attribute:: rectangle - .. py:attribute:: depth - .. py:attribute:: max_channels - .. py:attribute:: channels - .. py:attribute:: extra_data - """ - - -class FilterEffectChannel(pretty_namedtuple( - 'FilterEffectChannel', - 'is_written compression data' -)): - """ - .. py:attribute:: is_written - .. py:attribute:: compression - .. py:attribute:: data - """ - - -class PlacedLayerObsolete(pretty_namedtuple( - 'PlacedLayerObsolete', - 'type version uuid page total_pages anti_alias layer_type transformation ' - 'warp' -)): - """ - .. py:attribute:: type - .. py:attribute:: version - .. py:attribute:: uuid - .. py:attribute:: page - .. py:attribute:: total_pages - .. py:attribute:: anti_alias - .. py:attribute:: layer_type - .. py:attribute:: transformation - .. py:attribute:: warp - """ - - -class WarpInformation(pretty_namedtuple( - 'WarpInformation', - 'version descriptor_version descriptor' -)): - """ - .. py:attribute:: version - .. py:attribute:: descriptor_version - .. py:attribute:: descriptor - """ - - -class ComputerInfo(pretty_namedtuple( - 'ComputerInfo', - 'version data' -)): - """ - .. py:attribute:: version - .. py:attribute:: data - """ - - -class Divider(collections.namedtuple('Divider', 'block type key')): - """ - .. py:attribute:: block - .. py:attribute:: type - .. py:attribute:: key - """ - def __repr__(self): - return "Divider(%s %r %s, %s)" % ( - self.block, self.type, SectionDivider.name_of(self.type), self.key - ) - - -def decode(tagged_blocks, version): - """ - Replaces "data" attribute of a blocks from ``tagged_blocks`` list - with parsed data structure if it is known how to parse it. - """ - return [parse_tagged_block(block, version) for block in tagged_blocks] - - -def parse_tagged_block(block, version=1, **kwargs): - """ - Replaces "data" attribute of a block with parsed data structure - if it is known how to parse it. - """ - if not TaggedBlock.is_known(block.key): - warnings.warn("Unknown tagged block (%s)" % block.key) - - decoder = _tagged_block_decoders.get( - block.key, lambda data, **kwargs: data) - return Block(block.key, decoder(block.data, version=version)) - - -def _decode_descriptor_block(data, kls): - if len(data) == 0: - warnings.warn("Empty descriptor") - return data - try: - fp = io.BytesIO(data) - version = read_fmt("I", fp)[0] - return kls(version, decode_descriptor(None, fp)) - except UnknownOSType as e: - warnings.warn("Ignoring tagged block %s" % e) - return data - - -@register(TaggedBlock.SOLID_COLOR_SHEET_SETTING) -def _decode_soco(data, **kwargs): - return _decode_descriptor_block(data, SolidColorSetting) - - -@register(TaggedBlock.PATTERN_FILL_SETTING) -def _decode_ptfl(data, **kwargs): - return _decode_descriptor_block(data, PatternFillSetting) - - -@register(TaggedBlock.GRADIENT_FILL_SETTING) -def _decode_grfl(data, **kwargs): - return _decode_descriptor_block(data, GradientFillSetting) - - -@register(TaggedBlock.BRIGHTNESS_AND_CONTRAST) -def _decode_brightness_and_contrast(data, **kwargs): - return BrightnessContrast(*read_fmt("3H B", io.BytesIO(data))) - - -@register(TaggedBlock.LEVELS) -def _decode_levels(data, **kwargs): - def read_level_record(fp): - input_f, input_c, output_f, output_c, gamma = read_fmt("5H", fp) - return LevelRecord( - input_f, input_c, output_f, output_c, gamma / 100.0) - - fp = io.BytesIO(data) - version = read_fmt("H", fp)[0] - level_records = [read_level_record(fp) for i in range(29)] - - # decode extra level record, Photoshop CS (8.0) Additional information - if fp.tell() < len(data): - signature = read_fmt('4s', fp)[0] - assert signature == b'Lvls', 'unexpected token: {0}'.format(signature) - _ = read_fmt('H', fp)[0] # version (= 3) - count = read_fmt('H', fp)[0] - 29 - level_records = level_records + [ - read_level_record(fp) for i in range(count) - ] - - return LevelsSettings(version, level_records) - - -@register(TaggedBlock.CURVES) -def _decode_curves(data, **kwargs): - """ - Curve decoding is highly experimental and unstable. - """ - fp = io.BytesIO(data) - # Documentation wrong. - is_map, version, count_map = read_fmt("B H I", fp) - if version not in (1, 4): - warnings.warn("Invalid curves version {}".format(version)) - return data - if version == 1: - count = bin(count_map).count("1") # Bitmap = channel index? - - items = [] - for i in range(count): - if is_map: - # This lookup format is never documented. - points = list(read_fmt("256B", fp)) - else: - point_count = read_fmt("H", fp)[0] - if point_count < 2 or 19 < point_count: - warnings.warn("point count not in [2, 19]") - return data - points = [read_fmt("2H", fp) for c in range(point_count)] - items.append(CurveData(None, points)) - extra = None - if version == 1: - tag, version_, count_ = read_fmt("4s H I", fp) - assert tag == b'Crv ' - extra_items = [] - for i in range(count_): - if is_map: - channel_index = read_fmt("H", fp)[0] - points = list(read_fmt("256B", fp)) - else: - channel_index, point_count = read_fmt("2H", fp) - points = [read_fmt("2H", fp) for c in range(point_count)] - extra_items.append(CurveData(channel_index, points)) - extra = CurvesExtraMarker(tag, version_, count_, extra_items) - return CurvesSettings(version, count, items, extra) - - -@register(TaggedBlock.EXPOSURE) -def _decode_exposure(data, **kwargs): - return Exposure(*read_fmt("H 3f", io.BytesIO(data))) - - -@register(TaggedBlock.VIBRANCE) -def _decode_vibrance(data, **kwargs): - return _decode_descriptor_block(data, Vibrance) - - -@register(TaggedBlock.HUE_SATURATION_V4) -@register(TaggedBlock.HUE_SATURATION) -def _decode_hue_saturation(data, **kwargs): - fp = io.BytesIO(data) - version, enable_colorization, _ = read_fmt('H 2B', fp) - if version != 2: - warnings.warn("Invalid Hue/saturation version {}".format(version)) - return data - colorization = read_fmt("3h", fp) - master = read_fmt("3h", fp) - items = [] - for i in range(6): - range_values = read_fmt("4h", fp) - settings_values = read_fmt("3h", fp) - items.append(HueSaturationData(range_values, settings_values)) - return HueSaturation(version, enable_colorization, colorization, master, - items) - - -@register(TaggedBlock.COLOR_BALANCE) -def _decode_color_balance(data, **kwargs): - # Undocumented, following PhotoFilter format. - fp = io.BytesIO(data) - shadows = read_fmt("3h", fp) - midtones = read_fmt("3h", fp) - highlights = read_fmt("3h", fp) - preserve_luminosity = read_fmt("B", fp)[0] - return ColorBalance(shadows, midtones, highlights, preserve_luminosity) - - -@register(TaggedBlock.BLACK_AND_WHITE) -def _decode_black_white(data, **kwargs): - return _decode_descriptor_block(data, BlackWhite) - - -@register(TaggedBlock.PHOTO_FILTER) -def _decode_photo_filter(data, **kwargs): - fp = io.BytesIO(data) - version = read_fmt("H", fp)[0] - if version not in (2, 3): - warnings.warn("Invalid Photo Filter version {}".format(version)) - return data - if version == 3: - xyz = read_fmt("3I", fp) - color_space = None - color_components = None - else: - xyz = None - color_space = read_fmt("H", fp)[0] - color_components = read_fmt("4H", fp) - density, preserve_luminosity = read_fmt("I B", fp) - return PhotoFilter(version, xyz, color_space, color_components, - density, preserve_luminosity) - - -@register(TaggedBlock.CHANNEL_MIXER) -def _decode_channel_mixer(data, **kwargs): - fp = io.BytesIO(data) - version, monochrome = read_fmt("2H", fp) - settings = read_fmt("5H", fp) - return ChannelMixer(version, monochrome, settings) - - -@register(TaggedBlock.COLOR_LOOKUP) -def _decode_color_lookup(data, **kwargs): - fp = io.BytesIO(data) - version, descriptor_version = read_fmt("H I", fp) - - try: - return ColorLookup(version, descriptor_version, - decode_descriptor(None, fp)) - except UnknownOSType as e: - warnings.warn("Ignoring tagged block %s" % e) - return data - - -@register(TaggedBlock.INVERT) -def _decode_invert(data, **kwargs): - return Invert() - - -@register(TaggedBlock.POSTERIZE) -def _decode_posterize(data, **kwargs): - return Posterize(read_fmt("2H", io.BytesIO(data))[0]) - - -@register(TaggedBlock.THRESHOLD) -def _decode_threshold(data, **kwargs): - return Threshold(read_fmt("2H", io.BytesIO(data))[0]) - - -@register(TaggedBlock.SELECTIVE_COLOR) -def _decode_selective_color(data, **kwargs): - fp = io.BytesIO(data) - version, method = read_fmt("2H", fp) - if version != 1: - warnings.warn("Invalid Selective Color version %s" % (version)) - return data - items = [read_fmt("4h", fp) for i in range(10)] - return SelectiveColor(version, method, items) - - -@register(TaggedBlock.PATTERNS1) -@register(TaggedBlock.PATTERNS2) -@register(TaggedBlock.PATTERNS3) -def _decode_patterns(data, **kwargs): - fp = io.BytesIO(data) - patterns = [] - while fp.tell() < len(data) - 4: - length = read_fmt("I", fp)[0] - if length == 0: - break - patterns.append(_decode_pattern(fp.read(length))) - extra_bytes = fp.tell() % 4 - if extra_bytes: - fp.read(4 - extra_bytes) # 4-bytes padding. - return patterns - - -def _decode_pattern(data): - fp = io.BytesIO(data) - version, image_mode = read_fmt("2I", fp) - if version != 1: - warnings.warn("Unsupported patterns version %s" % (version)) - return data - - point = read_fmt("2h", fp) - name = read_unicode_string(fp) - pattern_id = read_pascal_string(fp, 'ascii') - color_table = None - if image_mode == ColorMode.INDEXED: - color_table = [read_fmt("3B", fp) for i in range(256)] - read_fmt('4B', fp) # Undocumented field here... - vma_list = _decode_virtual_memory_array_list(fp) - return Pattern(version, image_mode, point, name, pattern_id, color_table, - vma_list) - - -def _decode_virtual_memory_array_list(fp): - version, length = read_fmt("2I", fp) - if version != 3: - warnings.warn("Unsupported virtual memory array list %s" % (version)) - return None - start = fp.tell() - rectangle = read_fmt("4I", fp) - num_channels = read_fmt("I", fp)[0] - channels = [] - for i in range(num_channels + 2): - is_written = read_fmt("I", fp)[0] - if is_written == 0: - continue - array_length = read_fmt("I", fp)[0] - if array_length == 0: - continue - depth = read_fmt("I", fp)[0] - array_rect = read_fmt("4I", fp) - pixel_depth, compression = read_fmt("H B", fp) - channel_data = RawData(fp.read(array_length - 23)) - channels.append(VirtualMemoryArray( - is_written, depth, array_rect, pixel_depth, compression, - channel_data - )) - return VirtualMemoryArrayList(version, rectangle, channels) - - -@register(TaggedBlock.GRADIENT_MAP_SETTING) -def _decode_gradient_settings(data, **kwargs): - fp = io.BytesIO(data) - version, is_reversed, is_dithered = read_fmt("H 2B", fp) - if version != 1: - warnings.warn("Invalid Gradient settings version %s" % (version)) - return data - name = read_unicode_string(fp) - color_count = read_fmt("H", fp)[0] - color_stops = [] - for i in range(color_count): - location, midpoint, mode = read_fmt("2i H", fp) - color = read_fmt("4H", fp) - color_stops.append(ColorStop(location, midpoint, mode, color)) - read_fmt("H", fp) # Undocumented pad. - transparency_count = read_fmt("H", fp)[0] - transparency_stops = [] - for i in range(transparency_count): - transparency_stops.append(read_fmt("2I H", fp)) - - expansion, interpolation, length, mode = read_fmt("4H", fp) - if expansion != 2 or length != 32: - warnings.warn("Ignoring Gradient settings") - return data - random_seed, show_transparency, use_vector_color = read_fmt("I 2H", fp) - roughness, color_model = read_fmt("I H", fp) - minimum_color = read_fmt("4H", fp) - maximum_color = read_fmt("4H", fp) - read_fmt("H", fp) # Dummy pad. - - return GradientSettings( - version, is_reversed, is_dithered, name, color_stops, - transparency_stops, expansion, interpolation, length, mode, - random_seed, show_transparency, use_vector_color, roughness, - color_model, minimum_color, maximum_color) - - -@register(TaggedBlock.EXPORT_SETTING1) -@register(TaggedBlock.EXPORT_SETTING2) -def _decode_extn(data, **kwargs): - return _decode_descriptor_block(data, ExportSetting) - - -@register(TaggedBlock.REFERENCE_POINT) -def _decode_reference_point(data, **kwargs): - return read_fmt("2d", io.BytesIO(data)) - - -@register(TaggedBlock.SHEET_COLOR_SETTING) -def _decode_color_setting(data, **kwargs): - return read_fmt("4H", io.BytesIO(data)) - - -@register(TaggedBlock.SECTION_DIVIDER_SETTING) -def _decode_section_divider(data, **kwargs): - tp, key = _decode_divider(data) - return Divider(TaggedBlock.SECTION_DIVIDER_SETTING, tp, key) - - -@register(TaggedBlock.NESTED_SECTION_DIVIDER_SETTING) -def _decode_section_divider(data, **kwargs): - tp, key = _decode_divider(data) - return Divider(TaggedBlock.NESTED_SECTION_DIVIDER_SETTING, tp, key) - - -def _decode_divider(data): - fp = io.BytesIO(data) - key = None - tp = read_fmt("I", fp)[0] - if not SectionDivider.is_known(tp): - warnings.warn("Unknown section divider type (%s)" % tp) - - if len(data) == 12: - sig = fp.read(4) - if sig != b'8BIM': - warnings.warn("Invalid signature in section divider block") - key = fp.read(4) - - return tp, key - - -@register(TaggedBlock.PLACED_LAYER_DATA) -@register(TaggedBlock.SMART_OBJECT_PLACED_LAYER_DATA) -def _decode_placed_layer(data, **kwargs): - fp = io.BytesIO(data) - type, version, descriptorVersion = read_fmt("4s I I", fp) - descriptor = decode_descriptor(None, fp) - return descriptor.items - - -@register(TaggedBlock.VECTOR_STROKE_DATA) -def _decode_vector_stroke_data(data, **kwargs): - fp = io.BytesIO(data) - version = read_fmt("I", fp)[0] - - if version != 16: - warnings.warn("Invalid vstk version %s" % (version)) - return data - - try: - data = decode_descriptor(None, fp) - return VectorStrokeSetting(version, data) - except UnknownOSType as e: - warnings.warn("Ignoring vstk tagged block (%s)" % e) - return data - - -@register(TaggedBlock.VECTOR_STROKE_CONTENT_DATA) -def _decode_vector_stroke_content_data(data, **kwargs): - fp = io.BytesIO(data) - key, version = read_fmt("II", fp) - - if version != 16: - warnings.warn("Invalid vscg version %s" % (version)) - return data - - try: - descriptor = decode_descriptor(None, fp) - except UnknownOSType as e: - warnings.warn("Ignoring vscg tagged block (%s)" % e) - return data - - return VectorStrokeContentSetting(key, version, descriptor) - - -@register(TaggedBlock.METADATA_SETTING) -def _decode_metadata(data, **kwargs): - fp = io.BytesIO(data) - items_count = read_fmt("I", fp)[0] - items = [] - - for x in range(items_count): - sig = fp.read(4) - if sig != b'8BIM': - warnings.warn("Invalid signature in metadata item (%s)" % sig) - - key, copy_on_sheet, data_length = read_fmt("4s ? 3x I", fp) - - data = fp.read(data_length) - if data_length < 4+12: - # descr_version is 4 bytes, descriptor is at least 12 bytes, - # so data can't be a descriptor. - descr_ver = None - else: - # try load data as a descriptor - fp2 = io.BytesIO(data) - descr_ver = read_fmt("I", fp2)[0] - try: - data = decode_descriptor(None, fp2) - except UnknownOSType as e: - # FIXME: can it fail with other exceptions? - descr_ver = None - warnings.warn("Can't decode metadata item (%s)" % e) - - items.append(MetadataItem(key, copy_on_sheet, descr_ver, data)) - - return items - - -@register(TaggedBlock.PROTECTED_SETTING) -def _decode_protected(data, **kwargs): - flag = unpack("I", data)[0] - return ProtectedSetting( - bool(flag & 1), - bool(flag & 2), - bool(flag & 4), - ) - - -@register(TaggedBlock.LAYER_32) -def _decode_layer32(data, version=1, **kwargs): - from psd_tools.reader import layers - from psd_tools.decoder.decoder import decode_layers - fp = io.BytesIO(data) - layers = layers._read_layers( - fp, 'latin1', 32, length=len(data), version=version) - return decode_layers(layers, version) - - -@register(TaggedBlock.LAYER_16) -def _decode_layer16(data, version=1, **kwargs): - from psd_tools.reader import layers - from psd_tools.decoder.decoder import decode_layers - fp = io.BytesIO(data) - layers = layers._read_layers( - fp, 'latin1', 16, length=len(data), version=version) - return decode_layers(layers, version) - - -@register(TaggedBlock.TYPE_TOOL_OBJECT_SETTING) -def _decode_type_tool_object_setting(data, **kwargs): - fp = io.BytesIO(data) - ver, xx, xy, yx, yy, tx, ty, txt_ver, descr1_ver = read_fmt( - "H 6d H I", fp) - - # This decoder needs to be updated if we have new formats. - if ver != 1 or txt_ver != 50 or descr1_ver != 16: - warnings.warn( - "Ignoring type setting tagged block due to old versions") - return data - - try: - text_data = decode_descriptor(None, fp) - except UnknownOSType as e: - warnings.warn("Ignoring type setting tagged block (%s)" % e) - return data - - # Decode EngineData here. - for index in range(len(text_data.items)): - item = text_data.items[index] - if item[0] == b'EngineData': - text_data.items[index] = ( - b'EngineData', engine_data.decode(item[1].value)) - - warp_ver, descr2_ver = read_fmt("H I", fp) - if warp_ver != 1 or descr2_ver != 16: - warnings.warn( - "Ignoring type setting tagged block due to old versions") - return data - - try: - warp_data = decode_descriptor(None, fp) - except UnknownOSType as e: - warnings.warn("Ignoring type setting tagged block (%s)" % e) - return data - - left, top, right, bottom = read_fmt("4i", fp) # wrong info in specs... - return TypeToolObjectSetting( - ver, xx, xy, yx, yy, tx, ty, txt_ver, descr1_ver, text_data, - warp_ver, descr2_ver, warp_data, left, top, right, bottom - ) - - -@register(TaggedBlock.CONTENT_GENERATOR_EXTRA_DATA) -def _decode_content_generator_extra_data(data, **kwargs): - return _decode_descriptor_block(data, ContentGeneratorExtraData) - - -@register(TaggedBlock.TEXT_ENGINE_DATA) -def _decode_text_engine_data(data, **kwargs): - return engine_data.decode(data) - - -@register(TaggedBlock.UNICODE_PATH_NAME) -def _decode_unicode_path_name(data, **kwargs): - if data: - return _decode_descriptor_block(data, UnicodePathName) - else: - warnings.warn("Empty Unicode Path Name") - return None - - -@register(TaggedBlock.ANIMATION_EFFECTS) -def _decode_animation_effects(data, **kwargs): - return _decode_descriptor_block(data, AnimationEffects) - - -@register(TaggedBlock.FILTER_MASK) -def _decode_filter_mask(data, **kwargs): - fp = io.BytesIO(data) - color = decode_color(fp) - opacity = read_fmt("H", fp)[0] - return FilterMask(color, opacity) - - -@register(TaggedBlock.VECTOR_ORIGINATION_DATA) -def _decode_vector_origination_data(data, **kwargs): - fp = io.BytesIO(data) - ver, descr_ver = read_fmt("II", fp) - - if ver != 1 and descr_ver != 16: - warnings.warn("Invalid vmsk version %s %s" % (ver, descr_ver)) - return data - - try: - vector_origination_data = decode_descriptor(None, fp) - except UnknownOSType as e: - warnings.warn("Ignoring vector origination tagged block (%s)" % e) - return data - - return VectorOriginationData(ver, descr_ver, vector_origination_data) - - -@register(TaggedBlock.PIXEL_SOURCE_DATA1) -def _decode_pixel_source_data1(data, **kwargs): - return _decode_descriptor_block(data, PixelSourceData) - - -@register(TaggedBlock.PIXEL_SOURCE_DATA2) -def _decode_pixel_source_data2(data, **kwargs): - fp = io.BytesIO(data) - length = read_fmt("Q", fp)[0] - return fp.read(length) - - -@register(TaggedBlock.VECTOR_MASK_SETTING1) -@register(TaggedBlock.VECTOR_MASK_SETTING2) -def _decode_vector_mask_setting1(data, **kwargs): - fp = io.BytesIO(data) - ver, flags = read_fmt("II", fp) - - # This decoder needs to be updated if we have new formats. - if ver != 3: - warnings.warn("Ignoring vector mask setting1 tagged block due to " - "unsupported version %s" % (ver)) - return data - - path = decode_path_resource(fp.read()) - return VectorMaskSetting( - ver, (0x01 & flags) > 0, (0x02 & flags) > 0, (0x04 & flags) > 0, path) - - -@register(TaggedBlock.ARTBOARD_DATA1) -@register(TaggedBlock.ARTBOARD_DATA2) -@register(TaggedBlock.ARTBOARD_DATA3) -def _decode_artboard_data(data, **kwargs): - return _decode_descriptor_block(data, ArtboardData) - - -@register(TaggedBlock.PLACED_LAYER_OBSOLETE1) -@register(TaggedBlock.PLACED_LAYER_OBSOLETE2) -def _decode_placed_layer(data, **kwargs): - fp = io.BytesIO(data) - type_, version = read_fmt("2I", fp) - if version != 3: - warnings.warn("Unsupported placed layer version %s" % (version)) - return data - uuid = read_pascal_string(fp, "ascii") - page, total_pages, anti_alias, layer_type = read_fmt("4I", fp) - transformation = read_fmt("8d", fp) - warp_version, warp_desc_version = read_fmt("2I", fp) - descriptor = decode_descriptor(None, fp) - warp = WarpInformation(warp_version, warp_desc_version, descriptor) - return PlacedLayerObsolete(type_, version, uuid, page, total_pages, - anti_alias, layer_type, transformation, warp) - - -@register(TaggedBlock.LINKED_LAYER1) -@register(TaggedBlock.LINKED_LAYER2) -@register(TaggedBlock.LINKED_LAYER3) -@register(TaggedBlock.LINKED_LAYER_EXTERNAL) -def _decode_linked_layer(data, **kwargs): - from psd_tools.decoder.linked_layer import decode - return decode(data) - - -@register(TaggedBlock.CHANNEL_BLENDING_RESTRICTIONS_SETTING) -def _decode_channel_blending_restrictions_setting(data, **kwargs): - # Data contains color channels to restrict. - restrictions = [False, False, False] - fp = io.BytesIO(data) - while fp.tell() < len(data): - channel = read_fmt("I", fp)[0] - restrictions[channel] = True - return restrictions - - -@register(TaggedBlock.USER_MASK) -def _decode_user_mask(data, **kwargs): - fp = io.BytesIO(data) - color = decode_color(fp) - opacity, flag = read_fmt("H B", fp) - return UserMask(color, opacity, flag) - - -@register(TaggedBlock.FILTER_EFFECTS1) -@register(TaggedBlock.FILTER_EFFECTS2) -@register(TaggedBlock.FILTER_EFFECTS3) -def _decode_filter_effects(data, **kwargs): - fp = io.BytesIO(data) - version, length = read_fmt("I Q", fp) - if version not in (1, 2, 3): - warnings.warn("Unknown filter effects version %d" % version) - return data - - return _decode_filter_effect_item(fp.read(length)) - - -def _decode_filter_effect_item(data): - fp = io.BytesIO(data) - uuid = read_pascal_string(fp, "ascii") - version, length = read_fmt("I Q", fp) - assert version == 1, "Unknown filter effect version %d" % version - - rectangle = read_fmt("4i", fp) - depth, max_channels = read_fmt("2I", fp) - - channels = [] - for i in range(max_channels + 2): - is_written = read_fmt("I", fp)[0] - assert is_written in (0, 1) - if is_written: - channel_len, compression = read_fmt("Q H", fp) - channel_data = fp.read(max(0, channel_len - 2)) - channels.append(FilterEffectChannel(is_written, compression, - RawData(channel_data))) - else: - channels.append(FilterEffectChannel(is_written, 0, None)) - - # There seems to be undocumented extra fields. - extra_data = None - if len(data) > fp.tell() and read_fmt("B", fp)[0]: - extra_rect = read_fmt("4i", fp) - extra_length, extra_compression = read_fmt("Q H", fp) - extra_data = (extra_rect, extra_compression, - RawData(fp.read(extra_length))) - - return FilterEffects(uuid, version, rectangle, depth, max_channels, - channels, extra_data) - - -@register(TaggedBlock.COMPUTER_INFO) -def _decode_extn(data, **kwargs): - return _decode_descriptor_block(data, ComputerInfo) diff --git a/src/psd_tools/exceptions.py b/src/psd_tools/exceptions.py deleted file mode 100644 index b7851a46..00000000 --- a/src/psd_tools/exceptions.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - - -class Error(Exception): - pass diff --git a/src/psd_tools/icc_profiles/Gray-CIE_L.icc b/src/psd_tools/icc_profiles/Gray-CIE_L.icc deleted file mode 100644 index 9de70c8f845f7a4c05157db4cf9db7b9fd485f0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 424 zcmZQzU|iu}S(M0Rz`&53S5o92bn5lylt7ZjBMRUZIi*Ob)aWCjLC4j?E$KnrrLq@L+k~y7`WYw5-Sy)JzceY ez2IVn=f))U6A4WF- diff --git a/src/psd_tools/icc_profiles/Gray.icc b/src/psd_tools/icc_profiles/Gray.icc deleted file mode 100644 index 3050e5f012243e40639e2ed2873d77fc134d2e6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 420 zcmZQzU|ix~S(M0Rz`&53S5o92fxh=H9!mq9Esxwt?pFSEj2WX<-x z{Jc~UKElAj@a6ux?LZ0$zFlKr1ksmu^MPi8SR??}}jFFuMW7&JGE328vGsviV9DOn|}_ WVk9EG5Ml_+l1q!qpgcxUumS*PS3@uW diff --git a/src/psd_tools/icc_profiles/__init__.py b/src/psd_tools/icc_profiles/__init__.py deleted file mode 100644 index 16df69fa..00000000 --- a/src/psd_tools/icc_profiles/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import os - -GRAY_PATH = os.path.join(os.path.dirname(__file__), 'Gray-CIE_L.icc') - -try: - from PIL import ImageCms - gray = ImageCms.ImageCmsProfile(GRAY_PATH) - sRGB = ImageCms.createProfile('sRGB') -except ImportError: - gray = None - sRGB = None diff --git a/src/psd_tools/reader/__init__.py b/src/psd_tools/reader/__init__.py deleted file mode 100644 index dda90b22..00000000 --- a/src/psd_tools/reader/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from .reader import parse diff --git a/src/psd_tools/reader/color_mode_data.py b/src/psd_tools/reader/color_mode_data.py deleted file mode 100644 index 2840409c..00000000 --- a/src/psd_tools/reader/color_mode_data.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, division -import logging -from psd_tools.utils import read_fmt, write_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 - - -def write(fp, data): - """ - Writes data to 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. - """ - write_fmt(fp, "I", len(data)) - fp.write(data) - return data diff --git a/src/psd_tools/reader/header.py b/src/psd_tools/reader/header.py deleted file mode 100644 index 245fc3c8..00000000 --- a/src/psd_tools/reader/header.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals -import logging -import collections -import warnings - -from psd_tools.exceptions import Error -from psd_tools.utils import read_fmt, write_fmt -from psd_tools.constants import ColorMode - -logger = logging.getLogger(__name__) - - -class PsdHeader(collections.namedtuple( - "PsdHeader", - "version, number_of_channels, height, width, depth, color_mode" -)): - """ - Header section of the PSD file. - - Example:: - - PsdHeader(version=1, number_of_channels=2, height=359, width=400, \ -depth=8, color_mode=GRAYSCALE) - - .. py:attribute:: version - .. py:attribute:: number_of_channels - .. py:attribute:: height - .. py:attribute:: width - .. py:attribute:: depth - .. py:attribute:: color_mode - - :py:class:`~psd_tools.constants.ColorMode` - """ - @staticmethod - def read(fp): - return read(fp) - - def write(self, fp): - write(fp, self) - - def __repr__(self): - return ( - "PsdHeader(version=%s, number_of_channels=%s, height=%s, " - "width=%s, depth=%s, color_mode=%s)" % ( - self.version, self.number_of_channels, self.height, - self.width, self.depth, ColorMode.name_of(self.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 or PSB file") - - version = read_fmt("H", fp)[0] - if version not in (1, 2): - raise Error("Unsupported PSD version (%s)" % version) - - header = PsdHeader(version, *read_fmt("6x HIIHH", fp)) - - if not ColorMode.is_known(header.color_mode): - warnings.warn("Unknown color mode: %s" % header.color_mode) - - logger.debug(header) - return header - - -def write(fp, header): - """ - Write PSD file header. - """ - fp.write(b'8BPS') - write_fmt(fp, 'H', header.version) - write_fmt(fp, '6x HIIHH', *header[1:]) diff --git a/src/psd_tools/reader/image_resources.py b/src/psd_tools/reader/image_resources.py deleted file mode 100644 index 4a3b49fa..00000000 --- a/src/psd_tools/reader/image_resources.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- 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 -from psd_tools.constants import ImageResourceID - -logger = logging.getLogger(__name__) - - -class ImageResource(collections.namedtuple( - "ImageResource", "resource_id, name, data" -)): - """ - Image resource. - - .. py:attribute:: resource_id - - :py:class:`~psd_tools.constants.ImageResourceID` - - .. py:attribute:: name - .. py:attribute:: data - """ - def __repr__(self): - return "ImageResource(%r %s, %r, %s)" % ( - self.resource_id, ImageResourceID.name_of(self.resource_id), - self.name, trimmed_repr(self.data) - ) - - -def read(fp, encoding): - """ Reads image resources. """ - logger.debug("reading image resources..") - - resource_section_length = read_fmt("I", fp)[0] - position = fp.tell() - end_position = position + resource_section_length - - blocks = [] - while fp.tell() < end_position: - 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 not in {b'8BIM', b'MeSa', b'AgHg', b'PHUT', b'DCSR'}: - raise Error("Invalid resource signature (%r)" % sig) - - resource_id = read_fmt("H", fp)[0] - name = read_pascal_string(fp, encoding, 2) - - data_size = pad(read_fmt("I", fp)[0], 2) - data = fp.read(data_size) - - return ImageResource(resource_id, name, data) diff --git a/src/psd_tools/reader/layers.py b/src/psd_tools/reader/layers.py deleted file mode 100644 index c2a99ba1..00000000 --- a/src/psd_tools/reader/layers.py +++ /dev/null @@ -1,670 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import ( - absolute_import, unicode_literals, division, print_function - ) -import logging -import warnings -import zlib - -from psd_tools.utils import (read_fmt, read_pascal_string, - read_be_array, trimmed_repr, pad, synchronize) -from psd_tools.exceptions import Error -from psd_tools.constants import (Compression, Clipping, BlendMode, - ChannelID, TaggedBlock) -from psd_tools import compression -from psd_tools.debug import pretty_namedtuple - -logger = logging.getLogger(__name__) - - -class LayerAndMaskData(pretty_namedtuple( - 'LayerAndMaskData', - 'layers global_mask_info tagged_blocks' -)): - """ - Layer and mask data container. - - .. py:attribute:: layers - - :py:attr:`~psd_tools.reader.layers.Layers` - - .. py:attribute:: global_mask_info - - :py:attr:`~psd_tools.reader.layers.GlobalMaskInfo` - - .. py:attribute:: tagged_blocks - - :py:class:`list` of :py:attr:`~psd_tools.reader.layers.Block` - """ - - -class Layers(pretty_namedtuple( - 'Layers', - 'layer_count layer_records channel_image_data' -)): - """ - Layer container. - - .. py:attribute:: layer_count - .. py:attribute:: layer_records - - :py:class:`list` of :py:class:`~psd_tools.reader.layers.LayerRecord` - - .. py:attribute:: channel_image_data - - :py:class:`list` of :py:class:`~psd_tools.reader.layers.ChannelInfo` - """ - - -class LayerRecord(pretty_namedtuple( - 'LayerRecord', - 'top left bottom right num_channels channels blend_mode opacity clipping ' - 'flags mask_data blending_ranges name tagged_blocks' -)): - """ - Layer container. - - .. py:attribute:: top - .. py:attribute:: left - .. py:attribute:: bottom - .. py:attribute:: right - .. py:attribute:: num_channels - .. py:attribute:: channels - .. py:attribute:: blend_mode - .. py:attribute:: opacity - .. py:attribute:: clipping - .. py:attribute:: flags - .. py:attribute:: mask_data - .. py:attribute:: blending_ranges - .. py:attribute:: name - .. py:attribute:: tagged_blocks - - """ - - def width(self): - return self.right - self.left - - def height(self): - return self.bottom - self.top - - -class ChannelInfo(pretty_namedtuple('ChannelInfo', 'id length')): - """ - Channel information. - - .. py:attribute:: id - - :py:class:`~psd_tools.constants.ChannelID` - - .. py:attribute:: length - """ - def __repr__(self): - return "ChannelInfo(id=%s %s, length=%s)" % ( - self.id, ChannelID.name_of(self.id), self.length - ) - - -class LayerFlags(pretty_namedtuple( - 'LayerFlags', - 'transparency_protected visible pixel_data_irrelevant' -)): - """ - Layer flags. - - .. py:attribute:: transparency_protected - .. py:attribute:: visible - .. py:attribute:: pixel_data_irrelevant - """ - - -class MaskData(pretty_namedtuple( - 'MaskData', - 'top left bottom right background_color flags parameters real_flags ' - 'real_background_color real_top real_left real_bottom real_right' -)): - """ - Mask data. - - .. py:attribute:: top - .. py:attribute:: left - .. py:attribute:: bottom - .. py:attribute:: right - .. py:attribute:: background_color - .. py:attribute:: flags - .. py:attribute:: parameters - .. py:attribute:: real_flags - .. py:attribute:: real_background_color - .. py:attribute:: real_top - .. py:attribute:: real_left - .. py:attribute:: real_bottom - .. py:attribute:: real_right - """ - - def width(self): - return self.right - self.left - - def height(self): - return self.bottom - self.top - - def real_width(self): - return self.real_right - self.real_left - - def real_height(self): - return self.real_bottom - self.real_top - - -class MaskFlags(pretty_namedtuple( - 'MaskFlags', - 'pos_relative_to_layer mask_disabled invert_mask user_mask_from_render ' - 'parameters_applied' -)): - """ - Mask flags. - - .. py:attribute:: pos_relative_to_layer - .. py:attribute:: mask_disabled - .. py:attribute:: invert_mask - .. py:attribute:: user_mask_from_render - .. py:attribute:: parameters_applied - """ - - -class MaskParameters(pretty_namedtuple( - 'MaskParameters', - 'user_mask_density user_mask_feather vector_mask_density ' - 'vector_mask_feather' -)): - """ - Mask parameters. - - .. py:attribute:: user_mask_density - .. py:attribute:: user_mask_feather - .. py:attribute:: vector_mask_density - """ - - -class LayerBlendingRanges(pretty_namedtuple( - 'LayerBlendingRanges', - 'composite_ranges channel_ranges' -)): - """ - Layer blending ranges. - - .. py:attribute:: composite_ranges - .. py:attribute:: channel_ranges - """ - - -class Block(pretty_namedtuple('Block', 'key data')): - """ - Layer tagged block with extra info. - - .. py:attribute:: key - .. py:attribute:: data - """ - def __repr__(self): - return "Block(%s %s, %s)" % (self.key, TaggedBlock.name_of(self.key), - trimmed_repr(self.data)) - - def _repr_pretty_(self, p, cycle): - if cycle: - p.text('Block(...)') - else: - with p.group(1, 'Block(', ')'): - p.breakable() - p.text("%s %s," % (self.key, TaggedBlock.name_of(self.key))) - p.breakable() - if isinstance(self.data, bytes): - p.text(trimmed_repr(self.data)) - else: - p.pretty(self.data) - - -class ChannelData(pretty_namedtuple('ChannelData', 'compression data')): - """ - Channel data. - - .. py:attribute:: compression - - :py:class:`~psd_tools.constants.Compression` - - .. py:attribute:: data - """ - def __repr__(self): - return "ChannelData(compression=%r %s, len(data)=%r)" % ( - self.compression, Compression.name_of(self.compression), - len(self.data) if self.data is not None else None - ) - - def _repr_pretty_(self, p, cycle): - if cycle: - p.text('ChannelData(...)') - else: - p.text(repr(self)) - - -class GlobalMaskInfo(pretty_namedtuple( - 'GlobalMaskInfo', - 'overlay_color opacity kind' -)): - """ - Global mask information. - - .. py:attribute:: overlay_color - .. py:attribute:: opacity - .. py:attribute:: kind - """ - - -def read(fp, encoding, depth, version): - """ - Reads layers and masks information. - """ - if version == 1: - length = read_fmt("I", fp)[0] - elif version == 2: - length = read_fmt("Q", fp)[0] - start_pos = fp.tell() - - logger.debug('reading layers and masks information...') - logger.debug(' length=%d, start_pos=%d', length, start_pos) - - global_mask_info = None - tagged_blocks = [] - - if length > 0: - layers = _read_layers(fp, encoding, depth, version) - - remaining_length = length - (fp.tell() - start_pos) - if remaining_length > 0: - global_mask_info = _read_global_mask_info(fp) - - synchronize(fp) # hack hack hack - remaining_length = length - (fp.tell() - start_pos) - - logger.debug('reading tagged blocks...') - logger.debug(' length=%d, start_pos=%d', - remaining_length, fp.tell()) - - tagged_blocks = _read_layer_tagged_blocks( - fp, remaining_length, version, 4) - - remaining_length = length - (fp.tell() - start_pos) - if remaining_length > 0: - fp.seek(remaining_length, 1) - logger.debug('skipping %s bytes', remaining_length) - else: - layers = _read_layers(fp, encoding, depth, version, 0) - - return LayerAndMaskData(layers, global_mask_info, tagged_blocks) - - -def _read_layers(fp, encoding, depth, version, length=None): - """ - Reads info about layers. - """ - logger.debug('reading layers...') - - layer_records = [] - channel_image_data = [] - if length is None: - if version == 1: - length = read_fmt("I", fp)[0] - elif version == 2: - length = read_fmt("Q", fp)[0] - - if length > 0: - start_pos = fp.tell() - layer_count = read_fmt("h", fp)[0] - - logger.debug(' length=%d, layer_count=%d', length, layer_count) - - for idx in range(abs(layer_count)): - logger.debug('reading layer record %d, pos=%d', idx, fp.tell()) - layer = _read_layer_record(fp, encoding, version) - layer_records.append(layer) - - for idx, layer in enumerate(layer_records): - logger.debug('reading layer channel data %d, pos=%d', - idx, fp.tell()) - data = _read_channel_image_data(fp, layer, depth, version) - channel_image_data.append(data) - - remaining_length = length - (fp.tell() - start_pos) - if remaining_length > 0: - fp.seek(remaining_length, 1) - logger.debug('skipping %s bytes', remaining_length) - else: - layer_count = 0 - logger.debug(' length=0, layer_count=0') - - return Layers(layer_count, layer_records, channel_image_data) - - -def _read_layer_record(fp, encoding, version): - """ - Reads single layer record. - """ - top, left, bottom, right, num_channels = read_fmt("4i H", fp) - logger.debug(' top=%d, left=%d, bottom=%d, right=%d, num_channels=%d', - top, left, bottom, right, num_channels) - - channel_info = [] - for channel_num in range(num_channels): - if version == 1: - info = ChannelInfo(*read_fmt("hI", fp)) - elif version == 2: - info = ChannelInfo(*read_fmt("hQ", 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) - if not BlendMode.is_known(blend_mode): - warnings.warn("Unknown blend mode (%s)" % blend_mode) - - opacity, clipping, flags, extra_length = read_fmt("BBBxI", fp) - - if not Clipping.is_known(clipping): - warnings.warn("Unknown clipping (%s)" % clipping) - logger.debug(' extra_length=%s', extra_length) - - flags = LayerFlags( - bool(flags & 1), not bool(flags & 2), # why "not"? - bool(flags & 16) if bool(flags & 8) else None - ) - - start_pos = fp.tell() - mask_data = _read_layer_mask_data(fp) - blending_ranges = _read_layer_blending_ranges(fp) - - name = read_pascal_string(fp, encoding, 4) - - remaining_length = extra_length - (fp.tell() - start_pos) - - logger.debug(' reading layer tagged blocks...') - logger.debug(' length=%d, start_pos=%d', remaining_length, fp.tell()) - - tagged_blocks = _read_layer_tagged_blocks(fp, remaining_length, version) - - remaining_length = extra_length - (fp.tell() - start_pos) - if remaining_length > 0: - fp.seek(remaining_length, 1) # skip the remainder - logger.debug(' skipping %s bytes', remaining_length) - - 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_mask_data(fp): - """ Reads layer mask or adjustment layer data. """ - length = read_fmt("I", fp)[0] - start_pos = fp.tell() - - logger.debug(' reading layer mask data...') - logger.debug(' length=%d, start_pos=%d', length, start_pos) - - if not length: - return None - - top, left, bottom, right, background_color, flags = read_fmt("4i 2B", fp) - flags = MaskFlags( - bool(flags & 1), bool(flags & 2), bool(flags & 4), - bool(flags & 8), bool(flags & 16) - ) - - # Order is based on tests. The specification is messed up here... - - if length < 36: - real_flags, real_background_color = None, None - real_top, real_left, real_bottom, real_right = None, None, None, None - else: - real_flags, real_background_color = read_fmt("2B", fp) - real_flags = MaskFlags( - bool(real_flags & 1), bool(real_flags & 2), bool(real_flags & 4), - bool(real_flags & 8), bool(real_flags & 16) - ) - - real_top, real_left, real_bottom, real_right = read_fmt("4i", fp) - - if flags.parameters_applied: - parameters = read_fmt("B", fp)[0] - parameters = MaskParameters( - read_fmt("B", fp)[0] if bool(parameters & 1) else None, - read_fmt("d", fp)[0] if bool(parameters & 2) else None, - read_fmt("B", fp)[0] if bool(parameters & 4) else None, - read_fmt("d", fp)[0] if bool(parameters & 8) else None - ) - else: - parameters = None - - padding_size = length - (fp.tell() - start_pos) - if padding_size > 0: - fp.seek(padding_size, 1) - - return MaskData( - top, left, bottom, right, background_color, flags, parameters, - real_flags, real_background_color, real_top, real_left, real_bottom, - real_right - ) - - -def _read_layer_blending_ranges(fp): - """ Reads layer blending data. """ - - def read_channel_range(): - src_start, src_end, dest_start, dest_end = read_fmt("4H", fp) - return (src_start, src_end), (dest_start, dest_end) - - composite_ranges = None - channel_ranges = [] - length = read_fmt("I", fp)[0] - - logger.debug(' reading layer blending ranges...') - logger.debug(' length=%d, start_pos=%d', length, fp.tell()) - - if length: - composite_ranges = read_channel_range() - for x in range(length//8 - 1): - channel_ranges.append(read_channel_range()) - - return LayerBlendingRanges(composite_ranges, channel_ranges) - - -def _read_layer_tagged_blocks(fp, remaining_length, version, padding=0): - """ - 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, padding, version) - read_bytes = fp.tell() - start_pos - if block is None: - break - blocks.append(block) - - return blocks - - -def _read_additional_layer_info_block(fp, padding, version): - """ - Reads a tagged block with additional layer information. - """ - sig = fp.read(4) - if sig not in [b'8BIM', b'8B64']: - fp.seek(-4, 1) - # warnings.warn("not a block: %r" % sig) - return - - key = fp.read(4) - if version == 2 and key in ( - b'LMsk', b'Lr16', b'Lr32', b'Layr', b'Mt16', b'Mt32', b'Mtrn', - b'Alph', b'FMsk', b'lnk2', b'FEid', b'FXid', b'PxSD' - ): - length = read_fmt("Q", fp)[0] - else: - length = read_fmt("I", fp)[0] - if padding > 0: - length = pad(length, padding) - - data = fp.read(length) - return Block(key, data) - - -def _read_channel_image_data(fp, layer, depth, version): - """ - Reads image data for all channels in a layer. - """ - channel_data = [] - - bytes_per_pixel = depth // 8 - - for idx, channel in enumerate(layer.channels): - logger.debug(" reading %s", channel) - if channel.id == ChannelID.USER_LAYER_MASK: - w, h = layer.mask_data.width(), layer.mask_data.height() - elif channel.id == ChannelID.REAL_USER_LAYER_MASK: - w, h = layer.mask_data.real_width(), layer.mask_data.real_height() - else: - w, h = layer.width(), layer.height() - - start_pos = fp.tell() - compress_type = read_fmt("H", fp)[0] - - logger.debug(" start_pos=%s, compress_type=%s", - start_pos, Compression.name_of(compress_type)) - - data = None - - # read data size - if compress_type == Compression.RAW: - data_size = w * h * bytes_per_pixel - logger.debug(' data size = %sx%sx%s=%s bytes', - w, h, bytes_per_pixel, data_size) - - elif compress_type == Compression.PACK_BITS: - if version == 1: - byte_counts = read_be_array("H", h, fp) - elif version == 2: - byte_counts = read_be_array("I", h, fp) - data_size = sum(byte_counts) - logger.debug(' data size = %s bytes', data_size) - - elif compress_type in (Compression.ZIP, - Compression.ZIP_WITH_PREDICTION): - data_size = channel.length - 2 - logger.debug(' data size = %s-2=%s bytes', - channel.length, data_size) - - else: - warnings.warn("Bad compression type %s" % compress_type) - return [] - - # read the data itself - if data_size > channel.length: - warnings.warn("Incorrect data size: %s > %s" % ( - data_size, channel.length)) - else: - raw_data = fp.read(data_size) - if compress_type in (Compression.RAW, Compression.PACK_BITS): - data = raw_data - elif compress_type == Compression.ZIP: - data = zlib.decompress(raw_data) - elif compress_type == Compression.ZIP_WITH_PREDICTION: - decompressed = zlib.decompress(raw_data) - data = compression.decode_prediction( - decompressed, w, h, bytes_per_pixel) - - if data is None: - return [] - - channel_data.append(ChannelData(compress_type, data)) - - remaining_length = channel.length - (fp.tell() - start_pos) - if remaining_length > 0: - fp.seek(remaining_length, 1) - logger.debug(' skipping %s bytes', remaining_length) - - return channel_data - - -def _read_global_mask_info(fp): - """ - Reads global layer mask info. - """ - # XXX: What is it for? - length = read_fmt("I", fp)[0] - start_pos = fp.tell() - - logger.debug('reading global mask info...') - logger.debug(' length=%d, start_pos=%d', length, start_pos) - - if not length: - return None - - overlay_color = fp.read(10) - opacity, kind = read_fmt("HB", fp) - - filler_length = length - (fp.tell() - start_pos) - if filler_length > 0: - fp.seek(filler_length, 1) - - return GlobalMaskInfo(overlay_color, 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.width, header.height - compress_type = read_fmt("H", fp)[0] - - bytes_per_pixel = header.depth // 8 - - channel_byte_counts = [] - if compress_type == Compression.PACK_BITS: - for ch in range(header.number_of_channels): - if header.version == 1: - channel_byte_counts.append(read_be_array("H", h, fp)) - elif header.version == 2: - # Undocumented... - channel_byte_counts.append(read_be_array("I", h, fp)) - - channel_data = [] - for channel_id in range(header.number_of_channels): - - data = None - - if compress_type == Compression.RAW: - data_size = w * h * bytes_per_pixel - data = fp.read(data_size) - - elif compress_type == Compression.PACK_BITS: - byte_counts = channel_byte_counts[channel_id] - data_size = sum(byte_counts) - data = fp.read(data_size) - - # are there any ZIP-encoded composite images in a wild? - elif compress_type == Compression.ZIP: - warnings.warn( - "ZIP compression of composite image is not supported.") - - 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(compress_type, data)) - - return channel_data diff --git a/src/psd_tools/reader/reader.py b/src/psd_tools/reader/reader.py deleted file mode 100644 index 5bf6c073..00000000 --- a/src/psd_tools/reader/reader.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- 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 -from psd_tools.debug import pretty_namedtuple - -logger = logging.getLogger(__name__) - - -class ParseResult(pretty_namedtuple( - 'ParseResult', - 'header, color_data, image_resource_blocks, layer_and_mask_data, ' - 'image_data' -)): - """ - Result of :py:func:`~psd_tools.reader.parse`. The result consists of the - following fields in a PSD file. - - .. py:attribute:: header - - :py:class:`~psd_tools.reader.header.PsdHeader` - - .. py:attribute:: color_data - - :py:class:`bytes` - - .. py:attribute:: image_resource_blocks - - :py:class:`list` of - :py:class:`~psd_tools.reader.image_resources.ImageResource` - - .. py:attribute:: layer_and_mask_data - - :py:class:`~psd_tools.reader.layers.LayerAndMaskData` - - .. py:attribute:: image_data - - :py:class:`list` of :py:class:`~psd_tools.reader.layers.ChannelData` - """ - - -def parse(fp, encoding='utf8'): - """ - Read PSD file from file-like object. - - :rtype: ParseResult - - Example:: - - from psd_tools.reader.reader import parse - - with open('/path/to/input.psd', 'rb') as fp: - result = parse(fp) - """ - - header = psd_tools.reader.header.read(fp) - return ParseResult( - header, - psd_tools.reader.color_mode_data.read(fp), - psd_tools.reader.image_resources.read(fp, encoding), - psd_tools.reader.layers.read(fp, encoding, header.depth, - header.version), - psd_tools.reader.layers.read_image_data(fp, header) - ) diff --git a/src/psd_tools/user_api/__init__.py b/src/psd_tools/user_api/__init__.py deleted file mode 100644 index e769dffa..00000000 --- a/src/psd_tools/user_api/__init__.py +++ /dev/null @@ -1,100 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, print_function - -import logging -from collections import namedtuple - -logger = logging.getLogger(__name__) - - -class BBox(namedtuple('BBox', 'x1, y1, x2, y2')): - """ - Bounding box tuple representing (x1, y1, x2, y2). - """ - @property - def width(self): - """Width of the bounding box.""" - return self.x2 - self.x1 - - @property - def height(self): - """Height of the bounding box.""" - return self.y2 - self.y1 - - @property - def left(self): - """Alias of x1.""" - return self.x1 - - @property - def right(self): - """Alias of x2.""" - return self.x2 - - @property - def top(self): - """Alias of y1.""" - return self.y1 - - @property - def bottom(self): - """Alias of y2.""" - return self.y2 - - def is_empty(self): - """Return True if the box does not have an area.""" - return self.width <= 0 or self.height <= 0 - - def intersect(self, bbox): - """Intersect of two bounding boxes.""" - return BBox(max(self.x1, bbox.x1), - max(self.y1, bbox.y1), - min(self.x2, bbox.x2), - min(self.y2, bbox.y2)) - - def union(self, bbox): - """Union of two boxes.""" - return BBox(min(self.x1, bbox.x1), - min(self.y1, bbox.y1), - max(self.x2, bbox.x2), - max(self.y2, bbox.y2)) - - def offset(self, point): - """Subtract offset point from the bounding box.""" - return BBox(self.x1 - point[0], self.y1 - point[1], - self.x2 - point[0], self.y2 - point[1]) - - -class Pattern(object): - """Pattern data.""" - def __init__(self, pattern): - self._pattern = pattern - - @property - def pattern_id(self): - """Pattern UUID.""" - return self._pattern.pattern_id - - @property - def name(self): - """Name of the pattern.""" - return self._pattern.name - - @property - def width(self): - """Width of the pattern.""" - return self._pattern.point[1] - - @property - def height(self): - """Height of the pattern.""" - return self._pattern.point[0] - - def as_PIL(self): - """Returns a PIL image for this pattern.""" - return pil_support.pattern_to_PIL(self._pattern) - - def __repr__(self): - return "<%s: name='%s' size=%dx%d>" % ( - self.__class__.__name__.lower(), self.name, self.width, - self.height) diff --git a/src/psd_tools/user_api/actions.py b/src/psd_tools/user_api/actions.py deleted file mode 100644 index 47b6fb77..00000000 --- a/src/psd_tools/user_api/actions.py +++ /dev/null @@ -1,391 +0,0 @@ -# -*- coding: utf-8 -*- -""" -A module for translating "Actions" format to User API objects. -""" -from __future__ import absolute_import - -from collections import OrderedDict -from psd_tools.debug import pretty_namedtuple -from psd_tools.decoder.decoders import new_registry -from psd_tools.decoder.actions import ( - Descriptor, Reference, Property, UnitFloat, Double, Class, String, - EnumReference, Boolean, Offset, Alias, List, Integer, Enum, Identifier, - Index, Name, ObjectArray, ObjectArrayItem, RawData) -from psd_tools.decoder.tagged_blocks import ( - SolidColorSetting, PatternFillSetting, GradientFillSetting, - VectorStrokeSetting, VectorMaskSetting, VectorStrokeContentSetting, - ContentGeneratorExtraData, LevelsSettings, CurvesSettings, Exposure, - Vibrance, HueSaturation, ColorBalance, BlackWhite, PhotoFilter, - ChannelMixer, ColorLookup, Invert, Posterize, Threshold, SelectiveColor, - GradientSettings, VectorOriginationData) -from psd_tools.decoder.layer_effects import ObjectBasedEffects -from psd_tools.user_api.effects import ( - GradientOverlay, PatternOverlay, ColorOverlay) -from psd_tools.user_api import adjustments, BBox - -from psd_tools.user_api.shape import StrokeStyle, VectorMask, Origination - - -_translators, register = new_registry() -_desc_translators, desc_register = new_registry() - -#: Point object, x and y attributes. -Point = pretty_namedtuple('Point', 'x y') - -#: Shape object, contains list of points in curve. -Shape = pretty_namedtuple('Shape', 'name curve') - -#: Pattern object. -Pattern = pretty_namedtuple('Pattern', 'name id') - -_Gradient = pretty_namedtuple( - 'Gradient', 'desc_name name type smoothness colors transform') - -#: StopColor in gradient. -StopColor = pretty_namedtuple('StopColor', 'color type location midpoint') - -#: StopOpacity in gradient. -StopOpacity = pretty_namedtuple('StopOpacity', 'opacity location midpoint') - - -class Color(object): - """Color picker point representing a single color. - - Example:: - - color.name # => rgb - color.value # => (1.0, 1.0, 1.0) - - .. todo:: Add colorspace conversion support. Perhaps add ``rgb()`` method. - """ - def __init__(self, name, value): - self.name = name - self.value = value - - def __repr__(self): - return "%s%s" % (self.name, self.value) - - -class Gradient(_Gradient): - """Gradient object.""" - @property - def short_name(self): - """Short gradient name.""" - return self._name.split("=")[-1] - - -class NoiseGradient(pretty_namedtuple( - 'NoiseGradient', - 'desc_name name type add_transparency restrict_colors colors ' - 'random_seed smooth minimum maximum')): - """NoiseGradient object.""" - @property - def short_name(self): - """Short gradient name.""" - return self._name.split("=")[-1] - - -def translate(data): - """Translate descriptor-based formats.""" - translator = _translators.get(type(data), lambda data: data) - return translator(data) - - -@register(Descriptor) -def _translate_descriptor(data): - translator = _desc_translators.get(data.classID, - _translate_generic_descriptor) - return translator(data) - - -@register(Reference) -@register(List) -def _translate_list(data): - result = [] - for item in data.items: - translator = _translators.get(type(item), lambda data: data) - result.append(translator(item)) - return result - - -@register(Property) -def _translate_property(data): - return data - - -# @register(UnitFloat) -@register(Double) -@register(String) -@register(Boolean) -@register(Alias) -@register(Integer) -@register(Enum) -@register(Identifier) -@register(Index) -@register(Name) -@register(RawData) -def _translate_value(data): - return data.value - - -@register(ObjectBasedEffects) -def _translate_object_based_effects(data): - return translate(data.descriptor) - - -@register(VectorStrokeSetting) -def _translate_vector_stroke_setting(data): - return translate(data.data) - - -@register(VectorMaskSetting) -def _translate_vector_mask_setting(data): - return VectorMask(data) - - -@register(VectorStrokeContentSetting) -def _translate_vector_stroke_content_setting(data): - descriptor = translate(data.data) - if b'Ptrn' in descriptor: - return PatternOverlay(descriptor, None) - elif b'Grdn' in descriptor or b'Grad' in descriptor: - return GradientOverlay(descriptor, None) - else: - return ColorOverlay(descriptor, None) - - -@register(VectorOriginationData) -def _translate_vector_origination_data(data): - return Origination(translate(data.data).get(b'keyDescriptorList')[0]) - - -@register(SolidColorSetting) -def _translate_solid_color_setting(data): - descriptor = translate(data.data) - return ColorOverlay(descriptor, None) - - -@register(PatternFillSetting) -def _translate_pattern_fill_setting(data): - descriptor = translate(data.data) - return PatternOverlay(descriptor, None) - - -@register(GradientFillSetting) -def _translate_gradient_fill_setting(data): - descriptor = translate(data.data) - return GradientOverlay(descriptor, None) - - -@register(ContentGeneratorExtraData) -def _translate_content_generator_extra_data(data): - descriptor = _translate_generic_descriptor(data.descriptor) - return adjustments.BrightnessContrast(descriptor) - - -@register(LevelsSettings) -def _translate_levels_settings(data): - return adjustments.Levels(data) - - -@register(CurvesSettings) -def _translate_curves_settings(data): - return adjustments.Curves(data) - - -@register(Exposure) -def _translate_levels_settings(data): - return adjustments.Exposure(data) - - -@register(Vibrance) -def _translate_vibrance(data): - descriptor = _translate_generic_descriptor(data.descriptor) - return adjustments.Vibrance(descriptor) - - -@register(HueSaturation) -def _translate_hue_saturation(data): - return adjustments.HueSaturation(data) - - -@register(ColorBalance) -def _translate_color_balance(data): - return adjustments.ColorBalance(data) - - -@register(BlackWhite) -def _translate_black_and_white(data): - descriptor = _translate_generic_descriptor(data.descriptor) - return adjustments.BlackWhite(descriptor) - - -@register(PhotoFilter) -def _translate_photo_filter(data): - return adjustments.PhotoFilter(data) - - -@register(ChannelMixer) -def _translate_channel_mixer(data): - return adjustments.ChannelMixer(data) - - -@register(ColorLookup) -def _translate_color_lookup(data): - descriptor = _translate_generic_descriptor(data.descriptor) - return adjustments.ColorLookup(descriptor) - - -@register(Invert) -def _translate_invert(data): - return adjustments.Invert(data) - - -@register(Posterize) -def _translate_posterize(data): - return adjustments.Posterize(data) - - -@register(Threshold) -def _translate_threshold(data): - return adjustments.Threshold(data) - - -@register(SelectiveColor) -def _translate_selective_color(data): - return adjustments.SelectiveColor(data) - - -@register(GradientSettings) -def _translate_gradient_map(data): - return adjustments.GradientMap(data) - - -def _translate_generic_descriptor(data): - """ - Fallback descriptor translator. - """ - result = OrderedDict() - result[b'classID'] = data.classID - for key, value in data.items: - translator = _translators.get(type(value), lambda data: data) - result[key] = translator(value) - return result - - -@desc_register(b'Grsc') -def _translate_grsc_color(data): - colors = OrderedDict(data.items) - return Color('gray', ((1.0 - colors[b'Gry '][0] / 100.0),)) - - -@desc_register(b'RGBC') -def _translate_rgbc_color(data): - colors = OrderedDict(data.items) - return Color('rgb', (colors[b'Rd '].value, colors[b'Grn '].value, - colors[b'Bl '].value)) - - -@desc_register(b'CMYC') -def _translate_cmyc_color(data): - colors = OrderedDict(data.items) - return Color('cmyk', (colors[b'Cyn '].value, colors[b'Mgnt'].value, - colors[b'Ylw '].value, colors[b'Blck'].value)) - - -@desc_register(b'Pnt ') -@desc_register(b'CrPt') -def _translate_point(data): - items = dict(data.items) - return Point(translate(items.get(b'Hrzn')), translate(items.get(b'Vrtc'))) - - -@desc_register(b'Ptrn') -def _translate_point(data): - items = dict(data.items) - return Pattern(translate(items.get(b'Nm ')), - translate(items.get(b'Idnt'))) - - -@desc_register(b'Grdn') -def _translate_gradient(data): - items = dict(data.items) - if items.get(b'GrdF', dict(value=b'')).value == b'ClNs': - return NoiseGradient(data.name, - translate(items.get(b'Nm ')), - translate(items.get(b'GrdF')), - translate(items.get(b'ShTr')), - translate(items.get(b'VctC')), - translate(items.get(b'ClrS')), - translate(items.get(b'RndS')), - translate(items.get(b'Smth')), - translate(items.get(b'Mnm ')), - translate(items.get(b'Mxm '))) - else: - return Gradient(data.name, - translate(items.get(b'Nm ')), - translate(items.get(b'GrdF')), - translate(items.get(b'Intr')), - translate(items.get(b'Clrs')), - translate(items.get(b'Trns'))) - - -@desc_register(b'Clrt') -def _translate_stopcolor(data): - items = OrderedDict(data.items) - return StopColor(*[translate(items[key]) for key in items]) - - -@desc_register(b'TrnS') -def _translate_stopcolor(data): - items = OrderedDict(data.items) - return StopOpacity(*[translate(items[key]) for key in items]) - - -@desc_register(b'ShpC') -def _translate_shape(data): - items = dict(data.items) - return Shape(translate(items.get(b'Nm ')), translate(items.get(b'Crv '))) - - -@desc_register(b'metadata') -def _translate_metadata(data): - return _translate_generic_descriptor(data) - - -@desc_register(b'strokeStyle') -def _translate_stroke_style(data): - return StrokeStyle(_translate_generic_descriptor(data)) - - -@desc_register(b'solidColorLayer') -def _translate_solid_color_layer(data): - return ColorOverlay(_translate_generic_descriptor(data), None) - - -@desc_register(b'patternLayer') -def _translate_pattern_layer(data): - return PatternOverlay(_translate_generic_descriptor(data), None) - - -@desc_register(b'gradientLayer') -def _translate_gradient_layer(data): - return GradientOverlay(_translate_generic_descriptor(data), None) - - -@desc_register(b'radii') -def _translate_rrect_radii(data): - items = dict(data.items) - return (items.get(b'topLeft').value, - items.get(b'topRight').value, - items.get(b'bottomLeft').value, - items.get(b'bottomRight').value) - - -@desc_register(b'unitRect') -def _translate_unit_rect(data): - items = dict(data.items) - return BBox(items.get(b'Left').value, - items.get(b'Top ').value, - items.get(b'Rght').value, - items.get(b'Btom').value) diff --git a/src/psd_tools/user_api/adjustments.py b/src/psd_tools/user_api/adjustments.py deleted file mode 100644 index 528138a2..00000000 --- a/src/psd_tools/user_api/adjustments.py +++ /dev/null @@ -1,501 +0,0 @@ -# -*- coding: utf-8 -*- -"""Adjustment API. - -Adjustment classes are attached to ``data`` attribute of -:py:class:`~psd_tools.user_api.layers.AdjustmentLayer`. - - -Example:: - - if layer.kind == 'adjustment': - adjustment = layer.data -""" -from __future__ import absolute_import -import inspect -import logging -from psd_tools.constants import TaggedBlock -from psd_tools.decoder.actions import UnitFloat -import psd_tools.user_api.actions - -logger = logging.getLogger(__name__) - - -class _NameMixin(object): - """Nameable wrapper.""" - def __init__(self, data): - self._data = data - - @property - def name(self): - return self.__class__.__name__.lower() - - def __repr__(self): - return "<%s>" % (self.name,) - - -class _DescriptorMixin(_NameMixin): - """Descriptor wrapper.""" - def __init__(self, descriptor): - self._descriptor = descriptor - - def _get(self, key, default=None): - """Get attribute in the low-level structure. - - :param key: property key - :type key: bytes - :param default: default value to return - """ - return self._descriptor.get(key, default) - - -class BrightnessContrast(_DescriptorMixin): - """Brightness and contrast adjustment.""" - - @property - def brightness(self): - return self._get(b'Brgh', 0) - - @property - def contrast(self): - return self._get(b'Cntr', 0) - - @property - def mean(self): - return self._get(b'means', 0) - - @property - def lab(self): - return self._get(b'Lab ', False) - - @property - def use_legacy(self): - return self._get(b'useLegacy', False) - - @property - def vrsn(self): - return self._get(b'Vrsn', 1) - - @property - def automatic(self): - return self._get(b'auto', False) - - -class Curves(_NameMixin): - """ - Curves adjustment. - - Curves contain a list of - :py:class:`~psd_tools.decoder.tagged_blocks.CurveData`. - """ - @property - def count(self): - return self._data.count - - @property - def data(self): - """ - List of :py:class:`~psd_tools.decoder.tagged_blocks.CurveData` - - :rtype: list - """ - return self._data.data - - @property - def extra(self): - return self._data.extra - - def __repr__(self): - return "<%s: data=%s>" % (self.name, self.data) - - -class Exposure(_NameMixin): - """ - Exposure adjustment. - """ - @property - def exposure(self): - """Exposure. - - :rtype: float - """ - return self._data.exposure - - @property - def offset(self): - """Offset. - - :rtype: float - """ - return self._data.offset - - @property - def gamma(self): - """Gamma. - - :rtype: float - """ - return self._data.gamma - - def __repr__(self): - return "<%s: exposure=%g offset=%g gamma=%g>" % ( - self.name, self.exposure, self.offset, - self.gamma) - - -class Levels(_NameMixin): - """ - Levels adjustment. - - Levels contain a list of - :py:class:`~psd_tools.decoder.tagged_blocks.LevelRecord`. - """ - @property - def data(self): - """ - List of level records. The first record is the master. - - :rtype: list - """ - return self._data.data - - @property - def master(self): - """Master record. - - :rtype: psd_tools.decoder.tagged_blocks.LevelRecord - """ - return self._data.data[0] - - def __repr__(self): - return "<%s: master=%s>" % ( - self.name, self.master) - - -class Vibrance(_DescriptorMixin): - """Vibrance adjustment.""" - @property - def vibrance(self): - """Vibrance. - - :rtype: int - """ - return self._get(b'vibrance', 0) - - @property - def saturation(self): - """Saturation. - - :rtype: int - """ - return self._get(b'Strt', 0) - - def __repr__(self): - return "<%s: vibrance=%g saturation=%g>" % ( - self.name, self.vibrance, self.saturation) - - -class HueSaturation(_NameMixin): - """ - Hue/Saturation adjustment. - - HueSaturation contains a list of - :py:class:`~psd_tools.decoder.tagged_blocks.HueSaturationData`. - """ - @property - def data(self): - """ - List of Hue/Saturation records. - - :rtype: list - """ - return self._data.items - - @property - def enable_colorization(self): - """Enable colorization. - - :rtype: int - """ - return self._data.enable_colorization - - @property - def colorization(self): - """Colorization. - - :rtype: tuple - """ - return self._data.colorization - - @property - def master(self): - """Master record. - - :rtype: tuple - """ - return self._data.master - - def __repr__(self): - return "<%s: colorization=%s master=%s>" % ( - self.name, self.colorization, self.master) - - -class ColorBalance(_NameMixin): - """Color balance adjustment.""" - @property - def shadows(self): - """Shadows. - - :rtype: tuple - """ - return self._data.shadows - - @property - def midtones(self): - """Mid-tones. - - :rtype: tuple - """ - return self._data.midtones - - @property - def highlights(self): - """Highlights. - - :rtype: tuple - """ - return self._data.highlights - - @property - def preserve_luminosity(self): - return self._data.preserve_luminosity - - def __repr__(self): - return "<%s: shadows=%s midtones=%s highlights=%s>" % ( - self.name, self.shadows, self.midtones, self.highlights) - - -class BlackWhite(_DescriptorMixin): - """Black and white adjustment.""" - @property - def red(self): - return self._get(b'Rd ', 0) - - @property - def yellow(self): - return self._get(b'Yllw', 0) - - @property - def green(self): - return self._get(b'Grn ', 0) - - @property - def cyan(self): - return self._get(b'Cyn ', 0) - - @property - def blue(self): - return self._get(b'Bl ', 0) - - @property - def magenta(self): - return self._get(b'Mgnt', 0) - - @property - def use_tint(self): - return self._get(b'useTint', False) - - @property - def tint_color(self): - return self._get(b'tintColor') - - @property - def preset_kind(self): - return self._get(b'bwPresetKind', 1) - - @property - def preset_file_name(self): - return self._get(b'blackAndWhitePresetFileName', '') - - def __repr__(self): - return ( - "<%s: red=%g yellow=%g green=%g cyan=%g blue=%g magenta=%g>" % ( - self.name, self.red, self.yellow, self.green, self.cyan, - self.blue, self.magenta - ) - ) - - -class PhotoFilter(_NameMixin): - """Photo filter adjustment.""" - @property - def xyz(self): - """xyz. - - :rtype: bool - """ - return self._data.xyz - - @property - def color_space(self): - return self._data.color_space - - @property - def color_components(self): - return self._data.color_components - - @property - def density(self): - return self._data.density - - @property - def preserve_luminosity(self): - return self._data.preserve_luminosity - - def __repr__(self): - return ( - "<%s: xyz=%s color_space=%g color_components=%s density=%g>" % ( - self.name, self.xyz, self.color_space, self.color_components, - self.density - ) - ) - - -class ChannelMixer(_NameMixin): - """Channel mixer adjustment.""" - @property - def monochrome(self): - return self._data.monochrome - - @property - def mixer_settings(self): - return self._data.mixer_settings - - def __repr__(self): - return ( - "<%s: monochrome=%g, settings=%s>" % ( - self.name, self.monochrome, self.mixer_settings - ) - ) - - -class ColorLookup(_DescriptorMixin): - """Color lookup adjustment.""" - pass - - -class Invert(_NameMixin): - """Invert adjustment.""" - pass - - -class Posterize(_NameMixin): - """Posterize adjustment.""" - @property - def posterize(self): - """Posterize value. - - :rtype: int - """ - return self._data.value - - def __repr__(self): - return "<%s: posterize=%s>" % (self.name, self.posterize) - - -class Threshold(_NameMixin): - """Threshold adjustment.""" - @property - def threshold(self): - """Threshold value. - - :rtype: int - """ - return self._data.value - - def __repr__(self): - return "<%s: threshold=%s>" % (self.name, self.threshold) - - -class SelectiveColor(_NameMixin): - """Selective color adjustment.""" - @property - def method(self): - return self._data.method - - @property - def data(self): - return self._data.items - - def __repr__(self): - return "<%s: method=%g>" % (self.name, self.method) - - -class GradientMap(_NameMixin): - """Gradient map adjustment.""" - @property - def reversed(self): - return self._data.reversed - - @property - def dithered(self): - return self._data.dithered - - @property - def gradient_name(self): - return self._data.name.strip('\x00') - - @property - def color_stops(self): - return self._data.color_stops - - @property - def transparency_stops(self): - return self._data.transparency_stops - - @property - def expansion(self): - return self._data.expansion - - @property - def interpolation(self): - """Interpolation between 0.0 and 1.0.""" - return self._data.interpolation / 4096.0 - - @property - def length(self): - return self._data.length - - @property - def mode(self): - return self._data.mode - - @property - def random_seed(self): - return self._data.random_seed - - @property - def show_transparency(self): - return self._data.show_transparency - - @property - def use_vector_color(self): - return self._data.use_vector_color - - @property - def roughness(self): - return self._data.roughness - - @property - def color_model(self): - return self._data.color_model - - @property - def min_color(self): - return self._data.min_color - - @property - def max_color(self): - return self._data.max_color - - def __repr__(self): - return "<%s: name=%s>" % (self.name, self.gradient_name) diff --git a/src/psd_tools/user_api/composer.py b/src/psd_tools/user_api/composer.py deleted file mode 100644 index f0b4b2d1..00000000 --- a/src/psd_tools/user_api/composer.py +++ /dev/null @@ -1,198 +0,0 @@ -# -*- coding: utf-8 -*- -""" -PSD layer composer. -""" -from __future__ import absolute_import, unicode_literals -import logging -from psd_tools.user_api import BBox -from psd_tools.user_api import pil_support -from psd_tools.constants import TaggedBlock, ColorMode -import psd_tools.user_api.layers -from PIL import Image - -logger = logging.getLogger(__name__) - - -# Color mode mappings. -COLOR_MODES = { - ColorMode.BITMAP: '1', - ColorMode.GRAYSCALE: 'LA', - ColorMode.INDEXED: 'P', # Not supported. - ColorMode.RGB: 'RGBA', - ColorMode.CMYK: 'RGBA', # Force RGB. - ColorMode.MULTICHANNEL: 'RGB', # Not supported. - ColorMode.DUOTONE: 'RGB', # Not supported. - ColorMode.LAB: 'LAB', -} - - -def combined_bbox(layers): - """ - Returns a bounding box for ``layers`` or BBox(0, 0, 0, 0) if the layers - have no bbox. - """ - bboxes = [ - layer.bbox for layer in layers - if not layer.bbox.is_empty() and layer.is_visible() - ] - if len(bboxes) == 0: - return BBox(0, 0, 0, 0) - lefts, tops, rights, bottoms = zip(*bboxes) - return BBox(min(lefts), min(tops), max(rights), max(bottoms)) - - -def _get_default_color(mode): - color = 0 - if mode in ('RGBA', 'LA'): - color = tuple([255] * (len(mode) - 1) + [0]) - elif mode in ('RGB', 'L'): - color = tuple([255] * len(mode)) - return color - - -def _apply_opacity(layer_image, layer): - layer_opacity = layer.opacity - if layer.has_tag(TaggedBlock.BLEND_FILL_OPACITY): - layer_opacity *= layer.get_tag(TaggedBlock.BLEND_FILL_OPACITY) - if layer_opacity == 255: - return layer_image - return pil_support.apply_opacity(layer_image, layer_opacity) - - -# TODO: Implement and refactor layer effects. -def _apply_coloroverlay(layer, layer_image): - """ - Apply color overlay effect. - """ - for effect in layer.effects.find('coloroverlay'): - opacity = effect.opacity.value * 255.0 / 100 - color = tuple(int(x) for x in effect.color.value + (opacity,)) - tmp = Image.new("RGBA", layer_image.size, color=color) - - # Overlay only applies strokes when fill is disabled. - fill_only = ( - layer.kind == 'shape' and layer.has_stroke() and - layer.stroke.get(b'fillEnabled', True) - ) - if not fill_only: - tmp.putalpha(layer_image.split()[-1]) - - layer_image = Image.alpha_composite(layer_image, tmp) - return layer_image - - -def _blend(target, image, offset, mask=None): - if image.mode == 'RGBA': - tmp = Image.new(image.mode, target.size, - _get_default_color(image.mode)) - tmp.paste(image, offset, mask=mask) - target = Image.alpha_composite(target, tmp) - elif target.mode == 'LA': - tmp = Image.new('RGBA', target.size, _get_default_color('RGBA')) - tmp.paste(image.convert('RGBA'), offset, mask=mask) - target = Image.alpha_composite(target.convert('RGBA'), tmp) - target = target.convert('LA') - else: - target.paste(image, offset, mask=mask) - - return target - - -def compose(layers, respect_visibility=True, ignore_blend_mode=True, - skip_layer=lambda layer: False, bbox=None): - """ - Compose layers to a single ``PIL.Image`` (the first layer is on top). - - By default hidden layers are not rendered; - pass ``respect_visibility=False`` to render them. - - In order to skip some layers pass ``skip_layer`` function which - should take ``layer`` as an argument and return True or False. - - If ``bbox`` is not None, it should be a 4-tuple with coordinates; - returned image will be restricted to this rectangle. - - Adjustment and layer effects are ignored. - - This is experimental and requires PIL. - - :param layers: a layer, or an iterable of layers - :param respect_visibility: Take visibility flag into account - :param ignore_blend_mode: Ignore blending mode - :param skip_layer: skip composing the given layer if returns True - :rtype: `PIL.Image` - """ - - if isinstance(layers, psd_tools.user_api.layers._RawLayer): - layers = [layers] - - if bbox is None: - bbox = combined_bbox(layers) - - if bbox.is_empty(): - return None - - mode = 'RGBA' - if len(layers): - mode = COLOR_MODES.get(layers[0]._psd.header.color_mode, 'RGBA') - result = Image.new(mode, (bbox.width, bbox.height), - color=_get_default_color(mode)) - - for layer in reversed(layers): - if skip_layer(layer) or ( - not layer.is_visible() and respect_visibility): - continue - - if layer.is_group(): - layer_image = layer.as_PIL( - respect_visibility=respect_visibility, - ignore_blend_mode=ignore_blend_mode, - skip_layer=skip_layer) - else: - layer_image = layer.as_PIL() - - if not layer_image: - continue - - if not ignore_blend_mode and layer.blend_mode != 'normal': - logger.warning('Blend mode is not implemented: %s', - layer.blend_mode) - continue - - clip_image = None - if len(layer.clip_layers): - clip_box = combined_bbox(layer.clip_layers) - if not clip_box.is_empty(): - intersect = clip_box.intersect(layer.bbox) - if not intersect.is_empty(): - clip_image = compose( - layer.clip_layers, respect_visibility, - ignore_blend_mode, skip_layer) - clip_image = clip_image.crop( - intersect.offset((clip_box.x1, clip_box.y1))) - clip_mask = layer_image.crop( - intersect.offset((layer.bbox.x1, layer.bbox.y1))) - - layer_image = _apply_opacity(layer_image, layer) - layer_image = _apply_coloroverlay(layer, layer_image) - - layer_offset = layer.bbox.offset((bbox.x1, bbox.y1)) - mask = None - if layer.has_mask(): - mask_box = layer.mask.bbox - if not layer.mask.disabled and not mask_box.is_empty(): - mask_color = layer.mask.background_color - mask = Image.new('L', layer_image.size, color=(mask_color,)) - mask.paste( - layer.mask.as_PIL(), - mask_box.offset((layer.bbox.x1, layer.bbox.y1)) - ) - layer_image.putalpha(mask) - - result = _blend(result, layer_image, layer_offset) - - if clip_image is not None: - offset = (intersect.x1 - bbox.x1, intersect.y1 - bbox.y1) - result = _blend(result, clip_image, offset, clip_mask) - - return result diff --git a/src/psd_tools/user_api/effects.py b/src/psd_tools/user_api/effects.py deleted file mode 100644 index b8b8ef81..00000000 --- a/src/psd_tools/user_api/effects.py +++ /dev/null @@ -1,658 +0,0 @@ -# -*- coding: utf-8 -*- -"""Layer effects API. - -Class objects in this class corresponds to information in the layer effect -dialog in Photoshop. - -Usage:: - - for effect in psd.layers[0].effects: - print(effect.name(), effect.dict()) - -""" -from __future__ import absolute_import -import inspect -import logging -from psd_tools.constants import TaggedBlock, BlendMode2, ObjectBasedEffects -from psd_tools.decoder.actions import UnitFloat -import psd_tools.user_api.actions - -try: - basestring -except NameError: - basestring = str - - -logger = logging.getLogger(__name__) - - -def get_effects(layer, psd): - """Return effects block from the layer.""" - effects = layer.get_tag([ - TaggedBlock.OBJECT_BASED_EFFECTS_LAYER_INFO, - TaggedBlock.OBJECT_BASED_EFFECTS_LAYER_INFO_V0, - TaggedBlock.OBJECT_BASED_EFFECTS_LAYER_INFO_V1, - ]) - if not effects: - return Effects({}, psd) - return Effects(effects, psd) - - -class _BaseEffect(object): - """Base class for effect.""" - def __init__(self, descriptor, psd): - self._descriptor = descriptor - self._psd = psd # Some effects use global image resources - - @property - def enabled(self): - """Whether if the effect is enabled. - - :rtype: bool - """ - return self.get(b'enab', True) - - @property - def present(self): - """Whether if the effect is present in UI. - - :rtype: bool - """ - return self.get(b'present') - - @property - def show_in_dialog(self): - """Whether if the effect is shown in dialog. - - :rtype: bool - """ - return self.get(b'showInDialog') - - @property - def blend_mode(self): - """Effect blending mode. - - :returns: blending mode - :rtype: str - - Blending mode is one of the following. - - - normal - - dissolve - - darken - - multiply - - color burn - - linear burn - - darken - - lighten - - screen - - color dodge - - linear dodge - - lighter color - - overlay - - soft light - - hard light - - vivid light - - linear light - - pin light - - hard mix - - difference - - exclusion - - subtract - - divide - - hue - - saturation - - color - - luminosity - """ - return BlendMode2.human_name_of( - self.get(b'Md ', BlendMode2.NORMAL), 'normal') - - @property - def opacity(self): - """Layer effect opacity in percentage. - - :rtype: float - """ - return self.get(b'Opct', UnitFloat('PERCENT', 100.0)) - - @property - def name(self): - """Layer effect name. - - :rtype: str - """ - return self.__class__.__name__.lower() - - def get(self, key, default=None): - """Get attribute in the low-level structure. - - :param key: property key - :type key: bytes - :param default: default value to return - """ - return self._descriptor.get(key, default) - - def properties(self): - """Return a list of property names. - - :returns: list of properties. - :rtype: list - """ - return [k for (k, v) in inspect.getmembers( - self.__class__, lambda x: isinstance(x, property))] - - def dict(self): - """Convert to dict.""" - return {k: getattr(self, k) for k in self.properties()} - - def __repr__(self): - return "<%s>" % (self.name.lower(),) - - -class _ColorMixin(object): - @property - def color(self): - """Color. - - :returns: color tuple. - :rtype: psd_tools.user_api.actions.Color - """ - return self.get(b'Clr ') - - -class _ChokeNoiseMixin(_ColorMixin): - @property - def choke(self): - """Choke level.""" - return self.get(b'Ckmt', UnitFloat('PIXELS', 0.0)) - - @property - def size(self): - """Size in pixels.""" - return self.get(b'blur', UnitFloat('PIXELS', 41.0)) - - @property - def noise(self): - """Noise level.""" - return self.get(b'Nose', UnitFloat('PERCENT', 0.0)) - - @property - def anti_aliased(self): - """Angi-aliased.""" - return self.get(b'AntA', False) - - @property - def contour(self): - """Contour configuration.""" - return self.get(b'TrnS') - - -class _AngleMixin(object): - @property - def use_global_light(self): - """Using global light.""" - return self.get(b'uglg', True) - - @property - def angle(self): - """Angle value.""" - if self.use_global_light: - return UnitFloat( - 'ANGLE', - self._psd.image_resource_blocks.get('global_angle', 30.0) - ) - return self.get(b'lagl', UnitFloat('ANGLE', 90.0)) - - -class _GradientMixin(object): - """Mixin for gradient property.""" - @property - def gradient(self): - """Gradient configuration. - - :rtype: psd_tools.user_api.actions.Gradient - """ - return self.get(b'Grad') - - -class _PatternMixin(object): - """Mixin for pattern property.""" - @property - def pattern(self): - """Pattern config. - - :rtype: psd_tools.user_api.actions.Pattern - """ - # TODO: Expose nested property. - return self.get(b'Ptrn') - - -class _ShadowEffect(_BaseEffect, _ChokeNoiseMixin, _AngleMixin): - """Base class for shadow effect.""" - @property - def distance(self): - """Distance.""" - return self.get(b'Dstn', UnitFloat('PIXELS', 18.0)) - - -class DropShadow(_ShadowEffect): - """DropShadow effect.""" - @property - def layer_knocks_out(self): - """Layers are knocking out.""" - return self.get(b'layerConceals', True) - - -class InnerShadow(_ShadowEffect): - """InnerShadow effect.""" - pass - - -class _GlowEffect(_BaseEffect, _ChokeNoiseMixin, _GradientMixin): - """Base class for glow effect.""" - @property - def glow_type(self): - """ Elements technique, softer or precise """ - return {b'SfBL': 'softer'}.get(self.get(b'GlwT', b'SfBL'), 'precise') - - @property - def quality_range(self): - """Quality range.""" - return self.get(b'Inpr') - - @property - def quality_jitter(self): - """Quality jitter""" - return self.get(b'ShdN') - - -class OuterGlow(_GlowEffect): - """OuterGlow effect.""" - @property - def spread(self): - return self.get(b'ShdN') - - -class InnerGlow(_GlowEffect): - """InnerGlow effect.""" - @property - def glow_source(self): - """ Elements source, center or edge """ - return {b'SrcE': 'edge'}.get(self.get(b'glwS', b'SrcE'), 'center') - - -class _OverlayEffect(_BaseEffect): - pass - - -class _AlignScaleMixin(object): - @property - def scale(self): - """Scale value.""" - return self.get(b'Scl ', UnitFloat('PERCENT', 100.0)) - - @property - def aligned(self): - """Aligned.""" - return self.get(b'Algn') - - -class ColorOverlay(_OverlayEffect, _ColorMixin): - """ColorOverlay effect.""" - pass - - -class GradientOverlay(_OverlayEffect, _AlignScaleMixin, _GradientMixin): - """GradientOverlay effect.""" - TYPES = { - b'Lnr ': 'linear', - b'Rdl ': 'radial', - b'Angl': 'angle', - b'Rflc': 'reflected', - b'Dmnd': 'diamond', - } - - @property - def angle(self): - """Angle value.""" - return self.get(b'Angl', UnitFloat('ANGLE', 90.0)) - - @property - def type(self): - """ - Gradient type, one of `linear`, `radial`, `angle`, `reflected`, or - `diamond`. - """ - return self.TYPES.get(self.get(b'Type', b'Lnr ')) - - @property - def reversed(self): - """Reverse flag.""" - return self.get(b'Rvrs', False) - - @property - def dithered(self): - """Dither flag.""" - return self.get(b'Dthr', False) - - @property - def offset(self): - """Offset value.""" - return self.get(b'Ofst') - - -class PatternOverlay(_OverlayEffect, _AlignScaleMixin, _PatternMixin): - """PatternOverlay effect. - - Retrieving pattern data:: - - if effect.name() == 'PatternOverlay': - pattern = psd.patterns.get(effect.pattern.id) - """ - @property - def phase(self): - """Phase value in Point. - - :rtype: Point - """ - return self.get(b'phase', psd_tools.user_api.actions.Point(0.0, 0.0)) - - -class Stroke(_BaseEffect, _ColorMixin, _PatternMixin, _GradientMixin): - """Stroke effect.""" - - POSITIONS = { - b'InsF': 'inner', - b'OutF': 'outer', - b'CtrF': 'center', - } - - FILL_TYPES = { - b'SClr': 'solid-color', - b'GrFl': 'gradient', - b'Ptrn': 'pattern', - } - - @property - def position(self): - """Position of the stroke, `inner`, `outer`, or `center`.""" - return self.POSITIONS.get(self.get(b'Styl', b'OutF')) - - @property - def fill_type(self): - """Fill type, solid-color, gradient, or pattern.""" - return self.FILL_TYPES.get(self.get(b'PntT')) - - @property - def size(self): - """Size value.""" - return self.get(b'Sz ', UnitFloat('PIXELS', 1.0)) - - @property - def overprint(self): - """Overprint flag.""" - return self.get(b'overprint', False) - - @property - def fill(self): - if self.fill_type == 'solid-color': - return ColorOverlay(self._descriptor, self._psd) - elif self.fill_type.startswith('pattern'): - return PatternOverlay(self._descriptor, self._psd) - elif self.fill_type.startswith('gradient'): - return GradientOverlay(self._descriptor, self._psd) - logger.error("Unknown fill type: {}".format(self.fill_type)) - return None - - -class BevelEmboss(_BaseEffect, _AngleMixin): - """Bevel and Emboss effect.""" - - BEVEL_TYPE = { - b'SfBL': 'smooth', - b'PrBL': 'chiesel-hard', - b'Slmt': 'chiesel-soft', - } - - BEVEL_STYLE = { - b'OtrB': 'outer-bevel', - b'InrB': 'inner-bevel', - b'Embs': 'emboss', - b'PlEb': 'pillow-emboss', - b'strokeEmboss': 'stroke-emboss', - } - - DIRECTION = { - b'In ': 'up', - b'Out ': 'down', - } - - @property - def highlight_mode(self): - """Highlight blending mode.""" - return BlendMode2.human_name_of( - self.get(b'hglM', BlendMode2.NORMAL), 'normal') - - @property - def highlight_color(self): - """Highlight color value.""" - return self.get(b'hglC') - - @property - def highlight_opacity(self): - """Highlight opacity value.""" - return self.get(b'hglO') - - @property - def shadow_mode(self): - """Shadow blending mode.""" - return BlendMode2.human_name_of( - self.get(b'sdwM', BlendMode2.NORMAL), 'normal') - - @property - def shadow_color(self): - """Shadow color value.""" - return self.get(b'sdwC') - - @property - def shadow_opacity(self): - """Shadow opacity value.""" - return self.get(b'sdwO') - - @property - def bevel_type(self): - """Bevel type, one of `smooth`, `chiesel-hard`, `chiesel-soft`.""" - return self.BEVEL_TYPE.get(self.get(b'bvlT', b'SfBL')) - - @property - def bevel_style(self): - """ - Bevel style, one of `outer-bevel`, `inner-bevel`, `emboss`, - `pillow-emboss`, or `stroke-emboss`. - """ - return self.BEVEL_STYLE.get(self.get(b'bvlS', b'Embs')) - - @property - def altitude(self): - """Altitude value.""" - return self.get(b'Lald', 30.0) - - @property - def depth(self): - """Depth value.""" - return self.get(b'srgR', 100.0) - - @property - def size(self): - """Size value in pixel.""" - return self.get(b'blur', UnitFloat('PIXELS', 41.0)) - - @property - def direction(self): - """Direction, either `up` or `down`.""" - return self.DIRECTION.get(self.get(b'bvlD', b'In ')) - - @property - def contour(self): - """Contour configuration.""" - return self.get(b'TrnS') - - @property - def anti_aliased(self): - """Anti-aliased.""" - return self.get(b'antialiasGloss', False) - - @property - def soften(self): - """Soften value.""" - return self.get(b'Sftn', 0.0) - - @property - def use_shape(self): - """Using shape.""" - return self.get(b'useShape', False) - - @property - def use_texture(self): - """Using texture.""" - return self.get(b'useTexture', False) - - -class Satin(_BaseEffect, _ColorMixin): - """ Satin effect """ - @property - def anti_aliased(self): - """Anti-aliased.""" - return self.get(b'AntA', True) - - @property - def inverted(self): - """Inverted.""" - return self.get(b'Invr', True) - - @property - def angle(self): - """Angle value.""" - return self.get(b'lagl', UnitFloat('ANGLE', 90.0)) - - @property - def distance(self): - """Distance value.""" - return self.get(b'Dstn', UnitFloat('PIXELS', 250.0)) - - @property - def size(self): - """Size value in pixel.""" - return self.get(b'blur', UnitFloat('PIXELS', 250.0)) - - @property - def contour(self): - """Contour configuration.""" - return self.get(b'MpgS') - - -class Effects(object): - """Layer effects wrapper. Behaves like a list. - - Example:: - - for effect in psd.layers[0].effects: - print(effect.name()) - - for effect in psd.layers[0].effects.find('coloroverlay'): - print(effect.color) - """ - _KEYS = { - ObjectBasedEffects.DROP_SHADOW_MULTI: DropShadow, - ObjectBasedEffects.DROP_SHADOW: DropShadow, - ObjectBasedEffects.INNER_SHADOW_MULTI: InnerShadow, - ObjectBasedEffects.INNER_SHADOW: InnerShadow, - ObjectBasedEffects.OUTER_GLOW: OuterGlow, - ObjectBasedEffects.COLOR_OVERLAY_MULTI: ColorOverlay, - ObjectBasedEffects.COLOR_OVERLAY: ColorOverlay, - ObjectBasedEffects.GRADIENT_OVERLAY_MULTI: GradientOverlay, - ObjectBasedEffects.GRADIENT_OVERLAY: GradientOverlay, - ObjectBasedEffects.PATTERN_OVERLAY: PatternOverlay, - ObjectBasedEffects.STROKE_MULTI: Stroke, - ObjectBasedEffects.STROKE: Stroke, - ObjectBasedEffects.INNER_GLOW: InnerGlow, - ObjectBasedEffects.BEVEL_EMBOSS: BevelEmboss, - ObjectBasedEffects.SATIN: Satin, - } - - def __init__(self, descriptor, psd): - self._descriptor = descriptor - self.items = self._build_items(psd) - - @property - def scale(self): - """Scale value.""" - return self.get(b'Scl ') - - @property - def enabled(self): - """Whether if all the effects are enabled. - - :rtype: bool - """ - return self._descriptor.get(b'masterFXSwitch', True) - - def _build_items(self, psd): - items = [] - for key in self._descriptor: - cls = self._KEYS.get(key, None) - if not cls: - continue - if key.endswith(b'Multi'): - for value in self._descriptor[key]: - items.append(cls(value, psd)) - else: - items.append(cls(self._descriptor[key], psd)) - return items - - def present_items(self): - """List of effects present in Photoshop UI.""" - return [item for item in self.items if item.present] - - def enabled_items(self): - """List of enabled effects.""" - if self.enabled: - return [item for item in self.items - if getattr(item, 'enabled', False)] - return [] - - def has(self, kinds): - if isinstance(kinds, basestring): - kinds = [kinds] - kinds = {kind.lower() for kind in kinds} - return any(item.name.lower() in kinds - for item in self.enabled_items()) - - def find(self, kind): - """Return a list of specified effects. - - Names can be one of the following: - - - DropShadow - - InnerShadow - - OuterGlow - - ColorOverlay - - GradientOverlay - - PatternOverlay - - Stroke - - InnerGlow - - BevelEmboss - - Satin - """ - return [item for item in self.enabled_items() - if item.name.lower() == kind.lower()] - - def __getitem__(self, index): - return self.enabled_items()[index] - - def __len__(self): - return len(self.enabled_items()) - - def __repr__(self): - return "%s" % (self.enabled_items(),) diff --git a/src/psd_tools/user_api/layers.py b/src/psd_tools/user_api/layers.py deleted file mode 100644 index 57aa5265..00000000 --- a/src/psd_tools/user_api/layers.py +++ /dev/null @@ -1,809 +0,0 @@ -# -*- coding: utf-8 -*- -"""PSD layer classes. -""" -from __future__ import absolute_import, unicode_literals - -import logging -from psd_tools.constants import ( - ColorMode, TaggedBlock, SectionDivider, BlendMode, TextProperty, - PlacedLayerProperty, SzProperty) -from psd_tools.decoder.actions import Descriptor -from psd_tools.user_api import pil_support -from psd_tools.user_api import BBox -from psd_tools.user_api.actions import translate -from psd_tools.user_api.mask import Mask -from psd_tools.user_api.effects import get_effects -from psd_tools.user_api.composer import combined_bbox, compose - -logger = logging.getLogger(__name__) - - -class _TaggedBlockMixin(object): - - @property - def tagged_blocks(self): - """Returns the underlying tagged blocks structure.""" - if not self._tagged_blocks: - self._tagged_blocks = dict(self._record.tagged_blocks) - return self._tagged_blocks - - def get_tag(self, keys, default=None): - """Get specified record from tagged blocks.""" - if isinstance(keys, bytes): - keys = [keys] - for key in keys: - value = self.tagged_blocks.get(key) - if value is not None: - return translate(value) - return default - - def has_tag(self, keys): - """Returns if the specified record exists in the tagged blocks.""" - if isinstance(keys, bytes): - keys = [keys] - return any(key in self.tagged_blocks for key in keys) - - -class _RawLayer(_TaggedBlockMixin): - """ - Layer groups and layers are internally both 'layers' in PSD; - they share some common properties. - """ - def __init__(self, parent, index): - self._parent = parent - self._psd = parent._psd - self._index = index - self._clip_layers = [] - self._tagged_blocks = None - self._effects = None - - @property - def name(self): - """Layer name (as unicode). """ - return self.get_tag(TaggedBlock.UNICODE_LAYER_NAME, self._record.name) - - @property - def kind(self): - """ - Kind of this layer, either group, pixel, shape, type, smartobject, or - psdimage (root object). - """ - return self.__class__.__name__.lower().replace("layer", "") - - @property - def visible(self): - """Layer visibility. Doesn't take group visibility in account.""" - return self._record.flags.visible - - def is_visible(self): - """Layer visibility. Takes group visibility in account.""" - return self.visible and self.parent.is_visible() - - @property - def layer_id(self): - """ID of the layer.""" - return self.get_tag(TaggedBlock.LAYER_ID) - - @property - def opacity(self): - """Opacity of this layer.""" - return self._record.opacity - - @property - def parent(self): - """Parent of this layer.""" - return self._parent - - def is_group(self): - """Return True if the layer is a group.""" - return False - - @property - def blend_mode(self): - """ - Blend mode of this layer. See - :py:class:`~psd_tools.constants.BlendMode` - - :rtype: str - """ - return BlendMode.human_name_of(self._record.blend_mode) - - def has_mask(self): - """Returns True if the layer has a mask.""" - return ( - True if self._index is not None and self._record.mask_data - else False - ) - - def as_PIL(self): - """ - Returns a PIL.Image for this layer. - - Note that this method does not apply layer masks. To compose a layer - with mask, use :py:func:`psd_tools.compose`. - - :rtype: `PIL.Image` - """ - if self.has_pixels(): - return self._psd._layer_as_PIL(self._index) - else: - return None - - def as_pymaging(self): - """Returns a pymaging.Image for this layer.""" - if self.has_pixels(): - return self._psd._layer_as_pymaging(self._index) - else: - return None - - @property - def bbox(self): - """BBox(x1, y1, x2, y2) namedtuple with layer bounding box. - - :rtype: BBox - """ - return BBox(self._record.left, self._record.top, self._record.right, - self._record.bottom) - - @property - def left(self): - """Left coordinate. - - :rtype: int - """ - return self._record.left - - @property - def right(self): - """Right coordinate. - - :rtype: int - """ - return self._record.right - - @property - def top(self): - """Top coordinate. - - :rtype: int - """ - return self._record.top - - @property - def bottom(self): - """Bottom coordinate. - - :rtype: int - """ - return self._record.bottom - - @property - def width(self): - """Width. - - :rtype: int - """ - return self.right - self.left - - @property - def height(self): - """Height. - - :rtype: int - """ - return self.bottom - self.top - - def has_box(self): - """Return True if the layer has a nonzero area.""" - return self.width > 0 and self.height > 0 - - def has_pixels(self): - """Return True if the layer has associated pixels.""" - return any( - cinfo.id >= 0 and cdata.data and len(cdata.data) > 0 - for cinfo, cdata in zip(self._record.channels, self._channels) - ) - - def has_relevant_pixels(self): - """Return True if the layer has relevant associated pixels.""" - if self.flags.pixel_data_irrelevant: - return False - return self.has_pixels() - - def has_vector_mask(self): - """Return True if the layer has an associated vector mask.""" - return self.has_tag([TaggedBlock.VECTOR_MASK_SETTING1, - TaggedBlock.VECTOR_MASK_SETTING2]) - - @property - def vector_mask(self): - """Return the associated vector mask, or None. - - :rtype: ~psd_tools.user_api.shape.VectorMask - """ - return self.get_tag((TaggedBlock.VECTOR_MASK_SETTING1, - TaggedBlock.VECTOR_MASK_SETTING2)) - - @property - def flags(self): - """Return flags assocated to the layer. - - :rtype: ~psd_tools.reader.layers.LayerFlags - """ - return self._record.flags - - @property - def mask(self): - """ - Returns mask associated with this layer. - - :rtype: ~psd_tools.user_api.mask.Mask - """ - if not hasattr(self, "_mask"): - self._mask = Mask(self) if self.has_mask() else None - return self._mask - - def has_clip_layers(self): - """Returns True if the layer has associated clipping.""" - return len(self.clip_layers) > 0 - - @property - def clip_layers(self): - """ - Returns clip layers associated with this layer. - - :rtype: list of, AdjustmentLayer, PixelLayer, or ShapeLayer - """ - return self._clip_layers - - def has_effects(self): - """Returns True if the layer has layer effects.""" - return any(x in self.tagged_blocks for x in ( - TaggedBlock.OBJECT_BASED_EFFECTS_LAYER_INFO, - TaggedBlock.OBJECT_BASED_EFFECTS_LAYER_INFO_V0, - TaggedBlock.OBJECT_BASED_EFFECTS_LAYER_INFO_V1, - )) - - @property - def effects(self): - """ - Effects associated with this layer. - - :rtype: ~psd_tools.user_api.effects.Effects - """ - if not self._effects: - self._effects = get_effects(self, self._psd) - return self._effects - - @property - def _info(self): - """(Deprecated) Use `_record()`.""" - return self._record - - @property - def _record(self): - """Returns the underlying layer record.""" - return self._psd._layer_records(self._index) - - @property - def _channels(self): - """Returns the underlying layer channel images.""" - return self._psd._layer_channels(self._index) - - def __repr__(self): - return ( - "<%s: %r, size=%dx%d, x=%d, y=%d%s%s%s>" % ( - self.kind, self.name, self.width, self.height, - self.left, self.top, - ", mask=%s" % self.mask if self.has_mask() else "", - "" if self.visible else ", invisible", - ", effects=%s" % self.effects if self.has_effects() else "" - )) - - -class _GroupMixin(object): - """Group mixin.""" - - @property - def bbox(self): - """ - BBox(x1, y1, x2, y2) namedtuple with a bounding box for - all layers in this group; None if a group has no children. - """ - if not self._bbox: - self._bbox = combined_bbox(self.layers) - return self._bbox - - @property - def left(self): - """Left coordinate.""" - return self.bbox.x1 - - @property - def right(self): - """Right coordinate.""" - return self.bbox.x2 - - @property - def top(self): - """Top coordinate.""" - return self.bbox.y1 - - @property - def bottom(self): - """Bottom coordinate.""" - return self.bbox.y2 - - @property - def width(self): - """Width.""" - return self.bbox.width - - @property - def height(self): - """Height.""" - return self.bbox.height - - def has_box(self): - """Return True if the layer has a nonzero area.""" - return any(l.has_box() for l in self.layers) - - @property - def layers(self): - """ - Return a list of child layers in this group. - - :rtype: list of, - ~psd_tools.user_api.layers.Group, - ~psd_tools.user_api.layers.AdjustmentLayer, - ~psd_tools.user_api.layers.PixelLayer, - ~psd_tools.user_api.layers.ShapeLayer, - ~psd_tools.user_api.layers.SmartObjectLayer, or - ~psd_tools.user_api.layers.TypeLayer - """ - return self._layers - - def is_group(self): - """Return True if the layer is a group.""" - return True - - def descendants(self, include_clip=True): - """ - Return a generator to iterate over all descendant layers. - """ - for layer in self._layers: - yield layer - if layer.is_group(): - for child in layer.descendants(include_clip): - yield child - if include_clip: - for clip_layer in layer.clip_layers: - yield clip_layer - - def as_PIL(self, **kwargs): - """ - Returns a PIL image for this group. - This is highly experimental. - """ - return compose(self.layers, **kwargs) - - -class Group(_GroupMixin, _RawLayer): - """PSD layer group.""" - - def __init__(self, parent, index): - super(Group, self).__init__(parent, index) - self._layers = [] - self._bbox = None - - @property - def closed(self): - divider = self._divider - if divider is None: - return None - return divider.type == SectionDivider.CLOSED_FOLDER - - @property - def _divider(self): - return self.get_tag([TaggedBlock.SECTION_DIVIDER_SETTING, - TaggedBlock.NESTED_SECTION_DIVIDER_SETTING]) - - def __repr__(self): - return "<%s: %r, layer_count=%d%s%s>" % ( - self.kind, self.name, len(self.layers), - ", mask=%s" % self.mask if self.has_mask() else "", - "" if self.visible else ", invisible",) - - -class AdjustmentLayer(_RawLayer): - """PSD adjustment layer.""" - - def __init__(self, parent, index): - super(AdjustmentLayer, self).__init__(parent, index) - self._set_key() - - def _set_key(self): - self._key = None - for key in self.tagged_blocks: - if (TaggedBlock.is_adjustment_key(key) or - TaggedBlock.is_fill_key(key)): - self._key = key - return - logger.error("Unknown adjustment layer: {}".format(self)) - - @property - def adjustment_type(self): - """Type of adjustment.""" - return TaggedBlock.human_name_of(self._key).replace("-setting", "") - - @property - def data(self): - """ - Adjustment data. Depending on the adjustment type, return one of the - following instance. - - - :py:class:`~psd_tools.user_api.adjustments.BrightnessContrast` - - :py:class:`~psd_tools.user_api.adjustments.Levels` - - :py:class:`~psd_tools.user_api.adjustments.Curves` - - :py:class:`~psd_tools.user_api.adjustments.Exposure` - - :py:class:`~psd_tools.user_api.adjustments.Vibrance` - - :py:class:`~psd_tools.user_api.adjustments.HueSaturation` - - :py:class:`~psd_tools.user_api.adjustments.ColorBalance` - - :py:class:`~psd_tools.user_api.adjustments.BlackWhite` - - :py:class:`~psd_tools.user_api.adjustments.PhotoFilter` - - :py:class:`~psd_tools.user_api.adjustments.ChannelMixer` - - :py:class:`~psd_tools.user_api.adjustments.ColorLookup` - - :py:class:`~psd_tools.user_api.adjustments.Invert` - - :py:class:`~psd_tools.user_api.adjustments.Posterize` - - :py:class:`~psd_tools.user_api.adjustments.Threshold` - - :py:class:`~psd_tools.user_api.adjustments.SelectiveColor` - - :py:class:`~psd_tools.user_api.adjustments.GradientMap` - - """ - if (self.adjustment_type == 'brightness-and-contrast' and - self.has_tag(TaggedBlock.CONTENT_GENERATOR_EXTRA_DATA)): - data = self.get_tag(TaggedBlock.CONTENT_GENERATOR_EXTRA_DATA) - if not data.use_legacy: - return data - - return self.get_tag(self._key) - - def __repr__(self): - return "<%s: %r type=%r%s>" % ( - self.kind, self.name, self.adjustment_type, - "" if self.visible else ", invisible" - ) - - -class PixelLayer(_RawLayer): - """PSD pixel layer.""" - pass - - -class ShapeLayer(_RawLayer): - """PSD shape layer. - - The shape path is accessible by :py:attr:`vector_mask` attribute. - Stroke styling is specified by :py:attr:`stroke`, and fill styling is - specified by :py:attr:`stroke_content`. - """ - - def __init__(self, parent, index): - super(ShapeLayer, self).__init__(parent, index) - self._bbox = None - - def as_PIL(self, draw=False): - """Returns a PIL image for this layer.""" - if draw or self._must_draw(): - # TODO: Replace polygon with bezier curve. - mode = { - ColorMode.RGB: 'RGBA', - ColorMode.GRAYSCALE: 'LA', - ColorMode.CMYK: 'CMYKA', - }.get(self._psd.header.color_mode, 'RGBA') - drawing = pil_support.draw_polygon( - self._psd.viewbox, - self._get_anchors(), - mode=mode, - fill=self._get_color(mode), - ) - return drawing.crop(self.bbox) - else: - return super(ShapeLayer, self).as_PIL() - - @property - def bbox(self): - """BBox(x1, y1, x2, y2) namedtuple with layer bounding box. - - :rtype: BBox - """ - if self._bbox is None: - if self._must_draw(): - self._bbox = self._get_bbox() - else: - self._bbox = super(ShapeLayer, self).bbox - if self._bbox.is_empty(): - self._bbox = self._get_bbox() - return self._bbox - - def _is_unitrect(self): - if self.has_origination(): - return self.origination.origin_type == 1 - return False - - def _must_draw(self): - return self._is_unitrect() or not self.has_pixels() - - @property - def left(self): - """Left coordinate. - - :rtype: int - """ - return self.bbox.left - - @property - def right(self): - """Right coordinate. - - :rtype: int - """ - return self.bbox.right - - @property - def top(self): - """Top coordinate. - - :rtype: int - """ - return self.bbox.top - - @property - def bottom(self): - """Bottom coordinate. - - :rtype: int - """ - return self.bbox.bottom - - @property - def origination(self): - """Vector origination data. - - :rtype: ~psd_tools.user_api.shape.Origination - """ - return self.get_tag(TaggedBlock.VECTOR_ORIGINATION_DATA) - - @property - def stroke(self): - """Stroke style data. - - :rtype: ~psd_tools.user_api.shape.StrokeStyle - """ - return self.get_tag(TaggedBlock.VECTOR_STROKE_DATA) - - @property - def stroke_content(self): - """Shape fill data. - - :rtype: ~psd_tools.user_api.effects.ColorOverlay, - ~psd_tools.user_api.effects.PatternOverlay, or - ~psd_tools.user_api.effects.GradientOverlay - """ - return self.get_tag(TaggedBlock.VECTOR_STROKE_CONTENT_DATA) - - def has_origination(self): - """True if the layer has vector origination.""" - return self.has_tag(TaggedBlock.VECTOR_ORIGINATION_DATA) - - def has_stroke(self): - """True if the layer has stroke.""" - return self.has_tag(TaggedBlock.VECTOR_STROKE_DATA) - - def has_stroke_content(self): - """True if the layer has shape fill.""" - return self.has_tag(TaggedBlock.VECTOR_STROKE_CONTENT_DATA) - - def has_path(self): - """True if the layer has path knots.""" - return self.has_vector_mask() and any( - path.num_knots > 1 for path in self.vector_mask.paths) - - def _get_anchors(self): - """Anchor points of the shape [(x, y), (x, y), ...].""" - vector_mask = self.vector_mask - if not vector_mask: - return None - width, height = self._psd.width, self._psd.height - anchors = [ - [(int(round(p[1] * width)), int(round(p[0] * height))) - for p in path] - for path in vector_mask.anchors - ] - if not anchors: - return [[(0, 0), (0, height), (width, height), (width, 0)]] - return anchors - - def _get_bbox(self): - """BBox(x1, y1, x2, y2) namedtuple of the shape.""" - # TODO: Compute bezier curve. - anchors = self._get_anchors() - if not anchors or len(anchors[0]) < 2: - # Likely be all-pixel fill. - return BBox(0, 0, self._psd.width, self._psd.height) - return BBox(min(min([p[0] for p in path]) for path in anchors), - min(min([p[1] for p in path]) for path in anchors), - max(max([p[0] for p in path]) for path in anchors), - max(max([p[1] for p in path]) for path in anchors)) - - def _get_color(self, mode, default=(0, 0, 0, 0)): - effect = self.get_tag(TaggedBlock.SOLID_COLOR_SHEET_SETTING) - if not effect: - logger.warning("Gradient or pattern fill not supported") - return default - color = effect.color - if mode in ('RGBA', 'CMYKA'): - return tuple(list(map(int, map(round, color.value))) + - [int(round(self.opacity))]) - elif mode == 'LA': - if color.name != 'gray': - logger.warning('mode and color mismatch %r vs %r' % ( - mode, color.name - )) - return (int(round(255 * (1. - color.value[0]))), - int(round(self.opacity))) - else: - return default - - -class SmartObjectLayer(_RawLayer): - """PSD smartobject layer. - - Smart object is an embedded or external object in PSD document. Typically - smart objects is used for non-destructive editing. The linked data is - accessible by - :py:attr:`~psd_tools.user_api.layers.SmartObjectLayer.linked_data` - attribute. - """ - def __init__(self, parent, index): - super(SmartObjectLayer, self).__init__(parent, index) - self._block = self._get_block() - - @property - def unique_id(self): - return (self._block.get(PlacedLayerProperty.ID).value - if self._block else None) - - @property - def placed_bbox(self): - """ - BBox(x1, y1, x2, y2) with transformed box. The tranform of a layer - the points for all 4 corners. - """ - if self._block: - transform = self._block.get(PlacedLayerProperty.TRANSFORM).items - return BBox(transform[0].value, transform[1].value, - transform[4].value, transform[5].value) - else: - return None - - @property - def object_bbox(self): - """ - BBox(x1, y1, x2, y2) with original object content coordinates. - """ - if self._block: - size = dict(self._block.get(PlacedLayerProperty.SIZE).items) - return BBox(0, 0, - size[SzProperty.WIDTH].value, - size[SzProperty.HEIGHT].value) - else: - return None - - @property - def linked_data(self): - """ - Return linked layer data. - - :rtype: ~psd_tools.user_api.smart_object.SmartObject - """ - return self._psd.smart_objects.get(self.unique_id) - - def _get_block(self): - block = self.get_tag([ - TaggedBlock.SMART_OBJECT_PLACED_LAYER_DATA, - TaggedBlock.PLACED_LAYER_DATA, - TaggedBlock.PLACED_LAYER_OBSOLETE1, - TaggedBlock.PLACED_LAYER_OBSOLETE2, - ]) - if not block: - logger.warning("Empty smartobject") - return None - return dict(block) - - def __repr__(self): - return ( - "<%s: %r, size=%dx%d, x=%d, y=%d%s%s, linked=%s>") % ( - self.kind, self.name, self.width, self.height, - self.left, self.top, - ", mask=%s" % self.mask if self.has_mask() else "", - "" if self.visible else ", invisible", - self.linked_data) - - -class TypeLayer(_RawLayer): - """ - PSD type layer. - - A type layer has text information such as fonts and paragraph settings. - In contrast to pixels, texts are placed in the image by Affine - transformation matrix - :py:attr:`~psd_tools.user_api.layers.TypeLayer.matrix`. Low level - information is kept in - :py:attr:`~psd_tools.user_api.layers.TypeLayer.engine_data`. - """ - def __init__(self, parent, index): - super(TypeLayer, self).__init__(parent, index) - self._type_info = self.get_tag(TaggedBlock.TYPE_TOOL_OBJECT_SETTING) - self.text_data = dict(self._type_info.text_data.items) - - @property - def text(self): - """Unicode string.""" - return self.text_data[TextProperty.TXT].value - - @property - def matrix(self): - """Matrix [xx xy yx yy tx ty] applies affine transformation.""" - return (self._type_info.xx, self._type_info.xy, self._type_info.yx, - self._type_info.yy, self._type_info.tx, self._type_info.ty) - - @property - def engine_data(self): - """ - Type information in engine data format. See - :py:mod:`psd_tools.decoder.engine_data` - - :rtype: `dict` - """ - return self.text_data.get(b'EngineData') - - @property - def fontset(self): - """Font set.""" - return self.engine_data[b'DocumentResources'][b'FontSet'] - - @property - def writing_direction(self): - """Writing direction.""" - return self.engine_data[b'EngineDict'][ - b'Rendered'][b'Shapes'][b'WritingDirection'] - - @property - def full_text(self): - """Raw string including trailing newline.""" - return self.engine_data[b'EngineDict'][b'Editor'][b'Text'] - - def style_spans(self): - """Returns spans by text style segments.""" - text = self.full_text - fontset = self.fontset - engine_data = self.engine_data - runlength = engine_data[b'EngineDict'][b'StyleRun'][b'RunLengthArray'] - runarray = engine_data[b'EngineDict'][b'StyleRun'][b'RunArray'] - - start = 0 - spans = [] - for run, size in zip(runarray, runlength): - runtext = text[start:start + size] - stylesheet = run[b'StyleSheet'][b'StyleSheetData'].copy() - stylesheet[b'Text'] = runtext - stylesheet[b'Font'] = fontset[stylesheet.get(b'Font', 0)] - spans.append(stylesheet) - start += size - return spans - - -def merge_layers(*args, **kwargs): - """Deprecated, use :py:func:`psd_tools.user_api.composer.compose`.""" - return compose(*args, **kwargs) diff --git a/src/psd_tools/user_api/mask.py b/src/psd_tools/user_api/mask.py deleted file mode 100644 index 5862c5c3..00000000 --- a/src/psd_tools/user_api/mask.py +++ /dev/null @@ -1,158 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, print_function - -from psd_tools.user_api import pil_support -from psd_tools.user_api.psd_image import BBox - - -class Mask(object): - """Mask data attached to a layer. - - There are two distinct internal mask data: user mask and vector mask. - User mask refers any pixel-based mask whereas vector mask refers a mask - from a shape path. Internally, two masks are combined and referred - real mask. - """ - def __init__(self, layer): - self.mask_data = layer._record.mask_data - self._decoded_data = layer._psd.decoded_data - self._layer_index = layer._index - - @property - def background_color(self): - """Background color.""" - return self.get_background_color() - - def get_background_color(self, real=True): - """Get background color.""" - if real and self.mask_data.real_background_color: - return self.mask_data.real_background_color - return self.mask_data.background_color - - @property - def bbox(self): - """BBox""" - return self.get_bbox() - - @property - def left(self): - """Left coordinate.""" - if self.has_real(): - return self.mask_data.real_left - return self.mask_data.left - - @property - def right(self): - """Right coordinate.""" - if self.has_real(): - return self.mask_data.real_right - return self.mask_data.right - - @property - def top(self): - """Top coordinate.""" - if self.has_real(): - return self.mask_data.real_top - return self.mask_data.top - - @property - def bottom(self): - """Bottom coordinate.""" - if self.has_real(): - return self.mask_data.real_bottom - return self.mask_data.bottom - - @property - def width(self): - """Width.""" - return self.right - self.left - - @property - def height(self): - """Height.""" - return self.bottom - self.top - - @property - def disabled(self): - """Disabled.""" - return self.mask_data.flags.mask_disabled - - @property - def relative_to_layer(self): - """If the position is relative to the layer.""" - return self.mask_data.flags.pos_relative_to_layer - - @property - def inverted(self): - """If the mask is inverted.""" - return self.mask_data.flags.invert_mask - - @property - def user_mask_from_render(self): - """If the mask is rendered.""" - return self.mask_data.flags.user_mask_from_render - - @property - def parameters_applied(self): - """If the parameters are applied.""" - return self.mask_data.flags.parameters_applied - - @property - def flags(self): - """Flags.""" - return self.mask_data.flags - - @property - def parameters(self): - """Parameters.""" - return self.mask_data.parameters - - @property - def real_flags(self): - """Real flag.""" - return self.mask_data.real_flags - - def get_bbox(self, real=True): - """ - Get BBox(x1, y1, x2, y2) namedtuple with mask bounding box. - - :param real: When False, ignore real flags. - """ - if real and self.has_real(): - return BBox(self.mask_data.real_left, self.mask_data.real_top, - self.mask_data.real_right, self.mask_data.real_bottom) - else: - return BBox(self.mask_data.left, self.mask_data.top, - self.mask_data.right, self.mask_data.bottom) - - def has_real(self): - """Return True if the mask has a valid bbox.""" - return self.real_flags is not None - - def has_box(self): - """Return True if the mask has a valid bbox.""" - return self.width > 0 and self.height > 0 - - def is_valid(self): - """(Deprecated) Use `has_box`""" - return self.has_box() - - def as_PIL(self, real=True): - """ - Returns a PIL image for the mask. - - If ``real`` is True, extract real mask consisting of both bitmap - and vector mask. - - Returns ``None`` if the mask has zero size. - """ - if not self.has_box(): - return None - return pil_support.extract_layer_mask(self._decoded_data, - self._layer_index, - real) - - def __repr__(self): - return "<%s: size=%dx%d, x=%d, y=%d>" % ( - self.__class__.__name__.lower(), self.width, self.height, - self.left, self.top) diff --git a/src/psd_tools/user_api/pil_support.py b/src/psd_tools/user_api/pil_support.py deleted file mode 100644 index e5fea311..00000000 --- a/src/psd_tools/user_api/pil_support.py +++ /dev/null @@ -1,471 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, division - -import warnings -import io -from psd_tools.utils import be_array_from_bytes -from psd_tools.constants import ( - Compression, ChannelID, ColorMode, ImageResourceID, TaggedBlock -) -from psd_tools import icc_profiles - -try: - from PIL import Image, ImageDraw, ImageMath - if hasattr(Image, 'frombytes'): - frombytes = Image.frombytes - else: - frombytes = Image.fromstring # PIL and older Pillow versions -except ImportError: - Image = None - ImageDraw = None - -try: - from PIL import ImageCms -except ImportError: - ImageCms = None - - -def tobytes(image): - # Some versions of PIL are missing the tobytes alias for tostring - if hasattr(image, 'tobytes'): - return image.tobytes() - else: - return image.tostring() - - -def extract_layer_image(decoded_data, layer_index): - """ - Converts a layer from the ``decoded_data`` to a PIL image. - """ - layers = decoded_data.layer_and_mask_data.layers - layer = layers.layer_records[layer_index] - - return _channel_data_to_PIL( - channel_data=layers.channel_image_data[layer_index], - channel_ids=_get_layer_channel_ids(layer), - color_mode=decoded_data.header.color_mode, # XXX? - size=(layer.width(), layer.height()), - depth=decoded_data.header.depth, - icc_profile=get_icc_profile(decoded_data) - ) - - -def extract_layer_mask(decoded_data, layer_index, real_mask): - """ - Converts a layer mask from the ``decoded_data`` to a PIL image. - - If ``real_mask`` is True, extract real mask consisting of both bitmap and - vector mask. - """ - layers = decoded_data.layer_and_mask_data.layers - layer = layers.layer_records[layer_index] - mask_data = layer.mask_data - if not mask_data: - return None - real_mask = real_mask and mask_data.real_flags - if real_mask: - size = (mask_data.real_right - mask_data.real_left, - mask_data.real_bottom - mask_data.real_top) - else: - size = (mask_data.right - mask_data.left, - mask_data.bottom - mask_data.top) - - return _mask_data_to_PIL( - channel_data=layers.channel_image_data[layer_index], - channel_ids=_get_layer_channel_ids(layer), - size=size, - depth=decoded_data.header.depth, - real_mask=real_mask - ) - - -def extract_composite_image(decoded_data): - """ - Converts a composite (merged) image from the ``decoded_data`` - to a PIL image. - """ - header = decoded_data.header - size = header.width, header.height - if size == (0, 0): - return - - channel_ids = _get_header_channel_ids(header) - if channel_ids is None: - warnings.warn( - "This number of channels (%d) is unsupported for this color mode" - "(%s)" % (header.number_of_channels, header.color_mode)) - return - if len(channel_ids) > len(decoded_data.image_data): - warnings.warn("Image data is broken") - return - - use_alpha = _get_alpha_use(decoded_data) - image = _channel_data_to_PIL( - channel_data=decoded_data.image_data, - channel_ids=channel_ids, - color_mode=header.color_mode, - size=size, - depth=header.depth, - icc_profile=get_icc_profile(decoded_data), - use_alpha=use_alpha - ) - - # Composed image is blended into white background. Remove here. - return _remove_white_background(image) - - -def get_icc_profile(decoded_data): - """ - Return ICC image profile if it exists and was correctly decoded - """ - # fixme: move this function somewhere? - icc_profiles = [res.data for res in decoded_data.image_resource_blocks - if res.resource_id == ImageResourceID.ICC_PROFILE] - - if not icc_profiles: - return None - - icc_profile = icc_profiles[0] - - if isinstance(icc_profile, bytes): # profile was not decoded - return None - - return icc_profile - - -def apply_opacity(im, opacity): - """ Apply opacity to an image. """ - if im.mode in ('RGBA', 'LA'): - channels = list(im.split()) - opacity_scale = opacity / 255. - channels[-1] = channels[-1].point(lambda i: int(i * opacity_scale)) - return Image.merge(im.mode, channels) - elif im.mode in ('RGB', 'L'): - im.putalpha(opacity) - return im - else: - warnings.warn("%s converted to RGB" % im.mode) - im = im.convert('RGB') - im.putalpha(opacity) - return im - - -def pattern_to_PIL(pattern): - channels = [_decompress_pattern_channel(c) for c in pattern.data.channels] - if not all(channels): - warnings.warn("Failed to decompress pattern") - return None - # Bitmap = 0; Grayscale = 1; Indexed = 2; RGB = 3; CMYK = 4; - # Multichannel = 7; Duotone = 8; Lab = 9 - image_mode = pattern.image_mode - image = None - if image_mode == 1: - assert len(channels) in (1, 2) - image = channels[0] - if len(channels) > 1: - image.putalpha(channels[1]) - elif image_mode == 3: - assert len(channels) in (3, 4) - image = Image.merge('RGB', channels[:3]) - if len(channels) > 3: - image.putalpha(channels[3]) - elif image_mode == 4: - assert len(channels) in (4, 5) - image = Image.merge('CMYK', channels[:4]).convert('RGB') - if len(channels) > 4: - image.putalpha(channels[4]) - else: - warnings.warn("Unsupported image mode %d" % pattern.image_mode) - if len(channels) > 0: - image = channels[0] - return image - - -def draw_polygon(bbox, anchors, mode='RGBA', fill=(255, 255, 255, 255)): - color = { - 'RGBA': (255, 255, 255, 0), - 'LA': (255, 0), - 'CMYKA': (255, 255, 255, 255, 0), - }.get(mode) - image = Image.new(mode, (bbox.width, bbox.height), - color=color) - draw = ImageDraw.Draw(image) - for path in anchors: - draw.polygon(path, fill=fill) - del draw - return image - - -def extract_thumbnail(resource, mode="RGB"): - if resource.format == 0: - size = (resource.width, resource.height) - stride = resource.widthbytes - image = frombytes('RGBX', size, data.value, 'raw', mode, stride) - elif resource.format == 1: - image = Image.open(io.BytesIO(resource.data.value)) - return image - - -def _channel_data_to_PIL(channel_data, channel_ids, color_mode, size, depth, - icc_profile, use_alpha=True): - bands = _get_band_images( - channel_data=channel_data, - channel_ids=channel_ids, - color_mode=color_mode, - size=size, - depth=depth - ) - return _merge_bands(bands, color_mode, size, icc_profile, use_alpha) - - -def _merge_bands(bands, color_mode, size, icc_profile, use_alpha): - if Image is None: - raise Exception("This module requires PIL (or Pillow) installed.") - - if color_mode == ColorMode.RGB: - merged_image = Image.merge('RGB', [bands[key] for key in 'RGB']) - elif color_mode == ColorMode.CMYK: - merged_image = Image.merge('CMYK', [bands[key] for key in 'CMYK']) - merged_bytes = tobytes(merged_image) - # colors are inverted in Photoshop CMYK images; invert them back - merged_image = frombytes('CMYK', size, merged_bytes, 'raw', 'CMYK;I') - elif color_mode == ColorMode.GRAYSCALE: - merged_image = bands['L'] - else: - raise NotImplementedError() - - if icc_profile is not None: - assert ImageCms is not None - try: - if color_mode in [ColorMode.RGB, ColorMode.CMYK]: - merged_image = ImageCms.profileToProfile( - merged_image, icc_profile, icc_profiles.sRGB, - outputMode='RGB') - elif color_mode == ColorMode.GRAYSCALE: - ImageCms.profileToProfile( - merged_image, icc_profile, icc_profiles.gray, - inPlace=True, outputMode='L') - except ImageCms.PyCMSError as e: - # PIL/Pillow/(old littlecms?) can't convert some ICC profiles - warnings.warn(repr(e)) - - if color_mode == ColorMode.CMYK: - merged_image = merged_image.convert('RGB') - - alpha = bands.get('A') - if alpha and use_alpha: - merged_image.putalpha(alpha) - - return merged_image - - -def _get_band_images(channel_data, channel_ids, color_mode, size, depth): - bands = {} - for channel, channel_id in zip(channel_data, channel_ids): - pil_band = _channel_id_to_PIL(channel_id, color_mode) - if pil_band is None: - continue - - im = _decompress_channel(channel, depth, size) - if im: - bands[pil_band] = im - return bands - - -def _mask_data_to_PIL(channel_data, channel_ids, size, depth, real_mask): - target_id = ( - ChannelID.REAL_USER_LAYER_MASK if real_mask - else ChannelID.USER_LAYER_MASK - ) - for channel, channel_id in zip(channel_data, channel_ids): - if channel_id == target_id: - return _decompress_channel(channel, depth, size) - return None - - -def _decompress_channel(channel, depth, size): - if channel.compression in (Compression.RAW, Compression.ZIP, - Compression.ZIP_WITH_PREDICTION): - if depth == 8: - im = _from_8bit_raw(channel.data, size) - elif depth == 16: - im = _from_16bit_raw(channel.data, size) - elif depth == 32: - im = _from_32bit_raw(channel.data, size) - else: - warnings.warn("Unsupported depth (%s)" % depth) - return None - - elif channel.compression == Compression.PACK_BITS: - if depth != 8: - warnings.warn( - "Depth %s is unsupported for PackBits compression" % depth) - im = frombytes('L', size, channel.data, "packbits", 'L') - else: - if Compression.is_known(channel.compression): - warnings.warn( - "Compression method is not implemented " - "(%s)" % channel.compression) - else: - warnings.warn( - "Unknown compression method (%s)" % channel.compression) - return None - return im.convert('L') - - -def _decompress_pattern_channel(channel): - depth = channel.depth - size = (channel.rectangle[3], channel.rectangle[2]) - if channel.compression in (Compression.RAW, Compression.ZIP, - Compression.ZIP_WITH_PREDICTION): - if depth == 8: - im = _from_8bit_raw(channel.data.value, size) - elif depth == 16: - im = _from_16bit_raw(channel.data.value, size) - elif depth == 32: - im = _from_32bit_raw(channel.data.value, size) - else: - warnings.warn("Unsupported depth (%s)" % depth) - return None - elif channel.compression == Compression.PACK_BITS: - if depth != 8: - warnings.warn( - "Depth %s is unsupported for PackBits compression" % depth) - try: - import packbits - channel_data = packbits.decode(channel.data.value) - except ImportError as e: - warnings.warn("Install packbits (%s)" % e) - channel_data = b'\x00' * (size[0] * size[1]) # Default fill - except IndexError as e: - warnings.warn("Failed to decode pattern (%s)" % e) - channel_data = b'\x00' * (size[0] * size[1]) # Default fill - # Packbit pattern tends not to have the correct size ??? - padding = len(channel_data) - size[0] * size[1] - if padding < 0: - warnings.warn('Broken pattern data (%g for %g)' % ( - len(channel_data), size[0] * size[1])) - channel_data += b'\x00' * -padding # Append default fill - padding = 0 - im = frombytes('L', size, channel_data[padding:], "raw", 'L') - else: - if Compression.is_known(channel.compression): - warnings.warn( - "Compression method is not implemented " - "(%s)" % channel.compression) - else: - warnings.warn( - "Unknown compression method (%s)" % channel.compression) - return None - return im.convert('L') - - -def _from_8bit_raw(data, size): - return frombytes('L', size, data, "raw", 'L') - - -def _from_16bit_raw(data, size): - im = frombytes('I', size, data, "raw", 'I;16B') - return im.point(lambda i: i * (1/(256.0))) - - -def _from_32bit_raw(data, size): - pixels = be_array_from_bytes("f", data) - im = Image.new("F", size) - im.putdata(pixels, 255, 0) - return im - - -def _channel_id_to_PIL(channel_id, color_mode): - if ChannelID.is_known(channel_id): - if channel_id == ChannelID.TRANSPARENCY_MASK: - return 'A' - elif channel_id in (ChannelID.USER_LAYER_MASK, - ChannelID.REAL_USER_LAYER_MASK): - return None - return None - - try: - assert channel_id >= 0 - if color_mode == ColorMode.RGB: - return 'RGB'[channel_id] - elif color_mode == ColorMode.CMYK: - return 'CMYK'[channel_id] - elif color_mode == ColorMode.GRAYSCALE: - return 'L'[channel_id] - - except IndexError: - # spot channel - warnings.warn("Spot channel %s is not handled" % channel_id) - return None - - -def _get_header_channel_ids(header): - - if header.color_mode == ColorMode.RGB: - if header.number_of_channels == 3: - return [0, 1, 2] - elif header.number_of_channels >= 4: - if header.number_of_channels > 4: - warnings.warn("header.number_of_channels = %d > 4" % ( - header.number_of_channels)) - return [0, 1, 2, ChannelID.TRANSPARENCY_MASK] - - elif header.color_mode == ColorMode.CMYK: - if header.number_of_channels == 4: - return [0, 1, 2, 3] - elif header.number_of_channels >= 5: - # XXX: how to distinguish - # "4 CMYK + 1 alpha" and "4 CMYK + 1 spot"? - if header.number_of_channels > 5: - warnings.warn("header.number_of_channels = %d > 5" % ( - header.number_of_channels)) - return [0, 1, 2, 3, ChannelID.TRANSPARENCY_MASK] - - elif header.color_mode == ColorMode.GRAYSCALE: - if header.number_of_channels == 1: - return [0] - elif header.number_of_channels >= 2: - if header.number_of_channels > 2: - warnings.warn("header.number_of_channels = %d > 2" % ( - header.number_of_channels)) - return [0, ChannelID.TRANSPARENCY_MASK] - - else: - warnings.warn("Unsupported color mode (%s)" % header.color_mode) - - -def _get_layer_channel_ids(layer): - return [info.id for info in layer.channels] - - -def _get_alpha_use(decoded_data): - use_alpha = decoded_data.layer_and_mask_data.layers.layer_count < 0 - keys = { - TaggedBlock.SAVING_MERGED_TRANSPARENCY, - TaggedBlock.SAVING_MERGED_TRANSPARENCY16, - TaggedBlock.SAVING_MERGED_TRANSPARENCY32, - } - tagged_blocks = decoded_data.layer_and_mask_data.tagged_blocks - use_alpha |= any(block.key in keys for block in tagged_blocks) - return use_alpha - - -def _remove_white_background(image): - """Remove white background in the preview image.""" - if image.mode == "RGBA": - bands = image.split() - a = bands[3] - rgb = [ - ImageMath.eval( - 'convert(' - 'float(x + a - 255) * 255.0 / float(max(a, 1)) * ' - 'float(min(a, 1)) + float(x) * float(1 - min(a, 1))' - ', "L")', - x=x, a=a - ) - for x in bands[:3] - ] - return Image.merge(bands=rgb + [a], mode="RGBA") - - return image diff --git a/src/psd_tools/user_api/psd_image.py b/src/psd_tools/user_api/psd_image.py deleted file mode 100644 index a8388ea0..00000000 --- a/src/psd_tools/user_api/psd_image.py +++ /dev/null @@ -1,380 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, print_function - -import logging -# import weakref # FIXME: there should be weakrefs in this module -import psd_tools.reader -import psd_tools.decoder -from psd_tools.constants import TaggedBlock, SectionDivider, ImageResourceID -from psd_tools.user_api import pymaging_support -from psd_tools.user_api import pil_support -from psd_tools.user_api import BBox, Pattern -from psd_tools.user_api.smart_object import SmartObject -from psd_tools.user_api.layers import ( - Group, AdjustmentLayer, TypeLayer, ShapeLayer, SmartObjectLayer, - PixelLayer, _TaggedBlockMixin, _GroupMixin) - -logger = logging.getLogger(__name__) - - -class _PSDImageBuilder(object): - """Mixin for PSDImage building.""" - - def build(self, decoded_data): - """Build the tree structure.""" - self.decoded_data = decoded_data - layer_records = decoded_data.layer_and_mask_data.layers.layer_records - - group_stack = [self] - clip_stack = [] - - for index, record in reversed(list(enumerate(layer_records))): - current_group = group_stack[-1] - blocks = dict(record.tagged_blocks) - - divider = blocks.get( - TaggedBlock.SECTION_DIVIDER_SETTING, - blocks.get(TaggedBlock.NESTED_SECTION_DIVIDER_SETTING), - ) - if divider: - if divider.type in (SectionDivider.CLOSED_FOLDER, - SectionDivider.OPEN_FOLDER): - layer = Group(current_group, index) - group_stack.append(layer) - - elif divider.type == SectionDivider.BOUNDING_SECTION_DIVIDER: - if len(group_stack) == 1: - # This means that there is a BOUNDING_SECTION_DIVIDER - # without an OPEN_FOLDER before it. Create a new group - # and move layers to this new group in this case. - - # Assume the first layer is a group - # and convert it to a group: - layers = group_stack[0].layers[0] - group = Group(current_group, layers[0]._index) - group._layers = layers[1:] - - # replace moved layers with newly created group: - group_stack[0].layers = [group] - else: - assert group_stack.pop() is not self - continue - else: - logger.warning("Invalid state") - - elif TaggedBlock.TYPE_TOOL_OBJECT_SETTING in blocks: - layer = TypeLayer(current_group, index) - - elif ((TaggedBlock.VECTOR_ORIGINATION_DATA in blocks or - TaggedBlock.VECTOR_MASK_SETTING1 in blocks or - TaggedBlock.VECTOR_MASK_SETTING2 in blocks or - TaggedBlock.VECTOR_STROKE_DATA in blocks or - TaggedBlock.VECTOR_STROKE_CONTENT_DATA in blocks) and - record.flags.pixel_data_irrelevant): - layer = ShapeLayer(current_group, index) - - elif (TaggedBlock.SMART_OBJECT_PLACED_LAYER_DATA in blocks or - TaggedBlock.PLACED_LAYER_OBSOLETE2 in blocks or - TaggedBlock.PLACED_LAYER_DATA in blocks): - layer = SmartObjectLayer(current_group, index) - - elif any([TaggedBlock.is_adjustment_key(key) or - TaggedBlock.is_fill_key(key) - for key in blocks.keys()]): - layer = AdjustmentLayer(current_group, index) - - else: - layer = PixelLayer(current_group, index) - - if record.clipping: - clip_stack.append(layer) - else: - layer._clip_layers = clip_stack - clip_stack = [] - current_group._layers.append(layer) - - -class PSDImage(_TaggedBlockMixin, _GroupMixin, _PSDImageBuilder): - """ - PSD image. - - The internal layers are accessible with - :py:attr:`~psd_tools.PSDImage.layers` attribute. - - Example:: - - from psd_tools import PSDImage - psd = PSDImage.load("path/to/example.psd") - psd.print_tree() - - for layer in psd.layers: - print(layer.kind) - - image = psd.as_PIL() - - .. py:attribute:: decoded_data - - Low-level document structure from :py:mod:`~psd_tools.decoder`. - - """ - - def __init__(self, decoded_data): - self._psd = self - self._tagged_blocks = None - self._layers = [] - self._bbox = None - self._smart_objects = None - self._patterns = None - self._image_resource_blocks = None - self.build(decoded_data) - - @classmethod - def load(cls, path, encoding='utf8'): - """Returns a new :class:`PSDImage` loaded from ``path``.""" - with open(path, 'rb') as fp: - return cls.from_stream(fp, encoding) - - @classmethod - def from_stream(cls, fp, encoding='utf8'): - """Returns a new :class:`PSDImage` loaded from stream ``fp``.""" - decoded_data = psd_tools.decoder.parse( - psd_tools.reader.parse(fp, encoding) - ) - return cls(decoded_data) - - def as_PIL(self, render=False, **kwargs): - """ - Returns a PIL image for this PSD file. - - :param render: Force rendering the view if True - :returns: `PIL.Image` - """ - if not render and self.has_preview(): - image = pil_support.extract_composite_image(self.decoded_data) - if image: - return image - return super(PSDImage, self).as_PIL(bbox=self.viewbox, **kwargs) - - def as_PIL_merged(self, **kwargs): - """ - (Deprecated) Returns a PIL image with forced rendering. - """ - return self.as_PIL(render=True, **kwargs) - - def has_box(self): - """Return True if the layer has a nonzero area.""" - return self.width > 0 and self.height > 0 - - def has_preview(self): - """Returns if the image has a preview. - - PSD files may contain a preview for compatibility. If a preview - exists, PSDImage exports the preview as is in :py:attr:`as_PIL()`. - """ - version_info = self.image_resource_blocks.get("version_info") - return self.has_pixels() and ( - not version_info or version_info.has_real_merged_data) - - def has_pixels(self): - """Return True if the image has associated pixels.""" - return all(c.data and len(c.data) > 0 - for c in self.decoded_data.image_data) - - def as_pymaging(self): - """Returns a pymaging.Image for this PSD file.""" - return pymaging_support.extract_composite_image(self.decoded_data) - - @property - def name(self): - """Layer name as unicode. PSDImage is 'root'.""" - return "root" - - @property - def visible(self): - """ - Visiblity flag of this layer. PSDImage is always visible. - - :returns: True - """ - return True - - def is_visible(self): - """ - Layer visibility. PSDImage is always visible. - - :returns: True - """ - return True - - @property - def left(self): - """Left coordinate (0).""" - return 0 - - @property - def right(self): - """Right coordinate (width).""" - return self.width - - @property - def top(self): - """Top coordinate (0).""" - return 0 - - @property - def bottom(self): - """Bottom coordinate (height).""" - return self.height - - @property - def width(self): - """Width of the image.""" - return self.decoded_data.header.width - - @property - def height(self): - """Height of the image.""" - return self.decoded_data.header.height - - @property - def bbox(self): - """ - Return BBox enclosing all layers, or viewbox if there is no layer. - """ - bbox = super(PSDImage, self).bbox - return self.viewbox if bbox.is_empty() else bbox - - @property - def viewbox(self): - """Return BBox of the viewport.""" - return BBox(0, 0, self.width, self.height) - - @property - def depth(self): - """Depth of colors.""" - return self.decoded_data.header.depth - - @property - def channels(self): - """Number of color channels.""" - return self.decoded_data.header.number_of_channels - - @property - def embedded(self): - """(Deprecated) Use `smart_objects`.""" - return self.smart_objects - - @property - def smart_objects(self): - """Dict of the smart objects.""" - if not self._smart_objects: - links = self.get_tag([ - TaggedBlock.LINKED_LAYER1, - TaggedBlock.LINKED_LAYER2, - TaggedBlock.LINKED_LAYER3, - TaggedBlock.LINKED_LAYER_EXTERNAL - ]) - if links: - self._smart_objects = {item.unique_id: SmartObject(item) - for item in links.linked_list} - else: - self._smart_objects = {} - return self._smart_objects - - @property - def patterns(self): - """Returns a dict of pattern (texture) data in PIL.Image.""" - if not self._patterns: - patterns = self.get_tag([TaggedBlock.PATTERNS1, - TaggedBlock.PATTERNS2, - TaggedBlock.PATTERNS3], - []) - self._patterns = {p.pattern_id: Pattern(p) for p in patterns} - return self._patterns - - def print_tree(self, layers=None, indent=0, indent_width=2, **kwargs): - """Print the layer tree structure.""" - if layers is None: - layers = self.layers - print(((' ' * indent) + "{}").format(self), **kwargs) - indent = indent + indent_width - for l in layers: - for clip in l.clip_layers: - print(((' ' * indent) + "/{}").format(clip), **kwargs) - print(((' ' * indent) + "{}").format(l), **kwargs) - if l.is_group(): - self.print_tree(l.layers, indent + indent_width, **kwargs) - - def has_thumbnail(self): - """True if the PSDImage has a thumbnail resource.""" - return ("thumbnail_resource" in self.image_resource_blocks or - "thumbnail_resource_ps4" in self.image_resource_blocks) - - def thumbnail(self): - """ - Returns a thumbnail image in PIL.Image. When the file does not - contain an embedded thumbnail image, returns None. - """ - if "thumbnail_resource" in self.image_resource_blocks: - return pil_support.extract_thumbnail( - self.image_resource_blocks["thumbnail_resource"]) - elif "thumbnail_resource_ps4" in self.image_resource_blocks: - return pil_support.extract_thumbnail( - self.image_resource_blocks["thumbnail_resource_ps4"], "BGR") - return None - - @property - def header(self): - """ - Header section of the underlying PSD data. - - :rtype: psd_tools.reader.header.PsdHeader - """ - return self.decoded_data.header - - @property - def tagged_blocks(self): - """ - Returns dict of the underlying tagged blocks. See - :py:mod:`psd_tools.decoder.tagged_blocks` - - :rtype: `dict` - """ - if not self._tagged_blocks: - self._tagged_blocks = dict( - self.decoded_data.layer_and_mask_data.tagged_blocks) - return self._tagged_blocks - - @property - def image_resource_blocks(self): - """ - Returns dict of the underlying image resource blocks. See - :py:mod:`psd_tools.decoder.image_resources` - - :rtype: `dict` - """ - if not self._image_resource_blocks: - self._image_resource_blocks = { - ImageResourceID.name_of(block.resource_id).lower(): block.data - for block in self.decoded_data.image_resource_blocks - } - return self._image_resource_blocks - - def _layer_records(self, index): - records = self.decoded_data.layer_and_mask_data.layers.layer_records - return records[index] - - def _layer_channels(self, index): - data = self.decoded_data.layer_and_mask_data.layers.channel_image_data - return data[index] - - def _layer_as_PIL(self, index): - return pil_support.extract_layer_image(self.decoded_data, index) - - def _layer_as_pymaging(self, index): - return pymaging_support.extract_layer_image(self.decoded_data, index) - - def __repr__(self): - return "<%s: size=%dx%d, layer_count=%d>" % ( - self.__class__.__name__.lower(), - self.width, self.height, len(self.layers)) diff --git a/src/psd_tools/user_api/pymaging_support.py b/src/psd_tools/user_api/pymaging_support.py deleted file mode 100644 index 729edaf6..00000000 --- a/src/psd_tools/user_api/pymaging_support.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import array - -try: - import packbits - from pymaging.image import LoadedImage - from pymaging.colors import RGB, RGBA - from pymaging.pixelarray import get_pixel_array -except ImportError: - LoadedImage = None - packbits = None - -from psd_tools.constants import ColorMode, Compression, ChannelID - - -def extract_composite_image(decoded_data): - """ - Converts a composite (merged) image from the ``decoded_data`` - to a pymaging.Image. - """ - header = decoded_data.header - size = header.width, header.height - depth, mode = _validate_header(header) - - return _channels_data_to_image(decoded_data.image_data, mode, size, depth) - - -def extract_layer_image(decoded_data, layer_index): - """ - Converts a layer from the ``decoded_data`` to a ``pymaging.Image``. - """ - layers = decoded_data.layer_and_mask_data.layers - layer = layers.layer_records[layer_index] - - channels_data = layers.channel_image_data[layer_index] - channel_types = [info.id for info in layer.channels] - size = layer.width(), layer.height() - - depth, _ = _validate_header(decoded_data.header) - - # FIXME: support for layers with mask (there would be 5 channels) - if channel_types[0] == ChannelID.TRANSPARENCY_MASK: - # move alpha channel to the end - channels_data = [channels_data[i] for i in [1, 2, 3, 0]] - - mode = _get_mode(len(channels_data)) - return _channels_data_to_image(channels_data, mode, size, depth) - - -def _channels_data_to_image(channels_data, mode, size, depth): - - if size == (0, 0): - return - - w, h = size - num_channels = mode.length - assert depth == 8 - assert len(channels_data) == num_channels - - total_size = w*h*num_channels - image_bytes = array.array(str("B"), [0]*total_size) - - for index, channel in enumerate(channels_data): - - # zip and zip-with-prediction data is already decoded - data = channel.data - if channel.compression == Compression.PACK_BITS: - data = packbits.decode(data) - - image_bytes[index::num_channels] = array.array(str("B"), data) - - pixels = get_pixel_array(image_bytes, w, h, mode.length) - - return LoadedImage(mode, w, h, pixels) - - -def _get_mode(number_of_channels): - mode = None - if number_of_channels == 3: - mode = RGB - elif number_of_channels == 4: - mode = RGBA - return mode - - -def _validate_header(header): - """ - Validates header and returns (depth, mode) tuple. - """ - if LoadedImage is None or packbits is None: - raise Exception( - "This module requires `pymaging` and `packbits` packages.") - - if header.color_mode != ColorMode.RGB: - raise NotImplementedError( - "This color mode (%s) is not supported yet" % - ColorMode.name_of(header.color_mode) - ) - - mode = _get_mode(header.number_of_channels) - if mode is None: - raise NotImplementedError( - "This number of channels (%d) is unsupported for this color mode" - " (%s)" % (header.number_of_channels, header.color_mode)) - - if header.depth != 8: - raise NotImplementedError( - "Only 8bit images are currently supported with pymaging.") - - return 8, mode diff --git a/src/psd_tools/user_api/shape.py b/src/psd_tools/user_api/shape.py deleted file mode 100644 index 292389a7..00000000 --- a/src/psd_tools/user_api/shape.py +++ /dev/null @@ -1,338 +0,0 @@ -# -*- coding: utf-8 -*- -"""Shape layer API.""" - -from __future__ import absolute_import -import logging -from psd_tools.debug import pretty_namedtuple -from psd_tools.constants import TaggedBlock, PathResource -from psd_tools.decoder.actions import UnitFloat - -logger = logging.getLogger(__name__) - - -class StrokeStyle(object): - """StrokeStyle contains decorative infromation for strokes.""" - STROKE_STYLE_LINE_CAP_TYPES = { - b'strokeStyleButtCap': 'butt', - b'strokeStyleRoundCap': 'round', - b'strokeStyleSquareCap': 'square', - } - - STROKE_STYLE_LINE_JOIN_TYPES = { - b'strokeStyleMiterJoin': 'miter', - b'strokeStyleRoundJoin': 'round', - b'strokeStyleBevelJoin': 'bevel', - } - - STROKE_STYLE_LINE_ALIGNMENTS = { - b'strokeStyleAlignInside': 'inner', - b'strokeStyleAlignOutside': 'outer', - b'strokeStyleAlignCenter': 'center', - } - - def __init__(self, descriptor): - self._descriptor = descriptor - assert self.get(b'classID') == b'strokeStyle' - - def get(self, key, default=None): - return self._descriptor.get(key, default) - - @property - def enabled(self): - """If the stroke is enabled.""" - return self.get(b'strokeEnabled') - - @property - def fill_enabled(self): - """If the stroke fill is enabled.""" - return self.get(b'fillEnabled') - - @property - def line_width(self): - """Stroke width in float. - - :rtype: :py:class:`~psd_tools.decoder.actions.UnitFloat` - """ - return self.get(b'strokeStyleLineWidth', UnitFloat('PIXELS', 1.0)) - - @property - def line_dash_set(self): - """ - Line dash set in list of - :py:class:`~psd_tools.decoder.actions.UnitFloat`. - - :rtype: list - """ - return self.get(b'strokeStyleLineDashSet') - - @property - def line_dash_offset(self): - """ - Line dash offset in float. - - :rtype: float - """ - return self.get(b'strokeStyleLineDashOffset', 0.0) - - @property - def miter_limit(self): - """Miter limit in float.""" - return self.get(b'strokeStyleMiterLimit', 100.0) - - @property - def line_cap_type(self): - """Cap type, one of `butt`, `round`, `square`.""" - key = self.get(b'strokeStyleLineCapType') - return self.STROKE_STYLE_LINE_CAP_TYPES.get(key, str(key)) - - @property - def line_join_type(self): - """Join type, one of `miter`, `round`, `bevel`.""" - key = self.get(b'strokeStyleLineJoinType') - return self.STROKE_STYLE_LINE_JOIN_TYPES.get(key, str(key)) - - @property - def line_alignment(self): - """Alignment, one of `inner`, `outer`, `center`.""" - key = self.get(b'strokeStyleLineAlignment') - return self.STROKE_STYLE_LINE_ALIGNMENTS.get(key, str(key)) - - @property - def scale_lock(self): - return self.get(b'strokeStyleScaleLock') - - @property - def stroke_adjust(self): - """Stroke adjust""" - return self.get(b'strokeStyleStrokeAdjust') - - @property - def blend_mode(self): - """Blend mode.""" - return self.get(b'strokeStyleBlendMode') - - @property - def opacity(self): - """Opacity value. - - :rtype: :py:class:`~psd_tools.decoder.actions.UnitFloat` - """ - return self.get(b'strokeStyleOpacity', UnitFloat('PERCENT', 100.0)) - - @property - def content(self): - """ - Fill effect, one of - :py:class:`~psd_tools.user_api.effects.ColorOverlay`, - :py:class:`~psd_tools.user_api.effects.PatternOverlay`, - or :py:class:`~psd_tools.user_api.effects.GradientOverlay`. - - :rtype: :py:class:`~psd_tools.user_api.effects._OverlayEffect` - """ - return self.get(b'strokeStyleContent') - - def __repr__(self): - return self._descriptor.__repr__() - - -Path = pretty_namedtuple("Path", "closed num_knots knots") -Knot = pretty_namedtuple("Knot", "anchor leaving_knot preceding_knot") - - -class VectorMask(object): - """Shape path data.""" - _KNOT_KEYS = ( - PathResource.CLOSED_SUBPATH_BEZIER_KNOT_LINKED, - PathResource.CLOSED_SUBPATH_BEZIER_KNOT_UNLINKED, - PathResource.OPEN_SUBPATH_BEZIER_KNOT_LINKED, - PathResource.OPEN_SUBPATH_BEZIER_KNOT_UNLINKED, - ) - - def __init__(self, setting): - self._setting = setting - self._paths = [] - self._build() - - def _build(self): - for p in self._setting.path: - selector = p.get('selector') - if selector == PathResource.CLOSED_SUBPATH_LENGTH_RECORD: - self._paths.append(Path(True, p.get('num_knot_records'), [])) - elif selector == PathResource.OPEN_SUBPATH_LENGTH_RECORD: - self._paths.append(Path(False, p.get('num_knot_records'), [])) - elif selector in self._KNOT_KEYS: - knot = Knot(p.get('anchor'), - p.get('control_leaving_knot'), - p.get('control_preceding_knot')) - self._paths[-1].knots.append(knot) - elif selector == PathResource.PATH_FILL_RULE_RECORD: - pass - elif selector == PathResource.CLIPBOARD_RECORD: - self._clipboard_record = p - elif selector == PathResource.INITIAL_FILL_RULE_RECORD: - self._initial_fill_rule = p.get('initial_fill_rule', 0) - for path in self.paths: - assert path.num_knots == len(path.knots) - - @property - def invert(self): - """Invert the mask.""" - return self._setting.invert - - @property - def not_link(self): - """If the knots are not linked.""" - return self._setting.not_link - - @property - def disabled(self): - """If the mask is disabled.""" - return self._setting.disable - - @property - def paths(self): - """ - List of `Path`. Path contains `closed`, `num_knots`, and `knots`. - In PSD, path fill rule is even-odd for multiple paths. - - :return: Path named tuples - """ - return self._paths - - @property - def initial_fill_rule(self): - """ - Initial fill rule. - - When 0, fill inside of the path. When 1, fill outside of the shape. - """ - return self._initial_fill_rule - - @property - def anchors(self): - """List of vertices of all subpaths.""" - return [ - [knot.anchor for knot in path.knots] - for path in self.paths - ] - - -class Origination(object): - """Vector origination. - - Vector origination keeps live shape properties such as bounding box. - """ - def __init__(self, data): - self._descriptor = data - - @property - def origin_type(self): - """Type of the origination. - - * 1: rectangle - * 2: rounded rectangle - * 4: line - * 5: ellipse - - :rtype: int - """ - return self._descriptor.get(b'keyOriginType') - - @property - def resolution(self): - """Resolution. - - :rtype: float - """ - return self._descriptor.get(b'keyOriginResolution') - - @property - def radii(self): - """Corner radii of rounded rectangles. - The order is top-left, top-right, bottom-left, bottom-right. - - :rtype: tuple of float - """ - return self._descriptor.get(b'keyOriginRRectRadii') - - @property - def shape_bbox(self): - """Bounding box of the live shape. - - :rtype: BBox - """ - return self._descriptor.get(b'keyOriginShapeBBox') - - @property - def line_end(self): - """Line end. - - :rtype: Point - """ - return self._descriptor.get(b'keyOriginLineEnd') - - @property - def line_start(self): - """Line start. - - :rtype: Point - """ - return self._descriptor.get(b'keyOriginLineStart') - - @property - def line_weight(self): - """Line weight - - :rtype: float - """ - return self._descriptor.get(b'keyOriginLineWeight') - - @property - def arrow_start(self): - """Line arrow start. - - :rtype: bool - """ - return self._descriptor.get(b'keyOriginLineArrowSt') - - @property - def arrow_end(self): - """Line arrow end. - - :rtype: bool""" - return self._descriptor.get(b'keyOriginLineArrowEnd') - - @property - def arrow_width(self): - """Line arrow width. - - :rtype: float - """ - return self._descriptor.get(b'keyOriginLineArrWdth') - - @property - def arrow_length(self): - """Line arrow length. - - :rtype: float - """ - return self._descriptor.get(b'keyOriginLineArrLngth') - - @property - def arrow_conc(self): - """ - - :rtype: int - """ - return self._descriptor.get(b'keyOriginLineArrConc') - - @property - def index(self): - """ - - :rtype: int - """ - return self._descriptor.get(b'keyOriginIndex') - - def __repr__(self): - return self._descriptor.__repr__() diff --git a/src/psd_tools/user_api/smart_object.py b/src/psd_tools/user_api/smart_object.py deleted file mode 100644 index 0b49c6f1..00000000 --- a/src/psd_tools/user_api/smart_object.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals, division - -import os -import logging -from psd_tools.constants import LinkedLayerType - - -class SmartObject(object): - """Embedded smart object.""" - - def __init__(self, linked_layer): - self._linked_layer = linked_layer - - @property - def kind(self): - """Kind of the object, one of `data`, `external`, `alias`.""" - return LinkedLayerType.human_name_of(self._linked_layer.type) - - @property - def filename(self): - """Original file name of the object.""" - return self._linked_layer.filename.strip("\0x0") - - @property - def data(self): - """Embedded file content, or empty if kind is `external` or `alias`""" - return self._linked_layer.decoded - - @property - def unique_id(self): - """UUID of the object.""" - return self._linked_layer.unique_id - - @property - def filesize(self): - """File size of the object.""" - return self._linked_layer.filesize - - def preferred_extension(self): - """Preferred file extension, such as `jpg`.""" - return self._linked_layer.filetype.lower().strip() - - def is_psd(self): - """Return True if the file is embedded PSD/PSB.""" - return self.preferred_extension() in (b"8bpb", b"8bps") - - def save(self, filename=None): - """ - Save the embedded file. - - :param filename: File name to export. If None, use the embedded name. - """ - if filename is None: - filename = self.filename - with open(filename, 'wb') as f: - f.write(self.data) - - def __repr__(self): - return "<%r, type=%s, %s bytes>" % ( - self.filename, self.kind, len(self.data)) diff --git a/src/psd_tools/utils.py b/src/psd_tools/utils.py deleted file mode 100644 index da54edb3..00000000 --- a/src/psd_tools/utils.py +++ /dev/null @@ -1,132 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import ( - absolute_import, division, unicode_literals, print_function -) -import sys -import struct -import array - -try: - unichr = unichr -except NameError: - unichr = chr - - -def pack(fmt, *args): - fmt = str(">" + fmt) - return struct.pack(fmt, *args) - - -def unpack(fmt, data): - fmt = str(">" + fmt) - return struct.unpack(fmt, data) - - -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 write_fmt(fp, fmt, *args): - """ - Writes data to ``fp`` according to ``fmt``. - """ - fp.write(pack(fmt, *args)) - - -def pad(number, divisor): - if number % divisor: - number = (number // divisor + 1) * divisor - return number - - -def read_pascal_string(fp, encoding, padding=1): - length = read_fmt("B", fp)[0] - if length == 0: - fp.seek(padding-1, 1) - return '' - - res = fp.read(length) - # -1 accounts for the length byte - padded_length = pad(length+1, padding) - 1 - fp.seek(padded_length - length, 1) - return res.decode(encoding, 'replace') - - -def read_unicode_string(fp): - num_chars = read_fmt("I", fp)[0] - data = fp.read(num_chars*2) - chars = be_array_from_bytes("H", data) - return "".join(unichr(num) for num in chars) - - -def read_be_array(fmt, count, fp): - """ - Reads an array from a file with big-endian data. - """ - arr = array.array(str(fmt)) - if hasattr(arr, 'frombytes'): - arr.frombytes(fp.read(count * arr.itemsize)) - else: - arr.fromstring(fp.read(count * arr.itemsize)) - return fix_byteorder(arr) - - -def fix_byteorder(arr): - """ - Fixes the byte order of the array (assuming it was read - from a Big Endian data). - """ - if sys.byteorder == 'little': - arr.byteswap() - return arr - - -def be_array_from_bytes(fmt, data): - """ - Reads an array from bytestring with big-endian data. - """ - arr = array.array(str(fmt), data) - return fix_byteorder(arr) - - -def trimmed_repr(data, trim_length=30): - if isinstance(data, bytes): - if len(data) > trim_length: - return repr( - data[:trim_length] + b' ... =' + - str(len(data)).encode('ascii') - ) - return repr(data) - - -def synchronize(fp, limit=8): - # This is a hack for the cases where I gave up understanding PSD format. - signature_list = (b'8BIM', b'8B64') - - start = fp.tell() - data = fp.read(limit) - - for signature in signature_list: - pos = data.find(signature) - if pos != -1: - fp.seek(start+pos) - return True - - fp.seek(start) - return False - - -def decode_fixed_point_32bit(data): - """ - Decodes ``data`` as an unsigned 4-byte fixed-point number. - """ - lo, hi = unpack("2H", data) - # XXX: shouldn't denominator be 2**16 ? - return lo + hi / (2**16 - 1) diff --git a/src/psd_tools/version.py b/src/psd_tools/version.py deleted file mode 100644 index bd9676d5..00000000 --- a/src/psd_tools/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '1.7.30' diff --git a/tests/psd_tools/__init__.py b/tests/psd_tools/__init__.py deleted file mode 100644 index 139759b6..00000000 --- a/tests/psd_tools/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import diff --git a/tests/psd_tools/reader/test_color_mode_data.py b/tests/psd_tools/reader/test_color_mode_data.py deleted file mode 100644 index 684c5f1c..00000000 --- a/tests/psd_tools/reader/test_color_mode_data.py +++ /dev/null @@ -1,12 +0,0 @@ -from io import BytesIO -from psd_tools.reader.color_mode_data import read, write - - -def test_read_write(): - data = b'xxxxxxxxxxxxxxxx' - with BytesIO() as f: - write(f, data) - f.flush() - f.seek(0) - data2 = read(f) - assert data == data2 diff --git a/tests/psd_tools/reader/test_header.py b/tests/psd_tools/reader/test_header.py deleted file mode 100644 index 44213d3f..00000000 --- a/tests/psd_tools/reader/test_header.py +++ /dev/null @@ -1,14 +0,0 @@ -from io import BytesIO -from psd_tools.reader.header import PsdHeader, read, write -from psd_tools.constants import ColorMode - - -def test_read_write(): - header = PsdHeader(1, 3, 120, 180, 8, ColorMode.RGB) - with BytesIO() as f: - write(f, header) - f.flush() - f.seek(0) - header2 = read(f) - for i in range(len(header)): - assert header[i] == header2[i] diff --git a/tests/psd_tools/test_adjustments.py b/tests/psd_tools/test_adjustments.py deleted file mode 100644 index fe82792b..00000000 --- a/tests/psd_tools/test_adjustments.py +++ /dev/null @@ -1,203 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -import pytest -from psd_tools.constants import TaggedBlock -from psd_tools.user_api.psd_image import PSDImage -from psd_tools.user_api import adjustments -from PIL.Image import Image -from .utils import decode_psd, DATA_PATH - - -@pytest.fixture(scope="module") -def psd(): - return PSDImage(decode_psd('fill_adjustments.psd')) - - -def test_adjustment_types(psd): - assert psd.layers[15].adjustment_type == 'brightness-and-contrast' - assert psd.layers[14].adjustment_type == 'levels' - assert psd.layers[13].adjustment_type == 'curves' - assert psd.layers[12].adjustment_type == 'exposure' - assert psd.layers[11].adjustment_type == 'vibrance' - assert psd.layers[10].adjustment_type == 'hue-saturation' - assert psd.layers[9].adjustment_type == 'color-balance' - assert psd.layers[8].adjustment_type == 'black-and-white' - assert psd.layers[7].adjustment_type == 'photo-filter' - assert psd.layers[6].adjustment_type == 'channel-mixer' - assert psd.layers[5].adjustment_type == 'color-lookup' - assert psd.layers[4].adjustment_type == 'invert' - assert psd.layers[3].adjustment_type == 'posterize' - assert psd.layers[2].adjustment_type == 'threshold' - assert psd.layers[1].adjustment_type == 'selective-color' - assert psd.layers[0].adjustment_type == 'gradient-map' - - -def test_brightness_contrast(psd): - data = psd.layers[15].data - assert isinstance(data, adjustments.BrightnessContrast) - assert data.brightness == 34 - assert data.contrast == 18 - assert data.mean == 127 - assert data.use_legacy is False - assert data.automatic is False - - -def test_levels(psd): - data = psd.layers[14].data - assert isinstance(data, adjustments.Levels) - assert data.master - - -def test_curves(psd): - data = psd.layers[13].data - assert isinstance(data, adjustments.Curves) - assert data.data - assert data.count == len(data.data) - assert data.extra - - -def test_exposure(psd): - data = psd.layers[12].data - assert isinstance(data, adjustments.Exposure) - assert pytest.approx(data.exposure) == -0.39 - assert pytest.approx(data.offset) == 0.0168 - assert pytest.approx(data.gamma) == 0.91 - - -def test_vibrance(psd): - data = psd.layers[11].data - assert isinstance(data, adjustments.Vibrance) - assert data.vibrance == -6 - assert data.saturation == 2 - - -def test_hue_saturation(psd): - data = psd.layers[10].data - assert isinstance(data, adjustments.HueSaturation) - assert data.enable_colorization == 0 - assert data.colorization == (0, 25, 0) - assert data.master == (-17, 19, 4) - assert len(data.data) == 6 - - -def test_hue_saturation(psd): - data = psd.layers[9].data - assert isinstance(data, adjustments.ColorBalance) - assert data.shadows == (-4, 2, -5) - assert data.midtones == (10, 4, -9) - assert data.highlights == (1, -9, -3) - assert data.preserve_luminosity == 1 - - -def test_black_and_white(psd): - data = psd.layers[8].data - assert isinstance(data, adjustments.BlackWhite) - assert data.red == 40 - assert data.yellow == 60 - assert data.green == 40 - assert data.cyan == 60 - assert data.blue == 20 - assert data.magenta == 80 - assert data.use_tint is False - assert data.tint_color - assert data.preset_kind == 1 - assert data.preset_file_name == '' - - -def test_photo_filter(psd): - data = psd.layers[7].data - assert isinstance(data, adjustments.PhotoFilter) - assert data.xyz is None - assert data.color_space == 7 - assert data.color_components == (6706, 3200, 12000, 0) - assert data.density == 25 - assert data.preserve_luminosity == 1 - - -def test_channel_mixer(psd): - data = psd.layers[6].data - assert isinstance(data, adjustments.ChannelMixer) - assert data.monochrome == 0 - assert data.mixer_settings == (100, 0, 0, 0, 0) - - -def test_color_lookup(psd): - data = psd.layers[5].data - assert isinstance(data, adjustments.ColorLookup) - - -def test_invert(psd): - data = psd.layers[4].data - assert isinstance(data, adjustments.Invert) - - -def test_posterize(psd): - data = psd.layers[3].data - assert isinstance(data, adjustments.Posterize) - assert data.posterize == 4 - - -def test_threshold(psd): - data = psd.layers[2].data - assert isinstance(data, adjustments.Threshold) - assert data.threshold == 128 - - -def test_selective_color(psd): - data = psd.layers[1].data - assert isinstance(data, adjustments.SelectiveColor) - assert data.method == 0 - assert len(data.data) == 10 - - -def test_gradient_map(psd): - data = psd.layers[0].data - assert isinstance(data, adjustments.GradientMap) - assert data.reversed == 0 - assert data.dithered == 0 - assert data.gradient_name == u'Foreground to Background' - assert len(data.color_stops) == 2 - assert len(data.transparency_stops) == 2 - assert data.expansion == 2 - assert data.interpolation == 1.0 - assert data.length == 32 - assert data.mode == 0 - assert data.random_seed == 470415386 - assert data.show_transparency == 0 - assert data.use_vector_color == 1 - assert data.roughness == 2048 - assert data.color_model == 3 - assert data.min_color == (0, 0, 0, 0) - assert data.max_color == (32768, 32768, 32768, 32768) - - -def test_adjustment_and_shapes(): - psd = PSDImage(decode_psd('adjustment-fillers.psd')) - for layer in psd.layers: - if layer.bbox.width: - assert isinstance(layer.as_PIL(), Image) - if layer.kind == "adjustment": - assert layer.adjustment_type - assert layer.data - if layer.kind == "shape": - assert isinstance(layer.get_anchors(), list) - if layer.has_origination(): - assert layer.origination - if layer.has_vector_mask(): - vector_mask = layer.vector_mask - assert vector_mask - if layer.has_path(): - assert len(vector_mask.anchors) > 0 - if layer.has_stroke(): - assert layer.stroke - if layer.has_stroke_content(): - assert layer.stroke_content - - -def test_adjustment_and_shapes(): - psd = PSDImage(decode_psd('adjustment-mask.psd')) - for layer in psd.descendants(): - layer.as_PIL() - if layer.has_mask(): - layer.mask.as_PIL() diff --git a/tests/psd_tools/test_advanced_blending.py b/tests/psd_tools/test_advanced_blending.py deleted file mode 100644 index 807524ff..00000000 --- a/tests/psd_tools/test_advanced_blending.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -import os - -from psd_tools import PSDImage -from psd_tools.constants import TaggedBlock -from psd_tools.decoder.actions import Descriptor -from psd_tools.decoder.tagged_blocks import ArtboardData -from PIL import Image -from .utils import decode_psd, DATA_PATH - - -def test_advanced_blending(): - decoded = decode_psd('advanced-blending.psd') - layer_records = decoded.layer_and_mask_data.layers.layer_records - tagged_blocks = dict(layer_records[1].tagged_blocks) - assert not tagged_blocks.get(TaggedBlock.BLEND_CLIPPING_ELEMENTS) - assert tagged_blocks.get(TaggedBlock.BLEND_INTERIOR_ELEMENTS) - tagged_blocks = dict(layer_records[3].tagged_blocks) - assert isinstance(tagged_blocks.get(TaggedBlock.ARTBOARD_DATA1), - ArtboardData) - - -def test_blend_and_clipping(): - psd = PSDImage(decode_psd('blend-and-clipping.psd')) - for layer in psd.layers: - assert isinstance(layer.as_PIL(), Image.Image) diff --git a/tests/psd_tools/test_binary.py b/tests/psd_tools/test_binary.py deleted file mode 100644 index ce961e09..00000000 --- a/tests/psd_tools/test_binary.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import pytest -from psd_tools.user_api import pil_support, pymaging_support -from .utils import decode_psd, with_psb - - -def _tobytes(image): - if hasattr(image, 'tobytes'): - return image.tobytes() # PIL - elif hasattr(image, 'tostring'): - return image.tostring() # PIL - elif hasattr(image.pixels.data, 'tobytes'): - return image.pixels.data.tobytes() # pymaging - else: - return image.pixels.data.tostring() # pymaging - - -SINGLE_LAYER_FILES = with_psb([ - ['1layer.psd'], - ['transparentbg-gimp.psd'] -]) - -BACKENDS = [[pil_support], [pymaging_support]] - - -@pytest.mark.parametrize(["backend"], BACKENDS) -@pytest.mark.parametrize(["filename"], SINGLE_LAYER_FILES) -def test_single_layer(filename, backend): - psd = decode_psd(filename) - - composite_image = backend.extract_composite_image(psd) - layer_image = backend.extract_layer_image(psd, 0) - - assert len(psd.layer_and_mask_data.layers.layer_records) == 1 - assert _tobytes(layer_image) == _tobytes(composite_image) - assert len(_tobytes(layer_image)) diff --git a/tests/psd_tools/test_composer.py b/tests/psd_tools/test_composer.py deleted file mode 100644 index 62b0dbc0..00000000 --- a/tests/psd_tools/test_composer.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals -import pytest - -from PIL.Image import Image -import imagehash -import numpy as np -from psd_tools.user_api.psd_image import PSDImage -from .utils import decode_psd, full_name - - -CLIP_FILES = [ - ('clipping-mask.psd',), - ('clipping-mask2.psd',) -] - -QUALITY_TEST_FILES = [ - ('mask-index.psd',), - ('background-red-opacity-80.psd',), - ('32bit.psd',), -] - - -@pytest.mark.parametrize(("filename",), CLIP_FILES) -def test_render_clip_layers(filename): - psd = PSDImage.load(full_name(filename)) - image1 = psd.as_PIL() - image2 = psd.as_PIL(render=True) - assert isinstance(image1, Image) - assert isinstance(image2, Image) - - -@pytest.mark.parametrize(("filename",), QUALITY_TEST_FILES) -def test_render_quality(filename): - psd = PSDImage.load(full_name(filename)) - preview = psd.as_PIL() - rendered = psd.as_PIL(render=True) - assert isinstance(preview, Image) - assert isinstance(rendered, Image) - preview_hash = imagehash.average_hash(preview) - rendered_hash = imagehash.average_hash(rendered) - error_count = np.sum( - np.bitwise_xor(preview_hash.hash, rendered_hash.hash)) - error_rate = error_count / float(preview_hash.hash.size) - assert error_rate <= 0.1 diff --git a/tests/psd_tools/test_dimensions.py b/tests/psd_tools/test_dimensions.py deleted file mode 100644 index b9e0a93e..00000000 --- a/tests/psd_tools/test_dimensions.py +++ /dev/null @@ -1,93 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals -import pytest - -from .utils import load_psd, decode_psd, with_psb - -from psd_tools.user_api.psd_image import PSDImage, BBox -from psd_tools.decoder.image_resources import ResolutionInfo -from psd_tools.constants import ( - DisplayResolutionUnit, DimensionUnit, ImageResourceID -) - -DIMENSIONS = with_psb(( - ('1layer.psd', (101, 55)), - ('2layers.psd', (101, 55)), - ('32bit.psd', (100, 150)), - ('300dpi.psd', (100, 150)), - ('clipping-mask.psd', (360, 200)), - ('gradient-fill.psd', (100, 150)), - ('group.psd', (100, 200)), - ('hidden-groups.psd', (100, 200)), - ('hidden-layer.psd', (100, 150)), - ('history.psd', (100, 150)), - ('mask.psd', (100, 150)), - ('note.psd', (300, 300)), - ('pen-text.psd', (300, 300)), - ('smart-object-slice.psd', (100, 100)), - ('transparentbg.psd', (100, 150)), - ('transparentbg-gimp.psd', (40, 40)), - ('vector-mask.psd', (100, 150)), - ('gray0.psd', (400, 359)), - ('gray1.psd', (1800, 1200)), - ('empty-layer.psd', (100, 150)), -)) - -BBOXES = ( - ('1layer.psd', 0, BBox(0, 0, 101, 55)), - ('2layers.psd', 0, BBox(8, 4, 93, 50)), - ('2layers.psd', 1, BBox(0, 0, 101, 55)), - ('group.psd', 0, BBox(25, 24, 66, 98)), - ('empty-layer.psd', 0, BBox(37, 58, 51, 72)), - ('empty-layer.psd', 1, BBox(0, 0, 100, 150)), -) - -RESOLUTIONS = ( - ('1layer.psd', ResolutionInfo( - h_res=72.0, h_res_unit=DisplayResolutionUnit.PIXELS_PER_INCH, - v_res=72.0, v_res_unit=DisplayResolutionUnit.PIXELS_PER_INCH, - width_unit=DimensionUnit.INCH, height_unit=DimensionUnit.INCH)), - ('group.psd', ResolutionInfo( - h_res=72.0, h_res_unit=DisplayResolutionUnit.PIXELS_PER_INCH, - v_res=72.0, v_res_unit=DisplayResolutionUnit.PIXELS_PER_INCH, - width_unit=DimensionUnit.CM, height_unit=DimensionUnit.CM)), - ('1layer.psb', ResolutionInfo( - h_res=72.0, h_res_unit=DisplayResolutionUnit.PIXELS_PER_INCH, - v_res=72.0, v_res_unit=DisplayResolutionUnit.PIXELS_PER_INCH, - width_unit=DimensionUnit.CM, height_unit=DimensionUnit.CM)), - ('group.psb', ResolutionInfo( - h_res=72.0, h_res_unit=DisplayResolutionUnit.PIXELS_PER_INCH, - v_res=72.0, v_res_unit=DisplayResolutionUnit.PIXELS_PER_INCH, - width_unit=DimensionUnit.CM, height_unit=DimensionUnit.CM)), -) - - -@pytest.mark.parametrize(("filename", "size"), DIMENSIONS) -def test_dimensions(filename, size): - w, h = size - psd = load_psd(filename) - assert psd.header.width == w - assert psd.header.height == h - - -@pytest.mark.parametrize(("filename", "resolution"), RESOLUTIONS) -def test_resolution(filename, resolution): - psd = decode_psd(filename) - psd_res = dict( - (block.resource_id, block.data) for block in psd.image_resource_blocks - ) - assert psd_res[ImageResourceID.RESOLUTION_INFO] == resolution - - -@pytest.mark.parametrize(("filename", "size"), DIMENSIONS) -def test_dimensions_api(filename, size): - psd = PSDImage(decode_psd(filename)) - assert psd.header.width == size[0] - assert psd.header.height == size[1] - - -@pytest.mark.parametrize(("filename", "layer_index", "bbox"), BBOXES) -def test_bbox(filename, layer_index, bbox): - psd = PSDImage(decode_psd(filename)) - layer = psd.layers[layer_index] - assert layer.bbox == bbox diff --git a/tests/psd_tools/test_enum.py b/tests/psd_tools/test_enum.py deleted file mode 100644 index e152fefc..00000000 --- a/tests/psd_tools/test_enum.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -from psd_tools.constants import Enum - - -class SampleEnum(Enum): - FOO = 1 - BAR = 2 - _PRIVATE = 3 - - -def test_is_known(): - assert SampleEnum.FOO == 1 - assert SampleEnum.is_known(1) - assert SampleEnum.is_known(2) - assert not SampleEnum.is_known(0) - assert not SampleEnum.is_known(3) - - -def test_name_of(): - assert SampleEnum.name_of(1) == 'FOO' - assert SampleEnum.name_of(2) == 'BAR' - assert SampleEnum.name_of(3) == '' diff --git a/tests/psd_tools/test_grouping.py b/tests/psd_tools/test_grouping.py deleted file mode 100644 index afc5d5f9..00000000 --- a/tests/psd_tools/test_grouping.py +++ /dev/null @@ -1,111 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -from psd_tools.user_api.psd_image import PSDImage -from .utils import decode_psd, full_name - - -def test_no_groups(): - psd = PSDImage(decode_psd('2layers.psd')) - assert len(psd.layers) == 2 - assert not any(layer.kind == "group" for layer in psd.layers) - - -def test_groups_simple(): - psd = PSDImage(decode_psd('group.psd')) - assert len(psd.layers) == 2 - - group = psd.layers[0] - assert group.kind == "group" - assert len(group.layers) == 1 - assert group.name == 'Group 1' - assert group.closed is False - - group_element = group.layers[0] - assert group_element.name == 'Shape 1' - - -def test_groups_without_opening(): - psd = PSDImage(decode_psd('broken-groups.psd')) - group1, group2 = psd.layers - assert group1.name == 'bebek' - assert group2.name == 'anne' - - assert len(group1.layers) == 1 - assert len(group2.layers) == 1 - - assert group1.layers[0].name == 'el sol' - assert group2.layers[0].name == 'kas' - - -def test_group_visibility(): - psd = PSDImage(decode_psd('hidden-groups.psd')) - - group2, group1, bg = psd.layers - assert group2.name == 'Group 2' - assert group1.name == 'Group 1' - assert bg.name == 'Background' - - assert bg.visible is True - assert group2.visible is True - assert group1.visible is False - - assert group2.layers[0].visible is True - - # The flag is 'visible=True', but this layer is hidden - # because its group is not visible. - assert group1.layers[0].visible is True - - -def test_layer_visibility(): - visible = dict( - (layer.name, layer.visible) - for layer in PSDImage(decode_psd('hidden-layer.psd')).layers - ) - assert visible['Shape 1'] - assert not visible['Shape 2'] - assert visible['Background'] - - -def test_groups_32bit(): - psd = PSDImage(decode_psd('32bit5x5.psd')) - assert len(psd.layers) == 3 - assert psd.layers[0].name == 'Background copy 2' - - -def test_group_with_empty_layer(): - psd = PSDImage(decode_psd('empty-layer.psd')) - group1, bg = psd.layers - assert group1.name == 'group' - assert bg.name == 'Background' - - -def test_clipping(): - psd = PSDImage(decode_psd('clipping-mask2.psd')) - assert psd.layers[0].name == 'Group 1' - layer = psd.layers[0].layers[0] - assert layer.name == 'Rounded Rectangle 4' - assert layer.has_clip_layers() - assert layer.clip_layers[0].name == 'Color Balance 1' - assert psd.layers[1].name == 'Rounded Rectangle 3' - assert psd.layers[1].clip_layers[0].name == 'Brightness/Contrast 1' - assert psd.layers[2].name == 'Polygon 1' - assert psd.layers[2].clip_layers[0].name == 'Ellipse 1' - assert psd.layers[2].clip_layers[1].name == 'Rounded Rectangle 2' - assert psd.layers[2].clip_layers[2].name == 'Rounded Rectangle 1' - assert psd.layers[2].clip_layers[3].name == 'Color Fill 1' - assert psd.layers[3].name == 'Background' - - -def test_generator(): - psd = PSDImage.load(full_name('hidden-groups.psd')) - assert len([True for layer in psd.layers]) == 3 - assert len([True for layer in psd.descendants()]) == 5 - - -def test_generator_with_clip_layers(): - psd = PSDImage.load(full_name('clipping-mask.psd')) - assert not psd.layers[0].has_clip_layers() - assert len([True for layer in psd.layers]) == 2 - assert len([True for layer in psd.descendants()]) == 7 - assert len([True for layer in psd.descendants(include_clip=False)]) == 6 diff --git a/tests/psd_tools/test_image_resources.py b/tests/psd_tools/test_image_resources.py deleted file mode 100644 index b583e230..00000000 --- a/tests/psd_tools/test_image_resources.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import pytest - -from psd_tools.constants import ImageResourceID -from psd_tools.decoder.image_resources import ( - SlicesHeaderV6, SlicesResourceBlock) -from psd_tools.user_api.psd_image import PSDImage -from .utils import decode_psd, with_psb, full_name - - -SLICES_FILES = with_psb([ - ('slices.psd',), -]) - - -THUMBNAIL_FILES = with_psb([ - ('layer_comps.psd',), -]) - - -@pytest.mark.parametrize(["filename"], SLICES_FILES) -def test_slices_resource(filename): - decoded = decode_psd(filename) - for block in decoded.image_resource_blocks: - if block.resource_id == ImageResourceID.SLICES: - assert isinstance(block.data, SlicesHeaderV6) - for item in block.data.items: - assert isinstance(item, SlicesResourceBlock) - - -def test_resource_blocks(): - psd = PSDImage.load(full_name("fill_adjustments.psd")) - blocks = psd.image_resource_blocks - assert "version_info" in blocks - - -@pytest.mark.parametrize(["filename"], THUMBNAIL_FILES) -def test_thumbnail(filename): - psd = PSDImage.load(full_name(filename)) - assert psd.thumbnail() diff --git a/tests/psd_tools/test_info.py b/tests/psd_tools/test_info.py deleted file mode 100644 index f2113304..00000000 --- a/tests/psd_tools/test_info.py +++ /dev/null @@ -1,172 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals -import re -import pytest - -from psd_tools import PSDImage -from psd_tools.constants import TaggedBlock, SectionDivider -from psd_tools.decoder.tagged_blocks import VectorMaskSetting -from psd_tools.user_api.effects import PatternOverlay -from .utils import load_psd, decode_psd, with_psb - - -FILES_WITH_NO_LAYERS = ( - ('0layers.psd', False), - ('0layers_tblocks.psd', True), - ('16bit5x5.psd', True), - ('32bit.psd', True), - ('32bit5x5.psd', True), - ('300dpi.psd', True), - ('gradient-fill.psd', True), - ('history.psd', True), - ('pen-text.psd', True), - ('transparentbg.psd', True), - ('vector-mask.psd', True), - ('0layers.psb', True), - ('0layers_tblocks.psb', True), - ('16bit5x5.psb', True), - ('32bit.psb', True), - ('32bit5x5.psb', True), - ('300dpi.psb', True), - ('gradient-fill.psb', True), - ('history.psb', True), - ('pen-text.psb', True), - ('transparentbg.psb', True), - ('vector-mask.psb', True) -) - - -def test_1layer_name(): - psd = decode_psd('1layer.psd') - layers = psd.layer_and_mask_data.layers.layer_records - assert len(layers) == 1 - - layer = layers[0] - assert len(layer.tagged_blocks) == 1 - - block = layer.tagged_blocks[0] - assert block.key == TaggedBlock.UNICODE_LAYER_NAME - assert block.data == 'Фон' - - -def test_groups(): - psd = decode_psd('group.psd') - layers = psd.layer_and_mask_data.layers.layer_records - assert len(layers) == 3 + 1 # 3 layers + 1 divider - - assert ( - layers[1].tagged_blocks[3].key == TaggedBlock.SECTION_DIVIDER_SETTING - ) - assert ( - layers[1].tagged_blocks[3].data.type == - SectionDivider.BOUNDING_SECTION_DIVIDER - ) - - -@pytest.mark.parametrize( - ('filename', 'has_layer_and_mask_data'), - FILES_WITH_NO_LAYERS -) -def test_no_layers_has_tagged_blocks(filename, has_layer_and_mask_data): - psd = load_psd(filename) - - assert psd.layer_and_mask_data is not None - - layers = psd.layer_and_mask_data.layers - assert layers.layer_count == 0 - assert len(layers.layer_records) == 0 - assert len(layers.channel_image_data) == 0 - - tagged_blocks = psd.layer_and_mask_data.tagged_blocks - assert (len(tagged_blocks) != 0) == has_layer_and_mask_data - - -def test_patterns(): - psd = decode_psd('patterns.psd') - tagged_blocks = dict(psd.layer_and_mask_data.tagged_blocks) - assert b'Patt' in tagged_blocks - assert len(tagged_blocks[b'Patt']) == 6 - - -def test_layer_properties(): - psd = PSDImage(decode_psd('clipping-mask2.psd')) - assert psd.width - assert psd.height - assert psd.channels - assert psd.viewbox - for layer in psd.descendants(): - assert layer.bbox - - -def test_api(): - image = PSDImage(decode_psd('1layer.psd')) - assert len(image.layers) == 1 - - layer = image.layers[0] - assert layer.name == 'Фон' - assert layer.bbox == (0, 0, 101, 55) - assert layer.left == 0 - assert layer.right == 101 - assert layer.top == 0 - assert layer.bottom == 55 - assert layer.visible - assert layer.opacity == 255 - assert layer.blend_mode == 'normal' - - -def test_vector_mask(): - psd = decode_psd('vector-mask.psd') - layers = psd.layer_and_mask_data.layers.layer_records - assert layers[1].tagged_blocks[1].key == TaggedBlock.VECTOR_MASK_SETTING1 - assert isinstance(layers[1].tagged_blocks[1].data, VectorMaskSetting) - - -def test_shape_paths(): - psd = PSDImage(decode_psd('gray1.psd')) - assert psd.layers[1].has_vector_mask() - vector_mask = psd.layers[1].vector_mask - assert not vector_mask.invert - assert not vector_mask.disabled - assert not vector_mask.not_link - assert len(vector_mask.paths) == 2 - path = vector_mask.paths[0] - assert len(path.knots) == path.num_knots - assert path.closed - path = vector_mask.paths[1] - assert len(path.knots) == path.num_knots - assert path.closed - - -@pytest.fixture(scope='module') -def stroke_psd(): - psd = PSDImage(decode_psd('stroke.psd')) - yield psd - - -def test_vector_stroke_content_setting(stroke_psd): - assert stroke_psd.layers[1].kind == 'shape' - assert isinstance(stroke_psd.layers[1].stroke_content, PatternOverlay) - - -def test_vector_origination(stroke_psd): - assert stroke_psd.layers[0].has_origination - origination = stroke_psd.layers[0].origination - assert origination.origin_type == 4 - assert origination.resolution == 150.0 - assert origination.shape_bbox == (187.0, 146.0, 220.0, 206.0) - assert origination.line_end.x == 220.0 - assert origination.line_end.y == 146.0 - assert origination.line_start.x == 187.0 - assert origination.line_start.y == 206.0 - assert origination.line_weight == 1.0 - assert origination.arrow_start is False - assert origination.arrow_end is False - assert origination.arrow_width == 0.0 - assert origination.arrow_length == 0.0 - assert origination.arrow_conc == 0 - assert origination.index == 0 - - -def test_print(): - psd = PSDImage(decode_psd('empty-layer.psd')) - psd.print_tree() diff --git a/tests/psd_tools/test_layer_effects.py b/tests/psd_tools/test_layer_effects.py deleted file mode 100644 index 9e020d68..00000000 --- a/tests/psd_tools/test_layer_effects.py +++ /dev/null @@ -1,357 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals -import pytest - -from psd_tools.constants import BlendMode, TaggedBlock -from psd_tools.user_api.psd_image import PSDImage -from .utils import decode_psd, full_name - - -psd = decode_psd('layer_params.psd') -layer_records = psd.layer_and_mask_data.layers.layer_records - -EFFECTS_COUNT = ( - (1, 7), - (2, 7) -) - -EFFECTS_PARAMS = ( - (1, 1, 'enabled', True), - (1, 2, 'enabled', False), - (1, 3, 'enabled', False), - (1, 4, 'enabled', False), - (1, 5, 'enabled', True), - (1, 6, 'enabled', False), - - (2, 1, 'enabled', True), - (2, 2, 'enabled', False), - (2, 3, 'enabled', True), - (2, 4, 'enabled', False), - (2, 5, 'enabled', False), - (2, 6, 'enabled', False), - - (1, 1, 'distance', 65536 * 5), - (2, 1, 'distance', 65536 * 30), - - (1, 3, 'blend_mode', BlendMode.SCREEN), - (2, 3, 'blend_mode', BlendMode.HARD_LIGHT), - (1, 3, 'blur', 65536 * 5), - (2, 3, 'blur', 65536 * 40), - - (1, 5, 'bevel_style', 4), - (2, 5, 'bevel_style', 2) -) - -OBJECT_BASED_EFFECTS_PARAMS = ( - (1, b'DrSh', b'Md ', None, b'Nrml'), - (1, b'DrSh', b'Dstn', None, 5.0), - (1, b'DrSh', b'Ckmt', None, 0.0), - (1, b'DrSh', b'blur', None, 5.0), - (1, b'DrSh', b'TrnS', b'Nm ', '$$$/Contours/Defaults/Gaussian=Gaussian'), - - (1, b'ebbl', b'bvlS', None, b'PlEb'), - (1, b'ebbl', b'hglM', None, b'Scrn'), - (1, b'ebbl', b'useShape', None, False), - (1, b'ebbl', b'useTexture', None, False), - - (1, b'FrFX', b'Sz ', None, 4.0), - (1, b'FrFX', b'PntT', None, b'GrFl'), - (1, b'FrFX', b'Type', None, b'Rdl '), - (1, b'FrFX', b'Algn', None, False), - (1, b'FrFX', b'Scl ', None, 93.0), - - - (2, b'DrSh', b'Md ', None, b'Nrml'), - (2, b'DrSh', b'Dstn', None, 30.0), - (2, b'DrSh', b'Ckmt', None, 50.0), - (2, b'DrSh', b'blur', None, 5.0), - (2, b'DrSh', b'TrnS', b'Nm ', 'Линейный'), - - (2, b'OrGl', b'Md ', None, b'HrdL'), - (2, b'OrGl', b'blur', None, 40.0), - (2, b'OrGl', b'TrnS', b'Nm ', 'Заказная'), - (2, b'OrGl', b'AntA', None, True), - (2, b'OrGl', b'Inpr', None, 43.0) -) - - -@pytest.mark.parametrize(("layer_num", "count"), EFFECTS_COUNT) -def test_layer_effects_count(layer_num, count): - effects_info = layer_records[layer_num].tagged_blocks[1].data - assert effects_info.effects_count == count - - -@pytest.mark.parametrize( - ("layer_num", "effect_num", "param_name", "param_value"), - EFFECTS_PARAMS -) -def test_layer_effect(layer_num, effect_num, param_name, param_value): - effects_list = layer_records[layer_num].tagged_blocks[1].data.effects_list - effect_info = effects_list[effect_num].effect_info - assert effect_info.__getattribute__(param_name) == param_value - - -@pytest.mark.parametrize( - ("layer_num", "effect_key", "param_name", "subparam_name", "param_value"), - OBJECT_BASED_EFFECTS_PARAMS -) -def test_object_based_layer_effect( - layer_num, effect_key, param_name, subparam_name, param_value -): - effects_dict = dict( - layer_records[layer_num].tagged_blocks[0].data.descriptor.items - ) - effect_info = dict(effects_dict[effect_key].items) - - if subparam_name is None: - assert effect_info[param_name].value == param_value - else: - effect_info = dict(effect_info[param_name].items) - assert effect_info[subparam_name].value == param_value - - -def test_iopa_brst_block(): - decoded_data = decode_psd('layer_effects.psd') - layer_records = decoded_data.layer_and_mask_data.layers.layer_records - tagged_blocks = dict(layer_records[4].tagged_blocks) - assert tagged_blocks[TaggedBlock.BLEND_FILL_OPACITY] == 252 - setting = tagged_blocks[TaggedBlock.CHANNEL_BLENDING_RESTRICTIONS_SETTING] - assert setting[0] is False - assert setting[1] is False - assert setting[2] is True - - -@pytest.fixture(scope='module') -def effects_psd(): - psd = PSDImage.load(full_name('layer_effects.psd')) - yield psd - - -def test_effects_api(effects_psd): - effect_kinds = [ - 'DropShadow', - 'InnerShadow', - 'OuterGlow', - 'ColorOverlay', - 'GradientOverlay', - 'PatternOverlay', - 'Stroke', - 'InnerGlow', - 'BevelEmboss', - 'Satin', - ] - for layer in effects_psd.layers: - assert layer.has_effects() - assert len(layer.effects) == 1 - - -def test_coloroverlay(effects_psd): - layer = effects_psd.layers[5] - assert layer.effects.has('coloroverlay') - effect = list(layer.effects.find('coloroverlay'))[0] - assert effect.name.lower() == 'coloroverlay' - assert effect.blend_mode == 'normal' - assert effect.color - assert effect.opacity.value == 100.0 - - -def test_patternoverlay(effects_psd): - layer = effects_psd.layers[2] - assert layer.effects.has('patternoverlay') - effect = list(layer.effects.find('patternoverlay'))[0] - assert effect.name.lower() == 'patternoverlay' - assert effect.aligned is True - assert effect.blend_mode == 'normal' - assert effect.opacity.value == 100.0 - assert effect.pattern - assert effect.phase - assert effect.scale.value == 100.0 - - -def test_gradientoverlay(effects_psd): - layer = effects_psd.layers[3] - assert layer.effects.has('gradientoverlay') - effect = list(layer.effects.find('gradientoverlay'))[0] - assert effect.name.lower() == 'gradientoverlay' - assert effect.aligned is True - assert effect.angle.value == 87.0 - assert effect.blend_mode == 'normal' - assert effect.dithered is False - assert effect.gradient - assert effect.offset - assert effect.opacity.value == 100.0 - assert effect.reversed is False - assert effect.scale.value == 100.0 - assert effect.type == 'linear' - - -def test_satin(effects_psd): - layer = effects_psd.layers[0] - assert layer.effects.has('satin') - effect = list(layer.effects.find('satin'))[0] - assert effect.name.lower() == 'satin' - assert effect.angle.value == -60.0 - assert effect.anti_aliased is True - assert effect.blend_mode == 'multiply' - assert effect.color - assert effect.contour - assert effect.distance.value == 20.0 - assert effect.inverted is True - assert effect.opacity.value == 50.0 - assert effect.size.value == 35.0 - - -def test_stroke(effects_psd): - layer = effects_psd.layers[1] - assert layer.effects.has('stroke') - effect = list(layer.effects.find('stroke'))[0] - assert effect.name.lower() == 'stroke' - assert effect.blend_mode == 'normal' - assert effect.fill.name.lower() == 'coloroverlay' - assert effect.fill_type == 'solid-color' - assert effect.opacity.value == 100.0 - assert effect.overprint is False - assert effect.position == 'outer' - assert effect.size.value == 6.0 - assert effect.color - assert effect.gradient is None - assert effect.pattern is None - - -def test_dropshadow(effects_psd): - layer = effects_psd.layers[4] - assert layer.effects.has('dropshadow') - effect = list(layer.effects.find('dropshadow'))[0] - assert effect.name.lower() == 'dropshadow' - assert effect.angle.value == 90.0 - assert effect.anti_aliased is False - assert effect.blend_mode == 'multiply' - assert effect.choke.value == 0.0 - assert effect.color - assert effect.contour - assert effect.layer_knocks_out is True - assert effect.distance.value == 18.0 - assert effect.noise.value == 0.0 - assert effect.opacity.value == 35.0 - assert effect.size.value == 41.0 - assert effect.use_global_light is True - - -def test_innershadow(effects_psd): - layer = effects_psd.layers[6] - assert layer.effects.has('innershadow') - effect = list(layer.effects.find('innershadow'))[0] - assert effect.name.lower() == 'innershadow' - assert effect.angle.value == 90.0 - assert effect.anti_aliased is False - assert effect.blend_mode == 'multiply' - assert effect.choke.value == 0.0 - assert effect.color - assert effect.contour - assert effect.distance.value == 18.0 - assert effect.noise.value == 0.0 - assert effect.opacity.value == 35.0 - assert effect.size.value == 41.0 - assert effect.use_global_light is True - - -def test_innerglow(effects_psd): - layer = effects_psd.layers[7] - assert layer.effects.has('innerglow') - effect = list(layer.effects.find('innerglow'))[0] - assert effect.name.lower() == 'innerglow' - assert effect.anti_aliased is False - assert effect.blend_mode == 'screen' - assert effect.choke.value == 0.0 - assert effect.color - assert effect.contour - assert effect.glow_source == 'edge' - assert effect.glow_type == 'softer' - assert effect.noise.value == 0.0 - assert effect.opacity.value == 46.0 - assert effect.quality_jitter.value == 0.0 - assert effect.quality_range.value == 50.0 - assert effect.size.value == 18.0 - assert effect.gradient is None - - -def test_outerglow(effects_psd): - layer = effects_psd.layers[8] - assert layer.effects.has('outerglow') - effect = list(layer.effects.find('outerglow'))[0] - assert effect.name.lower() == 'outerglow' - assert effect.anti_aliased is False - assert effect.blend_mode == 'screen' - assert effect.choke.value == 0.0 - assert effect.color - assert effect.contour - assert effect.glow_type == 'softer' - assert effect.noise.value == 0.0 - assert effect.opacity.value == 35.0 - assert effect.quality_jitter.value == 0.0 - assert effect.quality_range.value == 50.0 - assert effect.size.value == 41.0 - assert effect.spread.value == 0.0 - assert effect.gradient is None - - -def test_emboss(effects_psd): - layer = effects_psd.layers[9] - assert layer.effects.has('bevelemboss') - effect = list(layer.effects.find('bevelemboss'))[0] - assert effect.name.lower() == 'bevelemboss' - assert effect.altitude.value == 30.0 - assert effect.angle.value == 90.0 - assert effect.anti_aliased is False - assert effect.bevel_style == 'emboss' - assert effect.bevel_type == 'smooth' - assert effect.blend_mode == 'normal' - assert effect.contour - assert effect.depth.value == 100.0 - assert effect.direction == 'up' - assert effect.enabled is True - assert effect.highlight_color - assert effect.highlight_mode == 'screen' - assert effect.highlight_opacity.value == 50.0 - assert effect.shadow_color - assert effect.shadow_mode == 'multiply' - assert effect.shadow_opacity.value == 50.0 - assert effect.size.value == 41.0 - assert effect.soften.value == 0.0 - assert effect.use_global_light is True - assert effect.use_shape is False - assert effect.use_texture is False - - -def test_bevel(effects_psd): - layer = effects_psd.layers[10] - assert layer.effects.has('bevelemboss') - effect = list(layer.effects.find('bevelemboss'))[0] - assert effect.name.lower() == 'bevelemboss' - assert effect.altitude.value == 30.0 - assert effect.angle.value == 90.0 - assert effect.anti_aliased is False - assert effect.bevel_style == 'inner-bevel' - assert effect.bevel_type == 'smooth' - assert effect.blend_mode == 'normal' - assert effect.contour - assert effect.depth.value == 100.0 - assert effect.direction == 'up' - assert effect.enabled is True - assert effect.highlight_color - assert effect.highlight_mode == 'screen' - assert effect.highlight_opacity.value == 50.0 - assert effect.shadow_color - assert effect.shadow_mode == 'multiply' - assert effect.shadow_opacity.value == 50.0 - assert effect.size.value == 41.0 - assert effect.soften.value == 0.0 - assert effect.use_global_light is True - assert effect.use_shape is False - assert effect.use_texture is False - - -def test_gradient_descriptor(): - psd = PSDImage.load(full_name('effect-stroke-gradient.psd')) - assert psd.layers[0].effects[0].gradient.type == b'ClNs' - assert psd.layers[1].effects[0].gradient.type == b'CstS' diff --git a/tests/psd_tools/test_layer_masks.py b/tests/psd_tools/test_layer_masks.py deleted file mode 100644 index a5997d4b..00000000 --- a/tests/psd_tools/test_layer_masks.py +++ /dev/null @@ -1,189 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import pytest - -from psd_tools.user_api.psd_image import PSDImage, Group -from .utils import load_psd, decode_psd, full_name - - -FILE_NAMES = ( - 'layer_mask_data.psd', - 'masks.psd', - 'masks2.psd', - 'masks.psb', - 'masks2.psb' -) - -# Each block correspond to file in FILE_NAMES at the same index: -# ( -# (, ), -# ... -# ) -# -# = (background_color, , parameters, -# , real_background_color) -# OR -# None -# -# = (mask_disabled, user_mask_from_render, -# parameters_applied) -# parameters = parameters OR None -# = OR None -# real_background_color = real_background_color OR None -# -MASK_DATA_BY_LAYERS = ( - ( - ( - 0, - None - ), - ( - 1, - (0, (False, True, True), (None, None, 204, None), None, None) - ), - ( - 2, - (0, (False, False, True), (230, 6, None, None), None, None) - ), - ( - 3, - (255, (False, False, False), None, None, None) - ), - ( - 4, - ( - 0, - (False, True, True), - (191, 3, None, 2), - (False, False, False), - 255 - ) - ), - ), - ( - (20, (0, (False, True, False), None, (True, False, False), 255)), - ), - - ( - (1, (0, (False, True, False), None, (False, False, False), 255)), - ), - - ( - (20, (0, (False, True, False), None, (True, False, False), 255)), - ), - - ( - (1, (0, (False, True, False), None, (False, False, False), 255)), - ) -) - - -@pytest.mark.parametrize('filename', FILE_NAMES) -def test_file_with_masks_is_parsed(filename): - psd = decode_psd(filename) - for layer_channels in psd.layer_and_mask_data.layers.channel_image_data: - assert len(layer_channels) >= 3 - - -@pytest.mark.parametrize( - ('filename', 'mask_data_by_layers'), - zip(FILE_NAMES, MASK_DATA_BY_LAYERS) -) -def test_layer_mask_data(filename, mask_data_by_layers): - psd = load_psd(filename) - layers = psd.layer_and_mask_data.layers.layer_records - - for layer_id, ethalon_mask_data in mask_data_by_layers: - mask_data = layers[layer_id].mask_data - - has_mask_data = (ethalon_mask_data is not None) - assert (mask_data is not None) == has_mask_data - - if has_mask_data: - assert mask_data.background_color == ethalon_mask_data[0] - - ethalon_flags = ethalon_mask_data[1] - assert mask_data.flags.mask_disabled == ethalon_flags[0] - assert mask_data.flags.user_mask_from_render == ethalon_flags[1] - assert mask_data.flags.parameters_applied == ethalon_flags[2] - - ethalon_parameters = ethalon_mask_data[2] - if ethalon_parameters is not None: - assert ( - mask_data.parameters.user_mask_density == - ethalon_parameters[0] - ) - assert ( - mask_data.parameters.user_mask_feather == - ethalon_parameters[1] - ) - assert ( - mask_data.parameters.vector_mask_density == - ethalon_parameters[2] - ) - assert ( - mask_data.parameters.vector_mask_feather == - ethalon_parameters[3] - ) - - ethalon_real_flags = ethalon_mask_data[3] - has_real_flags = (ethalon_real_flags is not None) - assert (mask_data.real_flags is not None) == has_real_flags - - if has_real_flags: - assert ( - mask_data.real_flags.mask_disabled == - ethalon_real_flags[0] - ) - assert ( - mask_data.real_flags.user_mask_from_render == - ethalon_real_flags[1] - ) - assert ( - mask_data.real_flags.parameters_applied == - ethalon_real_flags[2] - ) - - assert mask_data.real_background_color == ethalon_mask_data[4] - - -@pytest.mark.parametrize('filename', FILE_NAMES) -def test_mask_data_as_pil(filename): - psd = PSDImage.load(full_name(filename)) - for layer in psd.descendants(): - if layer.has_mask(): - mask = layer.mask - if mask.has_box(): - assert mask.as_PIL() is not None - else: - assert mask.width == 0 or mask.height == 0 - assert mask.bbox.width == 0 or mask.bbox.height == 0 - assert mask.background_color is not None - assert not mask.disabled - - -def test_mask_data_api(): - psd = PSDImage.load(full_name('layer_mask_data.psd')) - layer = psd.layers[0] - assert layer.has_mask() - mask = layer.mask - assert mask.has_real() - assert mask.top == 146 - assert mask.left == 36 - assert mask.bottom == 186 - assert mask.right == 170 - assert mask.background_color == 255 - assert mask.relative_to_layer is False - assert mask.disabled is False - assert mask.inverted is False - assert mask.user_mask_from_render is True - assert mask.parameters_applied is True - assert mask.parameters - assert mask.real_flags - - -def test_mask_index(): - psd = PSDImage.load(full_name('mask-index.psd')) - layer = psd.layers[0] - assert layer.has_mask() - assert layer._index == 0 diff --git a/tests/psd_tools/test_metadata.py b/tests/psd_tools/test_metadata.py deleted file mode 100644 index e32bae0e..00000000 --- a/tests/psd_tools/test_metadata.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from .utils import decode_psd - - -def test_metadata(): - decoded = decode_psd('metadata.psd') - layers = decoded.layer_and_mask_data.layers.layer_records - assert len(layers) == 1 - tagged_blocks = dict(layers[0].tagged_blocks) - metadata = tagged_blocks[b'shmd'] - assert len(metadata) == 2 - - # first block is not decoded yet - assert metadata[0].key == b'mdyn' - assert metadata[0].descriptor_version is None - assert metadata[0].data == b'\x00\x00\x00\x01' - - # data from second block is decoded like a descriptor - assert metadata[1].key == b'cust' - assert metadata[1].descriptor_version == 16 - assert metadata[1].data.classID == b'metadata' - assert len(metadata[1].data.items) == 1 - assert metadata[1].data.items[0][0] == b'layerTime' - assert abs(metadata[1].data.items[0][1].value - 1408226375) < 1.0 diff --git a/tests/psd_tools/test_patterns.py b/tests/psd_tools/test_patterns.py deleted file mode 100644 index 0526089c..00000000 --- a/tests/psd_tools/test_patterns.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import pytest - -from psd_tools import PSDImage -from .utils import decode_psd - - -def test_text(): - psd = PSDImage(decode_psd('patterns.psd')) - patterns = psd.patterns - assert len(patterns) == 6 - - try: - from PIL import Image - for pattern_id in patterns: - pattern = patterns[pattern_id] - image = pattern.as_PIL() - assert image.width == pattern.width - assert image.height == pattern.height - except ImportError: - pass diff --git a/tests/psd_tools/test_pixels.py b/tests/psd_tools/test_pixels.py deleted file mode 100644 index b90de27b..00000000 --- a/tests/psd_tools/test_pixels.py +++ /dev/null @@ -1,225 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals -import pytest - -from psd_tools.user_api.psd_image import PSDImage, Group - -from .utils import full_name, FuzzyInt, with_psb - - -# (filename, probe point, pixel value) -PIXEL_COLORS = with_psb(( - ('1layer.psd', (5, 5), (0x27, 0xBA, 0x0F)), - ('group.psd', (10, 20), (0xFF, 0xFF, 0xFF)), - ('hidden-groups.psd', (60, 100), (0xE1, 0x0B, 0x0B)), - ('hidden-layer.psd', (0, 0), (0xFF, 0xFF, 0xFF)), - # ('note.psd', (30, 30), (0, 0, 0)), # what is it? - # XXX: what is this test about? - ('smart-object-slice.psd', (70, 80), (0xAC, 0x19, 0x19)), -)) - -TRANSPARENCY_PIXEL_COLORS = ( - ('transparentbg-gimp.psd', (14, 14), (0xFF, 0xFF, 0xFF, 0x13)), - # why gimp shows it as F2F4C2 ? - ('2layers.psd', (70, 30), (0xF1, 0xF3, 0xC1)), - ('transparentbg-gimp.psb', (14, 14), (0xFF, 0xFF, 0xFF, 0x13)), - ('2layers.psb', (70, 30), (0xF1, 0xF4, 0xC1)), # actually photoshop also - ('background-red-opacity-80.psd', (0, 0), (0xFF, 0x00, 0x00, 0xCC)), -) - -MASK_PIXEL_COLORS = with_psb(( - # this is a clipped point - ('clipping-mask.psd', (182, 68), (0xDA, 0xE6, 0xF7)), - # mask truncates the layer here - ('mask.psd', (87, 7), (0xFF, 0xFF, 0xFF)), -)) - -NO_LAYERS_PIXEL_COLORS = with_psb(( - ('history.psd', (70, 85), (0x24, 0x26, 0x29)), -)) - -PIXEL_COLORS_8BIT = (PIXEL_COLORS + NO_LAYERS_PIXEL_COLORS + - MASK_PIXEL_COLORS + TRANSPARENCY_PIXEL_COLORS) - -PIXEL_COLORS_32BIT = with_psb(( - ('32bit.psd', (75, 15), (136, 139, 145)), - ('32bit.psd', (95, 15), (0, 0, 0)), - ('300dpi.psd', (70, 30), (0, 0, 0)), - ('300dpi.psd', (50, 60), (214, 59, 59)), - ('gradient-fill.psd', (10, 15), (235, 241, 250)), # background - ('gradient-fill.psd', (70, 50), (0, 0, 0)), # black circle - ('gradient-fill.psd', (50, 50), (205, 144, 110)), # filled ellipse - ('pen-text.psd', (50, 50), (229, 93, 93)), - ('pen-text.psd', (170, 40), (0, 0, 0)), - ('vector-mask.psd', (10, 15), (255, 255, 255)), - ('vector-mask.psd', (50, 90), (221, 227, 236)), - ('transparentbg.psd', (0, 0), (255, 255, 255, 0)), - ('transparentbg.psd', (50, 50), (0, 0, 0, 255)), - # why not equal to 16bit5x5.psd? - ('32bit5x5.psd', (0, 0), (235, 241, 250)), - ('32bit5x5.psd', (4, 0), (0, 0, 0)), - ('32bit5x5.psd', (1, 3), (46, 196, 104)), - ('empty-layer.psd', (0, 0), (255, 255, 255)), -)) - -PIXEL_COLORS_16BIT = with_psb(( - ('16bit5x5.psd', (0, 0), (236, 242, 251)), - ('16bit5x5.psd', (4, 0), (0, 0, 0)), - ('16bit5x5.psd', (1, 3), (46, 196, 104)), -)) - -PIXEL_COLORS_GRAYSCALE = with_psb(( - # exact colors depend on Gray ICC profile chosen, - # so allow a wide range for some of the values - ('gray0.psd', (0, 0), (255, 0)), - ('gray0.psd', (70, 57), (FuzzyInt(5, 250), 255)), - ('gray0.psd', (322, 65), (FuzzyInt(5, 250), 190)), - - ('gray1.psd', (0, 0), 255), - ('gray1.psd', (900, 500), 0), - ('gray1.psd', (400, 600), FuzzyInt(5, 250)), -)) - - -LAYER_COLORS = with_psb(( - ('1layer.psd', 0, (5, 5), (0x27, 0xBA, 0x0F)), - ('2layers.psd', 1, (5, 5), (0x27, 0xBA, 0x0F)), - ('2layers.psd', 1, (70, 30), (0x27, 0xBA, 0x0F)), - ('2layers.psd', 0, (0, 0), (0, 0, 0, 0)), - ('2layers.psd', 0, (62, 26), (0xF2, 0xF4, 0xC2, 0xFE)), -)) - -LAYER_COLORS_MULTIBYTE = with_psb(( - ('16bit5x5.psd', 1, (0, 0), (236, 242, 251, 255)), - ('16bit5x5.psd', 1, (1, 3), (46, 196, 104, 255)), - # why not equal to 16bit5x5.psd? - ('32bit5x5.psd', 1, (0, 0), (235, 241, 250, 255)), - ('32bit5x5.psd', 1, (1, 3), (46, 196, 104, 255)), - ('empty-layer.psd', 0, (0, 0), (0, 0, 0, 255)), - ('semi-transparent-layers.psd', 0, (56, 44), (201, 54, 0, 0xFF)), -)) - -LAYER_COLORS_GRAYSCALE = with_psb(( - # gray0: layer 0 is shifted 35px to the right - ('gray0.psd', 0, (0, 0), (255, 0)), - ('gray0.psd', 0, (70-35, 57), (FuzzyInt(5, 250), 255)), - ('gray0.psd', 0, (322-35, 65), (FuzzyInt(5, 250), 190)), - - # gray1: black ellipse - ('gray1.psd', 0, (0, 0), (0, 0)), - ('gray1.psd', 0, (500, 250), (0, 255)), - - # gray1: grey ellipse - ('gray1.psd', 1, (0, 0), (FuzzyInt(5, 250), 0)), - ('gray1.psd', 1, (700, 500), (FuzzyInt(5, 250), 255)), - - # gray1: background - ('gray1.psd', 2, (0, 0), 255), - ('gray1.psd', 2, (900, 500), 255), - ('gray1.psd', 2, (400, 600), 255), -)) - - -def color_PIL(psd, point): - im = psd.as_PIL() - return im.getpixel(point) - - -def color_pymaging(psd, point): - im = psd.as_pymaging() - return tuple(im.get_pixel(*point)) - -BACKENDS = [[color_PIL], [color_pymaging]] - - -@pytest.mark.parametrize(["get_color"], BACKENDS) -@pytest.mark.parametrize(["filename", "point", "color"], PIXEL_COLORS_8BIT) -def test_composite(filename, point, color, get_color): - if ( - get_color == color_pymaging and - filename == 'background-red-opacity-80.psd' - ): - pytest.xfail("Pymaging white-bg removal not implemented") - psd = PSDImage.load(full_name(filename)) - assert color == get_color(psd, point) - - -@pytest.mark.parametrize(["filename", "point", "color"], PIXEL_COLORS_32BIT) -def test_composite_32bit(filename, point, color): - psd = PSDImage.load(full_name(filename)) - assert color == color_PIL(psd, point) - - -@pytest.mark.parametrize(["filename", "point", "color"], PIXEL_COLORS_16BIT) -def test_composite_16bit(filename, point, color): - psd = PSDImage.load(full_name(filename)) - assert color == color_PIL(psd, point) - - -@pytest.mark.parametrize( - ["filename", "point", "color"], - PIXEL_COLORS_GRAYSCALE -) -def test_composite_grayscale(filename, point, color): - psd = PSDImage.load(full_name(filename)) - assert color == color_PIL(psd, point) - - -@pytest.mark.parametrize(["get_color"], BACKENDS) -@pytest.mark.parametrize( - ["filename", "layer_num", "point", "color"], - LAYER_COLORS -) -def test_layer_colors(filename, layer_num, point, color, get_color): - psd = PSDImage.load(full_name(filename)) - layer = psd.layers[layer_num] - assert color == get_color(layer, point) - - -@pytest.mark.parametrize( - ["filename", "layer_num", "point", "color"], - LAYER_COLORS_MULTIBYTE -) -def test_layer_colors_multibyte(filename, layer_num, point, color): - psd = PSDImage.load(full_name(filename)) - layer = psd.layers[layer_num] - assert color == color_PIL(layer, point) - - -@pytest.mark.parametrize( - ["filename", "layer_num", "point", "color"], - LAYER_COLORS_GRAYSCALE -) -def test_layer_colors_grayscale(filename, layer_num, point, color): - psd = PSDImage.load(full_name(filename)) - layer = psd.layers[layer_num] - assert color == color_PIL(layer, point) - - -@pytest.mark.parametrize( - ["filename", "point", "color"], - PIXEL_COLORS + MASK_PIXEL_COLORS + TRANSPARENCY_PIXEL_COLORS -) -def test_layer_merging_size(filename, point, color): - psd = PSDImage.load(full_name(filename)) - merged_image = psd.as_PIL_merged() - assert merged_image.size == psd.as_PIL().size - - -@pytest.mark.parametrize(["filename", "point", "color"], PIXEL_COLORS) -def test_layer_merging_pixels(filename, point, color): - psd = PSDImage.load(full_name(filename)) - merged_image = psd.as_PIL_merged() - assert color[:3] == merged_image.getpixel(point)[:3] - assert merged_image.getpixel(point)[3] == 255 # alpha channel - - -@pytest.mark.xfail -@pytest.mark.parametrize( - ["filename", "point", "color"], - TRANSPARENCY_PIXEL_COLORS -) -def test_layer_merging_pixels_transparency(filename, point, color): - psd = PSDImage.load(full_name(filename)) - merged_image = psd.as_PIL_merged() - assert color == merged_image.getpixel(point) diff --git a/tests/psd_tools/test_placed_layer.py b/tests/psd_tools/test_placed_layer.py deleted file mode 100644 index 83a8092a..00000000 --- a/tests/psd_tools/test_placed_layer.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -import os - -from psd_tools.user_api.psd_image import PSDImage, BBox -from psd_tools.constants import TaggedBlock -from .utils import decode_psd, DATA_PATH - - -def test_placed_layer(): - decoded = decode_psd('placedLayer.psd') - layers = decoded.layer_and_mask_data.layers.layer_records - place_linked1 = dict(layers[1].tagged_blocks).get( - TaggedBlock.SMART_OBJECT_PLACED_LAYER_DATA) - place_linked2 = dict(layers[2].tagged_blocks).get( - TaggedBlock.SMART_OBJECT_PLACED_LAYER_DATA) - place_embedded = dict(layers[3].tagged_blocks).get( - TaggedBlock.PLACED_LAYER_DATA) - assert place_linked1 is not None - assert place_linked2 is not None - assert place_embedded is not None - - -def test_userapi_no_placed_layers(): - img = PSDImage(decode_psd("1layer.psd")) - layer = img.layers[0] - assert not hasattr(layer, 'object_bbox') - assert not hasattr(layer, 'placed_bbox') - - -def test_userapi_placed_layers(): - img = PSDImage(decode_psd("placedLayer.psd")) - bg = img.layers[3] - assert bg.kind == 'pixel' - assert not hasattr(bg, 'object_bbox') - assert not hasattr(bg, 'placed_bbox') - - layer0 = img.layers[0] - assert layer0.kind == 'smartobject' - assert layer0.object_bbox == BBox(0, 0, 64, 64) - assert layer0.placed_bbox == BBox(x1=96.0, y1=96.0, x2=160.0, y2=160.0) - - layer1 = img.layers[1] - assert layer1.kind == 'smartobject' - assert layer1.object_bbox == BBox(0, 0, 101, 55) - assert layer1.object_bbox.width == 101 - assert layer1.placed_bbox == BBox(x1=27.0, y1=73.0, x2=229.0, y2=183.0) - - layer2 = img.layers[2] - assert layer2.kind == 'smartobject' - assert layer2.object_bbox == BBox(0, 0, 64, 64) - assert layer2.placed_bbox == BBox(x1=96.0, y1=96.0, x2=160.0, y2=160.0) - - -def test_embedded(): - # This file contains both an embedded and linked png - psd = PSDImage.load(os.path.join(DATA_PATH, 'placedLayer.psd')) - link = psd.smart_objects['5a96c404-ab9c-1177-97ef-96ca454b82b7'] - assert link.filename == 'linked-layer.png' - with open(os.path.join(DATA_PATH, 'linked-layer.png'), 'rb') as f: - assert link.data == f.read() diff --git a/tests/psd_tools/test_text.py b/tests/psd_tools/test_text.py deleted file mode 100644 index a7293dbd..00000000 --- a/tests/psd_tools/test_text.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import pytest - -from psd_tools import PSDImage -from .utils import decode_psd, with_psb - -TEXTS = with_psb([ - # filename, layer #, text - ('text.psd', 0, 'Line 1\rLine 2\rLine 3 and text'), - ('pen-text.psd', 2, 'Борис ельцин'), -]) - -TARGET_FILES = with_psb([ - ('unicode_pathname.psd',), -]) - - -@pytest.mark.parametrize(('filename', 'layer_num', 'text'), TEXTS) -def test_text(filename, layer_num, text): - psd = PSDImage(decode_psd(filename)) - - layer = psd.layers[layer_num] - assert layer.text == text - assert len(layer.matrix) == 6 - assert len(layer.fontset) == 3 # Specific to files. - assert layer.writing_direction == 0 # Specific to files. - assert layer.full_text.startswith(text) - assert all([isinstance(span, dict) for span in layer.style_spans()]) - - -def test_no_text(): - psd = PSDImage(decode_psd('1layer.psd')) - assert not hasattr(psd.layers[0], 'text_data') - - -@pytest.mark.parametrize(('filename',), TARGET_FILES) -def test_unicode_pathname(filename): - decode_data = decode_psd(filename) - assert decode_data diff --git a/tests/psd_tools/test_utils.py b/tests/psd_tools/test_utils.py deleted file mode 100644 index 9ac64cde..00000000 --- a/tests/psd_tools/test_utils.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import pytest -from io import BytesIO - -from psd_tools.utils import read_be_array -from psd_tools.user_api.psd_image import PSDImage -from .utils import decode_psd - - -PRINT_FILES = ( - ('empty-group.psd',), - ('layer_mask_data.psd',), - ('placedLayer.psd',), - ('adjustment-fillers.psd',), -) - - -def test_read_be_array_from_file_like_objects(): - fp = BytesIO(b"\x00\x01\x00\x05") - res = read_be_array("H", 2, fp) - assert list(res) == [1, 5] - - -@pytest.mark.parametrize(["filename"], PRINT_FILES) -def test_print_tree(filename): - psd = PSDImage(decode_psd(filename)) - psd.print_tree() diff --git a/tests/psd_tools/utils.py b/tests/psd_tools/utils.py deleted file mode 100644 index d21e2e87..00000000 --- a/tests/psd_tools/utils.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals -import os -import psd_tools.reader -import psd_tools.decoder - -DATA_PATH = os.path.join( - os.path.abspath(os.path.dirname(os.path.dirname(__file__))), - 'psd_files' -) - - -def full_name(filename): - return os.path.join(DATA_PATH, filename) - - -def load_psd(filename): - with open(full_name(filename), 'rb') as f: - return psd_tools.reader.parse(f) - - -def decode_psd(filename): - return psd_tools.decoder.parse(load_psd(filename)) - - -# see http://lukeplant.me.uk/blog/posts/fuzzy-testing-with-assertnumqueries/ -class FuzzyInt(int): - def __new__(cls, lowest, highest): - obj = super(FuzzyInt, cls).__new__(cls, highest) - obj.lowest = lowest - obj.highest = highest - return obj - - def __eq__(self, other): - return other >= self.lowest and other <= self.highest - - def __repr__(self): - return str("[%d..%d]") % (self.lowest, self.highest) - - -def with_psb(fixtures): - psb_fixtures = [] - for fixture in fixtures: - psb_fixtures.append( - type(fixture)([fixture[0].replace('.psd', '.psb')]) + fixture[1:]) - print(fixtures + type(fixtures)(psb_fixtures)) - return fixtures + type(fixtures)(psb_fixtures)