Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
78 changes: 77 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,79 @@ 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 (Code C + FNC1) to FNC1 — bwip auto-mode handles the C switch", () => {
expect(parseZplCode128Escapes(">91234")).toBe("^FNC11234");
});
Comment thread
u8array marked this conversation as resolved.
Outdated

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("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");
});
});

55 changes: 55 additions & 0 deletions src/components/Canvas/bwipHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,52 @@ 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 with FNC1 (bwip auto-mode switches to C for digit runs,
* so FNC1 alone is a sufficient translation)
Comment thread
u8array marked this conversation as resolved.
Outdated
* >: → 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":
case "9": out += "^FNC1"; i++; continue;
case "6": out += "^FNC2"; i++; continue;
case "7": out += "^FNC3"; i++; continue;
case "8": out += "^FNC4"; i++; continue;
case ":":
case ";": i++; continue; // subset switch — bwip auto-mode handles it
}
Comment thread
u8array marked this conversation as resolved.
Outdated
}
out += ch;
}
return out;
}

export function buildBwipOptions(
obj: LabelObject,
renderScale?: number,
Expand Down Expand Up @@ -280,6 +326,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