Data Structures and Functions

In this record we will learn more about functions are and how to make them. Also we will touch on basic data structure - list and how to iterate over them.

In this record we will learn more about functions are and how to make them. Also we will touch on basic data structure – list and how to iterate over them.

Before reading this record make sure to learn the materials of the previous record.

Syntax

Every programming language has its own set of rules regarding structuring the source code of the program and how to use its operators and command. Some of the rules are so loose that teams are forced to come up with the code style. In case of the Python things a bit easier, however, one should pay attention where the symbols go. The way the code has to be written is guided by the code standard and the rules of writing the code is called syntax.

One of the most probable pitfall that you might encounter is indentation. In Python language, the number of spaces before the first symbol will determine the association of this line with either a function or if statement.

Notice that when we compare the variable color to a string we create a spacing after the line with condition. This way we tell the interpreter, if the condition is true, you should print the line number 3.

color = "green"
if color == "red":
    print("The color is red")
else:
    print("The color is green"

If indentation will not match the interpreter will throw an error.

By default the number of spaces is 4 and most of the code editor replace a Tab key with the spaces. You should keep that in mind when you start writing more complicated scripts that we were writing before.

Control Statements

We already know that variable are always represent a type. 5 will be an integer and “green” will be a string.

In python we have another data type that is called bool. You can interpret this as a light switch. It can be either turned on (boolean value True) or turned off (boolean value False). This type is useful for comparing things. The result of these comparisons will be a boolean type, that we can assign to a variable or use directly with control statement. To compare two numbers with each other we can use comparison symbols: > (greater than), < (less than), >= (greater than or equal), <= (less than or equal), == (equal), != (not equal).

In most cases the comparison equal and not equal will be applicable for other than integer types. The objects can also be compared, however, we will not touch on this topic right now.

When we want to control of the flow of the operations in our script, we need something that will let us choose what part of the script to execute depending on the condition. Let’s say you want the selection expanded if the current color hue is larger than 130 otherwise you want the selection to be shrunk. This kind of building block is called control statement. The if statement is most widely used and it is what we often use in our script as well.

The construction of the if statement is the following:

# NOTE: pseudo code below
if condition-is-true:
    # execute this line if condition is true
elif another-condition-is-true:
    # execute this line if another condition is true
else:
    # execute this line if none of the above is true

Instead of “condition-is-true” we can put the condition we want hue > 130 and decide what to do in this situation.

hue = 120
if hue > 130:
    print("Expand selection.")
else:
    print("Shrink Selection")

Here we are using print for illustration. How to manipulate selections will be discussed in the further records.

Notice that we use the “hue > 130” directly in if statement. However we can write the result of condition into a boolean variable hue_large_enough and then use it in if statement.

hue = 120
hue_large_enough = hue > 130
if hue_large_enough:
    print("Expand selection.")
else:
    print("Shrink Selection")

Usually the condition is written inline with if statement to safe the time and variable is used only in cases if it is needed in other places of the script or if it is passed as an input argument to a function.

There are other control statements like for/while loops which we will discuss in the further records.

You are probably wondering what this explanation is for? And when we actually start doing Krita Scripting. It is easy on our side to throw at you a bunch of functions and classes that Krita Python API supports. Nevertheless, in this case you will only know what we show you. The goal of this series to give you understanding how these things operate. Then you will be able to figure out on your own where to get the operation you need use it and get the results. And if there is no such operation that you are looking for, you will be able to make it yourself.

Functions

We have already touched the notion of function in the first record. Now it is time dig deeper.

When we mention the function in the script for the first time it is called definition.

Let’s say you need to convert RGB pixel value to grayscale. And let’s imagine you will need to do this operation in multiple placed in the script. This one of the most frequent usages of the function. They are a good way to keep your repetitive operations in one place and nice helpers to organize the code in general.

According to Wikipedia, the formula to convert the RGB value into grayscale is as following:

Y = 0.2126 * R + 0.7152 * G + 0.0722 * B

Let’s make a function that will convert the RGB value into grayscale

def to_gray(r, g, b):
    print("Converting to grayscale.")
    gr = 0.2126 * r
    gg = 0.7152 * g
    gb = 0.0722 * b
    return gr + gg + gb

# Cadmuim Orage Hue from the Prismacolor set
r = 244
g = 122
b = 92

print("Start converting.")
gray = to_gray(r, g, b)
print(gray)

Run this code with the scripter. You should get the value close to 145.7712.

Notice that we need to first define the function, before we use it. The same way as with variables. Here we start to see that the order of definition of the code does not imply the order of execution.

Defining function to_gray the interpreter will walk through the function’s code and store it in memory, but will not execute it. Notice, when you run the script you get the message “Start converting.” first, then the message “Converting to grayscale.”.

The scope of the object

Since we already familiar with idea of indentation and functions we now ready to learn new aspect of programming – a scope. The scope of the object (variable, function, class) is basically the idea of accessibility of the code depending of its definition place. In other words, depending on the place where the object is defined it will impact where in the code the object will be available or not.

The objects defined outside any function or class will be in global scope. The object inside the function will be in function’s local scope. That means the object defined in the function will not be available outside the function. So to get the value from the function, we return that value.

To see this idea in action, try to print one of the values gr, gg, gb outside the to_gray function and see what happens.

Organizing the code with functions

Writing the code directly in the script’s global scope is not always a good idea. Generally, organizing everything into functions makes the code more clearly readable and allows eventually separate the script into small reusable components.

That is why from now on we will follow the following template:

def my_super_useful_functoin():
    pass

def my_other_function(some_arg):
    pass

def run():
    my_super_useful_functoin()
    print("The results")

run()

By changing previous example a little bit we are getting the following script

def to_gray(r, g, b):
    print("Converting to grayscale.")
    gr = 0.2126 * r
    gg = 0.7152 * g
    gb = 0.0722 * b
    return gr + gg + gb

def run():
    # Cadmuim Orage Hue from the Prismacolor set
    r = 244
    g = 122
    b = 92

    print("Start converting.")
    gray = to_gray(r, g, b)
    print(gray)

run()

Most of the programs have the entry point into the program. In out case it will be the run function.

List

In the previous record we showed how to make an instance of action and trigger it. To get the action we used a string with the name. However, you might wondering, how do we know what names can we pass to the action method?

The answer is we can ask Krita what actions are available for us. The application instance has a method actions, we can use it go get the list of the actions like this:

from pprint import pprint
from krita import Krita

def run():
    app: Krita = Krita.instance()
    actions = app.actions()

    pprint(actions)

run()

Since the actions will be a list, a better print function for displaying more human readable format will be pprint. That is right, the print with double p. Once you run the script you should the something like this (but much more longer).

[<PyQt5.QtWidgets.QWidgetAction object at 0x000001C6D56B6EF0>,
 <PyQt5.QtWidgets.QWidgetAction object at 0x000001C6D56B6F80>,
 <PyQt5.QtWidgets.QAction object at 0x000001C6D5703BE0>,
 <PyQt5.QtWidgets.QAction object at 0x000001C6D5703C70>,
 <PyQt5.QtWidgets.QAction object at 0x000001C6D5703D00>,
 <PyQt5.QtWidgets.QAction object at 0x000001C6D5703F40>,
 <PyQt5.QtWidgets.QWidgetAction object at 0x000001C6D570AE60>,
 <PyQt5.QtWidgets.QWidgetAction object at 0x000001C6D570BE20>,]

The lists

You should notice a square brackets around the weird looking lines (separated by commas). This is the Python’s way of telling that the set of object is contained in a list.

So far, we know what variable is. However, what if we have a set of variables that we want to always keep together. For this purposes we can use a data structure that is called list.

The list is essentially (as the name suggests) is a collection of objects that is grouped together. Here the example

color = [244, 122, 92]
print(color)

print(color[0])

color[0] = 120

print(color)

We define a list by writing the objects by commas wrapping whole thing in a square brackets (line 1). Here, we define the same color as a single object and now if we want to get the value for the red channel we need to refer to the index of the item that represent this channel.

Enumeration of items in the list is starting from 0 and goes all the way to the length of the list minus one. Here by using number 0 we refer to the first element, using number 1 to the second and so on. The number we use as an “address” in the list is called index. To read the value by index we use the square brackets after the list name and put the index into the square brackets (line 4). If we want to change the value by index we place this construction to the left from the equal sign and on the right sign of the equal sign we specify the value we want to assign to the list item (line 6).

Run this script and study the output. Notice how the first number in the list changed.

The lists can be provided by the object from the Krita framework like with actions() method and many other places.

Nevertheless, the square brackets is not the only way to define the list. It turns out that list is also an object that has its own methods.

grocery = list()

grocery.append("Banana")
grocery.append("Cabbage")
grocery.append("Cheese")
grocery.append(3)

print(grocery)

We can define a new list by using list type and round brackets. Then using list’s method append add items to the list. Notice that we can add all kind of things into the list and not to worry about having this things similar (we talk about being the same type), to illustrate this, we add a random number 3 here.

Iterating over the container

The actions variable will contain a list of actions. To get the names of actions we need to go over each action and figure out its name. The process of going over the items is called iterating. And to do this we will use the for loop. The syntax of the for loop is the following:

for item in iterable:
  print(item)

We start with the for keyword, then we define what reference we will use for each of the item in the list. Notice that each time we go over new item the item variable is replaced. Then after using the keyword in we tell the container we want to iterate, in our case it is iterable.

Let’s get back to our actions list. By using the knowledge we obtained above we can now get the list of actions like this

from pprint import pprint
from krita import Krita

def run():
    app: Krita = Krita.instance()
    actions = app.actions()

    for action in actions:
        print(action)

run()

No we have a list of objects and no square brackets.

Action Object

You might have expected that once we get actions list from app.actions() you will get the list of names that you can use as reference. But instead, what we get is objects of unknown nature:

<PyQt5.QtWidgets.QWidgetAction object at 0x000001FC038491B0>
<PyQt5.QtWidgets.QAction object at 0x000001FC03849240>

This is because under the hood Krita keeps the action objects and when you call the method app.action() you requesting single item from the actions list. Then when you call the method trigger(), you activate that object.

Try run this code and see what you will get for the output.

from krita import Krita

app : Krita = Krita.instance()

deselect = app.action("deselect")
deselect.trigger()

print(type(deselect))

Note that we use the new function called type. This is another useful built-in function of the Python language that can tell what kind of the object the variable represent. Krita Python API is still in its early development, and a lot of the aspects of the scripting is learned by getting the object and then inspecting it in the script. This function is useful for this kind of job.

Now, we need to figure out what these QAction and QWidgetAction are. This is a prefect occasion to teach you how to dig into APIs.

This particular job is not that pleasant but crucial to accomplish your goals in making the scripts you want and not what already written out there.

Try google the word “QtWidgets.QAction”. Very likely that you will stumble upon two major resources. One resource will lead you to the description of QAction class on the PySide framework and another will lead you to description of the QAtions on the C++ original QT implementation. If you go over all the methods that object has you should find the method text(). This is what we are looking for.

Either of the resources are good source of learning about these objects. PySide might be because it has Python description. The original documentation is much more vast and can give better insight, however much more complicated to the beginners. Keep this resource in mind and try use it whenever possible.

Krita relies on Qt heavily. That is why learning the Qt itself will prove useful, especially when we start writing our custom Dockers.

Let’s try to modify our script. Instead of printing the action right away, what if we use the text() method.

from pprint import pprint
from krita import Krita

def run():
    app: Krita = Krita.instance()
    actions = app.actions()

    for action in actions:
        print(action.text())

run()

The output should look like this

"""
...
Paste Shape Style
Copy &merged
Select &All
&Deselect
C&lear
&Reselect
&Invert Selection
...
"""

We can now see the names of the actions. However, it does not look like something we passed to app.action() method. Ignore triple quotes for now. They are used to describe multiline comment and used here for ease of reading the output.

This is something what is not easy to learn from the documentation of the object. The QAction inherits QObject class and this class has the method objectName(), by using it we will be able to get the list of actions that can be used with app.action().

The inheritance is the concept from Object Oriented Programming which will learn in later records.

from pprint import pprint
from krita import Krita

def run():
    app: Krita = Krita.instance()
    actions = app.actions()

    for action in actions:
        print(action.objectName())

run()

Now we have valid names

"""
...
paste_shape_style
copy_merged
select_all
deselect
clear
reselect
invert_selection
...
"""

Try to inspect the list of actions yourself and trigger some of them to learn what they do.

As a final touch, let’s make our listing of actions more human readable:

from pprint import pprint
from krita import Krita

def run():
    app: Krita = Krita.instance()
    actions = app.actions()

    for action in actions:
        print(action.text(), " -> ", action.objectName())

run()

Alternative source of actions

The advantage of the script above is it will list of the actions registered in the application. That means all the actions including the ones that were created by plugins or even the actions that you add yourself (that is right, you can add your own actions, and we will learn how to do it).

In the second record we mentioned Krita Scripting School. This resource is great place to search for system actions. You can quickly find the action you need and copy paste it into your script.

Final Words

If you get lost in the middle of the text, it is alright. It is common in the programming world to take the material in several attempts. Give it a rest and start over.

Learn more about Qt Objects

PiSide Qt Wrapping – good for starter.

Qt Widgetst C++ Classes – more advanced recourse to learn the exact API.

Next

Now it is time to dig deeper and learn about Classes and Objects.

Feedback and Suggestions

Should you have any questions or maybe you want to suggest next topic to cover. Please join our community and let us know.