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

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 px4.1s(源层逐字节没动)
同上,feather(120)177.7 px7.6s
createFilterLayer(f, sel.feather(40))59.9 px3.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)
presetxmlsetBrushSize(90) 实际系数fromXML(toXML())
b) Basic-1168992 px1.02无损
f) Bristles-5 Flat1651640 px0.44无损
b) Airbrush Soft955282 px0.91无损
c) Pencil 1 Sketch (mypaint)2265814 px0.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*loadScratchpadImagepaintEngine / 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,合成不透明像素:

操作合成不透明 pxFlat bounds
(原始)640,000800×800
move(+400, +300)640,000移到 (1800,1700)
rotateNode(0.3)641,5161002×1002
scaleNode(QPointF(cx,cy), 1200, 1200, "Bicubic")921,2211199×1200
cropNode(x, y, 400, 400)2,98343×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
setLayerStyleFromAsl0第 31 章

前三个是净赚:给接手的画师带上每个部件的现成选区、一张源图对照层、重复部件的克隆。

Swatch 的方法是 color / id / isValid / name / setColor / setId / setName / setSpotColor / spotColor