Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions src/psd_tools/api/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ def visible(self) -> bool:

@visible.setter
def visible(self, value: bool) -> None:
if self.visible != value and self._psd is not None:
self._psd._mark_updated()
self._invalidate_bbox()
self._record.flags.visible = bool(value)

Expand All @@ -161,6 +163,8 @@ def opacity(self) -> int:
@opacity.setter
def opacity(self, value: int) -> None:
assert 0 <= value and value <= 255
if self.opacity != value and self._psd is not None:
self._psd._mark_updated()
self._record.opacity = int(value)

@property
Expand Down Expand Up @@ -215,7 +219,10 @@ def blend_mode(self) -> BlendMode:
def blend_mode(self, value: Union[bytes, str, BlendMode]) -> None:
if isinstance(value, str):
value = value.encode("ascii")
self._record.blend_mode = BlendMode(value)
blend_mode = BlendMode(value)
if self.blend_mode != blend_mode and self._psd is not None:
self._psd._mark_updated()
self._record.blend_mode = blend_mode

@property
def left(self) -> int:
Expand All @@ -228,6 +235,8 @@ def left(self) -> int:

@left.setter
def left(self, value: int) -> None:
if self.left != value and self._psd is not None:
self._psd._mark_updated()
self._invalidate_bbox()
w = self.width
self._record.left = int(value)
Expand All @@ -244,6 +253,8 @@ def top(self) -> int:

@top.setter
def top(self, value: int) -> None:
if self.top != value and self._psd is not None:
self._psd._mark_updated()
self._invalidate_bbox()
h = self.height
self._record.top = int(value)
Expand Down Expand Up @@ -296,6 +307,7 @@ def offset(self) -> tuple[int, int]:

@offset.setter
def offset(self, value: tuple[int, int]) -> None:
assert len(value) == 2
self.left, self.top = tuple(int(x) for x in value)

@property
Expand Down Expand Up @@ -520,6 +532,9 @@ def composite(
"""
from psd_tools.composite import composite_pil

if self._psd is not None and self._psd.is_updated():
force = True

return composite_pil(
self, color, alpha, viewport, layer_filter, force, apply_icc=apply_icc
)
Expand Down Expand Up @@ -576,7 +591,10 @@ def clipping(self) -> bool:

@clipping.setter
def clipping(self, value: bool) -> None:
self._record.clipping = Clipping.NON_BASE if value else Clipping.BASE
clipping = Clipping.NON_BASE if value else Clipping.BASE
if self._record.clipping != clipping and self._psd is not None:
self._psd._mark_updated()
self._record.clipping = clipping

@property
def clipping_layer(self) -> bool:
Expand Down Expand Up @@ -947,9 +965,9 @@ def _update_psd_record(self) -> None:
from psd_tools.api.psd_image import PSDImage # Circular import

if isinstance(self, PSDImage):
self._updated_layers = True # type: ignore
self._mark_updated()
elif self._psd is not None:
self._psd._updated_layers = True
self._psd._mark_updated()

def descendants(self) -> Iterator[Layer]:
"""
Expand Down Expand Up @@ -1064,6 +1082,8 @@ def blend_mode(self) -> BlendMode:
@blend_mode.setter
def blend_mode(self, value: Union[str, bytes, BlendMode]) -> None:
_value = BlendMode(value.encode("ascii") if isinstance(value, str) else value)
if self.blend_mode != _value and self._psd is not None:
self._psd._mark_updated()
if _value == BlendMode.PASS_THROUGH:
self._record.blend_mode = BlendMode.NORMAL
else:
Expand Down Expand Up @@ -1091,7 +1111,10 @@ def clipping(self, value: bool) -> None:
"Cannot set clipping flag on groups in Photoshop compatibility mode."
)
return
self._record.clipping = Clipping.NON_BASE if value else Clipping.BASE
clipping = Clipping.NON_BASE if value else Clipping.BASE
if self._record.clipping != clipping and self._psd is not None:
self._psd._mark_updated()
self._record.clipping = clipping

def composite(
self,
Expand Down
23 changes: 19 additions & 4 deletions src/psd_tools/api/psd_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,9 @@ def save(

self._update_record()

if self._updated_layers:
composited_psd = self.composite(force=True)
if self.is_updated():
# Update the preview image if the layer structure has been changed.
composited_psd = self.composite()
self._record.image_data.set_data(
[channel.tobytes() for channel in composited_psd.split()],
self._record.header,
Expand Down Expand Up @@ -244,13 +245,25 @@ def composite(
if (
not (ignore_preview or force or layer_filter)
and self.has_preview()
and not self._updated_layers
and not self.is_updated()
):
return self.topil(apply_icc=apply_icc)
return composite_pil(
self, color, alpha, viewport, layer_filter, force, apply_icc=apply_icc
)

def _mark_updated(self) -> None:
"""Mark the layer tree as updated."""
self._updated_layers = True

def is_updated(self) -> bool:
"""
Returns whether the layer tree has been updated.

:return: `bool`
"""
return self._updated_layers

def is_visible(self) -> bool:
"""
Returns visibility of the element.
Expand Down Expand Up @@ -499,6 +512,8 @@ def compatibility_mode(self) -> CompatibilityMode:

@compatibility_mode.setter
def compatibility_mode(self, value: CompatibilityMode) -> None:
if self._compatibility_mode != value:
self._mark_updated()
self._compatibility_mode = value

@property
Expand Down Expand Up @@ -684,7 +699,7 @@ def _update_record(self) -> None:
Compiles the tree layer structure back into records and channels list recursively
"""

if not self._updated_layers:
if not self.is_updated():
# Skip if nothing is changed.
return

Expand Down
12 changes: 12 additions & 0 deletions tests/psd_tools/api/test_psd_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,15 @@ def test_update_record(fixture):
assert layer_info.channel_image_data[6] is smart_layer._channels
assert layer_info.channel_image_data[7] is type_layer._channels
assert layer_info.channel_image_data[8] is group_layer._channels


def test_is_updated():
psd = PSDImage.open(full_name("hidden-groups.psd"))
assert not psd.is_updated()
psd[0].visible = False
assert psd.is_updated()

psd = PSDImage.open(full_name("clipping-mask.psd"))
assert not psd.is_updated()
psd[1][2].clipping = False
assert psd.is_updated()
Loading