Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 kindwhat it becomeswhich section
groupcreateGroupLayer;passthrough → setPassThroughModeBuilding, and the two group pitfalls
fillcreateVectorLayer plus addShapesFromSvg; a full-canvas solid colour whose shape is null uses a paint layer plus ground insteadVector layers
strokespaintPath for a whole stroke, or paintLine segment by segment when it tapersch. 12
gradientcompute the RGBA yourself and setPixelDatach. 15
maskcreateTransparencyMask plus setSelection, attached to the parent grouptransparency mask
filtercreateFilterMask(name, f, selection), attached to the parent group likewise, and at most one per layerch. 14
rastersetPixelDataProperties and reading
clip: truesetInheritAlpha(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=X inserts it above X; and childNodes() 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 (removeChildNode plus addChildNode) 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

methodwhat 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 pixelData is an empty array (cannot reshape array of size 0); use projectionPixelData
  • 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.