Repository: psd-tools/psd-tools
Pull request #442 (2024-10-12 19:24:35)

Layer locking
Added layer locking methods.

The new constants `ProtectedFlags` describe the different values for protecting a layer in the same order as the image below.
![image](https://github.com/user-attachments/assets/0c727021-42ce-4d36-b79a-cef500205504)

The four first flags can be combined to make a mask to describe multiple locks on a layer.

The methods are:
`lock()` To lock a layer with the given integer value. Using bitwise 'or' operation on the constants described above its possible to make a proper layer protection. Fully locks as a default value.
`unlock()` Will remove all locks on a layer.
`locks()` Will return the ProtectedSetting data block of the layer to allow a user to check the layer protections.

Eg: `layer.lock(ProtectedFlags.COMPOSITE | ProtectedFlags.POSITION)`

https://github.com/psd-tools/psd-tools/issues/440.




Patch:
diff --git a/src/psd_tools/api/layers.py b/src/psd_tools/api/layers.py
index c7e92bd..54a11b8 100644
--- a/src/psd_tools/api/layers.py
+++ b/src/psd_tools/api/layers.py
@@ -11,11 +11,11 @@ from psd_tools.api.mask import Mask
 from psd_tools.api.shape import Origination, Stroke, VectorMask
 from psd_tools.api.smart_object import SmartObject
 from psd_tools.api.pil_io import get_pil_channels, get_pil_depth, get_pil_mode
-from psd_tools.constants import BlendMode, Clipping, Tag, TextType, Compression, ChannelID, SectionDivider
+from psd_tools.constants import BlendMode, Clipping, Tag, TextType, Compression, ChannelID, SectionDivider, ProtectedFlags
 from psd_tools.terminology import Key
 
 from psd_tools.psd.layer_and_mask import LayerRecord, ChannelDataList, ChannelData, ChannelInfo
-from psd_tools.psd.tagged_blocks import TaggedBlocks
+from psd_tools.psd.tagged_blocks import TaggedBlocks, ProtectedSetting
 from psd_tools.psd.patterns import Patterns
 
 logger = logging.getLogger(__name__)
@@ -352,6 +352,37 @@ class Layer(object):
                 self._stroke = Stroke(stroke)
         return self._stroke
 
+    def lock(self, lock_flags = ProtectedFlags.COMPLETE):
+        """
+        Locks a layer accordind to the combination of flags.
+
+        :param lockflags: An integer representing the locking state
+
+        Example using the constants of ProtectedFlags and bitwise or operation to lock both pixels and positions::
+
+            layer.lock(ProtectedFlags.COMPOSITE | ProtectedFlags.POSITION)
+        """
+
+        locks = self.locks
+
+        if locks is None:
+            locks = ProtectedSetting(0)
+            self.tagged_blocks.set(Tag.PROTECTED_SETTING, locks)
+
+        locks.lock(lock_flags)
+
+    def unlock(self):
+        self.lock(0)
+
+    @property
+    def locks(self):
+        protected_settings_block = self.tagged_blocks.get(Tag.PROTECTED_SETTING)
+        
+        if protected_settings_block is not None:
+            return protected_settings_block.data
+
+        return None
+
     def topil(self, channel=None, apply_icc=False):
         """
         Get PIL Image of the layer.
diff --git a/src/psd_tools/constants.py b/src/psd_tools/constants.py
index e241f7b..a5a48a7 100644
--- a/src/psd_tools/constants.py
+++ b/src/psd_tools/constants.py
@@ -518,6 +518,18 @@ class TextType(IntEnum):
     PARAGRAPH = 1
 
 
+class ProtectedFlags(IntEnum):
+    """
+    Flags for layer locking
+    """
+    TRANSPARENCY = 0x01
+    COMPOSITE = 0x02
+    POSITION = 0x04
+    NESTING = 0x08
+
+    COMPLETE = 2147483648
+
+    
 class AlphaChannelMode(IntEnum):
     """
     alpha channel types.
diff --git a/src/psd_tools/psd/tagged_blocks.py b/src/psd_tools/psd/tagged_blocks.py
index 25760b1..d2321e2 100644
--- a/src/psd_tools/psd/tagged_blocks.py
+++ b/src/psd_tools/psd/tagged_blocks.py
@@ -633,6 +633,17 @@ class ProtectedSetting(IntegerElement):
     def position(self):
         return bool(self.value & 0x04)
 
+    @property
+    def nesting(self):
+        return bool(self.value & 0x08)
+
+    @property
+    def complete(self):
+        return self.value == 2147483648
+
+    def lock(self, lock_flags):
+        self.value = int(lock_flags)
+
 
 @register(Tag.REFERENCE_POINT)
 @attr.s(repr=False, slots=True)


Test patch:
diff --git a/tests/psd_tools/api/test_layers.py b/tests/psd_tools/api/test_layers.py
index b4bd49f..cd22449 100644
--- a/tests/psd_tools/api/test_layers.py
+++ b/tests/psd_tools/api/test_layers.py
@@ -6,7 +6,7 @@ import pytest
 
 from psd_tools.api.layers import Group, PixelLayer, ShapeLayer, Artboard
 from psd_tools.api.psd_image import PSDImage
-from psd_tools.constants import BlendMode, Tag, SectionDivider
+from psd_tools.constants import BlendMode, Tag, SectionDivider, ProtectedFlags
 
 from psd_tools.api.pil_io import get_pil_channels, get_pil_depth
 
@@ -536,4 +536,28 @@ def test_artboard_move(group):
     assert artboard._channels is group._channels
     assert artboard._record is group._record
     assert artboard._bounding_channels is group._bounding_channels
-    assert artboard._bounding_record is group._bounding_record
\ No newline at end of file
+    assert artboard._bounding_record is group._bounding_record
+
+def test_lock_layer(pixel_layer):
+
+    pixel_layer.lock(ProtectedFlags.TRANSPARENCY | ProtectedFlags.COMPOSITE | ProtectedFlags.POSITION)
+
+    assert pixel_layer.locks.transparency
+    assert pixel_layer.locks.composite
+    assert pixel_layer.locks.position
+    assert not pixel_layer.locks.nesting
+
+    pixel_layer.lock(ProtectedFlags.NESTING)
+
+    assert not pixel_layer.locks.transparency
+    assert not pixel_layer.locks.composite
+    assert not pixel_layer.locks.position
+    assert pixel_layer.locks.nesting
+
+    pixel_layer.unlock()
+
+    assert pixel_layer.locks.value == 0
+
+    pixel_layer.lock()
+
+    assert pixel_layer.locks.complete
\ No newline at end of file