"""
Image resources section structure. Image resources are used to store non-pixel
data associated with images, such as pen tool paths or slices.
See :py:class:`~psd_tools.constants.Resource` to check available
resource names.
Example::
from psd_tools.constants import Resource
version_info = psd.image_resources.get_data(Resource.VERSION_INFO)
The following resources are plain bytes::
Resource.OBSOLETE1: 1000
Resource.MAC_PRINT_MANAGER_INFO: 1001
Resource.MAC_PAGE_FORMAT_INFO: 1002
Resource.OBSOLETE2: 1003
Resource.DISPLAY_INFO_OBSOLETE: 1007
Resource.BORDER_INFO: 1009
Resource.DUOTONE_IMAGE_INFO: 1018
Resource.EFFECTIVE_BW: 1019
Resource.OBSOLETE3: 1020
Resource.EPS_OPTIONS: 1021
Resource.QUICK_MASK_INFO: 1022
Resource.OBSOLETE4: 1023
Resource.WORKING_PATH: 1025
Resource.OBSOLETE5: 1027
Resource.IPTC_NAA: 1028
Resource.IMAGE_MODE_RAW: 1029
Resource.JPEG_QUALITY: 1030
Resource.URL: 1035
Resource.COLOR_SAMPLERS_RESOURCE_OBSOLETE: 1038
Resource.ICC_PROFILE: 1039
Resource.SPOT_HALFTONE: 1043
Resource.JUMP_TO_XPEP: 1052
Resource.EXIF_DATA_1: 1058
Resource.EXIF_DATA_3: 1059
Resource.XMP_METADATA: 1060
Resource.CAPTION_DIGEST: 1061
Resource.ALTERNATE_DUOTONE_COLORS: 1066
Resource.ALTERNATE_SPOT_COLORS: 1067
Resource.HDR_TONING_INFO: 1070
Resource.PRINT_INFO_CS2: 1071
Resource.COLOR_SAMPLERS_RESOURCE: 1073
Resource.DISPLAY_INFO: 1077
Resource.MAC_NSPRINTINFO: 1084
Resource.WINDOWS_DEVMODE: 1085
Resource.PATH_INFO_N: 2000-2999
Resource.PLUGIN_RESOURCES_N: 4000-4999
Resource.IMAGE_READY_VARIABLES: 7000
Resource.IMAGE_READY_DATA_SETS: 7001
Resource.IMAGE_READY_DEFAULT_SELECTED_STATE: 7002
Resource.IMAGE_READY_7_ROLLOVER_EXPANDED_STATE: 7003
Resource.IMAGE_READY_ROLLOVER_EXPANDED_STATE: 7004
Resource.IMAGE_READY_SAVE_LAYER_SETTINGS: 7005
Resource.IMAGE_READY_VERSION: 7006
Resource.LIGHTROOM_WORKFLOW: 8000
"""
import io
import logging
from typing import Any, BinaryIO, TypeVar
from attrs import define, field, astuple
from psd_tools.constants import PrintScaleStyle, Resource, AlphaChannelMode
from psd_tools.psd.base import (
BaseElement,
ByteElement,
DictElement,
IntegerElement,
ListElement,
NumericElement,
ShortIntegerElement,
StringElement,
ValueElement,
)
from psd_tools.psd.color import Color
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,
trimmed_repr,
write_bytes,
write_fmt,
write_length_block,
write_pascal_string,
write_unicode_string,
)
from psd_tools.registry import new_registry
from psd_tools.validators import in_
from psd_tools.version import __version__
logger = logging.getLogger(__name__)
T_ImageResources = TypeVar("T_ImageResources", bound="ImageResources")
T_ImageResource = TypeVar("T_ImageResource", bound="ImageResource")
TYPES, register = new_registry()
TYPES.update(
{
Resource.BACKGROUND_COLOR: Color,
Resource.LAYER_COMPS: DescriptorBlock,
Resource.MEASUREMENT_SCALE: DescriptorBlock,
Resource.SHEET_DISCLOSURE: DescriptorBlock,
Resource.TIMELINE_INFO: DescriptorBlock,
Resource.ONION_SKINS: DescriptorBlock,
Resource.COUNT_INFO: DescriptorBlock,
Resource.PRINT_INFO_CS5: DescriptorBlock,
Resource.PRINT_STYLE: DescriptorBlock,
Resource.PATH_SELECTION_STATE: DescriptorBlock,
Resource.ORIGIN_PATH_INFO: DescriptorBlock,
Resource.AUTO_SAVE_FILE_PATH: StringElement,
Resource.AUTO_SAVE_FORMAT: StringElement,
Resource.WORKFLOW_URL: StringElement,
}
)
[docs]
@define(repr=False)
class ImageResources(DictElement):
"""
Image resources section of the PSD file. Dict of
:py:class:`.ImageResource`.
"""
[docs]
def get_data(self, key: Any, default: Any = None) -> Any:
"""
Get data from the image resources.
Shortcut for the following::
if key in image_resources:
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
[docs]
@classmethod
def new(cls: type[T_ImageResources], **kwargs: Any) -> T_ImageResources:
"""
Create a new default image resouces.
:return: ImageResources
"""
return cls( # type: ignore[arg-type]
[
(
Resource.VERSION_INFO,
ImageResource(
key=Resource.VERSION_INFO,
data=VersionInfo( # type: ignore[arg-type]
has_composite=True,
writer="psd-tools %s" % __version__,
reader="psd-tools %s" % __version__,
),
),
),
]
)
@classmethod
def read(
cls: type[T_ImageResources],
fp: BinaryIO,
encoding: str = "macroman",
**kwargs: Any,
) -> T_ImageResources:
data = read_length_block(fp)
logger.debug("reading image resources, len=%d" % (len(data)))
with io.BytesIO(data) as f:
return cls._read_body(f, encoding=encoding)
@classmethod
def _read_body(
cls: type[T_ImageResources], fp: BinaryIO, *args: Any, **kwargs: Any
) -> T_ImageResources:
items = []
while is_readable(fp, 4):
item = ImageResource.read(fp, *args, **kwargs)
items.append((item.key, item))
return cls(items) # type: ignore[arg-type]
def write(self, fp: BinaryIO, encoding: str = "macroman", **kwargs: Any) -> int:
def writer(f: BinaryIO) -> int:
written = sum(self[key].write(f, encoding) for key in self)
logger.debug("writing image resources, len=%d" % (written))
return written
return write_length_block(fp, writer)
@classmethod
def _key_converter(cls, key: Any) -> Any:
return getattr(key, "value", key)
def _repr_pretty_(self, p: Any, cycle: bool) -> None:
if cycle:
return p.text("{{...}")
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(Resource(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]
@define(repr=False)
class ImageResource(BaseElement):
"""
Image resource block.
.. py:attribute:: signature
Binary signature. Usually ``b'8BIM'``, but some resources use
alternative signatures such as ``b'MeSa'`` (e.g.,
:py:attr:`~psd_tools.constants.Resource.IMAGE_READY_VARIABLES`),
``b'AgHg'``, ``b'PHUT'``, or ``b'DCSR'``.
.. py:attribute:: key
Unique identifier for the resource. See
:py:class:`~psd_tools.constants.Resource`.
.. py:attribute:: name
.. py:attribute:: data
The resource data.
"""
signature: bytes = field(
default=b"8BIM",
repr=False,
validator=in_({b"8BIM", b"MeSa", b"AgHg", b"PHUT", b"DCSR"}),
)
key: int = 1000
name: str = ""
data: bytes = field(default=b"", repr=False)
@classmethod
def read(
cls: type[T_ImageResource],
fp: BinaryIO,
encoding: str = "macroman",
**kwargs: Any,
) -> T_ImageResource:
signature, key = read_fmt("4sH", fp)
try:
key = Resource(key)
except ValueError:
if Resource.is_path_info(key):
logger.debug("Undefined PATH_INFO found: %d" % (key))
elif Resource.is_plugin_resource(key):
logger.debug("Undefined PLUGIN_RESOURCE found: %d" % (key))
else:
logger.info("Unknown image resource %d" % (key))
name = read_pascal_string(fp, encoding, padding=2)
raw_data = read_length_block(fp, padding=2)
if key in TYPES:
data = TYPES[key].frombytes(raw_data)
# try:
# _raw_data = data.tobytes(padding=1)
# assert _raw_data == raw_data, '%r vs %r' % (
# _raw_data, raw_data
# )
# except AssertionError as e:
# logger.error(e)
# raise
else:
data = raw_data
return cls(signature, key, name, data)
def write(self, fp: BinaryIO, encoding: str = "macroman", **kwargs: Any) -> int:
written = write_fmt(
fp, "4sH", self.signature, getattr(self.key, "value", self.key)
)
written += write_pascal_string(fp, self.name, encoding, 2)
def writer(f: BinaryIO) -> int:
if hasattr(self.data, "write"):
return self.data.write(f, padding=1)
return write_bytes(f, self.data)
written += write_length_block(fp, writer, padding=2)
return written
[docs]
@register(Resource.ALPHA_IDENTIFIERS)
class AlphaIdentifiers(ListElement):
"""
List of alpha identifiers.
"""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "AlphaIdentifiers":
items = []
while is_readable(fp, 4):
items.append(read_fmt("I", fp)[0])
return cls(items)
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return sum(write_fmt(fp, "I", item) for item in self)
[docs]
@register(Resource.ALPHA_NAMES_PASCAL)
class AlphaNamesPascal(ListElement):
"""
List of alpha names.
"""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "AlphaNamesPascal":
items = []
while is_readable(fp):
items.append(read_pascal_string(fp, "macroman", padding=1))
return cls(items) # type: ignore[arg-type]
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return sum(write_pascal_string(fp, item, padding=1) for item in self)
[docs]
@register(Resource.ALPHA_NAMES_UNICODE)
class AlphaNamesUnicode(ListElement):
"""
List of alpha names.
"""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "AlphaNamesUnicode":
items = []
while is_readable(fp):
items.append(read_unicode_string(fp))
return cls(items) # type: ignore[arg-type]
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return sum(write_unicode_string(fp, item) for item in self)
@register(Resource.DISPLAY_INFO)
@define(repr=False)
class DisplayInfo(BaseElement):
"""
DisplayInfo is a list of AlphaChannels
"""
version: int = 1
alpha_channels: list = field(factory=list, converter=list)
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "DisplayInfo":
# ref: https://github.com/MolecularMatters/psd_sdk/blob/311b5c2e3fe04c8cc6a563665e66b19b3fcf8116/src/Psd/PsdParseImageResourcesSection.cpp#L83
version = read_fmt("I", fp)[0]
items = []
while is_readable(fp, 13):
items.append(AlphaChannel.read(fp))
return cls(version=version, alpha_channels=items) # type: ignore[arg-type]
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = write_fmt(fp, "I", self.version)
written += sum(item.write(fp) for item in self.alpha_channels)
return written
@define(repr=False)
class AlphaChannel(BaseElement):
color_space: int = 0
c1: int = 0
c2: int = 0
c3: int = 0
c4: int = 0
opacity: int = 0
mode: AlphaChannelMode = AlphaChannelMode.ALPHA # type: ignore[assignment]
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "AlphaChannel":
vals = read_fmt("6H", fp)
mode = AlphaChannelMode(read_fmt("B", fp)[0])
return cls(*vals, mode) # type: ignore[call-arg]
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = write_fmt(
fp, "6H", self.color_space, self.c1, self.c2, self.c3, self.c4, self.opacity
)
written += write_fmt(fp, "B", self.mode)
return written
[docs]
@register(Resource.ICC_UNTAGGED_PROFILE)
@register(Resource.COPYRIGHT_FLAG)
@register(Resource.EFFECTS_VISIBLE)
@register(Resource.WATERMARK)
class Byte(ByteElement):
"""
Byte element.
"""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "Byte":
return cls(*read_fmt("B", fp))
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return write_fmt(fp, "B", self.value)
[docs]
@register(Resource.GRID_AND_GUIDES_INFO)
@define(repr=False)
class GridGuidesInfo(BaseElement):
"""
Grid and guides info structure.
.. py:attribute: version
"""
version: int = 1
horizontal: int = 0
vertical: int = 0
data: list = field(factory=list, converter=list)
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "GridGuidesInfo":
version, horizontal, vertical, count = read_fmt("4I", fp)
items = []
for _ in range(count):
items.append(read_fmt("IB", fp))
return cls(
version=version,
horizontal=horizontal,
vertical=vertical,
data=items, # type: ignore[arg-type]
)
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = write_fmt(
fp, "4I", self.version, self.horizontal, self.vertical, len(self.data)
)
written += sum(write_fmt(fp, "IB", *item) for item in self.data)
return written
[docs]
@register(Resource.COLOR_HALFTONING_INFO)
@register(Resource.DUOTONE_HALFTONING_INFO)
@register(Resource.GRAYSCALE_HALFTONING_INFO)
class HalftoneScreens(ListElement):
"""
Halftone screens.
"""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "HalftoneScreens":
items = []
while is_readable(fp, 18):
items.append(HalftoneScreen.read(fp))
return cls(items) # type: ignore[arg-type]
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return sum(item.write(fp) for item in self)
[docs]
@define(repr=False)
class HalftoneScreen(BaseElement):
"""
Halftone screen.
.. py:attribute:: freq
.. py:attribute:: unit
.. py:attribute:: angle
.. py:attribute:: shape
.. py:attribute:: use_accurate
.. py:attribute:: use_printer
"""
freq: int = 0
unit: int = 0
angle: int = 0
shape: int = 0
use_accurate: bool = False
use_printer: bool = False
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "HalftoneScreen":
freq = float(read_fmt("I", fp)[0]) / 0x10000
unit = read_fmt("H", fp)[0]
angle = float(read_fmt("i", fp)[0]) / 0x10000
shape, use_accurate, use_printer = read_fmt("H4x2?", fp)
return cls(freq, unit, angle, shape, use_accurate, use_printer) # type: ignore[arg-type]
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = write_fmt(fp, "I", int(self.freq * 0x10000))
written += write_fmt(fp, "H", self.unit)
written += write_fmt(fp, "i", int(self.angle * 0x10000))
written += write_fmt(
fp, "H4x2?", self.shape, self.use_accurate, self.use_printer
)
return written
[docs]
@register(Resource.GLOBAL_ALTITUDE)
@register(Resource.GLOBAL_ANGLE)
@register(Resource.IDS_SEED_NUMBER)
class Integer(IntegerElement):
"""
Integer element.
"""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "Integer":
return cls(*read_fmt("i", fp))
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return write_fmt(fp, "i", self.value)
[docs]
@register(Resource.LAYER_GROUPS_ENABLED_ID)
class LayerGroupEnabledIDs(ListElement):
"""
Layer group enabled ids.
"""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "LayerGroupEnabledIDs":
items = []
while is_readable(fp, 1):
items.append(read_fmt("B", fp)[0])
return cls(items)
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return sum(write_fmt(fp, "B", item) for item in self)
[docs]
@register(Resource.LAYER_GROUP_INFO)
class LayerGroupInfo(ListElement):
"""
Layer group info list.
"""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "LayerGroupInfo":
items = []
while is_readable(fp, 2):
items.append(read_fmt("H", fp)[0])
return cls(items)
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return sum(write_fmt(fp, "H", item) for item in self)
[docs]
@register(Resource.LAYER_SELECTION_IDS)
class LayerSelectionIDs(ListElement):
"""
Layer selection ids.
"""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "LayerSelectionIDs":
count = read_fmt("H", fp)[0]
return cls(read_fmt("I", fp)[0] for _ in range(count))
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = write_fmt(fp, "H", len(self))
written += sum(write_fmt(fp, "I", item) for item in self)
return written
[docs]
@register(Resource.INDEXED_COLOR_TABLE_COUNT)
@register(Resource.LAYER_STATE_INFO)
@register(Resource.TRANSPARENCY_INDEX)
class ShortInteger(ShortIntegerElement):
"""
Short integer element.
"""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "ShortInteger":
return cls(*read_fmt("H", fp))
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return write_fmt(fp, "H", self.value)
[docs]
@register(Resource.CAPTION_PASCAL)
@register(Resource.CLIPPING_PATH_NAME)
class PascalString(ValueElement):
"""
Pascal string element.
"""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "PascalString":
return cls(read_pascal_string(fp, "macroman"))
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return write_pascal_string(fp, self.value, "macroman", padding=1) # type: ignore[arg-type]
[docs]
@register(Resource.PIXEL_ASPECT_RATIO)
@define(repr=False)
class PixelAspectRatio(NumericElement):
"""
Pixel aspect ratio.
.. py:attribute: version
"""
version: int = 1
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "PixelAspectRatio":
version, value = read_fmt("Id", fp)
return cls(version=version, value=value)
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return write_fmt(fp, "Id", self.version, self.value)
[docs]
@register(Resource.PRINT_FLAGS)
@define(repr=False)
class PrintFlags(BaseElement):
"""
Print flags.
.. py:attribute: labels
.. py:attribute: crop_marks
.. py:attribute: colorbars
.. py:attribute: registration_marks
.. py:attribute: negative
.. py:attribute: flip
.. py:attribute: interpolate
.. py:attribute: caption
.. py:attribute: print_flags
"""
labels: bool = False
crop_marks: bool = False
colorbars: bool = False
registration_marks: bool = False
negative: bool = False
flip: bool = False
interpolate: bool = False
caption: bool = False
print_flags: bool | None = None # Not existing for old versions.
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "PrintFlags":
values = read_fmt("8?", fp)
print_flags_value = read_fmt("?", fp)[0] if is_readable(fp) else None
return cls(
labels=values[0],
crop_marks=values[1],
colorbars=values[2],
registration_marks=values[3],
negative=values[4],
flip=values[5],
interpolate=values[6],
caption=values[7],
print_flags=print_flags_value,
)
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
values = astuple(self)
if self.print_flags is None:
values = values[:-1]
return write_fmt(fp, "%d?" % len(values), *values)
[docs]
@register(Resource.PRINT_FLAGS_INFO)
@define(repr=False)
class PrintFlagsInfo(BaseElement):
"""
Print flags info structure.
.. py:attribute:: version
.. py:attribute:: center_crop
.. py:attribute:: bleed_width_value
.. py:attribute:: bleed_width_scale
"""
version: int = 0
center_crop: int = 0
bleed_width_value: int = 0
bleed_width_scale: int = 0
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "PrintFlagsInfo":
return cls(*read_fmt("HBxIH", fp))
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return write_fmt(fp, "HBxIH", *astuple(self))
[docs]
@register(Resource.PRINT_SCALE)
@define(repr=False)
class PrintScale(BaseElement):
"""
Print scale structure.
.. py:attribute:: style
.. py:attribute:: x
.. py:attribute:: y
.. py:attribute:: scale
"""
style: PrintScaleStyle = field(
default=PrintScaleStyle.CENTERED,
converter=PrintScaleStyle,
validator=in_(PrintScaleStyle),
)
x: float = 0.0
y: float = 0.0
scale: float = 0.0
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "PrintScale":
return cls(*read_fmt("H3f", fp))
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return write_fmt(fp, "H3f", self.style.value, self.x, self.y, self.scale)
[docs]
@register(Resource.RESOLUTION_INFO)
@define(repr=False)
class ResoulutionInfo(BaseElement):
"""
Resoulution info structure.
.. py:attribute:: horizontal
.. py:attribute:: horizontal_unit
.. py:attribute:: width_unit
.. py:attribute:: vertical
.. py:attribute:: vertical_unit
.. py:attribute:: height_unit
"""
horizontal: int = 0
horizontal_unit: int = 0
width_unit: int = 0
vertical: int = 0
vertical_unit: int = 0
height_unit: int = 0
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "ResoulutionInfo":
return cls(*read_fmt("I2HI2H", fp))
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return write_fmt(fp, "I2HI2H", *astuple(self))
[docs]
@register(Resource.SLICES)
@define(repr=False)
class Slices(BaseElement):
"""
Slices resource.
.. py:attribute:: version
.. py:attribute:: data
"""
version: int = field(default=0, validator=in_((6, 7, 8)))
data: object = None
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "Slices":
version = read_fmt("I", fp)[0]
assert version in (6, 7, 8), "Invalid version %d" % (version)
if version == 6:
return cls(version=version, data=SlicesV6.read(fp))
return cls(version=version, data=DescriptorBlock.read(fp))
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = write_fmt(fp, "I", self.version)
if hasattr(self.data, "write"):
written += self.data.write(fp, padding=1)
return written
[docs]
@define(repr=False)
class SlicesV6(BaseElement):
"""
Slices resource version 6.
.. py:attribute:: bbox
.. py:attribute:: name
.. py:attribute:: items
"""
bbox: list = field(factory=lambda: [0, 0, 0, 0], converter=list)
name: str = ""
items: list = field(factory=list, converter=list)
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "SlicesV6":
bbox = read_fmt("4I", fp)
name = read_unicode_string(fp)
count = read_fmt("I", fp)[0]
items = [SliceV6.read(fp) for _ in range(count)]
return cls(bbox=bbox, name=name, items=items) # type: ignore[arg-type]
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = write_fmt(fp, "4I", *self.bbox)
written += write_unicode_string(fp, self.name)
written += write_fmt(fp, "I", len(self.items))
written += sum(item.write(fp) for item in self.items)
return written
[docs]
@define(repr=False)
class SliceV6(BaseElement):
"""
Slice element for version 6.
.. py:attribute:: slice_id
.. py:attribute:: group_id
.. py:attribute:: origin
.. py:attribute:: associated_id
.. py:attribute:: name
.. py:attribute:: slice_type
.. py:attribute:: bbox
.. py:attribute:: url
.. py:attribute:: target
.. py:attribute:: message
.. py:attribute:: alt_tag
.. py:attribute:: cell_is_html
.. py:attribute:: cell_text
.. py:attribute:: horizontal
.. py:attribute:: vertical
.. py:attribute:: alpha
.. py:attribute:: red
.. py:attribute:: green
.. py:attribute:: blue
.. py:attribute:: data
"""
slice_id: int = 0
group_id: int = 0
origin: int = 0
associated_id: int | None = None
name: str = ""
slice_type: int = 0
bbox: list = field(factory=lambda: [0, 0, 0, 0], converter=list)
url: str = ""
target: str = ""
message: str = ""
alt_tag: str = ""
cell_is_html: bool = False
cell_text: str = ""
horizontal_align: int = 0
vertical_align: int = 0
alpha: int = 0
red: int = 0
green: int = 0
blue: int = 0
data: object = None
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "SliceV6":
slice_id, group_id, origin = read_fmt("3I", fp)
associated_id = read_fmt("I", fp)[0] if origin == 1 else None
name = read_unicode_string(fp)
slice_type = read_fmt("I", fp)[0]
bbox = read_fmt("4I", fp)
url = read_unicode_string(fp)
target = read_unicode_string(fp)
message = read_unicode_string(fp)
alt_tag = read_unicode_string(fp)
cell_is_html = read_fmt("?", fp)[0]
cell_text = read_unicode_string(fp)
horizontal_align, vertical_align = read_fmt("2I", fp)
alpha, red, green, blue = read_fmt("4B", fp)
data = None
if is_readable(fp, 4):
# There is no easy distinction between descriptor block and
# next slice v6 item here...
current_position = fp.tell()
version = read_fmt("I", fp)[0]
fp.seek(-4, 1)
if version == 16:
try:
data = DescriptorBlock.read(fp)
if data.classID == b"\x00\x00\x00\x00":
data = None
raise ValueError(data)
except ValueError:
logger.debug("Failed to read DescriptorBlock")
fp.seek(current_position)
return cls(
slice_id=slice_id,
group_id=group_id,
origin=origin,
associated_id=associated_id,
name=name,
slice_type=slice_type,
bbox=bbox,
url=url,
target=target,
message=message,
alt_tag=alt_tag,
cell_is_html=cell_is_html,
cell_text=cell_text,
horizontal_align=horizontal_align,
vertical_align=vertical_align,
alpha=alpha,
red=red,
green=green,
blue=blue,
data=data,
)
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = write_fmt(fp, "3I", self.slice_id, self.group_id, self.origin)
if self.origin == 1 and self.associated_id is not None:
written += write_fmt(fp, "I", self.associated_id)
written += write_unicode_string(fp, self.name, padding=1)
written += write_fmt(fp, "I", self.slice_type)
written += write_fmt(fp, "4I", *self.bbox)
written += write_unicode_string(fp, self.url, padding=1)
written += write_unicode_string(fp, self.target, padding=1)
written += write_unicode_string(fp, self.message, padding=1)
written += write_unicode_string(fp, self.alt_tag, padding=1)
written += write_fmt(fp, "?", self.cell_is_html)
written += write_unicode_string(fp, self.cell_text, padding=1)
written += write_fmt(fp, "2I", self.horizontal_align, self.vertical_align)
written += write_fmt(fp, "4B", self.alpha, self.red, self.green, self.blue)
if self.data is not None:
if hasattr(self.data, "write"):
written += self.data.write(fp, padding=1)
elif self.data:
written += write_bytes(fp, self.data) # type: ignore[arg-type]
return written
[docs]
@register(Resource.THUMBNAIL_RESOURCE)
@define(repr=False)
class ThumbnailResource(BaseElement):
"""
Thumbnail resource structure.
.. py:attribute:: fmt
.. py:attribute:: width
.. py:attribute:: height
.. py:attribute:: row
.. py:attribute:: total_size
.. py:attribute:: size
.. py:attribute:: bits
.. py:attribute:: planes
.. py:attribute:: data
"""
_RAW_MODE = "RGB"
fmt: int = 0
width: int = 0
height: int = 0
row: int = 0
total_size: int = 0
bits: int = 0
planes: int = 0
data: bytes = b""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "ThumbnailResource":
fmt, width, height, row, total_size, size, bits, planes = read_fmt("6I2H", fp)
data = fp.read(size)
return cls(fmt, width, height, row, total_size, bits, planes, data)
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = write_fmt(
fp,
"6I2H",
self.fmt,
self.width,
self.height,
self.row,
self.total_size,
len(self.data),
self.bits,
self.planes,
)
written += write_bytes(fp, self.data)
return written
[docs]
@register(Resource.THUMBNAIL_RESOURCE_PS4)
class ThumbnailResourceV4(ThumbnailResource):
_RAW_MODE = "BGR"
[docs]
@register(Resource.COLOR_TRANSFER_FUNCTION)
@register(Resource.DUOTONE_TRANSFER_FUNCTION)
@register(Resource.GRAYSCALE_TRANSFER_FUNCTION)
class TransferFunctions(ListElement):
"""
Transfer functions.
"""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "TransferFunctions":
items = []
while is_readable(fp, 28):
items.append(TransferFunction.read(fp))
return cls(items) # type: ignore[arg-type]
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return sum(item.write(fp) for item in self)
[docs]
@define(repr=False)
class TransferFunction(BaseElement):
"""
Transfer function for a single plate.
Contains 13 signed short integers representing curve control points in the
range 0...1000 (corresponding to 0.0%...100.0%). The first and last values
are always present; any intermediate value may be -1 to indicate that no
control point exists at that position. A NULL (identity) transfer curve has
the form::
[0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1000]
Followed by one unsigned short ``override`` flag (non-zero means the
transfer function overrides the default).
See `Adobe PSD spec §Transfer Functions
<https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1071095>`_.
"""
curve: list[int] = field(factory=list, converter=list)
override: int = 0
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "TransferFunction":
curve = read_fmt("13h", fp)
override = read_fmt("H", fp)[0]
return cls(curve=curve, override=override)
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = write_fmt(fp, "13h", *self.curve)
written += write_fmt(fp, "H", self.override)
return written
[docs]
@register(Resource.URL_LIST)
class URLList(ListElement):
"""
URL list structure.
"""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "URLList":
count = read_fmt("I", fp)[0]
items = []
for _ in range(count):
items.append(URLItem.read(fp))
return cls(items) # type: ignore[arg-type]
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = write_fmt(fp, "I", len(self))
written += sum(item.write(fp) for item in self)
return written
[docs]
@define(repr=False)
class URLItem(BaseElement):
"""
URL item.
.. py:attribute:: number
.. py:attribute:: id
.. py:attribute:: name
"""
number: int = 0
id: int = 0
name: str = ""
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "URLItem":
number, id = read_fmt("2I", fp)
name = read_unicode_string(fp)
return cls(number, id, name)
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = write_fmt(fp, "2I", self.number, self.id)
written += write_unicode_string(fp, self.name)
return written
[docs]
@register(Resource.VERSION_INFO)
@define(repr=False)
class VersionInfo(BaseElement):
"""
Version info structure.
.. py:attribute:: version
.. py:attribute:: has_composite
.. py:attribute:: writer
.. py:attribute:: reader
.. py:attribute:: file_version
"""
version: int = 1
has_composite: bool = False
writer: str = ""
reader: str = ""
file_version: int = 1
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> "VersionInfo":
version, has_composite = read_fmt("I?", fp)
writer = read_unicode_string(fp)
reader = read_unicode_string(fp)
file_version = read_fmt("I", fp)[0]
return cls(version, has_composite, writer, reader, file_version)
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = write_fmt(fp, "I?", self.version, self.has_composite)
written += write_unicode_string(fp, self.writer)
written += write_unicode_string(fp, self.reader)
written += write_fmt(fp, "I", self.file_version)
return written