Source code for psd_tools.psd.tagged_blocks

"""
Tagged block data structure.

.. todo:: Support the following tagged blocks: ``Tag.PATTERN_DATA``,
    ``Tag.TYPE_TOOL_INFO``, ``Tag.LAYER``,
    ``Tag.ALPHA``

"""
from __future__ import absolute_import, unicode_literals
import attr
import io
import logging
from warnings import warn

from psd_tools.constants import (
    BlendMode, SectionDivider, Tag, PlacedLayerType, SheetColorType
)
from psd_tools.psd.adjustments import ADJUSTMENT_TYPES
from psd_tools.psd.base import (
    BaseElement,
    ByteElement,
    DictElement,
    EmptyElement,
    IntegerElement,
    ListElement,
    StringElement,
    ValueElement,
)
from psd_tools.psd.color import Color
from psd_tools.psd.descriptor import DescriptorBlock, DescriptorBlock2
from psd_tools.psd.effects_layer import EffectsLayer
from psd_tools.psd.engine_data import EngineData, EngineData2
from psd_tools.psd.filter_effects import FilterEffects
from psd_tools.psd.linked_layer import LinkedLayers
from psd_tools.psd.patterns import Patterns
from psd_tools.psd.vector import (
    VectorMaskSetting, VectorStrokeContentSetting
)
from psd_tools.validators import in_
from psd_tools.utils import (
    read_fmt, write_fmt, read_length_block, write_length_block, is_readable,
    write_bytes, write_padding, read_pascal_string, write_pascal_string,
    trimmed_repr, new_registry
)

logger = logging.getLogger(__name__)

TYPES, register = new_registry()

TYPES.update(ADJUSTMENT_TYPES)
TYPES.update({
    Tag.ANIMATION_EFFECTS: DescriptorBlock,
    Tag.ARTBOARD_DATA1: DescriptorBlock,
    Tag.ARTBOARD_DATA2: DescriptorBlock,
    Tag.ARTBOARD_DATA3: DescriptorBlock,
    Tag.BLEND_CLIPPING_ELEMENTS: ByteElement,
    Tag.BLEND_FILL_OPACITY: ByteElement,
    Tag.BLEND_INTERIOR_ELEMENTS: ByteElement,
    Tag.COMPOSITOR_INFO: DescriptorBlock,
    Tag.CONTENT_GENERATOR_EXTRA_DATA: DescriptorBlock,
    Tag.EFFECTS_LAYER: EffectsLayer,
    Tag.EXPORT_SETTING1: DescriptorBlock,
    Tag.EXPORT_SETTING2: DescriptorBlock,
    Tag.FILTER_EFFECTS1: FilterEffects,
    Tag.FILTER_EFFECTS2: FilterEffects,
    Tag.FILTER_EFFECTS3: FilterEffects,
    Tag.FRAMED_GROUP: DescriptorBlock,
    Tag.KNOCKOUT_SETTING: ByteElement,
    Tag.LINKED_LAYER1: LinkedLayers,
    Tag.LINKED_LAYER2: LinkedLayers,
    Tag.LINKED_LAYER3: LinkedLayers,
    Tag.LINKED_LAYER_EXTERNAL: LinkedLayers,
    Tag.LAYER_ID: IntegerElement,
    Tag.LAYER_MASK_AS_GLOBAL_MASK: ByteElement,
    Tag.UNICODE_LAYER_NAME: StringElement,
    Tag.LAYER_VERSION: IntegerElement,
    Tag.OBJECT_BASED_EFFECTS_LAYER_INFO: DescriptorBlock2,
    Tag.OBJECT_BASED_EFFECTS_LAYER_INFO_V0: DescriptorBlock2,
    Tag.OBJECT_BASED_EFFECTS_LAYER_INFO_V1: DescriptorBlock2,
    Tag.PATTERNS1: Patterns,
    Tag.PATTERNS2: Patterns,
    Tag.PATTERNS3: Patterns,
    Tag.PATT: EmptyElement,
    Tag.PIXEL_SOURCE_DATA1: DescriptorBlock,
    Tag.SAVING_MERGED_TRANSPARENCY: EmptyElement,
    Tag.SAVING_MERGED_TRANSPARENCY16: EmptyElement,
    Tag.SAVING_MERGED_TRANSPARENCY32: EmptyElement,
    Tag.TEXT_ENGINE_DATA: EngineData2,
    Tag.TRANSPARENCY_SHAPES_LAYER: ByteElement,
    Tag.UNICODE_PATH_NAME: DescriptorBlock,
    Tag.USING_ALIGNED_RENDERING: IntegerElement,
    Tag.VECTOR_MASK_AS_GLOBAL_MASK: ByteElement,
    Tag.VECTOR_MASK_SETTING1: VectorMaskSetting,
    Tag.VECTOR_MASK_SETTING2: VectorMaskSetting,
    Tag.VECTOR_ORIGINATION_DATA: DescriptorBlock2,
    Tag.VECTOR_ORIGINATION_UNKNOWN: IntegerElement,
    Tag.VECTOR_STROKE_DATA: DescriptorBlock,
    Tag.VECTOR_STROKE_CONTENT_DATA: VectorStrokeContentSetting,
})


