Skip to content

Feat/zpl first barcode sizing - #39

Merged
u8array merged 16 commits into
mainfrom
feat/zpl-first-barcode-sizing
May 9, 2026
Merged

u8array merged 16 commits into
mainfrom
feat/zpl-first-barcode-sizing

Conversation

@u8array

@u8array u8array commented May 9, 2026

Copy link
Copy Markdown
Owner

No description provided.

u8array added 15 commits May 9, 2026 14:17
Phase 1 of the ZPL-first barcode sizing audit. The mechanism the
upcoming work was meant to add already exists — getDisplaySize maps
each barcode type from the bwip-js intrinsic canvas size to the
ZPL-correct display pixels per Zebra-spec module formulas. Document
that policy at the module level, audit each symbology's status:

- 18+ types are fully ZPL-correct (modules × moduleWidth match Zebra)
- planet/postal trade visual bar fidelity for ZPL-correct width
- code93/code11/plessey deliberately stay bwip-natural (algorithm
  differences would distort bars worse than a slight quiet-zone gap)

Add a static-parse coverage test so any future barcode type added
to BCID without a matching case in getUprightDisplaySize fails CI
instead of silently falling through to the default and returning
bwip-natural pixels (a ZPL-first violation).
… in bbox

Phase 2(A,B) of the ZPL-first sizing audit. The displayed bbox for
EAN/UPC barcodes is now barH + 13 dots and for LOGMARS barH + 20 dots,
matching what Zebra firmware actually reserves on the print regardless
of printInterpretation. The bwip-js bitmap (which only renders the
bars) gets stretched vertically to fill the larger bbox — that visual
distortion is an accepted intermediate compromise until the renderer
is split to draw the bitmap at bar height inside a taller bbox.

- New LOGMARS_TEXT_ZONE_DOTS constant (20)
- getDisplaySize includes both zones in the height
- labelarySync strict bounds check now applies to EAN/UPC/LOGMARS
  without subtraction; height is checked for LOGMARS too
- visualRegression failingTests gets the affected fixtures with a
  pointer to the Phase-3 follow-up

GS1 Databar stacked variants (Phase 2C) are still tracked as a
separate gap — height divergence per symbology needs per-fixture
investigation.
…nvas size

Phase 2(C) of the ZPL-first sizing audit. bwip-js renders all non-omni
GS1 DataBar variants at the same canvas height as Omnidirectional
(33 modules) regardless of symbology, while Zebra firmware uses the
per-symbology spec heights:

  Omni 33, Truncated 13, Stacked 14, Stacked Omni 72, Limited 10,
  Expanded 34 (per GS1 General Specifications)

getDisplaySize now consults a GS1_DATABAR_SPEC_HEIGHT_MODULES map for
the height instead of dividing the bwip canvas height — so the bbox
matches the printed footprint for variants 1-6 (sym 7 Expanded
Stacked is segments-dependent and still falls back to bwip-natural).

The labelarySync strict bounds check now passes for variants 1-6 too;
only sym 7 stays excluded. visualRegression keeps all variants skipped
because the bwip bitmap rendering still differs in shape — fixing the
visual diff is Phase-3 work (split bitmap from bbox).
…stretched

Phase 3 of the ZPL-first sizing audit. After Phase 2 the bbox includes
the firmware-reserved text zone, but the bitmap was stretched to fill
the bbox so the bars looked taller than the printed output. Split the
two:

- getDisplaySize now returns barH and barTopPx alongside the full
  w/h, describing where the bars sit inside the bbox.
- BarcodeObject draws the bwip-js bitmap at barH (not the full h) so
  the bars render at their true height. The text zone is empty
  whitespace inside the bbox.
- The default-path KImage gets wrapped in a Group with an invisible
  full-bbox Rect so the selection footprint still matches what the
  printer reserves, even when the bitmap is shorter.
- visualRegression test draws the bitmap at the bar sub-rect too,
  so the upright EAN/UPC and LOGMARS fixtures match Labelary again
  and can be removed from the failingTests skip list. Rotated
  EAN/UPC stays skipped — barH/barTopPx are not yet rotation-aware,
  tracked as a follow-up.
