Package P1 of the label layout design: the scene 2 core that text (P2), barcode fit (P3a) and graphics (P4) build on. A label is now one design in millimetres, inches, points or percentages that renders at 203, 300 and 600 dpi, with anchors and alignment on every element and a refusal that names the element and the numbers.
Source of truth:docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md, sections 2, 5, 8.1, 10 and work plan B (P1). The document comes into this branch by a merge of docs/label-layout-preview-design (PR 12's one commit, unchanged), because this PR records decisions in its section 8.1. If PR 12 merges first there is nothing to resolve; if this merges first PR 12 becomes empty.
This is the example in renderer/README.md; SceneSchemaTest.theReadmeExampleIsAScene reads it from the README and renders it at the three resolutions. The schema is renderer/schema/zpl-scene-2.schema.json.
Every element has a required, unique id, a type, an anchor (nine points, default top-left), x and y (distances inward from the anchored edge; on a centred axis a shift that may be negative), width and height (omitted: the rest of the container), align and valign.
box (with thickness or fill; a line is a box whose width or height is its thickness) and group (children relative to its box, four deep) are drawn. text, barcode and image are parsed and placed and then refused by name; see "What the next packages plug into".
Coordinates start at the top left of the printable area (stock less margins, margins rounded inward), so a design cannot print in a margin.
The worker routes by schema alone (RendererMain.label): qqq-zpl-scene/1 takes exactly the old path, qqq-zpl-scene/2 the new package, anything else is SCENE_INVALID at /schema (or at the root when the source is not a JSON object).
How lengths resolve to dots
A length is a string of a number (up to three decimals) and a unit. Length turns a physical length into whole micrometres first (1 in = 25400, 1 pt = 25400/72, nearest), then into dots at the render target with integer arithmetic only:
Use
Rule
Example at 203 / 300 / 600 dpi
Position (x, y)
Nearest dot, the existing Stock.dots rule; a negative length mirrors the positive one
0.5mm is 4 / 6 / 12
Box width, height
Down, the existing BarcodeFit.dotsInside rule
11mm is 87 / 129 / 259
thickness
Nearest, never less than 1
1pt is 3 / 4 / 8; 0.1mm is 1 at 203
Barcode module (for P3a)
Up from the stated minimum
0.25mm is 2 / 3 / 6
Font size (for P2)
Not rounded: points x dpi / 72
9pt is 25.375 dots at 203
%
Of the container's width (x, width) or height (y, height), by the rule of its use
50% of 764 is 382
dot
Itself; whole dots only; outside an override it raises the warning DEVICE_UNIT_USED
The render target is the request's new optional targetDpi (203, 300 or 600), else the stock's dpi. Centring splits slack with integer division, so a centred box is off by at most one dot (LayoutTest.mirroredAnchorsGiveMirroredBoxes proves the mirror property over 20000 seeded cases).
How the label reaches the printer
Canvas holds the whole label as one 1-bit picture; elements paint in list order. ZplEmitter cuts the rows into bands wherever eight or more blank rows separate ink, trims each band to its inked columns and continues a band in a further field at 99999 bytes. Only ^XA ^PW ^LL ^LH ^CI28 ^FO ^GFA ^FS ^XZ are emitted. ZplOutputPolicy is not changed: bridges/renderer has a new ZplSceneTwoPolicyTest that runs the gate over every golden label for its stock at its target. The worst case for the number of fields (a 32000 dot label with ink on every ninth row) is 3556, against the gate's 6000 (CanvasTest.theGreatestLabelStaysInsideTheFieldLimit).
How refusals are reported
Every version 2 refusal is one diagnostic. Without "diagnostics": "COLLECT" the first one ends the work and the error answer carries it as detail; with it, each element's refusal skips that element, the outcome is REFUSED, and diagnostics lists everything with the label of the elements that had no error.
pointer; the element id when inside a declared element; numbers for a limit (elements 1001 against 1000, depth, overrides, samples, unknownProperties), and widthDots/heightDots against 1 for a box that resolves to no dots at this target
STOCK_MISMATCH
The stock differs from design by more than 0.5 mm on a side
Both sizes in micrometres and the tolerance
ELEMENT_OUTSIDE_PRINTABLE_AREA
A box leaves the printable area
The element id, its box, its four edges against the area's
TARGET_REQUIRES_SCENE_2
A version 1 scene with a targetDpi other than its stock's
Both resolutions
RENDERER_TOO_OLD
A text, barcode or image element, until its package
The element id and the box it would have had
DEVICE_UNIT_USED (warning)
A dot length outside an override
The element id and the pointer of the property
The pointer decision (asked for by the review of the foundations; recorded in section 8.1 of the design in this PR). SCENE_INVALID needed a JSON pointer and GenerationPorts.Diagnostic had no field for one. The diagnostic gains one optional pointer, on the wire and as a seventh record component (the six-argument constructor stays, so nothing else changes). Its segments are only array indexes, property names the schema declares and element ids the scene declares. An unknown property is reported at its parent with a count, never by its name. The foundation holds the pointer to that shape; the renderer checks every segment where the diagnostic is created.
No data in a diagnostic.scene2.Diagnostics is the only maker of diagnostics and knows the ids the scene declares. A diagnostic that names any other string as its element, or points through any other name, is an IllegalStateException inside the worker and reaches the host as the opaque RENDER_FAILED. SceneRoutingTest.noAnswerCarriesRecordSampleOrSourceText puts a marker in the context, the samples, an unknown property name and several property values, and finds it in no answer.
What the next packages plug into
Package
Replaces
Uses
P2 text
scene2.text.TextFactory (today a placeholder that returns UnavailableElement)
ElementFactory (properties(), create(ElementSource)), ElementSource (typed readers, invalid(name), value(name) for fit), SceneElement (place, check, paint), Layout.align for lines in the box, Length.scale(dpi) for the face, Canvas.set/fill/invert (reverse), Diagnostics.refuse/warn/inform, RenderInput.reservedAssetNames() for RESERVED_FONT_NAME
P3a barcode fit
scene2.barcode.BarcodeFactory (placeholder)
The same, with Length.module(dpi) for the minimum module and Layout.align to centre the fitted symbol
P4 graphics
scene2.graphics.GraphicsFactory (placeholder, type image); reverse on scene2.BoxElement
The same, with Canvas.invert
Later
scene2.upgrade.SceneUpgrade and AssetInspector
Routed from RendererMain.process now; both refuse UNSUPPORTED_OPERATION
Each placeholder lists the property names the design gives its type, so a misspelt property is already SCENE_INVALID; the values are not read. ElementFactory.FACTORIES names the three classes, so each package edits only its own file.
From the review of the foundations (PR 13)
Mutation campaign.MutationCampaignTest counted any exception as a refusal. Both campaigns now accept only IllegalArgumentException and exactly JsonParseException, JsonEOFException, MismatchedInputException and StreamConstraintsException; onlyDeclaredRefusalsCountAsRefusals shows a NullPointerException is a finding. The version 1 parser has no findings under the stricter rule.
New campaign over the version 2 parser, through the worker's routing, with an independent reading of what is accepted. Proved by a deliberate miss: with the unknown-property check removed it fails at once with accepted box property ….
.gitattributes: renderer/src/test/resources/compat-v1/** -text (and the same for the new goldens).
Corpus manifest pinned: both ZplSceneOneCompatibilityTest classes assert the SHA-256 of SHA256SUMS, so regenerating the outputs together with their sums fails. The corpus itself is untouched.
Declared ids only and the pointer: above.
Where the design and the code disagreed
UNSUPPORTED_OPERATION (B.1) is not a FailureCode. The worker emits it as its error code; the package that first calls INSPECT_ASSET or UPGRADE_SCENE from the host must add it.
The bridge accepts only qqq-renderer-0.1.0 (IsolatedRendererBridge). The design gives the version bump to P1 and the acceptance to P5, so between the two merges develop's bridge refuses develop's renderer. No image should be built from develop until P5 is in. No test in the build runs the real worker behind the bridge, so the build is unaffected.
SceneElement has id(), check(Frame, RenderInput, Diagnostics) and children() beside the place and paint of B.1: the scene needs the id for diagnostics, VALIDATE needs a hook that does not paint (P2's worst-case proof will use it), and a group needs its children walked.
No placeholder code existed in the vocabulary for "this renderer cannot draw this element". RENDERER_TOO_OLD is used, with the element id.
Wire.java needed no change.
The coverage gate comment says to raise the bundle rule when coverage rises. The bundle is now at 95.5% of instructions and 93.8% of lines against a gate of 94% and 91%; renderer/pom.xml belongs to P13 in this slice, so it is not edited here.
Deliberately left out
Text, barcodes, images, reverse print, rotation of content: P2, P3a, P4. rotation is validated on the three placeholder types and refused on box and group.
The warnings ELEMENTS_OVERLAP and QUIET_ZONE_INTRUDED: no package owns them and text over a filled box is a legitimate overlap.
RESERVED_FONT_NAME: the hook is in RenderInput; the refusal belongs to P2 with the bundled faces.
defaults accepts only font and size (the design's example) and nothing reads them yet.
VALIDATE checks the grammar for all three resolutions but the layout only at the request's target. A caller that wants a design proved for every printer validates once per targetDpi.
design.stock: "atLeast": a later round by the design.
A stock the printer language cannot address stays the opaque failure it is for version 1; an output over maximumBytes likewise.
targetDpi is accepted and ignored for a PDF.
Review focus (from the work plan)
Rounding rules match section 2.2 exactly:LengthTest, with the position and size rules checked against Stock.dots and BarcodeFit.dotsInside over a sweep at each resolution.
No path from context values into diagnostics:DiagnosticsTest, SceneRoutingTest.noAnswerCarriesRecordSampleOrSourceText. Nothing in the package reads input.context.
Emitted ZPL passes the unchanged ZplOutputPolicy:ZplSceneTwoPolicyTest; git diff shows no change under bridges/renderer/src/main.
Limits on elements, depth and canvas size: 1000 elements, groups four deep, 32 overrides, 16 samples, 64 million canvas dots, the existing 9999 by 32000 dot page; each asserted with its numbers in Scene2Test.
Version 1 frozen:ZplRenderer.java, RasterText.java and BarcodeFit.java are not edited; the corpus is green in both modules.
Verification
./mvnw --batch-mode --no-transfer-progress -s <settings with the QRun-IO registry> -T1C verify at the repository root (the CI command), on the head of this branch 1992fdf, with a clean tree and no gate skipped: BUILD SUCCESS (Temurin 21.0.12, Windows, Docker for Testcontainers).
Coverage: the new package com.kingsrook.qbits.renderer.scene2 and its sub-packages are at 611 of 611 lines, and AssetInspector at 2 of 2, both enforced by the build's existing 100% rules with no POM edit. The renderer bundle is at 95.5% of instructions and 93.8% of lines (gate 94% and 91%). The foundation and bridge gates are met.
Checkstyle clean in every module; Error Prone and -Werror clean.
Version 1 corpus: 42 cases byte for byte in renderer, and through the host gate in bridges/renderer, with the manifest digest pinned in both.
Mutation campaigns: both seeds at 20000 iterations in the build; both seeds at 100000 iterations run by hand (-Dfuzz.seed=20261111 and 7919, -Dfuzz.iterations=100000), no findings for either parser.
Goldens: three cases at 203, 300 and 600 dpi, each as a picture and as ZPL (18 files, 280 KB). The pictures were looked at before they were stored.
Not done here: the independent review that section 10.8 of the design asks for (an agent that did not write this), and the physical print check of section 10.6, which no agent can do.
Package **P1** of the label layout design: the scene 2 core that text (P2), barcode fit (P3a) and graphics (P4) build on. A label is now one design in millimetres, inches, points or percentages that renders at 203, 300 and 600 dpi, with anchors and alignment on every element and a refusal that names the element and the numbers.
**Source of truth:** `docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md`, sections 2, 5, 8.1, 10 and work plan B (P1). The document comes into this branch by a merge of `docs/label-layout-preview-design` (PR 12's one commit, unchanged), because this PR records decisions in its section 8.1. If PR 12 merges first there is nothing to resolve; if this merges first PR 12 becomes empty.
## What a version 2 scene looks like
```json
{
"schema": "qqq-zpl-scene/2",
"design": { "width": "4in", "height": "1in" },
"elements": [
{ "id": "frame", "type": "box", "thickness": "0.5pt" },
{ "id": "panel", "type": "group", "anchor": "center", "width": "50%", "height": "60%", "children": [
{ "id": "mark", "type": "box", "anchor": "bottom-right", "x": "1mm", "y": "1mm", "width": "6mm", "height": "3mm", "fill": true } ] },
{ "id": "rule", "type": "box", "anchor": "bottom-center", "y": "2mm", "width": "80%", "height": "0.2mm", "thickness": "0.2mm" }
],
"overrides": [ { "when": { "dpi": [203] }, "set": { "frame": { "thickness": "1dot" } } } ]
}
```
This is the example in `renderer/README.md`; `SceneSchemaTest.theReadmeExampleIsAScene` reads it from the README and renders it at the three resolutions. The schema is `renderer/schema/zpl-scene-2.schema.json`.
- Every element has a required, unique `id`, a `type`, an `anchor` (nine points, default `top-left`), `x` and `y` (distances inward from the anchored edge; on a centred axis a shift that may be negative), `width` and `height` (omitted: the rest of the container), `align` and `valign`.
- `box` (with `thickness` or `fill`; a line is a box whose width or height is its thickness) and `group` (children relative to its box, four deep) are drawn. `text`, `barcode` and `image` are parsed and placed and then refused by name; see "What the next packages plug into".
- Coordinates start at the top left of the printable area (stock less margins, margins rounded inward), so a design cannot print in a margin.
- The worker routes by `schema` alone (`RendererMain.label`): `qqq-zpl-scene/1` takes exactly the old path, `qqq-zpl-scene/2` the new package, anything else is `SCENE_INVALID` at `/schema` (or at the root when the source is not a JSON object).
## How lengths resolve to dots
A length is a string of a number (up to three decimals) and a unit. `Length` turns a physical length into whole micrometres first (1 in = 25400, 1 pt = 25400/72, nearest), then into dots at the render target with integer arithmetic only:
| Use | Rule | Example at 203 / 300 / 600 dpi |
|---|---|---|
| Position (`x`, `y`) | Nearest dot, the existing `Stock.dots` rule; a negative length mirrors the positive one | `0.5mm` is 4 / 6 / 12 |
| Box `width`, `height` | Down, the existing `BarcodeFit.dotsInside` rule | `11mm` is 87 / 129 / 259 |
| `thickness` | Nearest, never less than 1 | `1pt` is 3 / 4 / 8; `0.1mm` is 1 at 203 |
| Barcode module (for P3a) | Up from the stated minimum | `0.25mm` is 2 / 3 / 6 |
| Font size (for P2) | Not rounded: points x dpi / 72 | `9pt` is 25.375 dots at 203 |
| `%` | Of the container's width (`x`, `width`) or height (`y`, `height`), by the rule of its use | `50%` of 764 is 382 |
| `dot` | Itself; whole dots only; outside an override it raises the warning `DEVICE_UNIT_USED` | |
The render target is the request's new optional `targetDpi` (203, 300 or 600), else the stock's `dpi`. Centring splits slack with integer division, so a centred box is off by at most one dot (`LayoutTest.mirroredAnchorsGiveMirroredBoxes` proves the mirror property over 20000 seeded cases).
## How the label reaches the printer
`Canvas` holds the whole label as one 1-bit picture; elements paint in list order. `ZplEmitter` cuts the rows into bands wherever eight or more blank rows separate ink, trims each band to its inked columns and continues a band in a further field at 99999 bytes. Only `^XA ^PW ^LL ^LH ^CI28 ^FO ^GFA ^FS ^XZ` are emitted. `ZplOutputPolicy` is not changed: `bridges/renderer` has a new `ZplSceneTwoPolicyTest` that runs the gate over every golden label for its stock at its target. The worst case for the number of fields (a 32000 dot label with ink on every ninth row) is 3556, against the gate's 6000 (`CanvasTest.theGreatestLabelStaysInsideTheFieldLimit`).
## How refusals are reported
Every version 2 refusal is one diagnostic. Without `"diagnostics": "COLLECT"` the first one ends the work and the error answer carries it as `detail`; with it, each element's refusal skips that element, the outcome is `REFUSED`, and `diagnostics` lists everything with the label of the elements that had no error.
```json
{ "protocol": "qbit-document-renderer/1", "requestId": "…", "outcome": "ERROR", "rendererVersion": "qqq-renderer-0.2.0", "catalogVersion": "qqq-barcodes-9",
"code": "ELEMENT_OUTSIDE_PRINTABLE_AREA",
"detail": { "code": "ELEMENT_OUTSIDE_PRINTABLE_AREA", "severity": "ERROR", "element": "wide", "box": [719, 0, 79, 39],
"measured": { "bottomDots": 39, "leftDots": 719, "rightDots": 798, "topDots": 0 },
"limit": { "bottomDots": 155, "leftDots": 0, "rightDots": 764, "topDots": 0 } } }
```
| Code | When | Carries |
|---|---|---|
| `SCENE_INVALID` | The source is not what the schema says | `pointer`; the element id when inside a declared element; numbers for a limit (`elements` 1001 against 1000, `depth`, `overrides`, `samples`, `unknownProperties`), and `widthDots`/`heightDots` against 1 for a box that resolves to no dots at this target |
| `STOCK_MISMATCH` | The stock differs from `design` by more than 0.5 mm on a side | Both sizes in micrometres and the tolerance |
| `ELEMENT_OUTSIDE_PRINTABLE_AREA` | A box leaves the printable area | The element id, its box, its four edges against the area's |
| `TARGET_REQUIRES_SCENE_2` | A version 1 scene with a `targetDpi` other than its stock's | Both resolutions |
| `RENDERER_TOO_OLD` | A `text`, `barcode` or `image` element, until its package | The element id and the box it would have had |
| `DEVICE_UNIT_USED` (warning) | A `dot` length outside an override | The element id and the pointer of the property |
**The pointer decision** (asked for by the review of the foundations; recorded in section 8.1 of the design in this PR). `SCENE_INVALID` needed a JSON pointer and `GenerationPorts.Diagnostic` had no field for one. The diagnostic gains one optional `pointer`, on the wire and as a seventh record component (the six-argument constructor stays, so nothing else changes). Its segments are only array indexes, property names the schema declares and element ids the scene declares. An unknown property is reported at its parent with a count, never by its name. The foundation holds the pointer to that shape; the renderer checks every segment where the diagnostic is created.
**No data in a diagnostic.** `scene2.Diagnostics` is the only maker of diagnostics and knows the ids the scene declares. A diagnostic that names any other string as its element, or points through any other name, is an `IllegalStateException` inside the worker and reaches the host as the opaque `RENDER_FAILED`. `SceneRoutingTest.noAnswerCarriesRecordSampleOrSourceText` puts a marker in the context, the samples, an unknown property name and several property values, and finds it in no answer.
## What the next packages plug into
| Package | Replaces | Uses |
|---|---|---|
| P2 text | `scene2.text.TextFactory` (today a placeholder that returns `UnavailableElement`) | `ElementFactory` (`properties()`, `create(ElementSource)`), `ElementSource` (typed readers, `invalid(name)`, `value(name)` for `fit`), `SceneElement` (`place`, `check`, `paint`), `Layout.align` for lines in the box, `Length.scale(dpi)` for the face, `Canvas.set`/`fill`/`invert` (reverse), `Diagnostics.refuse`/`warn`/`inform`, `RenderInput.reservedAssetNames()` for `RESERVED_FONT_NAME` |
| P3a barcode fit | `scene2.barcode.BarcodeFactory` (placeholder) | The same, with `Length.module(dpi)` for the minimum module and `Layout.align` to centre the fitted symbol |
| P4 graphics | `scene2.graphics.GraphicsFactory` (placeholder, type `image`); reverse on `scene2.BoxElement` | The same, with `Canvas.invert` |
| Later | `scene2.upgrade.SceneUpgrade` and `AssetInspector` | Routed from `RendererMain.process` now; both refuse `UNSUPPORTED_OPERATION` |
Each placeholder lists the property names the design gives its type, so a misspelt property is already `SCENE_INVALID`; the values are not read. `ElementFactory.FACTORIES` names the three classes, so each package edits only its own file.
## From the review of the foundations (PR 13)
- **Mutation campaign.** `MutationCampaignTest` counted any exception as a refusal. Both campaigns now accept only `IllegalArgumentException` and exactly `JsonParseException`, `JsonEOFException`, `MismatchedInputException` and `StreamConstraintsException`; `onlyDeclaredRefusalsCountAsRefusals` shows a `NullPointerException` is a finding. The version 1 parser has no findings under the stricter rule.
- **New campaign** over the version 2 parser, through the worker's routing, with an independent reading of what is accepted. Proved by a deliberate miss: with the unknown-property check removed it fails at once with `accepted box property …`.
- **`.gitattributes`**: `renderer/src/test/resources/compat-v1/** -text` (and the same for the new goldens).
- **Corpus manifest pinned**: both `ZplSceneOneCompatibilityTest` classes assert the SHA-256 of `SHA256SUMS`, so regenerating the outputs together with their sums fails. The corpus itself is untouched.
- **Declared ids only** and **the pointer**: above.
## Where the design and the code disagreed
- **`UNSUPPORTED_OPERATION`** (B.1) is not a `FailureCode`. The worker emits it as its error code; the package that first calls `INSPECT_ASSET` or `UPGRADE_SCENE` from the host must add it.
- **The bridge accepts only `qqq-renderer-0.1.0`** (`IsolatedRendererBridge`). The design gives the version bump to P1 and the acceptance to P5, so between the two merges `develop`'s bridge refuses `develop`'s renderer. No image should be built from `develop` until P5 is in. No test in the build runs the real worker behind the bridge, so the build is unaffected.
- **`SceneElement`** has `id()`, `check(Frame, RenderInput, Diagnostics)` and `children()` beside the `place` and `paint` of B.1: the scene needs the id for diagnostics, `VALIDATE` needs a hook that does not paint (P2's worst-case proof will use it), and a group needs its children walked.
- **No placeholder code existed in the vocabulary** for "this renderer cannot draw this element". `RENDERER_TOO_OLD` is used, with the element id.
- **`Wire.java`** needed no change.
- **The coverage gate comment** says to raise the bundle rule when coverage rises. The bundle is now at 95.5% of instructions and 93.8% of lines against a gate of 94% and 91%; `renderer/pom.xml` belongs to P13 in this slice, so it is not edited here.
## Deliberately left out
- Text, barcodes, images, reverse print, rotation of content: P2, P3a, P4. `rotation` is validated on the three placeholder types and refused on `box` and `group`.
- The warnings `ELEMENTS_OVERLAP` and `QUIET_ZONE_INTRUDED`: no package owns them and text over a filled box is a legitimate overlap.
- `RESERVED_FONT_NAME`: the hook is in `RenderInput`; the refusal belongs to P2 with the bundled faces.
- `defaults` accepts only `font` and `size` (the design's example) and nothing reads them yet.
- `VALIDATE` checks the grammar for all three resolutions but the layout only at the request's target. A caller that wants a design proved for every printer validates once per `targetDpi`.
- `design.stock: "atLeast"`: a later round by the design.
- A stock the printer language cannot address stays the opaque failure it is for version 1; an output over `maximumBytes` likewise.
- `targetDpi` is accepted and ignored for a PDF.
## Review focus (from the work plan)
- **Rounding rules match section 2.2 exactly:** `LengthTest`, with the position and size rules checked against `Stock.dots` and `BarcodeFit.dotsInside` over a sweep at each resolution.
- **No path from context values into diagnostics:** `DiagnosticsTest`, `SceneRoutingTest.noAnswerCarriesRecordSampleOrSourceText`. Nothing in the package reads `input.context`.
- **Emitted ZPL passes the unchanged `ZplOutputPolicy`:** `ZplSceneTwoPolicyTest`; `git diff` shows no change under `bridges/renderer/src/main`.
- **Limits on elements, depth and canvas size:** 1000 elements, groups four deep, 32 overrides, 16 samples, 64 million canvas dots, the existing 9999 by 32000 dot page; each asserted with its numbers in `Scene2Test`.
- **Version 1 frozen:** `ZplRenderer.java`, `RasterText.java` and `BarcodeFit.java` are not edited; the corpus is green in both modules.
## Verification
`./mvnw --batch-mode --no-transfer-progress -s <settings with the QRun-IO registry> -T1C verify` at the repository root (the CI command), on the head of this branch `1992fdf`, with a clean tree and no gate skipped: **BUILD SUCCESS** (Temurin 21.0.12, Windows, Docker for Testcontainers).
- **732 tests, 0 failures, 0 errors, 0 skipped:** renderer 294 (was 222), foundation 255 (was 254), bridges/renderer 107 (was 98), remote-renderer transport 19, record-files 19, printing 16, OpenHTML 15, scripting 7.
- **Coverage:** the new package `com.kingsrook.qbits.renderer.scene2` and its sub-packages are at 611 of 611 lines, and `AssetInspector` at 2 of 2, both enforced by the build's existing 100% rules with no POM edit. The renderer bundle is at 95.5% of instructions and 93.8% of lines (gate 94% and 91%). The foundation and bridge gates are met.
- **Checkstyle** clean in every module; Error Prone and `-Werror` clean.
- **Version 1 corpus:** 42 cases byte for byte in `renderer`, and through the host gate in `bridges/renderer`, with the manifest digest pinned in both.
- **Mutation campaigns:** both seeds at 20000 iterations in the build; both seeds at 100000 iterations run by hand (`-Dfuzz.seed=20261111` and `7919`, `-Dfuzz.iterations=100000`), no findings for either parser.
- **Goldens:** three cases at 203, 300 and 600 dpi, each as a picture and as ZPL (18 files, 280 KB). The pictures were looked at before they were stored.
Not done here: the independent review that section 10.8 of the design asks for (an agent that did not write this), and the physical print check of section 10.6, which no agent can do.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Design and work plan only; no code changes. Covers scene version 2 beside a frozen
version 1, bundled and database-stored fonts, whole-dot barcode fitting with
centring, structured diagnostics, a preview decoded from the validated ZPL, the
release order and what GG Forge pins and deletes.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The design says SCENE_INVALID carries the JSON pointer of the offending
property, and GenerationPorts.Diagnostic had no field for one. It gains an
optional seventh component, pointer, held to the shape of at most sixteen
segments that are array indexes or names; the six-argument constructor
stays, so existing callers compile unchanged. The renderer builds a
pointer only from schema property names, indexes and declared element ids.
Section 8.1 of the design records this decision and the others made while
building the scene 2 core.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A source with schema qqq-zpl-scene/2 is a label layout in millimetres,
inches, points, percentages and dots that renders at 203, 300 or 600 dpi.
The worker routes by the schema alone: version 1 takes exactly the path it
always took (the compatibility corpus is unchanged and green), version 2
takes the new package com.kingsrook.qbits.renderer.scene2, and anything
else is refused by name.
- Length: parse, whole micrometres, dots by the rule of each use
(position nearest, box size down, thickness nearest and at least one,
module up, font size not rounded).
- Layout: nine-point anchors, offsets inward from the anchored edge,
omitted sizes, content alignment, groups four deep, percentages of the
container. Coordinates start at the top left of the printable area.
- Canvas and ZplEmitter: the whole label on one 1-bit canvas, emitted as
trimmed ^GFA bands that pass the unchanged ZplOutputPolicy.
- Diagnostics: every refusal is one diagnostic with a code, the element
id, the box and named integers; SCENE_INVALID carries a JSON pointer.
A diagnostic names only an id the scene declares and points only
through schema names, indexes and declared ids.
- box and group elements; overrides by resolution, checked for all three
resolutions; samples accepted and ignored. Text, barcodes and images
are parsed and placed and then refused as RENDERER_TOO_OLD by
placeholder factories in their own sub-packages.
- Renderer 0.2.0: optional targetDpi and "diagnostics": "COLLECT" on the
request, diagnostics on the answer, detail on a version 2 error, the
REFUSED outcome, and INSPECT_ASSET and UPGRADE_SCENE routed and refused
as UNSUPPORTED_OPERATION. A version 1 scene with a target other than
its stock's dpi is refused as TARGET_REQUIRES_SCENE_2.
Tests: every length and rounding rule; all nine anchors; seeded property
tests (mirrored anchors, boxes never leave the printable area, the bitmap
rebuilt from the emitted fields equals the canvas); goldens at three
resolutions as pictures and as ZPL, the ZPL also checked against the host
gate in bridges/renderer; every refusal asserted as the whole diagnostic;
a mutation campaign over the version 2 parser. The new package is at 100%
line coverage.
Also, from the review of the foundations: the mutation campaigns accept
only declared refusals (a NullPointerException in a parser is now a
finding), git no longer treats the stored corpus as text, and both
compatibility tests pin the digest of the corpus manifest.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A scene may hold 1000 elements and a canvas 64 million dots, so a scene of
many large filled boxes visited tens of billions of dots one at a time.
Filling and reversing a box now write whole bytes, check the box against
the canvas before any dot changes (a box that leaves the canvas paints
nothing rather than its first rows), and a seeded property test holds the
result to painting each dot, at byte boundaries as everywhere else. The
goldens are unchanged.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Package P1 of the label layout design: the scene 2 core that text (P2), barcode fit (P3a) and graphics (P4) build on. A label is now one design in millimetres, inches, points or percentages that renders at 203, 300 and 600 dpi, with anchors and alignment on every element and a refusal that names the element and the numbers.
Source of truth:
docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md, sections 2, 5, 8.1, 10 and work plan B (P1). The document comes into this branch by a merge ofdocs/label-layout-preview-design(PR 12's one commit, unchanged), because this PR records decisions in its section 8.1. If PR 12 merges first there is nothing to resolve; if this merges first PR 12 becomes empty.What a version 2 scene looks like
This is the example in
renderer/README.md;SceneSchemaTest.theReadmeExampleIsAScenereads it from the README and renders it at the three resolutions. The schema isrenderer/schema/zpl-scene-2.schema.json.id, atype, ananchor(nine points, defaulttop-left),xandy(distances inward from the anchored edge; on a centred axis a shift that may be negative),widthandheight(omitted: the rest of the container),alignandvalign.box(withthicknessorfill; a line is a box whose width or height is its thickness) andgroup(children relative to its box, four deep) are drawn.text,barcodeandimageare parsed and placed and then refused by name; see "What the next packages plug into".schemaalone (RendererMain.label):qqq-zpl-scene/1takes exactly the old path,qqq-zpl-scene/2the new package, anything else isSCENE_INVALIDat/schema(or at the root when the source is not a JSON object).How lengths resolve to dots
A length is a string of a number (up to three decimals) and a unit.
Lengthturns a physical length into whole micrometres first (1 in = 25400, 1 pt = 25400/72, nearest), then into dots at the render target with integer arithmetic only:x,y)Stock.dotsrule; a negative length mirrors the positive one0.5mmis 4 / 6 / 12width,heightBarcodeFit.dotsInsiderule11mmis 87 / 129 / 259thickness1ptis 3 / 4 / 8;0.1mmis 1 at 2030.25mmis 2 / 3 / 69ptis 25.375 dots at 203%x,width) or height (y,height), by the rule of its use50%of 764 is 382dotDEVICE_UNIT_USEDThe render target is the request's new optional
targetDpi(203, 300 or 600), else the stock'sdpi. Centring splits slack with integer division, so a centred box is off by at most one dot (LayoutTest.mirroredAnchorsGiveMirroredBoxesproves the mirror property over 20000 seeded cases).How the label reaches the printer
Canvasholds the whole label as one 1-bit picture; elements paint in list order.ZplEmittercuts the rows into bands wherever eight or more blank rows separate ink, trims each band to its inked columns and continues a band in a further field at 99999 bytes. Only^XA ^PW ^LL ^LH ^CI28 ^FO ^GFA ^FS ^XZare emitted.ZplOutputPolicyis not changed:bridges/rendererhas a newZplSceneTwoPolicyTestthat runs the gate over every golden label for its stock at its target. The worst case for the number of fields (a 32000 dot label with ink on every ninth row) is 3556, against the gate's 6000 (CanvasTest.theGreatestLabelStaysInsideTheFieldLimit).How refusals are reported
Every version 2 refusal is one diagnostic. Without
"diagnostics": "COLLECT"the first one ends the work and the error answer carries it asdetail; with it, each element's refusal skips that element, the outcome isREFUSED, anddiagnosticslists everything with the label of the elements that had no error.SCENE_INVALIDpointer; the element id when inside a declared element; numbers for a limit (elements1001 against 1000,depth,overrides,samples,unknownProperties), andwidthDots/heightDotsagainst 1 for a box that resolves to no dots at this targetSTOCK_MISMATCHdesignby more than 0.5 mm on a sideELEMENT_OUTSIDE_PRINTABLE_AREATARGET_REQUIRES_SCENE_2targetDpiother than its stock'sRENDERER_TOO_OLDtext,barcodeorimageelement, until its packageDEVICE_UNIT_USED(warning)dotlength outside an overrideThe pointer decision (asked for by the review of the foundations; recorded in section 8.1 of the design in this PR).
SCENE_INVALIDneeded a JSON pointer andGenerationPorts.Diagnostichad no field for one. The diagnostic gains one optionalpointer, on the wire and as a seventh record component (the six-argument constructor stays, so nothing else changes). Its segments are only array indexes, property names the schema declares and element ids the scene declares. An unknown property is reported at its parent with a count, never by its name. The foundation holds the pointer to that shape; the renderer checks every segment where the diagnostic is created.No data in a diagnostic.
scene2.Diagnosticsis the only maker of diagnostics and knows the ids the scene declares. A diagnostic that names any other string as its element, or points through any other name, is anIllegalStateExceptioninside the worker and reaches the host as the opaqueRENDER_FAILED.SceneRoutingTest.noAnswerCarriesRecordSampleOrSourceTextputs a marker in the context, the samples, an unknown property name and several property values, and finds it in no answer.What the next packages plug into
scene2.text.TextFactory(today a placeholder that returnsUnavailableElement)ElementFactory(properties(),create(ElementSource)),ElementSource(typed readers,invalid(name),value(name)forfit),SceneElement(place,check,paint),Layout.alignfor lines in the box,Length.scale(dpi)for the face,Canvas.set/fill/invert(reverse),Diagnostics.refuse/warn/inform,RenderInput.reservedAssetNames()forRESERVED_FONT_NAMEscene2.barcode.BarcodeFactory(placeholder)Length.module(dpi)for the minimum module andLayout.alignto centre the fitted symbolscene2.graphics.GraphicsFactory(placeholder, typeimage); reverse onscene2.BoxElementCanvas.invertscene2.upgrade.SceneUpgradeandAssetInspectorRendererMain.processnow; both refuseUNSUPPORTED_OPERATIONEach placeholder lists the property names the design gives its type, so a misspelt property is already
SCENE_INVALID; the values are not read.ElementFactory.FACTORIESnames the three classes, so each package edits only its own file.From the review of the foundations (PR 13)
MutationCampaignTestcounted any exception as a refusal. Both campaigns now accept onlyIllegalArgumentExceptionand exactlyJsonParseException,JsonEOFException,MismatchedInputExceptionandStreamConstraintsException;onlyDeclaredRefusalsCountAsRefusalsshows aNullPointerExceptionis a finding. The version 1 parser has no findings under the stricter rule.accepted box property …..gitattributes:renderer/src/test/resources/compat-v1/** -text(and the same for the new goldens).ZplSceneOneCompatibilityTestclasses assert the SHA-256 ofSHA256SUMS, so regenerating the outputs together with their sums fails. The corpus itself is untouched.Where the design and the code disagreed
UNSUPPORTED_OPERATION(B.1) is not aFailureCode. The worker emits it as its error code; the package that first callsINSPECT_ASSETorUPGRADE_SCENEfrom the host must add it.qqq-renderer-0.1.0(IsolatedRendererBridge). The design gives the version bump to P1 and the acceptance to P5, so between the two mergesdevelop's bridge refusesdevelop's renderer. No image should be built fromdevelopuntil P5 is in. No test in the build runs the real worker behind the bridge, so the build is unaffected.SceneElementhasid(),check(Frame, RenderInput, Diagnostics)andchildren()beside theplaceandpaintof B.1: the scene needs the id for diagnostics,VALIDATEneeds a hook that does not paint (P2's worst-case proof will use it), and a group needs its children walked.RENDERER_TOO_OLDis used, with the element id.Wire.javaneeded no change.renderer/pom.xmlbelongs to P13 in this slice, so it is not edited here.Deliberately left out
rotationis validated on the three placeholder types and refused onboxandgroup.ELEMENTS_OVERLAPandQUIET_ZONE_INTRUDED: no package owns them and text over a filled box is a legitimate overlap.RESERVED_FONT_NAME: the hook is inRenderInput; the refusal belongs to P2 with the bundled faces.defaultsaccepts onlyfontandsize(the design's example) and nothing reads them yet.VALIDATEchecks the grammar for all three resolutions but the layout only at the request's target. A caller that wants a design proved for every printer validates once pertargetDpi.design.stock: "atLeast": a later round by the design.maximumByteslikewise.targetDpiis accepted and ignored for a PDF.Review focus (from the work plan)
LengthTest, with the position and size rules checked againstStock.dotsandBarcodeFit.dotsInsideover a sweep at each resolution.DiagnosticsTest,SceneRoutingTest.noAnswerCarriesRecordSampleOrSourceText. Nothing in the package readsinput.context.ZplOutputPolicy:ZplSceneTwoPolicyTest;git diffshows no change underbridges/renderer/src/main.Scene2Test.ZplRenderer.java,RasterText.javaandBarcodeFit.javaare not edited; the corpus is green in both modules.Verification
./mvnw --batch-mode --no-transfer-progress -s <settings with the QRun-IO registry> -T1C verifyat the repository root (the CI command), on the head of this branch1992fdf, with a clean tree and no gate skipped: BUILD SUCCESS (Temurin 21.0.12, Windows, Docker for Testcontainers).com.kingsrook.qbits.renderer.scene2and its sub-packages are at 611 of 611 lines, andAssetInspectorat 2 of 2, both enforced by the build's existing 100% rules with no POM edit. The renderer bundle is at 95.5% of instructions and 93.8% of lines (gate 94% and 91%). The foundation and bridge gates are met.-Werrorclean.renderer, and through the host gate inbridges/renderer, with the manifest digest pinned in both.-Dfuzz.seed=20261111and7919,-Dfuzz.iterations=100000), no findings for either parser.Not done here: the independent review that section 10.8 of the design asks for (an agent that did not write this), and the physical print check of section 10.6, which no agent can do.
🤖 Generated with Claude Code
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.