Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 86 additions & 1 deletion src/components/Canvas/bwipHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { buildBwipOptions, getDisplaySize, getEanUpcLayout } from "./bwipHelpers";
import { buildBwipOptions, getDisplaySize, getEanUpcLayout, parseZplCode128Escapes } from "./bwipHelpers";
import type { LabelObject } from "../../registry";

describe("getEanUpcLayout", () => {
Expand Down Expand Up @@ -217,3 +217,88 @@ describe("getDisplaySize coverage (ZPL-first policy)", () => {
expect(missing, `Missing explicit case for: ${missing.join(", ")}`).toEqual([]);
});
});

describe("parseZplCode128Escapes", () => {
it("returns null for plain ASCII (no escape sequences)", () => {
expect(parseZplCode128Escapes("ABC123")).toBeNull();
expect(parseZplCode128Escapes("")).toBeNull();
});

it("translates >5 to FNC1", () => {
expect(parseZplCode128Escapes("AB>5CD")).toBe("AB^FNC1CD");
});

it("translates >9 to FNC1 only at the start of the field (per ZPL spec)", () => {
expect(parseZplCode128Escapes(">91234")).toBe("^FNC11234");
// Mid-string >9 is just a Code-C invocation; bwip auto-mode picks C
// for the digit run, so we drop the escape entirely.
expect(parseZplCode128Escapes("A>9123")).toBe("A123");
});

it("translates >6/>7/>8 to FNC2/FNC3/FNC4", () => {
expect(parseZplCode128Escapes("A>6B")).toBe("A^FNC2B");
expect(parseZplCode128Escapes("A>7B")).toBe("A^FNC3B");
expect(parseZplCode128Escapes("A>8B")).toBe("A^FNC4B");
});

it("translates >0 to a literal `>`", () => {
expect(parseZplCode128Escapes("A>0B>5")).toBe("A>B^FNC1");
});

it("drops >: and >; (subset switches — bwip auto-mode picks the subset)", () => {
expect(parseZplCode128Escapes("A>:B>;C>5")).toBe("ABC^FNC1");
});

it("doubles literal `^` so bwip parsefnc does not treat it as an escape", () => {
expect(parseZplCode128Escapes("A^B>5")).toBe("A^^B^FNC1");
});

it("leaves trailing `>` and unknown `>X` literal — matches firmware behaviour", () => {
expect(parseZplCode128Escapes("abc>")).toBeNull();
// `>z` is not a defined ZPL escape; Zebra treats it as literal `>z`.
expect(parseZplCode128Escapes("a>z>5")).toBe("a>z^FNC1");
});

it("handles the reported case STRSTR>5… with auto Code-C compaction", () => {
// Without translation: 21 raw Subset-B symbols.
// After translation: bwip sees STRSTR + FNC1 + 16 digits, auto-switches
// to Code C for the digit run → ~15 data symbols, matching firmware.
expect(parseZplCode128Escapes("STRSTR>52316094000242201"))
.toBe("STRSTR^FNC12316094000242201");
});
});

describe("buildBwipOptions code128 escape handling", () => {
const code128 = (content: string): LabelObject =>
({
id: "1",
type: "code128",
x: 0,
y: 0,
rotation: 0,
props: {
content,
height: 100,
moduleWidth: 2,
printInterpretation: false,
checkDigit: false,
rotation: "N",
},
}) as LabelObject;

it("uses raw Subset-B mode for plain ASCII content (existing behaviour)", () => {
const opts = buildBwipOptions(code128("ABC123"), 1, 8);
expect(opts?.raw).toBe(true);
expect(opts?.parsefnc).toBeUndefined();
expect(typeof opts?.text).toBe("string");
expect((opts?.text as string).startsWith("^104")).toBe(true);
});

it("switches to parsefnc auto-mode when ZPL escape sequences are present", () => {
const opts = buildBwipOptions(code128("STRSTR>52316094000242201"), 1, 8);
expect(opts?.parsefnc).toBe(true);
expect(opts?.raw).toBeUndefined();
expect(opts?.text).toBe("STRSTR^FNC12316094000242201");
});
});

56 changes: 56 additions & 0 deletions src/components/Canvas/bwipHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,53 @@ export function toCode128BRaw(text: string): string | null {
return parts.join("");
}

/**
* Translate ZPL ^BC field-data escape sequences (`>X`) into bwip-js parsefnc
* syntax. Returns null when no recognized escape is present so the caller can
* stay on the existing raw-Subset-B path.
*
* ZPL Code 128 escapes (Zebra ZPL II Programming Guide):
* >0 → literal `>`
* >5 → FNC1
* >6 → FNC2
* >7 → FNC3
* >8 → FNC4
* >9 → invoke Code C; inserts FNC1 only when it is the first character
* of the field (per ZPL II manual). Mid-string `>9` is just a
* subset switch which bwip auto-mode handles for digit runs.
* >: → switch to Subset B (dropped; bwip auto-mode chooses the subset)
* >; → switch to Subset A (dropped; bwip auto-mode chooses the subset)
*
* Without this translation, `STRSTR>52316094000242201` is rendered as 21 raw
* Subset-B symbols, while the firmware reads `>5` as FNC1 and switches to
* Subset C for the 16 trailing digits — yielding ~15 symbols and a much
* narrower bbox. The mismatch is what users observe as "Länge stimmt nicht".
*/
export function parseZplCode128Escapes(text: string): string | null {
if (!/>[05-9:;]/.test(text)) return null;
let out = "";
for (let i = 0; i < text.length; i++) {
const ch = text[i];
// bwip parsefnc treats `^` as escape char; double it for a literal `^`.
if (ch === "^") { out += "^^"; continue; }
if (ch === ">" && i + 1 < text.length) {
const next = text[i + 1];
switch (next) {
case "0": out += ">"; i++; continue;
case "5": out += "^FNC1"; i++; continue;
case "6": out += "^FNC2"; i++; continue;
case "7": out += "^FNC3"; i++; continue;
case "8": out += "^FNC4"; i++; continue;
case "9": if (i === 0) out += "^FNC1"; i++; continue;
case ":":
case ";": i++; continue; // subset switch — bwip auto-mode handles it
}
}
out += ch;
}
return out;
}

export function buildBwipOptions(
obj: LabelObject,
renderScale?: number,
Expand Down Expand Up @@ -280,6 +327,15 @@ export function buildBwipOptions(
const text = p.content || "0";
// Note: ZPL ^BC e=Y (checkDigit) only prints the MOD-10 digit in the
// interpretation line — it does NOT append it to the encoded barcode data.
// ZPL escape sequences (e.g. `>5` for FNC1, `>9` for Code-C switch with
// FNC1) require parsefnc auto-mode so bwip emits the same compact symbol
// count Zebra firmware does. Plain ASCII falls through to the raw Code B
// path which keeps the existing module count behaviour unchanged.
const escaped = parseZplCode128Escapes(text);
if (escaped !== null) {
opts = { bcid, text: escaped, parsefnc: true, scale, height: 10 };
break;
}
const rawB = toCode128BRaw(text);
if (rawB) {
opts = { bcid, text: rawB, raw: true, scale, height: 10 };
Expand Down