Script for a setup of multiple layers of different types (WIP, feedback and advice appreciated)

Hello everyone! hope you guys are doing fine.

I am here to ask on some advice, help, and guidance.

In short, i would like to be able to create and modify layers of diferent types and its properties using custom scripts and the “Ten Scripts” plug in to speed up my workflow.

Now, a little background about my workflow:

I have a way of shading that is quite flexible and fun to work with for myself, unfortunately the downside is that it is overcomplicated and requires a specific layer set up that i have to manually make (example below)

Is this too much for such a simple task? yes, absolutely, but i really enjoy shading once i set everything up.

(I am aware this is a weird workflow, i am willing to explain and share my process and my reasons to do things this way, but i’ll leave that to a different post in another time)

Now, what i want to do is simply

  • create different types of layers (specifically paint, fill and filter) and put them in groups
  • be able to set specific parameters to each layer; opacity, fill type/color, filter layer type and presets
  • have these scripts on the Ten Scripts plug in to use them quickly

And i have a somewhat clear idea on what the basic procedure i want to achieve is. My only issue is that i’m struggling to understand how to make the script itself.

I have a very basic understanding of programing (no need to explain the basics of python, basic things like variables, functions, loops, that type of thing)

i took a look at both Krita’s documentation as well as the Krita Scripting School. They are useful for sure but i still have a lot of confusion due to my inexperience with doing anything code related.

But with my limited knowledge i think i was able to figure out how to create the layers and modify some aspects like the name and opacity of a layer. What i need to figure out is a way to change a fill layer type (between color or pattern) and change its color if the fill type is set to color. And for filter layers i need to know if i can tell the script to apply a preset.

what i want to achieve specifically is this (this is what i already do manually)

  1. Make a group layer on top of whatever layer i have selected, this will be the “lighting” group and will be set to “color dodge” blending mode
  2. inside of this group there will be a fill layer set to pure black, this is just the background, it will be invisible since the group is in color dodge mode but it will be necessary for colors to mix properly
  3. above the background layer there will be another group named something like “DRAW HERE” inside there will be only paint layers, more specifically i only need one in the script
  4. above this “draw here” group, there will be a filter layer set as a gradient map filter, to this, all i want is to set a specific preset i have already made.
  5. this is optional but at the end i want the selected layer to be the one paint layer inside the “draw here” group

Some clarifying before i go:

  • I am aware krita has the Task Sets Docker, this is not useful to me because i already do these actions using shortcuts
  • i know someone has made a plug in that automates actions like in photoshop, which im sure is great but it is a payed plug in, and right now i am unable to pay for it, i am considering getting it at some point for sure, but not right now
  • i do want to learn to do scripts, not only for this but i find the skill useful, specially on krita.
  • I only want to make this script for myself, not planning to do a perfectly optimized script, I simply want something that works, dont mind if its not the bestor fastest way of doing things

(I am doing all this explanation not to get someone to make this script it for me, but because i genuinely want to learn to do this myself, I apologize if this is a messy explanation, its a little late at the time i’m writing this and i’m feeling burned out from reading the documentation and trying to do this. I will try to explain further if needed once i feel more fresh)

1 Like

So, if I understand you correctly, and the structure you want is always identical, meaning it doesn’t need to be rearranged and varied internally multiple times during painting, then I wonder why you don’t work with a template?
And, at least that is what it sounds like, if you need this layer structure multiple times/repeatedly in your files, then you can cover even that with a template. How? Create a file with your complex layer structure, duplicate the finished group with the layer structure so that it exists twice in the document, then save the document as a template via File → Create Template From Current Image.

Now you can call up your template via File → New (CTRL+N), just like all the other templates, and have it created as a new document. In the document, you must never use one of your two groups with the special layer structure for painting. Instead, whenever you need this structure again in your document, i.e., a second, third, fourth, etc. time, copy the “untouchable” group and paste it back into the layer stack at the desired location. To make this copying and pasting faster, you can first define shortcuts for “Copy Layer” and “Paste Layer” in Krita under Settings → Configure Krita → Keyboard Shortcuts, and then continue to Krita → Layers.

