Appendix B: the code index
The code lives in dtp/, sitting beside this handbook one directory up with its own git. Since 4.0.0 it is one distribution per rank under packages/, all contributing into the one dtp. namespace (PEP 420), so an import path such as dtp.craft.copy says nothing about which package ships it. The library is atomic: pure functions take arrays, dicts, a parts table or the path of a single file, and return values; io.py is a thin layer of helpers for the fixed-format artifacts alone; and the library knows nothing about step directories, orchestrates no stages and has no command line – what wires it together is the main.py inside each step (templates in templates/). The library knows no particular picture.
The directories are five ranks, divided by who decides what, and the dependency rule is enforced by tests/test_layering.py, which walks the AST of every module (including imports inside function bodies): a rank may import downward and never upward.
| layer | rank | package | distribution | what it decides |
|---|---|---|---|---|
| atoms | 0 | geometry/ raster/ ir/ · io.py settings.py config.py vocab.py | dtp-core | pure mathematics, pure raster, and the layer tree’s schema. It knows neither Krita nor parts |
| capability | 1 | ops/ (the only place that imports krita) | dtp-krita | what Krita can do |
| reading | 1 | read/ | dtp-read | what can be read out of a picture |
| the vocabulary of painting | 2 | craft/ | dtp-craft | an artist’s actions (filling, banding, copying, hatching, strands, 色トレス), with aesthetic constants and no part names |
| part | 3 | paint/ | dtp-paint | how hair is painted, how skin is painted |
| wiring the stages | 4 | stages/ | dtp-stages | what a stage calls, producing the IR and the account |
| the rulers (cross-cutting) | 5 | gauge/ | dtp-gauge | measuring. No rank imports it |
Each package declares which other dtp-* packages it depends on, and test_layering.py holds that declaration against the edges its AST pass actually found. The rank rule is therefore written down twice, in two places that cannot drift apart in silence.
One stage, which modules
The main text names no module – it describes what a step does and sends you here. This is the way back in: one row per stage, in the order the routes run them.
| stage | ch. | what it calls |
|---|---|---|
segment | 1 | read/segment.py – the boundary barrier and the regions, the hand-written table, the majority vote and growing from a seed, lines as walls, and the sub-pixel rings; the isolines are geometry/contour.py, the rings geometry/rings.py |
lineart | 2 | read/lineart.py – source one (the contour candidates), source two (the AI candidates, with the crossing number, tracing and joining at junctions), then scoring, resolving overlaps, applying the overrules, the arbitration and the review image; thinning is geometry/skeleton.py, fitting geometry/fit.py; the ComfyUI driver is read/comfy.py |
complete | 3 | read/seethrough.py (running the model in its own venv, and the merge rules) and read/complete.py (the occluded area, the account, and the restack test); the groups’ masks are read/groups.py |
lines | 4 | read/lines.py – this layer’s image alone, then ch. 2’s candidate pool and arbitration reused, plus the invented-line account |
draft | 5 | stages/draft.py for each group’s rings and local colour, read/tiers.py for the local colour itself, stages/tree.py to turn the plan into a layer tree, ops/layers.py for the tree’s building blocks, and jobs/bake.py to paint it |
paint | 6 | stages/paint.py for the step, paint/ for one method per part (dispatched through the registry in paint/__init__.py), craft/ for the actions a method is built from, craft/color_trace.py for how heavy a line is, raster/sim.py for the geometry of a stroke, ops/brushes.py for the brush table, and jobs/bake.py to paint it |
gauge | 7 | gauge/report.py for the criteria and the per-group rulers, gauge/lines.py for the ruler on the lines, gauge/compare.py for the matching-position comparison, and ir/count.py for the description count |
trace | 8 | stages/trace.py reads the picture, read/tiers.py gives the value bands, and jobs/bake.py paints it |
Each stage is wired by the main.py in its own step directory (appendix A); none of this is reachable from a command line.
jobs/ is the Krita process’s entry point and may import only ops, ir and gauge (the last only to write the review images), and it ships together with ops in dtp-krita, one capability in one process – it knows no painting method, so what the tree looks like cannot possibly be hiding inside a job.
dtp/
ruff.toml the style shared by every package: 100 columns, E W F N D, Google docstrings
packages/ one distribution per rank, each with its own pyproject.toml and its own src/dtp tree
dtp/ the umbrella: no code, depends on the seven below, so `dtp` still installs the whole library
core/ read/ craft/ paint/ stages/ gauge/ krita/
tests/ 87 pytest items. Four guardrails: test_layering.py (the rank rules, jobs naming only what exists, pyflakes), test_style.py (ruff, English throughout, every why/cite resolving),
test_frozen.py (four methods over a toy input, compared by sha256 -- a pure move may not change one bit), test_ir.py (the layer tree's schema, counting and rendering).
test_layering.py also holds each package's declared dependencies against its actual imports
probes/ capability probes (appendix C)
templates/ project/parts.py, the parts-table template; <stage>/main.py, eight step templates; not in the wheel
tools/ setup_harness.py, selfcheck.py, comfy.py, probe.py, gauge.py, gui.py (appendix A)
packages/<dist>/src/dtp/ (no __init__.py at this level: the packages share one namespace)
io.py reading and writing the fixed-format artifacts: load/save_json, load/save_strokes (STROKE_KEYS), load_rgb/rgba/mask, save_png, load/save_plan, load/save_amodal, md_table, write_readme (which keeps the human's "## Verdict")
settings.py machine-level settings (dtp.toml / DTP_*) -- the one place in the library that reads the environment
vocab.py strings two ranks both have to say the same way (a review image's legend, a table header, the bands' role names), so that no two places keep their own copy
config.py the parts table: a parts.py path to a module; check_parts(methods=, tags=) has the methods and tags it knows **injected by the caller**, so config never imports upward
geometry/ L0, pure geometry
contour.py sub-pixel alpha, SDF, isolines (trace(forbid=) never covers a lower layer), RDP, Catmull-Rom to SVG, rasterization
fit.py Schneider least-squares cubic beziers, curvature sign changes, breaking at corners, bridging gaps
skeleton.py thinning, walking lines, RDP, sampling a line's width and colour, band_path() for the longest geodesic along a band
flow.py a double-angle harmonic flow field, anisotropic partitioning along it, evenly spaced streamlines (Jobard-Lefer)
rings.py vectorize() / rings_json() / fit_rings(): mask to rings to bezier rings (one ruler shared by lineart, bands and shapes)
raster/ L0, pure raster
sim.py Sim, the simulated canvas (paint it once while planning to see how far off it is), plus kernels, normalized blur, flow fields and seed growth
mask.py tidy_mask / tidy_labels (dust **goes to its neighbour** rather than being deleted) / clean_mask / seam_mask / nblur
color.py lab() / psnr()
ir/ L0, the layer tree -- the intermediate form "painting" takes, knowing neither Krita nor parts
schema.py IR_VERSION, KINDS, the required fields, the defaults, the 32 blending modes measured to work, walk(), validate()
build.py doc() shape() group() fill() gradient() strokes() mask() filter_node() raster()
count.py **the description count**: node_cost() / describe_cost() / honesty() -- ch. 7's criterion
render.py draw the tree without opening Krita (a 1x preview, with clipping, blending modes, blur and masks)
svg.py rings to SVG (what a fill node needs to land on a vector layer)
ops/ L1, the capability rank and **the only package that imports krita**
session.py Session: building trees, addressing, changing brushes, painting, reading and writing pixels, selections, filters, saving, the painting journal, note(), replay, manifest()
atoms.py capability atoms with no aesthetic constants: gradient_pixels / gradient / selection_from_mask / transparency_mask / filter_mask / blur_mask / transform
bake.py **one tree to one Krita document**: build() builds it, paint() paints it, bake() keeps the account. This is the only seam between "painting" and Krita
layers.py three building blocks: vector layer, group, paint layer
brushes.py the brush table KIND: kind to (preset, width factor, layer, taper), and the only one
harness.py setup(): a separate HOME, the autopaint extension and run_job.sh; run_job(): start Krita and run jobs.<x>.job(**args)
read/ L1, reading the picture
segment.py 1, segment: the boundary barrier, the hand-written table, lines as walls, the SAM majority vote and seed growth, review, vectorization
lineart.py 2, lineart: candidates, scoring, deduplication, arbitration
lines.py 4, per-layer lineart: this layer's image alone, candidates, arbitration, the made_up account
complete.py 3, completion: the upper area intersected with a See-through layer minus what is visible, to rings, to an account; plus the restack test
seethrough.py 3, See-through itself: starting its venv, mapping its square canvas back to 1x, the merge rules, the seam
groups.py groups: each one's visible mask, whole mask, colour clusters and mutually exclusive mask
tiers.py value bands: pick_k / levels / flat_color / lab / quantize / hsv_check / split_tiers
locks.py hair's locks: bright ridges and dark seams, partitioning along the flow, each lock's principal axis, the seams, each lock's own bands, the 天使の輪 (halo_cut draws the whole band and then cuts it)
comfy.py Comfy(url, dir): raster lineart and SAM masks
craft/ L2, the vocabulary of painting (an artist's actions, with aesthetic constants and no part names)
fill.py wash() to fill by scanning -- only the background still uses it (the in-band filling pass was deleted in stage 4: the vector layer had already filled the same area)
cylinder.py **the cylinder**: a form with a direction becomes an absolute-colour gradient along its axis plus a multiply gradient across it (a lock of hair, a finger, a ribbon)
tone.py bands: tier_entries() / one_tier()
copy.py the copy-it baseline: soft() / detail() / passes_for() / soft_passes()
texture.py hatch() for hatching, strands() for strands
hair_marks.py seams() between locks, tips() at the ends, stray() hairs
color_trace.py color_lines() for 色トレス (a line's weight from its own layer's image) and contour_lines()
paint/ L3, the parts: one method per part in one module, all with the same plan() signature
__init__.py the METHODS registry, PLACEHOLDERS, and the plan_group() dispatcher
generic.py the copy-it baseline, which is not a way of painting: error-driven soft and hard brushes; the default for "*", and what every part's method has to beat
skin.py hair.py eye.py background.py skin 🧪 / hair 🧪 (one cylinder per lock, producing an IR subtree) / eyes 🧪 / background (the colour field)
_ref.py target(): the reference image being copied
stocking cloth shoe metal building tree sky water ⬜ placeholders: the docstring is how an artist paints it plus what has to be decided, and plan() raises with the handbook page's name
stages/ L4, wiring the stages together, all pure functions
draft.py 5, the draft: plan_groups() for each group's rings and local colour, check_orphans(), plan_json(), preview()
paint.py 6, painting: each group by the PAINT table, plus line weights and the background field, giving strokes, each group's account and the 1x simulation
trace.py 8, one-shot: value bands to rings to strokes to the 1x simulation
tree.py **a plan to a layer tree**: from_draft / from_plan / from_trace plus prune() and graft() (attaching the subtree a method produced); brush_of() decides which brush a stroke uses
table.py load(): injects METHODS and TAGS into config.load_parts, so that config never imports upward
gauge/ the rulers, cross-cutting, imported by no rank
report.py 7, the criteria: report() for the five plus interior sharpness, the painting journal and the description count; group_metrics() (per group: interior sharpness, at the hard edges, the **8-level colour count**, 1x PSNR); tier_edges(); tree_rule()
lines.py shape.py compare.py review.py the ruler on lines / edge quality / matching-position comparison / review images
jobs/ imported only inside Krita; its arguments come from DTP_JOB as JSON
run.py the shell the autopaint extension execs: import dtp.jobs.<x>, job(**args)
bake.py **the only painting job**: read tree.json, build the tree, paint it, write the review images (shared by all three routes)
selfcheck.py probe.py 31 self-checks; running one probe script
What runs where
| runs in | modules |
|---|---|
plain Python (uv run python .../main.py, one process) | geometry/, raster/, ir/, read/, craft/, paint/, stages/, gauge/, io.py and config.py; seethrough.run_model() starts a separate process in its own venv |
Krita (krita-bot/run_job.sh -> xvfb-run krita -> the autopaint extension execs jobs/run.py) | jobs/*, ops/* and probes/* |
ir/render.py is the most useful thing on that boundary: it draws the whole tree in plain Python (at 1x, in twenty-odd seconds), so changing a method does not mean waiting three minutes for Krita every time. It is not a substitute for Krita – a brush’s texture, the antialiasing and the blending modes are all approximations – but measured, it is closer to the real output than the old “1x simulation of a list of strokes” was (the readings are in ch. 19). The numbers have to come from real painting: the criteria always measure what Krita actually painted (ch. 6).
Krita embeds the system Python; run_job.sh puts every package’s src on PYTHONPATH (colon-separated, passed in at run time as $DTP_SRC, so adding a package needs no re-setup either), so what a job imports is the library’s own source and changing the library needs no setup. A job has no __file__ (it is exec-ed), and from dtp.ops.session import * only works at module level, which is why the painting code lives in its own jobs/*.py. Starting Krita from inside a uv venv requires stripping VIRTUAL_ENV, PYTHONHOME and .venv/bin (which run_job() does), or Krita’s plugin system fails to start.
Session at a glance
s = Session(w, h, "name", journal=True, out_dir=...)
s.build(spec) / s.find(path) / s.use(path) / s.by_role(r) / s.by_label(n) / s.tree()
s.set_preset(name, size, opacity, flow) / s.set_color(r,g,b) / s.brush(color, size, opacity, flow, preset)
s.stroke(pts, pressures, node) / s.path(pts, node) / s.line(pts, width, node) / s.dab(x,y,p,node)
s.ellipse(...) / s.polygon(...)
s.layer(name, blend, opacity, clip, label) / s.group(...)
s.pixels(node, box) / s.projection(node, box) / s.composite(box) / s.set_pixels(rgba, node, box, why=…) / s.ground(r,g,b)
s.note(op, **fields) # append an entry to the painting journal (bake uses it to record how many numbers a node cost)
s.select(x,y,w,h) / s.select_mask(mask, feather) / s.select_none() / s.erase(box, mask)
s.filter(name, params, node, box)
s.settle(node) / s.stable(fn) / s.flush()
s.stage(label) / s.final() / s.save_kra(path) / s.manifest()