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
47 changes: 8 additions & 39 deletions packages/core/src/lib/barcodeDims.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
gtin14WithCheck,
} from "./gs1";
import { planGs1Fd } from "./gs1Plan";
import { code128ControlBwipRaw, code128FdToSymbols, code128PlainFd, code128SymbolsToBwipRaw } from "./code128Subset";
import { code128FdToSymbols, code128SymbolsToBwipRaw } from "./code128Subset";
import { planCode128Fd } from "./code128Plan";
import { isRectangular, dmVersionString, type DataMatrixProps } from "../registry/datamatrix";
import { MAXICODE_WIDTH_MM, MAXICODE_HEIGHT_MM } from "../registry/maxicode";
import { micropdf417ModeDims, micropdf417ModeFits } from "../registry/micropdf417";
Expand Down Expand Up @@ -249,19 +250,6 @@ export function getEanUpcHriFragmentsWith(
return txt.map((t) => ({ char: t[0], xModule: t[1] }));
}

// Forcing Code B keeps module count in sync with Labelary (^BC default).
// Returns null for chars outside ASCII 32..126.
function toCode128BRaw(text: string): string | null {
if (!text) return null;
const parts = ["^104"]; // Start B
for (const ch of text) {
const code = ch.charCodeAt(0);
if (code < 32 || code > 126) return null;
parts.push(`^${String(code - 32).padStart(3, "0")}`);
}
return parts.join("");
}

export function buildBwipOptions(
obj: LeafObject,
renderScale?: number,
Expand Down Expand Up @@ -340,31 +328,12 @@ export function buildBwipOptions(
break;
}
const text = p.content || "0";
// Control bytes render from the same symbol plan the emitter writes as
// subset invocations, so canvas width equals printed width by
// construction. This is planCode128Fd's whole mode in bwip symbol
// values; keep the two branches in lockstep with code128Plan.ts.
const ctrlRaw = code128ControlBwipRaw(text);
if (ctrlRaw) {
opts = { bcid, text: ctrlRaw, raw: true, scale, height: 10 };
break;
}
// Interpret the EMITTED payload: the printer only reads the
// fdPlainEscape form, and a bare `>` diverges from the model content.
const fdText = code128PlainFd(text);
if (/>[0-9:;<=]/.test(fdText)) {
const symbols = code128FdToSymbols(fdText);
if (symbols) {
opts = { bcid, text: code128SymbolsToBwipRaw(symbols), raw: true, scale, height: 10 };
break;
}
}
const rawB = toCode128BRaw(text);
if (rawB) {
opts = { bcid, text: rawB, raw: true, scale, height: 10 };
} else {
opts = { bcid, text, scale, height: 10 };
}
// The printer reads the emitted ^FD, not the model text, so the canvas
// draws its Table-2 read; a byte no subset carries is left to bwip.
const symbols = code128FdToSymbols(planCode128Fd(text, "whole").fd);
opts = symbols
? { bcid, text: code128SymbolsToBwipRaw(symbols), raw: true, scale, height: 10 }
: { bcid, text, scale, height: 10 };
break;
}
case "code39":
Expand Down
96 changes: 74 additions & 22 deletions packages/core/src/lib/code128Subset.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,43 @@
import { describe, expect, it } from "vitest";
import bwipjs from "bwip-js/generic";
import {
code128ControlBwipRaw,
code128ControlFd,
code128FdToBytes,
code128FdToDisplayText,
code128FdToSymbols,
code128PlainFd,
code128SymbolsToBwipRaw,
code128SymbolsToFd,
hasControlBytes,
planCode128Symbols,
} from "./code128Subset";
import { measureBarcodeFootprintDotsWith, resolveForMeasure, type BwipEngine } from "./barcodeDims";
import { buildBwipOptions, measureBarcodeFootprintDotsWith, resolveForMeasure, type BwipEngine } from "./barcodeDims";
import type { LeafObject } from "../registry";
import type { LabelObject } from "../types/Group";

const engine = bwipjs as unknown as BwipEngine;

const code128 = (content: string): LeafObject =>
({
id: "b", type: "code128", x: 0, y: 0, rotation: 0, positionType: "FO",
props: {
content, height: 100, moduleWidth: 2, printInterpretation: false,
printInterpretationAbove: false, checkDigit: false, rotation: "N",
},
}) as LabelObject as LeafObject;

/** Deterministic LCG corpus so a failure reproduces. */
function seededCorpus(seed: number, atoms: readonly string[], count: number, maxLen: number): string[] {
let state = seed;
const rnd = () => (state = (state * 48271) % 0x7fffffff) / 0x7fffffff;
return Array.from({ length: count }, () => {
const len = 1 + Math.floor(rnd() * maxLen);
let text = "";
for (let j = 0; j < len; j++) text += atoms[Math.floor(rnd() * atoms.length)];
return text;
});
}

const HT = "\x09";
const LF = "\x0A";
const CR = "\x0D";
Expand Down Expand Up @@ -48,7 +70,6 @@ describe("planCode128Symbols", () => {

it("returns null without a control byte (the plain path stays untouched)", () => {
expect(code128ControlFd("ABC123")).toBeNull();
expect(code128ControlBwipRaw("ABC123")).toBeNull();
});
});