If that is not the case, you may need to learn scripting.
And if your main goal is to learn scripting, then a template is obviously too simple. However, I mainly interpreted your question as meaning that you want to solve this problem and have only seen scripting as a possible solution so far.

Michelist

The best way to understand how people added nodes and configured them is looking at an examples.

Here is a link to the class list, relevant ones here are Document, Node, and FilterMaks. The Node page also contains links to all the other *layer( types).

I checked the Krita Scripting School page and it seems the one at the bottom is out of date? For whatever reason it’s now “legacy”, I think i searched through the kde for Krita to find that


(like Michalist suggests :slightly_smiling_face:) setting up a template, that might kickstart things rather than remaking every time.


Here is a script I’ve used for a long time, just for adding paint layers very handy (assigned with tenscripts f1 and f2 for 2 sets of layer configs to cycle through). edit: actually thats not entirely true this is a newer, cleaner version of a script i (myself) wrote and used for a long time :rofl:

from krita import *

inst = Krita.instance()
doc = inst.activeDocument()
layer = doc.activeNode()
qmwin = inst.activeWindow()
view = qmwin.activeView()
#layers = view.selectedNodes()

def msg(msg):
    duration = 500
    view.showFloatingMessage(msg, Krita.instance().icon("16_light_warning"), duration, 1)

def refresh_node(nd):
    if nd is None:
        return False
    # https://github.com/KDE/krita/blob/8b3a28df45153cdaa33be0d561d18ae8f5d8adfe/libs/pigment/KoCompositeOpRegistry.h
    nd_md = nd.blendingMode()
    if nd_md != 'allanon':
        nd.setBlendingMode('allanon')
    else:
        nd.setBlendingMode('parallel')
    nd.setBlendingMode(nd_md)
    return True

def set_layer_properties(layer, name, color_index, opacity, inherit_alpha):
    layer.setName(name)
    layer.setColorLabel(color_index)
    layer.setOpacity(int(opacity * (255 / 100)))
    layer.setInheritAlpha(inherit_alpha)
    refresh_node(layer)
    msg(f"Set selected layer to: {name}")

def get_next_configuration(current_name, configurations):
    keys = list(configurations.keys())
    if current_name in keys:
        index = keys.index(current_name)
        next_index = (index + 1) % len(keys)
        return keys[next_index], configurations[keys[next_index]]
    else:
        return keys[0], configurations[keys[0]]

def process_layer(layer, configurations):
    if layer.locked():
        return

    current_name = layer.name()
    next_name, config = get_next_configuration(current_name, configurations)
    set_layer_properties(layer, next_name, *config)

# Define the configurations for each layer name
configurations = {
    "lines": (8, 255, False),
    "base": (7, 255, False),
    # "shadow": (1, 30, True),
    "shade": (1, 100, True),
    "highlight": (2, 255, True),
    "RENAMETHIS": (0, 255, False),
}

process_layer(layer, configurations)

#lines GREY 8
#fill PURPLE 7
#shadow BLUE 1
#highlight GREEN 2

#future idea: check locked layers are red?

Here is a docker I’ve been using which contains buttons for inserting paint layers and group layers.

LMB sets the current layer
alt+LMB inserts a group above
shift+LMB attempts* to insert paint layer above
ctrl+LMB attempts* to insert paint layer below
*doesn’t work consistently, needs work :frowning:
(LMB = Left Mouse Button)

Configurable in xml file (i would have used json as its bit easier to read but cannot add comments or it breaks). Sets name, color label, opacity, blending mode, inherit alpha.

It des not support passthrough on groups, and to be clear anything not a paintlayer or group

Written by Gemini.

^also an example of how to add/configure nodes


Adding filtermask layers is not as straight forward butit can be done

Here is a script that adds a filtermask layer,

from PyQt5.QtCore import QTimer
from krita import InfoObject, Selection

def msg(msg, duration=500):
    Krita.instance().activeWindow().activeView().showFloatingMessage(msg, Krita.instance().icon("16_light_warning"), duration, 1)

def async_runner(func):
    """Decorator to automatically handle function execution with pauses."""
    import functools
    @functools.wraps(func)
    def wrapper():
        generator = func()
        def step():
            try:
                delay = int(next(generator))
                QTimer.singleShot(delay, step)  # Schedule next step
            except StopIteration:
                print('--- DELAYED EXECUTION COMPLETE ---')
        step()  # Start execution
    return wrapper

