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 io
import logging
from warnings import warn

import attr

from psd_tools.constants import (
    BlendMode,
    PlacedLayerType,
    SectionDivider,
    SheetColorType,
    Tag,
)
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.utils import (
    is_readable,
    new_registry,
    read_fmt,
    read_length_block,
    read_pascal_string,
    trimmed_repr,
    write_bytes,
    write_fmt,
    write_length_block,
    write_padding,
    write_pascal_string,
)
from psd_tools.validators import in_

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, item=item: 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=b"", 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.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.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