= {
rotation: 0,
props: { content: "123456789012", height: 100, moduleWidth: 2, printInterpretation: false, checkDigit: false, rotation: "B" },
},
+ barcode_upcean_supp5_standard: {
+ id: "supp1",
+ type: "upcEanExtension",
+ x: 50,
+ y: 50,
+ rotation: 0,
+ props: { content: "51999", height: 80, moduleWidth: 2, printInterpretation: false, checkDigit: false, rotation: "N" },
+ },
+ barcode_upcean_supp2_standard: {
+ id: "supp2",
+ type: "upcEanExtension",
+ x: 50,
+ y: 50,
+ rotation: 0,
+ props: { content: "42", height: 80, moduleWidth: 2, printInterpretation: false, checkDigit: false, rotation: "N" },
+ },
};
diff --git a/src/test/visualRegression.test.ts b/src/test/visualRegression.test.ts
index dc7e55f9..a2c60896 100644
--- a/src/test/visualRegression.test.ts
+++ b/src/test/visualRegression.test.ts
@@ -12,7 +12,7 @@ import {
buildBwipOptions,
getDisplaySize,
} from "../components/Canvas/bwipHelpers";
-import { QR_FO_Y_OFFSET_DOTS } from "../components/Canvas/bwipConstants";
+import { QR_FO_Y_OFFSET_DOTS, UPC_SUPP_TEXT_ZONE_DOTS } from "../components/Canvas/bwipConstants";
const FIXTURES_DIR = path.resolve(
process.cwd(),
@@ -120,8 +120,15 @@ describe("Visual Regression - bwip-js vs Labelary", () => {
);
// Zebra firmware renders ^FO-positioned QR codes with a +10 dot Y offset.
- // Match production BarcodeObject.tsx behaviour.
- const drawY = obj.type === "qrcode" ? obj.y + QR_FO_Y_OFFSET_DOTS : obj.y;
+ // Match production BarcodeObject.tsx behaviour. UPC/EAN supplements
+ // render the human-readable digits ABOVE the bars, so the bitmap's
+ // top edge sits text-zone above the FO anchor.
+ const drawY =
+ obj.type === "qrcode"
+ ? obj.y + QR_FO_Y_OFFSET_DOTS
+ : obj.type === "upcEanExtension" && obj.props.printInterpretation
+ ? obj.y - UPC_SUPP_TEXT_ZONE_DOTS
+ : obj.y;
// Bars draw at FO; bbox extends in the text-zone direction without
// shifting the bar pattern. barLeftPx/barTopPx describe where the
// bars sit inside the bbox, but the bitmap itself anchors at obj.x/y.
diff --git a/src/types/ObjectType.ts b/src/types/ObjectType.ts
index 88124ddc..e8180614 100644
--- a/src/types/ObjectType.ts
+++ b/src/types/ObjectType.ts
@@ -154,6 +154,32 @@ export interface ZplEmitContext {
variables?: readonly Variable[];
}
+/**
+ * Per-type HRI (human-readable interpretation) rendering behaviour. All
+ * fields are optional with sensible defaults: text is rendered below the
+ * bars in raw form with the standard textGap. Each leaf overrides only
+ * what differs from the baseline, keeping BarcodeObject type-agnostic
+ * for the generic HRI path.
+ *
+ * @example See registry/logmars.tsx (text above + wider gap + check digit
+ * formatter) and registry/upcEanExtension.tsx (text above + very tight gap)
+ * for the canonical patterns.
+ */
+export interface HriBehavior {
+ /** True when the HRI text sits above the bars (logmars spec, ^BS).
+ * Default: false. */
+ textAbove?: boolean;
+ /** Gap in dots between the bar edge and the text glyph. Applies to
+ * both the upright above-bars gap AND the side gap on rotated
+ * R/B/I, so a tighter ^BS (2) stays tight after rotation while
+ * logmars (10) keeps its wider air gap. Below-bars upright always
+ * uses the global textGap regardless of this value. */
+ aboveGapDots?: number;
+ /** Transform raw content into the displayed HRI string (add check
+ * digit, wrap with start/stop chars, pad, …). Default: identity. */
+ formatHri?: (content: string) => string;
+}
+
export interface ObjectTypeDefinition {
label: string;
icon: string;
@@ -211,6 +237,10 @@ export interface ObjectTypeDefinition
{
obj: LabelObjectBase & { props: P },
ctx: TransformContext,
) => Partial
;
+ /** See {@link HriBehavior}. Only meaningful for 1D barcode types
+ * that render an HRI text overlay; other types should leave this
+ * undefined. */
+ hri?: HriBehavior;
PropertiesPanel: React.ComponentType<{
obj: LabelObjectBase & { props: P };
onChange: (props: Partial
) => void;
diff --git a/tests/fixtures/labelary_images/barcode_upcean_supp2_standard.png b/tests/fixtures/labelary_images/barcode_upcean_supp2_standard.png
new file mode 100644
index 00000000..0e091f4e
Binary files /dev/null and b/tests/fixtures/labelary_images/barcode_upcean_supp2_standard.png differ
diff --git a/tests/fixtures/labelary_images/barcode_upcean_supp5_standard.png b/tests/fixtures/labelary_images/barcode_upcean_supp5_standard.png
new file mode 100644
index 00000000..ec94e683
Binary files /dev/null and b/tests/fixtures/labelary_images/barcode_upcean_supp5_standard.png differ
diff --git a/tests/fixtures/labelary_images/fixtures.json b/tests/fixtures/labelary_images/fixtures.json
index 16e71f7f..2234bf91 100644
--- a/tests/fixtures/labelary_images/fixtures.json
+++ b/tests/fixtures/labelary_images/fixtures.json
@@ -483,6 +483,28 @@
"height": 68
},
"image_ref": "barcode_gs1databar_expanded.png"
+ },
+ {
+ "id": "barcode_upcean_supp5_standard",
+ "zpl_input": "^XA^BY2^FO50,50^BSN,80,N^FD51999^FS^XZ",
+ "expected_bounds": {
+ "x": 50,
+ "y": 50,
+ "width": 94,
+ "height": 80
+ },
+ "image_ref": "barcode_upcean_supp5_standard.png"
+ },
+ {
+ "id": "barcode_upcean_supp2_standard",
+ "zpl_input": "^XA^BY2^FO50,50^BSN,80,N^FD42^FS^XZ",
+ "expected_bounds": {
+ "x": 50,
+ "y": 50,
+ "width": 40,
+ "height": 80
+ },
+ "image_ref": "barcode_upcean_supp2_standard.png"
}
]
}
\ No newline at end of file
diff --git a/tests/fixtures/testCases.ts b/tests/fixtures/testCases.ts
index 301a2a0b..a9fdff91 100644
--- a/tests/fixtures/testCases.ts
+++ b/tests/fixtures/testCases.ts
@@ -293,4 +293,25 @@ export const testCases: TestCase[] = [
expected_bounds: { x: 100, y: 100, width: 113, height: 190 },
image_ref: "barcode_ean13_rot_B.png",
},
+ // UPC/EAN supplements (^BS): the human-readable digits print ABOVE
+ // the bars per Zebra firmware (and Labelary). bbox top sits 18 dots
+ // above the FO anchor; total height = bar height + 18.
+ // ^BS visual regression uses printInterpretation=N for a bars-only
+ // comparison — bwip-js and Zebra ship slightly different glyph
+ // shapes for the supplement digits, which would exceed the strict
+ // ALLOWED_TOLERANCE. The text-zone reservation is still asserted
+ // structurally by labelarySync.test.ts against this fixture's
+ // expected_bounds (which include the 18-dot zone above the bars).
+ {
+ id: "barcode_upcean_supp5_standard",
+ zpl_input: "^XA^BY2^FO50,50^BSN,80,N^FD51999^FS^XZ",
+ expected_bounds: { x: 50, y: 50, width: 94, height: 80 },
+ image_ref: "barcode_upcean_supp5_standard.png",
+ },
+ {
+ id: "barcode_upcean_supp2_standard",
+ zpl_input: "^XA^BY2^FO50,50^BSN,80,N^FD42^FS^XZ",
+ expected_bounds: { x: 50, y: 50, width: 40, height: 80 },
+ image_ref: "barcode_upcean_supp2_standard.png",
+ },
];