feat(renderer): scene 2 barcodes: whole-dot module fit, alignment in the box, decode tests (P3a) #18

Merged
james.maes merged 2 commits from feat/scene2-barcode-fit into develop 2026-11-13 14:00:13 +00:00
Owner

Package P3a of docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md (sections 4.3, 4.4, 4.6): a barcode element of a version 2 scene is drawn into the picture of the label, in whole printer dots, in the middle of its box, and is never drawn below its minimum module without the author saying so. It replaces the placeholder scene2.barcode.BarcodeFactory that refused every barcode as RENDERER_TOO_OLD.

Why: a label whose barcode is sized in raw dots and anchored by hand prints small and off to one side, and one edit can make it unscannable with nothing to say so.

Rebased onto develop after the text package (PR 16) merged; the review findings are one commit on top. Read "Changes after the independent review" first: it changes defaults, and one of them refuses a common kind of label unless its scene names a profile.

Changes after the independent review

A label is scanned, so the failure that matters most is a symbol that reads as something other than the value it was given. Record values are untrusted and never appear in a diagnostic, log line or exception.

# Finding What changed Test that proves it
1 The encoder reads \<FNC1> to \<FNC4> in a value: a record value in a plain Code 128 field drew a GS1-128 of its own choosing, could set the reader-programming flag or change a character; QR dropped the escape A value, literal or bound, that holds the two characters \< is refused in every symbology: VALUE_NOT_ENCODABLE (GS1_INVALID for gs1-128) with a count of characters only. A function character enters a symbol only through its symbology PayloadTest.noValueHoldsAnEscapeOfTheEncoder (every escape, every symbology, every position); BarcodeElementTest.aValueThatWouldNotReadBackAsItselfIsRefused (literal and bound, no value text or FNC in the diagnostic; and the same value given to the encoder as before decodes as ]C1..., a GS1-128); everyValueEndsAsALabelOrARefusal now fails if such a value is ever drawn
2 Interleaved 2 of 5 of an odd number of digits was padded with a leading zero Refused, with limit.charactersMultipleOf 2 PayloadTest.interleavedTwoOfFiveOfAnOddLengthIsRefused; BarcodeElementTest.aValueThatWouldNotReadBackAsItselfIsRefused
3 A gs1-128 value with a wrong GTIN check digit was drawn The check digit of every GS1 key is verified: application identifiers 00, 01, 02, 03, 253, 255, 402, 410 to 417, 8003, 8006, 8017, 8018, 8026. GS1_INVALID with measured.applicationIdentifier and checkDigitMatches PayloadTest.theCheckDigitOfEveryGs1KeyIsVerified, aCheckDigitIsVerifiedByTheGs1Rule (against the rule worked the other way round)
4 EAN and UPC took the number without its check digit and padded a short one with zeros The value is the whole number, check digit included (13, 8, 12, 8 digits); the digit is verified; any other length is refused. The short form is not accepted PayloadTest.aRetailNumberIsWholeAndCarriesItsOwnCheckDigit, aUpcENumberIsCheckedAsTheNumberItStandsFor; BarcodeDecodeTest reads each back as the whole number
5 One quiet zone and one minimum module for every linear symbol A table by symbology, each number with its source (scene2/barcode/Symbology), and a named profile, barcodeProfile SymbologyTest (the table written out a second time); BarcodeElementTest.theQuietZonesAreThoseOfTheSymbology, theProfileGivesTheNarrowestBar; BarcodeDecodeTest
6 A linear symbol filled whatever height its box had, down to one dot Bars must be as tall as the symbology asks; the height is part of the fit; minHeight overrides, with the standing warning BARCODE_HEIGHT_BELOW_RECOMMENDED ModuleFitTest.theBarsOfALinearSymbolAreAsTallAsTheirWidthAsks; BarcodeElementTest.barsShorterThanTheSymbologyAsks...; the property tests
7 A later element could paint over a symbol or its quiet zone, a reverse text panel included QUIET_ZONE_INTRUDED is an error, by geometry, at validation and at render. An element painted before the barcode stays allowed BarcodeElementTest.nothingIsPaintedOverABarcodeOrItsQuietZones
8 Owner's decision, 2026-11-12 A barcode sits in the middle of its box by default (center, middle); other elements keep left, top BarcodeElementTest.alignmentPlacesTheSymbolInItsBox, aGs1SymbolIsFittedInWholeDotsAndSitsInTheMiddleOfItsBox; BarcodeDecodeTest and BarcodePropertyTest no longer state an alignment
9 Notes A symbol of more modules than the encoder draws is BARCODE_DOES_NOT_FIT; a bound barcode takes maxLength and is proven at validation; decode tests gained digits-only and mixed Code 128, retail numbers with their check digits and an EAN-13 in general distribution BarcodeElementTest.aDeclaredLengthProvesTheFitOfABoundValue, aValueThatDoesNotFit...; SymbologyTest.theWidestSymbolOfALengthIsTheWidestTheEncoderDraws; BarcodeDecodeTest (18 profiles)