@async_runner
def func():
	inst = Krita.instance()
	doc = inst.activeDocument()
	node = doc.activeNode()
	if node.type() != "paintlayer":
		msg("need to select paint layer")
		return

	fn = "hsvadjustment"
	msg(f"applying {fn}", 1000)
	f = inst.filter(fn)
	fp = InfoObject()


	props = {'colorize': True, 'compatibilityMode': False, 'h': 150, 's': 50, 'type': 1, 'v': 0}

	fp.setProperties(props)
	f.setConfiguration(fp)

	fs = Selection()
	fs.selectAll(node, 255)

	fm = doc.createFilterMask(fn, f, fs)

	node.addChildNode(fm, None)

	# doc.refreshProjection()

	# yield 50

	# doc.setActiveNode(node)
	
	# yield 50

	# inst.action("flatten_layer").trigger()

	yield 0

func()

Script for seeing properties of a filtermask, requires Krita to be run from the console to see the output.

from krita import *

def print_filtermask_properties():
	inst = Krita.instance()
	doc = inst.activeDocument()
	node = doc.activeNode()
	if node.type() != "filtermask": return
	f = node.filter()
	print(f.name())
	cfg = f.configuration()
	print(cfg.properties())

print_filtermask_properties()

I also recommend this for testing scripts, it works for me as I use sublime text, scroll down for the newer version. The thread also includes a bit of workflow suggestions/explanation for how to see console output, which is extremely useful!

1 Like

Correct. This layer structure is always the same setup. And I already use a template to have it in the file.

However I didn’t considered to do a special “backup” copy of this layer group to copy paste.

In the template I have, whenever I need to, I just duplicate the group and delete the painting layers. Which doesnt take long to do, as I do this with shortcuts.

I did considered alternatives to scripting but this is not the only thing I would like to automate, so I would still like to learn how to automate actions with scripts. As well that I want to make use of the ten brushes script so that potentially, things that take me a couple of clicks or keyboard shortcuts take only one or two.

I dont want to do anything too complicated like a plug in, just sets of actions that do the same things I could do manually on krita assigned to one button.

I thought recreating this layer structure could be a good enough start to get familiar with scripting, as I already know the steps I want to take, I just need to figure out how to write them in python.

Thanks a lot for the suggestion as well, I will try to incorporate it better in my template.

1 Like

This is perfect! I actually forgot to mention this link was down from the krita scripting school page, I assumed it was under maintenance or something but seems that it was updated but the link to it in the documentation was not.

I’ll make sure to give this a read as I think this does solve one of my main issues that is not having a reference on how to change layer properties.

As for the rest of your post, I think I am understanding a little how you’re doing things. I’ll give a try once i get back to my computer and see how it goes. Thank you!

I hate to make this more confusing and I’m not 100% on this but I think the api contains functions that don’t exist in current releases, only in the 5.3 prealphas. I’m fairly certain the one I came across was in pan() in Canvas. So if you attempted to use some function and it doesnt work for apparently no reason, that could be why. Most do exist though :slightly_smiling_face:

1 Like

not a big deal as im using a prealpha 5.3 version.

Now the API link you sent seems to be for Krita’s source code instead of PyKrita, unless i’m missing something i was not able to fully understand how to integrate this in my scripts.

As an update, i looked up similar scripts here on the forum, i realized that only using actions was not an option for fill and filter layers, so now i’m taking small steps, so far i’m trying to figure out how to add only a black fill layer.

I want to point out that the Krita Python API that is linked on the Krita scripting School page seems to be down. But i found this other page, i am not sure how reliable it is as it doesn’t seem to be official

But honestly i think reading this one made me understand a little bit more on what i should try.

now here is my code so far to generate only the fill layer, doesn’t throw an error but it doesn’t seem to do anything when i run it :kiki_upside_down:

from krita import *

instance = Krita.instance()
doc = instance.activeDocument()
active_layer = doc.activeNode()

