About krita
krita is a skill for running Python scripts inside a live Krita session, aimed at creators who automate image construction, checking, and export. It uses libkis and PyQt5 to work with documents and verifies results through canvas projections and script output.
Key features
- 01Run scripts in a live Krita session
- 02Create and edit documents and layers
- 03Draw pixels, vectors, and text
- 04Apply filters, selections, and masks
- 05Check canvas projections and export files
Use cases
- 01Build and export a poster automatically
- 02Batch-export layers or files
- 03Process animation frames into video
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ jablonkai/ skills --skill kritaAfter installing, type/kritain Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/api-reference.mdrecipes.mdscripts/example-poster.pykrita_bridge.desktopkrita-install-bridge.shkrita-send.shscripts/krita_bridge/__init__.pykrita_bridge.pyDetails
From the project READMEKrita Control
Krita (/Applications/krita.app) embeds Python 3 and exposes its whole document model
through the libkis API (from krita import *), with PyQt5 available for pixel work.
Drive it through the Krita Bridge — a pykrita plugin running inside a live Krita that
executes whatever script is POSTed to 127.0.0.1:8737. Because the script runs in the
running session on the GUI thread, the canvas updates as you build, the user watches it
happen, and the main-thread-only parts of the API are safe. Every call below was run
against Krita 5.3.4 (Python 3.13, PyQt5 5.15.11); the skill was first written on 5.3.3.
- scripts/krita-install-bridge.sh — install the bridge
plugin into the user's resource folder and enable it in
kritarc(one-time). - scripts/krita_bridge/ — the plugin itself; also pasteable into Tools ▸ Scripts ▸ Scripter for a one-off session.
- scripts/krita-send.sh — send a
.pyfile (or-c 'inline code') and print its captured output;--pingchecks the bridge is up. - scripts/example-poster.py — a complete build → verify → export script to copy from.
- references/api-reference.md — the version-checked libkis API (Krita, Document, Node, Selection, filters, animation, export). Read it before writing anything past the cheatsheet below.
- references/recipes.md — batch export, layer extraction, animation frames → video, why not to start a second Krita, and what the API cannot do.
The control loop
-
Make sure the bridge is up:
bash scripts/krita-send.sh --ping→{"ok": true, "bridge": "krita", "version": "5.3.4 (git e7e52a7)", "documents": [...]}. If nothing answers, either Krita isn't running (ask the user to start it — the bridge auto-starts with it once installed) or the plugin isn't installed yet:bash scripts/krita-install-bridge.sh # Krita must be quit: it rewrites kritarc on exitThe script copies the plugin into the pykrita folder and writes
enable_krita_bridge=trueintokritarc's[python]group — the same thing the Settings ▸ Configure Krita ▸ Python Plugin Manager checkbox does. Editing another app's config counts as a user-level change and some setups will refuse to let you run it; hand the user the one-liner instead of working around the refusal. For a session where nothing may be installed at all, they can paste krita_bridge.py into Tools ▸ Scripts ▸ Scripter and press Ctrl+R — same bridge, gone at quit. -
Write a build script to the scratchpad and send it:
OUT=/path/to/outdir bash scripts/krita-send.sh /path/build.py.KritaandOUTare pre-injected into the script's namespace (OUTalso asos.environ["OUT"]), andKRITA_SEND_TIMEOUT=600(seconds) covers slow exports. -
Read the feedback. The bridge returns the script's captured stdout/stderr, and on failure the Python traceback — so
print(...)is your channel back. Nothing else reports: Krita's own log window stays inside the app. -
Look at the result.
doc.projection(0, 0, w, h)hands back aQImageof the merged canvas — save it and Read the PNG. That is cheaper and more honest than a screenshot, because it is exactly what an export would produce. -
Iterate, then deliver: fix the script, re-send. Scripts must be re-runnable — create a fresh document per version rather than mutating the one already open, so a re-send can't stack layers onto the previous attempt.
Everything goes through the live session. Don't launch a second Krita from the shell —
no krita --export, no --export-sequence, no headless instance in the background. Two
Krita processes fight over the same resource database and kritarc, and the one the user
is working in loses; a batch job that pops up its own app also steals focus and hides its
failures from both of you. Plain format conversion is a bridge script too — open, export,
close, in the session that is already running (see
references/recipes.md).
Scripting cheatsheet
Krita.instance() is the application; everything hangs off it. Sizes are pixels, colours
depend on the document's colour space, and doc.setBatchmode(True) is what stops export
and save calls from popping dialogs at the user.
Document and layer stack
from krita import Krita, InfoObject
app = Krita.instance()
doc = app.createDocument(1200, 1600, "poster_v1", "RGBA", "U8", "", 300.0)
app.activeWindow().addView(doc) # show it — the user sees the build happen
doc.setBatchmode(True) # no dialogs on export/save
layer = doc.createNode("background", "paintlayer") # grouplayer, filterlayer,
doc.rootNode().addChildNode(layer, None) # filllayer, clonelayer, …
layer.setBlendingMode("multiply") # Krita's blend-mode ids, not CSS names
layer.setOpacity(180) # 0–255, not a percentage
A new document already contains one paint layer, filled opaque white and named in
the user's language (Háttér on a Hungarian Krita) — take it as
doc.rootNode().childNodes()[0], never by name, and hide or fill it when the result needs
transparency. Work on the open document instead with doc = app.activeDocument(), or open
a file with doc = app.openDocument(path) (add a view to make it visible). Close batch
documents when done — every open document keeps its whole image in memory — but read
Closing documents first: a careless close() hangs or crashes.
Brush strokes with the real paint engine
Node.paint* runs the current brush preset through the same paint ops as a tablet stroke,
so textured, watercolour and pattern presets behave as they do by hand:
from krita import ManagedColor
from PyQt5.QtCore import QPoint, QRectF
from PyQt5.QtGui import QColor
view = app.activeWindow().addView(doc) # paint* needs a view
presets = app.resources("preset")
view.setCurrentBrushPreset(presets["b) Basic-5 Size"]) # names include the "b) " prefix
view.setBrushSize(40); view.setPaintingOpacity(0.9)
view.setForeGroundColor(ManagedColor.fromQColor(QColor("#ff6b35")))
print(layer.paintAbility()) # expect "PAINT" before a long stroke loop
layer.paintLine(QPoint(120, 200), QPoint(900, 640), 1.0, 0.3, "ForegroundColor")
layer.paintEllipse(QRectF(300, 300, 400, 400), "ForegroundColor", "None")
paintLine takes QPoint — a QPointF raises TypeError, even though the rect-based
calls want QRectF. Add the view before touching paintAbility() or any paint* call:
on a document that has no view they don't fail politely, they crash Krita (5.3.4
dereferences the missing view). Everything inherits the user's active preset, colour and
blending mode, so set what matters explicitly. For geometry that must look identical every run, draw it with
QPainter instead:
Pixels: paint with QPainter, push once
Deterministic imagery is drawn into a QImage and pushed into a paint layer in one go.
Krita's RGBA8 space expects BGRA bytes, which is exactly the in-memory layout of
QImage.Format_ARGB32 on little-endian machines, so no channel swapping is needed:
from PyQt5.QtCore import QByteArray, QPointF, Qt
from PyQt5.QtGui import QColor, QImage, QPainter
img = QImage(doc.width(), doc.height(), QImage.Format_ARGB32)
img.fill(Qt.transparent)
p = QPainter(img)
p.setRenderHint(QPainter.Antialiasing)
p.setBrush(QColor("#ff6b35")); p.setPen(Qt.NoPen)
p.drawEllipse(QPointF(600, 500), 260, 260)
p.end()
ptr = img.constBits(); ptr.setsize(img.byteCount())
layer.setPixelData(QByteArray(ptr.asstring()), 0, 0, img.width(), img.height())
node.pixelData(x, y, w, h) reads the same buffer back, so an existing layer can be
round-tripped through QImage for analysis or retouching.
Vector and text
Text is vector work in Krita, and the reliable way in is SVG:
vec = doc.createVectorLayer("headline")
doc.rootNode().addChildNode(vec, None)
vec.addShapesFromSvg('<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1600">'
'<text x="90" y="1320" fill="#f5f5f5" font-family="Helvetica" '
'font-size="150">KRITA</text></svg>')
Filters, selections, masks
f = app.filter("blur") # bind it: a chained temporary takes its
cfg = f.configuration() # InfoObject down with it (see below)
cfg.setProperty("halfWidth", 40); cfg.setProperty("halfHeight", 40)
f.setConfiguration(cfg)
f.apply(layer, 0, 0, doc.width(), doc.height()) # destructive, on that node
from krita import Selection
sel = Selection(); sel.select(100, 100, 400, 400, 255) # x, y, w, h, selectedness
doc.setSelection(sel) # filters and edits then respect it
Filter ids are literal strings from app.filters() and several contain spaces
("gaussian blur"); a wrong id returns None rather than raising. Property names differ
per filter — f.configuration().properties() prints the defaults, which is the fastest way
to learn one. A non-destructive alternative is a filter mask or filter layer
(doc.createFilterMask / doc.createFilterLayer) — see the API reference.
Refresh before you look
Krita composites asynchronously. Anything that reads or writes the merged image —
projection(), thumbnail(), exportImage(), save() — needs the projection to be
current first:
doc.refreshProjection()
doc.waitForDone()
Export and save
doc.projection(0, 0, doc.width(), doc.height()).save(OUT + "/check.png") # for you to Read
doc.exportImage(OUT + "/poster.png", InfoObject()) # PNG/JPEG/TIFF/WEBP by extension
doc.saveAs(OUT + "/poster.kra") # re-editable, keeps the layer stack
doc.waitForDone()
Animation, per-layer export, and batch folder processing are in references/recipes.md.
Closing documents
doc.setModified(False) # or save first — then close() can't ask anything
doc.close()
setBatchmode(True) does not cover closing: a modified document that has a view pops a
modal "save changes?" box, the send blocks until KRITA_SEND_TIMEOUT, and Krita keeps
waiting for a click. And never close an animated document in the send that created its
keyframes — Krita crashes a moment later in the Layers docker, even after
setModified(False), a drained event loop, or file_close. Leave it open for the user, or
close it from a later send.
Verification
- Returned stdout is the immediate signal —
print(...)comes straight back through the sender; a failure returns the traceback and exits non-zero. - A projection PNG is the visual check: save
doc.projection(...)(scaled down is fine) into$OUTand Read it. Do this at the end of every build script — it is one line and it catches the errors that no assertion will. - Numbers: dump size, resolution, and the layer list (name, type, bounds, blend mode,
opacity) as
metrics.jsonand Read it, rather than eyeballing the stack. - Hand-off: the exported PNG/JPEG opens anywhere; the saved
.krakeeps every layer editable for the user.
Gotchas
- The bridge runs in the live session, so state persists between sends: open documents,
the active view, and selections all stick. Version your documents
(
createDocument(..., "poster_v2", ...)) so a re-send doesn't fight the previous one. setBatchmode(True)before any export or save — otherwise Krita may block on a format-options dialog that nobody is there to click, and the send times out. It does not coverclose()— see Closing documents.- A send that times out usually means a modal dialog, not a dead bridge: a dialog runs
a nested event loop, so the bridge still answers and
QApplication.activeModalWidget()names it — see references/recipes.md. refreshProjection()+waitForDone()before every read/export; without it you can export a canvas that is one composite behind.- Opacity is 0–255, blend modes are Krita's own ids (
"normal","multiply","add","erase", …). There is no API listing them in 5.3.x —app.blendingModes()exists only on master, so take ids from the Layers docker. - Don't chain
app.filter("x").configuration()— theFiltertemporary is collected and itsInfoObjectdies with it:RuntimeError: wrapped C/C++ object of type InfoObject has been deleted, on a line that looks fine. Bind the filter to a name. - Ints where you expect floats:
xRes()/yRes()return floats butscaleImageandsetResolutiontake ints and PyQt raisesTypeErrorinstead of rounding —int(...). node.save()returnsNone, not the documented bool; check the file exists.- Keyframes can't be created from the API —
setCurrentTime+setPixelDatanever makes one while every save looks right. Triggeradd_blank_frameper frame anddoc.waitForDone()after it — the action is asynchronous, and without the wait every frame's pixels land one frame early; see references/api-reference.md. setPixelDatawants the node's colour space, not RGBA-in-any-order: with"RGBA"/"U8"documents theFormat_ARGB32buffer works as-is; for U16/F32 documents build the bytes to match, or convert the document first.- Layer bounds are not the document — a paint layer's
bounds()is the painted extent and can be empty until pixels land; usedoc.width()/height()for full-canvas work. - Krita rewrites
kritarcwhen it quits, so installing/enabling the plugin while it runs gets silently reverted — quit Krita first (the installer refuses otherwise). - Env vars don't reach a GUI-launched Krita (
open -a krita), soKRITA_BRIDGE_PORTonly takes effect when Krita is started from a terminal. paint*needs a view and inherits the user's brush state — without a view evenpaintAbility()crashes Krita; with one, preset, size, opacity, blending mode and foreground colour all come from the active view, so set what matters and checkpaintAbility() == "PAINT"first.paintLineinsists onQPointwhile the rect calls takeQRectF. Tools, the transform widget and canvas input stay unscriptable;app.action(id).trigger()is the only door to menu commands, it acts on the active document, and it reports nothing back.- Scripted edits aren't reliably undoable — duplicate a layer or save a copy before destructive work on the user's own document; see references/recipes.md.
Security
Installing the bridge means running a code-execution server on the user's machine. Say so before asking them to install it.
- The bridge binds
127.0.0.1:8737(KRITA_BRIDGE_PORT) and executes any Python POSTed to it inside the live session — the user's privileges, the user's open documents. Requests carry no authentication: every local process, and every other user on a shared machine, can drive Krita through it. - Web pages cannot. Requests carrying an
Originheader or a cross-siteSec-Fetch-Siteare rejected with 403, so a page in the user's browser can't reach the bridge. That check is the only gate — there is no token. - The install choice decides how long the port stays open.
krita-install-bridge.shenables a pykrita plugin, so every Krita launch listens from then on, whether or not an agent is driving it. The Scripter paste lasts only until Krita quits — offer it when the user doesn't want a permanent listener. - To stop the bridge, quit Krita; to remove it, untick Krita Bridge in Settings ▸ Configure Krita ▸ Python Plugin Manager. There is no remote shutdown.