The table (item 5)

Symbology Quiet zone, modules before and after Smallest module with no profile stated Source
code128, code39, code93 10, 10 0.25 mm (2, 3, 6 dots at 203, 300, 600 dpi) quiet zone: ISO/IEC 15417, ISO/IEC 16388, AIM Code 93; module: the design's own proposal
itf 10, 10 0.495 mm (4, 6, 12 dots) quiet zone: ISO/IEC 16390; module: GS1 General Specifications, the symbol specification tables for general distribution (ITF-14)
gs1-128 10, 10 0.495 mm (4, 6, 12 dots) quiet zone: ISO/IEC 15417; module: GS1 General Specifications, table 5 (logistic units)
ean13 11, 7 0.264 mm (3, 4, 7 dots) GS1 General Specifications 5.2.3.4; table 1 (general retail)
ean8 7, 7 0.264 mm the same
upca 9, 9 0.264 mm the same
upce 9, 7 0.264 mm the same
qr, datamatrix, pdf417, aztec 4 on every side 0.375 mm (3, 5, 9 dots) quiet zone: ISO/IEC 18004 for QR; the others ask for less and keep 4; module: the design's own proposal

barcodeProfile is none (the design's 0.25 mm and 0.375 mm for everything), gs1-general (0.264 mm for EAN/UPC, the rest as none) or gs1-logistics (0.495 mm for GS1-128, ITF and EAN/UPC, the rest as none). Not stated, each symbology takes the strictest GS1 rule written for its own use, as in the table. A scene may still state a smaller minModule, with the standing warning BARCODE_MODULE_BELOW_RECOMMENDED. What was read again for this change: the 0.495 mm of GS1-128 and ITF-14 in general distribution, the 0.264 mm of EAN/UPC at retail, the quiet zones of EAN-13 and EAN-8, the EAN/UPC heights, and the 5 mm and 15% of ISO/IEC 15417 Annex G. As remembered, and for a reviewer to confirm: the quiet zones of UPC-A and UPC-E, the 10 modules of Code 39, Code 93 and Interleaved 2 of 5, and 0.495 mm for EAN/UPC in general distribution.

What this refuses that the first version of this pull request drew

  • A GS1-128 of a GTIN, a date and a serial number (44 characters as a person reads them, 299 modules of bars, 319 with quiet zones) on a label 4 in wide at 203 dpi:

    barcodeProfile Smallest module Result
    not stated, or gs1-logistics 4 dots, 0.50 mm refused, BARCODE_DOES_NOT_FIT: needs 1276 dots (160 mm); the label is 812 dots with no margins, 751 with 3 mm margins. It does not fit the 6 in of 4 x 6 in stock either (1218 dots)
    gs1-general 2 dots, 0.25 mm drawn at 2 dots, 638 dots wide; 3 dots would need 957
    none 2 dots, 0.25 mm the same

    So a scene for such a label must now name gs1-general or none (or state minModule, with the warning). Whether to print it below the GS1 minimum for logistic units, on wider stock or as a two-dimensional symbol is a decision for whoever owns the label; this change makes it a stated one.

  • Bars under 15% of their own width, or under 5 mm. The same symbol at 2 dots has bars 598 dots wide and needs a box 90 dots (11.3 mm) high at 203 dpi.

  • An EAN or UPC at less than its full height (69.24 modules; 26 mm for an EAN-13 at 3 dots) unless the scene states minHeight, and then with the warning. No label 1 in high has that height.

  • An element later in the list that enters a barcode's symbol or quiet zones, and, while a bound barcode is only being validated, its whole box.

  • An EAN or UPC value without its check digit.

Codes and names added, in one change

VALUE_NOT_ENCODABLE (error) and BARCODE_HEIGHT_BELOW_RECOMMENDED (warning) are new; QUIET_ZONE_INTRUDED moves from the warnings to the errors. barcodeProfile and minHeight are new property names, and a barcode now takes maxLength. They are added together in foundation's FailureCode (appended) and warning set, the preview's sentences, the bridge's accepted refusals and its schema vocabulary, the renderer's vocabulary and the schema. A dedicated code was chosen over reusing SCENE_INVALID because the scene is not what is wrong when a record's value cannot be drawn.

Not closed

  • A two-dimensional symbol is not proven by maxLength. The byte-capacity symbol is not the largest of a length: Data Matrix and PDF417 choose among shapes (seven bytes are wider than six), and QR and Aztec change mode at a cost that makes some ordinary values larger than as many plain bytes. SymbologyTest.noTwoDimensionalSymbologyHasALargestSymbolOfALength holds the encoders to that. Such an element is fitted for that many bytes and keeps FIT_NOT_PROVEN.
  • A declared length rarely proves a GS1-128 written as a value: its widest symbol of a length is one of letters, about 1.7 times the width of a label of digits. The builder (gs1, P3b) knows each identifier and can do better.
  • The bar heights of Code 39, Code 93 and Interleaved 2 of 5 are held to the rule of ISO/IEC 15417; their own specifications were not read. The check characters of application identifiers 8013 and 8014 are not verified. The 31.75 mm bar height GS1 asks of a logistic unit is not enforced.

The rule

All integer arithmetic (scene2/barcode/ModuleFit).

Step Rule Example: a 44-character GS1-128, gs1-general, in a 90 mm by 12 mm box at 203 dpi
Box width and height rounded down to dots 719 by 95 dots
Symbol modules of the bars plus the quiet zones of the symbology 299 + 10 + 10 = 319 modules
Minimum minModule, or the profile's, taken up to a whole dot 2 dots
Module the largest of min(maxModule, floor(box / modules)) at which the bars are tall enough; a two-dimensional symbol takes the smaller of its two axes floor(719 / 319) = 2 dots, 0.25 mm; bars 598 dots wide need 90 of the 95
Fits module at least the minimum, else BARCODE_DOES_NOT_FIT yes (3 dots would need 957)
Size modules x module; a linear symbol's bars take the whole height of the box 638 dots wide, 95 high
Place the middle, or by align and valign; the odd dot right and below 40 dots blank on the left, 41 on the right; bars start 60 dots in

The same label is 3 dots (957 of 1062) at 300 dpi and 6 dots (1914 of 2125) at 600 dpi. In a 60 mm box it is refused at 203 dpi with measured: modules 319, widthDots 638, moduleDots 1 against limit: widthDots 479, minModuleDots 2, when the scene is validated as when it is rendered. With "minModule": "1dot" it is drawn at 1 dot and the label carries BARCODE_MODULE_BELOW_RECOMMENDED every time.

What is in it

  • Symbology (the table), Payload (the value rules), ModuleFit, BarcodeElement, BarcodeFactory (the placeholder, replaced). The thirteen fit-qualified symbologies, value and valuePath, the dynamic symbology allowlist and the scanner profile are carried over. The bars come from the version 1 encoder path at one dot to the module, without its quiet zones, which the element lays out itself; version 1, its BarcodeFit and its corpus are not changed.
  • The fitted symbol paints its own white ground, quiet zones included, then its bars; rotation turns it clockwise in a box given as it appears on the label.
  • Every refusal is a diagnostic of ids and numbers. What an encoder says about a value it rejects is dropped; the tests assert that neither the diagnostic nor the exception holds the value.
  • Schema, renderer README, docs/security/fuzzing.md and the design: section 4.3, the vocabulary of section 8.1 and a "Decided after the independent review of P3a" note.

Tests

  • Decode (BarcodeDecodeTest): eighteen profiles over the thirteen symbologies at 203, 300 and 600 dpi, each for a short, a typical and the longest value its box holds (found by halving; one step longer is BARCODE_DOES_NOT_FIT). The page is rebuilt from the ^GFA fields of the emitted ZPL and read by ZXing: content, symbology, module in dots, the quiet zones of the symbology, the middle of the box, bar height. GS1-128 is read with FNC1 as ]C1, under both of its profiles.
  • Properties (ModuleFitTest, BarcodePropertyTest, SymbologyTest, seeded): the module is whole dots; the symbol and its quiet zones never leave the box; bars are as tall as asked or warned of; a symbol with room for what rounding can cost is refused at no resolution; no value is wider than the widest symbol of its length; every drawn label decodes.
  • Element (BarcodeElementTest, PayloadTest): the worked example to the dot; alignment; rotation dot for dot; refusal content; every malformed property; bound values, bound symbology, scanner profile; a sweep of awkward values through every symbology (each ends as a label or a Refusal, and none that would not read back as itself ends as a label).
  • Goldens: barcode-gs1, barcode-linear and barcode-matrix are unchanged, byte for byte: every symbol in them stated its alignment, and the cases now leave it out where it is the default. barcode-retail is new (an EAN-13 and a UPC-E with their own quiet zones). Generated in the CI image (maven:3.9.11-eclipse-temurin-21) and byte-identical on Windows (Temurin 21.0.12).
  • Mutation campaign: the version 2 campaign holds the three barcode cases and barcode tokens, the profile names and the encoder's escape among them, reads an accepted barcode element independently at each resolution, and asserts that it rendered labels holding a barcode. Only a Refusal counts as a refusal.
  • The version 1 corpus is untouched and green; scene2 and its sub-packages are at 100% lines; the bridge's vocabulary test holds the schema's 48 property names.