def make_fill_layer():

    fill_select = Selection()
    fill_select.selectAll(active_layer,255)
    info = InfoObject()
    info.setProperty("color",000000)
    doc.createFillLayer("SCRIPT FillLayer","color",info,fill_select)
    root_layer = doc.rootNode()
    child_layer = root_layer.childNodes()
    root_layer.addChildNode(active_layer, child_layer[0])
    
    doc.refreshProjection()


make_fill_layer()

feedback and critiques are appreciated

Yes it is confusing but i believe it is intentional as its generated from the source code, it takes a bit of getting used to but use whichever you’ve found works :slight_smile: The stuff under “Public Member Functions”, “Static Public Member Functions”, and “Additional Inherited Members" is not relevant. You can also copy/paste snippets into any chatbot to explain what it means in python.

In the KDE docs you may get even more confused if you click on any things like QRect, QString, QVariant,etc these will take you to the PyQt docs which is even more baffling. Unless you want to get into making a custom docker or widget I’d ignore it for now.

In your script the reason nothing seems to happen is the new FillLayer has no variable name so we cannot add it using addChildNodes, it just exists somewhere… You were on the right track though, adding a node is relevant but it has to be done after we created the node, then that node can be added as a child of another node. That node has to already have been added somewhere, or it will also exist in the void.

You would think Selection.selectAll would just select all but it seems to be related to each node? I’m not sure about that so I used the document bounds instead. Nodes can have a bounds which is greater or smaller than the image so that’s probably why.

Doing root.addChildNode(mynode, None) means it visually adds the node above the childnodes. In the docs it says the 2nd argument is the node you want to add the node “above”. If you wanted it to go above the active node, then you replace None with active_layer. If only it was explained in the exact place you would expect it rather than random examples in different pages :rofl:

addChildNode adds the given node in the list of children.

Parameters
    child	the node to be added
    above	the node above which this node will be placed 
from krita import *

instance = Krita.instance()
doc = instance.activeDocument()
active_layer = doc.activeNode()

def make_fill_layer():

    sel = doc.selection()
    if not sel:
        sel = Selection()
        sel.select(0, 0, doc.width(), doc.height(), 255)

    info = InfoObject()
    info.setProperty("color","#f2fc24")

    fill_layer = doc.createFillLayer("SCRIPT FillLayer", "color", info, sel)

    root_layer = doc.rootNode()
    root_layer.addChildNode(fill_layer, None)
    
    doc.refreshProjection()

make_fill_layer()

If you want it below then you might add it above, remove the node below, then add that above. Removing a node doesn’t mean it’s deleted.

And to see what a fill layer properties are:

fc = node.filterConfig()
print(fc.properties())
{'color': '#f2fc24'}

Your idea of using actions ideas can be done, it uses timers. See one of the examples I gave in my first reply, using the @async_runner decorator, each yield statement is an amount of time in milliseconds between things happening. Officially you would use doc.waitforDone() but I’m fairly certain it only works with specific actions. Using scripts in the first place has the problem of no undos, although some actions that are triggered through python scripts can be undone. Bit awkward but not a terrible tradeoff, save often.

Thanks a ton! seems to work fine so far and i think im undertanding better.

i have a small question though, on the selection part of the function, you used an if statement (or if not statement)

    sel = doc.selection()
    if not sel:
        sel = Selection()
        sel.select(0, 0, doc.width(), doc.height(), 255)

here is my code so far

from krita import *

instance = Krita.instance()
doc = instance.activeDocument()
active_layer = doc.activeNode()

def make_fill_layer():

    sel = doc.selection()
   
    sel = Selection()
    sel.select(0, 0, doc.width(), doc.height(), 255)

    info = InfoObject()
    info.setProperty("color","#000000")

    fill_layer = doc.createFillLayer("SCRIPT FillLayer", "color", info, sel)

    root_layer = doc.rootNode()
    root_layer.addChildNode(fill_layer, active_layer)
    
    doc.refreshProjection()
    
make_fill_layer()

i did not add this if statement and it does work as intended. Is there any reason why add that if statement?

Edit, i made some more progress and moved on onto making a function to add a gradient map filter layer i have used @Grum999 ‘s code from a similar post as reference and also i think it is a similar process as adding the fill layer, so i had no issues with this.