[docs]@attr.s(repr=False, slots=True) class TaggedBlocks(DictElement): """ Dict of tagged block items. See :py:class:`~psd_tools.constants.Tag` for available keys. Example:: from psd_tools.constants import Tag # Iterate over fields for key in tagged_blocks: print(key) # Get a field value = tagged_blocks.get_data(Tag.TYPE_TOOL_OBJECT_SETTING) """ def get_data(self, key, default=None): """ Get data from the tagged blocks. Shortcut for the following:: if key in tagged_blocks: value = tagged_blocks[key].data """ if key in self: value = self[key].data if isinstance(value, ValueElement): return value.value else: return value return default def set_data(self, key, *args, **kwargs): """ Set data for the given key. Shortut for the following:: key = getattr(Tag, key) kls = TYPES.get(key) self[key] = TaggedBlocks(key=key, data=kls(value)) """ key = self._key_converter(key) kls = TYPES.get(key) self[key] = TaggedBlock(key=key, data=kls(*args, **kwargs)) @classmethod def read(cls, fp, version=1, padding=1, end_pos=None): items = [] while is_readable(fp, 8): # len(signature) + len(key) = 8 if end_pos is not None and fp.tell() >= end_pos: break block = TaggedBlock.read(fp, version, padding) if block is None: break items.append((block.key, block)) return cls(items) @classmethod def _key_converter(self, key): return getattr(key, 'value', key) def _repr_pretty_(self, p, cycle): if cycle: return '{{...}' with p.group(2, '{', '}'): p.breakable('') for idx, key in enumerate(self._items): if idx: p.text(',') p.breakable() value = self._items[key] try: p.text(Tag(key).name) except ValueError: p.pretty(key) p.text(': ') if isinstance(value.data, bytes): p.text(trimmed_repr(value.data)) else: p.pretty(value.data) p.breakable('')
[docs]@attr.s(repr=False, slots=True) class TaggedBlock(BaseElement): """ Layer tagged block with extra info. .. py:attribute:: key 4-character code. See :py:class:`~psd_tools.constants.Tag` .. py:attribute:: data Data. """ _SIGNATURES = (b'8BIM', b'8B64') _BIG_KEYS = { Tag.USER_MASK, Tag.LAYER_16, Tag.LAYER_32, Tag.LAYER, Tag.SAVING_MERGED_TRANSPARENCY16, Tag.SAVING_MERGED_TRANSPARENCY32, Tag.SAVING_MERGED_TRANSPARENCY, Tag.SAVING_MERGED_TRANSPARENCY16, Tag.ALPHA, Tag.FILTER_MASK, Tag.LINKED_LAYER2, Tag.LINKED_LAYER3, Tag.LINKED_LAYER_EXTERNAL, Tag.FILTER_EFFECTS1, Tag.FILTER_EFFECTS2, Tag.FILTER_EFFECTS3, Tag.PIXEL_SOURCE_DATA2, Tag.UNICODE_PATH_NAME, Tag.EXPORT_SETTING1, Tag.EXPORT_SETTING2, Tag.COMPOSITOR_INFO, Tag.ARTBOARD_DATA2, } signature = attr.ib( default=b'8BIM', repr=False, validator=in_(_SIGNATURES) ) key = attr.ib(default=b'') data = attr.ib(default=b'', repr=True) @classmethod def read(cls, fp, version=1, padding=1): signature = read_fmt('4s', fp)[0] if signature not in cls._SIGNATURES: logger.warning('Invalid signature (%r)' % (signature)) fp.seek(-4, 1) return None key = read_fmt('4s', fp)[0] try: key = Tag(key) except ValueError: message = 'Unknown key: %r' % (key) warn(message) logger.warning(message) fmt = cls._length_format(key, version) raw_data = read_length_block(fp, fmt=fmt, padding=padding) kls = TYPES.get(key) if kls: data = kls.frombytes(raw_data, version=version) # _raw_data = data.tobytes(version=version, # padding=1 if padding == 4 else 4) # assert raw_data == _raw_data, '%r: %s vs %s' % ( # kls, trimmed_repr(raw_data), trimmed_repr(_raw_data) # ) else: message = 'Unknown tagged block: %r, %s' % ( key, trimmed_repr(raw_data) ) warn(message) logger.warning(message) data = raw_data return cls(signature, key, data) def write(self, fp, version=1, padding=1): key = self.key if isinstance(self.key, bytes) else self.key.value written = write_fmt(fp, '4s4s', self.signature, key) def writer(f): if hasattr(self.data, 'write'): # It seems padding size applies at the block level here. inner_padding = 1 if padding == 4 else 4 return self.data.write( f, padding=inner_padding, version=version ) return write_bytes(f, self.data) fmt = self._length_format(self.key, version) written += write_length_block(fp, writer, fmt=fmt, padding=padding) return written @classmethod def _length_format(cls, key, version): return ('I', 'Q')[int(version == 2 and key in cls._BIG_KEYS)]
[docs]@register(Tag.ANNOTATIONS) @attr.s(repr=False, slots=True) class Annotations(ListElement): """ List of Annotation, see :py:class: `.Annotation`. .. py:attribute:: major_version .. py:attribute:: minor_version """ major_version = attr.ib(default=2, type=int) minor_version = attr.ib(default=1, type=int) @classmethod def read(cls, fp, **kwargs): major_version, minor_version, count = read_fmt('2HI', fp) items = [] for _ in range(count): length = read_fmt('I', fp)[0] - 4 if length > 0: with io.BytesIO(fp.read(length)) as f: items.append(Annotation.read(f)) return cls( major_version=major_version, minor_version=minor_version, items=items ) def write(self, fp, **kwargs): written = write_fmt( fp, '2HI', self.major_version, self.minor_version, len(self) ) for item in self: data = item.tobytes() written += write_fmt(fp, 'I', len(data) + 4) written += write_bytes(fp, data) written += write_padding(fp, written, 4) return written
[docs]@attr.s(repr=False, slots=True) class Annotation(BaseElement): """ Annotation structure. .. py:attribute:: kind .. py:attribute:: is_open """ kind = attr.ib( default=b'txtA', type=bytes, validator=in_((b'txtA', b'sndM')) ) is_open = attr.ib(default=0, type=int) flags = attr.ib(default=0, type=int) optional_blocks = attr.ib(default=1, type=int) icon_location = attr.ib(factory=lambda: [0, 0, 0, 0], converter=list) popup_location = attr.ib(factory=lambda: [0, 0, 0, 0], converter=list) color = attr.ib(factory=Color) author = attr.ib(default='', type=str) name = attr.ib(default='', type=str) mod_date = attr.ib(default='', type=str) marker = attr.ib( default=b'txtC', type=bytes, validator=in_((b'txtC', b'sndM')) ) data = attr.ib(default=b'', type=bytes) @classmethod def read(cls, fp, **kwargs): kind, is_open, flags, optional_blocks = read_fmt('4s2BH', fp) icon_location = read_fmt('4i', fp) popup_location = read_fmt('4i', fp) color = Color.read(fp) author = read_pascal_string(fp, 'macroman', padding=2) name = read_pascal_string(fp, 'macroman', padding=2) mod_date = read_pascal_string(fp, 'macroman', padding=2) length, marker = read_fmt('I4s', fp) data = read_length_block(fp) return cls( kind, is_open, flags, optional_blocks, icon_location, popup_location, color, author, name, mod_date, marker, data ) def write(self, fp, **kwargs): written = write_fmt( fp, '4s2BH', self.kind, self.is_open, self.flags, self.optional_blocks ) written += write_fmt(fp, '4i', *self.icon_location) written += write_fmt(fp, '4i', *self.popup_location) written += self.color.write(fp) written += write_pascal_string(fp, self.author, 'macroman', padding=2) written += write_pascal_string(fp, self.name, 'macroman', padding=2) written += write_pascal_string( fp, self.mod_date, 'macroman', padding=2 ) written += write_fmt(fp, 'I4s', len(self.data) + 12, self.marker) written += write_length_block(fp, lambda f: write_bytes(f, self.data)) return written
[docs]@register(Tag.FOREIGN_EFFECT_ID) @register(Tag.LAYER_NAME_SOURCE_SETTING) @attr.s(repr=False, slots=True, eq=False, order=False) class Bytes(ValueElement): """ Bytes structure. .. py:attribute:: value """ value = attr.ib(default=b'\x00\x00\x00\x00', type=bytes) @classmethod def read(cls, fp, **kwargs): return cls(fp.read(4)) def write(self, fp, **kwargs): return write_bytes(fp, self.value)
[docs]@register(Tag.CHANNEL_BLENDING_RESTRICTIONS_SETTING) @attr.s(repr=False, slots=True) class ChannelBlendingRestrictionsSetting(ListElement): """ ChannelBlendingRestrictionsSetting structure. List of restricted channel numbers (int). """ @classmethod def read(cls, fp, **kwargs): items = [] while is_readable(fp, 4): items.append(read_fmt('I', fp)[0]) return cls(items) def write(self, fp, **kwargs): return write_fmt(fp, '%dI' % len(self), *self._items)
[docs]@register(Tag.FILTER_MASK) @attr.s(repr=False, slots=True) class FilterMask(BaseElement): """ FilterMask structure. .. py:attribute:: color .. py:attribute:: opacity """ color = attr.ib(default=None) opacity = attr.ib(default=0, type=int) @classmethod def read(cls, fp, **kwargs): color = Color.read(fp) opacity = read_fmt('H', fp)[0] return cls(color, opacity) def write(self, fp, **kwargs): written = self.color.write(fp) written += write_fmt(fp, 'H', self.opacity) return written
[docs]@register(Tag.METADATA_SETTING) class MetadataSettings(ListElement): """ MetadataSettings structure. """ @classmethod def read(cls, fp, **kwargs): count = read_fmt('I', fp)[0] items = [] for _ in range(count): items.append(MetadataSetting.read(fp)) return cls(items) def write(self, fp, **kwargs): written = write_fmt(fp, 'I', len(self)) written += sum(item.write(fp) for item in self) return written
[docs]@attr.s(repr=False, slots=True) class MetadataSetting(BaseElement): """ MetadataSetting structure. """ _KNOWN_KEYS = {b'cust', b'cmls', b'extn', b'mlst', b'tmln', b'sgrp'} _KNOWN_SIGNATURES = (b'8BIM', b'8ELE') signature = attr.ib( default=b'8BIM', type=bytes, repr=False, validator=in_(_KNOWN_SIGNATURES) ) key = attr.ib(default=b'', type=bytes) copy_on_sheet = attr.ib(default=False, type=bool) data = attr.ib(default=b'', type=bytes) @classmethod def read(cls, fp, **kwargs): signature = read_fmt('4s', fp)[0] assert signature in cls._KNOWN_SIGNATURES, 'Invalid signature %r' % signature key, copy_on_sheet = read_fmt("4s?3x", fp) data = read_length_block(fp) if key in (b'mdyn', b'sgrp'): with io.BytesIO(data) as f: data = read_fmt('I', f)[0] elif key in cls._KNOWN_KEYS: data = DescriptorBlock.frombytes(data, padding=4) else: message = 'Unknown metadata key %r' % (key) logger.warning(message) data = data return cls(signature, key, copy_on_sheet, data) def write(self, fp, **kwargs): written = write_fmt( fp, '4s4s?3x', self.signature, self.key, self.copy_on_sheet ) def writer(f): if hasattr(self.data, 'write'): return self.data.write(f, padding=4) elif isinstance(self.data, int): return write_fmt(fp, 'I', self.data) return write_bytes(f, self.data) written += write_length_block(fp, writer) return written
[docs]@register(Tag.PIXEL_SOURCE_DATA2) @attr.s(repr=False, slots=True) class PixelSourceData2(ListElement): """ PixelSourceData2 structure. """ @classmethod def read(cls, fp, **kwargs): items = [] while is_readable(fp, 8): items.append(read_length_block(fp, fmt='Q')) return cls(items) def write(self, fp, padding=4, **kwargs): written = 0 for item in self: written += write_length_block( fp, lambda f: write_bytes(f, item), fmt='Q' ) written += write_padding(fp, written, padding) return written
[docs]@register(Tag.PLACED_LAYER1) @register(Tag.PLACED_LAYER2) @attr.s(repr=False, slots=True) class PlacedLayerData(BaseElement): """ PlacedLayerData structure. """ kind = attr.ib(default=b'plcL', type=bytes) version = attr.ib(default=3, type=int, validator=in_((3, ))) uuid = attr.ib(default='', type=bytes) page = attr.ib(default=0, type=int) total_pages = attr.ib(default=0, type=int) anti_alias = attr.ib(default=0, type=int) layer_type = attr.ib( default=PlacedLayerType.UNKNOWN, converter=PlacedLayerType, validator=in_(PlacedLayerType) ) transform = attr.ib(default=(0., ) * 8, type=tuple) warp = attr.ib(default=None) @classmethod def read(cls, fp, **kwargs): kind, version = read_fmt('4sI', fp) uuid = read_pascal_string(fp, 'macroman', padding=1) page, total_pages, anti_alias, layer_type = read_fmt('4I', fp) transform = read_fmt('8d', fp) warp = DescriptorBlock2.read(fp, padding=1) return cls( kind, version, uuid, page, total_pages, anti_alias, layer_type, transform, warp ) def write(self, fp, padding=4, **kwargs): written = write_fmt(fp, '4sI', self.kind, self.version) written += write_pascal_string(fp, self.uuid, 'macroman', padding=1) written += write_fmt( fp, '4I', self.page, self.total_pages, self.anti_alias, self.layer_type.value ) written += write_fmt(fp, '8d', *self.transform) written += self.warp.write(fp, padding=1) written += write_padding(fp, written, padding) return written
[docs]@register(Tag.PROTECTED_SETTING) class ProtectedSetting(IntegerElement): """ ProtectedSetting structure. """ @property def transparency(self): return bool(self.value & 0x01) @property def composite(self): return bool(self.value & 0x02) @property def position(self): return bool(self.value & 0x04)
[docs]@register(Tag.REFERENCE_POINT) @attr.s(repr=False, slots=True) class ReferencePoint(ListElement): """ ReferencePoint structure. """ @classmethod def read(cls, fp, **kwargs): return cls(list(read_fmt('2d', fp))) def write(self, fp, **kwargs): return write_fmt(fp, '2d', *self._items)
[docs]@register(Tag.SECTION_DIVIDER_SETTING) @register(Tag.NESTED_SECTION_DIVIDER_SETTING) @attr.s(repr=False, slots=True) class SectionDividerSetting(BaseElement): """ SectionDividerSetting structure. .. py:attribute:: kind .. py:attribute:: blend_mode .. py:attribute:: sub_type """ kind = attr.ib( default=SectionDivider.OTHER, converter=SectionDivider, validator=in_(SectionDivider) ) signature = attr.ib(default=None, repr=False) blend_mode = attr.ib(default=None) sub_type = attr.ib(default=None) @classmethod def read(cls, fp, **kwargs): kind = SectionDivider(read_fmt('I', fp)[0]) signature, blend_mode = None, None if is_readable(fp, 8): signature = read_fmt('4s', fp)[0] assert signature == b'8BIM', 'Invalid signature %r' % signature blend_mode = BlendMode(read_fmt('4s', fp)[0]) sub_type = None if is_readable(fp, 4): sub_type = read_fmt('I', fp)[0] return cls( kind, signature=signature, blend_mode=blend_mode, sub_type=sub_type ) def write(self, fp, **kwargs): written = write_fmt(fp, 'I', self.kind.value) if self.signature and self.blend_mode: written += write_fmt( fp, '4s4s', self.signature, self.blend_mode.value ) if self.sub_type is not None: written += write_fmt(fp, 'I', self.sub_type) return written
[docs]@register(Tag.SHEET_COLOR_SETTING) @attr.s(repr=False, slots=True, eq=False, order=False) class SheetColorSetting(ValueElement): """ SheetColorSetting value. This setting represents color label in the layers panel in Photoshop UI. .. py:attribute:: value """ value = attr.ib(default=0, converter=SheetColorType, type=SheetColorType) @classmethod def read(cls, fp, **kwargs): return cls(SheetColorType(*read_fmt('H6x', fp))) def write(self, fp, **kwargs): return write_fmt(fp, 'H6x', self.value.value)
[docs]@register(Tag.SMART_OBJECT_LAYER_DATA1) @register(Tag.SMART_OBJECT_LAYER_DATA2) @attr.s(repr=False, slots=True) class SmartObjectLayerData(BaseElement): """ VersionedDescriptorBlock structure. .. py:attribute:: kind .. py:attribute:: version .. py:attribute:: data """ kind = attr.ib(default=b'soLD', type=bytes, validator=in_((b'soLD', ))) version = attr.ib(default=5, type=int, validator=in_((4, 5))) data = attr.ib(default=None, type=DescriptorBlock) @classmethod def read(cls, fp, **kwargs): kind, version = read_fmt('4sI', fp) data = DescriptorBlock.read(fp) return cls(kind, version, data) def write(self, fp, padding=4, **kwargs): written = write_fmt(fp, '4sI', self.kind, self.version) written += self.data.write(fp, padding=1) written += write_padding(fp, written, padding) return written
[docs]@register(Tag.TYPE_TOOL_OBJECT_SETTING) @attr.s(repr=False, slots=True) class TypeToolObjectSetting(BaseElement): """ TypeToolObjectSetting structure. .. py:attribute:: version .. py:attribute:: transform Tuple of affine transform parameters (xx, xy, yx, yy, tx, ty). .. py:attribute:: text_version .. py:attribute:: text_data .. py:attribute:: warp_version .. py:attribute:: warp .. py:attribute:: left .. py:attribute:: top .. py:attribute:: right .. py:attribute:: bottom """ version = attr.ib(default=1, type=int) transform = attr.ib(default=(0., ) * 6, type=tuple) text_version = attr.ib(default=1, type=int, validator=in_((50, ))) text_data = attr.ib(default=None, type=DescriptorBlock) warp_version = attr.ib(default=1, type=int, validator=in_((1, ))) warp = attr.ib(default=None, type=DescriptorBlock) left = attr.ib(default=0, type=int) top = attr.ib(default=0, type=int) right = attr.ib(default=0, type=int) bottom = attr.ib(default=0, type=int) @classmethod def read(cls, fp, **kwargs): version = read_fmt('H', fp)[0] transform = read_fmt('6d', fp) text_version = read_fmt('H', fp)[0] text_data = DescriptorBlock.read(fp) # Engine data. if b'EngineData' in text_data: try: engine_data = text_data[b'EngineData'].value engine_data = EngineData.frombytes(engine_data) text_data[b'EngineData'].value = engine_data except Exception: logger.warning('Failed to read engine data') warp_version = read_fmt('H', fp)[0] warp = DescriptorBlock.read(fp) left, top, right, bottom = read_fmt("4i", fp) return cls( version, transform, text_version, text_data, warp_version, warp, left, top, right, bottom ) def write(self, fp, padding=4, **kwargs): written = write_fmt(fp, 'H6d', self.version, *self.transform) written += write_fmt(fp, 'H', self.text_version) written += self.text_data.write(fp, padding=1) written += write_fmt(fp, 'H', self.warp_version) written += self.warp.write(fp, padding=1) written += write_fmt( fp, '4i', self.left, self.top, self.right, self.bottom ) written += write_padding(fp, written, padding) return written
[docs]@register(Tag.USER_MASK) @attr.s(repr=False, slots=True) class UserMask(BaseElement): """ UserMask structure. .. py:attribute:: color .. py:attribute:: opacity .. py:attribute:: flag """ color = attr.ib(default=None) opacity = attr.ib(default=0, type=int) flag = attr.ib(default=128, type=int) @classmethod def read(cls, fp, **kwargs): color = Color.read(fp) opacity, flag = read_fmt('HBx', fp) return cls(color, opacity, flag) def write(self, fp, **kwargs): written = self.color.write(fp) written += write_fmt(fp, 'HBx', self.opacity, self.flag) return written