Source code for psd_tools.psd.descriptor

"""
Descriptor data structure.

Descriptors are basic data structure used throughout PSD files. Descriptor is
one kind of serialization protocol for data objects, and
enum classes in :py:mod:`psd_tools.terminology` or bytes indicates what kind
of descriptor it is.

The class ID can be pre-defined enum if the tag is 4-byte length or plain
bytes if the length is arbitrary. They depend on the internal version of
Adobe Photoshop but the detail is unknown.

Pretty printing is the best approach to check the descriptor content::

    from IPython.pretty import pprint
    pprint(descriptor)
"""
from __future__ import absolute_import, unicode_literals

import logging

import attr

from psd_tools.constants import OSType
from psd_tools.psd.base import (
    BaseElement,
    BooleanElement,
    DictElement,
    IntegerElement,
    ListElement,
    NumericElement,
    StringElement,
)
from psd_tools.terminology import Enum, Event, Form, Key, Klass, Type, Unit
from psd_tools.utils import (
    new_registry,
    read_fmt,
    read_length_block,
    read_unicode_string,
    trimmed_repr,
    write_bytes,
    write_fmt,
    write_length_block,
    write_padding,
    write_unicode_string,
)
from psd_tools.validators import in_

logger = logging.getLogger(__name__)

TYPES, register = new_registry(attribute="ostype")

_TERMS = set(
    item.value for kls in (Klass, Enum, Event, Form, Key, Type, Unit) for item in kls
)


def read_length_and_key(fp):
    """
    Helper to read descriptor key.
    """
    length = read_fmt("I", fp)[0]
    key = fp.read(length or 4)
    if length == 0 and key not in _TERMS:
        logger.debug("Unknown term: %r" % (key))
        _TERMS.add(key)
    return key


def write_length_and_key(fp, value):
    """
    Helper to write descriptor key.
    """
    written = write_fmt(fp, "I", 0 if value in _TERMS else len(value))
    written += write_bytes(fp, value)
    return written


class _DescriptorMixin(DictElement):
    @classmethod
    def _read_body(cls, fp):
        name = read_unicode_string(fp, padding=1)
        classID = read_length_and_key(fp)
        items = []
        count = read_fmt("I", fp)[0]
        for _ in range(count):
            key = read_length_and_key(fp)
            ostype = OSType(fp.read(4))
            kls = TYPES.get(ostype)
            value = kls.read(fp)
            items.append((key, value))

        return dict(name=name, classID=classID, items=items)

    def _write_body(self, fp):
        written = write_unicode_string(fp, self.name, padding=1)
        written += write_length_and_key(fp, self.classID)
        written += write_fmt(fp, "I", len(self))
        for key in self:
            written += write_length_and_key(fp, key)
            written += write_bytes(fp, self[key].ostype.value)
            written += self[key].write(fp)
        return written

    @classmethod
    def _key_converter(cls, key):
        if hasattr(key, "encode"):
            return key.encode("ascii")
        return getattr(key, "value", key)

    def _repr_pretty_(self, p, cycle):
        if cycle:
            return "(...)"

        prefix = "{cls}({name}){{".format(
            cls=self.__class__.__name__,
            name=getattr(self.classID, "name", self.classID),
        )
        with p.group(2, prefix, "}"):
            p.breakable("")
            for idx, key in enumerate(self):
                if idx:
                    p.text(",")
                    p.breakable()
                value = self[key]
                p.pretty(key.decode("ascii"))
                p.text(": ")
                if isinstance(value, bytes):
                    p.text(trimmed_repr(value))
                else:
                    p.pretty(value)
            p.breakable("")


