"""
Base data structures intended for inheritance.
All the data objects in this subpackage inherit from the base classes here.
That means, all the data structures in the :py:mod:`psd_tools.psd` subpackage
implements the methods of :py:class:`~psd_tools.psd.BaseElement` for
serialization and decoding.
Objects that inherit from the :py:class:`~psd_tools.psd.BaseElement` typically
gets attrs_ decoration to have data fields.
.. _attrs: https://www.attrs.org/en/stable/index.html
"""
from __future__ import absolute_import, division, unicode_literals
import io
import logging
from collections import OrderedDict
from enum import Enum
import attr
from psd_tools.utils import (
read_fmt,
read_unicode_string,
trimmed_repr,
write_bytes,
write_fmt,
write_unicode_string,
)
logger = logging.getLogger(__name__)
[docs]
class BaseElement(object):
"""
Base element of various PSD file structs. All the data objects in
:py:mod:`psd_tools.psd` subpackage inherit from this class.
.. py:classmethod:: read(cls, fp)
Read the element from a file-like object.
.. py:method:: write(self, fp)
Write the element to a file-like object.
.. py:classmethod:: frombytes(self, data, *args, **kwargs)
Read the element from bytes.
.. py:method:: tobytes(self, *args, **kwargs)
Write the element to bytes.
.. py:method:: validate(self)
Validate the attribute.
"""
[docs]
@classmethod
def read(cls, fp):
raise NotImplementedError()
[docs]
def write(self, fp):
raise NotImplementedError()
[docs]
@classmethod
def frombytes(self, data, *args, **kwargs):
with io.BytesIO(data) as f:
return self.read(f, *args, **kwargs)
[docs]
def tobytes(self, *args, **kwargs):
with io.BytesIO() as f:
self.write(f, *args, **kwargs)
return f.getvalue()
[docs]
def validate(self):
return attr.validate(self)
def _repr_pretty_(self, p, cycle):
if cycle:
return "{name}(...)".format(name=self.__class__.__name__)
with p.group(2, "{name}(".format(name=self.__class__.__name__), ")"):
p.breakable("")
fields = [f for f in attr.fields(self.__class__) if f.repr]
for idx, field in enumerate(fields):
if idx:
p.text(",")
p.breakable()
p.text("{field}=".format(field=field.name))
value = getattr(self, field.name)
if isinstance(value, bytes):
p.text(trimmed_repr(value))
elif isinstance(value, Enum):
p.text(value.name)
else:
p.pretty(value)
p.breakable("")
def _find(self, condition=None):
"""
Traversal API intended for debugging.
"""
for _ in BaseElement._traverse(self, condition):
yield _
@staticmethod
def _traverse(element, condition=None):
"""
Traversal API intended for debugging.
"""
if condition is None or condition(element):
yield element
if isinstance(element, DictElement):
for child in element.values():
for _ in BaseElement._traverse(child, condition):
yield _
elif isinstance(element, ListElement):
for child in element:
for _ in BaseElement._traverse(child, condition):
yield _
elif attr.has(element.__class__):
for field in attr.fields(element.__class__):
child = getattr(element, field.name)
for _ in BaseElement._traverse(child, condition):
yield _
[docs]
@attr.s(slots=True)
class EmptyElement(BaseElement):
"""
Empty element that does not have a value.
"""
@classmethod
def read(cls, fp, *args, **kwargs):
return cls()
def write(self, fp, *args, **kwargs):
return 0
[docs]
@attr.s(repr=False, eq=False, order=False)
class ValueElement(BaseElement):
"""
Single value wrapper that has a `value` attribute.
Pretty printing shows the internal value by default. Inherit with
`@attr.s(repr=False)` decorator to keep this behavior.
.. py:attribute:: value
Internal value.
"""
value = attr.ib(default=None)
def __lt__(self, other):
return self.value < other
def __le__(self, other):
return self.value <= other
def __eq__(self, other):
return self.value == other
def __ne__(self, other):
return self.value != other
def __gt__(self, other):
return self.value > other
def __ge__(self, other):
return self.value >= other
def __add__(self, other):
return self.value + other
def __sub__(self, other):
return self.value - other
def __mul__(self, other):
return self.value * other
def __mod__(self, other):
return self.value % other
def __rmul__(self, other):
return self.value.__rmul__(other)
def __rmod__(self, other):
return self.value.__rmod__(other)
def __hash__(self):
return self.value.__hash__()
def __bool__(self):
return self.value.__bool__()
def __repr__(self):
return self.value.__repr__()
def _repr_pretty_(self, p, cycle):
if cycle:
return self.__repr__()
if isinstance(self.value, bytes):
p.text(trimmed_repr(self.value))
else:
p.pretty(self.value)
[docs]
@attr.s(repr=False, eq=False, order=False)
class NumericElement(ValueElement):
"""
Single value element that has a numeric `value` attribute.
"""
value = attr.ib(default=0.0, type=float, converter=float)
def __floordiv__(self, other):
return self.value.__floordiv__(other)
def __div__(self, other):
return self.value.__div__(other)
def __truediv__(self, other):
return self.value.__truediv__(other)
def __divmod__(self, other):
return self.value.__divmod__(other)
def __pow__(self, other):
return self.value.__pow__(other)
def __radd__(self, other):
return self.value.__radd__(other)
def __rsub__(self, other):
return self.value.__rsub__(other)
def __rfloordiv__(self, other):
return self.value.__rfloordiv__(other)
def __rdiv__(self, other):
return self.value.__rdiv__(other)
def __rtruediv__(self, other):
return self.value.__rtruediv__(other)
def __rdivmod__(self, other):
return self.value.__rdivmod__(other)
def __rpow__(self, other):
return self.value.__rpow__(other)
def __nonzero__(self):
return self.value.__nonzero__()
def __neg__(self):
return self.value.__neg__()
def __pos__(self):
return self.value.__pos__()
def __abs__(self):
return self.value.__abs__()
def __int__(self):
return self.value.__int__()
def __long__(self):
return self.value.__long__()
def __float__(self):
return self.value.__float__()
def __coerce__(self, other):
return self.value.__coerce__(other)
@classmethod
def read(cls, fp, **kwargs):
return cls(read_fmt("d", fp)[0])
def write(self, fp, **kwargs):
return write_fmt(fp, "d", self.value)
[docs]
@attr.s(repr=False, eq=False, order=False)
class IntegerElement(NumericElement):
"""
Single integer value element that has a `value` attribute.
Use with `@attr.s(repr=False)` decorator.
"""
value = attr.ib(default=0, type=int, converter=int)
def __cmp__(self, other):
return self.value.__cmp__(other)
def __lshift__(self, other):
return self.value.__lshift__(other)
def __rshift__(self, other):
return self.value.__rshift__(other)
def __and__(self, other):
return self.value.__and__(other)
def __xor__(self, other):
return self.value.__xor__(other)
def __or__(self, other):
return self.value.__or__(other)
def __rlshift__(self, other):
return self.value.__rlshift__(other)
def __rrshift__(self, other):
return self.value.__rrshift__(other)
def __rand__(self, other):
return self.value.__rand__(other)
def __rxor__(self, other):
return self.value.__rxor__(other)
def __ror__(self, other):
return self.value.__ror__(other)
def __invert__(self):
return self.value.__invert__()
def __oct__(self):
return self.value.__oct__()
def __hex__(self):
return self.value.__hex__()
def __index__(self):
return self.value.__index__()
@classmethod
def read(cls, fp, **kwargs):
return cls(read_fmt("I", fp)[0])
def write(self, fp, **kwargs):
return write_fmt(fp, "I", self.value)
[docs]
@attr.s(repr=False, eq=False, order=False)
class ShortIntegerElement(IntegerElement):
"""
Single short integer element that has a `value` attribute.
Use with `@attr.s(repr=False)` decorator.
"""
@classmethod
def read(cls, fp, **kwargs):
try:
return cls(read_fmt("H2x", fp)[0])
except AssertionError as e:
logger.error(e)
return cls(read_fmt("H", fp)[0])
def write(self, fp, **kwargs):
return write_fmt(fp, "H2x", self.value)
[docs]
@attr.s(repr=False, eq=False, order=False)
class ByteElement(IntegerElement):
"""
Single 1-byte integer element that has a `value` attribute.
Use with `@attr.s(repr=False)` decorator.
"""
@classmethod
def read(cls, fp, **kwargs):
try:
return cls(read_fmt("B3x", fp)[0])
except AssertionError as e:
logger.error(e)
return cls(read_fmt("B", fp)[0])
def write(self, fp, **kwargs):
return write_fmt(fp, "B3x", self.value)
[docs]
@attr.s(repr=False, eq=False, order=False)
class BooleanElement(IntegerElement):
"""
Single bool value element that has a `value` attribute.
Use with `@attr.s(repr=False)` decorator.
"""
value = attr.ib(default=False, type=bool, converter=bool)
@classmethod
def read(cls, fp, **kwargs):
try:
return cls(read_fmt("?3x", fp)[0])
except AssertionError as e:
logger.error(e)
return cls(read_fmt("?", fp)[0])
def write(self, fp, **kwargs):
return write_fmt(fp, "?3x", self.value)
[docs]
@attr.s(repr=False, slots=True, eq=False, order=False)
class StringElement(ValueElement):
"""
Single unicode string.
.. py:attribute:: value
`str` value
"""
value = attr.ib(default="", type=str)
@classmethod
def read(cls, fp, padding=1, **kwargs):
return cls(read_unicode_string(fp, padding=padding))
def write(self, fp, padding=1, **kwargs):
return write_unicode_string(fp, self.value, padding=padding)
[docs]
@attr.s(repr=False)
class ListElement(BaseElement):
"""
List-like element that has `items` list.
"""
_items = attr.ib(factory=list, converter=list)
def append(self, x):
return self._items.append(x)
def extend(self, L):
return self._items.extend(L)
def insert(self, i, x):
return self._items.insert(i, x)
def remove(self, x):
return self._items.remove(x)
def pop(self, *args):
return self._items.pop(*args)
def index(self, x):
return self._items.index(x)
def count(self, x):
return self._items.count(x)
def sort(self, *args, **kwargs):
return self._items.sort(*args, **kwargs)
def reverse(self):
return self._items.reverse()
def __len__(self):
return self._items.__len__()
def __iter__(self):
return self._items.__iter__()
def __getitem__(self, key):
return self._items.__getitem__(key)
def __setitem__(self, key, value):
return self._items.__setitem__(key, value)
def __delitem__(self, key):
return self._items.__delitem__(key)
def __repr__(self):
return self._items.__repr__()
def _repr_pretty_(self, p, cycle):
if cycle:
return "[...]"
with p.group(2, "[", "]"):
p.breakable("")
for idx in range(len(self._items)):
if idx:
p.text(",")
p.breakable()
value = self._items[idx]
if isinstance(value, bytes):
value = trimmed_repr(value)
p.pretty(value)
p.breakable("")
def write(self, fp, *args, **kwargs):
written = 0
for item in self:
if hasattr(item, "write"):
written += item.write(fp, *args, **kwargs)
elif isinstance(item, bytes):
written += write_bytes(fp, item)
return written
[docs]
@attr.s(repr=False)
class DictElement(BaseElement):
"""
Dict-like element that has `items` OrderedDict.
"""
_items = attr.ib(factory=OrderedDict, converter=OrderedDict)
def clear(self):
return self._items.clear()
def copy(self):
return self._items.copy()
@classmethod
def fromkeys(cls, seq, *args):
return cls(OrderedDict.fromkeys(seq, *args))
def get(self, key, *args):
key = self._key_converter(key)
return self._items.get(key, *args)
def items(self):
return self._items.items()
def keys(self):
return self._items.keys()
def pop(self, key, *args):
key = self._key_converter(key)
return self._items.pop(key, *args)
def popitem(self):
return self._items.popitem()
def setdefault(self, key, *args):
key = self._key_converter(key)
return self._items.setdefault(key, *args)
def update(self, *args):
return self._items.update(*args)
def values(self):
return self._items.values()
def __len__(self):
return self._items.__len__()
def __iter__(self):
return self._items.__iter__()
def __getitem__(self, key):
key = self._key_converter(key)
return self._items.__getitem__(key)
def __setitem__(self, key, value):
key = self._key_converter(key)
return self._items.__setitem__(key, value)
def __delitem__(self, key):
key = self._key_converter(key)
return self._items.__delitem__(key)
def __contains__(self, key):
key = self._key_converter(key)
return self._items.__contains__(key)
def __repr__(self):
return dict.__repr__(self._items)
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]
p.pretty(key)
p.text(": ")
if isinstance(value, bytes):
value = trimmed_repr(value)
p.pretty(value)
p.breakable("")
@classmethod
def _key_converter(cls, key):
return key
@classmethod
def read(cls, fp, *args, **kwargs):
raise NotImplementedError
def write(self, fp, *args, **kwargs):
written = 0
for key in self:
value = self[key]
if hasattr(value, "write"):
written += value.write(fp, *args, **kwargs)
elif isinstance(value, bytes):
written += write_bytes(fp, value)
return written