Where the design and the code differ

  • BarcodeFit is not called to fit. Its fit scales bar height with the module and stops at a scale of 16, which section 4.3 ends, so the package calls the encoder entry point BarcodeFit.fit itself measures with.
  • maxModule goes up to a whole dot, as the minimum does, so that the same length for both is one module at every resolution.
  • pdf417 is fitted as two-dimensional; the default Data Matrix encoder may choose a rectangular symbol.

Left out, on purpose

  • gs1 and hri (P3b): declared, and an element that states either is refused as RENDERER_TOO_OLD. The builder's element string enters at BarcodeElement.payload, where the check digits are already verified.

Review focus

No symbol reads as something other than its value; no code prints below the stated minimum; quiet zones are never cropped or painted over; integer arithmetic only; no value in a diagnostic, log line or exception.

🤖 Generated with Claude Code

Package **P3a** of `docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md` (sections 4.3, 4.4, 4.6): a `barcode` element of a version 2 scene is drawn into the picture of the label, in whole printer dots, in the middle of its box, and is never drawn below its minimum module without the author saying so. It replaces the placeholder `scene2.barcode.BarcodeFactory` that refused every barcode as `RENDERER_TOO_OLD`. Why: a label whose barcode is sized in raw dots and anchored by hand prints small and off to one side, and one edit can make it unscannable with nothing to say so. Rebased onto `develop` after the text package (PR 16) merged; the review findings are one commit on top. **Read "Changes after the independent review" first: it changes defaults, and one of them refuses a common kind of label unless its scene names a profile.** ## Changes after the independent review A label is scanned, so the failure that matters most is a symbol that reads as something other than the value it was given. Record values are untrusted and never appear in a diagnostic, log line or exception. | # | Finding | What changed | Test that proves it | |---|---|---|---| | 1 | The encoder reads `\<FNC1>` to `\<FNC4>` in a value: a record value in a plain Code 128 field drew a GS1-128 of its own choosing, could set the reader-programming flag or change a character; QR dropped the escape | A value, literal or bound, that holds the two characters `\<` is refused in every symbology: `VALUE_NOT_ENCODABLE` (`GS1_INVALID` for `gs1-128`) with a count of characters only. A function character enters a symbol only through its symbology | `PayloadTest.noValueHoldsAnEscapeOfTheEncoder` (every escape, every symbology, every position); `BarcodeElementTest.aValueThatWouldNotReadBackAsItselfIsRefused` (literal and bound, no value text or `FNC` in the diagnostic; and the same value given to the encoder as before decodes as `]C1...`, a GS1-128); `everyValueEndsAsALabelOrARefusal` now fails if such a value is ever drawn | | 2 | Interleaved 2 of 5 of an odd number of digits was padded with a leading zero | Refused, with `limit.charactersMultipleOf` 2 | `PayloadTest.interleavedTwoOfFiveOfAnOddLengthIsRefused`; `BarcodeElementTest.aValueThatWouldNotReadBackAsItselfIsRefused` | | 3 | A `gs1-128` value with a wrong GTIN check digit was drawn | The check digit of every GS1 key is verified: application identifiers 00, 01, 02, 03, 253, 255, 402, 410 to 417, 8003, 8006, 8017, 8018, 8026. `GS1_INVALID` with `measured.applicationIdentifier` and `checkDigitMatches` | `PayloadTest.theCheckDigitOfEveryGs1KeyIsVerified`, `aCheckDigitIsVerifiedByTheGs1Rule` (against the rule worked the other way round) | | 4 | EAN and UPC took the number without its check digit and padded a short one with zeros | The value is the whole number, check digit included (13, 8, 12, 8 digits); the digit is verified; any other length is refused. The short form is not accepted | `PayloadTest.aRetailNumberIsWholeAndCarriesItsOwnCheckDigit`, `aUpcENumberIsCheckedAsTheNumberItStandsFor`; `BarcodeDecodeTest` reads each back as the whole number | | 5 | One quiet zone and one minimum module for every linear symbol | A table by symbology, each number with its source (`scene2/barcode/Symbology`), and a named profile, `barcodeProfile` | `SymbologyTest` (the table written out a second time); `BarcodeElementTest.theQuietZonesAreThoseOfTheSymbology`, `theProfileGivesTheNarrowestBar`; `BarcodeDecodeTest` | | 6 | A linear symbol filled whatever height its box had, down to one dot | Bars must be as tall as the symbology asks; the height is part of the fit; `minHeight` overrides, with the standing warning `BARCODE_HEIGHT_BELOW_RECOMMENDED` | `ModuleFitTest.theBarsOfALinearSymbolAreAsTallAsTheirWidthAsks`; `BarcodeElementTest.barsShorterThanTheSymbologyAsks...`; the property tests | | 7 | A later element could paint over a symbol or its quiet zone, a reverse text panel included | `QUIET_ZONE_INTRUDED` is an error, by geometry, at validation and at render. An element painted before the barcode stays allowed | `BarcodeElementTest.nothingIsPaintedOverABarcodeOrItsQuietZones` | | 8 | Owner's decision, 2026-11-12 | A barcode sits in the middle of its box by default (`center`, `middle`); other elements keep `left`, `top` | `BarcodeElementTest.alignmentPlacesTheSymbolInItsBox`, `aGs1SymbolIsFittedInWholeDotsAndSitsInTheMiddleOfItsBox`; `BarcodeDecodeTest` and `BarcodePropertyTest` no longer state an alignment | | 9 | Notes | A symbol of more modules than the encoder draws is `BARCODE_DOES_NOT_FIT`; a bound barcode takes `maxLength` and is proven at validation; decode tests gained digits-only and mixed Code 128, retail numbers with their check digits and an EAN-13 in general distribution | `BarcodeElementTest.aDeclaredLengthProvesTheFitOfABoundValue`, `aValueThatDoesNotFit...`; `SymbologyTest.theWidestSymbolOfALengthIsTheWidestTheEncoderDraws`; `BarcodeDecodeTest` (18 profiles) | ### The table (item 5) | Symbology | Quiet zone, modules before and after | Smallest module with no profile stated | Source | |---|---|---|---| | `code128`, `code39`, `code93` | 10, 10 | 0.25 mm (2, 3, 6 dots at 203, 300, 600 dpi) | quiet zone: ISO/IEC 15417, ISO/IEC 16388, AIM Code 93; module: the design's own proposal | | `itf` | 10, 10 | 0.495 mm (4, 6, 12 dots) | quiet zone: ISO/IEC 16390; module: GS1 General Specifications, the symbol specification tables for general distribution (ITF-14) | | `gs1-128` | 10, 10 | 0.495 mm (4, 6, 12 dots) | quiet zone: ISO/IEC 15417; module: GS1 General Specifications, table 5 (logistic units) | | `ean13` | 11, 7 | 0.264 mm (3, 4, 7 dots) | GS1 General Specifications 5.2.3.4; table 1 (general retail) | | `ean8` | 7, 7 | 0.264 mm | the same | | `upca` | 9, 9 | 0.264 mm | the same | | `upce` | 9, 7 | 0.264 mm | the same | | `qr`, `datamatrix`, `pdf417`, `aztec` | 4 on every side | 0.375 mm (3, 5, 9 dots) | quiet zone: ISO/IEC 18004 for QR; the others ask for less and keep 4; module: the design's own proposal | `barcodeProfile` is `none` (the design's 0.25 mm and 0.375 mm for everything), `gs1-general` (0.264 mm for EAN/UPC, the rest as `none`) or `gs1-logistics` (0.495 mm for GS1-128, ITF and EAN/UPC, the rest as `none`). Not stated, each symbology takes the strictest GS1 rule written for its own use, as in the table. A scene may still state a smaller `minModule`, with the standing warning `BARCODE_MODULE_BELOW_RECOMMENDED`. What was read again for this change: the 0.495 mm of GS1-128 and ITF-14 in general distribution, the 0.264 mm of EAN/UPC at retail, the quiet zones of EAN-13 and EAN-8, the EAN/UPC heights, and the 5 mm and 15% of ISO/IEC 15417 Annex G. As remembered, and for a reviewer to confirm: the quiet zones of UPC-A and UPC-E, the 10 modules of Code 39, Code 93 and Interleaved 2 of 5, and 0.495 mm for EAN/UPC in general distribution. ### What this refuses that the first version of this pull request drew - **A GS1-128 of a GTIN, a date and a serial number** (44 characters as a person reads them, 299 modules of bars, 319 with quiet zones) on a label 4 in wide at 203 dpi: | `barcodeProfile` | Smallest module | Result | |---|---|---| | not stated, or `gs1-logistics` | 4 dots, 0.50 mm | refused, `BARCODE_DOES_NOT_FIT`: needs 1276 dots (160 mm); the label is 812 dots with no margins, 751 with 3 mm margins. It does not fit the 6 in of 4 x 6 in stock either (1218 dots) | | `gs1-general` | 2 dots, 0.25 mm | drawn at 2 dots, 638 dots wide; 3 dots would need 957 | | `none` | 2 dots, 0.25 mm | the same | So a scene for such a label must now name `gs1-general` or `none` (or state `minModule`, with the warning). Whether to print it below the GS1 minimum for logistic units, on wider stock or as a two-dimensional symbol is a decision for whoever owns the label; this change makes it a stated one. - **Bars under 15% of their own width, or under 5 mm.** The same symbol at 2 dots has bars 598 dots wide and needs a box 90 dots (11.3 mm) high at 203 dpi. - **An EAN or UPC at less than its full height** (69.24 modules; 26 mm for an EAN-13 at 3 dots) unless the scene states `minHeight`, and then with the warning. No label 1 in high has that height. - **An element later in the list that enters a barcode's symbol or quiet zones**, and, while a bound barcode is only being validated, its whole box. - **An EAN or UPC value without its check digit.** ### Codes and names added, in one change `VALUE_NOT_ENCODABLE` (error) and `BARCODE_HEIGHT_BELOW_RECOMMENDED` (warning) are new; `QUIET_ZONE_INTRUDED` moves from the warnings to the errors. `barcodeProfile` and `minHeight` are new property names, and a barcode now takes `maxLength`. They are added together in foundation's `FailureCode` (appended) and warning set, the preview's sentences, the bridge's accepted refusals and its schema vocabulary, the renderer's vocabulary and the schema. A dedicated code was chosen over reusing `SCENE_INVALID` because the scene is not what is wrong when a record's value cannot be drawn. ### Not closed - **A two-dimensional symbol is not proven by `maxLength`.** The byte-capacity symbol is not the largest of a length: Data Matrix and PDF417 choose among shapes (seven bytes are wider than six), and QR and Aztec change mode at a cost that makes some ordinary values larger than as many plain bytes. `SymbologyTest.noTwoDimensionalSymbologyHasALargestSymbolOfALength` holds the encoders to that. Such an element is fitted for that many bytes and keeps `FIT_NOT_PROVEN`. - **A declared length rarely proves a GS1-128 written as a `value`**: its widest symbol of a length is one of letters, about 1.7 times the width of a label of digits. The builder (`gs1`, P3b) knows each identifier and can do better. - The bar heights of Code 39, Code 93 and Interleaved 2 of 5 are held to the rule of ISO/IEC 15417; their own specifications were not read. The check characters of application identifiers 8013 and 8014 are not verified. The 31.75 mm bar height GS1 asks of a logistic unit is not enforced. ## The rule All integer arithmetic (`scene2/barcode/ModuleFit`). | Step | Rule | Example: a 44-character GS1-128, `gs1-general`, in a 90 mm by 12 mm box at 203 dpi | |---|---|---| | Box | width and height rounded down to dots | 719 by 95 dots | | Symbol | modules of the bars plus the quiet zones of the symbology | 299 + 10 + 10 = 319 modules | | Minimum | `minModule`, or the profile's, taken up to a whole dot | 2 dots | | Module | the largest of `min(maxModule, floor(box / modules))` at which the bars are tall enough; a two-dimensional symbol takes the smaller of its two axes | `floor(719 / 319)` = 2 dots, 0.25 mm; bars 598 dots wide need 90 of the 95 | | Fits | module at least the minimum, else `BARCODE_DOES_NOT_FIT` | yes (3 dots would need 957) | | Size | modules x module; a linear symbol's bars take the whole height of the box | 638 dots wide, 95 high | | Place | the middle, or by `align` and `valign`; the odd dot right and below | 40 dots blank on the left, 41 on the right; bars start 60 dots in | The same label is 3 dots (957 of 1062) at 300 dpi and 6 dots (1914 of 2125) at 600 dpi. In a 60 mm box it is refused at 203 dpi with `measured: modules 319, widthDots 638, moduleDots 1` against `limit: widthDots 479, minModuleDots 2`, when the scene is validated as when it is rendered. With `"minModule": "1dot"` it is drawn at 1 dot and the label carries `BARCODE_MODULE_BELOW_RECOMMENDED` every time. ## What is in it - `Symbology` (the table), `Payload` (the value rules), `ModuleFit`, `BarcodeElement`, `BarcodeFactory` (the placeholder, replaced). The thirteen fit-qualified symbologies, `value` and `valuePath`, the dynamic symbology allowlist and the scanner profile are carried over. The bars come from the version 1 encoder path at one dot to the module, without its quiet zones, which the element lays out itself; version 1, its `BarcodeFit` and its corpus are not changed. - The fitted symbol paints its own white ground, quiet zones included, then its bars; `rotation` turns it clockwise in a box given as it appears on the label. - Every refusal is a diagnostic of ids and numbers. What an encoder says about a value it rejects is dropped; the tests assert that neither the diagnostic nor the exception holds the value. - Schema, renderer README, `docs/security/fuzzing.md` and the design: section 4.3, the vocabulary of section 8.1 and a "Decided after the independent review of P3a" note. ## Tests - **Decode** (`BarcodeDecodeTest`): eighteen profiles over the thirteen symbologies at 203, 300 and 600 dpi, each for a short, a typical and the longest value its box holds (found by halving; one step longer is `BARCODE_DOES_NOT_FIT`). The page is rebuilt from the `^GFA` fields of the emitted ZPL and read by ZXing: content, symbology, module in dots, the quiet zones of the symbology, the middle of the box, bar height. GS1-128 is read with FNC1 as `]C1`, under both of its profiles. - **Properties** (`ModuleFitTest`, `BarcodePropertyTest`, `SymbologyTest`, seeded): the module is whole dots; the symbol and its quiet zones never leave the box; bars are as tall as asked or warned of; a symbol with room for what rounding can cost is refused at no resolution; no value is wider than the widest symbol of its length; every drawn label decodes. - **Element** (`BarcodeElementTest`, `PayloadTest`): the worked example to the dot; alignment; rotation dot for dot; refusal content; every malformed property; bound values, bound symbology, scanner profile; a sweep of awkward values through every symbology (each ends as a label or a `Refusal`, and none that would not read back as itself ends as a label). - **Goldens**: `barcode-gs1`, `barcode-linear` and `barcode-matrix` are unchanged, byte for byte: every symbol in them stated its alignment, and the cases now leave it out where it is the default. `barcode-retail` is new (an EAN-13 and a UPC-E with their own quiet zones). Generated in the CI image (`maven:3.9.11-eclipse-temurin-21`) and byte-identical on Windows (Temurin 21.0.12). - **Mutation campaign**: the version 2 campaign holds the three barcode cases and barcode tokens, the profile names and the encoder's escape among them, reads an accepted barcode element independently at each resolution, and asserts that it rendered labels holding a barcode. Only a `Refusal` counts as a refusal. - The version 1 corpus is untouched and green; `scene2` and its sub-packages are at 100% lines; the bridge's vocabulary test holds the schema's 48 property names. ## Where the design and the code differ - **`BarcodeFit` is not called to fit.** Its `fit` scales bar height with the module and stops at a scale of 16, which section 4.3 ends, so the package calls the encoder entry point `BarcodeFit.fit` itself measures with. - **`maxModule` goes up to a whole dot**, as the minimum does, so that the same length for both is one module at every resolution. - **`pdf417` is fitted as two-dimensional**; the default Data Matrix encoder may choose a rectangular symbol. ## Left out, on purpose - `gs1` and `hri` (P3b): declared, and an element that states either is refused as `RENDERER_TOO_OLD`. The builder's element string enters at `BarcodeElement.payload`, where the check digits are already verified. ## Review focus No symbol reads as something other than its value; no code prints below the stated minimum; quiet zones are never cropped or painted over; integer arithmetic only; no value in a diagnostic, log line or exception. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
james.maes added 2 commits 2026-11-13 00:26:26 +00:00
A `barcode` element of a version 2 scene is now drawn into the picture of
the label instead of being refused as RENDERER_TOO_OLD.

