I’ve started to learn scripting in Krita recently and I’m working on a plugin.
Little explanation about the plugin for the context : it will be use to manage a reference images library and load one image randomly when I click on a custom button.
This plugin needs to manipulate reference images and I’m struggling with how to do it properly because I’m not finding any documentations on reference images.
At this point of development, what I want to do is : When I click on a button in a custom docker, an image is load as a reference from a folder on my computer and is moved aside the canvas at a specific position.
I made a first version of the script which is partially doing what I want, here is the code below (my version of Krita is 5.0.6) :
from krita import *
from PyQt5.QtWidgets import *
class MyExtension(DockWidget):
def __init__(self):
super().__init__()
self.setWindowTitle('My Extension')
mainWidget = QWidget(self)
self.setWidget(mainWidget)
# Create the button for adding reference
ref_button = QPushButton("Add reference", mainWidget)
ref_button.clicked.connect(self.openReference)
# Add the button on the docker
mainWidget.setLayout(QVBoxLayout())
mainWidget.layout().addWidget(ref_button)
def openReference(self):
ref_file_name = 'C:/path/to/reference.png'
# The starting point of the script is a new blank file, with a default background layer
doc = Krita.instance().activeDocument()
current_layer = doc.activeNode()
# Step 1 : I create a new FileLayer named "ref_image" which reference an image
new_layer = doc.createFileLayer('ref_image',ref_file_name,"None")
current_layer.parentNode().addChildNode(new_layer, current_layer)
# Step 2 : I make the newly created FileLayer active (for steps 3 & 4)
ref_layer = doc.nodeByName("ref_image")
doc.setActiveNode(ref_layer)
# Step 3 : I convert the FileLayer into a paint layer (there is no way to create directly a paint layer ? Can't find it in the documentation)
Krita.instance().action('convert_to_paint_layer').trigger()
# Step 4 : I select all the ref_layer to copy it and paste it as a reference image (it's the only way I found to create a reference)
selection = Selection()
selection.selectAll(ref_layer, 255)
selection.copy(ref_layer)
Krita.instance().action('paste_as_reference').trigger()
# Step 5 (missing) : move the newly created pasted reference aside. I don't know how to interact with it.
Krita.instance().addDockWidgetFactory(DockWidgetFactory("myExtension", DockWidgetFactoryBase.DockRight, MyExtension))
Unfortunately, the script has several problems :
I create a FileLayer and convert it into a paint layer, is there a simpliest way ? Like create directly a paint layer and load an image in it ?
When I execute in scripter step 1 THEN step 2 THEN step 3 and finally step 4, it works BUT when I do every steps in once it does not work. It seems to stop at every step.
For step 5 : I don’t find a way to manipulate directly reference images, is there a class that allows to ? Like adding it, move, delete, scale,…
I tried to explain my problem clearly and I apologize if it’s not, plus english is not my native language so sorry about mistakes (my True native language is of course Python ).
Hey, you keep using the term reference image (and it probably makes sense in the context of your project) but reference images are a distinct thing. You probably have trouble finding documentation because you keep searching for “reference image” but Krita has a specialized tool called the Reference Image Tool while you just use standard layers.
Those reference images are not part of the layer stack, they live outside and are not bound to the canvas but the viewport. When you look for normal layer operations instead (because that’s what you’re using currently in your script), perhaps you find better documentation that actually helps you.
It’s probably best to clarify what your actual goal is, so perhaps when you actually want to make it work like the actual reference images created by the designated tool, we have to follow a completely different approach. However I’m not sure that real reference images (the way Krita defines them) are currently accessible from Python. Often the Python API lags a bit behind the Krita features.
Yes I’m talking about the Reference Images Tool which is not part of the layer stack, that is what I try to use.
I would like to create it directly with a script but I don’t find a way to do it in the API, that’s why I use this tricky way (I’m aware that it’s probably not the best).
The only manipulation of reference images I found in the API is the action “paste_as_reference”, that’s why I first create a standard layer and then copy and paste it as reference.
If you know a correct (and simpliest) method to create directly real reference images, could you please share it ?
Okay, that means your current approach is just a workaround and you very much want it to use the reference tool. I’m currently not aware of how to do it or if it’s possible, I could not find anything regarding Reference Tool in the API documentation, just like you.
You can load up a QImage, modify resize it how you want and then setPixelData. Not sure if it is “simpler” though but that is how you load an image into a paint layer
Likely you are dealing with a race condition, you will either have to do QTimer.singleShot or monitor model changes of the listLayers or undo docker via pyqt to know
Nope, at best you may be able to edit the kra file then reload the document, or simulate mouse/key events. Unfortunately nobody has contributed to the api to modify it directly. The API is always open to contribution
The UI framework used by krita is QT, the python version is pyqt. The undo docker is a krita docker, the above example uses the layer docker to monitor changes, you can do the same with the undo docker
A couple of these steps were broken recently by Krita. I think if your on stable you can’t do these type of thing. I have a script that does a similar thing and it is broken on the same step. I am waiting it to be fixed again.
Thanks everyone for your help, I found a way to deal with this problem although it seems very inconsistent.
I used QTimer.singleShot (suggested by KnowZero), it works…most of the time… (very random, sometime it works, sometime it half-works when I close and open Krita again, and I don’t really know why).
I’m giving you the new version of my script in case it helps anyone or you have any suggestions :
from krita import *
from PyQt5.QtWidgets import *
class MyExtension(DockWidget):
doc = None
def __init__(self):
super().__init__()
self.setWindowTitle('My Extension')
mainWidget = QWidget(self)
self.setWidget(mainWidget)
# Create the button for adding reference
ref_button = QPushButton("Add reference", mainWidget)
ref_button.clicked.connect(self.openReference)
# Add the button on the docker
mainWidget.setLayout(QVBoxLayout())
mainWidget.layout().addWidget(ref_button)
def convertToPaint(self):
refLayer = self.doc.nodeByName("ref_image")
self.doc.setActiveNode(refLayer)
Krita.instance().action('convert_to_paint_layer').trigger()
def convertToReference(self):
refLayer = self.doc.nodeByName("ref_image")
selection = Selection()
selection.selectAll(refLayer, 255)
selection.copy(refLayer)
Krita.instance().action('paste_as_reference').trigger()
def openReference(self):
ref_file_name = 'C:/path/to/reference.png'
# The starting point of the script is a new blank file, with a default background layer
self.doc = Krita.instance().activeDocument()
# Step 1 : I create a new FileLayer named "ref_image" which reference an image
new_layer = self.doc.createFileLayer('ref_image',ref_file_name,"None")
self.doc.rootNode().addChildNode(new_layer, None)
# Steps 2 & 3 : I make the newly created FileLayer active and convert it to paint layer
QTimer.singleShot(200, self.convertToPaint)
# Step 4 : I select all the ref_layer to copy it and paste it as a reference image (it's the only way I found to create a reference)
QTimer.singleShot(200, self.convertToReference)
# Step 5 (missing) : move the newly created pasted reference outside the canvas.
# At this point, I'll move it manually, it looks like there is no way to do it with scripting.
Krita.instance().addDockWidgetFactory(DockWidgetFactory("myExtension", DockWidgetFactoryBase.DockRight, MyExtension))
It’s far from ideal but it’s enough right now for my needs. I’m open to comments and suggestions, and I will gladly give you updates when I make progress on the script.
singleShot is non-blocking. So you shouldn’t really call one after the other, you have to call one inside the other but even that isn’t 100% guaranteed. If you want guarantees, you have to monitor the undo docker or at least the undo menu item(though undo docker is more reliable). In your case though you can also monitor the layerlist for changes
I would like to work with the undo docker but I don’t know how to find informations about this specific docker.
I tried to adapt the example you gave me earlier, I found the name of the undo docker, which is “History”, but I can’t find what kind of childs I have to monitor to know if there is a modification :
history = undoBox.findChild(QtWidgets.QTreeView,"listLayers")
is wrong.
I looked for informations in the QDockWidget class documentation (Qt) and in the DockWidget class documentation (Krita) but didn’t find anything to help me.
Here is a small example of tracking changes in history. Note if Krita has multiple Main Windows, this script needs to be edited (all windows have separate history dockers that all needs to be tracked)
from krita import Krita
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import QListView, QPlainTextEdit
def find_history_and_view():
app = Krita.instance()
history_docker = next((d for d in app.activeWindow().dockers() if d.objectName() == 'History'), None)
for view in history_docker.findChildren(QListView):
if view.metaObject().className() == 'KisUndoView':
return view
class MyHistory(QPlainTextEdit):
_instance = None # just protect instance from (G)arbage (C)ollector in scripter
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._previous_row = -1
self._previous_document = None
self.setFont(QFont('DejaVu Sans Mono')) # make it pretty!
self._connect_signals()
def _connect_signals(self):
# Note:
# if there are multiple Windows in Krita this needs to be extended!
# all history dockers views must be tracked for currentChanged signal!
#
view = find_history_and_view()
if not view:
raise RuntimeError('Nothing can be done! (history view not available?)')
self._history_view = view
s_model = self._history_view.selectionModel()
s_model.currentChanged.connect(self._on_history_view_current_changed)
def _on_history_view_current_changed(self, current, previous_is_broken):
# Bug in Krita:
# Krita double emits current changed signals,
# also previous is broken | ignored
if not current.isValid():
# Bug in Krita:
# if user changes current from Undo History View,
# last double emit will be invalid index!
return
document = Krita.instance().activeDocument()
document_name = '<no document>'
document_file_name = '<no document>'
if document:
document_name = document.name()
document_file_name = document.fileName()
current_row = current.row()
if (document != self._previous_document) or (current_row != self._previous_row):
# ignore second double emit.
self.appendPlainText(
f'current document name = {document_name!r}, '
f'current document file name = {document_file_name!r}, '
f'current text = {current.data(Qt.DisplayRole)}, '
f'current row = {current_row}')
self._previous_row = current_row
self._previous_document = document
def sizeHint(self):
return QSize(1000, 300)
my_history = MyHistory()
MyHistory._instance = my_history
my_history.show()