# Implementation Notes Design rationale and non-obvious invariants that used to live as long inline comments, moved here so the code stays skimmable while the reasoning stays discoverable. Code comments reference these sections by anchor. ## Content-Addressed Files & Caching ### FileCache Design Rationale `fileCache` (`frontend/src/fileCache.js`) is a shared, session-lifetime cache for the bytes behind `AuthenticatedImage`'s `src` (a `/media/...` hash-addressed path; see `backend/files/media_urls.py`). Content there is immutable and hash-addressed (SHA-256), so a cache hit never needs revalidation — the same `src` string can only ever resolve to the same bytes, for any owner or requester. It is deliberately NOT Vuex state: it holds `Blob`/object-URL data that nothing needs reactive access to (components only bind the resulting object-URL string, which they hold in their own local state), so a plain module-level `Map` avoids Vue's reactivity overhead entirely and sidesteps proxying `Blob` instances for no benefit. ### SHA-256 File Hashing Must Match The Backend Before uploading, the frontend computes a SHA-256 hash of a file's raw bytes via the Web Crypto API (`crypto.subtle.digest('SHA-256', ...)`). This hash must match the one the backend computes over the same bytes (`backend/files/models.py`, `hashlib.sha256`), so the hash computed client-side can later be used to identify the same `File` row server-side without a mismatch. This same computation appears in `CameraFileSource.vue`, `DragDropFileSource.vue`, `FsFileSource.vue`, and `WebcamFileSource.vue`. ### Stale Orphan Cleanup At The Canonical Hash Path `FileManager.create()` (`backend/files/models.py`) derives each file's upload path entirely from its content hash (`hash_upload`), and `hash` is unique in the database. That means if no `File` row currently owns a given hash, anything already sitting at that hash's computed storage path is necessarily a stale orphan — for example, left behind by a bug in cleanup code that deleted a `File` row without removing its stored bytes, or by a crashed upload. Before saving a new `File` row, `create()` proactively deletes anything already at the expected path rather than letting Django's storage layer invent an alternate filename to avoid the apparent "collision". A suffixed name would silently break every part of the app that derives a file's URL purely from its hash (media serving, thumbnail generation, item/avatar attachment), and no future caller could ever discover the file again. `backend/files/tests.py`'s `test_file_upload_reclaims_stale_orphan_at_canonical_path` is a regression test for this exact scenario. ### RGBA Flattening Avoids Revealing Black Under Transparent Pixels When generating thumbnails (`backend/files/media_urls.py`, `thumbnail_urls`), the source image is first converted to RGBA and composited onto an opaque white background before being flattened to RGB and saved as JPEG. This matters because some source modes (grayscale+alpha, palette-with-transparency, RGBA) store meaningless color or luminance data underneath fully transparent pixels, which is often zeroed out (i.e. black). Converting straight to RGB without compositing reveals that zeroed data as solid black instead of showing nothing, producing a thumbnail that looks broken even though the visible, opaque content is fine. `backend/files/tests.py`'s `test_thumbnail_flattens_transparency_instead_of_going_black` is a regression test built around an `'LA'` (grayscale + alpha) source image whose transparent half has zeroed-out luminance underneath, reproducing exactly this failure mode. ## Label Rendering Engine ### QR Encoder Loading `anyd-qr.js`'s own `loadAnyDCode()` memoizes the wasm instantiation itself, so calling it more than once (each of `Print.vue` and `LabelLayoutPreview.vue` does, on mount) is free — `label.js`'s module-level `anyd` variable just mirrors its resolved value so `buildRenderTree` can use it synchronously. Until it resolves, a QR-family leaf (any `QR_LEAF_TYPES` entry) throws (see `encodeQr`) the same way an oversized value already does — callers already have to handle `layoutContent` throwing, so this reuses that path rather than adding a second failure mode. ### QR Leaf Type Mapping `QR_LEAF_TYPES` maps each of `label-layouts.js`'s `LABEL_TEMPLATES` leaf types that draw a code to the `anyd-qr.js` symbology/error-correction level (and, for rMQR, size strategy) it renders as (see anyd's `EncodeOptions` — `ecc`/`size` — and its per-symbology `EcLevel` enums, `wasm.rs`'s `qr_ec`/`micro_ec`/`rmqr_ec`/`rmqr_size`). Which combination a given label uses is baked into its layout tree (see `label-layouts.js`'s `"qr"`-prefixed templates), rather than a single choice applied to every code leaf alike, so there's no longer a global selector for any of them (see `Print.vue`). A plain symbology id (no suffix) always means anyd's own defaults — ecc `"M"`, rMQR size `"balanced"` — every other value gets a `"-"` suffix naming it: - **ecc**: the same letter anyd itself uses (`qr_ec`/`micro_ec`'s L/M/Q/H), except micro-qr's `"Detection"` (`MicroEcLevel::Detection`, an M1-only error-*detection*-but-not-correction mode with no plain single-letter grade of its own). Coverage isn't uniform across symbologies (see `qr_ec`/`micro_ec`/`rmqr_ec`) — full QR takes all four grades, Micro QR swaps `"L"` (QR's actual lowest) for `"Detection"` (lower still, but M1-only) and has no `"H"` at all, and rMQR only ever supports `"M"` or `"H"`. - **size** (rMQR only, see `rmqr_size`/`SizeStrategy`): `"min"`/`"max"` prefer the shortest (flattest, widest) or tallest (narrowest) symbol that fits the text, over the default `"balanced"` (smallest total module area) — which shape to prefer depends on which of the tape's two axes (across vs. along the feed) is more constrained. rMQR's matrix isn't square (see `encodeQr`'s width/height), unlike qr/micro-qr, which always are. ### QR Module Matrix Shim `encodeQr` wraps anyd's row-major `Uint8Array` matrix in a BitMatrix-alike shim (`{width, height, get(row, col)}`), matching the shape `drawQrLeaf`/`snapQrToCrispSize` expect — they predate this and were written against the old `"qrcode"` package's own `modules.size`/`get()` shape. anyd's matrix already excludes the quiet zone from width/height (see its `ModuleMatrix` type), same as the old library's BitMatrix. width/height are kept separate rather than a single `size` (the old library's own shape, always square) since rMQR symbols are rectangular. ### Layout Tree Structure A layout is a tree built from two shapes, alternating orientation by nesting depth: - An array is a "split" node: its children sit side by side (a *row*) at even depth (the root, depth 0, is always a row), or stacked (a *column*) at odd depth. To turn a row into a column, wrap it in an extra one-element array — that array is one depth deeper, so its lone child (the original row) is now read at odd depth. - An object is a leaf: `{type, content}` where `type` is one of `QR_LEAF_TYPES`' keys draws a QR/Micro QR/rMQR code at that id's symbology/error-correction level (see QR Leaf Type Mapping above), `{type: "text", content}` draws a text block — either way `content` is a function from the resolved field values to the string (or, for `"text"`, an array of strings — one per line) to render. `{type: "empty", "min-width": "2mm"}` / `{type: "empty", "min-height": "2mm"}` is a spacer with no ink of its own — the *only* way padding/gaps enter a layout, since nothing here draws a border, margin or gap on its own. An "empty" leaf's dimension always names the axis its enclosing split flows along: `"min-width"` inside a row, `"min-height"` inside a column. See `label-layouts.js`'s `LABEL_TEMPLATES` for concrete trees. ### Pixel Font Selection Below 33px, a general-purpose sans-serif gets blurry/illegible, so `drawTextLeaf` switches to one of `fontTierFor`'s bitmap-style fonts instead (see `../assets/fonts/pixel/LICENSE.md`) — Silkscreen/Chava/Pixelon/Terminus across increasingly larger size bands, snapping to whichever fixed/hinted size each reads best at rather than whatever `fontPx` literally asks for. Unlike the top (Inter) tier, these are rendered through `pixel-font.js`'s `drawPixelText` rather than canvas `fillText` — see Freetype Production Rendering below for why. At 33px and up, `fontTierFor` names Inter explicitly (see `../scss/_label-fonts.scss`) rather than falling back to the CSS generic `sans-serif` keyword — that keyword resolves to a different real font per browser/OS, which would make the same label print differently depending on where it was rendered from. Every tier was chosen only after rendering through `prototypes/freetype-wasm/ladder.html` — FreeType's own monochrome rasterizer at real canvas pixel sizes — and inspecting the actual pixels; checking that `fillText` was merely *called* doesn't confirm anything legible got drawn, and neither does a DPI-adjusted size that was never the number actually handed to the rasterizer. Two earlier candidates were tried and dropped: Tom Thumb's declared ascent/descent (0 / ~fontPx, backwards from a normal font) turned out not to be a centering quirk — its actual visible ink was only ~1/3.2 of its own nominal font-size — and PICO-8 rendered cleanly but has no lowercase glyphs at all (silently draws lowercase input as uppercase), ruling it out for real label content (item handles, URLs) that isn't reliably all-caps; see `frontend/src/assets/fonts/pixel/LICENSE.md`. Chava carries the same uppercase-only limitation but was kept anyway, restricted to a narrow size band, since PICO-8's dealbreaker didn't get applied consistently — see that same LICENSE.md. Each tier's `size` (the actual px handed to the rasterizer) replaces the old Tom Thumb-only `scale` correction, generalized: `fontPx` (the layout-derived logical size, compared against each branch's threshold) still drives box centering/stacking regardless of what `size` a tier resolves to. Those thresholds and `MIN_TEXT_PX` are both compared against that same logical, unscaled `fontPx`, deliberately not adjusted for the tape's dpi: a browser's font rasterizer only ever sees a raw pixel count, with no notion of "physical size" at all, so that's what determines whether a glyph's fine detail survives — confirmed by real-Chromium testing, where a raw 4.35px render was a solid blob regardless of what a dpi-scaled version of that number would have implied. A tier's optional `tracking` retunes glyph spacing (`drawPixelText` adds it to every glyph's advance) without ever touching the rendered glyph size. ### Freetype Production Rendering Canvas 2D's `fillText` is always anti-aliased — there is no monochrome/hinted-bitmap text mode in the API, however carefully a glyph's size/position are snapped to the pixel grid — and `weblabel.js`'s `bitmap.js` hard-thresholds the whole canvas at 50% luminance before it ever reaches a printer (a pin either fires or it doesn't). Anti-aliased glyph edges surviving into that threshold read as stray or missing pixels, not a clean bitmap-font shape. `pixel-font.js`'s `drawPixelText` sidesteps this for every `fontTierFor` tier below Inter by rendering through the same FreeType-wasm rasterizer `ladder.html` uses to *pick* those fonts in the first place (`FT_LOAD_TARGET_MONO`, true 1-bit output) and blitting the result at integer pixel offsets — `measureLine`'s pass computes a line's real ink extent (advance width, max ascent/descent actually reached by *these* glyphs) the same way `drawTextLeaf`'s old `ctx.measureText(...).actualBoundingBox*` did, so centering math is unchanged; only where the pixels themselves come from is different. Inter (the one non-`pixel` tier) still uses `fillText` — anti-aliasing is expected/fine at that size, the same reasoning `weblabel.js`'s own text renderer relies on ("what stops small text turning to mush is not the threshold but the size"). `preloadPixelFontRenderer()` (re-exported from `label.js`, defined in `pixel-font.js`) loads `freetype.wasm` and fetches every bitmap tier's font bytes up front, the same pattern as `preloadQrEncoder`/`anyd` (see QR Encoder Loading) — `drawTree`/`drawTextLeaf` stay fully synchronous, so both `Print.vue` and `LabelLayoutPreview.vue` await it (alongside `preloadQrEncoder`) before their first `redraw()`, and `drawPixelText` throws the same "still loading" shape of error `encodeQr` does if a draw is ever attempted before it resolves. The wasm module holds exactly one font face loaded at a time (`ft_load_font` replaces whatever was there); `ensureFace` skips the reload when consecutive leaves already share a family. ### Affine Width Height Relations Every node's width/height relate to each other affinely — `width = A*height + B` for a node read in row context, `height = A*width + B` in column context — because a leaf is either scale-free (a text block, whose aspect ratio holds at any size: `A = aspect` or `1/aspect`, `B = 0`) or a fixed physical size (an "empty" spacer, or a QR code once its crisp pixel size is known — see Crisp QR Sizing: `A = 0`, `B` = the size in px). Splits combine their children's relations by addition (a row's total width is the sum of each child's width for the shared height, and symmetrically for a column), which stays affine, so the same two numbers describe a whole subtree no matter how deeply it nests. `ownAxis` is true if the split directly containing `node` is a row, false if a column — for a leaf, that's what "empty" measures itself against; for a split, its own axis (and thus how it combines its children) is always the opposite, per the alternating-depth rule. `wantWidth` is true to ask for `{A, B}` such that `width = A*height + B`, false for `height = A*width + B`; requesting the direction a split doesn't naturally combine in just inverts its own relation. ### Fixed Size Relation Edge Case In `relation()`, when every child is a fixed size (`a === 0`) in the combining direction — e.g. a row that's just one crisp QR leaf, with no scale-free (text) sibling to invert against — inverting "width = b" for an `a` of 0 would divide by zero: a constant width genuinely doesn't determine a height, since nothing here actually scales with it. Instead, ask each child directly for its own size in the wanted direction (every one of them must be similarly fixed, since only a fixed leaf ever contributes `a === 0`), and take the largest — the shared dimension has to fit whichever child needs the most room, with any child that ends up with room to spare centered within it (see `drawQrLeaf`). ### Crisp Qr Sizing A QR code needs an integer number of pixels per module to render crisply rather than blurring at a fractional scale, so its true size is whatever that rounds down to — almost never the scale-free box its aspect ratio alone would suggest. `snapQrToCrispSize`, called once every QR-family leaf has a provisional (scale-free) box from a first `layoutTree` pass, pins each one's real `box.width`/`box.height` as `crispWidth`/`crispHeight`, so `relation()` starts treating it as a fixed size, the same as an "empty" leaf, instead of one that scales with whatever height/width it's offered. A second `relation()`/`layoutTree()` pass (see Label Content Layout) then resizes everything else around that real footprint, so nothing downstream reserves — and leaves unfilled — room for a squarer/differently-shaped code than what actually gets drawn. Kept as two independent dimensions rather than one `crispSize` (as when every code here was a square QR) since an rMQR symbol isn't square — see `encodeQr`. ### Render Tree Construction `buildRenderTree` turns a resolved content tree (see `templateContent` — leaf objects carry a `value` rather than a `content` function) into one ready for layout: a QR-family leaf gets its actual encoded modules (see `encodeQr`, keyed off the leaf's own type via `QR_LEAF_TYPES`) and an aspect ratio taken from their real width/height — 1 (square) for qr/micro-qr, but not for rmqr, whose symbols are rectangular — a text leaf gets its measured natural aspect ratio, and an "empty" leaf passes through untouched. Multi-line text (`value` is an array) measures as one leaf, not one per line — splitting it into a column of independently-sized leaves would let each line grow to its own full width, ending up at a different font size than its neighbors, which is legible but not what "one text field" should look like. ### Canvas Font Loading Applies only to Inter, the one `fontTierFor` tier still drawn with `fillText` (see Freetype Production Rendering above — every other tier goes through `pixel-font.js` instead, which fetches its own font bytes directly and has no `@font-face`/`document.fonts` involvement at all). A `@font-face` family already in use elsewhere on the page loads in time for `drawTextLeaf`'s use, but canvas text silently falls back to the next font in the stack (there isn't one here, so the browser default) if drawn before its first-ever load finishes — unlike DOM text, a canvas `fillText` never waits or repaints on its own once the real font arrives. Kicking off `document.fonts.load()` there means only that very first draw at a given size risks the fallback; every redraw after it (`Print.vue`'s live preview redraws on every keystroke) picks up the real font. ### Label Content Layout `layoutContent` builds, sizes and validates the tree for a fixed `fixedSize` (the tape's cross-web `printAreaPx`, or the fallback preview's reference height) — the one dimension every layout scales from, plus `pxPerMm` to turn "empty" leaves' physical sizes into pixels. `fixedSize` and the tree's content fully determine its overall size along the other, growing axis (the one that runs along the tape as it feeds); `maxLength`, when finite (a fixed-length/die-cut tape), rejects content that doesn't fit rather than shrinking it. `orientation` picks which axis `fixedSize` binds to: `"along"` (the default) fixes the tree's height — the tape's cross-web width — and grows its width along the feed direction, same as a plain read top-to-bottom design. `"across"` fixes the tree's width instead and grows its height, so the design is built turned 90deg from how it'd read `"along"` — `drawLabel`/`drawFallbackLabel` are what actually rotate the drawing back into the physical raster's fixed orientation; nothing here needs to know about that rotation, since `relation()`/`layoutTree()` already solve the tree in either direction symmetrically. Sizing runs twice: a first pass treats every QR-family leaf as the scale-free box its real width/height ratio suggests, purely to find out how much room each one would actually be offered; from that, `snapQrToCrispSize` pins each one's real (smaller, crisp-pixel) size. The second pass then resolves the whole tree again with that real size fixed in, so every sibling and the overall size reflect what's actually drawn rather than the idealized box no code ever quite fills. ### Tape Fed Label Drawing `drawLabel` is the tape-fed layout — it draws a fully resolved content tree (see `templateContent`) at the tape's real pixel dimensions. `orientation` is `"along"` (the default) to lay the design out reading along the tape's feed direction, or `"across"` to turn it 90deg so it reads across the tape instead — either way the physical raster this returns is still exactly `printedLength x tape.printAreaPx` (that's fixed by the tape/print head, not a choice this makes); `"across"` just draws the (now width-fixed, see Label Content Layout) tree through a rotated canvas transform so it lands correctly in that same raster, rather than transposing every box the tree itself computed. See `DEBUG_LEAF_BORDERS` to outline every leaf's box. ### Across Orientation Rotation When `drawLabel` draws with `orientation === "across"`, the tree was solved width-fixed (see Label Content Layout) — its width already exactly fills `tape.printAreaPx`, so only its (growing) height needs the same along-the-feed centering `origin` got earlier; `translate`+`rotate` then carries that tree-local `(x, y)` box straight into the physical (`printedLength x printAreaPx`) raster, a quarter turn at a time. ### Fallback Label Preview `drawFallbackLabel` is the no-webusb preview/PNG — same layout tree and renderer as `drawLabel`, just scaled from a fixed reference height instead of a real tape's, and with no `maxLength` (there's no physical tape to run out of, so the canvas just grows to fit) and no printer feed margin, since there's no real print head here to keep clear of. ## Label Templates, Preview & Pixel Fonts ### Template Layout Tree A template's `layout` is a tree as described in label.js, with leaves whose `type` is one of label.js's `QR_LEAF_TYPES` keys or `"text"`, and whose `content` is a function from the resolved field values (see label.js's `buildLabelFields`) to what they render. `null`/`undefined` returned from that function means the field isn't available yet (see `templateIsAvailable`). A template is only selectable once every leaf's `content` resolves to a value. ### Generated QR-Only Template Matrix `QR_ONLY_TEMPLATES` is the full "just the code" matrix: every `{symbology, error-correction level, [rMQR] size strategy}` combination label.js's `QR_LEAF_TYPES` supports, one template each. A plain `id` (no suffix) is always anyd's own defaults: ecc `"M"` and, for rMQR, size `"balanced"`. Coverage isn't uniform (see `QR_LEAF_TYPES`): full QR gets all four ecc grades L/M/Q/H; Micro QR swaps `"L"` (QR's actual lowest) for the even-lower, M1-only, detection-only `"Detection"`, and has no `"H"` at all; rMQR only ever supports ecc `"M"` or `"H"`, each crossed with all three size strategies (balanced/min/max). The matrix is generated (rather than hand-writing every near-duplicate entry) so that a symbology/level/size combination it's missing is one new row here, not a new block to keep in sync with its neighbors. `id` doubles as the layout's leaf `type`, since that's exactly what `QR_LEAF_TYPES` is keyed by. ### Content Kinds Registry `CONTENT_KINDS`/`CONTENT_KINDS_BY_ID` (`label-layouts.js`) is the single registry for everything that varies per content kind (currently `item`/`storage-location`): the handles-and-shortids.md kind letter (`typedPrefix`), the short-id.js schema name to use for an individually- vs. group-owned thing of that kind (`shortIdKind`/`groupShortIdKind`), the field name short-id.js's `serializeShortId` expects for its local id (`localIdField`), and a builder for the kind's long-form URL (`buildUrl`, `undefined` for a future kind added with no such route - see the `url` `DERIVED_VARS` entry). Every place that used to special-case "item vs. storage location" - `label.js`'s prefill builders, `label-layouts.js`'s `DERIVED_VARS`, Print.vue's `shortId()` - reads this one table instead, keyed by the Content card's `kind` field (a `