Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22063ccfb1
|
@@ -0,0 +1,973 @@
|
||||
# Design: great labels, barcodes, page layout and preview
|
||||
|
||||
Status: draft for review, 2026-11-11. No code in this change. Owner decisions are in section 12.
|
||||
|
||||
This document designs four things the document module lacks and the preview James asked for, then gives a work
|
||||
plan that separate agents can build from. It covers `qbit-document-generation` (foundation, renderer and bridges)
|
||||
and says what the neighbouring repositories and GG Forge must do.
|
||||
|
||||
1. Layouts in millimetres, inches or points, so one design serves 203, 300 and 600 dpi.
|
||||
2. Alignment and fit rules for text and barcodes, so a long value shrinks or wraps by a stated rule.
|
||||
3. A preview inside the QBit that shows the exact bitmap before anything prints.
|
||||
4. Fonts that work in every environment: a bundled default set, plus uploads that need no object storage.
|
||||
|
||||
## 0. What exists today
|
||||
|
||||
Every statement here was read in code on 2026-11-11. Paths are relative to this repository unless a repository is
|
||||
named. "Not verified" marks what was not confirmed. Facts about other repositories (Forge, printing, the print
|
||||
agent, the scanner QBit, Record Files, the frontend and QQQ core) come from read-only surveys of those repositories
|
||||
made for this task, read at the versions Forge pins where that matters.
|
||||
|
||||
| Area | Today | Where |
|
||||
|---|---|---|
|
||||
| Label scene | `qqq-zpl-scene/1`, a JSON list of `text`, `rasterText`, `box`, `image`, `barcode`. Every coordinate and size is an integer printer dot. Stock dpi must be 203, 300 or 600. | `renderer/.../ZplRenderer.java` |
|
||||
| Printer font text | `text` emits `^A0N` (printer font 0). The renderer cannot measure that font, so it refuses when `x + characters * width` passes the label width. | `ZplRenderer.java` `render`, case `text` |
|
||||
| TrueType text | `rasterText` draws one supplied TrueType face into a dot box. It already wraps and already has `align` left, center, right. It never shrinks. Overflow is `RASTER_TEXT_DOES_NOT_FIT`. | `renderer/.../RasterText.java` |
|
||||
| Barcode | 71 catalogue ids. A `fit` box in microns picks the largest whole-dot module for 13 symbologies; the raster sits at the box origin (no centring). Linear bar height scales with the module. No human-readable line. | `BarcodeFit.java`, `Barcodes.java`, `renderer/BARCODE-FIT.md` |
|
||||
| Fonts | None bundled in the renderer. A font is a template asset whose bytes the host must supply on every render. | `RenderInput.java`, `bridges/renderer/.../IsolatedRendererBridge.java` `resolve` |
|
||||
| Asset bytes | The foundation stores an asset row only (name, file version id, hash). The only shipped reader is Record Files, whose upload path is a Collaboration attachment and needs a storage bucket. | `GenerationTables.java`, `bridges/record-files/...`, `qbit-record-files` |
|
||||
| Refusals | Apart from 13 barcode fit codes, every renderer failure is the single code `RENDER_FAILED`, by design, so that no record data leaks. | `RendererMain.java` `errorCode` |
|
||||
| Output check | The host re-parses the ZPL and allows only `^A0N` text, `^GB` and uncompressed `^GFA`. | `bridges/renderer/.../ZplOutputPolicy.java` |
|
||||
| Preview | "Preview draft" generates a retained, audited file from a real record. A PDF opens in the shared viewer. A ZPL file cannot be shown as an image. | `GenerationAuthoringProcesses.java`, `DocumentGenerationService.preview`, `qqq-frontend-next` `attachment-download.ts` |
|
||||
| Stock and dpi | A template has one stock; the stock carries the dpi. A revision copies both. | `GenerationTables.java` |
|
||||
| Page documents | FreeMarker HTML with paged CSS 2.1 through a patched OpenHTML to PDF; TrueType assets; one starter (packing slip). | `PdfRenderer.java`, `renderer/README.md` |
|
||||
| Coverage gate | JaCoCo 85% instructions and 90% classes in `foundation` only. `renderer` and the bridges have no gate. | module `pom.xml` files |
|
||||
| Release | Shared Gitea workflow: `develop` publishes a SNAPSHOT, `release/X.Y` publishes `X.Y.0-RC.N` and tags it. `develop` is `1.4.0-SNAPSHOT`. Renderer images are built by hand from a released version. | `.gitea/workflows/build.yaml`, `renderer/IMAGE-PUBLISH.md` |
|
||||
|
||||
How GG Forge uses it today (read from Forge `origin/develop`):
|
||||
|
||||
- Forge pins the document generation family at `1.1.0-RC.2` and serves `qqq-frontend-next` `1.0.0-RC.41`.
|
||||
- Forge renders through `RemoteRendererBridge`, so `ZplOutputPolicy` applies to every label.
|
||||
- `EchoTemplatePublisher` computes all geometry by hand (microns to dots, insets, a module search) and Forge seeds
|
||||
ten documents: the same designs repeated for 4 x 1 in at 203 and 300 dpi and 3 x 2 in at 203 dpi.
|
||||
- `EchoPrintingDestinations.resolve` offers a document only when the revision's stock width, height and dpi equal
|
||||
the media profile the print agent reports from the Windows queue default. When nothing matches, the document
|
||||
list is empty and no reason is given. This filter is Forge code, not scanner or printing QBit code.
|
||||
- `EchoBundledFonts` answers one font from Forge's jar because no environment could hold an uploaded font, and a
|
||||
Liquibase changeset cannot put bytes in a bucket.
|
||||
|
||||
The printing QBit's built-in test label (`qbit-printing` `PrintingTestDocuments`) is the quality bar: it is drawn in
|
||||
points on a Java canvas, scaled by `dpi / 72`, with a bundled bold TrueType face, and sent as whole-label `^GFA`
|
||||
strips plus a native `^BC` barcode.
|
||||
|
||||
## 1. Goals and non-goals
|
||||
|
||||
### What a person designing a label experiences
|
||||
|
||||
- They write sizes in millimetres, inches or points. They never compute dots.
|
||||
- They say "centre this in that box" for any element, and it is centred for every value and every dpi.
|
||||
- They pick a font by name from a small set that is always there, or upload their own once.
|
||||
- They state what happens to a long value: shrink to a floor, wrap to a number of lines, cut with a visible mark,
|
||||
or refuse. The default never changes data silently.
|
||||
- They open the template and see the label. They change the layout and see it again in seconds, at 203, 300 or
|
||||
600 dpi, with sample values or a real record, without printing.
|
||||
- Beside the picture they read plain warnings: text was shrunk, a barcode is at its smallest safe module, a field
|
||||
would not fit its longest allowed value.
|
||||
- Publishing proves the layout fits the longest value each field declares, so a published label is not refused
|
||||
for length at print time.
|
||||
|
||||
### What a person printing a label experiences
|
||||
|
||||
- The label prints at the printer's own resolution. Nobody creates a 203 dpi copy and a 300 dpi copy.
|
||||
- When a label cannot print, the message names the field, what it needed and what it had.
|
||||
- When a document is not offered for a printer, the screen says why (the stock size differs) instead of showing
|
||||
an empty list.
|
||||
|
||||
### Non-goals in this round
|
||||
|
||||
- A drag-and-drop visual designer. The editor stays source first; the preview makes that workable.
|
||||
- Raw ZPL templates or any printer command escape hatch.
|
||||
- Printer languages other than ZPL (EPL, TSPL), colour, RFID.
|
||||
- Changing how existing `qqq-zpl-scene/1` documents print.
|
||||
- GS1 conformance grading or certification. The design gives rules and tests, not a verifier.
|
||||
|
||||
## 2. Layout model
|
||||
|
||||
### 2.1 Scene version 2
|
||||
|
||||
A new schema id, `qqq-zpl-scene/2`, lives in the same `zplSource` field. The renderer chooses the code path from
|
||||
the `schema` value alone.
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "qqq-zpl-scene/2",
|
||||
"design": { "width": "4in", "height": "1in" },
|
||||
"defaults": { "font": "qqq-sans-bold", "size": "9pt" },
|
||||
"samples": {
|
||||
"typical": { "record": { "humanReadable": "(01)00012345678905(10)L240611A", "rendererValue": "[01]00012345678905[10]L240611A" } },
|
||||
"longest": { "record": { "humanReadable": "(01)00012345678905(10)L240611A-RETURNED-000123", "rendererValue": "[01]00012345678905[10]L240611A-RETURNED-000123" } }
|
||||
},
|
||||
"elements": [
|
||||
{ "id": "code", "type": "barcode", "symbology": "gs1-128", "valuePath": "/record/rendererValue",
|
||||
"anchor": "top-center", "y": "0.5mm", "width": "94mm", "height": "11mm",
|
||||
"align": "center", "valign": "middle", "minModule": "0.25mm", "quiet": 10 },
|
||||
{ "id": "caption", "type": "text", "valuePath": "/record/humanReadable",
|
||||
"anchor": "bottom-center", "y": "0.5mm", "width": "94mm", "height": "6.5mm",
|
||||
"align": "center", "valign": "middle", "size": "11pt", "maxLength": 48,
|
||||
"fit": { "maxLines": 2, "shrinkTo": "7pt", "overflow": "refuse" } }
|
||||
],
|
||||
"overrides": [
|
||||
{ "when": { "dpi": [203] }, "set": { "caption": { "threshold": 144 } } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The example assumes 3 mm margins on 4 x 1 in stock, a printable area of 95.6 x 19.4 mm.
|
||||
|
||||
### 2.2 Units
|
||||
|
||||
Every length is a string made of a number and a unit: `"12.5mm"`, `"0.5in"`, `"9pt"`, `"25%"`, `"1dot"`.
|
||||
|
||||
| Unit | Meaning | Use |
|
||||
|---|---|---|
|
||||
| `mm`, `in` | Physical length | Positions and sizes |
|
||||
| `pt` | 1/72 inch | Font sizes, line thickness |
|
||||
| `%` | Share of the container's width (for `x`, `width`) or height (for `y`, `height`) | Layouts that adapt to the stock |
|
||||
| `dot` | One printer dot | Only where a device unit is meant (a hairline). Outside an override it raises the warning `DEVICE_UNIT_USED`. |
|
||||
|
||||
One rule, no ambient unit setting, so a value read out of context is never ambiguous. Up to three decimals are
|
||||
accepted. Lengths convert to integer micrometres first (the unit the stock already uses), then to dots:
|
||||
|
||||
| Quantity | Rounding | Reason |
|
||||
|---|---|---|
|
||||
| Position | Nearest dot, the existing `Stock.dots` rule | Symmetric error |
|
||||
| Box width and height | Down, the existing `BarcodeFit.dotsInside` rule | Content never leaves its designed box |
|
||||
| Line and box thickness | Nearest, never less than 1 dot | A thin rule must not vanish |
|
||||
| Barcode module | Up to a whole dot from the stated minimum (section 4) | Bars must be whole dots |
|
||||
| Font size | Not rounded; the face is scaled by `points * dpi / 72` | Type scales continuously |
|
||||
|
||||
### 2.3 Stock, printable area and render target
|
||||
|
||||
- Coordinates start at the top left of the printable area: the stock less its margins. A design therefore never
|
||||
prints in the margin.
|
||||
- `design.width` and `design.height` state the stock the layout was drawn for. At render time the stock must match
|
||||
within 0.5 mm on each side, otherwise the refusal `STOCK_MISMATCH` reports both sizes. (Exact micrometre
|
||||
equality, which Forge's filter uses today, fails on driver rounding such as 101.5 against 101.6 mm.)
|
||||
- Resolution stops being a property of the document. A version 2 revision renders at a **render target** dpi
|
||||
supplied by the caller: the printer's reported resolution when printing, the chosen value when previewing. The
|
||||
stock row's `dpi` remains and becomes the default target and the only target for version 1 scenes.
|
||||
- `DocumentGenerationService.Request` gains an optional `targetDpi` (203, 300 or 600). It is recorded on the
|
||||
request row in a new nullable column and framed into the intent digest when present, following the pattern used
|
||||
for the media profile (`docs/EXPECTED-PRINT-INTENT.md`). The renderer refuses a target that differs from the
|
||||
stock's dpi for a version 1 scene (`TARGET_REQUIRES_SCENE_2`), because only the renderer reads the scene.
|
||||
- Later round: `design.stock: "atLeast"` lets one design print on a larger stock, with anchors and percentages
|
||||
resolving against the actual printable area. The first round supports `exact` only.
|
||||
|
||||
### 2.4 Anchors and alignment
|
||||
|
||||
Two separate ideas, both available on every element.
|
||||
|
||||
**Where the box sits (`anchor`, `x`, `y`, `width`, `height`).** `anchor` names one of nine points:
|
||||
`top-left` (default), `top-center`, `top-right`, `middle-left`, `center`, `middle-right`, `bottom-left`,
|
||||
`bottom-center`, `bottom-right`. The same point of the element's box is placed on that point of its container.
|
||||
`x` and `y` are then distances inward from the anchored edge; on a centred axis they shift right or down and may be
|
||||
negative. An omitted `width` or `height` means the rest of the container on that axis.
|
||||
|
||||
```
|
||||
container (printable area or group)
|
||||
+--------------------------------------------------+
|
||||
| anchor: top-center, y: 1mm |
|
||||
| +------------------------------+ |
|
||||
| | box (width x height) | |
|
||||
| | content placed by | |
|
||||
| | align / valign | |
|
||||
| +------------------------------+ |
|
||||
| |
|
||||
| anchor: bottom-left anchor: bottom-right
|
||||
| x: 2mm, y: 1mm x: 2mm, y: 1mm |
|
||||
| +--------+ +--------+ |
|
||||
+--------------------------------------------------+
|
||||
```
|
||||
|
||||
**Where the content sits in the box (`align`, `valign`).** `align` is `left`, `center` or `right`; `valign` is `top`,
|
||||
`middle` or `bottom`. They apply to text lines, to a fitted barcode whose size depends on its data, and to a scaled
|
||||
image. Slack is split with integer division, so a centred element is off by at most one dot.
|
||||
|
||||
`rotation` is 0, 90, 180 or 270 degrees clockwise for text, barcodes and images. The box is given as it appears on
|
||||
the label, after rotation.
|
||||
|
||||
### 2.5 Containers
|
||||
|
||||
`group` is in: a box with its own `anchor`, position and size, whose children are positioned relative to it. It
|
||||
lets a block move as a unit and gives percentages something smaller than the label to refer to. Groups nest to a
|
||||
depth of four.
|
||||
|
||||
Rows and columns that distribute children automatically (`stack`, tables) are out of this round. No label in hand
|
||||
needs them, and they can be added to version 2 later without breaking a scene that does not use them.
|
||||
|
||||
### 2.6 One design, several resolutions
|
||||
|
||||
A scene has no dots in it, so the same revision renders at any target. Small physical differences between
|
||||
resolutions are expected (a 0.25 mm module is 2 dots at 203 dpi and 3 dots, 0.254 mm, at 300 dpi) and the
|
||||
diagnostics report the actual values.
|
||||
|
||||
`overrides` is the only place a scene may depend on resolution. An entry has `when` (`dpi`: a list) and `set` (a
|
||||
map from element id to replacement properties; `id` and `type` cannot change). Legitimate uses:
|
||||
|
||||
- a heavier face or a different `threshold` for small type at 203 dpi;
|
||||
- a larger minimum type size at 203 dpi;
|
||||
- a logo asset prepared for one resolution;
|
||||
- a `dot` thickness for a hairline.
|
||||
|
||||
An override is not for repositioning the layout per printer. If a design needs that, it is two designs.
|
||||
|
||||
### 2.7 Versions and upgrade
|
||||
|
||||
- **Version 1 is frozen.** A `qqq-zpl-scene/1` source keeps its existing code path and its exact output bytes. A
|
||||
test corpus pins this (section 10). No new field is added to version 1, including `align` on a barcode, because
|
||||
published scenes already carry that word and would start printing differently.
|
||||
- **Version 2 is a separate package** in the renderer. The two share only the barcode encoders and the font
|
||||
rasteriser.
|
||||
- **Upgrade is an explicit act by an author, never automatic.** A new authoring action, "Convert layout to physical
|
||||
units", asks the renderer for a version 2 rendering of the draft's version 1 scene (new operation
|
||||
`UPGRADE_SCENE`, a pure function that returns the new source and a list of notes). It converts dots to
|
||||
millimetres with the template stock's dpi, turns `rasterText` into `text` with the same asset, turns a fitted
|
||||
barcode into a barcode box, and turns printer font `text` into `text` in the default bundled face with a note
|
||||
that the face changed. The action shows the two previews side by side and saves the result as the draft only.
|
||||
Publishing stays a separate human step. Nothing rewrites a stored revision.
|
||||
- A renderer that does not know version 2 answers a version 2 scene with a refusal, and the host reports
|
||||
`RENDERER_TOO_OLD` (section 11.3).
|
||||
|
||||
## 3. Text
|
||||
|
||||
### 3.1 Fonts
|
||||
|
||||
Version 2 has one kind of text: a TrueType face rasterised by the renderer. Printer font 0 is not offered, because
|
||||
the renderer cannot measure it or draw it, so it can be neither fitted nor previewed truthfully. Version 1 keeps
|
||||
its `text` element.
|
||||
|
||||
| Source | How it is named in a scene | Where the bytes live | Works without object storage |
|
||||
|---|---|---|---|
|
||||
| Bundled default set | `qqq-sans`, `qqq-sans-bold`, `qqq-condensed-bold`, `qqq-mono`, `qqq-mono-bold` | Inside the renderer jar and image, with licence files and hashes | Yes |
|
||||
| Built-in asset store | The asset's name in the template manifest | A new table owned by this QBit, bytes in the database (section 3.2) | Yes |
|
||||
| Record Files (as today) | The asset's name in the template manifest | Collaboration attachment storage | No |
|
||||
|
||||
The prefix `qqq-` is reserved: a version 2 scene whose manifest has an asset with that prefix is refused with
|
||||
`RESERVED_FONT_NAME`.
|
||||
|
||||
**The bundled set.** Five faces: a proportional sans in regular and bold, a condensed bold for long values on
|
||||
narrow labels, and a monospace in regular and bold for codes and human-readable lines. Selection criteria: real
|
||||
bold and condensed cuts (stroke weight comes from the face, not from a threshold; Forge's Abel needed
|
||||
`threshold` 220 to print dark enough), clear `0/O` and `1/l/I`, Latin Extended coverage, static TrueType outlines
|
||||
(the renderer accepts only `font/ttf`), a licence that permits bundling. Proposed: IBM Plex Sans (Regular, Bold),
|
||||
IBM Plex Sans Condensed (Bold) and JetBrains Mono (Regular, Bold). JetBrains Mono Bold already ships in
|
||||
`qbit-printing` and its OFL text was read there; the IBM Plex licence was not verified in this task and must be
|
||||
confirmed from the licence file of the exact release taken. Each face ships with its licence text, a provenance
|
||||
note and a SHA-256, and a test fails the build if any of the three is missing. Licence handling is decision 1 in
|
||||
section 12.
|
||||
|
||||
The same faces are also registered as CSS families for page documents (section 7).
|
||||
|
||||
### 3.2 Uploaded fonts in every environment
|
||||
|
||||
Today an uploaded font needs Record Files, Collaboration attachments and a bucket, and it cannot be seeded by a
|
||||
database migration. The design adds a store that needs only the database the QBit already has.
|
||||
|
||||
- **Table** `document_generation_asset_content`: `sha256` (unique), `media_type`, `length`, `bytes`, `file_name`,
|
||||
`rights_statement`, `created_by`, `create_date`. Limit 2 MiB per asset, the renderer's existing bound. Shipped as
|
||||
migration scripts for PostgreSQL, MySQL and H2 beside the existing ones; the host applies them (Forge writes its
|
||||
own changeset).
|
||||
- **Reference.** An asset row points at it with `fileVersionId = "dg:<sha256>"`. The bridge resolves that prefix
|
||||
from the built-in store itself and passes every other id to the host's reader as today. No host code is needed.
|
||||
- **Upload.** A new authoring action, "Upload font or image", takes a file through the existing `FILE_UPLOAD` field
|
||||
adornment, a name, and a required rights statement. Permission: `document-generation.configure`.
|
||||
- **Checks.** The host checks size and leading bytes only. The file is then sent to the isolated renderer
|
||||
(new operation `INSPECT_ASSET`), which parses it and returns family names, glyph count and the embedding
|
||||
permission bits. The application JVM never parses a font or an image. A font whose embedding bits forbid
|
||||
embedding is refused.
|
||||
- **Seeding.** Because the bytes are a database row, a host can seed a logo or a brand font with a migration, which
|
||||
is how Forge installs its setup data.
|
||||
- **When Record Files is absent** nothing is lost: selection lists the built-in store and the bundled faces. When
|
||||
it is present its released versions appear in the same list, as today.
|
||||
|
||||
This moves a boundary: the README says the QBit "does not own retained file bytes". That stays true for generated
|
||||
output. Template assets are definition data, like the scene source. This is decision 2 in section 12.
|
||||
|
||||
### 3.3 Size, fit and legibility
|
||||
|
||||
`size` is the em size as a length (`"9pt"`, `"3mm"`). `lineHeight` is a multiple of the size (default from the face
|
||||
metrics).
|
||||
|
||||
`fit` states what happens when the value does not fit the box:
|
||||
|
||||
| Property | Meaning | Default |
|
||||
|---|---|---|
|
||||
| `maxLines` | Wrap to at most this many lines | 1 |
|
||||
| `shrinkTo` | Smallest size the text may be reduced to | 70% of `size`, not below 5 pt |
|
||||
| `overflow` | `refuse` or `truncate`, applied when it still does not fit at the floor | `refuse` |
|
||||
|
||||
The order is fixed and stated: lay out at the nominal size within `maxLines`; if it does not fit, find the largest
|
||||
size down to the floor (0.25 pt steps) that fits within `maxLines`; if none fits, apply `overflow`. `truncate` cuts
|
||||
at a character boundary, appends an ellipsis and raises the warning `TEXT_TRUNCATED`. It is opt-in per element
|
||||
because a shortened identifier on a warehouse label can be misread; the default keeps the label from printing
|
||||
something different from the data.
|
||||
|
||||
`maxLength` (characters) declares the longest value a bound element must hold. Validation at publish lays out a
|
||||
worst-case string of that length (the widest glyph of the face, repeated) and refuses publication if it does not
|
||||
fit. At print time a value longer than `maxLength` is refused with `VALUE_TOO_LONG` before layout. Together these
|
||||
make a length refusal a design-time event. An element without `maxLength` raises the warning `FIT_NOT_PROVEN` at
|
||||
preview and publish.
|
||||
|
||||
Legibility on thermal heads:
|
||||
|
||||
- Glyphs are drawn antialiased and reduced to black and white at `threshold` (1 to 254, default 128). A higher
|
||||
value prints heavier. The default is not changed per resolution; an override may.
|
||||
- The renderer warns `TEXT_SMALL_FOR_DPI` when the capital height at the target is under 12 dots (about 6 pt at
|
||||
203 dpi), and `TEXT_LIGHT_FACE` when a regular-weight face is used with a capital height under 20 dots (about
|
||||
10 pt at 203 dpi). The numbers are starting values to be confirmed on the specimen label (section 10.6).
|
||||
- Guidance for the authoring guide: at 203 dpi use a bold face below 10 pt, keep text at or above 6 pt, prefer the
|
||||
condensed bold over shrinking.
|
||||
|
||||
## 4. Barcodes
|
||||
|
||||
### 4.1 Symbologies
|
||||
|
||||
Supported today: 71 ids (the OkapiBarcode families, four through Zint, Code 93i, Co-op 2 of 5). Fit boxes are
|
||||
qualified for 13: `code128`, `code39`, `code93`, `itf`, `gs1-128`, `qr`, `ean13`, `ean8`, `upca`, `upce`,
|
||||
`datamatrix`, `pdf417`, `aztec`.
|
||||
|
||||
Version 2 keeps the catalogue and the dynamic symbology allowlist. To add, in this order:
|
||||
|
||||
1. GS1 Data Matrix and GS1 QR as fit-qualified ids (`gs1-datamatrix`, `gs1-qr`). The encoders already accept a GS1
|
||||
data type as an option, but the fit profile admits default options only.
|
||||
2. SSCC-18 (`nve18` exists in the catalogue) and ITF-14 with bearer bars as fit-qualified ids.
|
||||
3. GS1 DataBar family fit qualification, when a label needs it.
|
||||
|
||||
### 4.2 GS1 application identifiers and the human-readable line
|
||||
|
||||
Today a host builds the bracketed element string and the caption itself (Forge's `EchoGs1`). Version 2 adds:
|
||||
|
||||
- `gs1`: a list of `{ "ai": "01", "valuePath": "/record/gtin" }` entries in place of `value`. The renderer builds
|
||||
the element string, checks each identifier's length and character set, checks the check digit of 00, 01 and 02,
|
||||
and places separators after variable-length fields. An unknown identifier or a bad value is the refusal
|
||||
`GS1_INVALID` with the identifier and a reason code. (What the encoder library already validates was not
|
||||
verified here; the builder must not duplicate it.)
|
||||
- `hri`: `{ "position": "below" | "above" | "none", "font": "qqq-mono", "size": "7pt", "fit": {...} }`. The line is
|
||||
drawn by the text engine inside the barcode element's box, so bars and caption are centred as one unit. For GS1
|
||||
it reads `(01)00012345678905(10)L240611A`. It uses the same fit rules as any text and may wrap between
|
||||
identifiers.
|
||||
|
||||
A plain `value` in bracket syntax keeps working, so the first release does not depend on the builder.
|
||||
|
||||
### 4.3 Module size, centring and quiet zones
|
||||
|
||||
- Bars are whole dots. The renderer picks the largest module, in dots, that fits the box on both axes including
|
||||
quiet zones, never smaller than `minModule` and never larger than `maxModule` if given.
|
||||
- `minModule` is physical. Defaults: 0.25 mm for linear symbologies and 0.375 mm for two-dimensional ones. That is
|
||||
2 and 3 dots at 203 dpi, 3 and 5 at 300 dpi, 6 and 9 at 600 dpi. These defaults are proposals to confirm on a
|
||||
printer (section 10.6).
|
||||
- For linear symbologies the bar height fills the box height (less the human-readable line) and no longer scales
|
||||
with the module. In version 1 the height scales with the module, so a short box caps the module.
|
||||
- The fitted symbol is placed in its box by `align` and `valign`. Left over dots are blank.
|
||||
- `quiet` is modules on each side, with the existing minimums (10 linear, 4 matrix). Quiet zones are inside the
|
||||
box and are never cropped. The warning `QUIET_ZONE_INTRUDED` fires when another element's ink lands in one.
|
||||
|
||||
### 4.4 When the data does not fit
|
||||
|
||||
The label never prints a code below its stated minimum module.
|
||||
|
||||
- If even `minModule` does not fit, the refusal is `BARCODE_DOES_NOT_FIT` with the dots needed at the minimum and
|
||||
the dots available. The message reads, for example: "Barcode 'code': the value needs 742 dots at the 2 dot
|
||||
minimum module; the box is 690 dots wide at 203 dpi."
|
||||
- An author who accepts the risk writes `"minModule": "1dot"`. Preview and publish then show the warning
|
||||
`BARCODE_MODULE_BELOW_RECOMMENDED` every time.
|
||||
- A later addition, not in the first round: `fallback`, an ordered list of alternative symbologies to try.
|
||||
|
||||
### 4.5 Print quality at 203 dpi
|
||||
|
||||
For the authoring guide, to be confirmed by the physical check:
|
||||
|
||||
- A module is 0.125 mm per dot. Use 2 dots or more for linear codes; 1 dot bars are at the limit of the head.
|
||||
- Print linear bars parallel to the feed direction (rotation 0 on a normally fed label). Bars across the feed lose
|
||||
edge quality at speed.
|
||||
- Keep the quiet zone clear; a centred code inside a wide box is the simplest way.
|
||||
- Prefer a two-dimensional symbol when the data is long and the label is short.
|
||||
|
||||
### 4.6 Verification by decoding
|
||||
|
||||
Every barcode test decodes what will be printed, not what the encoder returned: the test takes the emitted ZPL,
|
||||
rebuilds the bitmap from its graphic fields, and decodes it with ZXing (already a test dependency). It asserts the
|
||||
payload, the symbology, the module size in dots and the blank margins. This runs at 203, 300 and 600 dpi for each
|
||||
fit-qualified symbology, for short, typical and longest values.
|
||||
|
||||
## 5. Graphics
|
||||
|
||||
- **Image**: an asset (PNG or JPEG, as today) in a physical box. `fit` is `contain` (scale to the box, keep the
|
||||
ratio; default) or `none` (one image pixel per dot, as version 1). Scaling averages area and then reduces to
|
||||
black and white by `mode`: `threshold` (default, for line art and logos) or `dither` (for photographs).
|
||||
`IMAGE_UPSCALED` warns when a small image is enlarged.
|
||||
- **Box and line**: `box` with `thickness` (a length, at least 1 dot) and optional `fill`. A line is a box whose
|
||||
height equals its thickness.
|
||||
- **Reverse print**: `"reverse": true` on a text or box draws white on a black field the size of the box.
|
||||
- **Order**: elements paint in list order onto one canvas, so text over a filled box behaves as written.
|
||||
|
||||
**How version 2 reaches the printer.** The renderer composes the whole label on one 1-bit canvas at the target
|
||||
resolution and emits it as `^GFA` strips, trimmed to the rows and columns that hold ink. The output uses only
|
||||
commands the host output policy already allows, so `ZplOutputPolicy` needs no grammar change. This is the same
|
||||
method as the printing QBit's test label. The cost is size: a full 4 x 6 in label is about 250 KB of ZPL at 203 dpi,
|
||||
540 KB at 300 dpi and 2.2 MB at 600 dpi before trimming; a 4 x 1 in label is about 41 KB at 203 dpi. Section 12
|
||||
covers the consequences.
|
||||
|
||||
## 6. Preview inside the QBit
|
||||
|
||||
### 6.1 What the preview is
|
||||
|
||||
The preview is the print path stopped before retention. The same `IsolatedRenderer` renders the same revision
|
||||
content with the same context code, the host runs the same `ZplOutputPolicy`, and the picture is then decoded on
|
||||
the host **from the ZPL bytes that would be sent**. There is no second renderer and no parallel drawing code, so
|
||||
the preview cannot differ from the print for a version 2 scene.
|
||||
|
||||
```
|
||||
editor / record screen
|
||||
| template, draft fingerprint, sample name or record id, target dpi
|
||||
v
|
||||
DocumentPreviewService (foundation) authorise, build context, no rows written
|
||||
| PreviewRequest
|
||||
v
|
||||
IsolatedRenderer.preview (bridges/renderer, or the remote renderer adapter over it)
|
||||
| RENDER with diagnostics collected -> isolated renderer (remote service image)
|
||||
| <- ZPL bytes + diagnostics
|
||||
| ZplOutputPolicy.validate (unchanged)
|
||||
| ZplRaster: ^GFA and ^GB -> 1-bit bitmap -> PNG
|
||||
v
|
||||
Preview: PNG pages, exact flag, diagnostics, output size and hash
|
||||
```
|
||||
|
||||
- **Which component renders.** The isolated renderer, through whatever `IsolatedRenderer` the host configured (the
|
||||
remote renderer image in Forge). Never in process: the renderer README forbids adding the engine to the
|
||||
application JVM, and this design keeps that.
|
||||
- **Who makes the picture.** New class `ZplRaster` in `bridges/renderer`. It walks the already validated output and
|
||||
paints `^GFA` and `^GB` exactly. PNG encoding of our own bitmap is the only image work in the application JVM. A
|
||||
preview over 16 million dots is refused (`PREVIEW_TOO_LARGE`); a 4 x 6 in label at 600 dpi is 8.6 million.
|
||||
- **Version 1 scenes** can be previewed too. Graphics and boxes are exact. Printer font text cannot be drawn, so
|
||||
each `^A0N` field is shown as a hatched placeholder of its estimated size, the result carries `exact: false` and
|
||||
the warning `PRINTER_FONT_NOT_SHOWN`. A version 1 scene previews only at its stock's dpi.
|
||||
- **Refusals in preview.** The request asks the renderer to collect diagnostics instead of stopping at the first
|
||||
failure. Elements that failed are left out of the bitmap and reported with their boxes, and the result says
|
||||
`printable: false`. When there is no error, the ZPL is byte for byte what printing produces; a test asserts the
|
||||
hashes are equal.
|
||||
|
||||
### 6.2 Data for a preview
|
||||
|
||||
- **Sample data.** `samples` in the scene holds up to eight named context objects (64 KiB in total). They are
|
||||
written by the author and must be synthetic. The first is the default. They travel with the scene, so a seeded
|
||||
document previews on a fresh database.
|
||||
- **Stress.** "Longest values" replaces every bound value with the worst-case string for its `maxLength`.
|
||||
- **A real record.** The author chooses a source record, as in today's "Preview draft". The context is built the
|
||||
same way as for generation, including the pinned context script when the host has one.
|
||||
|
||||
### 6.3 API and permissions
|
||||
|
||||
- Port: `GenerationPorts.IsolatedRenderer.preview(PreviewRequest)`, a default method that refuses, implemented by
|
||||
`IsolatedRendererBridge` and delegated by `RemoteRendererBridge`. A host with its own adapter keeps compiling.
|
||||
- Service: `DocumentPreviewService.preview(templateId, draftFingerprint or published, format, data, targetDpi)`
|
||||
returns `Preview(pages, exact, printable, diagnostics, outputBytes, outputSha256)`.
|
||||
- It writes no revision, request, event or file. It logs one structured line (actor, template id, target, outcome,
|
||||
diagnostic codes) with no content.
|
||||
|
||||
| Preview of | Data | Permission |
|
||||
|---|---|---|
|
||||
| A draft | Sample or stress | `document-generation.configure` |
|
||||
| A draft | A real record | `document-generation.configure` and `.generate`, plus read access to that record, as today |
|
||||
| The published revision | Sample | `document-generation.view` |
|
||||
| The published revision | A real record | `document-generation.generate`, plus read access to that record |
|
||||
|
||||
No new permission name is needed.
|
||||
|
||||
### 6.4 How it appears, with no host code
|
||||
|
||||
Forge serves `qqq-frontend-next`. A QBit can show an image there today in three ways that need no frontend change:
|
||||
a `FILE_PREVIEW` field holding a `data:image/png` value (a thumbnail of at most 512 px that opens a lightbox), a
|
||||
composite widget with an `IMAGE` block (an inline `<img>` whose `path` may be a data URL, with width and height
|
||||
styles) and text blocks, or sanitised HTML. The content security policy allows `data:` images. The QBit already
|
||||
uses standard forms and adornments only; this design keeps to that for the first release.
|
||||
|
||||
**First release (existing components only):**
|
||||
|
||||
1. **On the template record screen**: a new section "Label preview" holding a composite widget
|
||||
(`QFieldSection.withWidgetName`). Its renderer class, in the foundation, calls `DocumentPreviewService` with the
|
||||
draft, the default sample and the stock's dpi, and returns an `IMAGE` block (the PNG as a data URL, shown at a
|
||||
fixed physical scale) followed by one text block per diagnostic. Opening a template shows its label. A
|
||||
template with no ZPL source shows a line saying so.
|
||||
2. **Action "Preview layout"** on a template: a form with the data choice (a sample, "longest values", or a
|
||||
record), the format and the resolution; then a result step that is the same composite widget, seeded from the
|
||||
process value of the same name, with the list of diagnostics beside the image. Running it again after an edit
|
||||
takes one click.
|
||||
3. The existing action is relabelled "Generate retained preview file" so the two are not confused. Its behaviour
|
||||
does not change.
|
||||
|
||||
The first task of that package is a spike in a running frontend to confirm that a seeded composite widget in a
|
||||
process step renders an `IMAGE` block from a data URL (the pieces were read in the frontend source, the
|
||||
combination was not run). The fallback, also already present, is a view form with a `FILE_PREVIEW` field and an
|
||||
HTML list.
|
||||
|
||||
**Second release (a `qqq-frontend-next` change):** a purpose-built preview component, in the frontend's existing
|
||||
pattern for QBit screens (`src/components/<qbit>/` plus a widget type): the source editor and the picture side by
|
||||
side, refresh on save, zoom, a resolution switch, and element boxes and quiet zones drawn over the picture from the
|
||||
diagnostics' coordinates. The backend contract is the same `Preview` value, so nothing in this repository changes
|
||||
for it.
|
||||
|
||||
**A host application** gets all of the above by registering the QBit as it does now. Forge adds help entries for
|
||||
the new process, fields and widget (its help coverage test requires them) and nothing else.
|
||||
|
||||
### 6.5 Page (PDF) templates
|
||||
|
||||
- Today's retained preview keeps working for PDF and stays the way to obtain the exact file.
|
||||
- Second release: the same "Preview layout" action for PDF. The frontend preview accepts a data URL only for
|
||||
images, so the isolated renderer rasterises the first pages of the PDF it just produced (PDFBox, at a stated
|
||||
screen resolution) and returns PNG pages through the same `Preview` value. The result is marked as page images of
|
||||
the exact PDF.
|
||||
|
||||
## 7. Templates and page layout beyond labels
|
||||
|
||||
**Today.** HTML with FreeMarker (autoescaped), paged CSS 2.1: stock size and margins, tables, repeating headers and
|
||||
footers, page breaks, absolute positioning, borders, images, TrueType assets, the `<qqq-barcode>` element with fit
|
||||
boxes. One starter, the packing slip. No flex or grid, no SVG, no script.
|
||||
|
||||
**What great means for page documents.**
|
||||
|
||||
- Default fonts without an upload: the bundled faces are available as CSS families (`'QQQ Sans'`, `'QQQ Sans
|
||||
Condensed'`, `'QQQ Mono'`).
|
||||
- A fast preview with sample data (section 6.5) and the same diagnostics list.
|
||||
- Starters that look finished: packing slip (exists), pick list, pallet content sheet, each with sample data.
|
||||
- Refusals that name the cause: an unknown asset, an image over the limits, more pages than allowed.
|
||||
|
||||
**In this round:** bundled CSS families; page preview images; refusal codes for the PDF path. **Out:** a visual
|
||||
designer, flex or grid layout, SVG, new starters beyond one, any change to the OpenHTML fork.
|
||||
|
||||
**Label starters.** A small library of version 2 scenes ships as resources, like the packing slip starter:
|
||||
GS1-128 on 4 x 1 in, a product label with text and a code on 3 x 2 in, a 4 x 6 in shipping layout, and two tools,
|
||||
a **type specimen** (each bundled face at 5 to 14 pt and several thresholds) and a **resolution ruler** (rulers
|
||||
drawn for 203, 300 and 600 dpi; the one that measures true tells the operator the printer's resolution).
|
||||
|
||||
## 8. Errors and diagnosis
|
||||
|
||||
### 8.1 Diagnostics
|
||||
|
||||
The renderer's answer gains a `diagnostics` list, and its error answer gains one diagnostic as `detail`. A
|
||||
diagnostic is closed vocabulary and numbers only:
|
||||
|
||||
```json
|
||||
{ "code": "TEXT_DOES_NOT_FIT", "severity": "ERROR", "element": "caption",
|
||||
"box": [24, 131, 764, 56], "measured": { "widthDots": 902, "sizeCentipoints": 700, "lines": 2 },
|
||||
"limit": { "widthDots": 764, "lines": 2 } }
|
||||
```
|
||||
|
||||
- `element` is the element's `id`, which the template author wrote and which is limited to
|
||||
`[A-Za-z][A-Za-z0-9_-]{0,31}`. No record value, context text or exception message is ever included. This keeps the
|
||||
rule the renderer has today (errors must not carry source data) while giving up the single opaque code.
|
||||
- The host turns a diagnostic into a sentence with the element, the measured value and the limit, in physical
|
||||
units as well as dots. `GenerationPorts.FailureCode` grows, and `RenderException` carries the diagnostic.
|
||||
|
||||
| Severity | Codes (first release) |
|
||||
|---|---|
|
||||
| ERROR | `SCENE_INVALID` (with the JSON pointer of the offending property), `STOCK_MISMATCH`, `TARGET_REQUIRES_SCENE_2`, `ELEMENT_OUTSIDE_PRINTABLE_AREA`, `TEXT_DOES_NOT_FIT`, `VALUE_MISSING`, `VALUE_TOO_LONG`, `FONT_UNKNOWN`, `FONT_MISSING_GLYPH`, `RESERVED_FONT_NAME`, `BARCODE_DOES_NOT_FIT`, `GS1_INVALID`, `ASSET_UNKNOWN`, `PREVIEW_TOO_LARGE`, `RENDERER_TOO_OLD` |
|
||||
| WARNING | `TEXT_TRUNCATED`, `TEXT_SMALL_FOR_DPI`, `TEXT_LIGHT_FACE`, `FIT_NOT_PROVEN`, `BARCODE_MODULE_BELOW_RECOMMENDED`, `QUIET_ZONE_INTRUDED`, `ELEMENTS_OVERLAP`, `IMAGE_UPSCALED`, `DEVICE_UNIT_USED`, `PRINTER_FONT_NOT_SHOWN` |
|
||||
| INFO | `TEXT_SHRUNK` (from and to size), `TEXT_WRAPPED` (lines), `BARCODE_MODULE` (dots and micrometres), `IMAGE_SCALED` |
|
||||
|
||||
Version 1 scenes keep their existing codes and the opaque `RENDER_FAILED`.
|
||||
|
||||
### 8.2 A document that is not offered for a printer
|
||||
|
||||
Whether a document can print on a printer has three owners, and the message is assembled from them:
|
||||
|
||||
| Part | Owner | What it provides |
|
||||
|---|---|---|
|
||||
| What the document needs | Document generation | New `DocumentAvailability.explain(table, format, target)`: for each published template, offered or not, with a reason code and both sizes |
|
||||
| What the printer has | Printing | The media profile the agent reports (width, height, dpi), as today |
|
||||
| Joining the two | The printing bridge in this repository (`bridges/printing`) | New `PrintableDocuments.forPrinter(...)`, so hosts stop writing the match themselves |
|
||||
| Showing it | The application screen (Echo in Forge) | Lists what is offered and states what is not and why |
|
||||
|
||||
With version 2 and a render target, resolution is no longer a reason; a 203 dpi printer and a 300 dpi printer take
|
||||
the same document. The remaining reasons are `STOCK_SIZE_MISMATCH` ("needs 4 x 1 in; the printer reports 4 x 2 in")
|
||||
and, for version 1 documents only, `RESOLUTION_MISMATCH` ("this older document was drawn for 300 dpi; the printer
|
||||
reports 203 dpi; convert it to physical units").
|
||||
|
||||
The scanner QBit holds no document logic (its base module was searched), and the Android client only shows what
|
||||
the server sends, so this needs no scanner or app release. The client's own wording for an empty list was not found and is not changed.
|
||||
|
||||
## 9. Security and data
|
||||
|
||||
- **Template source** is still interpreted only inside the isolated renderer. The version 2 parser, the asset
|
||||
inspector and the upgrade function all run there. The host's new code handles only bytes it already validated
|
||||
(`ZplRaster`) and bytes it stores (`document_generation_asset_content`).
|
||||
- **Uploads.** Fonts: TrueType only (as today), 2 MiB, leading bytes checked on the host, parsed in the renderer,
|
||||
embedding bits honoured, a rights statement required and stored. Images: PNG or JPEG, the existing dimension and
|
||||
pixel limits, SVG still refused. Assets are addressed by hash; identical bytes are stored once.
|
||||
- **Bundled fonts.** Unmodified, each with licence, provenance and hash, listed in `THIRD-PARTY.md` and `NOTICE`.
|
||||
- **No preview is stored.** No revision, request, event, retained file or log content. The PNG of a real-record
|
||||
preview exists in the response and, when shown through a process step, in that process's server-side state until
|
||||
it expires, as any process screen showing record data does. (That store's retention was not verified.) The record
|
||||
screen widget uses sample data only, so opening a template never renders customer data.
|
||||
- **Samples** are synthetic by rule, stated in the field help and the authoring guide.
|
||||
- **Diagnostics and logs** carry element ids and numbers only.
|
||||
- **Permissions** are the existing five; section 6.3 gives the mapping. Uploading and converting need `configure`.
|
||||
- **Parser hardening.** The version 2 scene parser and `ZplRaster` take untrusted input. Both get negative tests
|
||||
and a bounded mutation campaign (section 10.5).
|
||||
|
||||
## 10. Test strategy and coverage
|
||||
|
||||
### 10.1 Compatibility of version 1 (first, before any renderer change)
|
||||
|
||||
A corpus of version 1 scenes with the SHA-256 of their ZPL at 203, 300 and 600 dpi, generated from the tag Forge
|
||||
runs (`v1.1.0-RC.2`; the renderer sources are identical on `develop` today) and never regenerated. It contains the
|
||||
shapes stored in Forge's database (plain, scanner fidelity, compact GS1, the raster text document, with synthetic
|
||||
values), the README examples and one scene per element type and option. The test fails on any byte difference.
|
||||
|
||||
### 10.2 Golden images per resolution
|
||||
|
||||
For each version 2 fixture (every starter and one scene per feature) a PNG at 203, 300 and 600 dpi is committed.
|
||||
Barcode and box pixels are compared exactly everywhere. Text pixels depend on the Java runtime's rasteriser, so
|
||||
unit tests on a developer machine compare with a small tolerance and always assert structure (ink bounds inside
|
||||
the box, centring within one dot), and the exact comparison runs in the image qualification on the pinned runtime,
|
||||
the existing `*ImageQualification` pattern. A changed golden is reviewed as a picture in the pull request.
|
||||
|
||||
### 10.3 Decode tests
|
||||
|
||||
As section 4.6: decode the bitmap rebuilt from the emitted ZPL, per symbology, per resolution, per value length.
|
||||
The same rebuild is `ZplRaster`, so the preview code is exercised by every barcode test.
|
||||
|
||||
### 10.4 Property tests for fit
|
||||
|
||||
Seeded, deterministic generators (no clock, fixed seeds) over text, box, size, floor, lines and resolution assert:
|
||||
|
||||
- the result is either a layout whose ink is inside the box or a refusal whose measured value exceeds its limit;
|
||||
- ink is never clipped;
|
||||
- the chosen size is never below the floor and never above the nominal size;
|
||||
- a shorter string never gets a smaller size than a longer one in the same box;
|
||||
- a published `maxLength` implies no length refusal for any string up to that length in the tested alphabets;
|
||||
- for barcodes, the module is whole dots, at least the minimum, and the symbol decodes.
|
||||
|
||||
### 10.5 Negative and fuzz tests
|
||||
|
||||
Malformed scenes, lengths and assets; ZPL with wrong lengths for `ZplRaster`. A bounded mutation campaign over the
|
||||
scene parser and `ZplRaster` runs in the normal test phase, with its seeds, limits and replay command in
|
||||
`docs/security/fuzzing.md` (new), as the testing standard requires for parsers of untrusted input.
|
||||
|
||||
### 10.6 Physical check before a final release
|
||||
|
||||
Print the specimen, the ruler and the GS1 starter on a 203 dpi Zebra and a 300 dpi Zebra and scan them. This
|
||||
confirms the proposed defaults (minimum modules, small type thresholds) and is recorded with the release. An RC
|
||||
may reach dev before it; a final version may not. No agent can do this step.
|
||||
|
||||
### 10.7 Coverage
|
||||
|
||||
The testing standard requires 80% line coverage of changed lines and 100% for published libraries, per package.
|
||||
The repository today gates only `foundation` at 85% instructions.
|
||||
|
||||
- New packages (`com.kingsrook.qbits.renderer.scene2` and below, and the new foundation and bridge classes) are
|
||||
gated at 100% line coverage, with exclusions only by explicit marker and written reason.
|
||||
- JaCoCo is added to `renderer` and `bridges/renderer` with a bundle gate at the measured baseline, never lowered.
|
||||
- The `foundation` gate is not lowered.
|
||||
- Changed-line coverage of 80% is checked by the reviewer from the JaCoCo report; the shared Gitea workflow has no
|
||||
such gate that I could find.
|
||||
|
||||
### 10.8 Review
|
||||
|
||||
Every package goes through a pull request into `develop` with: the full `./mvnw verify` on the branch head with no
|
||||
skipped gate; an independent review by an agent that did not write it, covering correctness, silent failures and
|
||||
test adequacy; and the review focus listed for the package. James merges.
|
||||
|
||||
## 11. Repositories, versions and what Forge does
|
||||
|
||||
### 11.1 Order and versions
|
||||
|
||||
| Order | Repository | Change | Released as |
|
||||
|---|---|---|---|
|
||||
| 1 | `qbit-document-generation` | First slice (section B) | `1.4.0-RC.1` from a new `release/1.4` |
|
||||
| 2 | same | Renderer images built from that version | `qqq-document-renderer` and `qqq-remote-renderer`, tag `1.4.0-RC.1` |
|
||||
| 3 | `gg-forge`, `gg-forge-cd` | Pins, schema, one version 2 document on dev | Forge dev build |
|
||||
| 4 | `qbit-document-generation` | Follow-on packages | `1.4.0-RC.2` and later |
|
||||
| 5 | `qqq-frontend-next` | Purpose-built preview component | next `v1.0.0-RC.N` tag |
|
||||
| 6 | `gg-forge` | Remove workarounds, convert the standard documents | Forge dev, then staging |
|
||||
|
||||
`develop` is `1.4.0-SNAPSHOT` and the workflow names an RC from the branch, so the new line is `release/1.4`. The
|
||||
`release/1.1` line that Forge runs in production stays untouched for fixes. Only one release-branch merge may be in
|
||||
flight across QRun repositories.
|
||||
|
||||
### 11.2 Other repositories
|
||||
|
||||
| Repository | Needed for the first slice | Later |
|
||||
|---|---|---|
|
||||
| `qbit-printing` | Nothing. A label is opaque ZPL to it, and the default limit is 20 MiB. | Optional: move the resolution ruler into the built-in test documents. |
|
||||
| `qqq-print-agent` | Nothing. ZPL is written raw to the queue and never parsed. No agent release is needed for anything in this design. | It repeats the payload once per copy; for large 600 dpi labels and many copies that multiplies the spool size. |
|
||||
| `qqq-print-protocol` | Nothing. | Nothing. |
|
||||
| `qbit-mobile-scanners` and the Android client | Nothing. | Nothing. |
|
||||
| `qbit-record-files`, `qbit-collaboration` | Nothing. Record Files remains an optional asset source. | Nothing. |
|
||||
| `qqq` core | Nothing. The widget, block, adornment and section APIs used exist in the version Forge pins. | Nothing. |
|
||||
| `qqq-frontend-next` | Nothing. | The preview component (section 6.4). |
|
||||
|
||||
Nothing in this design is held back by a client or agent release.
|
||||
|
||||
### 11.3 Rollout rule for the renderer image
|
||||
|
||||
The bridge accepts only an exact renderer version string, and the new answers have new fields. To allow a rolling
|
||||
change:
|
||||
|
||||
- Bridge `1.4` accepts renderer `0.1.0` (today's answers) and `0.2.0` (new answers). With an old image, version 1
|
||||
printing continues and a version 2 scene or a preview is refused with `RENDERER_TOO_OLD`, which names the fix.
|
||||
- Renderer `0.2.0` is not accepted by bridges before `1.4`.
|
||||
- Therefore: upgrade the host library first, then the image. Never the reverse.
|
||||
|
||||
### 11.4 What Forge pins, adds and deletes
|
||||
|
||||
Pins and additions (first slice):
|
||||
|
||||
- The five document generation artifacts at `1.4.0-RC.1`; the two image digests in `gg-forge-cd`, after the host.
|
||||
- A changeset for the new request column (`target_dpi`). Later a changeset for
|
||||
`document_generation_asset_content`.
|
||||
- Help entries for the new process, fields, section and widget; the permission and composition lists its tests
|
||||
check.
|
||||
- `EchoDocumentGenerator` passes the printer's reported dpi as the render target.
|
||||
- One new setup-data changeset that seeds a version 2 GS1 document for 4 x 1 in, using a bundled face. Existing
|
||||
changesets are not edited.
|
||||
|
||||
Deletions, once the matching upstream piece is on dev and one label has printed:
|
||||
|
||||
| Forge workaround | Retired by | Condition |
|
||||
|---|---|---|
|
||||
| `EchoBundledFonts`, its font resources, test and script | Bundled faces in the renderer | No published revision still references the `forge-bundled-font:` asset. The 0050 document is republished by a new changeset as a version 2 scene with a bundled face; its old revision row stays but is never rendered again, because reprints use retained bytes. |
|
||||
| Hand geometry in `EchoTemplatePublisher` (dot conversion, insets, module search, the centred scene builder) and the duplicate scene builders in `scripts/local` | Version 2 scenes | Standard documents seeded as version 2 |
|
||||
| One document per resolution (0039 seeds 203 and 300 dpi copies) | Render target | New seeds carry one document per size; old rows remain editable data |
|
||||
| The exact stock match and silent drop in `EchoPrintingDestinations.resolve` and `EchoService.stockMatches` | `PrintableDocuments` in the printing bridge | Follow-on package |
|
||||
| Offline bitmaps in `docs/history` used to judge a layout | Preview | First slice |
|
||||
|
||||
Forge's `echoPublishDocument` process and the SQL that re-implements the draft fingerprint are not addressed here.
|
||||
A declarative import for documents would retire them and is worth its own design.
|
||||
|
||||
## 12. Risks and decisions
|
||||
|
||||
### Decisions for James
|
||||
|
||||
| # | Decision | Recommendation |
|
||||
|---|---|---|
|
||||
| 1 | **Bundled fonts and their licence.** The dependency standard's allowlist does not include the SIL Open Font License 1.1, yet `qbit-printing` already ships JetBrains Mono under it and Forge ships Abel under it, with no recorded exception. | Record one exception for unmodified OFL 1.1 fonts shipped as data, covering printing's existing font and the renderer's set. Bundle IBM Plex Sans (Regular, Bold), IBM Plex Sans Condensed (Bold) and JetBrains Mono (Regular, Bold), after confirming each licence file. The alternative with no exception is to restrict the set to fonts published under Apache 2.0. |
|
||||
| 2 | **Asset bytes in the QBit's own table.** Fonts and logos up to 2 MiB stored in the database, so upload and seeding work everywhere. It moves the stated boundary that the QBit owns no file bytes. | Yes. Assets are definition data. Record Files stays as an optional second source. |
|
||||
| 3 | **Version 2 prints the whole label as a bitmap and offers no printer font.** This is what makes preview exact and fit measurable. It makes jobs larger (section 5) and gives up printer-drawn text. | Yes. It is how the printing test label already works. Add compression only if 600 dpi job size proves a problem; that would mean widening the host output policy. |
|
||||
| 4 | **Release line.** A new `release/1.4` for this work, leaving `release/1.1` for production fixes, with the host-first rollout rule of section 11.3. Production later moves from the 1.1 line to the 1.4 line. | Yes. |
|
||||
| 5 | **Renderer images.** They are built and pushed by hand to `ghcr.io/gg-engineering`, which is the slowest step between a merged change and dev. | Add a build job for them to this repository's CI in the follow-on, and decide whether QRun images belong in a QRun registry. Not required for the first slice. |
|
||||
|
||||
### Decided in this design
|
||||
|
||||
- Version 1 is frozen; version 2 is a separate schema id and code path; upgrade is an explicit authoring action.
|
||||
- Pull request 11 is replaced, not merged (see below).
|
||||
- Lengths are strings with a unit; coordinates are relative to the printable area; nine-point anchors plus
|
||||
`align` and `valign`; `group` is in, rows and tables are out.
|
||||
- Default text fit is one line, shrink to 70%, then refuse; truncation is opt-in; `maxLength` makes fit provable at
|
||||
publish.
|
||||
- The barcode never prints below its stated minimum module; the default refuses.
|
||||
- The preview picture is decoded on the host from the validated ZPL; the renderer does not draw a second image.
|
||||
- The first preview UI uses components the frontend already has; the purpose-built component follows.
|
||||
- Resolution is a render target chosen at print or preview time; flexible stock size comes later.
|
||||
- Structured diagnostics replace the opaque failure for version 2, carrying ids and numbers only.
|
||||
|
||||
### Pull request 11
|
||||
|
||||
PR 11 ("align a fitted ZPL barcode inside its fit box; justified font 0 text blocks") changes version 1 in place.
|
||||
|
||||
- Its barcode half is sound and its arithmetic and decode tests are carried into version 2, where every element
|
||||
has `align` and `valign`. It is not applied to version 1, because published scenes that already carry `align` on
|
||||
a barcode would begin to print differently.
|
||||
- Its text half emits `^FB` field blocks. The host output policy (`ZplOutputPolicy.text`) requires `^FH_^FD`
|
||||
directly after the font command and rejects anything else, and the PR does not change the bridge. Through the
|
||||
bridge Forge uses, every label with a justified text block would be refused. It also keeps the character-count
|
||||
estimate and relies on the printer to centre, which cannot be previewed.
|
||||
|
||||
So the design absorbs the idea and replaces the PR. Its tests move into the version 2 barcode package, and the PR
|
||||
is closed when that package merges. Forge's unused `zplGs1CentredScene`, which targets PR 11, is deleted with the
|
||||
other hand geometry.
|
||||
|
||||
### Risks
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| Text rasterisation differs between Java builds, so goldens flap | Exact comparison only on the pinned runtime; structural assertions elsewhere (10.2) |
|
||||
| Proposed module and type defaults are wrong for real media | Physical check before a final version (10.6); defaults are data in one place |
|
||||
| Larger ZPL exceeds a host limit (Forge sets a 1 MiB output limit) or slows a 600 dpi printer | Ink trimming; documented sizes; the limit is host configuration; compression held in reserve (decision 3) |
|
||||
| Host and image versions out of step | Section 11.3 rule and the named `RENDERER_TOO_OLD` refusal |
|
||||
| The seeded composite widget does not render as read | Spike first; `FILE_PREVIEW` fallback (6.4) |
|
||||
| The record screen widget renders on every open | Renderer admission already bounds concurrency; the widget uses sample data and can be collapsed |
|
||||
| Scope: the plan is large | The first slice is seven packages; everything else is independent and follows |
|
||||
| Manual image builds delay dev | Decision 5 |
|
||||
|
||||
---
|
||||
|
||||
# B. Work plan
|
||||
|
||||
All packages are in `qbit-document-generation` unless a repository is named. Each is one pull request into
|
||||
`develop`. Branch names never start with `release/`. Sizes: S about a day, M two to three days, L about a week of
|
||||
agent work including review.
|
||||
|
||||
## B.1 Shared contracts (so packages can be built in parallel)
|
||||
|
||||
**Renderer wire (protocol id unchanged, renderer version `qqq-renderer-0.2.0`).**
|
||||
|
||||
- `RENDER` and `VALIDATE` accept optional `"targetDpi"` and `"diagnostics": "COLLECT"`.
|
||||
- A successful answer adds `"diagnostics": [ ... ]` (section 8.1). With `COLLECT` and at least one error, the
|
||||
outcome is `"REFUSED"`, with `diagnostics` and the partial output.
|
||||
- An error answer adds `"detail": { ... }`, one diagnostic.
|
||||
- New operations, each routed by `RendererMain` to one class: `INSPECT_ASSET` (`AssetInspector`), `UPGRADE_SCENE`
|
||||
(`scene2.upgrade.SceneUpgrade`). P1 creates both as classes that refuse with `UNSUPPORTED_OPERATION`.
|
||||
|
||||
**Renderer element contract (package `com.kingsrook.qbits.renderer.scene2`, owned by P1).**
|
||||
|
||||
- `Length` (parse, micrometres, dots by rule), `Frame` (a resolved box in dots), `Layout` (anchor resolution),
|
||||
`Canvas` (1-bit, paint order, reverse), `Diagnostics` (collector), `SceneElement` with
|
||||
`Frame place(Layout)` and `void paint(Canvas, Frame, RenderInput, Diagnostics)`, `ElementFactory` keyed by `type`.
|
||||
- P2, P3 and P4 each add one sub-package and register one factory in a file they own
|
||||
(`scene2/text/TextFactory`, `scene2/barcode/BarcodeFactory`, `scene2/graphics/GraphicsFactory`); P1 lists the
|
||||
three class names in `ElementFactory` from the start, with placeholders that refuse.
|
||||
|
||||
**Foundation ports (owned by P6a).** `GenerationPorts.Diagnostic`, `PreviewRequest`, `PreviewPage`, `Preview`, the
|
||||
new `FailureCode` values, `RenderException(FailureCode, Diagnostic)`, `IsolatedRenderer.preview` as a refusing
|
||||
default, `Integer targetDpi` on `RenderRequest`. P6a also adds the registration calls for
|
||||
`GenerationPreviewProcesses`, `GenerationAssetProcesses`, `GenerationUpgradeProcesses` and `GenerationAssetTables`
|
||||
in the producer, and the call to `GenerationPreviewWidget.section(table)` in `GenerationTables`, each an empty
|
||||
class or no-op, so later packages fill only their own file.
|
||||
|
||||
## B.2 Packages
|
||||
|
||||
### First slice
|
||||
|
||||
| Id | Package | Branch | Size | Depends on |
|
||||
|---|---|---|---|---|
|
||||
| P0 | Version 1 compatibility corpus | `test/v1-compat-corpus` | S | none |
|
||||
| P13 | Coverage gates and fuzz scaffolding | `test/coverage-and-fuzz-gates` | S | none |
|
||||
| P6a | Ports and registration hooks | `feat/preview-ports` | S | none |
|
||||
| P1 | Scene 2 core, canvas, emitter, diagnostics | `feat/scene2-core` | L | P0, P13 |
|
||||
| P2 | Text: bundled fonts, measurement, fit | `feat/scene2-text` | L | P1 |
|
||||
| P3a | Barcode: module fit, alignment, decode tests | `feat/scene2-barcode-fit` | M | P1 |
|
||||
| P5 | Bridge: `ZplRaster`, diagnostics, preview, version rule | `feat/bridge-preview` | M | P6a |
|
||||
| P6b | Preview service, action and record widget | `feat/preview-ui` | M | P6a (P5 for the integration test) |
|
||||
| P7 | Render target dpi | `feat/render-target-dpi` | M | P6a |
|
||||
|
||||
**P0. Version 1 compatibility corpus.**
|
||||
Scope: section 10.1. Files: new tests and resources only, under `renderer/src/test` and `bridges/renderer/src/test`
|
||||
(`ZplSceneOneCompatibilityTest`, `resources/compat-v1/`). Tests: the corpus itself, run against untouched code,
|
||||
plus proof that each stored ZPL passes `ZplOutputPolicy`. Review focus: the corpus covers every version 1 element,
|
||||
option and the Forge scene shapes; values are synthetic; hashes come from the tagged source, not from the branch.
|
||||
|
||||
**P13. Coverage gates and fuzz scaffolding.**
|
||||
Scope: section 10.7 and 10.5 scaffolding. Files: `renderer/pom.xml`, `bridges/renderer/pom.xml` (JaCoCo with a
|
||||
per-package rule for new packages and a baseline bundle rule), `docs/security/fuzzing.md`, a seeded mutation test
|
||||
harness class in each module's test tree. This package is the only one that edits those two `pom.xml` files in the
|
||||
first slice. Review focus: gates fail when they should (prove with a deliberate miss), no skip switches.
|
||||
|
||||
**P6a. Ports and registration hooks.**
|
||||
Scope: B.1 foundation contract. Files: `foundation/.../GenerationPorts.java`, `DocumentGenerationQBitProducer.java`
|
||||
and `GenerationTables.java` (registration calls only), five new empty classes. Tests: record validation, default
|
||||
refusals, source compatibility of existing constructors. Review focus: nothing breaks an existing host adapter; diagnostics cannot hold free text.
|
||||
|
||||
**P1. Scene 2 core.**
|
||||
Scope: sections 2, 5 (box, canvas, emission), 8.1 for the renderer; `RendererMain` routing by schema and for the
|
||||
new operations; renderer version `0.2.0`; `renderer/schema/zpl-scene-2.schema.json`; `box` and `group` elements;
|
||||
`overrides`; `samples` accepted and ignored by rendering. Files: new package `scene2`; `RendererMain.java`,
|
||||
`Wire.java` and `RenderInput.java` (target dpi, bundled-name reservation hook). `ZplRenderer.java`,
|
||||
`RasterText.java` and `BarcodeFit.java` are not edited. Tests: unit tests for every length and rounding rule,
|
||||
anchor resolution for all nine anchors and negative shifts, row-band emission (the rebuilt bitmap equals the
|
||||
canvas), goldens for box scenes at three resolutions, negative tests for the parser, P0 still green, 100% line
|
||||
coverage of the package. Review focus: rounding rules match section 2.2 exactly; no path from context values into
|
||||
diagnostics; emitted ZPL passes the unchanged `ZplOutputPolicy`; limits on elements, depth and canvas size.
|
||||
|
||||
**P2. Text.**
|
||||
Scope: section 3.1 (bundled set and reserved names), 3.3 (fit, `maxLength`, legibility warnings), reverse text,
|
||||
rotation. Files: `scene2/text/*`; `renderer/src/main/resources/fonts/**` with licence, provenance and hash files;
|
||||
`renderer/THIRD-PARTY.md`, `renderer/NOTICE`. Tests: property tests of section 10.4; goldens; glyph coverage
|
||||
refusal; a test that every bundled face has licence, provenance and a matching hash; worst-case proof for
|
||||
`maxLength`. Review focus: the fit order is exactly as stated; truncation only when asked; licences confirmed from
|
||||
files; determinism (no locale, no installed fonts).
|
||||
|
||||
**P3a. Barcode fit and alignment.**
|
||||
Scope: sections 4.3, 4.4, 4.6 for the 13 fit-qualified symbologies, with `value` and `valuePath`, dynamic
|
||||
symbology allowlist and scanner profile carried over. Files: `scene2/barcode/*` (new `ModuleFit`; the version 1
|
||||
`BarcodeFit` is called, not changed). The alignment tests of PR 11 are ported here. Tests: decode tests at three
|
||||
resolutions for short, typical and longest values; centring within one dot; refusal content; below-minimum warning.
|
||||
Review focus: no code prints below the stated minimum; quiet zones never cropped; integer arithmetic only.
|
||||
|
||||
**P5. Bridge preview.**
|
||||
Scope: sections 6.1, 6.3 (port implementation), 11.3. Files: `bridges/renderer/.../ZplRaster.java` (new),
|
||||
`IsolatedRendererBridge.java` (diagnostics parsing, accepted versions, `preview`, target dpi in the request and in
|
||||
the stock passed to the policy), `RendererJson.java`; `bridges/remote-renderer/adapter/.../RemoteRendererBridge.java`
|
||||
(delegate). `ZplOutputPolicy.java` grammar is not changed. Tests: raster of every corpus ZPL from P0; refusal of
|
||||
malformed graphics; the size limit; old-image behaviour (`RENDERER_TOO_OLD`); print and preview hashes equal;
|
||||
mutation campaign over `ZplRaster`. Review focus: only validated bytes are rasterised; no font or image decoding on
|
||||
the host; error mapping stays closed vocabulary; the version rule.
|
||||
|
||||
**P6b. Preview service and UI.**
|
||||
Scope: sections 6.2 to 6.4 first release. Files: new `DocumentPreviewService.java`,
|
||||
`GenerationPreviewProcesses.java`, `GenerationPreviewWidget.java` (including the "Label preview" section it
|
||||
attaches to the template); `GenerationAuthoringProcesses.java` (the relabel only); help text and labels for every
|
||||
new field. It begins with the frontend
|
||||
spike of section 6.4. Tests: permission matrix of section 6.3; nothing written to any table (assert row counts);
|
||||
sample, stress and record data paths with a fake renderer; diagnostics to sentences; one browser check in the
|
||||
host's local stack. Review focus: no persistence and no content in logs; record access checked before context is
|
||||
built; messages name element, measured value and limit.
|
||||
|
||||
**P7. Render target dpi.**
|
||||
Scope: section 2.3. Files: `DocumentGenerationService.java`, `GenerationStore.java`, `GenerationProcesses.java`
|
||||
(option on generate), `GenerationTables.java` (the request field), `DocumentGenerationSchema.java`, three
|
||||
migration scripts, `docs/EXPECTED-PRINT-INTENT.md`;
|
||||
`bridges/printing/.../PrintingDispatch.java` only if it must carry the value. Tests: digest framing (absent value
|
||||
keeps old digests; present value changes them); refusal for version 1; MySQL, PostgreSQL and H2 suites; reprint
|
||||
unchanged. Review focus: old rows and old digests are untouched; the target is validated against 203, 300, 600.
|
||||
|
||||
### Follow-on
|
||||
|
||||
| Id | Package | Branch | Repository | Size | Depends on |
|
||||
|---|---|---|---|---|---|
|
||||
| P3b | GS1 builder and human-readable line | `feat/scene2-gs1-hri` | this | M | P2, P3a |
|
||||
| P4 | Graphics: scaled images, dither, reverse box | `feat/scene2-graphics` | this | M | P1 |
|
||||
| P8 | Built-in asset store and upload | `feat/asset-store` | this | L | P6a, P1 |
|
||||
| P9 | Version 1 to 2 conversion | `feat/scene-upgrade` | this | M | P2, P3a, P6b |
|
||||
| P10 | Document availability and printer match | `feat/printable-documents` | this | M | P7 |
|
||||
| P11 | Page documents: bundled CSS families, page preview | `feat/pdf-fonts-preview` | this | M | P2, P5, P6b |
|
||||
| P12 | Starters, specimen and ruler | `feat/label-starters` | this | S | P2, P3a |
|
||||
| P14 | Authoring guide and README | `docs/label-authoring` | this | S | first slice |
|
||||
| P15 | Flexible stock (`atLeast`) | `feat/flexible-stock` | this | M | P10 |
|
||||
| P16 | Renderer image build in CI | `ci/renderer-images` | this | S | decision 5 |
|
||||
| F1 | Purpose-built preview component | `feat/document-preview` | `qqq-frontend-next` | L | P6b released |
|
||||
| G1 | Forge: pins, schema, one version 2 document on dev | per Forge rules | `gg-forge`, `gg-forge-cd` | M | `1.4.0-RC.1` and images |
|
||||
| G2 | Forge: remove workarounds, convert standard documents | per Forge rules | `gg-forge` | M | G1, P9, P10 |
|
||||
|
||||
Notes for the follow-on packages:
|
||||
|
||||
- **P3b** files: `scene2/barcode/gs1/*`, `scene2/barcode/Hri.java`. Tests: identifier table cases, check digits,
|
||||
separator placement decoded back, caption wraps between identifiers. Review focus: do not duplicate what the
|
||||
encoder already validates.
|
||||
- **P4** files: `scene2/graphics/*`. Tests: goldens, upscaling warning, pixel limits.
|
||||
- **P8** files: `GenerationAssetTables.java`, `GenerationAssetStore.java`, `GenerationAssetProcesses.java`,
|
||||
`GenerationAssetChoices.java`, three migration scripts, `renderer/.../AssetInspector.java`, and the `dg:` branch
|
||||
in `IsolatedRendererBridge.resolve` (coordinate with P5's owner; it is one method). Tests: upload limits and
|
||||
refusals, hash addressing, permission, selection listing, the three databases. Review focus: no parsing on the
|
||||
host; rights statement required; bytes never leave through a query API.
|
||||
- **P9** files: `scene2/upgrade/SceneUpgrade.java`, `GenerationUpgradeProcesses.java`. Tests: each corpus scene
|
||||
converts, and the converted scene's graphics match the original within one dot.
|
||||
- **P10** files: `DocumentAvailability.java` (foundation), `bridges/printing/.../PrintableDocuments.java`.
|
||||
- **P11** files: `PdfRenderer.java` and a new page rasteriser class in the renderer; the PDF branch of the preview.
|
||||
- **P14** integrates each package's own notes (written under `docs/labels/` by that package) into
|
||||
`renderer/README.md` and `docs/AUTHORING.md`, which no other package edits.
|
||||
|
||||
## B.3 First on dev
|
||||
|
||||
The smallest set that puts a real improvement on dev is the first slice: P0, P13, P6a, P1, P2, P3a, P5, P6b, P7.
|
||||
It delivers physical-unit layout, anchors and alignment, measured text with fit rules and bundled fonts, centred
|
||||
whole-dot barcodes that never go below their minimum, structured refusals, one design for every resolution, and a
|
||||
preview on the template screen.
|
||||
|
||||
```
|
||||
P0 ----+
|
||||
P13 ---+--> P1 --+--> P2 ---+
|
||||
+--> P3a --+
|
||||
P6a ---+--> P5 -------------+--> release/1.4 -> 1.4.0-RC.1 -> images -> G1 (Forge dev)
|
||||
+--> P6b ------------+
|
||||
+--> P7 -------------+
|
||||
```
|
||||
|
||||
P0, P13 and P6a start together. P1 follows; P5, P6b and P7 need only P6a and run beside it. P2 and P3a start when
|
||||
P1 merges. Then: one merge of `develop` into `release/1.4`, the two images built from `1.4.0-RC.1`, and Forge's G1
|
||||
(host pins first, image digests second). The acceptance for the slice is one version 2 GS1 label, previewed in
|
||||
Forge on dev, then printed on the dev printer at its reported resolution and scanned.
|
||||
|
||||
What follows, in value order: P3b and P12 (GS1 done properly, starters and the ruler), P8 (uploads everywhere),
|
||||
P9 with G2 (convert the standard documents and delete the Forge workarounds), P10 (say why a document is not
|
||||
offered), P4, P11, P14, F1, P15, P16.
|
||||
Reference in new issue
Block a user