The text zone reserved by Labelary travels around the bbox as the
symbol rotates: bottom for N, left for R, top for I, right for B.
Previous Phase 3 only handled N. Extend BarcodeDisplaySize with
barW + barLeftPx so the bar sub-rect can be on any side, and shift
the Konva Group origin by (-barLeftPx, -barTopPx) so the rendered
bars stay anchored at FO while the bbox extends in the text-zone
direction.

Drag-end recovers the saved obj.x/y by adding back barLeftPx/barTopPx
in pixels. The FT baseline correction now subtracts barH instead of
displayH, since the FT origin is the bar bottom, not the bbox bottom
(this also fixes a regression Phase 2 introduced for FT EAN/LOGMARS).

The visual regression test draws the bitmap at FO directly — the
bbox shift is a Konva rendering concern, not a pixel-position one.
With this change the rotated EAN/UPC fixtures pass strict-bounds
and pixel-match, no remaining failingTests skips for these types.
…behavior

Sym 7 (Expanded Stacked) cannot be cross-validated against Labelary:
bwip-js requires the (AI)data parens-AI input format that Zebra ^BR
sym 7 silently rejects and renders empty for. Neither side can
produce a shared ground truth, so the implementation falls back to
the bwip-natural canvas height — what the user sees in bwip's
preview is what they get in the bbox.

Document the limitation in the gs1databar case comment and add a
unit test pinning the bwip-natural height formula so any future
change requires an explicit decision.
…ext branches

Smell found in self-validation: the showText branch (LOGMARS / Code128
with HRI) and showRotatedText branch had no full-bbox guard. Their
HRI Text node uses getSelfRect=()=>0 to keep resize anchored at the
bars, so the Group's auto-bbox shrank to the bar sub-rectangle
(barH x barW) instead of spanning the firmware-reserved footprint
(displayH x displayW). Selection-handles missed the text-zone
reservation; only the default-path Group had this guard.

Add an invisible full-bbox Rect to both branches, mirroring the
default path. The Rect is non-listening so it doesn't intercept
clicks but contributes to getClientRect, restoring the ZPL-correct
selection footprint.
Smell #3 from validation: extending the text-zone detection (e.g. for
a future barcode type with reserved whitespace) meant adding another
branch to a growing if/else chain. Replace with a Partial<Record>
keyed by LabelObject type, letting new entries land as one-line
additions to the table.
…bles

Smells #2 and #4 from validation:

- The FT/FO Y-shift logic was duplicated forward in the render path
  and inverted in the drag-end handler — same per-type branches in
  both, easy to drift apart. Compute the shift once via an IIFE
  (ftYShiftDots) and a foYShiftDots constant, then apply forward in
  render and inverted in drag-end. Render and inverse stay in
  lockstep by construction.
- The component held six separate top-level variables (displayW,
  displayH, barW, barH, barLeftPx, barTopPx) that re-aliased the
  fields of the BarcodeDisplaySize struct. Consolidate into a single
   so callers reference dim.w / dim.barW etc.
  No more accidental shadowing of bbox vs bar dims.
Re-validation found that EAN/UPC printInterp digits and
showRotatedText/showText fallbacks computed text Y from `h`
(full bbox including the firmware-reserved text zone) after the
Phase 2 bbox extension. The visual digits drifted below the text
zone instead of sitting inside it where Labelary draws them.

Switch every "text below bars" position from h to bh (= barH).
For symbologies without a text zone bh equals h so the math is
unchanged; for EAN/UPC and LOGMARS the digits/HRI now sit just
below the bars, matching the printed layout. Untested by fixtures
(printInterpretation:true exists only for LOGMARS upright, which
uses the text-above branch); manual verification recommended in
the next browser pass.
User-reported visual mismatch in the designer:
- code93/code11 displayed too short (bwip's narrower quiet zone)
- plessey displayed too long (different bar encoding)

Both were previously listed as 'bwip-natural width returned' in the
audit and skipped from labelarySync's strict bounds check. Following
the user's direction (ZPL-Treue first, accept visual stretching as
fallback), correct the displayed bbox to match Zebra firmware:

- code93: add 17 modules to bwip canvas count (constant quiet-zone
  shortfall, content-independent)
