Source code for psd_tools.psd.effects_layer

"""
Effects layer structure.

Note the structures in this module is obsolete and object-based layer effects
are stored in tagged blocks.
"""

import logging
from typing import Any, BinaryIO, TypeVar

from attrs import define, field, astuple

from psd_tools.constants import BlendMode, EffectOSType
from psd_tools.psd.base import BaseElement, DictElement
from psd_tools.psd.color import Color
from psd_tools.psd.bin_utils import (
    read_fmt,
    read_length_block,
    write_fmt,
    write_length_block,
    write_padding,
)
from psd_tools.validators import in_

logger = logging.getLogger(__name__)

T_CommonStateInfo = TypeVar("T_CommonStateInfo", bound="CommonStateInfo")
T_ShadowInfo = TypeVar("T_ShadowInfo", bound="ShadowInfo")
T_OuterGlowInfo = TypeVar("T_OuterGlowInfo", bound="OuterGlowInfo")
T_InnerGlowInfo = TypeVar("T_InnerGlowInfo", bound="InnerGlowInfo")
T_BevelInfo = TypeVar("T_BevelInfo", bound="BevelInfo")
T_SolidFillInfo = TypeVar("T_SolidFillInfo", bound="SolidFillInfo")
T_EffectsLayer = TypeVar("T_EffectsLayer", bound="EffectsLayer")


