Can't set perchannel nTransfers through 5.2 API

Ok, so this is how I implemmented @KnowZero’s suggestion:
This is a snippet from a bigger script, here’s the whole file if you are curious.

Essentially, this is what I’m doing:

  1. Extract maindoc.xml from the *.kra file. This is where Krita stores layer information.

  2. Parse the maindoc.xml for a specifically named layer, so that I can access the correct *.filterconfig file. This is where krita stores the XML tree for the layer’s curves.

  3. Create a temporary *.kra file without the target *.filterconfig. If we just write a new file with the same filename to a zip, we’ll have duplicate files. I don’t think we can exclude files from a zip with zipfile but we can write a new zip without a file.

  4. Open the temprorary file and create a new *.filterconfig file with the desired configuration.

  5. Rename the temporary file and cleanup

Here’s the code:

# TODO: This is a hack to allow `perchannel` layer configuration because it's broken
# in Krita 5.2.0
import os
import xml.etree.ElementTree as ET
import zipfile as zp

# `xml` representation of the desired Color Adjust Filter Layer configuration
# Indentation is the same as in a regular `filterconfig` file
XML_LAYER_NAME = "Alpha Adjust"
XML_LAYER_ASSTR = """<!DOCTYPE params>
<params version="1">
 <param name="nTransfers">8</param>
 <param name="curve0">0,0;1,1;</param>
 <param name="curve1">0,0;1,1;</param>
 <param name="curve2">0,0;1,1;</param>
 <param name="curve3">0,0;1,1;</param>
 <param name="curve4">0,0;0.156377,0.0809037;0.850071,0.758355;1,1;</param>
 <param name="curve5">0,0;1,1;</param>
 <param name="curve6">0,0;1,1;</param>
 <param name="curve7">0,0;1,1;</param>
</params>
"""
XML_FILE = "maindoc.xml"


def xml_find_layer_byname(xml_root: ET.Element, layer_name: str) -> ET.Element:
    """Looks into all `<layer>` elements in the document's root and returns the
    layer named `Alpha Adjust`.

    Attributes:
        `xml_root` (`ET.Element`): the root element from the xml
        `layer_name` (`str`): the name of the target layer

    Returns:
        `layer` (`Et.Element`): the target layer element hardcoded in the function
    """
    for layer in xml_root[0].iter("{http://www.calligra.org/DTD/krita}layer"):
        if layer_name in layer.attrib.values():
            return layer
        else:
            pass


# Close the document to allow for `XML` editing
image_closed = current_document.close()

# Change the working directory to the current document's folder
# This is required because the script's CWD is Krita's install folder
cwd = os.chdir(path=current_doc_path)

# Assign a name to a temporary file
temp_zip = str("_" + Path(new_file_path).name)

# Declare empty variables to hold data from `maindoc.xml`
filterconfig_file = None

# Check whether the new image is closed and the file was saved
if image_closed:
    # Open the original Krita file as if it were a `zip`
    with zp.ZipFile(new_file_path, mode="r") as archive:
        print("About to extract the XML")
        archive.extract(XML_FILE)

        # Read the extracted `xml`
        xml_tree = ET.parse(XML_FILE)
        xml_root = xml_tree.getroot()

        # Get the target node's xml element
        target_node = xml_find_layer_byname(
            xml_root=xml_root, layer_name=XML_LAYER_NAME
        )

        # Get the target `.filterconfig` filename
        filterconfig_file = f"{target_node.get('filename')}.filterconfig"

        # Open a new, empty Krita file
        with zp.ZipFile(temp_zip, mode="w") as new_archive:
            print("About to build new .kra")
            # Iterate through each file in the `.kra` archive and write all contents
            # to the new file, except for the target `filterconfig` file
            for item in archive.infolist():
                if item.filename == f"Unnamed/layers/{filterconfig_file}":
                    continue
                with archive.open(item.filename) as file:
                    new_archive.writestr(item, file.read())

    # Replace the old file
    os.remove(new_file_path)
    os.rename(temp_zip, Path(new_file_path).name)

    # Write the correct `filterconfig` file in the new `.kra` file
    with zp.ZipFile(Path(new_file_path).name, mode="a") as archive:
        with archive.open(f"Unnamed/layers/{filterconfig_file}", mode="w") as file:
            binary = XML_LAYER_ASSTR.encode("utf-8")
            file.write(binary)

    # Cleanup the unneded xml file
    os.remove(XML_FILE)

    # Reload the ready document
    finished_document = app.openDocument(new_file_path)
    app.activeWindow().addView(finished_document)

Anyway, thank you very much for the help!

EDIT:
I’ve written a self-contained script that can be tested with any Krita file:
It creates a Color Adjust Filter Layer, attempts to save the file and configures the layer XML properties.

Click on the spoiler tag to see the code:
import os
import xml.etree.ElementTree as ET
import zipfile as zp
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ..PyKrita import *
else:
    from krita import *


# Connect to the application
app = Krita.instance()
current_document = app.activeDocument()
# Get the root node object
root_node = current_document.rootNode()
# Get current path and file name
current_doc_filename = Path(current_document.fileName())
current_doc_path = current_doc_filename.parent
current_doc_name = current_doc_filename.stem


# Names for new layers
ALPHA_ADJUST = "Alpha Adjust"