- code11: add 19 modules (same reason, slightly different delta)
- plessey: multiply bwip width by 49/82 (≈0.598, derived from the
  canonical '12345678' fixture, content-proportional)

The bitmap inside still renders bwip's bar pattern at its natural
size, so it stretches slightly (~10-25% wider for code93/code11) or
squeezes (~40% narrower for plessey) to fill the corrected bbox.
Tracked as a known visual-fidelity limitation; the next commit will
add a UI hint for affected types.

The hasBwipSizeMismatch skip in labelarySync.test.ts is removed —
all symbologies now pass strict bounds.
User-prompted: when a symbology renders with visual stretching (e.g.
code93/code11/plessey after the bbox-correction fix), surface that
fact without screaming "the designer is wrong". Tiny info icon next
to the type label in the Properties header — muted opacity, only
visible if the user looks at the panel — with a native title tooltip
carrying the localised explanation.

- Add BWIP_VISUAL_APPROX_TYPES set in bwipConstants
- Add properties.visualApproxHint locale key in all 32 languages via
  the add_locale_key script
- PropertiesPanel renders the hint icon when the selected type is in
  the set
User-reported: GS1 Databar's image inside the bbox is a few pixels
shorter at top and bottom than what Labelary prints. Cause: bwip-js
opts include paddingheight=2 which adds whitespace rows on top and
bottom of the canvas; when we then draw that bitmap at the
spec-correct bbox height, the bars fill proportionally less than
the firmware-reserved space.

Add an optional bitmapCrop field to BarcodeDisplaySize describing
which sub-rect of the source bitmap the renderer should use, and
populate it for gs1databar with the padding rows excluded
(2 × bwipScale per side, derived from the paddingheight option).

BarcodeObject's KImage forwards crop to Konva, the visualRegression
test uses the 9-arg drawImage form for the same effect. Other
symbologies pass undefined and use the full bitmap as before.
User observation: even with the bbox dimensions and bitmap padding
fixed, the GS1 DataBar bar pattern doesn't match Labelary 100% —
bwip-js and Zebra firmware encode the same GS1 data with the same
total module count but place the bar/space transitions at different
positions. Reimplementing the encoder is out of scope.

Add gs1databar to BWIP_VISUAL_APPROX_TYPES so the Properties hint
icon surfaces for it too, and extend the docstring to note that the
limitation here is encoder-divergence, not stretching.
…t GS1 input

AI 01 followed by exactly 11 numeric digits is not a valid GTIN-14 element
string. Zebra firmware emits General Compaction (~149 modules at 8dpmm)
rather than Method 1 padding. bwip-js with (01)<padded GTIN> would force
Method 1 (~133 modules), producing a 16-module bbox shortfall vs print.

Route this specific input class through (99) so bwip uses General Compaction
too. Empirical cutoff (probed against Labelary). The fallback lives in
bwipHelpers.ts next to the call site so wrapGs1AIs stays a pure GS1 domain
helper.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a "ZPL-first" sizing policy for barcodes to ensure that the designer's bounding boxes accurately reflect Zebra firmware print outputs. Key changes include a refactored BarcodeObject component that uses a new BarcodeDisplaySize interface to manage bar sub-rectangles and reserved text zones, and updated logic in bwipHelpers.ts to handle symbology-specific offsets and scaling for EAN, UPC, LOGMARS, and GS1 DataBar. Review feedback suggests sanitizing GS1 content strings to improve the robustness of fragment detection and adding a default fallback for moduleWidth to prevent potential NaN values in sizing calculations.

Comment thread src/components/Canvas/bwipHelpers.ts
Comment thread src/components/Canvas/bwipHelpers.ts
App tsconfig kept node types out of scope but tests need them — pre-existing
`tsc -b` failure on test files importing `node:fs` etc. App config now
excludes `*.test.ts` and `src/test`, and a standalone tsconfig.test.json
adds node types for the test sources. Build pipeline runs both.
@u8array
u8array merged commit 7a6e703 into main May 9, 2026
2 checks passed
@u8array
u8array deleted the feat/zpl-first-barcode-sizing branch May 9, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant