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
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
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
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.
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)
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>
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 P3a of
docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md(sections 4.3, 4.4, 4.6): abarcodeelement 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 placeholderscene2.barcode.BarcodeFactorythat refused every barcode asRENDERER_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
developafter 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.
\<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\<is refused in every symbology:VALUE_NOT_ENCODABLE(GS1_INVALIDforgs1-128) with a count of characters only. A function character enters a symbol only through its symbologyPayloadTest.noValueHoldsAnEscapeOfTheEncoder(every escape, every symbology, every position);BarcodeElementTest.aValueThatWouldNotReadBackAsItselfIsRefused(literal and bound, no value text orFNCin the diagnostic; and the same value given to the encoder as before decodes as]C1..., a GS1-128);everyValueEndsAsALabelOrARefusalnow fails if such a value is ever drawnlimit.charactersMultipleOf2PayloadTest.interleavedTwoOfFiveOfAnOddLengthIsRefused;BarcodeElementTest.aValueThatWouldNotReadBackAsItselfIsRefusedgs1-128value with a wrong GTIN check digit was drawnGS1_INVALIDwithmeasured.applicationIdentifierandcheckDigitMatchesPayloadTest.theCheckDigitOfEveryGs1KeyIsVerified,aCheckDigitIsVerifiedByTheGs1Rule(against the rule worked the other way round)PayloadTest.aRetailNumberIsWholeAndCarriesItsOwnCheckDigit,aUpcENumberIsCheckedAsTheNumberItStandsFor;BarcodeDecodeTestreads each back as the whole numberscene2/barcode/Symbology), and a named profile,barcodeProfileSymbologyTest(the table written out a second time);BarcodeElementTest.theQuietZonesAreThoseOfTheSymbology,theProfileGivesTheNarrowestBar;BarcodeDecodeTestminHeightoverrides, with the standing warningBARCODE_HEIGHT_BELOW_RECOMMENDEDModuleFitTest.theBarsOfALinearSymbolAreAsTallAsTheirWidthAsks;BarcodeElementTest.barsShorterThanTheSymbologyAsks...; the property testsQUIET_ZONE_INTRUDEDis an error, by geometry, at validation and at render. An element painted before the barcode stays allowedBarcodeElementTest.nothingIsPaintedOverABarcodeOrItsQuietZonescenter,middle); other elements keepleft,topBarcodeElementTest.alignmentPlacesTheSymbolInItsBox,aGs1SymbolIsFittedInWholeDotsAndSitsInTheMiddleOfItsBox;BarcodeDecodeTestandBarcodePropertyTestno longer state an alignmentBARCODE_DOES_NOT_FIT; a bound barcode takesmaxLengthand 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 distributionBarcodeElementTest.aDeclaredLengthProvesTheFitOfABoundValue,aValueThatDoesNotFit...;SymbologyTest.theWidestSymbolOfALengthIsTheWidestTheEncoderDraws;BarcodeDecodeTest(18 profiles)The table (item 5)
code128,code39,code93itfgs1-128ean13ean8upcaupceqr,datamatrix,pdf417,aztecbarcodeProfileisnone(the design's 0.25 mm and 0.375 mm for everything),gs1-general(0.264 mm for EAN/UPC, the rest asnone) orgs1-logistics(0.495 mm for GS1-128, ITF and EAN/UPC, the rest asnone). Not stated, each symbology takes the strictest GS1 rule written for its own use, as in the table. A scene may still state a smallerminModule, with the standing warningBARCODE_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:
barcodeProfilegs1-logisticsBARCODE_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-generalnoneSo a scene for such a label must now name
gs1-generalornone(or stateminModule, 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) andBARCODE_HEIGHT_BELOW_RECOMMENDED(warning) are new;QUIET_ZONE_INTRUDEDmoves from the warnings to the errors.barcodeProfileandminHeightare new property names, and a barcode now takesmaxLength. They are added together in foundation'sFailureCode(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 reusingSCENE_INVALIDbecause the scene is not what is wrong when a record's value cannot be drawn.Not closed
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.noTwoDimensionalSymbologyHasALargestSymbolOfALengthholds the encoders to that. Such an element is fitted for that many bytes and keepsFIT_NOT_PROVEN.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 rule
All integer arithmetic (
scene2/barcode/ModuleFit).gs1-general, in a 90 mm by 12 mm box at 203 dpiminModule, or the profile's, taken up to a whole dotmin(maxModule, floor(box / modules))at which the bars are tall enough; a two-dimensional symbol takes the smaller of its two axesfloor(719 / 319)= 2 dots, 0.25 mm; bars 598 dots wide need 90 of the 95BARCODE_DOES_NOT_FITalignandvalign; the odd dot right and belowThe 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 1againstlimit: 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 carriesBARCODE_MODULE_BELOW_RECOMMENDEDevery time.What is in it
Symbology(the table),Payload(the value rules),ModuleFit,BarcodeElement,BarcodeFactory(the placeholder, replaced). The thirteen fit-qualified symbologies,valueandvaluePath, 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, itsBarcodeFitand its corpus are not changed.rotationturns it clockwise in a box given as it appears on the label.docs/security/fuzzing.mdand the design: section 4.3, the vocabulary of section 8.1 and a "Decided after the independent review of P3a" note.Tests
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 isBARCODE_DOES_NOT_FIT). The page is rebuilt from the^GFAfields 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.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.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 aRefusal, and none that would not read back as itself ends as a label).barcode-gs1,barcode-linearandbarcode-matrixare unchanged, byte for byte: every symbol in them stated its alignment, and the cases now leave it out where it is the default.barcode-retailis 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).Refusalcounts as a refusal.scene2and 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
BarcodeFitis not called to fit. Itsfitscales bar height with the module and stops at a scale of 16, which section 4.3 ends, so the package calls the encoder entry pointBarcodeFit.fititself measures with.maxModulegoes up to a whole dot, as the minimum does, so that the same length for both is one module at every resolution.pdf417is fitted as two-dimensional; the default Data Matrix encoder may choose a rectangular symbol.Left out, on purpose
gs1andhri(P3b): declared, and an element that states either is refused asRENDERER_TOO_OLD. The builder's element string enters atBarcodeElement.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
4a3aaa118eto05518acd02