Source code for psd_tools.api.mask

"""
Mask module.

Masks are used to determine the visible area of a layer.
There are two types of masks: user mask and vector mask.
User mask refers to any pixel-based mask, whereas vector mask refers to a mask from a shape path.

Masks are accessible from the layer's `mask` property::

    from psd_tools import PSDImage

    psdimage = PSDImage.open('example.psd')
    layer = psdimage[0]
    mask = layer.mask
    assert mask is not None
    print(mask.bbox)  # (left, top, right, bottom)

    mask_image = mask.topil()  # Show the mask as a PIL Image.
    if mask_image is not None:
        mask_image.save("mask.png")

To create a new mask, use the layer's :py:func:`~psd_tools.api.layers.Layer.create_mask` method::

    from psd_tools import PSDImage
    from PIL import Image

    psdimage = PSDImage.new(mode="RGB", size=(128, 128))
    layer = psdimage.create_pixel_layer(Image.new("RGB", (128, 128)))
    layer.create_mask(Image.new("L", (128, 128), 255))

Note that creating a pixel layer from RGBA image will automatically create a user mask::

    psdimage = PSDImage.new(mode="RGB", size=(128, 128))
    layer = psdimage.create_pixel_layer(Image.new("RGBA", (128, 128)))

"""

import logging
from typing import Any, cast

from PIL import Image

from psd_tools.api.protocols import LayerProtocol, MaskProtocol
from psd_tools.constants import ChannelID
from psd_tools.psd.layer_and_mask import MaskData, MaskFlags

logger = logging.getLogger(__name__)


[docs] class Mask(MaskProtocol): """Mask data attached to a layer. There are two distinct internal mask data: user mask and vector mask. User mask refers any pixel-based mask whereas vector mask refers a mask from a shape path. Internally, two masks are combined and referred real mask. """ def __init__(self, layer: LayerProtocol): self._layer = layer self._data: MaskData = cast(MaskData, layer._record.mask_data) @property def background_color(self) -> int: """Background color.""" if self.has_real(): real_bg = self._data.real_background_color return real_bg if real_bg is not None else self._data.background_color return self._data.background_color @property def bbox(self) -> tuple[int, int, int, int]: """BBox""" return self.left, self.top, self.right, self.bottom @property def left(self) -> int: """Left coordinate.""" if self.has_real(): return ( self._data.real_left if self._data.real_left is not None else self._data.left ) return self._data.left @property def right(self) -> int: """Right coordinate.""" if self.has_real(): return ( self._data.real_right if self._data.real_right is not None else self._data.right ) return self._data.right @property def top(self) -> int: """Top coordinate.""" if self.has_real(): return ( self._data.real_top if self._data.real_top is not None else self._data.top ) return self._data.top @property def bottom(self) -> int: """Bottom coordinate.""" if self.has_real(): return ( self._data.real_bottom if self._data.real_bottom is not None else self._data.bottom ) return self._data.bottom @property def width(self) -> int: """Width.""" return self.right - self.left @property def height(self) -> int: """Height.""" return self.bottom - self.top @property def size(self) -> tuple[int, int]: """(Width, Height) tuple.""" return self.width, self.height @property def disabled(self) -> bool: """Disabled.""" return self._data.flags.mask_disabled @disabled.setter def disabled(self, value: bool) -> None: self._data.flags.mask_disabled = value self._layer._psd._mark_updated() @property def flags(self) -> MaskFlags: """Flags.""" return self._data.flags @property def parameters(self) -> Any: """Parameters.""" return self._data.parameters @property def real_flags(self) -> MaskFlags | None: """Real flag.""" return cast(MaskFlags | None, self._data.real_flags) @property def data(self) -> MaskData: """Return raw mask data, or None if no data.""" return self._data
[docs] def has_real(self) -> bool: """Return True if the mask has real flags.""" return self.real_flags is not None and self.real_flags.parameters_applied
[docs] def topil( self, real: bool = True, layer_sized: bool = False, **kwargs: Any ) -> Image.Image | None: """ Get PIL Image of the mask. :param real: When True, returns pixel + vector mask combined. :param layer_sized: When True, returns a layer-sized image pre-filled with ``background_color``, with the mask data pasted at the correct position. When False (default), returns the raw stored mask data at mask dimensions. :return: PIL Image object, or None if the mask is empty. """ if real and self.has_real(): channel = ChannelID.REAL_USER_LAYER_MASK offset_left = (self._data.real_left or 0) - self._layer.left offset_top = (self._data.real_top or 0) - self._layer.top real_bg = self._data.real_background_color bg = real_bg if real_bg is not None else self._data.background_color else: channel = ChannelID.USER_LAYER_MASK offset_left = self._data.left - self._layer.left offset_top = self._data.top - self._layer.top bg = self._data.background_color raw = self._layer.topil(channel, **kwargs) if raw is None or not layer_sized: return raw # Compose a full layer-sized mask applying background_color outside stored bbox. # Out-of-bounds mask data is clipped naturally by Image.paste. full = Image.new(raw.mode, self._layer.size, bg) full.paste(raw, (offset_left, offset_top)) return full
def __repr__(self) -> str: return "%s(offset=(%d,%d) size=%dx%d)" % ( self.__class__.__name__, self.left, self.top, self.width, self.height, )