[docs] @register(OSType.DESCRIPTOR) @attr.s(repr=False) class Descriptor(_DescriptorMixin): """ Dict-like descriptor structure. Key values can be 4-character `bytes` in :py:class:`~psd_tools.terminology.Key` or arbitrary length `bytes`. Supports direct access by :py:class:`~psd_tools.terminology.Key`. Example:: from psd_tools.terminology import Key descriptor[Key.Enabled] for key in descriptor: print(descriptor[key]) .. py:attribute:: name `str` .. py:attribute:: classID bytes in :py:class:`~psd_tools.terminology.Klass` """ name = attr.ib(default="", type=str) classID = attr.ib(default=Klass.Null.value) @classmethod def read(cls, fp): return cls(**cls._read_body(fp)) def write(self, fp): return self._write_body(fp)
[docs] @register(OSType.OBJECT_ARRAY) @attr.s(repr=False) class ObjectArray(_DescriptorMixin): """ Object array structure almost equivalent to :py:class:`~psd_tools.psd.descriptor.Descriptor`. .. py:attribute:: items_count `int` value .. py:attribute:: name `str` value .. py:attribute:: classID bytes in :py:class:`~psd_tools.terminology.Klass` """ items_count = attr.ib(default=0, type=int) name = attr.ib(default="", type=str) classID = attr.ib(default=Klass.Null.value) @classmethod def read(cls, fp): items_count = read_fmt("I", fp)[0] return cls(items_count=items_count, **cls._read_body(fp)) def write(self, fp): written = write_fmt(fp, "I", self.items_count) written += self._write_body(fp) return written
[docs] @register(OSType.LIST) @attr.s(repr=False) class List(ListElement): """ List structure. Example:: for item in list_value: print(item) """ @classmethod def read(cls, fp): items = [] count = read_fmt("I", fp)[0] for _ in range(count): key = OSType(fp.read(4)) kls = TYPES.get(key) value = kls.read(fp) items.append(value) return cls(items) def write(self, fp): written = write_fmt(fp, "I", len(self)) for item in self: written += write_bytes(fp, item.ostype.value) written += item.write(fp) return written
[docs] @register(OSType.PROPERTY) @attr.s(repr=False) class Property(BaseElement): """ Property structure. .. py:attribute:: name `str` value .. py:attribute:: classID bytes in :py:class:`~psd_tools.terminology.Klass` .. py:attribute:: keyID bytes in :py:class:`~psd_tools.terminology.Key` """ name = attr.ib(default="", type=str) classID = attr.ib(default=b"\x00\x00\x00\x00", type=bytes) keyID = attr.ib(default=b"\x00\x00\x00\x00", type=bytes) @classmethod def read(cls, fp): name = read_unicode_string(fp) classID = read_length_and_key(fp) keyID = read_length_and_key(fp) return cls(name, classID, keyID) def write(self, fp): written = write_unicode_string(fp, self.name) written += write_length_and_key(fp, self.classID) written += write_length_and_key(fp, self.keyID) return written
[docs] @register(OSType.UNIT_FLOAT) @attr.s(slots=True, repr=False, eq=False, order=False) class UnitFloat(NumericElement): """ Unit float structure. .. py:attribute:: unit unit of the value in :py:class:`Unit` or :py:class:`Enum` .. py:attribute:: value `float` value """ value = attr.ib(default=0.0, type=float) unit = attr.ib(default=Unit._None) @classmethod def read(cls, fp): unit, value = read_fmt("4sd", fp) try: unit = Unit(unit) except ValueError: logger.warning("Using Enum for Unit field") unit = Enum(unit) return cls(unit=unit, value=value) def write(self, fp): return write_fmt(fp, "4sd", self.unit.value, self.value) def _repr_pretty_(self, p, cycle): if cycle: return self.__repr__() p.pretty(self.value) p.text(" ") p.text(self.unit.name)
[docs] @register(OSType.UNIT_FLOATS) @attr.s(repr=False) class UnitFloats(BaseElement): """ Unit floats structure. .. py:attribute:: unit unit of the value in :py:class:`Unit` or :py:class:`Enum` .. py:attribute:: values List of `float` values """ unit = attr.ib(default=Unit._None) values = attr.ib(factory=list) @classmethod def read(cls, fp): unit, count = read_fmt("4sI", fp) try: unit = Unit(unit) except ValueError: logger.warning("Using Enum for Unit field") unit = Enum(unit) values = list(read_fmt("%dd" % count, fp)) return cls(unit=unit, values=values) def write(self, fp): return write_fmt( fp, "4sI%dd" % len(self.values), self.unit.value, len(self.values), *self.values, ) def __iter__(self): for value in self.values: yield value def __getitem__(self, index): return self.values[index] def __len__(self): return len(self.values)
[docs] @register(OSType.DOUBLE) class Double(NumericElement): """ Double structure. .. py:attribute:: value `float` value """ @classmethod def read(cls, fp): return cls(*read_fmt("d", fp)) def write(self, fp): return write_fmt(fp, "d", self.value)
[docs] @attr.s(repr=False) class Class(BaseElement): """ Class structure. .. py:attribute:: name `str` value .. py:attribute:: classID bytes in :py:class:`~psd_tools.terminology.Klass` """ name = attr.ib(default="", type=str) classID = attr.ib(default=b"\x00\x00\x00\x00", type=bytes) @classmethod def read(cls, fp): name = read_unicode_string(fp) classID = read_length_and_key(fp) return cls(name, classID) def write(self, fp): written = write_unicode_string(fp, self.name) written += write_length_and_key(fp, self.classID) return written
[docs] @register(OSType.STRING) class String(StringElement): """ String structure. .. py:attribute:: value `str` value """ pass
[docs] @register(OSType.ENUMERATED_REFERENCE) @attr.s(repr=False) class EnumeratedReference(BaseElement): """ Enumerated reference structure. .. py:attribute:: name `str` value .. py:attribute:: classID bytes in :py:class:`~psd_tools.terminology.Klass` .. py:attribute:: typeID bytes in :py:class:`~psd_tools.terminology.Type` .. py:attribute:: enum bytes in :py:class:`~psd_tools.terminology.Enum` """ name = attr.ib(default="", type=str) classID = attr.ib(default=b"\x00\x00\x00\x00", type=bytes) typeID = attr.ib(default=b"\x00\x00\x00\x00", type=bytes) enum = attr.ib(default=b"\x00\x00\x00\x00", type=bytes) @classmethod def read(cls, fp): name = read_unicode_string(fp) classID = read_length_and_key(fp) typeID = read_length_and_key(fp) enum = read_length_and_key(fp) return cls(name, classID, typeID, enum) def write(self, fp): written = write_unicode_string(fp, self.name) written += write_length_and_key(fp, self.classID) written += write_length_and_key(fp, self.typeID) written += write_length_and_key(fp, self.enum) return written
[docs] @register(OSType.OFFSET) @attr.s(repr=False) class Offset(BaseElement): """ Offset structure. .. py:attribute:: name `str` value .. py:attribute:: classID bytes in :py:class:`~psd_tools.terminology.Klass` .. py:attribute:: value `int` value """ name = attr.ib(default="", type=str) classID = attr.ib(default=b"\x00\x00\x00\x00", type=bytes) value = attr.ib(default=0) @classmethod def read(cls, fp): name = read_unicode_string(fp) classID = read_length_and_key(fp) offset = read_fmt("I", fp)[0] return cls(name, classID, offset) def write(self, fp): written = write_unicode_string(fp, self.name) written += write_length_and_key(fp, self.classID) written += write_fmt(fp, "I", self.value) return written
[docs] @register(OSType.BOOLEAN) class Bool(BooleanElement): """ Bool structure. .. py:attribute:: value `bool` value """ @classmethod def read(cls, fp): return cls(read_fmt("?", fp)[0]) def write(self, fp): return write_fmt(fp, "?", self.value)
[docs] @register(OSType.LARGE_INTEGER) class LargeInteger(IntegerElement): """ LargeInteger structure. .. py:attribute:: value `int` value """ @classmethod def read(cls, fp): return cls(read_fmt("q", fp)[0]) def write(self, fp): return write_fmt(fp, "q", self.value)
[docs] @register(OSType.INTEGER) class Integer(IntegerElement): """ Integer structure. .. py:attribute:: value `int` value """ @classmethod def read(cls, fp): return cls(read_fmt("i", fp)[0]) def write(self, fp): return write_fmt(fp, "i", self.value)
[docs] @register(OSType.ENUMERATED) @attr.s(repr=False) class Enumerated(BaseElement): """ Enum structure. .. py:attribute:: typeID bytes in :py:class:`~psd_tools.terminology.Type` .. py:attribute:: enum bytes in :py:class:`~psd_tools.terminology.Enum` """ typeID = attr.ib(default=b"\x00\x00\x00\x00", type=bytes) enum = attr.ib(default=b"\x00\x00\x00\x00", type=bytes) @classmethod def read(cls, fp): typeID = read_length_and_key(fp) enum = read_length_and_key(fp) return cls(typeID, enum) def write(self, fp): written = write_length_and_key(fp, self.typeID) written += write_length_and_key(fp, self.enum) return written def _repr_pretty_(self, p, cycle): if cycle: return self.__repr__() p.text("(") p.pretty(getattr(self.typeID, "name", self.typeID)) p.text(", ") p.pretty(getattr(self.enum, "name", self.enum)) p.text(")")
[docs] def get_name(self): """Get enum name.""" if len(self.enum) == 4: try: return Enum(self.enum).name except ValueError: pass return str(self.enum)
[docs] @register(OSType.RAW_DATA) @attr.s(repr=False) class RawData(BaseElement): """ RawData structure. .. py:attribute:: value `bytes` value """ value = attr.ib(default=b"\x00\x00\x00\x00", type=bytes) @classmethod def read(cls, fp): return cls(read_length_block(fp)) def write(self, fp): def writer(f): if hasattr(self.value, "write"): return self.value.write(f) return write_bytes(f, self.value) return write_length_block(fp, writer)
[docs] @register(OSType.CLASS1) class Class1(Class): """ Class structure equivalent to :py:class:`~psd_tools.psd.descriptor.Class`. """ pass
[docs] @register(OSType.CLASS2) class Class2(Class): """ Class structure equivalent to :py:class:`~psd_tools.psd.descriptor.Class`. """ pass
[docs] @register(OSType.CLASS3) class Class3(Class): """ Class structure equivalent to :py:class:`~psd_tools.psd.descriptor.Class`. """ pass
[docs] @register(OSType.REFERENCE) class Reference(List): """ Reference structure equivalent to :py:class:`~psd_tools.psd.descriptor.List`. """ pass
[docs] @register(OSType.ALIAS) class Alias(RawData): """ Alias structure equivalent to :py:class:`~psd_tools.psd.descriptor.RawData`. """ pass
[docs] @register(OSType.GLOBAL_OBJECT) class GlobalObject(Descriptor): """ Global object structure equivalent to :py:class:`~psd_tools.psd.descriptor.Descriptor`. """ pass
[docs] @register(OSType.PATH) class Path(RawData): """ Undocumented path structure equivalent to :py:class:`~psd_tools.psd.descriptor.RawData`. """ pass
[docs] @register(OSType.IDENTIFIER) class Identifier(Integer): """ Identifier equivalent to :py:class:`~psd_tools.psd.descriptor.Integer`. """ pass
[docs] @register(OSType.INDEX) class Index(Integer): """ Index equivalent to :py:class:`~psd_tools.psd.descriptor.Integer`. """ pass
[docs] @register(OSType.NAME) @attr.s(repr=False) class Name(BaseElement): """ Name structure (Undocumented). .. py:attribute:: name str .. py:attribute:: classID bytes in :py:class:`~psd_tools.terminology.Klass` .. py:attribute:: value str """ name = attr.ib(default="", type=str) classID = attr.ib(default=b"\x00\x00\x00\x00", type=bytes) value = attr.ib(default="", type=str) @classmethod def read(cls, fp): name = read_unicode_string(fp) classID = read_length_and_key(fp) value = read_unicode_string(fp) return cls(name, classID, value) def write(self, fp): written = write_unicode_string(fp, self.name) written += write_length_and_key(fp, self.classID) written += write_unicode_string(fp, self.value) return written
@attr.s(repr=False) class DescriptorBlock(Descriptor): """ Dict-like Descriptor-based structure that has `version` field. See :py:class:`~psd_tools.psd.descriptor.Descriptor`. .. py:attribute:: version """ version = attr.ib(default=16, type=int, validator=in_((16,))) @classmethod def read(cls, fp, **kwargs): version = read_fmt("I", fp)[0] return cls(version=version, **cls._read_body(fp)) def write(self, fp, padding=4, **kwargs): written = write_fmt(fp, "I", self.version) written += self._write_body(fp) written += write_padding(fp, written, padding) return written @attr.s(repr=False) class DescriptorBlock2(Descriptor): """ Dict-like Descriptor-based structure that has `version` and `data_version` fields. See :py:class:`~psd_tools.psd.descriptor.Descriptor`. .. py:attribute:: version .. py:attribute:: data_version """ version = attr.ib(default=1, type=int) data_version = attr.ib(default=16, type=int, validator=in_((16,))) @classmethod def read(cls, fp, **kwargs): version, data_version = read_fmt("2I", fp) return cls(version=version, data_version=data_version, **cls._read_body(fp)) def write(self, fp, padding=4, **kwargs): written = write_fmt(fp, "2I", self.version, self.data_version) written += self._write_body(fp) written += write_padding(fp, written, padding) return written