- ModuleFit: the module is the largest whole number of dots at which the
  symbol, quiet zones included, fits its box; never under `minModule`
  (default 0.25 mm linear, 0.375 mm two-dimensional, taken up to whole dots)
  and never over `maxModule`. Integer arithmetic only.
- A linear symbol is fitted along its width and its bars take the whole
  height of the box; qr, datamatrix, pdf417 and aztec are fitted on both
  axes. The fitted symbol is placed by `align` and `valign`, turned by
  `rotation`, and paints its own white ground, quiet zones included.
- A value that does not fit at the minimum module is BARCODE_DOES_NOT_FIT,
  validated or rendered, with the modules and dots it needs and the dots
  the box has. A smaller module is drawn only when the scene states one,
  and then with the warning BARCODE_MODULE_BELOW_RECOMMENDED every time.
- The thirteen fit-qualified symbologies, `value` and `valuePath`, the
  dynamic symbology allowlist and the scanner profile are carried over from
  version 1, whose own barcode path is not changed. Every refusal is a
  diagnostic of numbers; what an encoder says about a value is dropped.
- `gs1` and `hri` stay declared; an element that states either is refused
  as RENDERER_TOO_OLD until the package that draws them.

Tests: every symbology at 203, 300 and 600 dpi for a short, a typical and
the longest value, rebuilt from the emitted ZPL and decoded by ZXing
(GS1-128 with FNC1 as `]C1`); seeded properties of the fit and of the
picture; goldens for a GS1-128 on 4 x 1 in, linear and two-dimensional
symbols; the version 2 mutation campaign extended to barcode elements.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A label is scanned, so a symbol must never read as something other than
the value it was given. Record values are untrusted and stay out of every
diagnostic and exception.

