Layers
✅ verified
Where the layer tree comes from
Nobody writes this Python by hand. A tree is a file (appendix F), and the painting job builds it from that. There used to be one job per route each building its own tree (seven hundred-odd lines, three nearly identical copies of the build-and-paint code), so a change had to be made in three places, and what the tree looked like could not be seen without opening Krita.
Node kinds, and the API in this chapter each maps to:
| IR kind | what it becomes | which section |
|---|---|---|
group | createGroupLayer;passthrough → setPassThroughMode | Building, and the two group pitfalls |
fill | createVectorLayer plus addShapesFromSvg; a full-canvas solid colour whose shape is null uses a paint layer plus ground instead | Vector layers |
strokes | paintPath for a whole stroke, or paintLine segment by segment when it tapers | ch. 12 |
gradient | compute the RGBA yourself and setPixelData | ch. 15 |
mask | createTransparencyMask plus setSelection, attached to the parent group | transparency mask |
filter | createFilterMask(name, f, selection), attached to the parent group likewise, and at most one per layer | ch. 14 |
raster | setPixelData | Properties and reading |
clip: true | setInheritAlpha(True) | clip(inherit alpha) |
opacity(0..1) | setOpacity(int(v * 255)) | Properties |
Two things only measurement can tell you: a full-canvas solid fill has to be a paint layer – built as a vector layer, setPixelData never reaches its projection, the output is five dB worse and nothing raises at all; and stacking more than one FilterMask on the same layer is O(n^2) (appendix C), so wrap another group around it instead.
The declarative Session.build(spec) is still there and is what the probes and the self-check use; production trees all go through bake.
Building
n = doc.createNode(name, "paintlayer"); parent.addChildNode(n, above)
g = doc.createGroupLayer(name) # this one is required; createNode(..., "grouplayer") gives you no setPassThroughMode
v = doc.createVectorLayer(name)
doc.createCloneLayer / createFilterLayer / createFillLayer / createTransformMask / createSelectionMask / createColorizeMask
addChildNode(child, None)puts it at the top of that parent’s stack;above=Xinserts it above X; andchildNodes()returns them bottom to top- these layers can be
paintLined directly, with no need to write pixels and diff them - four levels of nesting measure fine, but a group composites separately and each level stores another set of tiles, so do not go beyond three or four
- reparenting (
removeChildNodeplusaddChildNode) works
Session.build(spec) builds a tree declaratively from leaf_spec() and group_spec(), with list order running bottom to top; afterwards it reads childNodes() back and compares against the spec, raising on a duplicate name. Addressing is by s.find("04_Color/Skin/Skin_Shadow"), s.find("Skin_Shadow"), by_role or by_label.
Properties
setBlendingMode, setOpacity, setColorLabel, setVisible, setLocked, setAlphaLocked and setInheritAlpha all work (the last two measured: with alpha locked, another stroke on an empty area changes no pixel count; inherit alpha is under “clipping” below).
setBlendingMode validates nothing at all: pass "xyzzy" and it reads back unchanged while compositing as normal. A matching read-back proves nothing about the mode being real. Measured on base (128,64,192) plus top (100,200,80), these 39 were confirmed to change the composite:
multiply screen overlay soft_light soft_light_svg hard_light hard overlay
dodge burn linear_dodge linear_burn vivid_light pin_light
add subtract divide inverse_subtract exclusion negation
darken lighten darker color
hue saturation color luminize lightness
grain_merge grain_extract allanon parallel geometric_mean
reflect glow freeze heat behind greater erase
darker color and hard overlay have a space in them. linear_light, hard_mix, difference, lighter color, copy, dissolve and alphadarken could not be confirmed (they composite the same as normal).
Clipping (inherit alpha): three measured rules
Clipping is called inherit alpha in Krita (setInheritAlpha(True)). It is the pivot of ch. 5’s tree: the flat colour is clipped to the vector Shape and the parts group to the silhouette. Three probes back this (appendix C).
One: it clips to the accumulated alpha of the layers below it in the same group. Inside a group, a red base block at x in [100,200) and a green bar above it at x in [20,380): off, it spans (20,379); on, it spans (100,199). Add a blue block to the base and it becomes (100,339) – the accumulation, not the layer immediately below. Make the group passthrough and the clipping stops working. Hence the flat colour has to be at the bottom of its group, and the group must not be passthrough.
Two: a vector layer can be the base, and a group can be clipped too. A paint layer clipped to a vector layer (probed at 8K, with 39 strokes deliberately painted past the edge): the boundary-band deviation is exactly what the vector layer alone gives (0.0828) and nothing spills over – inherit alpha accepts a vector layer as its base and passes the vector’s fractional alpha straight through to the paint layer above. For comparison, clipping to a binary raster layer reads 0.3791, 4.6 times worse, and the whole difference is whether the group’s base is a curve or a pixel grid. setInheritAlpha on a group itself: a parts group painting past its edge covers 10,270,050 px, and clipped to the silhouette 7,147,891, exactly what the silhouette alone covers – it clips the whole group’s composited alpha, clipping alpha without changing content.
Three: the criterion has to look at alpha too. The mechanism is disableAlphaChannel(): compositing writes only RGB and not A, so RGB has been written even where alpha is 0. Looking at RGB alone concludes “not clipped”. Building the base and top under the root node (where a fully opaque Background makes the accumulated alpha 255 everywhere) records it as “clipping does not work” – the first case in ch. 17.
The two group pitfalls
Passthrough breaks clipping (rule one above).
When it is not passthrough, a multiply inside the group does not multiply against what is below the group. Put a multiply vignette in 09_Post and the pure black background is raised to (6,6,6). A group composites its siblings together first and then lays the whole thing down – so the vignette is multiplying against a transparent sibling and gives a patch of grey. A single-layer probe cannot find this (multiply over pure black on its own gives (0,0,0)). It has to be measured inside the real group structure. The fix is passthrough=True on that group, which has no clipped layer in it.
Reading
| method | what it reads |
|---|---|
node.pixelData(x,y,w,h) | that layer’s own pixels, in BGRA |
node.projectionPixelData(x,y,w,h) | that node composited with its children |
doc.projection / s.composite() | the whole document’s composite |
- a group’s
pixelDatais an empty array (cannot reshape array of size 0); useprojectionPixelData - a vector layer has no readable
pixelData; look at the composite - on a large canvas with many layers, always pass a box: one 2000x3000 layer is 24 MB, and a hundred of them several gigabytes
Vector layers
vl = doc.createVectorLayer("v"); parent.addChildNode(vl, None)
vl.addShapesFromSvg('<svg xmlns="http://www.w3.org/2000/svg"><path d="…" fill="#…"/></svg>')
The vector probe passes 5 of 5: it reaches the composite (its span matches the SVG exactly); bezier C and Q render; coordinates are 1:1 pixels, with (7800,11800) on 8192x12288 offset by (+0,+0); the .kra round trip is faithful; and a single shape spanning 7700 px is fine.
vl.toSvg() exports back out. node.save() segfaults on a vector layer and kills the process outright – and it is not needed, since vectors reach the composite anyway.
It can still be edited afterwards: shapes(), setZIndex, setTransformation, createGroupShape and setAntialiased all work. But the geometry is in points, not pixels (at 300 DPI, 1 px = 0.24 pt), so asking shapeAtPosition in pixel coordinates returns None. See ch. 15.
15 vector layers with 5000 control points take about 12 s; run the event loop a few rounds after addShapesFromSvg.
Node-level transforms
move, rotateNode, scaleNode, shearNode and cropNode all work inside a group, and the clipping relationships follow along. The non-destructive version is a TransformMask – read its factory toXML() skeleton and edit that, because a hand-written transform_params is rejected. See ch. 15.
transparency mask
It works only when attached under the target layer; attached to the rootNode it is an orphan. After mask.setSelection(sel) it clips exactly. It is editable, which suits handing over to an artist better than inherit alpha does.
.kra
doc.saveAs("x.kra") works (5.x segfaults). The round trip is faithful in layer count, inheritAlpha, blend, opacity, label and vector layers. doc.exportImage(path, InfoObject()) writes a PNG. Call doc.setBatchmode(True) before saving.