Source code for psd_tools.api.psd_image

"""
PSD Image module.

This module provides the main :py:class:`PSDImage` class, which is the primary
entry point for users of psd-tools. It represents a complete Photoshop document
and provides high-level methods for reading, manipulating, and saving PSD/PSB files.

The :py:class:`PSDImage` class wraps the low-level :py:class:`~psd_tools.psd.PSD`
structure and reconstructs the layer tree from the flat layer list, making it much
easier to work with layers and groups.

Key functionality:

- **Opening files**: :py:meth:`PSDImage.open` and :py:meth:`PSDImage.new`
- **Layer access**: Iterate, index, and search layers
- **Compositing**: Render layers to PIL Images via :py:meth:`~PSDImage.composite`
- **Saving**: Write modified documents back to PSD format
- **Metadata access**: Document properties, ICC profiles, resolution, etc.

Example usage::

    from psd_tools import PSDImage

    # Open a PSD file
    psd = PSDImage.open('document.psd')

    # Access document properties
    print(f"Size: {psd.width}x{psd.height}")
    print(f"Color mode: {psd.color_mode}")

    # Iterate through layers
    for layer in psd:
        print(f"{layer.name}: {layer.kind}")

    # Access layers by index
    layer = psd[0]  # First layer

    # Modify layers
    layer.visible = False
    layer.opacity = 128

    # Composite to image
    image = psd.composite()
    image.save('output.png')

    # Save changes
    psd.save('modified.psd')

The class inherits from :py:class:`~psd_tools.api.layers.GroupMixin`, providing
group-like behavior for accessing child layers.
"""

from __future__ import annotations

import logging
import os
from collections.abc import Sequence
from typing import Any, BinaryIO, Callable, Iterable, Literal

from typing_extensions import Self

import numpy as np
from PIL import Image

from psd_tools.api import adjustments, layers, numpy_io, pil_io
from psd_tools.api.protocols import PSDProtocol
from psd_tools.api.utils import (
    EXPECTED_CHANNELS,
    ColorInput,
    denormalize_color,
    normalize_color,
)
from psd_tools.constants import (
    BlendMode,
    ChannelID,
    ColorMode,
    CompatibilityMode,
    Compression,
    Resource,
    SectionDivider,
    Tag,
)
from psd_tools.psd.document import PSD
from psd_tools.psd.header import FileHeader
from psd_tools.psd.image_data import ImageData
from psd_tools.psd.image_resources import ImageResources
from psd_tools.psd.layer_and_mask import (
    ChannelImageData,
    GlobalLayerMaskInfo,
    LayerInfo,
    LayerRecords,
)
from psd_tools.psd.patterns import Patterns
from psd_tools.psd.tagged_blocks import TaggedBlocks

logger = logging.getLogger(__name__)


