Source code for psd_tools.psd.image_resources

"""
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, always ``b'8BIM'``. .. 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