"""
Linked layer structure.
"""
import io
import logging
from typing import Any, BinaryIO, TypeVar
from attrs import define, field
from psd_tools.constants import LinkedLayerType
from psd_tools.psd.base import BaseElement, ListElement
from psd_tools.psd.descriptor import DescriptorBlock
from psd_tools.psd.bin_utils import (
is_readable,
read_fmt,
read_length_block,
read_pascal_string,
read_unicode_string,
write_bytes,
write_fmt,
write_length_block,
write_padding,
write_pascal_string,
write_unicode_string,
)
from psd_tools.validators import in_, range_
logger = logging.getLogger(__name__)
T_LinkedLayers = TypeVar("T_LinkedLayers", bound="LinkedLayers")
T_LinkedLayer = TypeVar("T_LinkedLayer", bound="LinkedLayer")
[docs]
class LinkedLayers(ListElement):
"""
List of LinkedLayer structure. See :py:class:`.LinkedLayer`.
"""
@classmethod
def read(cls: type[T_LinkedLayers], fp: BinaryIO, **kwargs: Any) -> T_LinkedLayers:
items = []
while is_readable(fp, 8):
data = read_length_block(fp, fmt="Q", padding=4)
with io.BytesIO(data) as f:
items.append(LinkedLayer.read(f))
return cls(items) # type: ignore[arg-type]
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = 0
for item in self:
written += write_length_block(fp, item.write, fmt="Q", padding=4)
return written
[docs]
@define(repr=False)
class LinkedLayer(BaseElement):
"""
LinkedLayer structure.
.. py:attribute:: kind
.. py:attribute:: version
.. py:attribute:: uuid
.. py:attribute:: filename
.. py:attribute:: filetype
.. py:attribute:: creator
.. py:attribute:: filesize
.. py:attribute:: open_file
.. py:attribute:: linked_file
.. py:attribute:: timestamp
.. py:attribute:: data
.. py:attribute:: child_id
.. py:attribute:: mod_time
.. py:attribute:: lock_state
"""
kind: LinkedLayerType = field(
default=LinkedLayerType.ALIAS, validator=in_(LinkedLayerType)
)
version: int = field(default=1, validator=range_(1, 8))
uuid: str = ""
filename: str = ""
filetype: bytes = b"\x00\x00\x00\x00"
creator: bytes = b"\x00\x00\x00\x00"
filesize: int | None = None
open_file: DescriptorBlock | None = None
linked_file: DescriptorBlock | None = None
timestamp: tuple | None = None
data: bytes | None = None
child_id: str | None = None
mod_time: float | None = None
lock_state: int | None = None
@classmethod
def read(cls: type[T_LinkedLayer], fp: BinaryIO, **kwargs: Any) -> T_LinkedLayer:
kind = LinkedLayerType(read_fmt("4s", fp)[0])
version = read_fmt("I", fp)[0]
assert 1 <= version and version <= 8, "Invalid version %d" % (version)
uuid = read_pascal_string(fp, "macroman", padding=1)
filename = read_unicode_string(fp)
filetype, creator, datasize, open_file = read_fmt("4s4sQB", fp)
if open_file:
open_file = DescriptorBlock.read(fp, padding=1)
else:
open_file = None
linked_file = None
timestamp = None
data = None
filesize = None
child_id = None
mod_time = None
lock_state = None
if kind == LinkedLayerType.EXTERNAL:
linked_file = DescriptorBlock.read(fp, padding=1)
if version > 3:
timestamp = read_fmt("I4Bd", fp)
filesize = read_fmt("Q", fp)[0] # External file size.
if version > 2:
data = fp.read(datasize)
elif kind == LinkedLayerType.ALIAS:
read_fmt("8x", fp)
if kind == LinkedLayerType.DATA:
data = fp.read(datasize)
assert len(data) == datasize, "(%d vs %d)" % (len(data), datasize)
# The followings are not well documented...
if version >= 5:
child_id = read_unicode_string(fp)
if version >= 6:
mod_time = read_fmt("d", fp)[0]
if version >= 7:
lock_state = read_fmt("B", fp)[0]
if kind == LinkedLayerType.EXTERNAL and version == 2:
data = fp.read(datasize)
return cls(
kind,
version,
uuid,
filename,
filetype,
creator,
filesize,
open_file,
linked_file,
timestamp,
data,
child_id,
mod_time,
lock_state,
)
def write(self, fp: BinaryIO, padding: int = 1, **kwargs: Any) -> int:
written = write_fmt(fp, "4sI", self.kind.value, self.version)
written += write_pascal_string(fp, self.uuid, "macroman", padding=1)
written += write_unicode_string(fp, self.filename)
written += write_fmt(
fp,
"4s4sQB",
self.filetype,
self.creator,
len(self.data) if self.data is not None else 0,
self.open_file is not None,
)
if self.open_file is not None:
written += self.open_file.write(fp, padding=1)
if self.kind == LinkedLayerType.EXTERNAL:
written += self.linked_file.write(fp, padding=1) # type: ignore[union-attr]
if self.version > 3:
written += write_fmt(fp, "I4Bd", *self.timestamp) # type: ignore[misc,operator]
written += write_fmt(fp, "Q", self.filesize)
if self.version > 2:
assert self.data is not None
written += write_bytes(fp, self.data)
elif self.kind == LinkedLayerType.ALIAS:
written += write_fmt(fp, "8x")
if self.kind == LinkedLayerType.DATA:
assert self.data is not None
written += write_bytes(fp, self.data)
if self.child_id is not None:
written += write_unicode_string(fp, self.child_id)
if self.mod_time is not None:
written += write_fmt(fp, "d", self.mod_time)
if self.lock_state is not None:
written += write_fmt(fp, "B", self.lock_state)
if self.kind == LinkedLayerType.EXTERNAL and self.version == 2:
assert self.data is not None
written += write_bytes(fp, self.data)
written += write_padding(fp, written, padding)
return written