[docs] class PSDImage(layers.GroupMixin, PSDProtocol): """ Photoshop PSD/PSB document. The low-level data structure is accessible at :py:attr:`PSDImage._record`. Example:: from psd_tools import PSDImage psdimage = PSDImage.open('example.psd') image = psdimage.composite() for layer in psdimage: layer_image = layer.composite() """ def __init__(self, data: PSD): if not isinstance(data, PSD): raise TypeError(f"Expected PSD instance, got {type(data).__name__}") self._record = data self._layers: list[layers.Layer] = [] self._compatibility_mode = CompatibilityMode.DEFAULT self._background_color: float | tuple[float, ...] | None = None self._updated: bool = False # Flag to check if the layer tree is edited. self._psd = self # For GroupMixin protocol compatibility. self._init()
[docs] @classmethod def new( cls, mode: str, size: tuple[int, int], color: ColorInput = 0, depth: Literal[8, 16, 32] = 8, **kwargs: Any, ) -> Self: """ Create a new PSD document. :param mode: The color mode to use for the new image. :param size: A tuple containing (width, height) in pixels. :param color: What color to use for the image. Default is black. Accepts a single integer (0-255 for 8-bit) or float in the [0.0, 1.0] range, or a sequence of per-channel values using the same ranges (e.g., 3 values for ``"RGB"``, 4 for ``"CMYK"``). Mixed int/float sequences are allowed. The color is used both as the initial image data fill and as the :py:attr:`~PSDImage.background_color` compositing backdrop when saving. :param depth: Bit depth (8, 16, or 32). :return: A :py:class:`~psd_tools.api.psd_image.PSDImage` object. """ header = cls._make_header(mode, size, depth) # Strip alpha channel(s) from color for background_color since # composite() only expects color channels (alpha is separate). bg_input: ColorInput = color if isinstance(color, Sequence): expected = EXPECTED_CHANNELS.get(header.color_mode) if expected is not None and len(color) > expected: bg_input = tuple(color[:expected]) bg_color = normalize_color(bg_input, depth, header.color_mode) fill_color = denormalize_color(color, depth) image_data = ImageData.new(header, color=fill_color, **kwargs) # TODO: Add default metadata. psdimage = cls( PSD( header=header, image_data=image_data, image_resources=ImageResources.new(), ) ) psdimage._background_color = bg_color return psdimage
[docs] @classmethod def frompil( cls, image: Image.Image, compression: Compression = Compression.RLE, color: ColorInput | None = None, ) -> Self: """ Create a new layer-less PSD document from PIL Image. :param image: PIL Image object. :param compression: ImageData compression option. See :py:class:`~psd_tools.constants.Compression`. :param color: Background color for the compositing backdrop when saving. Accepts the same types as :py:meth:`~PSDImage.new` ``color``. When set, :py:meth:`~PSDImage.save` will composite layers against an opaque backdrop of this color. :return: A :py:class:`~psd_tools.api.psd_image.PSDImage` object. """ header = cls._make_header(image.mode, image.size) # TODO: Add default metadata. # TODO: Perhaps make this smart object. image_data = ImageData(compression=compression) image_data.set_data([channel.tobytes() for channel in image.split()], header) psdimage = cls( PSD( header=header, image_data=image_data, image_resources=ImageResources.new(), ) ) if color is not None: psdimage.background_color = color return psdimage
[docs] @classmethod def open(cls, fp: BinaryIO | str | bytes | os.PathLike, **kwargs: Any) -> Self: """ Open a PSD document. :param fp: filename or file-like object. :param encoding: charset encoding of the pascal string within the file, default 'macroman'. Some psd files need explicit encoding option. :return: A :py:class:`~psd_tools.api.psd_image.PSDImage` object. """ if isinstance(fp, (str, bytes, os.PathLike)): with open(fp, "rb") as f: self = cls(PSD.read(f, **kwargs)) else: self = cls(PSD.read(fp, **kwargs)) return self
[docs] def save( self, fp: BinaryIO | str | bytes | os.PathLike, mode: str = "wb", **kwargs: Any, ) -> None: """ Save the PSD file. Updates the ImageData section if the layer structure has been updated. :param fp: filename or file-like object. :param encoding: charset encoding of the pascal string within the file, default 'macroman'. :param mode: file open mode, default 'wb'. """ if self.is_updated(): # Update the preview image if the layer structure has been changed. # TODO: Set a `has_composite` flag in VersionInfo resource. try: if self._background_color is not None: composited_psd = self.composite( color=self._background_color, alpha=1.0 ).convert(self.pil_mode) else: composited_psd = self.composite().convert(self.pil_mode) self._record.image_data.set_data( [channel.tobytes() for channel in composited_psd.split()], self._record.header, ) except ImportError as e: logger.warning( "Failed to update preview image: %s. " "Install composite dependencies with: pip install 'psd-tools[composite]'", e, ) if isinstance(fp, (str, bytes, os.PathLike)): with open(fp, mode) as f: self._record.write(f, **kwargs) # type: ignore[arg-type] else: self._record.write(fp, **kwargs) # type: ignore[arg-type]
[docs] def topil( self, channel: int | ChannelID | None = None, apply_icc: bool = True ) -> Image.Image | None: """ Get PIL Image. :param channel: Which channel to return; e.g., 0 for 'R' channel in RGB image. See :py:class:`~psd_tools.constants.ChannelID`. When `None`, the method returns all the channels supported by PIL modes. :param apply_icc: Whether to apply ICC profile conversion to sRGB. :return: :py:class:`PIL.Image`, or `None` if the composed image is not available. """ if self.has_preview(): return pil_io.convert_image_data_to_pil(self, channel, apply_icc) return None
[docs] def numpy( self, channel: Literal["color", "shape", "alpha", "mask"] | None = None ) -> np.ndarray: """ Get NumPy array of the layer. :param channel: Which channel to return, can be 'color', 'shape', 'alpha', or 'mask'. Default is 'color+alpha'. :return: :py:class:`numpy.ndarray` """ array = numpy_io.get_array(self, channel) assert array is not None return array
[docs] def composite( self, viewport: tuple[int, int, int, int] | None = None, force: bool = False, color: float | tuple[float, ...] | np.ndarray | None = 1.0, alpha: float | np.ndarray = 0.0, layer_filter: Callable | None = None, ignore_preview: bool = False, apply_icc: bool = True, ) -> Image.Image: """ Composite the PSD image. :param viewport: Viewport bounding box specified by (x1, y1, x2, y2) tuple. Default is the viewbox of the PSD. :param ignore_preview: Boolean flag to whether skip compositing when a pre-composited preview is available. :param force: Boolean flag to force vector drawing. :param color: Backdrop color specified by scalar or tuple of scalar. The color value should be in [0.0, 1.0]. For example, (1., 0., 0.) specifies red in RGB color mode. :param alpha: Backdrop alpha in [0.0, 1.0]. :param layer_filter: Callable that takes a layer as argument and returns whether if the layer is composited. Default is :py:func:`~psd_tools.api.layers.PixelLayer.is_visible`. :return: :py:class:`PIL.Image`. """ from psd_tools.composite import composite_pil if ( not (ignore_preview or force or layer_filter) and self.has_preview() and not self.is_updated() ): result = self.topil(apply_icc=apply_icc) if result is None: raise ValueError("Failed to composite PSD image from preview") return result result = composite_pil( self, color if color is not None else 1.0, alpha, viewport, layer_filter, force, apply_icc=apply_icc, ) if result is None: raise ValueError("Failed to composite PSD image") return result
def _mark_updated(self) -> None: """Mark the layer tree as updated.""" self._updated = True
[docs] def is_updated(self) -> bool: """ Returns whether the layer tree has been updated. :return: `bool` """ return self._updated
@property def parent(self) -> None: """Parent of this layer.""" return None
[docs] def has_preview(self) -> bool: """ Returns if the document has real merged data. When True, `topil()` returns pre-composed data. """ version_info = self.image_resources.get_data(Resource.VERSION_INFO) if version_info: return version_info.has_composite return True # Assuming the image data is valid by default.
@property def name(self) -> str: """ Element name. :return: `'Root'` """ return "Root" @property def kind(self) -> str: """ Kind. :return: `'psdimage'` """ return self.__class__.__name__.lower() @property def visible(self) -> bool: """ Visibility. :return: `True` """ return True @property def left(self) -> int: """ Left coordinate. :return: `0` """ return 0 @property def top(self) -> int: """ Top coordinate. :return: `0` """ return 0 @property def right(self) -> int: """ Right coordinate. :return: `int` """ return self.width @property def bottom(self) -> int: """ Bottom coordinate. :return: `int` """ return self.height @property def width(self) -> int: """ Document width. :return: `int` """ return self._record.header.width @property def height(self) -> int: """ Document height. :return: `int` """ return self._record.header.height @property def size(self) -> tuple[int, int]: """ (width, height) tuple. :return: `tuple` """ return self.width, self.height @property def offset(self) -> tuple[int, int]: """ (left, top) tuple. :return: `tuple` """ return self.left, self.top @property def bbox(self) -> tuple[int, int, int, int]: """ Minimal bounding box that contains all the visible layers. Use :py:attr:`~psd_tools.api.psd_image.PSDImage.viewbox` to get viewport bounding box. When the psd is empty, bbox is equal to the canvas bounding box. :return: (left, top, right, bottom) `tuple`. """ bbox = super(PSDImage, self).bbox if bbox == (0, 0, 0, 0): bbox = self.viewbox return bbox @property def viewbox(self) -> tuple[int, int, int, int]: """ Return bounding box of the viewport. :return: (left, top, right, bottom) `tuple`. """ return self.left, self.top, self.right, self.bottom @property def color_mode(self) -> ColorMode: """ Document color mode, such as 'RGB' or 'GRAYSCALE'. See :py:class:`~psd_tools.constants.ColorMode`. :return: :py:class:`~psd_tools.constants.ColorMode` """ return self._record.header.color_mode @property def channels(self) -> int: """ Number of color channels. :return: `int` """ return self._record.header.channels @property def depth(self) -> int: """ Pixel depth bits. :return: `int` """ return self._record.header.depth @property def version(self) -> int: """ Document version. PSD file is 1, and PSB file is 2. :return: `int` """ return self._record.header.version @property def image_resources(self) -> ImageResources: """ Document image resources. :py:class:`~psd_tools.psd.image_resources.ImageResources` is a dict-like structure that keeps various document settings. See :py:class:`psd_tools.constants.Resource` for available keys. :return: :py:class:`~psd_tools.psd.image_resources.ImageResources` Example:: from psd_tools.constants import Resource version_info = psd.image_resources.get_data(Resource.VERSION_INFO) slices = psd.image_resources.get_data(Resource.SLICES) Image resources contain an ICC profile. The following shows how to export a PNG file with embedded ICC profile:: from psd_tools.constants import Resource icc_profile = psd.image_resources.get_data(Resource.ICC_PROFILE) image = psd.compose(apply_icc=False) image.save('output.png', icc_profile=icc_profile) """ return self._record.image_resources @property def tagged_blocks(self) -> TaggedBlocks | None: """ Document tagged blocks that is a dict-like container of settings. See :py:class:`psd_tools.constants.Tag` for available keys. :return: :py:class:`~psd_tools.psd.tagged_blocks.TaggedBlocks` or `None`. Example:: from psd_tools.constants import Tag patterns = psd.tagged_blocks.get_data(Tag.PATTERNS1) """ return self._record.layer_and_mask_information.tagged_blocks @property def compatibility_mode(self) -> CompatibilityMode: """ Set the compositing and layer organization compatibility mode. Writable. This property checks whether the PSD file is compatible with specific authoring tools, such as Photoshop or CLIP Studio Paint. Different authoring tools may have different ways of handling layers, such as the use of clipping masks for groups. Default is Photoshop compatibility mode. :return: :py:class:`~psd_tools.constants.CompatibilityMode` """ return self._compatibility_mode @compatibility_mode.setter def compatibility_mode(self, value: CompatibilityMode) -> None: if self._compatibility_mode != value: self._mark_updated() self._compatibility_mode = value @property def background_color(self) -> float | tuple[float, ...] | None: """ Background color for the merged composite image. Writable. When set, :py:meth:`~PSDImage.save` composites layers against an opaque backdrop of this color instead of a transparent one. This is useful for creating RGB-mode PSD files where transparent areas should appear as a specific color (e.g., white) in the merged composite. Values are in [0.0, 1.0] range, matching the :py:meth:`~PSDImage.composite` ``color`` parameter: - **RGB / Grayscale**: ``1.0`` = white, ``0.0`` = black - **CMYK**: ``(0.0, 0.0, 0.0, 0.0)`` = white (no ink) Use a scalar for uniform color or a tuple for per-channel values. Set to ``None`` for transparent backdrop (legacy behavior). Documents created via :py:meth:`~PSDImage.new` have this set automatically from the ``color`` parameter. :return: `float`, `tuple[float, ...]`, or `None` Example:: psd = PSDImage.new('RGB', (640, 480), color=1.0) psd.save('output.psd') # White background, 3 channels (no alpha) """ return self._background_color @background_color.setter def background_color( self, value: ColorInput | None, ) -> None: if value is not None: value = normalize_color(value, self._record.header.depth, self.color_mode) if self._background_color != value: self._mark_updated() self._background_color = value @property def pil_mode(self) -> str: alpha = self.channels - pil_io.get_pil_channels( pil_io.get_pil_mode(self.color_mode) ) # TODO: Check when alpha > 1; Photoshop allows multiple alpha channels. return pil_io.get_pil_mode(self.color_mode, alpha > 0)
[docs] def has_thumbnail(self) -> bool: """True if the PSDImage has a thumbnail resource.""" return ( Resource.THUMBNAIL_RESOURCE in self.image_resources or Resource.THUMBNAIL_RESOURCE_PS4 in self.image_resources )
[docs] def thumbnail(self) -> Image.Image | None: """ Returns a thumbnail image in PIL.Image. When the file does not contain an embedded thumbnail image, returns None. """ if Resource.THUMBNAIL_RESOURCE in self.image_resources: return pil_io.convert_thumbnail_to_pil( self.image_resources.get_data(Resource.THUMBNAIL_RESOURCE) ) elif Resource.THUMBNAIL_RESOURCE_PS4 in self.image_resources: return pil_io.convert_thumbnail_to_pil( self.image_resources.get_data(Resource.THUMBNAIL_RESOURCE_PS4) ) return None
# Editing API
[docs] def create_pixel_layer( self, image: Image.Image, name: str = "Layer", top: int = 0, left: int = 0, compression: Compression = Compression.RLE, opacity: int = 255, blend_mode: BlendMode = BlendMode.NORMAL, ) -> layers.PixelLayer: """ Create a new pixel layer and add it to the PSDImage. Example:: psdimage = PSDImage.new("RGB", (640, 480)) layer = psdimage.create_pixel_layer(image, name='Layer 1') :param name: Name of the new layer. :param image: PIL Image object. :param top: Top coordinate of the new layer. :param left: Left coordinate of the new layer. :param compression: Compression method for the layer image data. :param opacity: Opacity of the new layer (0-255). :param blend_mode: Blend mode of the new layer, default is ``BlendMode.NORMAL``. :return: The created :py:class:`~psd_tools.api.layers.PixelLayer` object. """ layer = layers.PixelLayer.frompil( image, parent=self, name=name, top=top, left=left, compression=compression ) layer.opacity = opacity layer.blend_mode = blend_mode self._mark_updated() return layer
[docs] def create_group( self, layer_list: Iterable[layers.Layer] | None = None, name: str = "Group", opacity: int = 255, blend_mode: BlendMode = BlendMode.PASS_THROUGH, open_folder: bool = True, ) -> layers.Group: """ Create a new group layer and add it to the PSDImage. Example:: group = psdimage.create_group(name='New Group') group.append(psdimage.create_pixel_layer(image, name='Layer in Group')) :param layer_list: Optional list of layers to add to the group. :param name: Name of the new group. :param opacity: Opacity of the new layer (0-255). :param blend_mode: Blend mode of the new layer, default is ``BlendMode.PASS_THROUGH``. :param open_folder: Whether the group is an open folder in the Photoshop UI. :return: The created :py:class:`~psd_tools.api.layers.Group` object. """ group = layers.Group.new(parent=self, name=name, open_folder=open_folder) if layer_list: group.extend(layer_list) group.opacity = opacity group.blend_mode = blend_mode self._mark_updated() return group
# TODO: Add more editing APIs, such as duplicate_layers, resize_canvas, etc. # Private methods def __repr__(self) -> str: return ("%s(mode=%s size=%dx%d depth=%d channels=%d)") % ( self.__class__.__name__, self.color_mode, self.width, self.height, self._record.header.depth, self._record.header.channels, ) def _repr_pretty_(self, p: Any, cycle: bool) -> None: if cycle: p.text(self.__repr__()) return def _pretty(layer: layers.Layer | PSDImage, p: Any) -> None: p.text(layer.__repr__()) if isinstance(layer, layers.GroupMixin): with p.indent(2): for idx, child in enumerate(layer): p.break_() p.text("[%d] " % idx) if child.clipping: p.text("+") _pretty(child, p) _pretty(self, p) @classmethod def _make_header( cls, mode: str, size: tuple[int, int], depth: Literal[8, 16, 32] = 8 ) -> FileHeader: if depth not in (8, 16, 32): raise ValueError(f"Invalid depth: {depth}. Must be 8, 16, or 32") if size[0] > 300000: raise ValueError(f"Width too large: {size[0]} > 300,000") if size[1] > 300000: raise ValueError(f"Height too large: {size[1]} > 300,000") version = 1 if size[0] > 30000 or size[1] > 30000: logger.debug("Width or height larger than 30,000 pixels") version = 2 color_mode = pil_io.get_color_mode(mode) alpha = mode.upper().endswith("A") channels = ColorMode.channels(color_mode, alpha) return FileHeader( version=version, width=size[0], height=size[1], depth=depth, channels=channels, color_mode=color_mode, ) def _get_pattern(self, pattern_id: str) -> Any | None: """Get pattern item by id.""" if self.tagged_blocks is None: return None for key in (Tag.PATTERNS1, Tag.PATTERNS2, Tag.PATTERNS3): if key in self.tagged_blocks: data = self.tagged_blocks.get_data(key) for pattern in data: if pattern.pattern_id == pattern_id: return pattern return None def _init(self) -> None: """Initialize layer structure.""" from psd_tools.api import layers group_stack: list[layers.Group | PSDImage] = [self] for record, channels in self._record._iter_layers(): current_group = group_stack[-1] blocks = record.tagged_blocks end_of_group = False layer: layers.Layer | PSDImage | None = None divider = blocks.get_data(Tag.SECTION_DIVIDER_SETTING, None) divider = blocks.get_data(Tag.NESTED_SECTION_DIVIDER_SETTING, divider) if ( divider is not None and # Some PSDs contain dividers with SectionDivider.OTHER. # Ignoring them, allows the correct categorization of the layer. # Issue : https://github.com/psd-tools/psd-tools/issues/338 divider.kind is not SectionDivider.OTHER ): if divider.kind == SectionDivider.BOUNDING_SECTION_DIVIDER: layer = layers.Group( # type: ignore parent=current_group, # We need to fill in the record and channels later. record=None, # type: ignore channels=None, # type: ignore ) layer._set_bounding_records(record, channels) group_stack.append(layer) elif divider.kind in ( SectionDivider.OPEN_FOLDER, SectionDivider.CLOSED_FOLDER, ): layer = group_stack.pop() if not isinstance(layer, layers.Group): raise TypeError( f"Expected Group layer, got {type(layer).__name__}" ) # Set the record and channels now. layer._record = record layer._channels = channels # If the group is an artboard, convert it. for key in ( Tag.ARTBOARD_DATA1, Tag.ARTBOARD_DATA2, Tag.ARTBOARD_DATA3, ): if key in blocks: layer = layers.Artboard._move(layer) end_of_group = True else: logger.warning("Divider %s found." % divider.kind) elif Tag.TYPE_TOOL_OBJECT_SETTING in blocks or Tag.TYPE_TOOL_INFO in blocks: layer = layers.TypeLayer(current_group, record, channels) elif ( Tag.SMART_OBJECT_LAYER_DATA1 in blocks or Tag.SMART_OBJECT_LAYER_DATA2 in blocks or Tag.PLACED_LAYER1 in blocks or Tag.PLACED_LAYER2 in blocks ): layer = layers.SmartObjectLayer(current_group, record, channels) else: for key in adjustments.TYPES.keys(): if key in blocks: layer = adjustments.TYPES[key](current_group, record, channels) break # If nothing applies, this is either a shape or pixel layer. shape_condition = record.flags.pixel_data_irrelevant and ( Tag.VECTOR_ORIGINATION_DATA in blocks or Tag.VECTOR_MASK_SETTING1 in blocks or Tag.VECTOR_MASK_SETTING2 in blocks or Tag.VECTOR_STROKE_DATA in blocks or Tag.VECTOR_STROKE_CONTENT_DATA in blocks ) if isinstance(layer, (type(None), layers.FillLayer)) and shape_condition: layer = layers.ShapeLayer(current_group, record, channels) if layer is None: layer = layers.PixelLayer(current_group, record, channels) if layer is None: raise ValueError("Failed to create layer from record") if not end_of_group: if isinstance(layer, PSDImage): raise TypeError("Cannot add PSDImage as a layer") current_group._layers.append(layer) def _update_record(self) -> None: """ Compiles the tree layer structure back into records and channels list recursively from the API layer structure. """ # Initialize the layer structure information if not present. if self._record.layer_and_mask_information.layer_info is None: self._record.layer_and_mask_information.layer_info = LayerInfo() if self._record.layer_and_mask_information.global_layer_mask_info is None: self._record.layer_and_mask_information.global_layer_mask_info = ( GlobalLayerMaskInfo() ) if self._record.layer_and_mask_information.tagged_blocks is None: self._record.layer_and_mask_information.tagged_blocks = TaggedBlocks() # Set layer records and channel image data. layer_records, channel_image_data = _build_record_tree(self) layer_info = self._record.layer_and_mask_information.layer_info layer_info.layer_records = layer_records layer_info.channel_image_data = channel_image_data layer_info.layer_count = len(layer_records) # Flag as updated. self._mark_updated() def _copy_patterns(self, psdimage: PSDProtocol) -> None: """Copy patterns from this psdimage to the target psdimage.""" if self.tagged_blocks is None: # Nothing to copy. return if psdimage.tagged_blocks is None: logger.debug("Creating tagged blocks for psdimage.") psdimage._record.layer_and_mask_information.tagged_blocks = TaggedBlocks() if psdimage.tagged_blocks is None: raise ValueError("Failed to create tagged blocks for psdimage") for tag in self.tagged_blocks.keys(): if not isinstance(tag, Tag): raise TypeError(f"Expected Tag instance, got {type(tag).__name__}") if tag in (Tag.PATTERNS1, Tag.PATTERNS2, Tag.PATTERNS3): logger.debug("Copying patterns for tag %s", tag) source_patterns: Patterns = self.tagged_blocks.get_data(tag) target_patterns: Patterns = psdimage.tagged_blocks.get_data(tag) if target_patterns is None: target_patterns = Patterns() psdimage.tagged_blocks.set_data(tag, target_patterns) target_pattern_ids = {p.pattern_id for p in target_patterns} for pattern in source_patterns: if pattern.pattern_id not in target_pattern_ids: target_patterns.append(pattern)
def _build_record_tree( layer_group: layers.GroupMixin, ) -> tuple[LayerRecords, ChannelImageData]: """ Builds the layer tree structure from records and channels list recursively """ layer_records = LayerRecords() channel_image_data = ChannelImageData() for layer in layer_group: if isinstance(layer, (layers.Group, layers.Artboard)): layer_records.append(layer._bounding_record) channel_image_data.append(layer._bounding_channels) tmp_layer_records, tmp_channel_image_data = _build_record_tree(layer) layer_records.extend(tmp_layer_records) channel_image_data.extend(tmp_channel_image_data) layer_records.append(layer._record) channel_image_data.append(layer._channels) return (layer_records, channel_image_data)