[docs] @define(repr=False) class CommonStateInfo(BaseElement): """ Effects layer common state info. .. py:attribute:: version .. py:attribute:: visible """ version: int = 0 visible: int = 1 @classmethod def read( cls: type[T_CommonStateInfo], fp: BinaryIO, **kwargs: Any ) -> T_CommonStateInfo: return cls(*read_fmt("IB2x", fp)) def write(self, fp: BinaryIO, **kwargs: Any) -> int: return write_fmt(fp, "IB2x", *astuple(self))
[docs] @define(repr=False) class ShadowInfo(BaseElement): """ Effects layer shadow info. .. py:attribute:: version .. py:attribute:: blur .. py:attribute:: intensity .. py:attribute:: angle .. py:attribute:: distance .. py:attribute:: color .. py:attribute:: blend_mode .. py:attribute:: enabled .. py:attribute:: use_global_angle .. py:attribute:: opacity .. py:attribute:: native_color """ version: int = 0 blur: int = 0 intensity: int = 0 angle: int = 0 distance: int = 0 color: Color = field(factory=Color) blend_mode: BlendMode = field( default=BlendMode.NORMAL, converter=BlendMode, validator=in_(BlendMode) ) enabled: int = 0 use_global_angle: int = 0 opacity: int = 0 native_color: Color = field(factory=Color) @classmethod def read(cls: type[T_ShadowInfo], fp: BinaryIO, **kwargs: Any) -> T_ShadowInfo: # TODO: Check 4-byte = 2-byte int + 2-byte fraction? version, blur, intensity, angle, distance = read_fmt("IIIiI", fp) color = Color.read(fp) signature = read_fmt("4s", fp)[0] assert signature == b"8BIM", "Invalid signature %r" % (signature) blend_mode = BlendMode(read_fmt("4s", fp)[0]) enabled, use_global_angle, opacity = read_fmt("3B", fp) native_color = Color.read(fp) return cls( version=version, blur=blur, intensity=intensity, angle=angle, distance=distance, color=color, blend_mode=blend_mode, enabled=enabled, use_global_angle=use_global_angle, opacity=opacity, native_color=native_color, ) def write(self, fp: BinaryIO, **kwargs: Any) -> int: written = write_fmt( fp, "IIIiI", self.version, self.blur, self.intensity, self.angle, self.distance, ) written += self.color.write(fp) written += write_fmt( fp, "4s4s3B", b"8BIM", self.blend_mode.value, self.enabled, self.use_global_angle, self.opacity, ) written += self.native_color.write(fp) return written
class _GlowInfo: # Type hints for mixin attributes (defined in subclasses) version: int blur: int intensity: int color: Color blend_mode: BlendMode enabled: int opacity: int @classmethod def _read_body( cls, fp: BinaryIO ) -> tuple[int, int, int, Color, BlendMode, int, int]: # TODO: Check 4-byte = 2-byte int + 2-byte fraction? version, blur, intensity = read_fmt("III", fp) color = Color.read(fp) signature = read_fmt("4s", fp)[0] assert signature == b"8BIM", "Invalid signature %r" % (signature) blend_mode = BlendMode(read_fmt("4s", fp)[0]) enabled, opacity = read_fmt("2B", fp) return version, blur, intensity, color, blend_mode, enabled, opacity def _write_body(self, fp: BinaryIO) -> int: written = write_fmt(fp, "III", self.version, self.blur, self.intensity) written += self.color.write(fp) written += write_fmt( fp, "4s4s2B", b"8BIM", self.blend_mode.value, self.enabled, self.opacity ) return written
[docs] @define(repr=False) class OuterGlowInfo(BaseElement, _GlowInfo): """ Effects layer outer glow info. .. py:attribute:: version .. py:attribute:: blur .. py:attribute:: intensity .. py:attribute:: color .. py:attribute:: blend_mode .. py:attribute:: enabled .. py:attribute:: opacity .. py:attribute:: native_color """ version: int = 0 blur: int = 0 intensity: int = 0 color: Color = field(factory=Color) blend_mode: BlendMode = field( default=BlendMode.NORMAL, converter=BlendMode, validator=in_(BlendMode) ) enabled: int = 0 opacity: int = 0 native_color: object = None @classmethod def read( cls: type[T_OuterGlowInfo], fp: BinaryIO, **kwargs: Any ) -> T_OuterGlowInfo: version, blur, intensity, color, blend_mode, enabled, opacity = cls._read_body( fp ) native_color = None if version >= 2: native_color = Color.read(fp) return cls( version=version, blur=blur, intensity=intensity, color=color, blend_mode=blend_mode, enabled=enabled, opacity=opacity, native_color=native_color, ) def write(self, fp: BinaryIO, **kwargs: Any) -> int: written = self._write_body(fp) if self.native_color and hasattr(self.native_color, "write"): written += self.native_color.write(fp) # type: ignore[attr-defined] return written
[docs] @define(repr=False) class InnerGlowInfo(BaseElement, _GlowInfo): """ Effects layer inner glow info. .. py:attribute:: version .. py:attribute:: blur .. py:attribute:: intensity .. py:attribute:: color .. py:attribute:: blend_mode .. py:attribute:: enabled .. py:attribute:: opacity .. py:attribute:: invert .. py:attribute:: native_color """ version: int = 0 blur: int = 0 intensity: int = 0 color: Color = field(factory=Color) blend_mode: BlendMode = field( default=BlendMode.NORMAL, converter=BlendMode, validator=in_(BlendMode) ) enabled: int = 0 opacity: int = 0 invert: int | None = None native_color: object | None = None @classmethod def read( cls: type[T_InnerGlowInfo], fp: BinaryIO, **kwargs: Any ) -> T_InnerGlowInfo: version, blur, intensity, color, blend_mode, enabled, opacity = cls._read_body( fp ) invert, native_color = None, None if version >= 2: invert = read_fmt("B", fp)[0] native_color = Color.read(fp) return cls( version=version, blur=blur, intensity=intensity, color=color, blend_mode=blend_mode, enabled=enabled, opacity=opacity, invert=invert, native_color=native_color, ) def write(self, fp: BinaryIO, **kwargs: Any) -> int: written = self._write_body(fp) if self.version >= 2: written += write_fmt(fp, "B", self.invert) if hasattr(self.native_color, "write"): written += self.native_color.write(fp) # type: ignore[attr-defined] return written
[docs] @define(repr=False) class BevelInfo(BaseElement): """ Effects layer bevel info. .. py:attribute:: version .. py:attribute:: angle .. py:attribute:: depth .. py:attribute:: blur .. py:attribute:: highlight_blend_mode .. py:attribute:: shadow_blend_mode .. py:attribute:: highlight_color .. py:attribute:: shadow_color .. py:attribute:: highlight_opacity .. py:attribute:: shadow_opacity .. py:attribute:: enabled .. py:attribute:: use_global_angle .. py:attribute:: direction .. py:attribute:: real_hightlight_color .. py:attribute:: real_shadow_color """ version: int = 0 angle: int = 0 depth: int = 0 blur: int = 0 highlight_blend_mode: BlendMode = field( default=BlendMode.NORMAL, converter=BlendMode, validator=in_(BlendMode) ) shadow_blend_mode: BlendMode = field( default=BlendMode.NORMAL, converter=BlendMode, validator=in_(BlendMode) ) highlight_color: Color = field(factory=Color) shadow_color: Color = field(factory=Color) bevel_style: int = 0 highlight_opacity: int = 0 shadow_opacity: int = 0 enabled: int = 0 use_global_angle: int = 0 direction: int = 0 real_highlight_color: object = None real_shadow_color: object = None @classmethod def read(cls: type[T_BevelInfo], fp: BinaryIO, **kwargs: Any) -> T_BevelInfo: # TODO: Check 4-byte = 2-byte int + 2-byte fraction? version, angle, depth, blur = read_fmt("Ii2I", fp) signature, highlight_blend_mode = read_fmt("4s4s", fp) assert signature == b"8BIM", "Invalid signature %r" % (signature) signature, shadow_blend_mode = read_fmt("4s4s", fp) assert signature == b"8BIM", "Invalid signature %r" % (signature) highlight_color = Color.read(fp) shadow_color = Color.read(fp) bevel_style, highlight_opacity, shadow_opacity = read_fmt("3B", fp) enabled, use_global_angle, direction = read_fmt("3B", fp) real_highlight_color, real_shadow_color = None, None if version == 2: real_highlight_color = Color.read(fp) real_shadow_color = Color.read(fp) return cls( version=version, angle=angle, depth=depth, blur=blur, highlight_blend_mode=highlight_blend_mode, shadow_blend_mode=shadow_blend_mode, highlight_color=highlight_color, shadow_color=shadow_color, bevel_style=bevel_style, highlight_opacity=highlight_opacity, shadow_opacity=shadow_opacity, enabled=enabled, use_global_angle=use_global_angle, direction=direction, real_highlight_color=real_highlight_color, real_shadow_color=real_shadow_color, ) def write(self, fp: BinaryIO, **kwargs: Any) -> int: written = write_fmt(fp, "Ii2I", self.version, self.angle, self.depth, self.blur) written += write_fmt( fp, "4s4s4s4s", b"8BIM", self.highlight_blend_mode.value, b"8BIM", self.shadow_blend_mode.value, ) written += self.highlight_color.write(fp) written += self.shadow_color.write(fp) written += write_fmt( fp, "6B", self.bevel_style, self.highlight_opacity, self.shadow_opacity, self.enabled, self.use_global_angle, self.direction, ) if self.version >= 2: written += self.highlight_color.write(fp) written += self.shadow_color.write(fp) return written
[docs] @define(repr=False) class SolidFillInfo(BaseElement): """ Effects layer inner glow info. .. py:attribute:: version .. py:attribute:: blend_mode .. py:attribute:: color .. py:attribute:: opacity .. py:attribute:: enabled .. py:attribute:: native_color """ version: int = 2 blend_mode: BlendMode = field( default=BlendMode.NORMAL, converter=BlendMode, validator=in_(BlendMode) ) color: Color = field(factory=Color) opacity: int = 0 enabled: int = 0 native_color: Color = field(factory=Color) @classmethod def read( cls: type[T_SolidFillInfo], fp: BinaryIO, **kwargs: Any ) -> T_SolidFillInfo: version = read_fmt("I", fp)[0] signature, blend_mode = read_fmt("4s4s", fp) assert signature == b"8BIM", "Invalid signature %r" % (signature) color = Color.read(fp) opacity, enabled = read_fmt("2B", fp) native_color = Color.read(fp) return cls( version=version, blend_mode=blend_mode, color=color, opacity=opacity, enabled=enabled, native_color=native_color, ) def write(self, fp: BinaryIO, **kwargs: Any) -> int: written = write_fmt(fp, "I4s4s", self.version, b"8BIM", self.blend_mode.value) written += self.color.write(fp) written += write_fmt(fp, "2B", self.opacity, self.enabled) written += self.native_color.write(fp) return written
[docs] @define(repr=False) class EffectsLayer(DictElement): """ Dict-like EffectsLayer structure. See :py:class:`psd_tools.constants.EffectOSType` for available keys. .. py:attribute:: version """ version: int = 0 EFFECT_TYPES = { EffectOSType.COMMON_STATE: CommonStateInfo, EffectOSType.DROP_SHADOW: ShadowInfo, EffectOSType.INNER_SHADOW: ShadowInfo, EffectOSType.OUTER_GLOW: OuterGlowInfo, EffectOSType.INNER_GLOW: InnerGlowInfo, EffectOSType.BEVEL: BevelInfo, EffectOSType.SOLID_FILL: SolidFillInfo, } @classmethod def read(cls: type[T_EffectsLayer], fp: BinaryIO, **kwargs: Any) -> T_EffectsLayer: version, count = read_fmt("2H", fp) items = [] for _ in range(count): signature = read_fmt("4s", fp)[0] assert signature == b"8BIM", "Invalid signature %r" % (signature) ostype = EffectOSType(read_fmt("4s", fp)[0]) kls = cls.EFFECT_TYPES.get(ostype) assert kls is not None items.append((ostype, kls.frombytes(read_length_block(fp)))) # type: ignore[attr-defined] return cls(version=version, items=items) # type: ignore[arg-type] def write(self, fp: BinaryIO, **kwargs: Any) -> int: written = write_fmt(fp, "2H", self.version, len(self)) for key in self: written += write_fmt(fp, "4s4s", b"8BIM", key.value) written += write_length_block(fp, self[key].write) written += write_padding(fp, written, 4) return written