Expand Down Expand Up @@ -156,16 +177,10 @@ describe("code128FdToSymbols", () => {

describe("strict/lenient reader drift", () => {
it("the lenient reader reproduces every emitted invocation plan", () => {
// Deterministic LCG so failures reproduce; the two Table-2 readers are
// separate by design (adoption gate vs firmware emulation), this pins
// that they agree on everything the emitter can produce.
let seed = 0x2f6e2b1;
const rnd = () => (seed = (seed * 48271) % 0x7fffffff) / 0x7fffffff;
const alphabet = "AZaz09 >^~\t\r\n\x1D\x1C\x01\x7F";
for (let i = 0; i < 2000; i++) {
const len = 1 + Math.floor(rnd() * 12);
let text = "";
for (let j = 0; j < len; j++) text += alphabet[Math.floor(rnd() * alphabet.length)];
// The two Table-2 readers are separate by design (adoption gate vs
// firmware emulation); this pins that they agree on everything the
// emitter can produce.
for (const text of seededCorpus(0x2f6e2b1, [..."AZaz09 >^~\t\r\n\x1D\x1C\x01\x7F"], 2000, 12)) {
const plan = planCode128Symbols(text);
if (!plan) continue;
const fd = code128SymbolsToFd(plan);
Expand Down Expand Up @@ -217,15 +232,6 @@ describe("code128PlainFd", () => {
});

describe("printed vs rendered module count", () => {
const code128 = (content: string): LeafObject =>
({
id: "b", type: "code128", x: 0, y: 0, rotation: 0, positionType: "FO",
props: {
content, height: 100, moduleWidth: 2, printInterpretation: false,
printInterpretationAbove: false, checkDigit: false, rotation: "N",
},
}) as LabelObject as LeafObject;

/** Symbols = plan + check + stop; stop carries 2 modules more than the rest. */
const plannedModules = (text: string) => 11 * (planCode128Symbols(text)!.length + 1) + 13;

Expand Down Expand Up @@ -270,3 +276,49 @@ describe("printed vs rendered module count", () => {
expect(dots?.w).toBe(plainModules(3) * 2);
});
});

describe("canvas read of the emitted FD", () => {
const START_B = 104;

/** Printed symbols per payload class: control bytes via the invocation
* plan, invocation sequences via the Table-2 read, plain ASCII as one
* Subset-B symbol per character, a byte no subset carries left to bwip. */
const printed = (text: string): { text: string; raw: boolean } => {
if (hasControlBytes(text)) {
const values = planCode128Symbols(text);
if (values) return { text: code128SymbolsToBwipRaw(values), raw: true };
}
const fd = code128PlainFd(text);
if (/>[0-9:;<=]/.test(fd)) {
const symbols = code128FdToSymbols(fd);
if (symbols) return { text: code128SymbolsToBwipRaw(symbols), raw: true };
}
const values = [START_B];
for (const ch of text) {
const code = ch.charCodeAt(0);
if (code < 32 || code > 126) return { text, raw: false };
values.push(code - 32);
}
return { text: code128SymbolsToBwipRaw(values), raw: true };
};

it("renders every payload class as the printer reads it", () => {
const atoms = [
..."AZaz09 >^~\t\r\n\x1D\x1C\x01\x7Fä€",
...[..."0123456789:;<="].map((c) => `>${c}`),
];
for (const text of ["", ...seededCorpus(0x5bd1e99, atoms, 3000, 8)]) {
const expected = printed(text || "0");
const opts = buildBwipOptions(code128(text), 1, 8);
expect(opts?.text, JSON.stringify(text)).toBe(expected.text);
expect(opts?.raw ?? false, JSON.stringify(text)).toBe(expected.raw);
}
});

it("resolves a control chip like the whole-field emit does", () => {
// Callers without a binding chain (MCP footprint) hand chips through
// unresolved; the whole-mode plan still encodes the byte the printer gets.
expect(buildBwipOptions(code128("A«ctrl:TAB»B"), 1, 8)?.text)
.toBe(buildBwipOptions(code128(`A${HT}B`), 1, 8)?.text);
});
});
14 changes: 4 additions & 10 deletions packages/core/src/lib/code128Subset.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/** Code 128 subset-invocation coding for ^BC field data (spec p.95-99).
*
* The firmware drops ^FH-escaped C0 bytes from the symbol (ZD230-verified);
* only the invocation form encodes them. Emit and canvas share the planned
* symbol values, so printed module count equals rendered by construction
* (shared ^FN slots excepted: they emit raw and preflight warns). */
* only the invocation form encodes them. The canvas reads the emitted ^FD
* back through the same Table-2 walk, so printed module count equals
* rendered by construction (shared ^FN slots excepted: they emit raw and
* preflight warns). */

import { mapLiteralSpans } from "./fnTemplate";
import { C0_OR_DEL_RE } from "../types/controlKey";
Expand Down Expand Up @@ -144,13 +145,6 @@ export function code128ControlFd(text: string): string | null {
return values ? code128SymbolsToFd(values) : null;
}

/** {@link code128ControlFd}'s canvas twin: the same plan as bwip raw data. */
export function code128ControlBwipRaw(text: string): string | null {
if (!hasControlBytes(text)) return null;
const values = planCode128Symbols(text);
return values ? code128SymbolsToBwipRaw(values) : null;
}

/** Shared Table-2 walk over a ^BC ^FD payload: symbol values plus the data
* characters the interpretation line shows. `ok` is false when a byte fits no
* subset (the only unreadable case under the lenient rules). */
Expand Down
Loading