def make_gradientMap_layer():
    
    #Using Grum999 code as reference 
    #https://krita-artists.org/t/scripting-with-colour-adjustment-filter-layer/23229/11

    #Set what the filter is
    f = Krita.instance().filter("gradientmap")

    #Select 
    sel = Selection()
    sel.select(0, 0, doc.width(), doc.height(), 255)
    gradMap_layer=doc.createFilterLayer("Test", f, sel)
    doc.rootNode().addChildNode(gradMap_layer, active_layer)
    
    # here need to retrieve filter from layer, using the one (filter 'f' here) that has been created does nothing
    f2=gradMap_layer.filter()
    cfg = f2.configuration()

    #SetCustom ColorMode to "Nearest"
    cfg.setProperty('colorMode', 1)
    
    #SetCustom XML
    cfg.setProperty('gradientXML', 
    
    '<gradient name="GPS Fire Incandescent" type="stop" md5sum="6727dc24540c57dc737186fe9d501ca8">\n <stop alpha="0" offset="0" stoptype="0" bitdepth="U8">\n  <RGB space="sRGB-elle-V2-srgbtrc.icc" b="0" r="0" g="0"/>\n </stop>\n <stop alpha="1" offset="0.227340267459138" stoptype="0" bitdepth="U16">\n  <RGB space="sRGB-elle-V2-g10.icc" b="0.201083391904831" r="0.00033569848164916" g="0.108278021216393"/>\n </stop>\n <stop alpha="1" offset="0.349182763744428" stoptype="0" bitdepth="U16">\n  <RGB space="sRGB-elle-V2-g10.icc" b="0.0881055891513824" r="0.190447852015495" g="4.5777065679431e-05"/>\n </stop>\n <stop alpha="1" offset="0.75631500742942" stoptype="0" bitdepth="U16">\n  <RGB space="sRGB-elle-V2-g10.icc" b="0.170778974890709" r="0.715465009212494" g="0.00532539887353778"/>\n </stop>\n <stop alpha="1" offset="0.832095096582467" stoptype="0" bitdepth="U8">\n  <RGB space="sRGB-elle-V2-srgbtrc.icc" b="0" r="0.729411780834198" g="0.239215686917305"/>\n </stop>\n <stop alpha="1" offset="0.925705794947994" stoptype="0" bitdepth="U8">\n  <RGB space="sRGB-elle-V2-srgbtrc.icc" b="0.0862745121121407" r="0.847058832645416" g="0.549019634723663"/>\n </stop>\n <stop alpha="1" offset="1" stoptype="0" bitdepth="U8">\n  <RGB space="sRGB-elle-V2-srgbtrc.icc" b="0.552941203117371" r="0.905882358551025" g="0.803921580314636"/>\n </stop>\n</gradient>\n'


    )

    doc.refreshProjection()

this does a custom gradient map on top of a selected layer, it works

But testing things i realized this only works on non-grouped layers, when the “active_layer” is inside a group the code runs but it does nothing, i noticed this was the case with the fill layer function too.

My question is, why does this happen? and also if this is intended, would it be better to make the layers first and then group them?

doc.selection() returns None if you have not made any selection, it’s not necessary just an alternative. You can use if x not None, if x is not None, and if x != None, we are just confirming the variable in question is not None because it probably usually means the values we passed to some function (that returns an object) was not valid in some way or another.

If you don’t have a selection uses a new one so the fill layer appear in a small square. You can make any number of new selections, they do not effect the selection in the current document (if you previously made a selection, or not).

If doc.activeNode() is inside a group that is not the root, you can get the nodes parent node with Node.parentNode()then do parent_node.addChildNode. I made the mistake a bunch of times of trying Node.parent(), I believe this returns an internal object which is not supported.

You can create a whole layer hierarchy without adding them as child of a node that you can see in the layers docker.

Here is my little help script for adding nodes to document.

  1. create Krita document and add layers / filters / groups to document, then save document.
  2. close document
  3. create new document / open some other document.
  4. select node in document.
  5. run script with path to saved document.
    (if selected node was group, new nodes are added inside of that group, else nodes will be added above selected node)
from krita import Krita