# `xml` representation of the desired Color Adjust Filter Layer configuration
# Indentation is the same as in a regular `filterconfig` file
XML_LAYER_NAME = "Alpha Adjust"
XML_LAYER_ASSTR = """<!DOCTYPE params>
<params version="1">
 <param name="nTransfers">8</param>
 <param name="curve0">0,0;1,1;</param>
 <param name="curve1">0,0;1,1;</param>
 <param name="curve2">0,0;1,1;</param>
 <param name="curve3">0,0;1,1;</param>
 <param name="curve4">0,0;0.156377,0.0809037;0.850071,0.758355;1,1;</param>
 <param name="curve5">0,0;1,1;</param>
 <param name="curve6">0,0;1,1;</param>
 <param name="curve7">0,0;1,1;</param>
</params>
"""
XML_FILE = "maindoc.xml"


# TODO: This would be the correct way to create a `perchannel` layer
# but it's not working as of 5.2.0
def create_filterlayer(
    name: str, parent_layer: Node = root_node, layer_bellow: str | None = None
) -> Node:
    """Creates an Adjustment Filter Layer with specific curve values
    hardcoded in this function.

    Args:
        `name` (`str`): Name for the new layer\n
        `parent_layer` (`Node`): The layer where this new one will be nested on.
        Defaults to `root_node`.\n
        `layer_bellow` (`str` | None, optional): The layer that will be bellow this new one.
        Defaults to `None`.\n

    Returns:
        `color_adjust_filterlayer` (`Node`): The node object for the new layer.
    """
    # Check if layer position is required and get it's `Node` object
    layer_position = current_document.nodeByName(layer_bellow) if layer_bellow else None

    # Define the configuration for the filter
    filter_config = InfoObject()
    filter_config.setProperties(
        {
            "nTransfers": 8,
            "curve0": "0,0;1,1;",
            "curve1": "0,0;1,1;",
            "curve2": "0,0;1,1;",
            "curve3": "0,0;1,1;",
            "curve4": "0,0;0.156377,0.0809037;0.850071,0.758355;1,1;",
            "curve5": "0,0;1,1;",
            "curve6": "0,0;1,1;",
            "curve7": "0,0;1,1;",
        }
    )

    # Create a Color Adjust filter and set it's configuration
    color_adjust_filter = Filter()
    color_adjust_filter.setName("perchannel")
    color_adjust_filter.setConfiguration(filter_config)

    # Create a new selection
    selection = Selection()
    selection.select(0, 0, current_document.width(), current_document.height(), 255)

    # Create a new filter layer from the selection and the filter
    color_adjust_filterlayer = current_document.createFilterLayer(
        name, color_adjust_filter, selection
    )
    # Add the new node to the parent layer in the parameters
    parent_layer.addChildNode(color_adjust_filterlayer, layer_position)

    return color_adjust_filterlayer

def xml_find_layer_byname(xml_root: ET.Element, layer_name: str) -> ET.Element:
    """Looks into all `<layer>` elements in the document's root and returns the
    layer named `Alpha Adjust`.

    Attributes:
        `xml_root` (`ET.Element`): the root element from the xml
        `layer_name` (`str`): the name of the target layer

    Returns:
        `layer` (`Et.Element`): the target layer element hardcoded in the function
    """
    for layer in xml_root[0].iter("{http://www.calligra.org/DTD/krita}layer"):
        if layer_name in layer.attrib.values():
            return layer
        else:
            pass


# Refresh the viewport
current_document.refreshProjection()

# Save the file as a `.kra` file in the same place, with the same name
saved_document = current_document.save()

# TODO: As of 5.2.0, there is a bug preventing the `perchannel` layer
# from being configured, so this just creates the layer
alpha_adjust_layer = create_filterlayer(name=ALPHA_ADJUST)

# Close the document to allow for `XML` editing after saving
image_closed = current_document.close() if saved_document else None

# Change the working directory to the current document's folder
# This is required because the script's CWD is Krita's install folder
cwd = os.chdir(path=current_doc_path)

# Assign a name to a temporary file
temp_zip = str("_" + Path(current_doc_filename).name)

# Declare empty variables to hold data from `maindoc.xml`
filterconfig_file = None

# Check whether the new image is closed and the file was saved
if image_closed:
    # Open the original Krita file as if it were a `zip`
    with zp.ZipFile(current_doc_filename, mode="r") as archive:
        print("About to extract the XML")
        archive.extract(XML_FILE)

        # Read the extracted `xml`
        xml_tree = ET.parse(XML_FILE)
        xml_root = xml_tree.getroot()

        # Get the target node's xml element
        target_node = xml_find_layer_byname(
            xml_root=xml_root, layer_name=XML_LAYER_NAME
        )

        # Get the target `.filterconfig` filename
        filterconfig_file = f"{target_node.get('filename')}.filterconfig"

        # Open a new, empty Krita file
        with zp.ZipFile(temp_zip, mode="w") as new_archive:
            print("About to build new .kra")
            # Iterate through each file in the `.kra` archive and write all contents
            # to the new file, except for the target `filterconfig` file
            for item in archive.infolist():
                if item.filename == f"Unnamed/layers/{filterconfig_file}":
                    continue
                with archive.open(item.filename) as file:
                    new_archive.writestr(item, file.read())

    # Replace the old file
    os.remove(current_doc_filename)
    os.rename(temp_zip, Path(current_doc_filename).name)

    # Write the correct `filterconfig` file in the new `.kra` file
    with zp.ZipFile(Path(current_doc_filename).name, mode="a") as archive:
        with archive.open(f"Unnamed/layers/{filterconfig_file}", mode="w") as file:
            binary = XML_LAYER_ASSTR.encode("utf-8")
            file.write(binary)

    # Cleanup the unneded xml file
    os.remove(XML_FILE)

    # Reload the ready document
    finished_document = app.openDocument(str(current_doc_filename))
    app.activeWindow().addView(finished_document)
1 Like