From 28ce1ae8865189d2df7808d49d456a079a2a59b5 Mon Sep 17 00:00:00 2001 From: Will Stott Date: Mon, 6 Oct 2025 00:17:17 +0100 Subject: [PATCH 1/3] Fix and expand parsing of PROP_COMPOSITE_MODE and PROP_COMPOSITE_SPACE --- gimpformats/GimpIOBase.py | 8 +- gimpformats/enums.py | 3 +- tests/with_paths/with_paths.txt | 8 +- tests/xcf_document/dest/issue_14.txt | 8 +- tests/xcf_document/dest/layer_comp_modes.txt | 158 ++++++++++++++++++ tests/xcf_document/dest/layer_groups.txt | 12 +- tests/xcf_document/dest/testComplexImage.txt | 32 ++-- .../dest/testOneLayerWithTransparency.txt | 4 +- tests/xcf_document/src/layer_comp_modes.xcf | Bin 0 -> 3277 bytes tests/xcf_document/test_xcf_document.py | 2 +- 10 files changed, 197 insertions(+), 38 deletions(-) create mode 100644 tests/xcf_document/dest/layer_comp_modes.txt create mode 100644 tests/xcf_document/src/layer_comp_modes.xcf diff --git a/gimpformats/GimpIOBase.py b/gimpformats/GimpIOBase.py index 10c90be..f5ce88f 100644 --- a/gimpformats/GimpIOBase.py +++ b/gimpformats/GimpIOBase.py @@ -325,9 +325,9 @@ def _propertyDecode(self, prop: int, data: bytearray) -> int: elif _prop_cmp(prop, AllProps.PROP_COLOR_TAG): self.colorTag = list(TagColor)[ioBuf.u32] elif _prop_cmp(prop, AllProps.PROP_COMPOSITE_MODE): - self.compositeMode = list(CompositeMode)[ioBuf.i32] + self.compositeMode = list(CompositeMode)[abs(ioBuf.i32) - 1] elif _prop_cmp(prop, AllProps.PROP_COMPOSITE_SPACE): - self.compositeSpace = list(CompositeSpace)[ioBuf.i32] + self.compositeSpace = list(CompositeSpace)[abs(ioBuf.i32) - 1] elif _prop_cmp(prop, AllProps.PROP_BLEND_SPACE): self.blendSpace = ioBuf.u32 elif _prop_cmp(prop, AllProps.PROP_FLOAT_COLOR): @@ -487,10 +487,10 @@ def _propertyEncode(self, prop: int) -> bytearray: ioBuf.u32 = list(TagColor).index(self.colorTag) elif _prop_cmp(prop, AllProps.PROP_COMPOSITE_MODE): if self.compositeMode is not None: - ioBuf.i32 = list(CompositeMode).index(self.compositeMode) + ioBuf.i32 = list(CompositeMode).index(self.compositeMode) + 1 elif _prop_cmp(prop, AllProps.PROP_COMPOSITE_SPACE): if self.compositeSpace is not None: - ioBuf.i32 = list(CompositeSpace).index(self.compositeSpace) + ioBuf.i32 = list(CompositeSpace).index(self.compositeSpace) + 1 elif _prop_cmp(prop, AllProps.PROP_BLEND_SPACE): if self.blendSpace is not None: ioBuf.u32 = self.blendSpace diff --git a/gimpformats/enums.py b/gimpformats/enums.py index cfe20a1..f7ad59c 100644 --- a/gimpformats/enums.py +++ b/gimpformats/enums.py @@ -26,8 +26,9 @@ class CompositeMode(Enum): class CompositeSpace(Enum): RGB_linear = "RGB (linear)" - RGB_perceptual = "RGB (perceptual)" + RGB_profile = "RGB (from color profile)" LAB = "LAB" + RGB_perceptual = "RGB (perceptual)" class TagColor(Enum): diff --git a/tests/with_paths/with_paths.txt b/tests/with_paths/with_paths.txt index 484ebae..e11c781 100644 --- a/tests/with_paths/with_paths.txt +++ b/tests/with_paths/with_paths.txt @@ -58,8 +58,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 @@ -88,8 +88,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 diff --git a/tests/xcf_document/dest/issue_14.txt b/tests/xcf_document/dest/issue_14.txt index 840cb10..8ed41f0 100644 --- a/tests/xcf_document/dest/issue_14.txt +++ b/tests/xcf_document/dest/issue_14.txt @@ -60,8 +60,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 @@ -90,8 +90,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 diff --git a/tests/xcf_document/dest/layer_comp_modes.txt b/tests/xcf_document/dest/layer_comp_modes.txt new file mode 100644 index 0000000..4891a2e --- /dev/null +++ b/tests/xcf_document/dest/layer_comp_modes.txt @@ -0,0 +1,158 @@ +Version: 23 +Width: 10 +Height: 10 +Base Color Mode: 0 +Precision: +Selected: False +Is Selection: False +Blend Mode: GimpBlendMode.ADDITION +Visible: False +Is Linked: False +Lock Alpha: False +Apply Mask: False +Editing Mask: False +Show Mask: False +Show Masked: False +X Offset: 0 +Y Offset: 0 +Compression: CompressionMode.RLE +Horizontal Resolution: 300.0 +Vertical Resolution: 300.0 +Unique Id: 00000006 +Units: Units.Millimetres +Group Item Flags: 0 +Position Locked: False +Opacity: 1.0 +Color Tag: TagColor.Blue +Composite Mode: CompositeMode.Union +Composite Space: CompositeSpace.RGB_linear +Vectors Version: 0 +Active Vector Index: 0 +Paths: [] +Offset: 0 x 0 +Resolution: 300.0ppi x 300.0ppi +Parasites: + + + +Layers: + Name: RGB Color Profile - Intersection + Size: 10 x 10 + colorMode: RGB color with alpha + Selected: True + Is Selection: False + Blend Mode: GimpBlendMode.NORMAL + Visible: True + Is Linked: False + Lock Alpha: False + Apply Mask: False + Editing Mask: False + Show Mask: False + Show Masked: False + X Offset: 0 + Y Offset: 0 + Compression: CompressionMode.None_Compression + Unique Id: 00000006 + Units: Units.Inches + Locked: False + Group Item Flags: 0 + Position Locked: False + Opacity: 1.0 + Color Tag: TagColor.None_Color + Composite Mode: CompositeMode.Intersection + Composite Space: CompositeSpace.RGB_profile + Blend Space: 0 + Vectors Version: 0 + Active Vector Index: 0 + Paths: [] + Offset: 0 x 0 + Name: RGB Linear - Clip To Backdrop + Size: 10 x 10 + colorMode: RGB color with alpha + Selected: False + Is Selection: False + Blend Mode: GimpBlendMode.NORMAL + Visible: True + Is Linked: False + Lock Alpha: False + Apply Mask: False + Editing Mask: False + Show Mask: False + Show Masked: False + X Offset: 0 + Y Offset: 0 + Compression: CompressionMode.None_Compression + Unique Id: 00000004 + Units: Units.Inches + Locked: False + Group Item Flags: 0 + Position Locked: False + Opacity: 1.0 + Color Tag: TagColor.None_Color + Composite Mode: CompositeMode.Clip_to_backdrop + Composite Space: CompositeSpace.RGB_linear + Blend Space: 0 + Vectors Version: 0 + Active Vector Index: 0 + Paths: [] + Offset: 0 x 0 + Name: RGB Perceptual - Union + Size: 10 x 10 + colorMode: RGB color with alpha + Selected: False + Is Selection: False + Blend Mode: GimpBlendMode.NORMAL + Visible: True + Is Linked: False + Lock Alpha: False + Apply Mask: False + Editing Mask: False + Show Mask: False + Show Masked: False + X Offset: 0 + Y Offset: 0 + Compression: CompressionMode.None_Compression + Unique Id: 00000003 + Units: Units.Inches + Locked: False + Group Item Flags: 0 + Position Locked: False + Opacity: 1.0 + Color Tag: TagColor.None_Color + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_perceptual + Blend Space: 0 + Vectors Version: 0 + Active Vector Index: 0 + Paths: [] + Offset: 0 x 0 + Name: Auto + Size: 10 x 10 + colorMode: RGB color with alpha + Selected: False + Is Selection: False + Blend Mode: GimpBlendMode.NORMAL + Visible: True + Is Linked: False + Lock Alpha: False + Apply Mask: False + Editing Mask: False + Show Mask: False + Show Masked: False + X Offset: 0 + Y Offset: 0 + Compression: CompressionMode.None_Compression + Unique Id: 00000005 + Units: Units.Inches + Locked: False + Group Item Flags: 0 + Position Locked: False + Opacity: 1.0 + Color Tag: TagColor.None_Color + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear + Blend Space: 0 + Vectors Version: 0 + Active Vector Index: 0 + Paths: [] + Offset: 0 x 0 \ No newline at end of file diff --git a/tests/xcf_document/dest/layer_groups.txt b/tests/xcf_document/dest/layer_groups.txt index 73361c0..b8ce850 100644 --- a/tests/xcf_document/dest/layer_groups.txt +++ b/tests/xcf_document/dest/layer_groups.txt @@ -59,8 +59,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 @@ -89,8 +89,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 @@ -119,8 +119,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 diff --git a/tests/xcf_document/dest/testComplexImage.txt b/tests/xcf_document/dest/testComplexImage.txt index 58f34b6..c2b2822 100644 --- a/tests/xcf_document/dest/testComplexImage.txt +++ b/tests/xcf_document/dest/testComplexImage.txt @@ -58,8 +58,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 @@ -88,8 +88,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 @@ -118,8 +118,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 @@ -148,8 +148,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 @@ -179,8 +179,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 @@ -209,8 +209,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 @@ -239,8 +239,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 @@ -269,8 +269,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 diff --git a/tests/xcf_document/dest/testOneLayerWithTransparency.txt b/tests/xcf_document/dest/testOneLayerWithTransparency.txt index 47196d1..7e22ce2 100644 --- a/tests/xcf_document/dest/testOneLayerWithTransparency.txt +++ b/tests/xcf_document/dest/testOneLayerWithTransparency.txt @@ -58,8 +58,8 @@ Layers: Position Locked: False Opacity: 1.0 Color Tag: TagColor.None_Color - Composite Mode: CompositeMode.Intersection - Composite Space: CompositeSpace.LAB + Composite Mode: CompositeMode.Union + Composite Space: CompositeSpace.RGB_linear Blend Space: 0 Vectors Version: 0 Active Vector Index: 0 diff --git a/tests/xcf_document/src/layer_comp_modes.xcf b/tests/xcf_document/src/layer_comp_modes.xcf new file mode 100644 index 0000000000000000000000000000000000000000..e2b7a275cd207bc98bf82b51413d39d8feda8b34 GIT binary patch literal 3277 zcmdT`PjB2r6yE@$+uc;O(1I!_uf4J=AR>x|uAe0dPbIV)*!lie z!I|U%`9a7L+1`1ui#?YsQ<1QUccVfCr~~Vz>#Lp3!3-kJ4v%?pTE0gn-Xa5wLcwA|D4;JL|(%Upo$g)4pU8Nm;@N);ALo+B-NO_NI{KE-xOjB#*f@ zyi4HeJ77BB0$Qruz zgb^$2??kYW@g9e@IF>-_W0*Wh?u6h^rViHdHl>bxg2U6r9d<{V%wi&8pJgk;_y+&?Pw@2qs(oqK^_fvXK6F%yQiyk*6Cwaj` zF&S7na$AnF+#=&59#|tO^Pc0RrOkruIk(|ha}I4B*j|n`>^p~Xj_i#mNp2rn+c*pA z7&Q6c*E{6o>?X-^c}-ZpP(~2NuJgc*7D{D$Lf|K1fCuX2`9C4xR0lzP*v> zvG5sgN$+)PyDsh0wKwg#*PU@WPUl4(S2L5*cc$kh9<@ff@Um_+|E}xS*LB_g*)Tc^ z^<9fMzI&1B~P8asdFw!m&PA7^=o+3S;h{DvWmK{{u92 zY5t~>QEQ@9uFXH*&Id4|7diO^Nxm;q&I(|6D;7D~&&Ve8zYmHmN9Xg% zEPk107FpKJDz@;muw4ANu4mqw9cA0PZuIItP_ GimpDocument: @pytest.mark.parametrize( - ("image_name"), ["testOneLayerWithTransparency", "testComplexImage", "issue_14", "layer_groups"] + ("image_name"), ["testOneLayerWithTransparency", "testComplexImage", "issue_14", "layer_groups", "layer_comp_modes"] ) def test_image_repr(gimp_doc: GimpDocument, image_name: str) -> None: """Test the text representation of an image.""" From 6d9ddfdff37d6654ccf5f02bbb74cf92048fad4f Mon Sep 17 00:00:00 2001 From: Will Stott Date: Sun, 5 Oct 2025 22:37:16 +0100 Subject: [PATCH 2/3] Add basic parsing of PROP_LOCK_VISIBILITY (required for new test files) --- gimpformats/GimpIOBase.py | 6 ++++++ gimpformats/enums.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/gimpformats/GimpIOBase.py b/gimpformats/GimpIOBase.py index f5ce88f..8b73ee7 100644 --- a/gimpformats/GimpIOBase.py +++ b/gimpformats/GimpIOBase.py @@ -67,6 +67,7 @@ def __init__(self, parent: Any = None) -> None: self.isGroup = None self.groupItemFlags: int = 0 self.positionLocked: bool = False + self.visibilityLocked: bool = False self.opacity: float = 1.0 self.colorTag: TagColor = TagColor.Blue self.compositeMode: CompositeMode = CompositeMode.Union @@ -320,6 +321,8 @@ def _propertyDecode(self, prop: int, data: bytearray) -> int: self.groupItemFlags = ioBuf.u32 elif _prop_cmp(prop, AllProps.PROP_LOCK_POSITION): self.positionLocked = ioBuf.boolean + elif _prop_cmp(prop, AllProps.PROP_LOCK_VISIBILITY): + self.visibilityLocked = ioBuf.boolean elif _prop_cmp(prop, AllProps.PROP_FLOAT_OPACITY): self.opacity = ioBuf.float32 elif _prop_cmp(prop, AllProps.PROP_COLOR_TAG): @@ -479,6 +482,9 @@ def _propertyEncode(self, prop: int) -> bytearray: elif _prop_cmp(prop, AllProps.PROP_LOCK_POSITION): if self.positionLocked is not None and self.positionLocked: ioBuf.boolean = self.positionLocked + elif _prop_cmp(prop, AllProps.PROP_LOCK_VISIBILITY): + if self.visibilityLocked is not None and self.visibilityLocked: + ioBuf.boolean = self.visibilityLocked elif _prop_cmp(prop, AllProps.PROP_FLOAT_OPACITY): if self.opacity is not None and isinstance(self.opacity, float): ioBuf.float32 = self.opacity diff --git a/gimpformats/enums.py b/gimpformats/enums.py index f7ad59c..affb7b3 100644 --- a/gimpformats/enums.py +++ b/gimpformats/enums.py @@ -151,7 +151,7 @@ class GeneralProperties(Enum): PROP_VISIBLE = 8 PROP_TATTOO = 20 # PROP_ITEM_SET_ITEM = 41 - # PROP_LOCK_VISIBILITY = 42 + PROP_LOCK_VISIBILITY = 42 class ImageProperties(Enum): From 7aac86cdcd7ee293cd156935b9cb61330885499c Mon Sep 17 00:00:00 2001 From: Will Stott Date: Mon, 6 Oct 2025 23:20:38 +0100 Subject: [PATCH 3/3] Small optimization of RLE decoding to avoid temporary lists. list is a generic container with indirection for every element's type. This makes it slower than a bytearray for basically any operation. >>> timeit("a.extend(a[1:2] * 6024)", "a = bytearray(b'123' * 1024)", globals=globals()) 7.540333299897611 >>> timeit("a.extend([a[1]] * 6024)", "a = bytearray(b'123' * 1024)", globals=globals()) 35.999150899937376 --- gimpformats/GimpImageLevel.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gimpformats/GimpImageLevel.py b/gimpformats/GimpImageLevel.py index 9d3eacb..3baf342 100644 --- a/gimpformats/GimpImageLevel.py +++ b/gimpformats/GimpImageLevel.py @@ -127,9 +127,9 @@ def _decodeRLE(self, data: bytearray, pixels: int, bpp: int, index: int = 0) -> index += 1 if 0 <= opcode <= 126: # Short run of identical bytes - val = data[index] + val = data[index:index+1] # use 1-len bytearray to avoid slow list index += 1 - ret[chan].extend([val] * (opcode + 1)) # Extend is faster than append in a loop + ret[chan].extend(val * (opcode + 1)) # Extend is faster than append in a loop n += opcode + 1 elif opcode == 127: # Long run of identical bytes @@ -137,10 +137,10 @@ def _decodeRLE(self, data: bytearray, pixels: int, bpp: int, index: int = 0) -> index += 1 b = data[index] index += 1 - val = data[index] + val = data[index:index+1] # use 1-len bytearray to avoid slow list index += 1 amt = (m << 8) + b - ret[chan].extend([val] * amt) + ret[chan].extend(val * amt) n += amt elif opcode == 128: # Long run of different bytes