30 非破坏、生成与离屏
✅ 已验证 · Krita 6.0.2.1 + PyQt6 + Xvfb,六个探针(附录 C)
前面四章讲的是「怎么把像素写进图层」。这一章讲的是另外三类能力,它们的共同点是 不写像素:非破坏的效果层、Krita 自己算出来的内容、以及文档之外的一块画布。
书里第 29 章原来写着「filter 不受选区限制」——那只是 apply() 的结论。这一章的
第一节就是来改写它的。
非破坏的局部 filter
filter.apply(node, x, y, w, h) 是破坏性的,范围只受 box 控制,刀切硬边。
但 createFilterMask / createFilterLayer 的签名里直接收 Selection:
f = Krita.instance().filter("gaussian blur")
c = f.configuration(); c.setProperty("horizRadius", 25.0); c.setProperty("vertRadius", 25.0)
f.setConfiguration(c)
sel = Selection(); sel.select(x, y, w, h, 255); sel.feather(40)
m = doc.createFilterMask("FM", f, sel)
target.addChildNode(m, None) # 挂在【目标图层下面】, 不是 rootNode
判据是模糊强度剖面的 0.9→0.1 过渡带宽度(理想台阶经平滑后是 7.2 px,这是地板):
| 路 | 过渡带 | 耗时 | 破坏性 |
|---|---|---|---|
filter.apply(node, box) | 7.2 px(=地板,刀切) | 4.4s | 是 |
createFilterMask(f, sel.feather(40)) | 59.9 px | 4.1s | 否(源层逐字节没动) |
同上,feather(120) | 177.7 px | 7.6s | 否 |
createFilterLayer(f, sel.feather(40)) | 59.9 px | 3.9s | 否 |
createFilterMask(f, node) | 无——整幅都被处理 | 9.3s | 否 |
- 过渡带 ≈ 1.5 × feather(59.9/40 = 1.50,177.7/120 = 1.48)。
createFilterMask(name, filter, node)那个重载建得出来但不限制范围,源 node 可见、放在根节点也一样。要局部就用 Selection 重载。- 往返:
.kra里 filtermask / filterlayer 都存得住,重开后过渡带一模一样。 - 量它必须用
s.projection(group)。mask 是非破坏的,node.pixelData()一个 字节都不会变,量源层会得到「完全没生效」的反向结论。
ColorizeMask:线稿 + 几个色点 → flats
Krita 自带 lazy-brush。无头下跑得通,但调用顺序里有两步在 api.kde.org 的方法表里 根本没有,只在源码里:
m = doc.createColorizeMask("CM")
lineart.addChildNode(m, None)
m.setEditKeyStrokes(True)
m.initializeKeyStrokeColors([ManagedColor, ...], -1) # ← 少了这步, 下面全返回 False
cc = m.keyStrokesColors() # ← 用它返回的对象去 set
m.setLimitToDeviceBounds(True) # 否则背景色往整幅 8K 漫开
m.setKeyStrokePixelData(buf, cc[i], x, y, w, h) # buf 是 1 字节/像素, 返回 bool
m.setEditKeyStrokes(False) # ← 不关掉的话输出是 50% 混白的预览
m.updateMask(True); m.setShowOutput(True)
setKeyStrokePixelData 只往已存在的同色 keystroke 里写,找不到就返回 False
——它是老实的,不静默。颜色比较走 KoColor 精确相等,所以要用 keyStrokesColors()
返回的那些对象,别自己另造一个「一样的」颜色。
在 8192×12288 上、一条解析闭曲线被两条横线切成三块、线宽 6 px:
| 判据 | 结果 |
|---|---|
| 三个区各自的面积 / 解析面积 | 0.98 / 0.98 / 0.98(差的 2% 就是线宽) |
| 未归类像素 | 0.3% |
| 耗时 / 内存 | 12.3s,RSS +1100 MB |
| 边界带偏差 | 0.4981 = 二值硬边 |
.kra 往返 | colorizemask 存得住 |
区域归属很准,边很糟。 0.498 和 paintPolygon 同档,离矢量层的 0.083 差一个数量级
(第 27 章的四条路对比)。所以它的位置是「切 flats」,不是「出成品边」。
量它的时候判据自己也会骗人:用「离哪个色点最近」的硬分类去算覆盖率,边界带偏差 恒等于 0.5——那量的是我的分类器是二值的,不是 colorize 的边是二值的。换成 把像素投影到「背景色→区色」线段上的连续覆盖率,才是 0.4981 这个可信的数。
Preset:程序化改笔刷
from krita import Preset
p = Preset(res["b) Airbrush Soft"]) # 收一个 Resource
xml = p.toXML() # 1.7k - 22.7k 字符
p.fromXML(xml2)
| preset | xml | setBrushSize(90) 实际 | 系数 | fromXML(toXML()) |
|---|---|---|---|---|
| b) Basic-1 | 1689 | 92 px | 1.02 | 无损 |
| f) Bristles-5 Flat | 16516 | 40 px | 0.44 | 无损 |
| b) Airbrush Soft | 9552 | 82 px | 0.91 | 无损 |
| c) Pencil 1 Sketch (mypaint) | 22658 | 14 px | 0.16 | 有损:14 → 442 px(×31),不报错 |
- 改
brush_definition里的diameter生效:Airbrush Soft 90 → 200,之后setCurrentBrushPreset会把view.brushSize()自动带到 200,画出 184 px。 - 但对预定义笔尖的 preset(Bristles-5 Flat)改
diameter完全无效,不报错。 - 把
hfade/vfade归零,两个 preset 的系数纹丝不动(0.91 / 0.44)。 系数来自笔尖形状本身,第 21 章的宽度系数表作废不了。 fromXML的改动在进程内全局生效(重新resources("preset")拿到的是改过的), 但不写盘(paintoppresets/0 新增 0 改动),fromXML(原 xml)能还原。
Scratchpad:文档之外的一块画布
原来的指望是拿它绕开撤销栈(第 19 章:每次 paint* 约 233 KB
且清不掉)。这条路不成立——Scratchpad 上没有任何程序化落笔的 API,只有 fill*
和 loadScratchpadImage。paintEngine / paintEvent / paintingActive 是 QWidget
自带的,不要被它们骗了。
但它有另一个价值:这是拿到 19 个 gradient 和 112 个 pattern 光栅结果的唯一一条路
——Node 上没有「铺渐变」的 API。
from krita import Scratchpad
sp = Scratchpad(view, QColor(0, 0, 0), None)
sp.show(); sp.resize(1200, 800) # 顺序不能反, 见下
sp.setModeManually(False); sp.setMode("painting")
view.setCurrentGradient(Krita.instance().resources("gradient")[name])
sp.fillGradient(QPoint(0, 0), QPoint(1200, 0), "linear", "none", False, False)
img = sp.copyScratchpadImageData() # QImage
- 无头下能构造,不崩。
resize()/setGeometry()静默无效:出厂 640×480,调完还是 640×480。 必须先show()再resize()才变成 1180×780(还差一圈边框)。fillForeground填出的颜色和setForeGroundColor精确一致。fillGradient/fillPattern真的出东西(渐变 32 色;pattern 纹理梯度均值 54.9)。copyScratchpadImageData()→ numpy →setPixelData无损(最大逐通道差 0)。- 内存:200 次
fillForeground= 2.9 KB/次,同画布 200 次paintLine= 53.5 KB/次。
Shape:矢量层建完之后还能改
shape 的几何单位是「点 pt」,不是像素。 300 DPI 下 1 px = 0.24 pt。
拿像素坐标去问 shapeAtPosition 会得到 None,很容易被读成「这个功能不存在」。
PT = 72.0 / doc.resolution()
v.shapes(); v.shapesInRect(QRectF(...)); v.shapeAtPosition(QPointF(x * PT, y * PT))
sh.setTransformation(T0 * QTransform().translate(dx * PT, dy * PT)) # 见下
| 操作 | 结果 |
|---|---|
shapes() / shapesInRect / shapeAtPosition | 用 pt 正常;用 px 返回 None / 0 个 |
setTransformation(T0 * translate) | boundingBox 精确位移,光栅对「挪过的解析真值」偏差 0.0861 |
setTransformation(T0) | 精确还原(偏差回到 0.0891) |
setPosition(P0 + delta) | 与上等价 |
setZIndex | 生效,合成里的上下关系真的换了 |
createGroupShape(name, shapes) | 生效,2 个 shape → 1 个 groupshape |
setAntialiased(False / True) | 边界带偏差 0.2624 / 0.0891 —— 开关真的改边 |
setTransformation是替换不是叠加。 直接喂QTransform().translate(...)会把 shape 原本的摆放一起冲掉;而且QTransform.translate是在局部坐标里先平移, 要在父坐标里平移得写成T0 * QTransform().translate(...)。- 组的投影在形状挪走之后留残影:同一个大框里的不透明像素从 2,584,626 涨到
2,858,295。所以不要用重心判位移,会被残影拖着走。判位移要用
boundingBox()(对象读,精确)或者对着「挪过的解析真值」量偏差。
Node 级变换与 TransformMask
Node 级变换在 group 里都生效,clip(inherit alpha)关系跟着走。 一个 800×800 的 Flat + 一个裁到它的 Marks,合成不透明像素:
| 操作 | 合成不透明 px | Flat bounds |
|---|---|---|
| (原始) | 640,000 | 800×800 |
move(+400, +300) | 640,000 | 移到 (1800,1700) |
rotateNode(0.3) | 641,516 | 1002×1002 |
scaleNode(QPointF(cx,cy), 1200, 1200, "Bicubic") | 921,221 | 1199×1200 |
cropNode(x, y, 400, 400) | 2,983 | 43×138 |
TransformMask 是这些的非破坏版本,而且和 Preset 是同一个套路
——读出厂骨架再改,不要手写:
tm = doc.createTransformMask("TM")
target.addChildNode(tm, None)
xml = tm.toXML() # 926 字符的出厂骨架
xml = 改 xml 里的 transformedCenter # 手写一份 transform_params -> fromXML 返回 False
tm.fromXML(xml) # -> True
tm.finalAffineTransform()
改 transformedCenter 之后重心精确位移 (+500, +300),源层像素一个字节没动,
.kra 往返 transformmask 还在。
交付的 .kra 还能带什么
| 东西 | 建 | 对合成的影响 | .kra 往返 |
|---|---|---|---|
createSelectionMask + setSelection | ✅ | 逐通道差 0(不改画面,只是随文件带走) | ✅ |
createFileLayer(name, path, method) | ✅ None / ToImagePPI / ToImageSize 三种都行 | 正常渲染 | ✅ |
createCloneLayer(name, source) | ✅ | 与源逐通道差 0,改源会跟着变 | ✅ |
Palette + Swatch + addEntry | ✅ 内存里加得进去 | — | ❌ save() 返回 False,不写盘;也没有 Document 级 palette API |
setLayerStyleFromAsl | ❌ | 0 | 见第 31 章 |
前三个是净赚:给接手的画师带上每个部件的现成选区、一张源图对照层、重复部件的克隆。
Swatch 的方法是 color / id / isValid / name / setColor / setId / setName / setSpotColor / spotColor。