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:
-
Extract
maindoc.xmlfrom the*.krafile. This is where Krita stores layer information. -
Parse the
maindoc.xmlfor a specifically named layer, so that I can access the correct*.filterconfigfile. This is where krita stores the XML tree for the layer’s curves. -
Create a temporary
*.krafile 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 withzipfilebut we can write a new zip without a file. -
Open the temprorary file and create a new
*.filterconfigfile with the desired configuration. -
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)