- No value, literal or bound, may hold the two characters an escape of the
  encoder begins with. \<FNC1> in a plain Code 128 value drew a GS1-128 of
  the record's choosing, \<FNC3> set the reader-programming flag, and QR
  dropped the escape. Refused in every symbology, with a count only.
- Interleaved 2 of 5 of an odd number of digits is refused, not padded.
- EAN and UPC values are the whole number with its check digit, which is
  verified; a shorter number is refused, never completed or padded.
- The check digit of every GS1 key in a gs1-128 element string is
  verified: the identifiers the GS1 Syntax Dictionary marks, as the
  bundled Zint carries it.
- VALUE_NOT_ENCODABLE joins the errors for these and for a value a
  symbology cannot hold; SCENE_INVALID is no longer used for a value. A
  symbol of more modules than the encoder draws is BARCODE_DOES_NOT_FIT.
- Quiet zones and the smallest module are a table by symbology
  (scene2/barcode/Symbology), each number with its source. barcodeProfile
  names the rule: none, gs1-general or gs1-logistics; not stated, the
  strictest written for the symbology. A GS1-128 is so held to 0.495 mm
  unless its scene says otherwise, at which a GTIN with a date and a
  serial number fits no label 4 in wide.
- The bars of a linear symbol must be as tall as its symbology asks, and
  the height is part of the fit. minHeight overrides, with the standing
  warning BARCODE_HEIGHT_BELOW_RECOMMENDED.
- QUIET_ZONE_INTRUDED is an error: an element later in the list whose ink
  would enter a symbol or its quiet zones is refused, when a scene is
  validated as when it is rendered.
- A barcode sits in the middle of its box unless the scene says otherwise.
- A bound barcode takes maxLength and is proven at validation in a linear
  symbology. FIT_NOT_PROVEN stays for no length and for two-dimensional
  symbols, whose encoders have no largest symbol of a length.

Codes and property names are added in one change: foundation's
FailureCode and warning set, the preview's sentences, the bridge's
accepted refusals and schema vocabulary, the renderer's vocabulary and
the schema. The three barcode goldens are unchanged byte for byte;
barcode-retail is new.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
james.maes force-pushed feat/scene2-barcode-fit from 4a3aaa118e to 05518acd02 2026-11-13 00:26:26 +00:00 Compare
james.maes merged commit 188c8b1ef7 into develop 2026-11-13 14:00:13 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: QRun-IO/qbit-document-generation#18