def gather_node_tree(parent_node):
    """
    recursive gathering of nodes to structure.
    result = [
        {'node': <Node>, 'children': [
            {'node': <Node>, 'children': [
                {'node': <Node>, 'children': [...]},
                ...
            ]},
            ...
        ]},
        ...
    ]
    """
    result = list()
    for child_node in parent_node.childNodes():
        result.append({'node': child_node, 'children': gather_node_tree(child_node)})
        child_node.remove()  # free child node from old parent
    return result


def add_node_tree(children_list, dst_parent, above):
    """
    recursively add tree under given parent.
    """
    for child_dct in children_list:
        child_node = child_dct['node']
        dst_parent.addChildNode(child_node, above)
        add_node_tree(child_dct['children'], child_node, None)
        above = child_node


def import_template(template_path, dst_parent):
    """
    if dst_parent is group layer, add nodes from template inside of it,
    else if it's not, then add nodes from template above it

    get Krita application and open template document
    gather node tree from template document
    add node tree into, or above given parent (depending on parent type)
    finally close template document
    """
    above = None
    if dst_parent.type() != 'grouplayer':
        above = dst_parent
        dst_parent = dst_parent.parentNode()

    app = Krita.instance()
    template_document = app.openDocument(template_path)

    node_tree = gather_node_tree(template_document.rootNode())
    add_node_tree(node_tree, dst_parent, above)

    template_document.close()


# testing
app = Krita.instance()
doc = app.activeDocument()
node = doc.activeNode()
import_template('/path/to/some/krita_file.kra', node)

ps: Sorry about the non-beginner friendly recursive functions, tree structures kinda repeat themselves.

/AkiR

1 Like

i am not too sure if this is what im trying to do? im not trying to add nodes/layers into a new/saved document, i am only trying to add layers to the current document opened. Basically an action sequencer

thanks anyways, but i am not really understanding well what your code does exactly, i am still a beginner so i would appreciate an explanation on what this code does

Just for testing it it adds all nodes from krita document from given path to current document at above / into currently selected node.

But you can use script as a part of action sequencer and call import_template(template_path, dst_parent) from action sequencer, that way you don’t have to hard code every setting of every layer that you are adding.

In Your script you just first import stuff from template, and then find some spesific layers that where imported (ie. based on layer name) and adjust small set of layer properties in code, rest of the properties can use values defined in template.

Edit: more explanations…

def gather_node_tree(parent_node) -> list:

is used to gather all nodes under given parent node (if document.rootNode() is given all nodes of document will be gathered) Note: function is recursive, so it is a bit hard to understand, inception movie may help to understand calling a function inside of a function, just like dream inside of dream.

def add_node_tree(children_list, dst_parent, above) -> None:

is used to add given node tree under given parent, this is also recursive, so that grand-grand-… children of children_list will be added.

def import_template(template_path, dst_parent) -> None:

this is the primary function to call, function takes template_path to krita file all it’s nodes will be added to given dst_parent. First document from given path is opened to Krita, next gather_node_tree() is called to get tree of nodes from opened document. After tree is gathered all nodes in tree are added to destination parent dst_parent, finally opened template document is closed (template document is NOT saved as all it’s nodes where removed and moved to new document.)

/AkiR

Great script, only thing I would add is options for clearing the content of each layer and having them go in a named group to keep it tidy. This reminds me Krita doesnt have an import button… im so used to opening documents, copying the nodes and pasting them into another document. Would be a good addition to the File menu right where you would expect it to be. Or even more intuitive in the context menu that appears when you drag a file in, “Insert as New Layer” only imports toplevel nodes :sleepy_face:

I have given it a try, and it works well for this specific usecase. So I thank you for that, I think it is a smarter way to do things for this case.

I am still gonna try to tinker with it because as a begginer I do struggle to understand it, and as I said I do want to learn from this, not only get the script done.

And while I can see myself using this script of yours for my regular workflow, I am still gonna try to do the hard-coded approach for learning and practicing.

1 Like

If I’m understanding right, you mean something like the link/append options in blender? If so, I can totally see the potential of adding that into Krita! Would be really cool to be able to select layers in another file to import them into another file, and maybe choose if we import it into the new file as regular layers or file layers

:kiki_shocked:

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.