Package P2 of docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md (work plan B): text for version 2 label scenes. Bundled fonts, measurement before drawing, and a stated rule for a value that does not fit. Builds on the scene 2 core (#14); version 1 is untouched.
What a label author gets
Real fonts. Five faces ship inside the renderer, so a label needs no printer font, no installed font and no object storage: qqq-sans, qqq-sans-bold, qqq-condensed-bold, qqq-mono, qqq-mono-bold.
Sizes in points or millimetres."size": "9pt" or "3mm", resolved at the render target (203, 300 or 600 dpi) and not rounded to dots.
A text is measured before it is drawn and placed in its box by align and valign. Ink never leaves the box and is never clipped.
A stated rule when a value does not fit, chosen per element (table below), with a diagnostic that names the element, what was measured and the limit.
maxLength proves the fit when the document is published, so a length refusal is a design-time event and not a failed print.
Thermal legibility controls: threshold (heavier strokes), bold faces, warnings for small type, and resolution overrides for either.
Rotation (0, 90, 180, 270) and reverse text (white on a black box).
Fit rules as implemented
Step
Rule
Reported as
1
Lay the value out at size within fit.maxLines (default 1). If it fits, it is never shrunk.
nothing
2
Otherwise take the largest size, in steps of 0.25 pt down to fit.shrinkTo, at which it fits. shrinkTo defaults to 70% of size and not below 5 pt (the size itself when the size is under 5 pt).
If nothing fits down to the floor, apply fit.overflow: refuse (the default) or truncate.
TEXT_DOES_NOT_FIT (error): measured width, height, lines and size against the box
Wrap
Only with maxLines above 1. At each size a line is broken between words while the text then fits, and at any character when it does not. So a value with no space wraps, and a larger size is preferred to whole words. Never more than maxLines lines.
TEXT_WRAPPED (info): lines
Truncate
Only with overflow: "truncate", only at the floor size: as many lines as the box holds, the last ending with an ellipsis after as many of its characters as fit with it. The mark is always shown and the result always fits; where not even the mark fits, the text is refused.
maxLength (1 to 1024). Validation lays out that many of the face's widest character, each taken to reach as far in every direction as the furthest-reaching character of the face, and refuses the scene if they do not fit. Printing refuses a longer value before any layout. The proof never cuts: with overflow: "truncate" a length that is not proven does not refuse the scene but warns at every validation, with the length that is proven.
TEXT_DOES_NOT_FIT at validation, VALUE_TOO_LONG at print, FIT_NOT_PROVEN (warning) for a bound element with no maxLength, and with provenCharacters for a cutting element whose maxLength is not proven
Other refusals: VALUE_MISSING (the pointer finds no text, number or true/false), FONT_MISSING_GLYPH (a character the face cannot draw as one glyph, with the count), FONT_UNKNOWN (a face that is not bundled, at the place that names it), RESERVED_FONT_NAME (a supplied asset whose name begins qqq-). Warnings: TEXT_SMALL_FOR_DPI (capitals under 12 dots), TEXT_LIGHT_FACE (a regular weight with capitals under 20 dots). A diagnostic holds ids and numbers only; no value ever enters a diagnostic, a log line or an exception message, and a test puts a marker through every refusal to hold that.
A rendered sample
renderer/src/test/resources/scene2/cases/text-fit.json on 2 x 1 in stock with 2 mm margins, at 203 dpi (golden: scene2/golden/text-fit-203.png; the 300 and 600 dpi goldens are beside it). Thin outlines show each element's box.
Element
Scene
What was drawn
Fit applied
kept
LOT 240611, qqq-sans-bold 10 pt, 22 x 5 mm, centred
10 pt, one line, centred within one dot
none
right
9.5 kg, qqq-condensed-bold 11 pt, 22 x 5 mm, right and bottom
10.25 pt
TEXT_SHRUNK: 11 pt is 41 dots high and the box is 39
shrunk
(01)00012345678905(10)L240611A, qqq-mono-bold 9 pt, 44 x 4 mm
6.75 pt, one line
TEXT_SHRUNK from 9 pt (floor 6.3 pt)
wrapped
46 characters with no space, qqq-mono 7 pt, 36 x 7.5 mm, maxLines: 2, maxLength: 46
7 pt, two lines of 24 and 22 characters, each centred
TEXT_WRAPPED; also TEXT_LIGHT_FACE (capitals 14 dots, regular weight)
cut
a 42-character description, qqq-sans 7 pt, 30 x 3.6 mm, overflow: "truncate"
A description of the item…
TEXT_TRUNCATED: 25 of 42 characters shown
The diagnostic of a refusal, here the design's own example caption (two lines, floor 7 pt, maxLength 48, 94 x 6.5 mm) when validated at 203 dpi:
How each licence was confirmed. From the release archive itself, not from memory. Each IBM Plex archive's LICENSE.txt and the JetBrains Mono archive's OFL.txt state "This Font Software is licensed under the SIL Open Font License, Version 1.1" and carry the full text. The licence field of every font's name table says the same, and every font's embedding bits (OS/2.fsType) are 0, installable embedding. The licence files ship byte for byte beside the fonts (.gitattributes keeps git from rewriting anything under /fonts), each directory has a SOURCE.md with the release, archive, path and hashes, and /fonts/SHA256SUMS lists the hashes. FaceTest fails the build when a licence, a source note or a hash is missing or a byte changes. The files are unmodified and keep their names; IBM Plex's Reserved Font Name "Plex" is not used for anything derived. The notes are named SOURCE.md because this repository's .gitignore ignores PROVENANCE.md.
The fonts add 531,819 bytes to the renderer jar (1,172,935 uncompressed), which is now 736 KB.
Dependency-licence exception (K22-DEP-02), for the maintainer to file
The SIL OFL 1.1 is not on the K22-DEP-02 allowlist. The exception register is exceptions/register.md in the standards repository, which this pull request does not edit. Proposed entry, with the next free number:
EX-00NN
Unmodified SIL Open Font License 1.1 fonts shipped as data in the document renderer
Status: Open upon merge of this record. The maintainer approved the exception and named the set on 2026-11-12.
Requirements:K22-DEP-02. The SIL Open Font License 1.1 (SPDX OFL-1.1) is not on the allowlist of licences for dependencies that ship with a product. No vulnerability, provenance, signing or branch-protection requirement is waived.
Scope:QRun-IO/qbit-document-generation, module renderer, exactly these unmodified font files shipped as data under /fonts of the renderer JAR, with these SHA-256: IBM Plex Sans Regular 975dcda37d80f038dcd143c22e33ca2d97a0cc5a929aace1c749153b0fe1afa5 and Bold 9e6c74a889a700d707613d24548fe4ffa6bc59559a0689d2cf9e133bdcdafb2f (release @ibm/plex-sans@1.1.0); IBM Plex Sans Condensed Bold 723e245902b45a49f8cf602d15891bae08c31309dce7c56d209d10cd471b88c7 (release @ibm/plex-sans-condensed@2.0.0); JetBrains Mono Regular a0bf60ef0f83c5ed4d7a75d45838548b1f6873372dfac88f71804491898d138f and Bold 5590990c82e097397517f275f430af4546e1c45cff408bde4255dad142479dcb (release v2.304). No other font, version, modified or renamed font, or other licence is covered. A font is data read by the renderer; it is not linked code.
Reason: Version 2 label text is rasterised by the renderer so that it can be measured, fitted and previewed truthfully, which a printer's built-in font cannot be. That needs real bold, condensed and monospace faces with clear 0/O and 1/l/I that work with no object storage and no installed font. The OFL expressly permits bundling and redistribution with software. The alternative, restricting the set to fonts under Apache-2.0, gives up the faces chosen for legibility on thermal print.
Risk: The OFL's conditions apply to every distribution of the renderer: the copyright and licence notices must accompany the fonts, the fonts may not be sold by themselves, a modified version may not use a Reserved Font Name ("Plex"), and the fonts and their derivatives stay under the OFL. A later change to a file (subsetting, renaming, format conversion) would make it a Modified Version and could breach the Reserved Font Name condition.
Compensating controls: The files are taken unmodified from the projects' official release archives; each licence was confirmed from the archive's own licence file, which is shipped byte for byte beside the fonts with a source note (release, archive URL and hash, path, font version); /fonts/SHA256SUMS lists every hash; a test (FaceTest) fails the build if a licence, a source note or a hash is missing or any byte differs; .gitattributes forbids git from rewriting the files; renderer/THIRD-PARTY.md and renderer/NOTICE carry the attribution. The scene names (qqq-sans and so on) name the renderer's faces and are not font names.
Expires: Reviewed yearly, next on 2027-11-12, or on any change to a covered file, its version or its licence, whichever occurs first.
Owner: the maintainer of QRun-IO/qbit-document-generation.
Review date: 2026-11-12.
Evidence: the release archives named in renderer/THIRD-PARTY.md and their licence files; renderer/src/main/resources/fonts/*/SOURCE.md; this pull request.
The design's decision 1 proposed that one exception also cover the JetBrains Mono Bold that qbit-printing already ships. This entry is exact for what this pull request verified; that repository's file and hash were not read here and would be added to the scope by its maintainer.
How text is measured and drawn
Integer arithmetic. The faces have 1000 units to the em. Advances and outline boxes are read once at a reference size and scaled as units x millipoints x dpi / 72 000 000, so a measurement is the same on every machine and never depends on the font engine at the drawing size.
Whole-dot pens. Each character sits at the nearest dot to its exact pen position and is drawn there by the Java rasteriser that version 1 raster text uses, antialiased, then reduced to ink at threshold.
A line's box is its pen advance widened to the ink of every character in it, plus two dots on each inked side: the rasteriser fits an outline to the dot grid within 1.4 dots of its exact scale (measured over every character of the five faces). A text whose glyph bitmap or ink would leave the measured box is not clipped, not printed outside it and not printed in part: the one element is refused (TEXT_DOES_NOT_FIT with glyphsOutsideMeasure and dotsOutsideMeasure).
One glyph for each character, left to right. No kerning, ligatures, mark placement or bidirectional reordering, as in version 1 raster text.
Determinism across platforms
Same bytes everywhere tested, with no tolerance:
Runtime
Result
Linux, Temurin 21.0.9 (maven:3.9.11-eclipse-temurin-21, the CI image)
the 18 text golden files (ZPL and PNG at 203, 300 and 600 dpi) were generated here
Windows 11, Temurin 21.0.12
byte-identical ZPL and PNG for all 18
Linux, Temurin 21.0.13 (eclipse-temurin:21-jdk; the version the worker requires)
the golden test passes exactly
Before writing the code, a probe hashed glyph metrics, glyph bitmaps and filled outlines for the five faces at ten sizes and three resolutions on Windows and in the CI image: all identical, for the bitmap path (up to 100 dots to the em) and for the outline path above it.
One case depends on more than the scene, stated plainly: a line whose strip is over 8 million dots (type of several hundred points) is drawn in bands of whole rows so that it never needs more grey than that, and the rasteriser may shade a few edge dots at a band's border differently than it would in one piece (4 of 121 thousand ink dots at 203 dpi, 40 of 1.06 million at 600 dpi, for 200 pt type forced into small bands). Where the bands fall is a function of the line's box alone, so the result is still the same on every machine; up to 100 dots to the em banding changes nothing at all, which a test holds row by row.
Tests
Goldens at 203, 300 and 600 dpi: every bundled face (text-faces), the fit rules (text-fit), rotation, reverse and a resolution override (text-turned). Exact comparison of ZPL and picture.
Measurement equals what is drawn: every character of every face is drawn at seven size and resolution pairs and its ink read back inside its measured line; every character's bitmap box is held to the measured box at thirty more.
Property tests of fit (seeded, no clock, 3 seeds x 600 cases): the answer is a layout inside the box or a refusal whose measurement exceeds its limit; a text that fits is never shrunk; the size chosen is the largest that fits and never below the floor; never more lines than allowed; cut only when asked, always with the mark, always fitting; a shorter text never gets a smaller size than a longer one that starts with it.
maxLength: a proven length is never refused for any text of the face's characters up to that length (the widest characters, the furthest-reaching ones, random ones, with and without spaces, cut or not); the proof agrees with rendering of the widest character to within one character a line.
Ink is never outside the box and never clipped, at every rotation, for 160 generated cases; alignment and centring within one dot.
Licence, source note and hash of every bundled face; the face-wide bounds the proof relies on are recomputed from every character of the font files.
Mutation campaign extended to text: three text scenes in the corpus, text tokens spliced in, an independent reading of every accepted text element at each resolution. Only a Refusal counts as a refusal.
Version 1 corpus unchanged and green.
Build
The CI command, ./mvnw --batch-mode --no-transfer-progress -s <settings> -T1C verify, on head 589b6d4 (the head after the independent review), with no gate skipped:
Where
Result
Linux, Temurin 21.0.9, inside maven:3.9.11-eclipse-temurin-21 (the CI image), on a git archive of the head
Windows 11, Temurin 21.0.12, in the worktree, from a clean tree
BUILD SUCCESS, all 11 modules, the same 925 tests, 0 failures, 0 errors, 0 skipped
Renderer module: 360 tests. Coverage of the scene 2 packages, the same in both builds: scene2 632 of 632 lines, scene2.text 465 of 465 lines, the placeholders 8 of 8; the build's 100% line gate is met with nothing excluded. Branches of scene2.text: 360 of 371. Of the eleven not taken, six are sides of the checks in TextRaster that count ink outside the measure (a test forces them with lines whose boxes are stated too small, on some sides and not all), and five are in TextElement.paint, which reuses what check composed only for the same box, request and collector and would otherwise compose again: a case the scene walk never produces.
The first head of this pull request, f5acf93, was built the same way on both (916 tests). The branch was rebased onto develop after #15 merged. develop has since gained #17; this branch was not rebased again, because it merges into the new develop without conflict, and on that merged tree the bridge's tests and the scene 2 schema, golden and text element tests pass (353 tests).
The independent review that section 10.8 of the design asks of every package (by an agent that did not write it) has been done. Its verdict was "fix first"; what changed is in the last section.
Where the design and the code differ
The design's example scene does not validate. Its caption (two lines, floor 7 pt, maxLength 48) is in a 6.5 mm box; two lines at 7 pt need 59 dots at 203 dpi (53 from rounding the ascent, line pitch and descent up to whole dots, 6 more for an accent above a capital and a deep descender) and the box has 51. With a 7.5 mm box it is proven at all three resolutions. The proof is sound for any value, so it is stricter than a typical value needs: a box of exactly lines x line height is not proven.
A face is named by font alone. The merged schema has no weight or style property; the name carries family and weight and the set has no italic.
Wrapping was not specified beyond "wrap to at most this many lines". Words first, then any character at the same size; this is what lets an unspaced value wrap and what makes a length provable.
RESERVED_FONT_NAME is raised by a text element (with the count), not for a scene as a whole.
The members of fit are declared in the schema (changed after the independent review). maxLines, shrinkTo and overflow are under properties of fit with additionalProperties: false; the three names are in ElementFactory.VOCABULARY and in the bridge's SceneFacts.VOCABULARY, which a bridge test holds equal to the schema; and a wrong member is reported at the member (/elements/1/fit/maxLines). The first head described them in the schema's prose and reported everything at /fit.
Section 10.2 expected text goldens to need a tolerance on developer machines. They do not.
The proof is for the render target of the request. A host that prints one revision at several resolutions should validate at each; that belongs to the bridge and foundation packages.
Core seams (two small additions to P1's ElementSource, used by the parser): inherit, which gives an element the scene's defaults for the properties its type reads, and refuse, a coded refusal at a property. defaults was parsed by the core but could not reach an element.
The design is updated in section 3.3 ("Decided while building text").
Deliberately left out
Uploaded and supplied fonts (P8). Only bundled faces are drawn; a supplied font/ttf asset named by a version 2 text is FONT_UNKNOWN. The seam is in place: RenderInput.reservedAssetNames() and the RESERVED_FONT_NAME refusal.
Shaping: kerning, ligatures, combining marks, right-to-left and complex scripts, characters outside the Basic Multilingual Plane. They are refused by count, never drawn wrongly.
Explicit line breaks in a value (a line feed is a character the face cannot draw).
Warm-up: the worker's warm-up fixture does not draw version 2 text, so the first text of a request loads its face (the first sample label took 240 ms in a cold JVM, later ones 5 to 30 ms). WarmUp is outside this package's files.
Page (PDF) CSS families for the same faces (P11), the GS1 human-readable line (P3b), starters and the specimen (P12).
The physical print check of section 10.6, which confirms the legibility thresholds on real printers.
No container image was built or pushed.
Changes after the independent review
The independent review that section 10.8 of the design asks for was done by an agent that did not write this package. Its verdict was fix first. Every finding and note is closed in one commit, 589b6d4, on top of f5acf93. No golden changed.
1. The maxLength proof never cuts
With overflow: "truncate" the proof was handed a cut block, which counts as fitting, so it proved nothing: a bound element with maxLength: 40 in a box that holds ten worst-case characters validated clean, and every long value was cut at print.
The proof now runs with cutting off, always.
When it fails and the element did not ask for cutting, the scene is refused as before (TEXT_DOES_NOT_FIT).
When it fails and the element did ask for cutting, the scene still validates, because cutting is what its author chose, but every validation carries a standing warning: FIT_NOT_PROVEN with measured.provenCharacters (the longest length proven to fit whole) against limit.characters (the maxLength stated). Fewer characters never fit worse, so that length is found by halving.
No new code.FIT_NOT_PROVEN already means "the fit of this bound value is not proven"; it now carries numbers when a length was stated. Foundation's closed FailureCode and the host's lists are unchanged.
Tests (TextElementTest.theProofOfALengthNeverCutsAndACuttingElementIsWarnedHowMuchIsProven): the refusal without cutting; the warning with cutting, with the number (7 of 40 in the test's box); that the number is the truth (that length validates clean with and without cutting, one more does not); that a value of the proven length prints whole and a longer one is cut; more lines prove more; a box that holds no character proves 0.
2. The shrink search is bounded
Stepping 0.25 pt from 1000 pt to a 1 pt floor is four thousand sizes, each laid out twice, and every element was composed twice (check, then paint).
Before
After
One fit of 1024 characters at 600 dpi, 1000 pt to 1 pt (36 boxes and texts)
180 to 250 ms; at least 15.6 million characters visited
0.13 to 0.2 ms on average in the builds, under 1 ms cold; at most 21 555 characters visited
A scene of 1000 such elements, validated
minutes (killed by the worker's 15 s of processor, no refusal named)
200 ms in the CI image, 281 ms on Windows; every element refused by name as TEXT_DOES_NOT_FIT, none for the work
Fits per element per render
2
1
Refused at once when one line at the floor size is higher than the box, whatever the text: nothing is searched. A cut tries no line count the box is too low for.
Halving, then a short linear check. The size chosen is defined as the one a search of every quarter-point step gives, and is exactly that. A count that errs only toward yes says whether a size could hold the text: lines filled at any character, each held to a width no real line can be under (its advance, or the exact span of its ink plus the rasteriser's allowance less one dot of rounding), and a block held to its lines' ascent, pitches and descent. It never refuses a size that fits, and once it allows a size it allows every smaller one, so the largest size it allows is found by halving and no size above that fits. Only the steps from there down are laid out, as before.
Equivalence is tested, not assumed.TextFitTest.theAnswerIsExactlyThatOfTheSearchOfEverySize holds fit to the old linear search, block for block (size, lines, every number; fitted, refused or cut), over 3600 generated cases: texts with and without spaces, the widest and furthest-reaching characters, the proof's worst character, boxes from one dot to a large label, sizes from 1 pt to 1000 pt, any floor, 1 to 64 lines, any line height. The linear search in the test shares only the layout at one size with fit. The same comparison was soaked over 4.3 million further cases with other seeds, weighted to type of 1 to 12 pt where rounding decides: no difference.
The layout is cached between check and paint (TextElement.check keeps what it composed for the paint of the same pass).
A work budget for the text of one scene, with a named refusal. A character visited while fitting is one step, a character to be drawn 16, and a glyph the scene draws for the first time at a size 4096 (the runtime rasterises it once: 20 to 50 microseconds measured, against 0.25 after). One scene may take 64 million steps, about a second of a worker. The element that passes it, and every text after it, is SCENE_INVALID at the element with measured.textWorkSteps against limit.textWorkSteps, the same shape as the painted-area limit. The count is integer arithmetic, so the refusal is the same on every machine, and it is the same whether the scene is validated or rendered. No scene the parser accepts can exceed it.
Tests: TextFitTest.theWorstCaseOfTheShrinkSearchIsHeldToAStatedWork (the reviewer's case: at most 40 times the text visited, where the linear search visits over 10 000 times it; same answer), aBoxLowerThanOneLineAtTheFloorIsRefusedWithoutASearch, fittingEndsWhenItsAllowanceIsPassed; TextElementTest.aThousandOfTheWorstTextsAreRefusedByNameInsideTheWorkOfOneScene (1000 elements, validated and rendered, each refused as TEXT_DOES_NOT_FIT, none for the work, inside 15 s) and aSceneWhoseTextPassesItsWorkIsRefusedByName (the refusal, its numbers, the same validated and rendered, every later text refused too).
3. Notes closed
Widest advance over every drawable character.FaceTest took the widest character over inked characters only; a no-break space or an em quad has no ink and still moves the pen. The greatest advance is now taken over every character a face draws, and the widest inked character must have it, so a font update cannot break the proof unseen.
Ink outside the measure. The bound is now tested at 1, 2, 300 and 1000 pt as well as 5 to 72 pt, for every character of every face at 203, 300 and 600 dpi (glyph bounds), with every character drawn at 1 and 2 pt and the furthest-reaching ones at 300 and 1000 pt. Nothing strays, on Windows or in the CI image: the furthest any glyph reaches past its exact outline is one dot of the two allowed, at every one of those sizes. TEXT_INK_OUTSIDE_MEASURE is no longer an IllegalStateException that fails the whole render with part of the text already on the label: the dots of a text are gathered first and reach the label only when none strayed; otherwise the one element is refused as TEXT_DOES_NOT_FIT with measured.glyphsOutsideMeasure and measured.dotsOutsideMeasure, and the label holds none of it. A literal is held to the glyph bounds when the scene is validated; a bound value can only be checked when it is known, at render.
Legibility at the proven size. A bound element with maxLength is judged, when validated, at the size the proof shrank to (the floor when the proof fails and the element cuts), which is the smallest any value prints at. Without maxLength it is still judged at its stated size, since nothing says how far it will shrink.
fit is declared in the schema.maxLines, shrinkTo and overflow are under properties with additionalProperties: false. The three names are in ElementFactory.VOCABULARY and in the bridge's SceneFacts.VOCABULARY in the same change (SceneFactsTest.theVocabularyIsThePropertyNamesOfTheSchema holds them equal: 46 names). A wrong member is reported at the member: /elements/0/fit/maxLines, or /overrides/0/set/<id>/fit/maxLines when an override states fit. A member fit does not have is still reported at /fit, because its name is not one the schema declares.
Bound numbers print as the schema says. A number is printed in plain decimal digits: no exponent, no fraction that is only zeros. 1.0E10 prints 10000000000, 2.50 prints 2.5, 12.0 prints 12, 1E-7 prints 0.0000001; an integer prints its digits however long. A number that is not finite is VALUE_MISSING. The schema's description now says exactly this.
FreeType and JDK of the goldens are recorded in renderer/src/test/resources/scene2/README.md, beside the goldens (the golden directory itself is held by a test to exactly the case files). Java draws glyphs with the FreeType the JDK itself ships, not the system's: 2.13.3 in Temurin 21.0.9+10 (the CI image, where the goldens were generated), 2.14.3 in Temurin 21.0.12.1+1 (Windows) and in Temurin 21.0.13+9 (the version the worker image requires). The same bytes on all three.
Decisions made here, for the maintainers
No diagnostic code was added. The three new refusals and the new warning use codes that exist, told apart by their named numbers. A dedicated code for the work limit or for ink outside the measure would mean a change to foundation's closed FailureCode, the schema and both vocabularies; say so if you want them.
Where the scene's work is counted. A text element is given nothing that belongs to its scene except the collector of the render or validation, so the count is kept in the text package against that collector and forgotten with it. A counter on the canvas beside the painted-area count would be the plainer home; it was not added because other packages are changing the core at the same time.
Two small changes outside the text package, both needed for the pointer at a member: ElementSource resolves fit/maxLines where fit is stated (element, defaults or override), and ElementFactory.VOCABULARY gains the three names.
The limit on glyphs is new behaviour. A scene that draws more than about fifteen thousand different glyph-and-size pairs is now refused where it was slow before. A label of a hundred values in a handful of sizes uses a few percent of the allowance.
Goldens
None changed and none was regenerated. The 18 text files and the 24 layout files are byte-identical to f5acf93; SceneGoldenTest passes on them in both builds above, and the 21 labels were rendered again on Temurin 21.0.13+9 with the same bytes.
Build
The results under "Build" above are for this head, 589b6d4, on both platforms. The version 1 corpus is untouched and green.
Package **P2** of `docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md` (work plan B): text for version 2 label scenes. Bundled fonts, measurement before drawing, and a stated rule for a value that does not fit. Builds on the scene 2 core (#14); version 1 is untouched.
## What a label author gets
- **Real fonts.** Five faces ship inside the renderer, so a label needs no printer font, no installed font and no object storage: `qqq-sans`, `qqq-sans-bold`, `qqq-condensed-bold`, `qqq-mono`, `qqq-mono-bold`.
- **Sizes in points or millimetres.** `"size": "9pt"` or `"3mm"`, resolved at the render target (203, 300 or 600 dpi) and not rounded to dots.
- **A text is measured before it is drawn** and placed in its box by `align` and `valign`. Ink never leaves the box and is never clipped.
- **A stated rule when a value does not fit**, chosen per element (table below), with a diagnostic that names the element, what was measured and the limit.
- **`maxLength`** proves the fit when the document is published, so a length refusal is a design-time event and not a failed print.
- **Thermal legibility controls**: `threshold` (heavier strokes), bold faces, warnings for small type, and resolution overrides for either.
- **Rotation** (0, 90, 180, 270) and **reverse** text (white on a black box).
## Fit rules as implemented
| Step | Rule | Reported as |
| --- | --- | --- |
| 1 | Lay the value out at `size` within `fit.maxLines` (default 1). If it fits, it is never shrunk. | nothing |
| 2 | Otherwise take the **largest** size, in steps of 0.25 pt down to `fit.shrinkTo`, at which it fits. `shrinkTo` defaults to 70% of `size` and not below 5 pt (the size itself when the size is under 5 pt). | `TEXT_SHRUNK` (info): chosen size, nominal size, floor |
| 3 | If nothing fits down to the floor, apply `fit.overflow`: `refuse` (the default) or `truncate`. | `TEXT_DOES_NOT_FIT` (error): measured width, height, lines and size against the box |
| Wrap | Only with `maxLines` above 1. At each size a line is broken between words while the text then fits, and at any character when it does not. So a value with no space wraps, and a larger size is preferred to whole words. Never more than `maxLines` lines. | `TEXT_WRAPPED` (info): lines |
| Truncate | Only with `overflow: "truncate"`, only at the floor size: as many lines as the box holds, the last ending with an ellipsis after as many of its characters as fit with it. The mark is always shown and the result always fits; where not even the mark fits, the text is refused. | `TEXT_TRUNCATED` (warning): characters, characters shown |
| Length | `maxLength` (1 to 1024). **Validation** lays out that many of the face's widest character, each taken to reach as far in every direction as the furthest-reaching character of the face, and refuses the scene if they do not fit. **Printing** refuses a longer value before any layout. The proof never cuts: with `overflow: "truncate"` a length that is not proven does not refuse the scene but warns at every validation, with the length that is proven. | `TEXT_DOES_NOT_FIT` at validation, `VALUE_TOO_LONG` at print, `FIT_NOT_PROVEN` (warning) for a bound element with no `maxLength`, and with `provenCharacters` for a cutting element whose `maxLength` is not proven |
Other refusals: `VALUE_MISSING` (the pointer finds no text, number or true/false), `FONT_MISSING_GLYPH` (a character the face cannot draw as one glyph, with the count), `FONT_UNKNOWN` (a face that is not bundled, at the place that names it), `RESERVED_FONT_NAME` (a supplied asset whose name begins `qqq-`). Warnings: `TEXT_SMALL_FOR_DPI` (capitals under 12 dots), `TEXT_LIGHT_FACE` (a regular weight with capitals under 20 dots). A diagnostic holds ids and numbers only; no value ever enters a diagnostic, a log line or an exception message, and a test puts a marker through every refusal to hold that.
## A rendered sample
`renderer/src/test/resources/scene2/cases/text-fit.json` on 2 x 1 in stock with 2 mm margins, at 203 dpi (golden: `scene2/golden/text-fit-203.png`; the 300 and 600 dpi goldens are beside it). Thin outlines show each element's box.
| Element | Scene | What was drawn | Fit applied |
| --- | --- | --- | --- |
| `kept` | `LOT 240611`, `qqq-sans-bold` 10 pt, 22 x 5 mm, centred | 10 pt, one line, centred within one dot | none |
| `right` | `9.5 kg`, `qqq-condensed-bold` 11 pt, 22 x 5 mm, right and bottom | 10.25 pt | `TEXT_SHRUNK`: 11 pt is 41 dots high and the box is 39 |
| `shrunk` | `(01)00012345678905(10)L240611A`, `qqq-mono-bold` 9 pt, 44 x 4 mm | 6.75 pt, one line | `TEXT_SHRUNK` from 9 pt (floor 6.3 pt) |
| `wrapped` | 46 characters with no space, `qqq-mono` 7 pt, 36 x 7.5 mm, `maxLines: 2`, `maxLength: 46` | 7 pt, two lines of 24 and 22 characters, each centred | `TEXT_WRAPPED`; also `TEXT_LIGHT_FACE` (capitals 14 dots, regular weight) |
| `cut` | a 42-character description, `qqq-sans` 7 pt, 30 x 3.6 mm, `overflow: "truncate"` | `A description of the item…` | `TEXT_TRUNCATED`: 25 of 42 characters shown |
The diagnostic of a refusal, here the design's own example caption (two lines, floor 7 pt, `maxLength` 48, 94 x 6.5 mm) when validated at 203 dpi:
```json
{ "code": "TEXT_DOES_NOT_FIT", "severity": "ERROR", "element": "caption", "box": [6, 100, 751, 51],
"measured": { "characters": 48, "heightDots": 59, "lines": 2, "sizeCentipoints": 700, "widthDots": 751 },
"limit": { "heightDots": 51, "lines": 2, "widthDots": 751 } }
```
## Bundled fonts
| Scene name | Family and style | File (under `/fonts` in the jar) | Version | Source | SHA-256 | Licence |
| --- | --- | --- | --- | --- | --- | --- |
| `qqq-sans` | IBM Plex Sans Regular | `ibm-plex-sans/IBMPlexSans-Regular.ttf` | release `@ibm/plex-sans@1.1.0`, font 3.005 | https://github.com/IBM/plex/releases/download/%40ibm/plex-sans%401.1.0/ibm-plex-sans.zip | `975dcda37d80f038dcd143c22e33ca2d97a0cc5a929aace1c749153b0fe1afa5` | SIL OFL 1.1 |
| `qqq-sans-bold` | IBM Plex Sans Bold | `ibm-plex-sans/IBMPlexSans-Bold.ttf` | the same release, font 3.005 | the same archive | `9e6c74a889a700d707613d24548fe4ffa6bc59559a0689d2cf9e133bdcdafb2f` | SIL OFL 1.1 |
| `qqq-condensed-bold` | IBM Plex Sans Condensed Bold | `ibm-plex-sans-condensed/IBMPlexSansCondensed-Bold.ttf` | release `@ibm/plex-sans-condensed@2.0.0`, font 3.000 | https://github.com/IBM/plex/releases/download/%40ibm/plex-sans-condensed%402.0.0/ibm-plex-sans-condensed.zip | `723e245902b45a49f8cf602d15891bae08c31309dce7c56d209d10cd471b88c7` | SIL OFL 1.1 |
| `qqq-mono` | JetBrains Mono Regular | `jetbrains-mono/JetBrainsMono-Regular.ttf` | release `v2.304`, font 2.304 | https://github.com/JetBrains/JetBrainsMono/releases/download/v2.304/JetBrainsMono-2.304.zip | `a0bf60ef0f83c5ed4d7a75d45838548b1f6873372dfac88f71804491898d138f` | SIL OFL 1.1 |
| `qqq-mono-bold` | JetBrains Mono Bold | `jetbrains-mono/JetBrainsMono-Bold.ttf` | the same release, font 2.304 | the same archive | `5590990c82e097397517f275f430af4546e1c45cff408bde4255dad142479dcb` | SIL OFL 1.1 |
Archive SHA-256: `ibm-plex-sans.zip` `fb365d910566e6d199cc2c15579a7dd9a267128e18431a394ed81f1970c69200`, `ibm-plex-sans-condensed.zip` `f86ba73abbeed7cf7690ead863192a87211dfc2b0cef0ac6bf4700533ec5ab9e`, `JetBrainsMono-2.304.zip` `6f6376c6ed2960ea8a963cd7387ec9d76e3f629125bc33d1fdcd7eb7012f7bbf`.
**How each licence was confirmed.** From the release archive itself, not from memory. Each IBM Plex archive's `LICENSE.txt` and the JetBrains Mono archive's `OFL.txt` state "This Font Software is licensed under the SIL Open Font License, Version 1.1" and carry the full text. The licence field of every font's name table says the same, and every font's embedding bits (`OS/2.fsType`) are 0, installable embedding. The licence files ship byte for byte beside the fonts (`.gitattributes` keeps git from rewriting anything under `/fonts`), each directory has a `SOURCE.md` with the release, archive, path and hashes, and `/fonts/SHA256SUMS` lists the hashes. `FaceTest` fails the build when a licence, a source note or a hash is missing or a byte changes. The files are unmodified and keep their names; IBM Plex's Reserved Font Name "Plex" is not used for anything derived. The notes are named `SOURCE.md` because this repository's `.gitignore` ignores `PROVENANCE.md`.
The fonts add **531,819 bytes** to the renderer jar (1,172,935 uncompressed), which is now 736 KB.
### Dependency-licence exception (K22-DEP-02), for the maintainer to file
The SIL OFL 1.1 is not on the K22-DEP-02 allowlist. The exception register is `exceptions/register.md` in the standards repository, which this pull request does not edit. Proposed entry, with the next free number:
> ## EX-00NN
>
> **Unmodified SIL Open Font License 1.1 fonts shipped as data in the document renderer**
>
> - **Status:** Open upon merge of this record. The maintainer approved the exception and named the set on 2026-11-12.
> - **Requirements:** `K22-DEP-02`. The SIL Open Font License 1.1 (SPDX `OFL-1.1`) is not on the allowlist of licences for dependencies that ship with a product. No vulnerability, provenance, signing or branch-protection requirement is waived.
> - **Scope:** `QRun-IO/qbit-document-generation`, module `renderer`, exactly these unmodified font files shipped as data under `/fonts` of the renderer JAR, with these SHA-256: IBM Plex Sans Regular `975dcda37d80f038dcd143c22e33ca2d97a0cc5a929aace1c749153b0fe1afa5` and Bold `9e6c74a889a700d707613d24548fe4ffa6bc59559a0689d2cf9e133bdcdafb2f` (release `@ibm/plex-sans@1.1.0`); IBM Plex Sans Condensed Bold `723e245902b45a49f8cf602d15891bae08c31309dce7c56d209d10cd471b88c7` (release `@ibm/plex-sans-condensed@2.0.0`); JetBrains Mono Regular `a0bf60ef0f83c5ed4d7a75d45838548b1f6873372dfac88f71804491898d138f` and Bold `5590990c82e097397517f275f430af4546e1c45cff408bde4255dad142479dcb` (release `v2.304`). No other font, version, modified or renamed font, or other licence is covered. A font is data read by the renderer; it is not linked code.
> - **Reason:** Version 2 label text is rasterised by the renderer so that it can be measured, fitted and previewed truthfully, which a printer's built-in font cannot be. That needs real bold, condensed and monospace faces with clear `0/O` and `1/l/I` that work with no object storage and no installed font. The OFL expressly permits bundling and redistribution with software. The alternative, restricting the set to fonts under Apache-2.0, gives up the faces chosen for legibility on thermal print.
> - **Risk:** The OFL's conditions apply to every distribution of the renderer: the copyright and licence notices must accompany the fonts, the fonts may not be sold by themselves, a modified version may not use a Reserved Font Name ("Plex"), and the fonts and their derivatives stay under the OFL. A later change to a file (subsetting, renaming, format conversion) would make it a Modified Version and could breach the Reserved Font Name condition.
> - **Compensating controls:** The files are taken unmodified from the projects' official release archives; each licence was confirmed from the archive's own licence file, which is shipped byte for byte beside the fonts with a source note (release, archive URL and hash, path, font version); `/fonts/SHA256SUMS` lists every hash; a test (`FaceTest`) fails the build if a licence, a source note or a hash is missing or any byte differs; `.gitattributes` forbids git from rewriting the files; `renderer/THIRD-PARTY.md` and `renderer/NOTICE` carry the attribution. The scene names (`qqq-sans` and so on) name the renderer's faces and are not font names.
> - **Expires:** Reviewed yearly, next on 2027-11-12, or on any change to a covered file, its version or its licence, whichever occurs first.
> - **Owner:** the maintainer of `QRun-IO/qbit-document-generation`.
> - **Review date:** 2026-11-12.
> - **Evidence:** the release archives named in `renderer/THIRD-PARTY.md` and their licence files; `renderer/src/main/resources/fonts/*/SOURCE.md`; this pull request.
The design's decision 1 proposed that one exception also cover the JetBrains Mono Bold that `qbit-printing` already ships. This entry is exact for what this pull request verified; that repository's file and hash were not read here and would be added to the scope by its maintainer.
## How text is measured and drawn
- **Integer arithmetic.** The faces have 1000 units to the em. Advances and outline boxes are read once at a reference size and scaled as `units x millipoints x dpi / 72 000 000`, so a measurement is the same on every machine and never depends on the font engine at the drawing size.
- **Whole-dot pens.** Each character sits at the nearest dot to its exact pen position and is drawn there by the Java rasteriser that version 1 raster text uses, antialiased, then reduced to ink at `threshold`.
- **A line's box** is its pen advance widened to the ink of every character in it, plus two dots on each inked side: the rasteriser fits an outline to the dot grid within 1.4 dots of its exact scale (measured over every character of the five faces). A text whose glyph bitmap or ink would leave the measured box is not clipped, not printed outside it and not printed in part: the one element is refused (`TEXT_DOES_NOT_FIT` with `glyphsOutsideMeasure` and `dotsOutsideMeasure`).
- **One glyph for each character, left to right.** No kerning, ligatures, mark placement or bidirectional reordering, as in version 1 raster text.
## Determinism across platforms
Same bytes everywhere tested, with no tolerance:
| Runtime | Result |
| --- | --- |
| Linux, Temurin 21.0.9 (`maven:3.9.11-eclipse-temurin-21`, the CI image) | the 18 text golden files (ZPL and PNG at 203, 300 and 600 dpi) were generated here |
| Windows 11, Temurin 21.0.12 | byte-identical ZPL and PNG for all 18 |
| Linux, Temurin 21.0.13 (`eclipse-temurin:21-jdk`; the version the worker requires) | the golden test passes exactly |
Before writing the code, a probe hashed glyph metrics, glyph bitmaps and filled outlines for the five faces at ten sizes and three resolutions on Windows and in the CI image: all identical, for the bitmap path (up to 100 dots to the em) and for the outline path above it.
One case depends on more than the scene, stated plainly: a line whose strip is over 8 million dots (type of several hundred points) is drawn in bands of whole rows so that it never needs more grey than that, and the rasteriser may shade a few edge dots at a band's border differently than it would in one piece (4 of 121 thousand ink dots at 203 dpi, 40 of 1.06 million at 600 dpi, for 200 pt type forced into small bands). Where the bands fall is a function of the line's box alone, so the result is still the same on every machine; up to 100 dots to the em banding changes nothing at all, which a test holds row by row.
## Tests
- **Goldens** at 203, 300 and 600 dpi: every bundled face (`text-faces`), the fit rules (`text-fit`), rotation, reverse and a resolution override (`text-turned`). Exact comparison of ZPL and picture.
- **Measurement equals what is drawn**: every character of every face is drawn at seven size and resolution pairs and its ink read back inside its measured line; every character's bitmap box is held to the measured box at thirty more.
- **Property tests of fit** (seeded, no clock, 3 seeds x 600 cases): the answer is a layout inside the box or a refusal whose measurement exceeds its limit; a text that fits is never shrunk; the size chosen is the largest that fits and never below the floor; never more lines than allowed; cut only when asked, always with the mark, always fitting; a shorter text never gets a smaller size than a longer one that starts with it.
- **`maxLength`**: a proven length is never refused for any text of the face's characters up to that length (the widest characters, the furthest-reaching ones, random ones, with and without spaces, cut or not); the proof agrees with rendering of the widest character to within one character a line.
- **Ink is never outside the box and never clipped**, at every rotation, for 160 generated cases; alignment and centring within one dot.
- **Licence, source note and hash** of every bundled face; the face-wide bounds the proof relies on are recomputed from every character of the font files.
- **Mutation campaign** extended to text: three text scenes in the corpus, text tokens spliced in, an independent reading of every accepted text element at each resolution. Only a `Refusal` counts as a refusal.
- **Version 1 corpus** unchanged and green.
## Build
The CI command, `./mvnw --batch-mode --no-transfer-progress -s <settings> -T1C verify`, on head `589b6d4` (the head after the independent review), with no gate skipped:
| Where | Result |
| --- | --- |
| Linux, Temurin 21.0.9, inside `maven:3.9.11-eclipse-temurin-21` (the CI image), on a `git archive` of the head | BUILD SUCCESS, all 11 modules, 925 tests, 0 failures, 0 errors, 0 skipped |
| Windows 11, Temurin 21.0.12, in the worktree, from a clean tree | BUILD SUCCESS, all 11 modules, the same 925 tests, 0 failures, 0 errors, 0 skipped |
Renderer module: 360 tests. Coverage of the scene 2 packages, the same in both builds: `scene2` 632 of 632 lines, `scene2.text` 465 of 465 lines, the placeholders 8 of 8; the build's 100% line gate is met with nothing excluded. Branches of `scene2.text`: 360 of 371. Of the eleven not taken, six are sides of the checks in `TextRaster` that count ink outside the measure (a test forces them with lines whose boxes are stated too small, on some sides and not all), and five are in `TextElement.paint`, which reuses what `check` composed only for the same box, request and collector and would otherwise compose again: a case the scene walk never produces.
The first head of this pull request, `f5acf93`, was built the same way on both (916 tests). The branch was rebased onto `develop` after #15 merged. `develop` has since gained #17; this branch was not rebased again, because it merges into the new `develop` without conflict, and on that merged tree the bridge's tests and the scene 2 schema, golden and text element tests pass (353 tests).
The independent review that section 10.8 of the design asks of every package (by an agent that did not write it) has been done. Its verdict was "fix first"; what changed is in the last section.
## Where the design and the code differ
- **The design's example scene does not validate.** Its caption (two lines, floor 7 pt, `maxLength` 48) is in a 6.5 mm box; two lines at 7 pt need 59 dots at 203 dpi (53 from rounding the ascent, line pitch and descent up to whole dots, 6 more for an accent above a capital and a deep descender) and the box has 51. With a 7.5 mm box it is proven at all three resolutions. The proof is sound for any value, so it is stricter than a typical value needs: a box of exactly `lines x line height` is not proven.
- **A face is named by `font` alone.** The merged schema has no weight or style property; the name carries family and weight and the set has no italic.
- **Wrapping** was not specified beyond "wrap to at most this many lines". Words first, then any character at the same size; this is what lets an unspaced value wrap and what makes a length provable.
- **`RESERVED_FONT_NAME`** is raised by a text element (with the count), not for a scene as a whole.
- **The members of `fit` are declared in the schema** (changed after the independent review). `maxLines`, `shrinkTo` and `overflow` are under `properties` of `fit` with `additionalProperties: false`; the three names are in `ElementFactory.VOCABULARY` and in the bridge's `SceneFacts.VOCABULARY`, which a bridge test holds equal to the schema; and a wrong member is reported at the member (`/elements/1/fit/maxLines`). The first head described them in the schema's prose and reported everything at `/fit`.
- **Section 10.2** expected text goldens to need a tolerance on developer machines. They do not.
- **The proof is for the render target of the request.** A host that prints one revision at several resolutions should validate at each; that belongs to the bridge and foundation packages.
- **Core seams** (two small additions to P1's `ElementSource`, used by the parser): `inherit`, which gives an element the scene's `defaults` for the properties its type reads, and `refuse`, a coded refusal at a property. `defaults` was parsed by the core but could not reach an element.
- The design is updated in section 3.3 ("Decided while building text").
## Deliberately left out
- **Uploaded and supplied fonts** (P8). Only bundled faces are drawn; a supplied `font/ttf` asset named by a version 2 text is `FONT_UNKNOWN`. The seam is in place: `RenderInput.reservedAssetNames()` and the `RESERVED_FONT_NAME` refusal.
- **Shaping**: kerning, ligatures, combining marks, right-to-left and complex scripts, characters outside the Basic Multilingual Plane. They are refused by count, never drawn wrongly.
- **Explicit line breaks** in a value (a line feed is a character the face cannot draw).
- **Warm-up**: the worker's warm-up fixture does not draw version 2 text, so the first text of a request loads its face (the first sample label took 240 ms in a cold JVM, later ones 5 to 30 ms). `WarmUp` is outside this package's files.
- **Page (PDF) CSS families** for the same faces (P11), the GS1 human-readable line (P3b), starters and the specimen (P12).
- **The physical print check** of section 10.6, which confirms the legibility thresholds on real printers.
- No container image was built or pushed.
## Changes after the independent review
The independent review that section 10.8 of the design asks for was done by an agent that did not write this package. Its verdict was **fix first**. Every finding and note is closed in one commit, `589b6d4`, on top of `f5acf93`. No golden changed.
### 1. The `maxLength` proof never cuts
With `overflow: "truncate"` the proof was handed a cut block, which counts as fitting, so it proved nothing: a bound element with `maxLength: 40` in a box that holds ten worst-case characters validated clean, and every long value was cut at print.
- The proof now runs with cutting off, always.
- When it fails and the element did **not** ask for cutting, the scene is refused as before (`TEXT_DOES_NOT_FIT`).
- When it fails and the element **did** ask for cutting, the scene still validates, because cutting is what its author chose, but every validation carries a standing warning: `FIT_NOT_PROVEN` with `measured.provenCharacters` (the longest length proven to fit whole) against `limit.characters` (the `maxLength` stated). Fewer characters never fit worse, so that length is found by halving.
- **No new code.** `FIT_NOT_PROVEN` already means "the fit of this bound value is not proven"; it now carries numbers when a length was stated. Foundation's closed `FailureCode` and the host's lists are unchanged.
- Tests (`TextElementTest.theProofOfALengthNeverCutsAndACuttingElementIsWarnedHowMuchIsProven`): the refusal without cutting; the warning with cutting, with the number (7 of 40 in the test's box); that the number is the truth (that length validates clean with and without cutting, one more does not); that a value of the proven length prints whole and a longer one is cut; more lines prove more; a box that holds no character proves 0.
### 2. The shrink search is bounded
Stepping 0.25 pt from 1000 pt to a 1 pt floor is four thousand sizes, each laid out twice, and every element was composed twice (check, then paint).
| | Before | After |
| --- | --- | --- |
| One fit of 1024 characters at 600 dpi, 1000 pt to 1 pt (36 boxes and texts) | 180 to 250 ms; at least 15.6 million characters visited | 0.13 to 0.2 ms on average in the builds, under 1 ms cold; at most 21 555 characters visited |
| A scene of 1000 such elements, validated | minutes (killed by the worker's 15 s of processor, no refusal named) | 200 ms in the CI image, 281 ms on Windows; every element refused by name as `TEXT_DOES_NOT_FIT`, none for the work |
| Fits per element per render | 2 | 1 |
- **Refused at once** when one line at the floor size is higher than the box, whatever the text: nothing is searched. A cut tries no line count the box is too low for.
- **Halving, then a short linear check.** The size chosen is *defined* as the one a search of every quarter-point step gives, and is exactly that. A count that errs only toward yes says whether a size could hold the text: lines filled at any character, each held to a width no real line can be under (its advance, or the exact span of its ink plus the rasteriser's allowance less one dot of rounding), and a block held to its lines' ascent, pitches and descent. It never refuses a size that fits, and once it allows a size it allows every smaller one, so the largest size it allows is found by halving and no size above that fits. Only the steps from there down are laid out, as before.
- **Equivalence is tested, not assumed.** `TextFitTest.theAnswerIsExactlyThatOfTheSearchOfEverySize` holds `fit` to the old linear search, block for block (size, lines, every number; fitted, refused or cut), over 3600 generated cases: texts with and without spaces, the widest and furthest-reaching characters, the proof's worst character, boxes from one dot to a large label, sizes from 1 pt to 1000 pt, any floor, 1 to 64 lines, any line height. The linear search in the test shares only the layout at one size with `fit`. The same comparison was soaked over 4.3 million further cases with other seeds, weighted to type of 1 to 12 pt where rounding decides: no difference.
- **The layout is cached** between check and paint (`TextElement.check` keeps what it composed for the `paint` of the same pass).
- **A work budget for the text of one scene**, with a named refusal. A character visited while fitting is one step, a character to be drawn 16, and a glyph the scene draws for the first time at a size 4096 (the runtime rasterises it once: 20 to 50 microseconds measured, against 0.25 after). One scene may take **64 million steps**, about a second of a worker. The element that passes it, and every text after it, is `SCENE_INVALID` at the element with `measured.textWorkSteps` against `limit.textWorkSteps`, the same shape as the painted-area limit. The count is integer arithmetic, so the refusal is the same on every machine, and it is the same whether the scene is validated or rendered. No scene the parser accepts can exceed it.
- Tests: `TextFitTest.theWorstCaseOfTheShrinkSearchIsHeldToAStatedWork` (the reviewer's case: at most 40 times the text visited, where the linear search visits over 10 000 times it; same answer), `aBoxLowerThanOneLineAtTheFloorIsRefusedWithoutASearch`, `fittingEndsWhenItsAllowanceIsPassed`; `TextElementTest.aThousandOfTheWorstTextsAreRefusedByNameInsideTheWorkOfOneScene` (1000 elements, validated and rendered, each refused as `TEXT_DOES_NOT_FIT`, none for the work, inside 15 s) and `aSceneWhoseTextPassesItsWorkIsRefusedByName` (the refusal, its numbers, the same validated and rendered, every later text refused too).
### 3. Notes closed
- **Widest advance over every drawable character.** `FaceTest` took the widest character over inked characters only; a no-break space or an em quad has no ink and still moves the pen. The greatest advance is now taken over every character a face draws, and the widest inked character must have it, so a font update cannot break the proof unseen.
- **Ink outside the measure.** The bound is now tested at 1, 2, 300 and 1000 pt as well as 5 to 72 pt, for every character of every face at 203, 300 and 600 dpi (glyph bounds), with every character drawn at 1 and 2 pt and the furthest-reaching ones at 300 and 1000 pt. Nothing strays, on Windows or in the CI image: the furthest any glyph reaches past its exact outline is one dot of the two allowed, at every one of those sizes. `TEXT_INK_OUTSIDE_MEASURE` is no longer an `IllegalStateException` that fails the whole render with part of the text already on the label: the dots of a text are gathered first and reach the label only when none strayed; otherwise the **one element** is refused as `TEXT_DOES_NOT_FIT` with `measured.glyphsOutsideMeasure` and `measured.dotsOutsideMeasure`, and the label holds none of it. A literal is held to the glyph bounds when the scene is **validated**; a bound value can only be checked when it is known, at render.
- **Legibility at the proven size.** A bound element with `maxLength` is judged, when validated, at the size the proof shrank to (the floor when the proof fails and the element cuts), which is the smallest any value prints at. Without `maxLength` it is still judged at its stated size, since nothing says how far it will shrink.
- **`fit` is declared in the schema.** `maxLines`, `shrinkTo` and `overflow` are under `properties` with `additionalProperties: false`. The three names are in `ElementFactory.VOCABULARY` and in the bridge's `SceneFacts.VOCABULARY` in the same change (`SceneFactsTest.theVocabularyIsThePropertyNamesOfTheSchema` holds them equal: 46 names). A wrong member is reported at the member: `/elements/0/fit/maxLines`, or `/overrides/0/set/<id>/fit/maxLines` when an override states `fit`. A member `fit` does not have is still reported at `/fit`, because its name is not one the schema declares.
- **Bound numbers print as the schema says.** A number is printed in plain decimal digits: no exponent, no fraction that is only zeros. `1.0E10` prints `10000000000`, `2.50` prints `2.5`, `12.0` prints `12`, `1E-7` prints `0.0000001`; an integer prints its digits however long. A number that is not finite is `VALUE_MISSING`. The schema's description now says exactly this.
- **FreeType and JDK of the goldens** are recorded in `renderer/src/test/resources/scene2/README.md`, beside the goldens (the golden directory itself is held by a test to exactly the case files). Java draws glyphs with the FreeType the JDK itself ships, not the system's: 2.13.3 in Temurin 21.0.9+10 (the CI image, where the goldens were generated), 2.14.3 in Temurin 21.0.12.1+1 (Windows) and in Temurin 21.0.13+9 (the version the worker image requires). The same bytes on all three.
### Decisions made here, for the maintainers
- **No diagnostic code was added.** The three new refusals and the new warning use codes that exist, told apart by their named numbers. A dedicated code for the work limit or for ink outside the measure would mean a change to foundation's closed `FailureCode`, the schema and both vocabularies; say so if you want them.
- **Where the scene's work is counted.** A text element is given nothing that belongs to its scene except the collector of the render or validation, so the count is kept in the text package against that collector and forgotten with it. A counter on the canvas beside the painted-area count would be the plainer home; it was not added because other packages are changing the core at the same time.
- **Two small changes outside the text package**, both needed for the pointer at a member: `ElementSource` resolves `fit/maxLines` where `fit` is stated (element, defaults or override), and `ElementFactory.VOCABULARY` gains the three names.
- **The limit on glyphs is new behaviour.** A scene that draws more than about fifteen thousand different glyph-and-size pairs is now refused where it was slow before. A label of a hundred values in a handful of sizes uses a few percent of the allowance.
### Goldens
None changed and none was regenerated. The 18 text files and the 24 layout files are byte-identical to `f5acf93`; `SceneGoldenTest` passes on them in both builds above, and the 21 labels were rendered again on Temurin 21.0.13+9 with the same bytes.
### Build
The results under "Build" above are for this head, `589b6d4`, on both platforms. The version 1 corpus is untouched and green.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Version 2 label scenes draw text in faces shipped inside the renderer, so a
label needs no printer font, no installed font and no object storage:
qqq-sans, qqq-sans-bold IBM Plex Sans Regular and Bold (@ibm/plex-sans@1.1.0)
qqq-condensed-bold IBM Plex Sans Condensed Bold (@ibm/plex-sans-condensed@2.0.0)
qqq-mono, qqq-mono-bold JetBrains Mono Regular and Bold (v2.304)
The files are unmodified, taken from each project's official release archive.
Each licence was confirmed as the SIL Open Font License 1.1 from the archive's
own licence file, which is shipped byte for byte beside the fonts with a
provenance note (release, archive URL and hash, path in the archive, font
version). /fonts/SHA256SUMS lists the hashes. git is told never to rewrite
anything under /fonts.
THIRD-PARTY.md and NOTICE record the fonts. The OFL is not on the K22-DEP-02
allowlist; the maintainer approved one recorded exception for unmodified OFL
fonts shipped as data, and the register entry is proposed with the pull request.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A version 2 `text` element is now drawn. It replaces the placeholder that
parsed and placed a text and refused it as RENDERER_TOO_OLD.
What a scene can say
- `font` names one of the five bundled faces (or comes from `defaults`); any
other name is FONT_UNKNOWN. There is no printer font and no installed font.
- `size` is the em as a physical length (1 pt to 1000 pt), not rounded to dots.
- `value` or `valuePath`; `align`/`valign` place the block in the box and each
line in the block; `lineHeight`, `rotation`, `reverse`, `threshold`.
- `fit`: one line by default, shrunk in quarter-point steps to 70% of the size
(not below 5 pt), then refused. Opt in to more lines (`maxLines`) and to
cutting with an ellipsis (`overflow: truncate`).
- `maxLength`: validation proves the longest value fits, so a length refusal
happens when a scene is published; printing refuses a longer value first.
How it is measured and drawn
- Measurement is integer arithmetic on the face's own numbers (thousandths of
the em, read once at a reference size), so it is the same on every machine.
Every character sits at a whole-dot pen position and is drawn there by the
Java rasteriser that version 1 raster text uses, then reduced to ink at the
threshold. A line's box is its advance widened to its ink plus two dots a
side; a render whose ink would leave it fails instead of clipping.
- At each size a line is broken between words while that fits and at any
character when it does not, which is also what makes a length provable.
- One glyph for each character, left to right: no kerning, ligatures, mark
placement or bidirectional text. A character that cannot be drawn that way
is FONT_MISSING_GLYPH with a count.
Diagnostics (ids and numbers only; no value ever enters one)
TEXT_DOES_NOT_FIT, VALUE_MISSING, VALUE_TOO_LONG, FONT_UNKNOWN,
FONT_MISSING_GLYPH, RESERVED_FONT_NAME; TEXT_TRUNCATED, TEXT_SMALL_FOR_DPI,
TEXT_LIGHT_FACE, FIT_NOT_PROVEN; TEXT_SHRUNK, TEXT_WRAPPED.
Core seams added: `ElementSource.inherit` gives an element the scene's
`defaults` for the properties its type reads, and `ElementSource.refuse`
raises a coded refusal at a property (FONT_UNKNOWN at the place that names
the face, in the element, the defaults or an override).
Tests: goldens for the five faces and for fit, rotation and reverse at 203,
300 and 600 dpi, generated in the CI image (Linux, Temurin 21.0.9) and the
same bytes on Windows (21.0.12) and on Linux Temurin 21.0.13; every character
of every face drawn inside its measured line; seeded property tests of fit
(never shrunk when it fits, the largest size that fits, never below the floor,
never more lines than allowed, cut only when asked and always with the mark,
a proven length never refused); licence, provenance and hash of every face;
the version 2 mutation campaign extended to text. The version 1 corpus is
untouched. 100% line coverage of the scene 2 packages.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The proof of `maxLength`
- The proof never cuts. With `overflow: truncate` the proof was given a cut
block, which counts as fitting, so a bound element whose box held ten
characters validated clean with `maxLength: 40` and every long value was cut
at print. The proof now runs with cutting off. When it fails, an element
that did not ask for cutting is refused as before; one that did still
validates, since cutting is what its author chose, with the warning
FIT_NOT_PROVEN at every validation carrying `measured.provenCharacters`,
the longest length proven to fit whole (found by halving: fewer characters
never fit worse), against `limit.characters`. No new code: FIT_NOT_PROVEN
already means that the fit of a bound value is not proven.
- A bound element with `maxLength` is judged for legibility, when validated,
at the size the proof shrank to (the floor when it fails and the element
cuts), not at its stated size.
The shrink search
- The size chosen is still exactly the one a search of every quarter-point
step gives, but that search is no longer made. A count that errs only
toward yes says whether a size could hold the text: lines filled at any
character, each held to a width no real line can be under, and a block to
its lines' ascent, pitches and descent. It never refuses a size that fits
and, once it allows a size, allows every smaller one, so the largest size it
allows is found by halving and only the steps from there are laid out. When
one line at the floor is higher than the box nothing is searched, and a cut
tries no line count the box is too low for.
- 1024 characters at 600 dpi from 1000 pt to 1 pt took 180 to 250 ms a fit and
now takes under 1 ms: at most 21 555 characters visited where the search of
every size visits 15.6 million.
- A text is fitted once: what `check` composed is what `paint` draws.
- The text of one scene may take 64 million steps (a character visited while
fitting is one, a character to be drawn 16, a glyph the scene draws for the
first time at a size 4096, since the runtime rasterises it once). The
element that passes it, and every text after it, is SCENE_INVALID at the
element with `textWorkSteps`, as the painted-area limit is. The count is
integer arithmetic: the same on every machine, and the same validated or
rendered. A thousand of the worst elements validate in well under a second.
Ink outside the measure
- TEXT_INK_OUTSIDE_MEASURE was an IllegalStateException thrown while
painting, which failed the whole render after part of the text was on the
label. The dots of a text are now gathered first and reach the label only
when none strayed; otherwise the one element is refused as
TEXT_DOES_NOT_FIT with `glyphsOutsideMeasure` and `dotsOutsideMeasure`. A
literal is held to the glyph bounds when the scene is validated.
- The bound is tested at 1, 2, 300 and 1000 pt as well as from 5 to 72 pt, for
every character of every face at the three resolutions; nothing strays.
Schema and vocabulary
- `fit` declares `maxLines`, `shrinkTo` and `overflow` under `properties` with
`additionalProperties: false`. The three names are in the renderer's and the
host bridge's pointer vocabularies, and a wrong member is reported at the
member (`/elements/3/fit/maxLines`, or in the override that states `fit`).
- A bound number is printed as the schema says: plain decimal digits, no
exponent, no fraction that is only zeros (`1.0E10` prints `10000000000`,
`2.50` prints `2.5`). A number that is not finite is VALUE_MISSING.
Tests
- The widest-advance check covers every character a face draws, inked or not
(a no-break space advances the pen), so a font update cannot break the proof
unseen.
- Property: `fit` equals the search of every size, block for block, over 3600
generated texts, boxes, sizes, floors and line counts.
- The goldens are unchanged. A README beside them records the JDK builds and the
FreeType each ships.
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 P2 of
docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md(work plan B): text for version 2 label scenes. Bundled fonts, measurement before drawing, and a stated rule for a value that does not fit. Builds on the scene 2 core (#14); version 1 is untouched.What a label author gets
qqq-sans,qqq-sans-bold,qqq-condensed-bold,qqq-mono,qqq-mono-bold."size": "9pt"or"3mm", resolved at the render target (203, 300 or 600 dpi) and not rounded to dots.alignandvalign. Ink never leaves the box and is never clipped.maxLengthproves the fit when the document is published, so a length refusal is a design-time event and not a failed print.threshold(heavier strokes), bold faces, warnings for small type, and resolution overrides for either.Fit rules as implemented
sizewithinfit.maxLines(default 1). If it fits, it is never shrunk.fit.shrinkTo, at which it fits.shrinkTodefaults to 70% ofsizeand not below 5 pt (the size itself when the size is under 5 pt).TEXT_SHRUNK(info): chosen size, nominal size, floorfit.overflow:refuse(the default) ortruncate.TEXT_DOES_NOT_FIT(error): measured width, height, lines and size against the boxmaxLinesabove 1. At each size a line is broken between words while the text then fits, and at any character when it does not. So a value with no space wraps, and a larger size is preferred to whole words. Never more thanmaxLineslines.TEXT_WRAPPED(info): linesoverflow: "truncate", only at the floor size: as many lines as the box holds, the last ending with an ellipsis after as many of its characters as fit with it. The mark is always shown and the result always fits; where not even the mark fits, the text is refused.TEXT_TRUNCATED(warning): characters, characters shownmaxLength(1 to 1024). Validation lays out that many of the face's widest character, each taken to reach as far in every direction as the furthest-reaching character of the face, and refuses the scene if they do not fit. Printing refuses a longer value before any layout. The proof never cuts: withoverflow: "truncate"a length that is not proven does not refuse the scene but warns at every validation, with the length that is proven.TEXT_DOES_NOT_FITat validation,VALUE_TOO_LONGat print,FIT_NOT_PROVEN(warning) for a bound element with nomaxLength, and withprovenCharactersfor a cutting element whosemaxLengthis not provenOther refusals:
VALUE_MISSING(the pointer finds no text, number or true/false),FONT_MISSING_GLYPH(a character the face cannot draw as one glyph, with the count),FONT_UNKNOWN(a face that is not bundled, at the place that names it),RESERVED_FONT_NAME(a supplied asset whose name beginsqqq-). Warnings:TEXT_SMALL_FOR_DPI(capitals under 12 dots),TEXT_LIGHT_FACE(a regular weight with capitals under 20 dots). A diagnostic holds ids and numbers only; no value ever enters a diagnostic, a log line or an exception message, and a test puts a marker through every refusal to hold that.A rendered sample
renderer/src/test/resources/scene2/cases/text-fit.jsonon 2 x 1 in stock with 2 mm margins, at 203 dpi (golden:scene2/golden/text-fit-203.png; the 300 and 600 dpi goldens are beside it). Thin outlines show each element's box.keptLOT 240611,qqq-sans-bold10 pt, 22 x 5 mm, centredright9.5 kg,qqq-condensed-bold11 pt, 22 x 5 mm, right and bottomTEXT_SHRUNK: 11 pt is 41 dots high and the box is 39shrunk(01)00012345678905(10)L240611A,qqq-mono-bold9 pt, 44 x 4 mmTEXT_SHRUNKfrom 9 pt (floor 6.3 pt)wrappedqqq-mono7 pt, 36 x 7.5 mm,maxLines: 2,maxLength: 46TEXT_WRAPPED; alsoTEXT_LIGHT_FACE(capitals 14 dots, regular weight)cutqqq-sans7 pt, 30 x 3.6 mm,overflow: "truncate"A description of the item…TEXT_TRUNCATED: 25 of 42 characters shownThe diagnostic of a refusal, here the design's own example caption (two lines, floor 7 pt,
maxLength48, 94 x 6.5 mm) when validated at 203 dpi:Bundled fonts
/fontsin the jar)qqq-sansibm-plex-sans/IBMPlexSans-Regular.ttf@ibm/plex-sans@1.1.0, font 3.005975dcda37d80f038dcd143c22e33ca2d97a0cc5a929aace1c749153b0fe1afa5qqq-sans-boldibm-plex-sans/IBMPlexSans-Bold.ttf9e6c74a889a700d707613d24548fe4ffa6bc59559a0689d2cf9e133bdcdafb2fqqq-condensed-boldibm-plex-sans-condensed/IBMPlexSansCondensed-Bold.ttf@ibm/plex-sans-condensed@2.0.0, font 3.000723e245902b45a49f8cf602d15891bae08c31309dce7c56d209d10cd471b88c7qqq-monojetbrains-mono/JetBrainsMono-Regular.ttfv2.304, font 2.304a0bf60ef0f83c5ed4d7a75d45838548b1f6873372dfac88f71804491898d138fqqq-mono-boldjetbrains-mono/JetBrainsMono-Bold.ttf5590990c82e097397517f275f430af4546e1c45cff408bde4255dad142479dcbArchive SHA-256:
ibm-plex-sans.zipfb365d910566e6d199cc2c15579a7dd9a267128e18431a394ed81f1970c69200,ibm-plex-sans-condensed.zipf86ba73abbeed7cf7690ead863192a87211dfc2b0cef0ac6bf4700533ec5ab9e,JetBrainsMono-2.304.zip6f6376c6ed2960ea8a963cd7387ec9d76e3f629125bc33d1fdcd7eb7012f7bbf.How each licence was confirmed. From the release archive itself, not from memory. Each IBM Plex archive's
LICENSE.txtand the JetBrains Mono archive'sOFL.txtstate "This Font Software is licensed under the SIL Open Font License, Version 1.1" and carry the full text. The licence field of every font's name table says the same, and every font's embedding bits (OS/2.fsType) are 0, installable embedding. The licence files ship byte for byte beside the fonts (.gitattributeskeeps git from rewriting anything under/fonts), each directory has aSOURCE.mdwith the release, archive, path and hashes, and/fonts/SHA256SUMSlists the hashes.FaceTestfails the build when a licence, a source note or a hash is missing or a byte changes. The files are unmodified and keep their names; IBM Plex's Reserved Font Name "Plex" is not used for anything derived. The notes are namedSOURCE.mdbecause this repository's.gitignoreignoresPROVENANCE.md.The fonts add 531,819 bytes to the renderer jar (1,172,935 uncompressed), which is now 736 KB.
Dependency-licence exception (K22-DEP-02), for the maintainer to file
The SIL OFL 1.1 is not on the K22-DEP-02 allowlist. The exception register is
exceptions/register.mdin the standards repository, which this pull request does not edit. Proposed entry, with the next free number:The design's decision 1 proposed that one exception also cover the JetBrains Mono Bold that
qbit-printingalready ships. This entry is exact for what this pull request verified; that repository's file and hash were not read here and would be added to the scope by its maintainer.How text is measured and drawn
units x millipoints x dpi / 72 000 000, so a measurement is the same on every machine and never depends on the font engine at the drawing size.threshold.TEXT_DOES_NOT_FITwithglyphsOutsideMeasureanddotsOutsideMeasure).Determinism across platforms
Same bytes everywhere tested, with no tolerance:
maven:3.9.11-eclipse-temurin-21, the CI image)eclipse-temurin:21-jdk; the version the worker requires)Before writing the code, a probe hashed glyph metrics, glyph bitmaps and filled outlines for the five faces at ten sizes and three resolutions on Windows and in the CI image: all identical, for the bitmap path (up to 100 dots to the em) and for the outline path above it.
One case depends on more than the scene, stated plainly: a line whose strip is over 8 million dots (type of several hundred points) is drawn in bands of whole rows so that it never needs more grey than that, and the rasteriser may shade a few edge dots at a band's border differently than it would in one piece (4 of 121 thousand ink dots at 203 dpi, 40 of 1.06 million at 600 dpi, for 200 pt type forced into small bands). Where the bands fall is a function of the line's box alone, so the result is still the same on every machine; up to 100 dots to the em banding changes nothing at all, which a test holds row by row.
Tests
text-faces), the fit rules (text-fit), rotation, reverse and a resolution override (text-turned). Exact comparison of ZPL and picture.maxLength: a proven length is never refused for any text of the face's characters up to that length (the widest characters, the furthest-reaching ones, random ones, with and without spaces, cut or not); the proof agrees with rendering of the widest character to within one character a line.Refusalcounts as a refusal.Build
The CI command,
./mvnw --batch-mode --no-transfer-progress -s <settings> -T1C verify, on head589b6d4(the head after the independent review), with no gate skipped:maven:3.9.11-eclipse-temurin-21(the CI image), on agit archiveof the headRenderer module: 360 tests. Coverage of the scene 2 packages, the same in both builds:
scene2632 of 632 lines,scene2.text465 of 465 lines, the placeholders 8 of 8; the build's 100% line gate is met with nothing excluded. Branches ofscene2.text: 360 of 371. Of the eleven not taken, six are sides of the checks inTextRasterthat count ink outside the measure (a test forces them with lines whose boxes are stated too small, on some sides and not all), and five are inTextElement.paint, which reuses whatcheckcomposed only for the same box, request and collector and would otherwise compose again: a case the scene walk never produces.The first head of this pull request,
f5acf93, was built the same way on both (916 tests). The branch was rebased ontodevelopafter #15 merged.develophas since gained #17; this branch was not rebased again, because it merges into the newdevelopwithout conflict, and on that merged tree the bridge's tests and the scene 2 schema, golden and text element tests pass (353 tests).The independent review that section 10.8 of the design asks of every package (by an agent that did not write it) has been done. Its verdict was "fix first"; what changed is in the last section.
Where the design and the code differ
maxLength48) is in a 6.5 mm box; two lines at 7 pt need 59 dots at 203 dpi (53 from rounding the ascent, line pitch and descent up to whole dots, 6 more for an accent above a capital and a deep descender) and the box has 51. With a 7.5 mm box it is proven at all three resolutions. The proof is sound for any value, so it is stricter than a typical value needs: a box of exactlylines x line heightis not proven.fontalone. The merged schema has no weight or style property; the name carries family and weight and the set has no italic.RESERVED_FONT_NAMEis raised by a text element (with the count), not for a scene as a whole.fitare declared in the schema (changed after the independent review).maxLines,shrinkToandoverfloware underpropertiesoffitwithadditionalProperties: false; the three names are inElementFactory.VOCABULARYand in the bridge'sSceneFacts.VOCABULARY, which a bridge test holds equal to the schema; and a wrong member is reported at the member (/elements/1/fit/maxLines). The first head described them in the schema's prose and reported everything at/fit.ElementSource, used by the parser):inherit, which gives an element the scene'sdefaultsfor the properties its type reads, andrefuse, a coded refusal at a property.defaultswas parsed by the core but could not reach an element.Deliberately left out
font/ttfasset named by a version 2 text isFONT_UNKNOWN. The seam is in place:RenderInput.reservedAssetNames()and theRESERVED_FONT_NAMErefusal.WarmUpis outside this package's files.Changes after the independent review
The independent review that section 10.8 of the design asks for was done by an agent that did not write this package. Its verdict was fix first. Every finding and note is closed in one commit,
589b6d4, on top off5acf93. No golden changed.1. The
maxLengthproof never cutsWith
overflow: "truncate"the proof was handed a cut block, which counts as fitting, so it proved nothing: a bound element withmaxLength: 40in a box that holds ten worst-case characters validated clean, and every long value was cut at print.TEXT_DOES_NOT_FIT).FIT_NOT_PROVENwithmeasured.provenCharacters(the longest length proven to fit whole) againstlimit.characters(themaxLengthstated). Fewer characters never fit worse, so that length is found by halving.FIT_NOT_PROVENalready means "the fit of this bound value is not proven"; it now carries numbers when a length was stated. Foundation's closedFailureCodeand the host's lists are unchanged.TextElementTest.theProofOfALengthNeverCutsAndACuttingElementIsWarnedHowMuchIsProven): the refusal without cutting; the warning with cutting, with the number (7 of 40 in the test's box); that the number is the truth (that length validates clean with and without cutting, one more does not); that a value of the proven length prints whole and a longer one is cut; more lines prove more; a box that holds no character proves 0.2. The shrink search is bounded
Stepping 0.25 pt from 1000 pt to a 1 pt floor is four thousand sizes, each laid out twice, and every element was composed twice (check, then paint).
TEXT_DOES_NOT_FIT, none for the workTextFitTest.theAnswerIsExactlyThatOfTheSearchOfEverySizeholdsfitto the old linear search, block for block (size, lines, every number; fitted, refused or cut), over 3600 generated cases: texts with and without spaces, the widest and furthest-reaching characters, the proof's worst character, boxes from one dot to a large label, sizes from 1 pt to 1000 pt, any floor, 1 to 64 lines, any line height. The linear search in the test shares only the layout at one size withfit. The same comparison was soaked over 4.3 million further cases with other seeds, weighted to type of 1 to 12 pt where rounding decides: no difference.TextElement.checkkeeps what it composed for thepaintof the same pass).SCENE_INVALIDat the element withmeasured.textWorkStepsagainstlimit.textWorkSteps, the same shape as the painted-area limit. The count is integer arithmetic, so the refusal is the same on every machine, and it is the same whether the scene is validated or rendered. No scene the parser accepts can exceed it.TextFitTest.theWorstCaseOfTheShrinkSearchIsHeldToAStatedWork(the reviewer's case: at most 40 times the text visited, where the linear search visits over 10 000 times it; same answer),aBoxLowerThanOneLineAtTheFloorIsRefusedWithoutASearch,fittingEndsWhenItsAllowanceIsPassed;TextElementTest.aThousandOfTheWorstTextsAreRefusedByNameInsideTheWorkOfOneScene(1000 elements, validated and rendered, each refused asTEXT_DOES_NOT_FIT, none for the work, inside 15 s) andaSceneWhoseTextPassesItsWorkIsRefusedByName(the refusal, its numbers, the same validated and rendered, every later text refused too).3. Notes closed
FaceTesttook the widest character over inked characters only; a no-break space or an em quad has no ink and still moves the pen. The greatest advance is now taken over every character a face draws, and the widest inked character must have it, so a font update cannot break the proof unseen.TEXT_INK_OUTSIDE_MEASUREis no longer anIllegalStateExceptionthat fails the whole render with part of the text already on the label: the dots of a text are gathered first and reach the label only when none strayed; otherwise the one element is refused asTEXT_DOES_NOT_FITwithmeasured.glyphsOutsideMeasureandmeasured.dotsOutsideMeasure, and the label holds none of it. A literal is held to the glyph bounds when the scene is validated; a bound value can only be checked when it is known, at render.maxLengthis judged, when validated, at the size the proof shrank to (the floor when the proof fails and the element cuts), which is the smallest any value prints at. WithoutmaxLengthit is still judged at its stated size, since nothing says how far it will shrink.fitis declared in the schema.maxLines,shrinkToandoverfloware underpropertieswithadditionalProperties: false. The three names are inElementFactory.VOCABULARYand in the bridge'sSceneFacts.VOCABULARYin the same change (SceneFactsTest.theVocabularyIsThePropertyNamesOfTheSchemaholds them equal: 46 names). A wrong member is reported at the member:/elements/0/fit/maxLines, or/overrides/0/set/<id>/fit/maxLineswhen an override statesfit. A memberfitdoes not have is still reported at/fit, because its name is not one the schema declares.1.0E10prints10000000000,2.50prints2.5,12.0prints12,1E-7prints0.0000001; an integer prints its digits however long. A number that is not finite isVALUE_MISSING. The schema's description now says exactly this.renderer/src/test/resources/scene2/README.md, beside the goldens (the golden directory itself is held by a test to exactly the case files). Java draws glyphs with the FreeType the JDK itself ships, not the system's: 2.13.3 in Temurin 21.0.9+10 (the CI image, where the goldens were generated), 2.14.3 in Temurin 21.0.12.1+1 (Windows) and in Temurin 21.0.13+9 (the version the worker image requires). The same bytes on all three.Decisions made here, for the maintainers
FailureCode, the schema and both vocabularies; say so if you want them.ElementSourceresolvesfit/maxLineswherefitis stated (element, defaults or override), andElementFactory.VOCABULARYgains the three names.Goldens
None changed and none was regenerated. The 18 text files and the 24 layout files are byte-identical to
f5acf93;SceneGoldenTestpasses on them in both builds above, and the 21 labels were rendered again on Temurin 21.0.13+9 with the same bytes.Build
The results under "Build" above are for this head,
589b6d4, on both platforms. The version 1 corpus is untouched and green.🤖 Generated with Claude Code