"""
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.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.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