From 667e3f55da9c9ae5091307d7427c4ce2c2edd407 Mon Sep 17 00:00:00 2001 From: Hiep Le Date: Wed, 28 Oct 2026 12:16:12 +0700 Subject: [PATCH 1/2] feat(code128): the mode prop emits ^BC m=A and the canvas packs digit runs into Subset C MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code 128 had no way to pick the ^BC m parameter, so every export was mode N (Subset B, one symbol per digit) and import dropped an m=A. A numeric payload prints much wider than it needs to: 0240018268623 at ^BY2 is 356 dots in mode N and 246 dots in mode A. - `mode` (N | A) on the 1D props, honoured by Code 128 only; gs1 (m=D) wins - emit `^BC…,A`, parse m=A back into `mode: 'A'` - canvas renders mode A from an automatic subset plan (ISO/IEC 15417 Annex E, Subsets B/C) so the preview width matches the print - panel checkbox (hidden while GS1 is on), MCP prop doc, label in every locale Co-Authored-By: Claude Opus 5.5 --- packages/core/src/lib/barcodeDims.ts | 9 +- packages/core/src/lib/code128Auto.test.ts | 94 +++++++++++++++++++ packages/core/src/lib/code128Subset.ts | 58 ++++++++++++ packages/core/src/lib/zplParser/context.ts | 4 + packages/core/src/lib/zplParser/flushField.ts | 1 + .../src/lib/zplParser/handlers/barcodes.ts | 1 + packages/core/src/registry/barcode1d.ts | 6 ++ packages/core/src/registry/code128.ts | 7 +- packages/mcp-server/src/boundary.ts | 1 + src/locales/ar.ts | 1 + src/locales/bg.ts | 1 + src/locales/ca.ts | 1 + src/locales/cs.ts | 1 + src/locales/da.ts | 1 + src/locales/de.ts | 1 + src/locales/el.ts | 1 + src/locales/en.ts | 1 + src/locales/es.ts | 1 + src/locales/et.ts | 1 + src/locales/fa.ts | 1 + src/locales/fi.ts | 1 + src/locales/fr.ts | 1 + src/locales/he.ts | 1 + src/locales/hr.ts | 1 + src/locales/hu.ts | 1 + src/locales/it.ts | 1 + src/locales/ja.ts | 1 + src/locales/ko.ts | 1 + src/locales/lt.ts | 1 + src/locales/lv.ts | 1 + src/locales/nl.ts | 1 + src/locales/no.ts | 1 + src/locales/pl.ts | 1 + src/locales/pt.ts | 1 + src/locales/ro.ts | 1 + src/locales/sk.ts | 1 + src/locales/sl.ts | 1 + src/locales/sr.ts | 1 + src/locales/sv.ts | 1 + src/locales/tr.ts | 1 + src/locales/zh-hans.ts | 1 + src/locales/zh-hant.ts | 1 + src/registry/barcode1d.panel.tsx | 13 +++ src/registry/code128.panel.tsx | 1 + 44 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/lib/code128Auto.test.ts diff --git a/packages/core/src/lib/barcodeDims.ts b/packages/core/src/lib/barcodeDims.ts index 1ef978b99..385877fe2 100644 --- a/packages/core/src/lib/barcodeDims.ts +++ b/packages/core/src/lib/barcodeDims.ts @@ -18,7 +18,7 @@ import { gtin14WithCheck, } from "./gs1"; import { planGs1Fd } from "./gs1Plan"; -import { code128FdToSymbols, code128SymbolsToBwipRaw } from "./code128Subset"; +import { code128FdToSymbols, code128SymbolsToBwipRaw, planCode128AutoSymbols } from "./code128Subset"; import { planCode128Fd } from "./code128Plan"; import { isRectangular, dmVersionString, type DataMatrixProps } from "../registry/datamatrix"; import { MAXICODE_WIDTH_MM, MAXICODE_HEIGHT_MM } from "../registry/maxicode"; @@ -328,6 +328,13 @@ export function buildBwipOptions( break; } const text = p.content || "0"; + // Mode A: the firmware picks subsets itself (digit runs in C), so the + // canvas follows the same automatic plan instead of forcing Subset B. + const autoSymbols = p.mode === "A" ? planCode128AutoSymbols(text) : null; + if (autoSymbols) { + opts = { bcid, text: code128SymbolsToBwipRaw(autoSymbols), raw: true, scale, height: 10 }; + break; + } // 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); diff --git a/packages/core/src/lib/code128Auto.test.ts b/packages/core/src/lib/code128Auto.test.ts new file mode 100644 index 000000000..da13184a3 --- /dev/null +++ b/packages/core/src/lib/code128Auto.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from "vitest"; +import bwipjs from "bwip-js/generic"; +import { planCode128AutoSymbols } from "./code128Subset"; +import { measureBarcodeFootprintDotsWith, type BwipEngine } from "./barcodeDims"; +import { code128 } from "../registry/code128"; +import { parseZPL } from "./zplParser"; +import type { LeafObject } from "../registry"; +import type { Code128Props } from "../registry/code128"; + +const engine = bwipjs as unknown as BwipEngine; + +const START_B = 104; +const START_C = 105; +const CODE_B = 100; +const CODE_C = 99; + +const leaf = (content: string, extra: Partial = {}): 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", ...extra, + }, + }) as LeafObject; + +/** Start + data symbols + check at 11 modules each, stop at 13. */ +const modules = (dataSymbols: number) => 11 * (dataSymbols + 2) + 13; + +const firstProps = (zpl: string): Code128Props => + (parseZPL(zpl, 8).pages[0]?.objects[0] as LeafObject & { props: Code128Props }).props; + +describe("planCode128AutoSymbols (^BC mode A)", () => { + it("packs an even digit string into Subset C from the start", () => { + expect(planCode128AutoSymbols("123456")).toEqual([START_C, 12, 34, 56]); + }); + + it("starts in C and drops to B for the last digit of an odd run", () => { + expect(planCode128AutoSymbols("0240018268623")).toEqual( + [START_C, 2, 40, 1, 82, 68, 62, CODE_B, 19], + ); + }); + + it("keeps short digit runs in B", () => { + expect(planCode128AutoSymbols("A12B")).toEqual([START_B, 33, 17, 18, 34]); + expect(planCode128AutoSymbols("123")).toEqual([START_B, 17, 18, 19]); + }); + + it("uses C for a two-digit payload", () => { + expect(planCode128AutoSymbols("42")).toEqual([START_C, 42]); + }); + + it("switches into C mid-payload, leaving the first digit of an odd run in B", () => { + expect(planCode128AutoSymbols("AB12345")).toEqual( + [START_B, 33, 34, 17, CODE_C, 23, 45], + ); + expect(planCode128AutoSymbols("AB1234CD")).toEqual( + [START_B, 33, 34, CODE_C, 12, 34, CODE_B, 35, 36], + ); + }); + + it("returns null for control bytes so the plain plan takes over", () => { + expect(planCode128AutoSymbols("A\x09B")).toBeNull(); + expect(planCode128AutoSymbols("")).toBeNull(); + }); +}); + +describe("Code 128 mode A emit, import and canvas width", () => { + it("emits m=A and omits it for the default mode", () => { + expect(code128.toZPL(leaf("0240018268623", { mode: "A" }) as never)) + .toContain("^BCN,100,N,N,N,A^FD0240018268623^FS"); + expect(code128.toZPL(leaf("0240018268623") as never)) + .toContain("^BCN,100,N,N,N^FD0240018268623^FS"); + }); + + it("lets GS1 mode win over mode A", () => { + const zpl = code128.toZPL(leaf("0104012345678901", { mode: "A", gs1: true }) as never); + expect(zpl).toContain("^BCN,100,N,N,N,D"); + expect(zpl).not.toContain(",A"); + }); + + it("imports ^BC..,A as mode A and a bare ^BC without it", () => { + expect(firstProps("^XA^FO10,10^BY2^BCN,80,Y,N,N,A^FD0240018268623^FS^XZ").mode).toBe("A"); + expect(firstProps("^XA^FO10,10^BY2^BCN,80,Y,N,N^FD0240018268623^FS^XZ").mode).toBeUndefined(); + }); + + it("renders mode A at the Subset C width instead of Subset B", () => { + const text = "0240018268623"; + const auto = measureBarcodeFootprintDotsWith(engine, leaf(text, { mode: "A" }), 8); + const plain = measureBarcodeFootprintDotsWith(engine, leaf(text), 8); + expect(auto?.w).toBe(modules(planCode128AutoSymbols(text)!.length - 1) * 2); + expect(auto?.w).toBe(246); + expect(plain?.w).toBe(356); + }); +}); diff --git a/packages/core/src/lib/code128Subset.ts b/packages/core/src/lib/code128Subset.ts index f509e92ee..275749dda 100644 --- a/packages/core/src/lib/code128Subset.ts +++ b/packages/core/src/lib/code128Subset.ts @@ -80,6 +80,64 @@ export function planCode128Symbols(text: string): number[] | null { return values; } +const CODE_C = 99; + +const isDigitAt = (text: string, i: number): boolean => /\d/.test(text.charAt(i)); + +/** Length of the digit run starting at `from`. */ +function digitRun(text: string, from: number): number { + let n = 0; + while (isDigitAt(text, from + n)) n += 1; + return n; +} + +/** + * Symbol values (start code first, no check/stop) for `^BC` mode A, the + * firmware's automatic subset selection: four or more digits in a row pack + * into Subset C, two per symbol (ISO/IEC 15417 Annex E). Printable ASCII + * only; returns null for control bytes or anything outside SP..DEL so the + * caller falls back to the plain (Subset A/B) plan. + */ +export function planCode128AutoSymbols(text: string): number[] | null { + if (!text) return null; + const bValues: number[] = []; + for (let i = 0; i < text.length; i += 1) { + const value = valueInB(text.charCodeAt(i)); + if (value === null) return null; + bValues.push(value); + } + const lead = digitRun(text, 0); + let inC = lead >= 4 || (lead === 2 && text.length === 2); + const values = [inC ? START_C : START_B]; + let i = 0; + while (i < text.length) { + if (inC) { + if (digitRun(text, i) >= 2) { + values.push(parseInt(text.slice(i, i + 2), 10)); + i += 2; + continue; + } + values.push(CODE_B); + inC = false; + continue; + } + const run = digitRun(text, i); + if (run >= 4) { + // Odd run: the first digit stays in B so the rest pairs up evenly. + if (run % 2 === 1) { + values.push(bValues[i] ?? 0); + i += 1; + } + values.push(CODE_C); + inC = true; + continue; + } + values.push(bValues[i] ?? 0); + i += 1; + } + return values; +} + /** Literal escapes (spec p.96, Table 2); same codes in Subsets A and B, the * values here are Subset B's. */ const B_LITERAL_ESCAPE = new Map([ diff --git a/packages/core/src/lib/zplParser/context.ts b/packages/core/src/lib/zplParser/context.ts index 9c0accc5f..86bf709f9 100644 --- a/packages/core/src/lib/zplParser/context.ts +++ b/packages/core/src/lib/zplParser/context.ts @@ -280,6 +280,8 @@ export interface FieldState { bcRotation: ZplRotation; /** ^BC m=D (UCC/EAN) → GS1-128; consumed by the code128 case in flushField. */ bcGs1: boolean; + /** ^BC m=A (automatic subset selection); consumed by the code128 case in flushField. */ + bcAuto: boolean; bcCode49Mode: Code49Props["mode"]; /** ^B4 h as sent, or null when omitted; resolved at ^FS. */ bcCode49Mult: number | null; @@ -581,6 +583,7 @@ export function resetSymbologyModeFlags(field: FieldState): void { field.qrHeaderPartial = false; field.qrHeaderLossy = false; field.bcGs1 = false; + field.bcAuto = false; field.dmEscape = undefined; field.dmQuality = 200; field.bcMsiCheckMode = undefined; @@ -744,6 +747,7 @@ export function freshFieldState(): FieldState { bcMsiHriCheck: false, bcRotation: "N", bcGs1: false, + bcAuto: false, bcCode49Mode: "A", bcCode49Mult: null, symRot: "N", diff --git a/packages/core/src/lib/zplParser/flushField.ts b/packages/core/src/lib/zplParser/flushField.ts index 1b29c09e2..b36d62789 100644 --- a/packages/core/src/lib/zplParser/flushField.ts +++ b/packages/core/src/lib/zplParser/flushField.ts @@ -387,6 +387,7 @@ export function createCloseField( checkDigit: s.field.bcCheck, rotation: s.field.bcRotation, ...(gs1 ? { gs1: true } : {}), + ...(s.field.bcAuto ? { mode: "A" as const } : {}), } satisfies Code128Props, posType, comment, diff --git a/packages/core/src/lib/zplParser/handlers/barcodes.ts b/packages/core/src/lib/zplParser/handlers/barcodes.ts index 035a34843..cd3d018b9 100644 --- a/packages/core/src/lib/zplParser/handlers/barcodes.ts +++ b/packages/core/src/lib/zplParser/handlers/barcodes.ts @@ -64,6 +64,7 @@ export function createBarcodeHandlers(s: ParserState): Record { BC: (p, rest, cmd) => { mkBarcode("code128", 1, 2, "Y", 4)(p, rest, cmd); s.field.bcGs1 = (p[5] ?? "").toUpperCase() === "D"; + s.field.bcAuto = (p[5] ?? "").toUpperCase() === "A"; }, B3: mkBarcode("code39", 2, 3, "Y", 1), // ^B3N,c,h,i,N BE: mkBarcode("ean13", 1, 2), // ^BEN,h,i,N diff --git a/packages/core/src/registry/barcode1d.ts b/packages/core/src/registry/barcode1d.ts index f567ef072..bfbc7e3be 100644 --- a/packages/core/src/registry/barcode1d.ts +++ b/packages/core/src/registry/barcode1d.ts @@ -17,6 +17,7 @@ import type { ContentSpec } from '../types/contentSpec'; import { ROTATION_SPEC, type ZplRotation } from './rotation'; export const MSI_CHECK_MODES = ['C', 'D'] as const; +export const CODE128_MODES = ['N', 'A'] as const; export interface Barcode1DProps { content: string; @@ -33,6 +34,10 @@ export interface Barcode1DProps { preSerialContent?: string; /** GS1-128 mode (`^BC…,D`). Only honoured by a `gs1Capable` symbology. */ gs1?: boolean; + /** Code 128 m-param: N (default, Subset B unless the ^FD carries + * invocation codes) or A (automatic: the firmware packs digit runs into + * Subset C). Only honoured by Code 128; `gs1` (m=D) wins over it. */ + mode?: (typeof CODE128_MODES)[number]; /** MSI multi-check ^BM e beyond the boolean (C = 2x Mod 10, D = Mod 11 + * Mod 10); set by import, not the panel. */ msiCheckMode?: (typeof MSI_CHECK_MODES)[number]; @@ -97,6 +102,7 @@ export const BARCODE_1D_PROP_SPECS: PropSpecs = { serial: { type: 'object' }, preSerialContent: { type: 'string' }, gs1: { type: 'boolean' }, + mode: { type: 'string', values: CODE128_MODES }, msiCheckMode: { type: 'string', values: MSI_CHECK_MODES }, msiHriCheck: { type: 'boolean' }, }; diff --git a/packages/core/src/registry/code128.ts b/packages/core/src/registry/code128.ts index 8b9eaeeca..5b39bc399 100644 --- a/packages/core/src/registry/code128.ts +++ b/packages/core/src/registry/code128.ts @@ -17,9 +17,10 @@ export const code128CoreConfig: Barcode1DCoreConfig = { zplCommand: (p) => { const interp = p.printInterpretation ? 'Y' : 'N'; const check = p.checkDigit ? 'Y' : 'N'; - // m=D (UCC/EAN) turns a plain Code 128 into GS1-128; omit it otherwise so a - // non-GS1 field round-trips as the bare command. - const mode = p.gs1 ? ',D' : ''; + // m=D (UCC/EAN) turns a plain Code 128 into GS1-128; m=A lets the firmware + // pick subsets (digit runs in C). Omit m for the default N so a plain field + // round-trips as the bare command. + const mode = p.gs1 ? ',D' : p.mode === 'A' ? ',A' : ''; return `^BC${p.rotation},${p.height},${interp},${p.printInterpretationAbove ? 'Y' : 'N'},${check}${mode}`; }, }; diff --git a/packages/mcp-server/src/boundary.ts b/packages/mcp-server/src/boundary.ts index 550e39f85..41d39f1f8 100644 --- a/packages/mcp-server/src/boundary.ts +++ b/packages/mcp-server/src/boundary.ts @@ -285,6 +285,7 @@ export const PROP_SUMMARIES: Record> = { checkDigit: "boolean", rotation: "N | R | I | B", gs1: "boolean, GS1-128 mode", + mode: "N (default) | A: A lets the printer pick subsets, packing 4+ digit runs two per symbol (Subset C) for a much narrower numeric barcode; ignored when gs1 is on", }, image: { imageId: "string, id of an image the app holds; empty for a pure ZPL graphic", diff --git a/src/locales/ar.ts b/src/locales/ar.ts index 7f84e7a12..cb82e0d32 100644 --- a/src/locales/ar.ts +++ b/src/locales/ar.ts @@ -1354,6 +1354,7 @@ const ar = { printInterpretation: 'قابل للقراءة البشرية', checkDigit: 'رقم التحقق', gs1Mode: 'وضع GS1', + autoMode: 'مجموعة فرعية تلقائية (أرقام مضغوطة)', moduleWidth: 'عرض الوحدة', }, box: { diff --git a/src/locales/bg.ts b/src/locales/bg.ts index 95c74f88c..1b84f1782 100644 --- a/src/locales/bg.ts +++ b/src/locales/bg.ts @@ -1354,6 +1354,7 @@ const bg = { printInterpretation: 'Четливо от хора', checkDigit: 'Контролна цифра', gs1Mode: 'Режим GS1', + autoMode: 'Автоматичен набор (компактни цифри)', moduleWidth: 'Ширина на модула', }, box: { diff --git a/src/locales/ca.ts b/src/locales/ca.ts index cf066ad37..1cc171b7c 100644 --- a/src/locales/ca.ts +++ b/src/locales/ca.ts @@ -1410,6 +1410,7 @@ const ca = { "printInterpretation": "Text llegible", "checkDigit": "Dígit de control", "gs1Mode": "Mode GS1", + "autoMode": "Subconjunt automàtic (dígits compactes)", "moduleWidth": "Amplada del mòdul" }, "box": { diff --git a/src/locales/cs.ts b/src/locales/cs.ts index 91e1992b1..b2aa569a5 100644 --- a/src/locales/cs.ts +++ b/src/locales/cs.ts @@ -1354,6 +1354,7 @@ const cs = { printInterpretation: 'Čitelný pro člověka', checkDigit: 'Kontrolní číslice', gs1Mode: 'Režim GS1', + autoMode: 'Automatická sada (kompaktní číslice)', moduleWidth: 'Šířka modulu', }, box: { diff --git a/src/locales/da.ts b/src/locales/da.ts index 670818faf..d2e307761 100644 --- a/src/locales/da.ts +++ b/src/locales/da.ts @@ -1354,6 +1354,7 @@ const da = { printInterpretation: 'Læsbar for mennesker', checkDigit: 'Kontrolciffer', gs1Mode: 'GS1-tilstand', + autoMode: 'Automatisk delsæt (kompakte cifre)', moduleWidth: 'Modulbredde', }, box: { diff --git a/src/locales/de.ts b/src/locales/de.ts index 58dc3b104..a33a2a864 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1419,6 +1419,7 @@ const de = { printInterpretation: 'Klartext', checkDigit: 'Prüfziffer', gs1Mode: 'GS1-Modus', + autoMode: 'Auto-Subset (Ziffern kompakt)', moduleWidth: 'Modulbreite', }, box: { diff --git a/src/locales/el.ts b/src/locales/el.ts index e2e4c9f18..9d02a8fd3 100644 --- a/src/locales/el.ts +++ b/src/locales/el.ts @@ -1354,6 +1354,7 @@ const el = { printInterpretation: 'Αναγνώσιμο από άνθρωπο', checkDigit: 'Ψηφίο ελέγχου', gs1Mode: 'Λειτουργία GS1', + autoMode: 'Αυτόματο υποσύνολο (συμπαγή ψηφία)', moduleWidth: 'Πλάτος module', }, box: { diff --git a/src/locales/en.ts b/src/locales/en.ts index f66fd4252..baac39840 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1419,6 +1419,7 @@ const en = { printInterpretation: 'Human readable', checkDigit: 'Check digit', gs1Mode: 'GS1 mode', + autoMode: 'Auto subset (compact digits)', moduleWidth: 'Module width', }, box: { diff --git a/src/locales/es.ts b/src/locales/es.ts index 3480d8ea3..6de1fe88f 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -1354,6 +1354,7 @@ const es = { printInterpretation: 'Legible por humanos', checkDigit: 'Dígito de control', gs1Mode: 'Modo GS1', + autoMode: 'Subconjunto automático (dígitos compactos)', moduleWidth: 'Ancho de módulo', }, box: { diff --git a/src/locales/et.ts b/src/locales/et.ts index 52ea368ba..ae3997329 100644 --- a/src/locales/et.ts +++ b/src/locales/et.ts @@ -1354,6 +1354,7 @@ const et = { printInterpretation: 'Inimesele loetav', checkDigit: 'Kontrollnumber', gs1Mode: 'GS1-režiim', + autoMode: 'Automaatne alamhulk (kompaktsed numbrid)', moduleWidth: 'Mooduli laius', }, box: { diff --git a/src/locales/fa.ts b/src/locales/fa.ts index cb96d16d6..3ad9f506d 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -1354,6 +1354,7 @@ const fa = { printInterpretation: 'قابل خواندن توسط انسان', checkDigit: 'رقم کنترل', gs1Mode: 'حالت GS1', + autoMode: 'زیرمجموعه خودکار (ارقام فشرده)', moduleWidth: 'عرض ماژول', }, box: { diff --git a/src/locales/fi.ts b/src/locales/fi.ts index 2c2c89a3f..50aee77fe 100644 --- a/src/locales/fi.ts +++ b/src/locales/fi.ts @@ -1354,6 +1354,7 @@ const fi = { printInterpretation: 'Ihmisluettava', checkDigit: 'Tarkistusnumero', gs1Mode: 'GS1-tila', + autoMode: 'Automaattinen osajoukko (tiiviit numerot)', moduleWidth: 'Moduulin leveys', }, box: { diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 4fb194459..b0b815d58 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -1354,6 +1354,7 @@ const fr = { printInterpretation: 'Lisible par l\'homme', checkDigit: 'Chiffre de contrôle', gs1Mode: 'Mode GS1', + autoMode: 'Sous-ensemble auto (chiffres compacts)', moduleWidth: 'Largeur de module', }, box: { diff --git a/src/locales/he.ts b/src/locales/he.ts index 667f53b6f..d8886c153 100644 --- a/src/locales/he.ts +++ b/src/locales/he.ts @@ -1354,6 +1354,7 @@ const he = { printInterpretation: 'קריא לאדם', checkDigit: 'ספרת ביקורת', gs1Mode: 'מצב GS1', + autoMode: 'תת-קבוצה אוטומטית (ספרות דחוסות)', moduleWidth: 'רוחב מודול', }, box: { diff --git a/src/locales/hr.ts b/src/locales/hr.ts index a3b25ddc1..edb23f105 100644 --- a/src/locales/hr.ts +++ b/src/locales/hr.ts @@ -1354,6 +1354,7 @@ const hr = { printInterpretation: 'Čitljivo za čovjeka', checkDigit: 'Kontrolna znamenka', gs1Mode: 'GS1 način', + autoMode: 'Automatski podskup (kompaktne znamenke)', moduleWidth: 'Širina modula', }, box: { diff --git a/src/locales/hu.ts b/src/locales/hu.ts index 81e688ae3..5acece513 100644 --- a/src/locales/hu.ts +++ b/src/locales/hu.ts @@ -1354,6 +1354,7 @@ const hu = { printInterpretation: 'Ember által olvasható', checkDigit: 'Ellenőrzőjegy', gs1Mode: 'GS1 mód', + autoMode: 'Automatikus készlet (tömör számjegyek)', moduleWidth: 'Modulszélesség', }, box: { diff --git a/src/locales/it.ts b/src/locales/it.ts index 0b5d1d452..d07b8041f 100644 --- a/src/locales/it.ts +++ b/src/locales/it.ts @@ -1354,6 +1354,7 @@ const it = { printInterpretation: "Leggibile dall'uomo", checkDigit: 'Cifra di controllo', gs1Mode: 'Modalità GS1', + autoMode: 'Sottoinsieme automatico (cifre compatte)', moduleWidth: 'Larghezza modulo', }, box: { diff --git a/src/locales/ja.ts b/src/locales/ja.ts index 2a5084da5..5ad15634a 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -1354,6 +1354,7 @@ const ja = { printInterpretation: '人間可読', checkDigit: 'チェックデジット', gs1Mode: 'GS1モード', + autoMode: '自動サブセット(数字を圧縮)', moduleWidth: 'モジュール幅', }, box: { diff --git a/src/locales/ko.ts b/src/locales/ko.ts index adc3d26b0..2592f0a3d 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -1354,6 +1354,7 @@ const ko = { printInterpretation: '사람이 읽을 수 있는', checkDigit: '체크 숫자', gs1Mode: 'GS1 모드', + autoMode: '자동 서브셋 (숫자 압축)', moduleWidth: '모듈 너비', }, box: { diff --git a/src/locales/lt.ts b/src/locales/lt.ts index 2829fd5a0..864f5b61a 100644 --- a/src/locales/lt.ts +++ b/src/locales/lt.ts @@ -1354,6 +1354,7 @@ const lt = { printInterpretation: 'Žmogui skaitomas', checkDigit: 'Tikrinimo skaitmuo', gs1Mode: 'GS1 režimas', + autoMode: 'Automatinis poaibis (kompaktiški skaitmenys)', moduleWidth: 'Modulio plotis', }, box: { diff --git a/src/locales/lv.ts b/src/locales/lv.ts index c7756f663..c1d99109c 100644 --- a/src/locales/lv.ts +++ b/src/locales/lv.ts @@ -1354,6 +1354,7 @@ const lv = { printInterpretation: 'Cilvēkam lasāms', checkDigit: 'Pārbaudes cipars', gs1Mode: 'GS1 režīms', + autoMode: 'Automātiska apakškopa (kompakti cipari)', moduleWidth: 'Moduļa platums', }, box: { diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 73efa1a06..be2052390 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -1354,6 +1354,7 @@ const nl = { printInterpretation: 'Leesbaar voor mensen', checkDigit: 'Controlegetal', gs1Mode: 'GS1-modus', + autoMode: 'Automatische subset (compacte cijfers)', moduleWidth: 'Modulebreedte', }, box: { diff --git a/src/locales/no.ts b/src/locales/no.ts index 22944a180..b4f682ac1 100644 --- a/src/locales/no.ts +++ b/src/locales/no.ts @@ -1354,6 +1354,7 @@ const no = { printInterpretation: 'Lesbar for mennesker', checkDigit: 'Kontrollsiffer', gs1Mode: 'GS1-modus', + autoMode: 'Automatisk delsett (kompakte sifre)', moduleWidth: 'Modulbredde', }, box: { diff --git a/src/locales/pl.ts b/src/locales/pl.ts index 7430a90e0..9764c68cf 100644 --- a/src/locales/pl.ts +++ b/src/locales/pl.ts @@ -1354,6 +1354,7 @@ const pl = { printInterpretation: 'Czytelny dla człowieka', checkDigit: 'Cyfra kontrolna', gs1Mode: 'Tryb GS1', + autoMode: 'Automatyczny podzbiór (zwarte cyfry)', moduleWidth: 'Szerokość modułu', }, box: { diff --git a/src/locales/pt.ts b/src/locales/pt.ts index e749f46b3..d8cb33311 100644 --- a/src/locales/pt.ts +++ b/src/locales/pt.ts @@ -1354,6 +1354,7 @@ const pt = { printInterpretation: 'Legível por humanos', checkDigit: 'Dígito de verificação', gs1Mode: 'Modo GS1', + autoMode: 'Subconjunto automático (dígitos compactos)', moduleWidth: 'Largura do módulo', }, box: { diff --git a/src/locales/ro.ts b/src/locales/ro.ts index 06656c4fa..5bbc1c756 100644 --- a/src/locales/ro.ts +++ b/src/locales/ro.ts @@ -1354,6 +1354,7 @@ const ro = { printInterpretation: 'Lizibil de om', checkDigit: 'Cifră de control', gs1Mode: 'Mod GS1', + autoMode: 'Subset automat (cifre compacte)', moduleWidth: 'Lățime modul', }, box: { diff --git a/src/locales/sk.ts b/src/locales/sk.ts index 3634909cc..2c0e594ff 100644 --- a/src/locales/sk.ts +++ b/src/locales/sk.ts @@ -1354,6 +1354,7 @@ const sk = { printInterpretation: 'Čitateľný pre človeka', checkDigit: 'Kontrolná číslica', gs1Mode: 'Režim GS1', + autoMode: 'Automatická sada (kompaktné číslice)', moduleWidth: 'Šírka modulu', }, box: { diff --git a/src/locales/sl.ts b/src/locales/sl.ts index 73a6f4c63..068a250e7 100644 --- a/src/locales/sl.ts +++ b/src/locales/sl.ts @@ -1354,6 +1354,7 @@ const sl = { printInterpretation: 'Berljivo za človeka', checkDigit: 'Kontrolna številka', gs1Mode: 'Način GS1', + autoMode: 'Samodejna podmnožica (strnjene števke)', moduleWidth: 'Širina modula', }, box: { diff --git a/src/locales/sr.ts b/src/locales/sr.ts index 8ebc8185e..e0c6e7c43 100644 --- a/src/locales/sr.ts +++ b/src/locales/sr.ts @@ -1354,6 +1354,7 @@ const sr = { printInterpretation: 'Čitljivo za čoveka', checkDigit: 'Kontrolna cifra', gs1Mode: 'GS1 режим', + autoMode: 'Automatski podskup (kompaktne cifre)', moduleWidth: 'Širina modula', }, box: { diff --git a/src/locales/sv.ts b/src/locales/sv.ts index c331d77bd..1fb2d5394 100644 --- a/src/locales/sv.ts +++ b/src/locales/sv.ts @@ -1354,6 +1354,7 @@ const sv = { printInterpretation: 'Läsbar för människa', checkDigit: 'Kontrollsiffra', gs1Mode: 'GS1-läge', + autoMode: 'Automatisk delmängd (kompakta siffror)', moduleWidth: 'Modulbredd', }, box: { diff --git a/src/locales/tr.ts b/src/locales/tr.ts index 4857e2f81..d170e4dae 100644 --- a/src/locales/tr.ts +++ b/src/locales/tr.ts @@ -1354,6 +1354,7 @@ const tr = { printInterpretation: 'İnsan tarafından okunabilir', checkDigit: 'Kontrol hanesi', gs1Mode: 'GS1 modu', + autoMode: 'Otomatik alt küme (sıkıştırılmış rakamlar)', moduleWidth: 'Modül genişliği', }, box: { diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts index be601ab51..09952663d 100644 --- a/src/locales/zh-hans.ts +++ b/src/locales/zh-hans.ts @@ -1354,6 +1354,7 @@ const zhHans = { printInterpretation: '人工可读', checkDigit: '校验位', gs1Mode: 'GS1 模式', + autoMode: '自动子集(数字紧凑)', moduleWidth: '模块宽度', }, box: { diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts index d0914ecc8..7825f2268 100644 --- a/src/locales/zh-hant.ts +++ b/src/locales/zh-hant.ts @@ -1354,6 +1354,7 @@ const zhHant = { printInterpretation: '人工可讀', checkDigit: '校驗碼', gs1Mode: 'GS1 模式', + autoMode: '自動子集(數字緊湊)', moduleWidth: '模組寬度', }, box: { diff --git a/src/registry/barcode1d.panel.tsx b/src/registry/barcode1d.panel.tsx index 43f75d184..2e7fb898e 100644 --- a/src/registry/barcode1d.panel.tsx +++ b/src/registry/barcode1d.panel.tsx @@ -31,6 +31,8 @@ export interface BarcodeLocale { placeholder?: string; /** GS1 mode toggle label; only needed by a `gs1Capable` symbology. */ gs1Mode?: string; + /** Automatic subset (^BC m=A) toggle label; only needed with `autoModeCapable`. */ + autoMode?: string; } export interface Barcode1DPanelConfig { @@ -50,6 +52,8 @@ export interface Barcode1DPanelConfig { /** Symbology offers a GS1 mode: renders the toggle + builder and swaps in the * GS1 charset while on. */ gs1Capable?: boolean; + /** Symbology offers the automatic subset mode (Code 128 m=A). */ + autoModeCapable?: boolean; } export function createBarcode1DPanel(config: Barcode1DPanelConfig): ObjectTypeUi { @@ -106,6 +110,15 @@ export function createBarcode1DPanel(config: Barcode1DPanelConfig): ObjectTypeUi /> )} + {config.autoModeCapable && !p.gs1 && ( + onChange({ mode: c ? 'A' : undefined })} + label={loc.autoMode ?? ''} + cmd={config.zplCommand} + /> + )} + Date: Fri, 30 Oct 2026 23:00:57 +0100 Subject: [PATCH 2/2] fix(code128): one ^BC m derivation, m=A packs the literal payload with its bound chips, and an unmodeled m is reported --- packages/core/src/lib/barcodeDims.ts | 20 +-- packages/core/src/lib/code128Auto.test.ts | 159 ++++++++++++++++-- packages/core/src/lib/code128Plan.ts | 8 + packages/core/src/lib/code128Subset.ts | 138 +++++++++------ packages/core/src/lib/gs1ModeDFns.ts | 8 +- packages/core/src/lib/markerResolve.ts | 14 +- packages/core/src/lib/preflight.ts | 3 +- packages/core/src/lib/zplParser/context.ts | 15 +- packages/core/src/lib/zplParser/flushField.ts | 11 +- .../src/lib/zplParser/handlers/barcodes.ts | 9 +- .../core/src/lib/zplParser/handlers/fields.ts | 3 +- packages/core/src/registry/barcode1d.ts | 50 +++--- packages/core/src/registry/code128.ts | 10 +- packages/core/src/registry/hriFormatters.ts | 7 +- packages/core/src/registry/index.ts | 10 +- packages/core/src/types/ObjectType.ts | 8 +- packages/core/src/types/ZplEmit.ts | 4 +- packages/mcp-server/src/boundary.ts | 2 +- src/components/Canvas/BarcodeObject.tsx | 6 +- src/lib/designFile.test.ts | 19 +++ src/lib/symbologySwitch.test.ts | 4 + src/locales/ar.ts | 2 +- src/locales/bg.ts | 2 +- src/locales/ca.ts | 2 +- src/locales/cs.ts | 2 +- src/locales/da.ts | 2 +- src/locales/de.ts | 2 +- src/locales/el.ts | 2 +- src/locales/en.ts | 2 +- src/locales/es.ts | 2 +- src/locales/et.ts | 2 +- src/locales/fa.ts | 2 +- src/locales/fi.ts | 2 +- src/locales/fr.ts | 2 +- src/locales/he.ts | 2 +- src/locales/hr.ts | 2 +- src/locales/hu.ts | 2 +- src/locales/it.ts | 2 +- src/locales/ja.ts | 2 +- src/locales/ko.ts | 2 +- src/locales/lt.ts | 2 +- src/locales/lv.ts | 2 +- src/locales/nl.ts | 2 +- src/locales/no.ts | 2 +- src/locales/pl.ts | 2 +- src/locales/pt.ts | 2 +- src/locales/ro.ts | 2 +- src/locales/sk.ts | 2 +- src/locales/sl.ts | 2 +- src/locales/sr.ts | 2 +- src/locales/sv.ts | 2 +- src/locales/tr.ts | 2 +- src/locales/zh-hans.ts | 2 +- src/locales/zh-hant.ts | 2 +- src/registry/barcode1d.panel.tsx | 4 +- src/registry/code128.panel.tsx | 2 +- src/registry/hriFormatters.test.ts | 8 + 57 files changed, 401 insertions(+), 187 deletions(-) diff --git a/packages/core/src/lib/barcodeDims.ts b/packages/core/src/lib/barcodeDims.ts index 385877fe2..55d048d9b 100644 --- a/packages/core/src/lib/barcodeDims.ts +++ b/packages/core/src/lib/barcodeDims.ts @@ -19,7 +19,8 @@ import { } from "./gs1"; import { planGs1Fd } from "./gs1Plan"; import { code128FdToSymbols, code128SymbolsToBwipRaw, planCode128AutoSymbols } from "./code128Subset"; -import { planCode128Fd } from "./code128Plan"; +import { code128Mode, planCode128Fd } from "./code128Plan"; +import { resolveControlMarkers } from "../types/controlKey"; import { isRectangular, dmVersionString, type DataMatrixProps } from "../registry/datamatrix"; import { MAXICODE_WIDTH_MM, MAXICODE_HEIGHT_MM } from "../registry/maxicode"; import { micropdf417ModeDims, micropdf417ModeFits } from "../registry/micropdf417"; @@ -318,26 +319,19 @@ export function buildBwipOptions( case "code128": { const p = obj.props; const scale = bwipScale1D(p.moduleWidth, renderScale, renderDpmm); + const mode = code128Mode(p); // GS1-128 renders from the emit plan; unparsed content is the raw // mode-D read (leading FNC1 plus literals, see the plan's parsefnc doc). - if (p.gs1) { + if (mode === "D") { const plan = planGs1Fd(p.content, "code128"); opts = plan.bwipParsefncText !== null ? { bcid: "code128", text: plan.bwipParsefncText, parsefnc: true, scale, height: 10 } : { bcid: "gs1-128", text: plan.bwipText || "(01)00000000000000", scale, height: 10 }; break; } - const text = p.content || "0"; - // Mode A: the firmware picks subsets itself (digit runs in C), so the - // canvas follows the same automatic plan instead of forcing Subset B. - const autoSymbols = p.mode === "A" ? planCode128AutoSymbols(text) : null; - if (autoSymbols) { - opts = { bcid, text: code128SymbolsToBwipRaw(autoSymbols), raw: true, scale, height: 10 }; - break; - } - // 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); + // m=A plans the literal payload, m=N the emitted ^FD. A byte no subset carries falls to bwip. + const text = mode === "A" ? resolveControlMarkers(p.content) || "0" : p.content || "0"; + const symbols = mode === "A" ? planCode128AutoSymbols(text) : code128FdToSymbols(planCode128Fd(text, "whole").fd); opts = symbols ? { bcid, text: code128SymbolsToBwipRaw(symbols), raw: true, scale, height: 10 } : { bcid, text, scale, height: 10 }; diff --git a/packages/core/src/lib/code128Auto.test.ts b/packages/core/src/lib/code128Auto.test.ts index da13184a3..4f3766e22 100644 --- a/packages/core/src/lib/code128Auto.test.ts +++ b/packages/core/src/lib/code128Auto.test.ts @@ -1,33 +1,43 @@ import { describe, expect, it } from "vitest"; import bwipjs from "bwip-js/generic"; import { planCode128AutoSymbols } from "./code128Subset"; -import { measureBarcodeFootprintDotsWith, type BwipEngine } from "./barcodeDims"; +import { measureBarcodeFootprintDotsWith, resolveForMeasure, type BwipEngine } from "./barcodeDims"; +import { importZplText } from "./zplImportService"; +import { generateBatchZpl, generateMultiPageZPL } from "./zplGenerator"; +import { dropPageOverlays } from "./pageOverlay"; +import { pageLabelConfig, type LabelObject, type Page } from "../types/Group"; +import { slotEncodingConflicts } from "./slotConflicts"; import { code128 } from "../registry/code128"; +import { aztec, type AztecProps } from "../registry/aztec"; +import { ctrlParityFor } from "../registry"; import { parseZPL } from "./zplParser"; import type { LeafObject } from "../registry"; import type { Code128Props } from "../registry/code128"; +const label = { widthMm: 70, heightMm: 40, dpmm: 8 }; + const engine = bwipjs as unknown as BwipEngine; +const START_A = 103; const START_B = 104; const START_C = 105; const CODE_B = 100; const CODE_C = 99; +const SHIFT = 98; -const leaf = (content: string, extra: Partial = {}): LeafObject => +const leaf = (content: string, extra: Partial = {}): LeafObject & { props: Code128Props } => ({ 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", ...extra, }, - }) as LeafObject; - -/** Start + data symbols + check at 11 modules each, stop at 13. */ -const modules = (dataSymbols: number) => 11 * (dataSymbols + 2) + 13; + }) as LeafObject & { props: Code128Props }; const firstProps = (zpl: string): Code128Props => (parseZPL(zpl, 8).pages[0]?.objects[0] as LeafObject & { props: Code128Props }).props; +const importedProps = (zpl: string): Code128Props => + (importZplText(zpl, 8).pages[0]?.objects[0] as LeafObject & { props: Code128Props }).props; describe("planCode128AutoSymbols (^BC mode A)", () => { it("packs an even digit string into Subset C from the start", () => { @@ -53,27 +63,36 @@ describe("planCode128AutoSymbols (^BC mode A)", () => { expect(planCode128AutoSymbols("AB12345")).toEqual( [START_B, 33, 34, 17, CODE_C, 23, 45], ); + expect(planCode128AutoSymbols("A12345B")).toEqual([START_B, 33, 17, CODE_C, 23, 45, CODE_B, 34]); expect(planCode128AutoSymbols("AB1234CD")).toEqual( [START_B, 33, 34, CODE_C, 12, 34, CODE_B, 35, 36], ); }); - it("returns null for control bytes so the plain plan takes over", () => { - expect(planCode128AutoSymbols("A\x09B")).toBeNull(); + it("takes a control byte through Subset A", () => { + expect(planCode128AutoSymbols("A\tB")).toEqual([START_A, 33, 73, 34]); + }); + + it("shifts from Subset B for a single control byte", () => { + expect(planCode128AutoSymbols("a\tb")).toEqual([START_B, 65, SHIFT, 73, 66]); + }); + + it("returns null for no data or a byte above DEL", () => { expect(planCode128AutoSymbols("")).toBeNull(); + expect(planCode128AutoSymbols("ä")).toBeNull(); }); }); describe("Code 128 mode A emit, import and canvas width", () => { it("emits m=A and omits it for the default mode", () => { - expect(code128.toZPL(leaf("0240018268623", { mode: "A" }) as never)) + expect(code128.toZPL(leaf("0240018268623", { mode: "A" }))) .toContain("^BCN,100,N,N,N,A^FD0240018268623^FS"); - expect(code128.toZPL(leaf("0240018268623") as never)) + expect(code128.toZPL(leaf("0240018268623"))) .toContain("^BCN,100,N,N,N^FD0240018268623^FS"); }); it("lets GS1 mode win over mode A", () => { - const zpl = code128.toZPL(leaf("0104012345678901", { mode: "A", gs1: true }) as never); + const zpl = code128.toZPL(leaf("0104012345678901", { mode: "A", gs1: true })); expect(zpl).toContain("^BCN,100,N,N,N,D"); expect(zpl).not.toContain(",A"); }); @@ -87,8 +106,124 @@ describe("Code 128 mode A emit, import and canvas width", () => { const text = "0240018268623"; const auto = measureBarcodeFootprintDotsWith(engine, leaf(text, { mode: "A" }), 8); const plain = measureBarcodeFootprintDotsWith(engine, leaf(text), 8); - expect(auto?.w).toBe(modules(planCode128AutoSymbols(text)!.length - 1) * 2); expect(auto?.w).toBe(246); expect(plain?.w).toBe(356); }); }); + +describe("one ^BC m for emit, canvas and parser", () => { + const widthOf = (content: string, extra: Partial = {}) => measureBarcodeFootprintDotsWith(engine, leaf(content, extra), 8)?.w; + + it("lets GS1 win in the canvas as it does in the emit", () => { + expect(widthOf("(01)04012345678901", { gs1: true })).toBe(268); + expect(widthOf("(01)04012345678901", { mode: "A", gs1: true })).toBe(widthOf("(01)04012345678901", { gs1: true })); + }); + + // ZD230 V89.21.46Z, ^BY2: m=A reads no Table-2 escape, every byte is literal and packed by Annex E. + it("reads the payload literally under m=A, as the printer does", () => { + expect(code128.toZPL(leaf(">;123", { mode: "A" }))).toContain("^BCN,100,N,N,N,A^FD>;123^FS"); + expect(widthOf(">;123", { mode: "A" })).toBe(180); + expect(widthOf("0240018268623", { mode: "A" })).toBe(246); + expect(widthOf(">812345678901", { mode: "A" })).toBe(246); + expect(widthOf("A>0B", { mode: "A" })).toBe(158); + expect(widthOf("AB>4CD", { mode: "A" })).toBe(202); + expect(widthOf(">:>4", { mode: "A" })).toBe(158); + expect(widthOf("12345", { mode: "A" })).toBe(158); + expect(widthOf("AB12345", { mode: "A" })).toBe(202); + }); + + it("packs AB12345 one symbol wider under m=N, as the printer does", () => { + expect(widthOf("AB12345")).toBe(224); + }); + + it("leaves a bound default literal under m=A, on emit and on import", () => { + const variables = [{ id: "v", name: "v", fnNumber: 1, defaultValue: "A>B" }]; + const zpl = generateMultiPageZPL(label, [{ objects: [leaf("X«v»Y", { mode: "A" })] }], variables); + expect(zpl).toContain("A>B"); + expect(zpl).not.toContain(">0"); + expect(importedProps("^XA^FO10,10^BY2^BCN,80,Y,N,N,A^FN1^FDA>0B^FS^XZ").mode).toBe("A"); + expect(importZplText("^XA^FO10,10^BY2^BCN,80,Y,N,N,A^FN1^FDA>0B^FS^XZ", 8).variables[0]?.defaultValue).toBe("A>0B"); + }); + + it("takes ^, ~ and a control byte through ^FH under m=A and through the invocation form under m=N", () => { + expect(code128.toZPL(leaf("A^B", { mode: "A" }))).toContain("^FH_^FDA_5EB^FS"); + expect(code128.toZPL(leaf("A~B", { mode: "A" }))).toContain("^FH_^FDA_7EB^FS"); + expect(code128.toZPL(leaf("A«ctrl:TAB»B", { mode: "A" }))).toContain("^FH_^FDA_09B^FS"); + expect(widthOf("A^B", { mode: "A" })).toBe(136); + expect(widthOf("A~B", { mode: "A" })).toBe(136); + expect(widthOf("A«ctrl:TAB»B", { mode: "A" })).toBe(136); + expect(code128.toZPL(leaf("A^B"))).toContain("^FDA> { + const variables = [{ id: "v", name: "v", fnNumber: 1, defaultValue: "A«ctrl:TAB»B" }]; + const bound = leaf("«v»", { mode: "A" }); + expect(ctrlParityFor(bound)).toBe("byteWhole"); + expect(code128.toZPL(bound, { label, variables })).toContain("^FN1^FH_^FDA_09B^FS"); + const batch = generateBatchZpl(pageLabelConfig(label, {}), [bound], variables, { headers: ["v"], rows: [["A«ctrl:TAB»B"]] }, { bindings: { v: "v" } }); + expect(batch).toContain("^FN1^FH_^FDA_09B^FS"); + expect((resolveForMeasure(bound as LabelObject, variables) as typeof bound).props.content).toBe("A\tB"); + expect(importZplText("^XA^FO10,10^BY2^BCN,80,Y,N,N,A^FN1^FH_^FDA_09B^FS^XZ", 8).variables[0]?.defaultValue).toBe("A«ctrl:TAB»B"); + }); + + it("keeps a bound chip literal on a 2D type, whose emit leaves it in the value", () => { + const variables = [{ id: "v", name: "v", fnNumber: 1, defaultValue: "A«ctrl:TAB»B" }]; + const bound = { id: "z", type: "aztec", x: 0, y: 0, rotation: 0, positionType: "FO", props: { ...aztec.defaultProps, content: "«v»" } } as LeafObject & { props: AztecProps }; + expect(ctrlParityFor(bound)).toBe("byte"); + expect(aztec.toZPL(bound, { label, variables })).toContain("^FN1^FDA«ctrl:TAB»B^FS"); + expect((resolveForMeasure(bound, variables) as { props: { content: string } }).props.content).toBe("A«ctrl:TAB»B"); + }); + + it("reports a slot an m=A field shares with a text field, which prints the chip as text", () => { + const variables = [{ id: "v", name: "v", fnNumber: 1, defaultValue: "A«ctrl:TAB»B" }]; + const text = { id: "t", type: "text", x: 0, y: 0, rotation: 0, positionType: "FO", props: { content: "«v»", font: "0", fontSize: 30 } } as unknown as LabelObject; + expect([...slotEncodingConflicts([leaf("«v»", { mode: "A" }), text], variables).fns]).toEqual([1]); + expect([...slotEncodingConflicts([leaf("«v»", { mode: "A" }), { ...leaf("«v»", { mode: "A" }), id: "c" }], variables).fns]).toEqual([]); + }); + + it("keeps m=A on a serial field", () => { + expect(code128.toZPL(leaf("123456", { mode: "A", serial: { increment: 1, zplMode: "SN" } }))).toContain("^BCN,100,N,N,N,A^"); + }); + + it("emits no mode D for a non-boolean gs1 flag from a hand-edited design", () => { + const zpl = code128.toZPL(leaf("(10)AB", { gs1: 1 as unknown as boolean })); + expect(zpl).toContain("^BCN,100,N,N,N^"); + expect(zpl).not.toContain(">8"); + }); + + it("resolves the chip for the bwip fallback under m=A as for the plan", () => { + expect(widthOf("ä«ctrl:TAB»", { mode: "A" })).toBe(widthOf("ä\t", { mode: "A" })); + }); + + it("maps m=D to gs1 and reports an m the design cannot hold", () => { + const d = firstProps("^XA^FO10,10^BY2^BCN,80,Y,N,N,D^FD>;0104012345678901^FS^XZ"); + expect([d.gs1, d.mode]).toEqual([true, undefined]); + expect(importedProps("^XA^FO10,10^BY2^BCN,80,Y,N,N, u^FD123^FS^XZ").mode).toBeUndefined(); + expect(importZplText("^XA^FO10,10^BY2^BCN,80,Y,N,N, u^FD123^FS^XZ", 8).report.partial).toEqual(["^BC"]); + expect(importZplText("^XA^FO10,10^BY2^BCN,80,Y,N,N,N^FD123^FS^XZ", 8).report.partial).toEqual([]); + }); + + it("imports a literal m=A payload as typed and re-emits it unchanged, and a bare ^BC the same way", () => { + expect(importedProps("^XA^FO10,10^BY2^BCN,80,Y,N,N,A^FD>;123^FS^XZ").content).toBe(">;123"); + const r = importZplText("^XA^FO10,10^BY2^BCN,80,Y,N,N,A^FD>;123^FS^XZ", 8); + expect(generateMultiPageZPL(label, dropPageOverlays(r.pages), r.variables)).toContain("^BCN,80,Y,N,N,A^FD>;123^FS"); + expect(importedProps("^XA^FO10,10^BY2^BCN,80,Y,N,N^FD>;123^FS^XZ").content).toBe(">;123"); + }); + + it("keeps m=A on a template field and in a stored format with its recalls", () => { + const fn = importZplText("^XA^FO10,10^BY2^BCN,80,Y,N,N,A^FN1^FS^XZ", 8); + expect(importedProps("^XA^FO10,10^BY2^BCN,80,Y,N,N,A^FN1^FS^XZ").mode).toBe("A"); + expect(generateMultiPageZPL(label, dropPageOverlays(fn.pages), fn.variables)).toContain("^BCN,80,Y,N,N,A^FN1"); + const NL = String.fromCharCode(10); + const r = importZplText(["^XA^DFR:A.ZPL^FS^FO10,10^BY2^BCN,80,Y,N,N,A^FN1^FS^XZ", "^XA^XFR:A.ZPL^FS^FN1^FDone^FS^XZ"].join(NL), 8); + const page = r.pages[0] as Page; + const batch = generateBatchZpl(pageLabelConfig(label, page), page.objects, r.variables, r.batch!.dataset, r.batch!.columnMapping); + expect(batch).toContain("^DFR:A.ZPL"); + expect(batch).toContain("^BCN,80,Y,N,N,A"); + }); + + it("regenerates an imported m=A once the overlay is gone", () => { + const r = importZplText("^XA^FO10,10^BY2^BCN,80,Y,N,N,A^FD0240018268623^FS^XZ", 8); + expect(generateMultiPageZPL(label, dropPageOverlays(r.pages), r.variables)).toContain("^BCN,80,Y,N,N,A"); + }); +}); diff --git a/packages/core/src/lib/code128Plan.ts b/packages/core/src/lib/code128Plan.ts index ed5e53b93..a832846da 100644 --- a/packages/core/src/lib/code128Plan.ts +++ b/packages/core/src/lib/code128Plan.ts @@ -5,6 +5,14 @@ import { } from "./code128Subset"; import { C0_RE, resolveControlMarkers } from "../types/controlKey"; +type Code128Mode = "N" | "A" | "D"; + +/** The ^BC m from the props: GS1 wins over m=A. The content never decides, since a template or serial field + * changes it per label. */ +export function code128Mode(p?: { gs1?: boolean; mode?: string }): Code128Mode { + return p?.gs1 === true ? "D" : p?.mode === "A" ? "A" : "N"; +} + /** How a payload reaches a plain-^BC ^FD: `whole` = entire field (chips * resolve), `template` = literal spans around markers, `templateValue` = * ^FN declaration (chips stay marker TEXT in `fd`, losses approximate on diff --git a/packages/core/src/lib/code128Subset.ts b/packages/core/src/lib/code128Subset.ts index 275749dda..79d36fb83 100644 --- a/packages/core/src/lib/code128Subset.ts +++ b/packages/core/src/lib/code128Subset.ts @@ -1,10 +1,11 @@ /** 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. The canvas reads the emitted ^FD + * At m=N the firmware drops ^FH-escaped C0 bytes from the symbol (ZD230-verified). + * Only the invocation form encodes them, and 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). */ + * preflight warns). At m=A the firmware reads the payload literally and packs + * it itself, so the canvas plans the bytes. */ import { mapLiteralSpans } from "./fnTemplate"; import { C0_OR_DEL_RE } from "../types/controlKey"; @@ -80,60 +81,97 @@ export function planCode128Symbols(text: string): number[] | null { return values; } -const CODE_C = 99; +/** Encoder position: Subset A, Subset B, or Subset C with the pairs of its stretch so far, capped at two. */ +type State = "A" | "B" | 0 | 1 | 2; +const STATES: readonly State[] = ["B", "A", 0, 1, 2]; +const C_AFTER_PAIR: Record<0 | 1 | 2, 1 | 2> = { 0: 1, 1: 2, 2: 2 }; +const START: Record<"A" | "B" | 0, number> = { A: START_A, B: START_B, 0: START_C }; +const CODE: Record<"A" | "B" | 0, number> = { A: CODE_A, B: CODE_B, 0: CODE_C }; + +/** The last two terms exist only to break symbol-count ties as ISO/IEC 15417 Annex E does, which opens C for four digits. */ +interface Cost { + symbols: number; + shortStretches: number; + pairs: number; +} + +const cheaper = (a: Cost, b: Cost): boolean => + a.symbols !== b.symbols ? a.symbols < b.symbols : a.shortStretches !== b.shortStretches ? a.shortStretches < b.shortStretches : a.pairs > b.pairs; + +const add = (step: Cost, tail: Cost): Cost => ({ symbols: step.symbols + tail.symbols, shortStretches: step.shortStretches + tail.shortStretches, pairs: step.pairs + tail.pairs }); + +/** Annex E's four-digit rule prefers a C stretch of two pairs, so a shorter one costs. */ +const shortLeave = (state: State): number => (state === 0 || state === 1 ? 1 : 0); -const isDigitAt = (text: string, i: number): boolean => /\d/.test(text.charAt(i)); +const letterValue = (subset: "A" | "B", code: number): number | null => (subset === "A" ? valueInA(code) : valueInB(code)); +const other = (subset: "A" | "B"): "A" | "B" => (subset === "A" ? "B" : "A"); -/** Length of the digit run starting at `from`. */ -function digitRun(text: string, from: number): number { - let n = 0; - while (isDigitAt(text, from + n)) n += 1; - return n; +interface Move { + emits: number[]; + consumed: number; + to: State; + cost: Cost; } -/** - * Symbol values (start code first, no check/stop) for `^BC` mode A, the - * firmware's automatic subset selection: four or more digits in a row pack - * into Subset C, two per symbol (ISO/IEC 15417 Annex E). Printable ASCII - * only; returns null for control bytes or anything outside SP..DEL so the - * caller falls back to the plain (Subset A/B) plan. - */ -export function planCode128AutoSymbols(text: string): number[] | null { - if (!text) return null; - const bValues: number[] = []; - for (let i = 0; i < text.length; i += 1) { - const value = valueInB(text.charCodeAt(i)); - if (value === null) return null; - bValues.push(value); +const move = (emits: number[], consumed: number, to: State, shortStretches = 0, pairs = 0): Move => ({ emits, consumed, to, cost: { symbols: emits.length, shortStretches, pairs } }); + +/** Moves at `i`. `cheaper` is strict, so this order decides every tie. */ +function movesAt(text: string, i: number, from: State): Move[] { + const code = text.charCodeAt(i); + const next = text.charCodeAt(i + 1); + const pair = /\d/.test(text.charAt(i)) && /\d/.test(text.charAt(i + 1)) ? (code - 48) * 10 + (next - 48) : undefined; + if (from === 0 || from === 1 || from === 2) { + if (pair !== undefined) return [move([pair], 2, C_AFTER_PAIR[from], 0, 1)]; + const leave = shortLeave(from); + return (["B", "A"] as const).flatMap((subset) => { + const value = letterValue(subset, code); + return value === null ? [] : [move([CODE[subset], value], 1, subset, leave)]; + }); } - const lead = digitRun(text, 0); - let inC = lead >= 4 || (lead === 2 && text.length === 2); - const values = [inC ? START_C : START_B]; - let i = 0; - while (i < text.length) { - if (inC) { - if (digitRun(text, i) >= 2) { - values.push(parseInt(text.slice(i, i + 2), 10)); - i += 2; - continue; - } - values.push(CODE_B); - inC = false; - continue; - } - const run = digitRun(text, i); - if (run >= 4) { - // Odd run: the first digit stays in B so the rest pairs up evenly. - if (run % 2 === 1) { - values.push(bValues[i] ?? 0); - i += 1; + const own = letterValue(from, code); + const away = letterValue(other(from), code); + const moves: Move[] = []; + if (own !== null) moves.push(move([own], 1, from)); + if (pair !== undefined) moves.push(move([CODE_C, pair], 2, 1, 0, 1)); + if (away !== null) moves.push(move([SHIFT, away], 1, from), move([CODE[other(from)], away], 1, other(from))); + return moves; +} + +/** Symbol values (start code first, no check and stop) for ^BC mode A: the cheapest plan over Subsets A, B and C, + * ties read as ISO/IEC 15417 Annex E does. Null for no data or a byte above DEL. Which digit of an odd run the + * firmware leaves in B is unmeasured. */ +export function planCode128AutoSymbols(text: string): number[] | null { + if (text === "" || [...text].some((ch) => ch.charCodeAt(0) > 127)) return null; + const key = (i: number, state: State): string => `${i}:${state}`; + // Filled from the end, so a move's tail is scored before the move. + const plan = new Map(); + const planned = (i: number, state: State): { cost: Cost; move: Move } => { + const entry = plan.get(key(i, state)); + if (entry === undefined) throw new Error("plan table filled from the end"); + return entry; + }; + const tail = (i: number, state: State): Cost => + i === text.length ? { symbols: 0, shortStretches: shortLeave(state), pairs: 0 } : planned(i, state).cost; + for (let i = text.length - 1; i >= 0; i -= 1) { + for (const state of STATES) { + let best: { cost: Cost; move: Move } | undefined; + for (const candidate of movesAt(text, i, state)) { + const cost = add(candidate.cost, tail(i + candidate.consumed, candidate.to)); + if (best === undefined || cheaper(cost, best.cost)) best = { cost, move: candidate }; } - values.push(CODE_C); - inC = true; - continue; + if (best !== undefined) plan.set(key(i, state), best); } - values.push(bValues[i] ?? 0); - i += 1; + } + const start = (state: "A" | "B" | 0): Cost => add({ symbols: 1, shortStretches: 0, pairs: 0 }, tail(0, state)); + // A full tie opens in C for a leading run, and in B before A, as Annex E does. + let state: State = cheaper(start("B"), start(0)) ? "B" : 0; + if (cheaper(start("A"), start(state))) state = "A"; + const values = [START[state]]; + for (let i = 0; i < text.length; ) { + const { move } = planned(i, state); + values.push(...move.emits); + state = move.to; + i += move.consumed; } return values; } diff --git a/packages/core/src/lib/gs1ModeDFns.ts b/packages/core/src/lib/gs1ModeDFns.ts index dd22319b2..7a54a3df0 100644 --- a/packages/core/src/lib/gs1ModeDFns.ts +++ b/packages/core/src/lib/gs1ModeDFns.ts @@ -1,11 +1,13 @@ import { exportableLeaves, type LabelObject } from "../types/Group"; +import { getEntry, usesPlainCode128Escape } from "../registry"; import { extractTemplateRefs } from "./fnTemplate"; import { getObjectStringContent } from "./variableBinding"; +import { code128Mode } from "./code128Plan"; import type { Variable } from "../types/Variable"; /** ^BR and GS1 DataMatrix encode FNC1 differently and must not match. */ export function isModeDLeaf(leaf: LabelObject): boolean { - return leaf.type === "code128" && (leaf.props as { gs1?: boolean }).gs1 === true; + return leaf.type === "code128" && code128Mode(leaf.props as { gs1?: boolean; mode?: string }) === "D"; } function sweepFnConsumers( @@ -20,11 +22,9 @@ function sweepFnConsumers( for (const leaf of exportableLeaves(objects)) { const c = getObjectStringContent(leaf); if (c === undefined) continue; - // Type literal mirrors the only fdPlainEscape carrier (registry/code128); - // a second carrier must be bucketed here too or its defaults emit raw. const target = isModeDLeaf(leaf) ? modeD - : leaf.type === "code128" ? plain128 : other; + : usesPlainCode128Escape(getEntry(leaf.type), (leaf as { props?: object }).props) ? plain128 : other; for (const name of extractTemplateRefs(c)) { const fn = fnByName.get(name); if (fn !== undefined) target.add(fn); diff --git a/packages/core/src/lib/markerResolve.ts b/packages/core/src/lib/markerResolve.ts index 785a9d164..fad30b04c 100644 --- a/packages/core/src/lib/markerResolve.ts +++ b/packages/core/src/lib/markerResolve.ts @@ -39,10 +39,10 @@ export function clockDatesThunk(clock?: ClockResolveCtx): () => ChannelDates { ); } -/** How the emitter treats control bytes, so preview resolution can mirror it. - * `byteOutsideTemplate`: the symbology's own escape form (Code 128) cannot - * wrap a ^FE template payload, so the firmware drops the bytes there. */ -export type CtrlParity = "literal" | "byte" | "byteOutsideTemplate"; +/** Where the emitter turns a chip into a byte, so preview resolution mirrors it. + * `byte` in the field's own content, `byteWhole` in a bound whole value too. + * `byteOutsideTemplate` as byteWhole, minus ^FE templates, where ^FH drops the bytes. */ +export type CtrlParity = "literal" | "byte" | "byteWhole" | "byteOutsideTemplate"; /** Shared control -> clock -> variable order: field-own display markers first * (a chip in substituted data stays literal, matching export; lone binds are @@ -63,9 +63,9 @@ export function resolveMarkerChain( if (getDates) { if (ctrl !== "literal" && hasControlMarkers(next)) next = resolveControlMarkers(next); // Emitter gate, evaluated pre-^FC there too: clock markers keep the lossy - // ^FH path; a lone bind is a whole ^FD and encodes its own escape form. - // Chips-only payloads stay raw (they print via the invocation plan). - loneBind = ctrl === "byteOutsideTemplate" && SINGLE_MARKER_RE.test(next); + // ^FH path; a lone bind is a whole ^FD and takes the field's whole-value + // transform. Chips-only payloads are bytes by now and match neither gate. + loneBind = (ctrl === "byteWhole" || ctrl === "byteOutsideTemplate") && SINGLE_MARKER_RE.test(next); templateFd = ctrl === "byteOutsideTemplate" && hasTemplateMarkers(next) && !loneBind; if (templateFd) next = code128EscapeLiterals(next); if (hasClockMarkers(next)) next = resolveClockMarkers(next, getDates()); diff --git a/packages/core/src/lib/preflight.ts b/packages/core/src/lib/preflight.ts index 9bce4b877..d6f78566f 100644 --- a/packages/core/src/lib/preflight.ts +++ b/packages/core/src/lib/preflight.ts @@ -243,8 +243,7 @@ export function markerValueFindings( } } }; - const plainLeaf = (leaf: LeafObject) => - !!getEntry(leaf.type)?.ctrlNeedsOwnEscape && !isModeDLeaf(leaf); + const plainLeaf = (leaf: LeafObject) => usesPlainCode128Escape(getEntry(leaf.type), leaf.props); const buckets = fnConsumerBuckets(leaves, deps.variables); slotValueWarnings( buckets.modeDShared, diff --git a/packages/core/src/lib/zplParser/context.ts b/packages/core/src/lib/zplParser/context.ts index 86bf709f9..c4638c6f3 100644 --- a/packages/core/src/lib/zplParser/context.ts +++ b/packages/core/src/lib/zplParser/context.ts @@ -278,10 +278,8 @@ export interface FieldState { bcMsiCheckMode: "C" | "D" | undefined; bcMsiHriCheck: boolean; bcRotation: ZplRotation; - /** ^BC m=D (UCC/EAN) → GS1-128; consumed by the code128 case in flushField. */ - bcGs1: boolean; - /** ^BC m=A (automatic subset selection); consumed by the code128 case in flushField. */ - bcAuto: boolean; + /** ^BC m as parsed: A maps to `mode`, D to `gs1`. */ + bcMode: "A" | "D" | undefined; bcCode49Mode: Code49Props["mode"]; /** ^B4 h as sent, or null when omitted; resolved at ^FS. */ bcCode49Mult: number | null; @@ -577,13 +575,15 @@ export function getPosType(field: FieldState): "FT" | "FO" { return field.positionIsFT ? "FT" : "FO"; } +/** m=D is UCC/EAN, the GS1-128 form. */ +export const bcIsGs1 = (field: Pick): boolean => field.bcMode === "D"; + /** Sticky per-field symbology state (GS1/^BX modes, the ^BQ header verdicts) * would act on any later field, so every flush path drops it. */ export function resetSymbologyModeFlags(field: FieldState): void { field.qrHeaderPartial = false; field.qrHeaderLossy = false; - field.bcGs1 = false; - field.bcAuto = false; + field.bcMode = undefined; field.dmEscape = undefined; field.dmQuality = 200; field.bcMsiCheckMode = undefined; @@ -746,8 +746,7 @@ export function freshFieldState(): FieldState { bcMsiCheckMode: undefined, bcMsiHriCheck: false, bcRotation: "N", - bcGs1: false, - bcAuto: false, + bcMode: undefined, bcCode49Mode: "A", bcCode49Mult: null, symRot: "N", diff --git a/packages/core/src/lib/zplParser/flushField.ts b/packages/core/src/lib/zplParser/flushField.ts index b36d62789..7df5fd0e8 100644 --- a/packages/core/src/lib/zplParser/flushField.ts +++ b/packages/core/src/lib/zplParser/flushField.ts @@ -55,6 +55,7 @@ import { notePartial, claimVariableName, type FnDefaultCandidate, type ParserState, + bcIsGs1, isTbField, truncatesAtHexControl, } from "./context"; @@ -108,6 +109,7 @@ function fnDefaultCandidate( leaf: LabelObject, varDefault: string, ): NonNullable { + // No m=A branch: the ctrlNeedsOwnEscape gate below already excludes every ^BC slot. if (!isGroup(leaf) && !isModeDLeaf(leaf) && !getEntry(leaf.type)?.ctrlNeedsOwnEscape) { const leafContent = getObjectStringContent(leaf); // Resolve the hook against THIS leaf: an optional encoder (plain ^BX) has @@ -257,10 +259,11 @@ export function createCloseField( // and never in GS1 mode: there a raw GS is the AI separator the GS1 // normalisation in the type cases below must still see. Text keeps raw // newlines (^FB); other types keep the bytes for the suspicious preflight. - const gs1Field = s.field.bcGs1 || (s.field.dmQuality === 200 && !!s.field.dmEscape); + const gs1Field = bcIsGs1(s.field) || (s.field.dmQuality === 200 && !!s.field.dmEscape); // ^FN defaults are excluded: their escape depends on slot exclusivity, // which only the page-close pass knows (normalizeCode128PlainDefaults). - if (!gs1Field && s.field.fieldType === "code128" && s.comment.fnNumber === null) { + // ^BC m=A reads its payload literally (ZD230), so only m=N has an escape form to adopt. + if (!gs1Field && s.field.bcMode !== "A" && s.field.fieldType === "code128" && s.comment.fnNumber === null) { content = adoptCode128Fd(content, s); } if (!gs1Field && s.field.fieldType && getEntry(s.field.fieldType)?.controlChars) { @@ -372,7 +375,7 @@ export function createCloseField( } case "code128": { // GS1-128 (^BC…,D): store the canonical compact form, not the parens. - const gs1 = s.field.bcGs1; + const gs1 = bcIsGs1(s.field); pushLeaf( makeObj( "code128", @@ -387,7 +390,7 @@ export function createCloseField( checkDigit: s.field.bcCheck, rotation: s.field.bcRotation, ...(gs1 ? { gs1: true } : {}), - ...(s.field.bcAuto ? { mode: "A" as const } : {}), + ...(s.field.bcMode === "A" ? { mode: "A" as const } : {}), } satisfies Code128Props, posType, comment, diff --git a/packages/core/src/lib/zplParser/handlers/barcodes.ts b/packages/core/src/lib/zplParser/handlers/barcodes.ts index cd3d018b9..9d6d1013a 100644 --- a/packages/core/src/lib/zplParser/handlers/barcodes.ts +++ b/packages/core/src/lib/zplParser/handlers/barcodes.ts @@ -59,12 +59,13 @@ export function createBarcodeHandlers(s: ParserState): Record { }, // ── 1D barcodes via mkBarcode(type, hIdx, iIdx, iDefault?, cIdx?) ───── - // ^BCo,h,f,g,e,m; m=D (UCC/EAN) is GS1-128. Assigned every time so a later - // plain ^BC clears it. + // ^BCo,h,f,g,e,m. Assigned every time so a later plain ^BC clears it. BC: (p, rest, cmd) => { mkBarcode("code128", 1, 2, "Y", 4)(p, rest, cmd); - s.field.bcGs1 = (p[5] ?? "").toUpperCase() === "D"; - s.field.bcAuto = (p[5] ?? "").toUpperCase() === "A"; + const m = strParam(p[5]); + s.field.bcMode = m === "A" || m === "D" ? m : undefined; + // Any other m, m=U included, has no model, so the import must report the loss. + if (!["", "N", "A", "D"].includes(m)) notePartial(s.result, "^BC"); }, B3: mkBarcode("code39", 2, 3, "Y", 1), // ^B3N,c,h,i,N BE: mkBarcode("ean13", 1, 2), // ^BEN,h,i,N diff --git a/packages/core/src/lib/zplParser/handlers/fields.ts b/packages/core/src/lib/zplParser/handlers/fields.ts index 09e5e1236..06d451557 100644 --- a/packages/core/src/lib/zplParser/handlers/fields.ts +++ b/packages/core/src/lib/zplParser/handlers/fields.ts @@ -9,6 +9,7 @@ import { notePartial, dropArmedSlot, fieldHasContent, getDefaultTextH, + bcIsGs1, getDefaultTextW, openDefaultText, resetFieldBlockDefaults, @@ -245,7 +246,7 @@ export function createFieldHandlers( // empty-seed serial still survives (the pendingFD===null guard). // GS1 fields drop the ^SN at flush, so the seed must not replace // their ^FD either. - const serialisable = !!getEntry(s.field.fieldType)?.serialisable && !s.field.bcGs1; + const serialisable = !!getEntry(s.field.fieldType)?.serialisable && !bcIsGs1(s.field); if (snStart && serialisable) { s.field.pendingFD = snStart; // The seed is the token's first parameter. A later parameter cannot decode, so the diff --git a/packages/core/src/registry/barcode1d.ts b/packages/core/src/registry/barcode1d.ts index bfbc7e3be..ec6a78794 100644 --- a/packages/core/src/registry/barcode1d.ts +++ b/packages/core/src/registry/barcode1d.ts @@ -8,7 +8,7 @@ import { commitBarcodeWidthHeightTransform, moduleWidthSpec } from './transformH import { hasTemplateMarkers } from '../lib/fnTemplate'; import { moduleTooSmallPreflight } from '../lib/barcodeScannability'; import { classifyField, isLoneMarker } from '../lib/variableField'; -import { planCode128Fd } from '../lib/code128Plan'; +import { code128Mode, planCode128Fd } from '../lib/code128Plan'; import { resolveControlMarkers } from '../types/controlKey'; import { parseGs1ToSegments, segmentsToZplFd } from '../lib/gs1'; import { planGs1Fd } from '../lib/gs1Plan'; @@ -17,7 +17,8 @@ import type { ContentSpec } from '../types/contentSpec'; import { ROTATION_SPEC, type ZplRotation } from './rotation'; export const MSI_CHECK_MODES = ['C', 'D'] as const; -export const CODE128_MODES = ['N', 'A'] as const; +/** Absent is N, and D comes from `gs1`. */ +export const CODE128_STORED_MODES = ['A'] as const; export interface Barcode1DProps { content: string; @@ -34,10 +35,8 @@ export interface Barcode1DProps { preSerialContent?: string; /** GS1-128 mode (`^BC…,D`). Only honoured by a `gs1Capable` symbology. */ gs1?: boolean; - /** Code 128 m-param: N (default, Subset B unless the ^FD carries - * invocation codes) or A (automatic: the firmware packs digit runs into - * Subset C). Only honoured by Code 128; `gs1` (m=D) wins over it. */ - mode?: (typeof CODE128_MODES)[number]; + /** ^BC m=A, the firmware's automatic subsets. Code 128 only, and `gs1` wins. */ + mode?: (typeof CODE128_STORED_MODES)[number]; /** MSI multi-check ^BM e beyond the boolean (C = 2x Mod 10, D = Mod 11 + * Mod 10); set by import, not the panel. */ msiCheckMode?: (typeof MSI_CHECK_MODES)[number]; @@ -75,6 +74,8 @@ export interface Barcode1DCoreConfig { /** Symbology offers a GS1 mode (Code 128 → GS1-128); `zplCommand` must emit * the mode flag when `props.gs1` is set. */ gs1Capable?: boolean; + /** Symbology offers ^BC m=A. Declared here so the panel reads one flag, the emit reads m from the props. */ + autoModeCapable?: boolean; /** See {@link ObjectTypeCore.contentSpec}; static half, GS1 mode is derived. */ contentSpec?: ContentSpec; /** See {@link ObjectTypeCore.controlChars}. */ @@ -102,7 +103,7 @@ export const BARCODE_1D_PROP_SPECS: PropSpecs = { serial: { type: 'object' }, preSerialContent: { type: 'string' }, gs1: { type: 'boolean' }, - mode: { type: 'string', values: CODE128_MODES }, + mode: { type: 'string', values: CODE128_STORED_MODES }, msiCheckMode: { type: 'string', values: MSI_CHECK_MODES }, msiHriCheck: { type: 'boolean' }, }; @@ -123,8 +124,9 @@ export function createBarcode1DCore(config: Barcode1DCoreConfig): ObjectTypeCore const fdTransformFor = config.fdContent || config.gs1Capable || config.fdPlainEscape ? (obj: LabelObjectBase & { props: Barcode1DProps }) => { - // GS1 mode brings its own escaping. - const escape = obj.props.gs1 ? undefined : config.fdPlainEscape; + const gs1 = config.gs1Capable === true && obj.props.gs1 === true; + // GS1 mode brings its own escaping, and m=A reads the payload literally. + const escape = code128Mode(obj.props) === 'N' ? config.fdPlainEscape : undefined; // A lone marker (single-bind) still transforms its default/CSV value; // only a real template is skipped. Ctrl-capable types resolve chips // first: chips-only is non-template there (whole-field ctrlEncode). @@ -134,21 +136,15 @@ export function createBarcode1DCore(config: Barcode1DCoreConfig): ObjectTypeCore ? resolveControlMarkers(obj.props.content) : obj.props.content, ) && !isLoneMarker(obj.props.content); - const base = isTemplate - ? undefined - : config.gs1Capable && obj.props.gs1 - ? gs1ModeDFd - : config.fdContent; - // Non-template payloads are whole ^FD values; the plan owns the - // invocation-vs-^FH decision (fdPlainEscape implies the code128 - // grammar, pinned by the carrier tripwire in gs1ModeDFns.test). - const ctrlWhole = - !isTemplate && escape && config.ctrlFdEncode && config.controlChars === true; - const plain = !base ? escape : !escape ? base : (s: string) => escape(base(s)); - if (!ctrlWhole || !plain) return plain; - // base stays in the chain (runs pre chip-resolve; unreachable until - // a carrier defines both fdContent and ctrlFdEncode). - return (s: string) => planCode128Fd(base ? base(s) : s, 'whole').fd; + const base = isTemplate ? undefined : gs1 ? gs1ModeDFd : config.fdContent; + // The plain grammar's own encoder plans the bytes, otherwise fdField's ^FH pass carries them. + const chips = !isTemplate && config.controlChars === true && !gs1; + const whole = chips && escape && config.ctrlFdEncode + ? (s: string) => planCode128Fd(s, 'whole').fd + : chips && escape + ? (s: string) => escape(resolveControlMarkers(s)) + : chips ? resolveControlMarkers : escape; + return !base ? whole : !whole ? base : (s: string) => whole(base(s)); } : undefined; @@ -179,6 +175,7 @@ export function createBarcode1DCore(config: Barcode1DCoreConfig): ObjectTypeCore gs1Capable: config.gs1Capable, controlChars: config.controlChars, ctrlNeedsOwnEscape: config.ctrlFdEncode !== undefined, + ctrlResolvesBound: config.controlChars === true, preflight: moduleTooSmallPreflight('moduleWidth'), @@ -222,7 +219,8 @@ export function createBarcode1DCore(config: Barcode1DCoreConfig): ObjectTypeCore // Shared-slot single-bind: one ^FN value serves several field types, so // the Code 128 escape would corrupt the co-consumers; emit raw like // mode-D does (preflight warns on the value instead). - if (!p.gs1 && config.fdPlainEscape && ctx?.rawFdFns && ctx.variables + const plainEscape = code128Mode(p) === 'N' ? config.fdPlainEscape : undefined; + if (plainEscape && ctx?.rawFdFns && ctx.variables && isLoneMarker(content)) { const cls = classifyField(content, ctx.variables); if (cls.kind === 'single' && ctx.rawFdFns.has(cls.variable.fnNumber)) { @@ -234,7 +232,7 @@ export function createBarcode1DCore(config: Barcode1DCoreConfig): ObjectTypeCore // literal `>` next to a token as an invocation and skip the `>0`. // Chips-only payloads stay raw for fdTransformFor's invocation plan; // marker BODIES stay raw too (names may carry >/^/~). - if (!p.gs1 && config.fdPlainEscape && !isLoneMarker(content) + if (plainEscape && !isLoneMarker(content) && hasTemplateMarkers(resolveControlMarkers(content))) { content = planCode128Fd(content, 'template').fd; fdTransformOnce = undefined; diff --git a/packages/core/src/registry/code128.ts b/packages/core/src/registry/code128.ts index 5b39bc399..589b27d64 100644 --- a/packages/core/src/registry/code128.ts +++ b/packages/core/src/registry/code128.ts @@ -1,5 +1,6 @@ import { createBarcode1DCore, type Barcode1DCoreConfig } from './barcode1d'; import { code128ControlFd, code128PlainFd } from '../lib/code128Subset'; +import { code128Mode } from '../lib/code128Plan'; import { formatCode128Hri } from './hriFormatters'; export type { Barcode1DProps as Code128Props } from './barcode1d'; @@ -9,6 +10,7 @@ export const code128CoreConfig: Barcode1DCoreConfig = { placeholderContent: '12345678', group: 'code-1d', gs1Capable: true, + autoModeCapable: true, // Code Set A covers the C0 range. controlChars: true, ctrlFdEncode: code128ControlFd, @@ -17,11 +19,9 @@ export const code128CoreConfig: Barcode1DCoreConfig = { zplCommand: (p) => { const interp = p.printInterpretation ? 'Y' : 'N'; const check = p.checkDigit ? 'Y' : 'N'; - // m=D (UCC/EAN) turns a plain Code 128 into GS1-128; m=A lets the firmware - // pick subsets (digit runs in C). Omit m for the default N so a plain field - // round-trips as the bare command. - const mode = p.gs1 ? ',D' : p.mode === 'A' ? ',A' : ''; - return `^BC${p.rotation},${p.height},${interp},${p.printInterpretationAbove ? 'Y' : 'N'},${check}${mode}`; + // m=N is the bare command, so a plain field round-trips byte-identically. + const mode = code128Mode(p); + return `^BC${p.rotation},${p.height},${interp},${p.printInterpretationAbove ? 'Y' : 'N'},${check}${mode === 'N' ? '' : `,${mode}`}`; }, }; diff --git a/packages/core/src/registry/hriFormatters.ts b/packages/core/src/registry/hriFormatters.ts index 79061d9bf..d6fcf7839 100644 --- a/packages/core/src/registry/hriFormatters.ts +++ b/packages/core/src/registry/hriFormatters.ts @@ -2,6 +2,7 @@ import { mod10CheckDigit } from "../lib/gs1"; import type { HriFormatProps } from "../types/ZplEmit"; import { code11CheckDigits, msiCheckDigits, eanCheckDigit, upceCheckDigit } from '../lib/barcodeCheckDigits'; import { code128FdToDisplayText, code128PlainFd } from '../lib/code128Subset'; +import { code128Mode } from '../lib/code128Plan'; import { C0_OR_DEL_RE } from '../types/controlKey'; /** @@ -115,7 +116,7 @@ export function stripHriControlBytes(text: string): string { /** Plain ^BC HRI shows the DECODED data, never invocation codes (spec p.98, * Fig 3/4). Runs on the emit-escaped form (planCode128Fd's plain fallback; * keep in lockstep), so typed text the escape protects is a fixed point. - * GS1 mode bypasses this (mode-D grammar). */ -export function formatCode128Hri(content: string): string { - return code128FdToDisplayText(code128PlainFd(content)); + * At m=A the line is the payload as typed. GS1 mode bypasses this (mode-D grammar). */ +export function formatCode128Hri(content: string, props?: HriFormatProps): string { + return code128Mode(props) === 'A' ? content : code128FdToDisplayText(code128PlainFd(content)); } diff --git a/packages/core/src/registry/index.ts b/packages/core/src/registry/index.ts index caf52e767..2cdf60714 100644 --- a/packages/core/src/registry/index.ts +++ b/packages/core/src/registry/index.ts @@ -1,3 +1,4 @@ +import { code128Mode } from '../lib/code128Plan'; import type { ObjectTypeCore } from '../types/ObjectType'; import { hasTemplateMarkers } from '../lib/fnTemplate'; import { gs1CarrierFor, planGs1Fd } from '../lib/gs1Plan'; @@ -140,13 +141,12 @@ export function isGs1Active( return entry.gs1Capable === true && (props as { gs1?: boolean }).gs1 === true; } -/** The leaf's ^FD grammar is the plain Code 128 escape (ctrl-capable type - * outside GS1 mode). */ +/** The leaf's ^FD grammar is the plain Code 128 escape: a ctrl-capable type at ^BC m=N. */ export function usesPlainCode128Escape( entry: Parameters[0], props: object | undefined, ): boolean { - return entry?.ctrlNeedsOwnEscape === true && !isGs1Active(entry, props); + return entry?.ctrlNeedsOwnEscape === true && code128Mode(props) === "N"; } /** Static GS1 content that ships verbatim: owns the gs1ContentUnparsed @@ -173,7 +173,9 @@ export function objectResolvesCtrl(obj: { type: string; props?: object }): boole * so a preview renders what the firmware prints (see {@link CtrlParity}). */ export function ctrlParityFor(obj: { type: string; props?: object }): CtrlParity { if (!objectResolvesCtrl(obj)) return "literal"; - return getEntry(obj.type)?.ctrlNeedsOwnEscape ? "byteOutsideTemplate" : "byte"; + const entry = getEntry(obj.type); + if (!entry?.ctrlResolvesBound) return "byte"; + return usesPlainCode128Escape(entry, obj.props) ? "byteOutsideTemplate" : "byteWhole"; } /** The object's effective content spec: its entry's rule resolved against props. */ diff --git a/packages/core/src/types/ObjectType.ts b/packages/core/src/types/ObjectType.ts index b4b4712df..048312259 100644 --- a/packages/core/src/types/ObjectType.ts +++ b/packages/core/src/types/ObjectType.ts @@ -83,10 +83,12 @@ export interface ObjectTypeCore

{ * False/absent for fixed-charset symbologies (EAN/UPC, Code 39) and text. * Each registry feeds the same constant to its emitter, so flag and chips never desync. */ controlChars?: boolean; - /** Control bytes reach this symbology's symbol only through its own escape - * form (Code 128 invocations), never through ^FH hex, which the firmware - * drops. Set by the emitters that carry such an encoder. */ + /** At ^BC m=N, control bytes reach this symbology's symbol only through its + * own escape form, since the firmware drops ^FH hex there. Set by the + * emitters that carry such an encoder. */ ctrlNeedsOwnEscape?: boolean; + /** The type's `fdTransform` resolves control chips in a whole bound value. */ + ctrlResolvesBound?: boolean; /** 1:1 aspect-locked free resize (ellipse with lockAspect). For * integer-module 2D symbologies use uniformScaleProp instead. */ uniformScale?: boolean | ((props: P) => boolean); diff --git a/packages/core/src/types/ZplEmit.ts b/packages/core/src/types/ZplEmit.ts index 35b999aa7..6a5e595a9 100644 --- a/packages/core/src/types/ZplEmit.ts +++ b/packages/core/src/types/ZplEmit.ts @@ -40,8 +40,10 @@ export interface ZplEmitContext { bareFnSlots?: ReadonlySet; } -/** The prop slice HRI formatters may read; every leaf props bag satisfies it. */ +/** The prop slice HRI formatters may read. */ export interface HriFormatProps { + /** Code 128 ^BC m: under A the interpretation line is the payload as typed. */ + mode?: 'A'; checkDigit?: boolean; msiCheckMode?: "C" | "D"; msiHriCheck?: boolean; diff --git a/packages/mcp-server/src/boundary.ts b/packages/mcp-server/src/boundary.ts index 41d39f1f8..daa5ddec4 100644 --- a/packages/mcp-server/src/boundary.ts +++ b/packages/mcp-server/src/boundary.ts @@ -285,7 +285,7 @@ export const PROP_SUMMARIES: Record> = { checkDigit: "boolean", rotation: "N | R | I | B", gs1: "boolean, GS1-128 mode", - mode: "N (default) | A: A lets the printer pick subsets, packing 4+ digit runs two per symbol (Subset C) for a much narrower numeric barcode; ignored when gs1 is on", + mode: "A, the printer's automatic subsets. Omit for the default, and gs1 wins over it.", }, image: { imageId: "string, id of an image the app holds; empty for a pure ZPL graphic", diff --git a/src/components/Canvas/BarcodeObject.tsx b/src/components/Canvas/BarcodeObject.tsx index ccf78e3a1..fefd0f538 100644 --- a/src/components/Canvas/BarcodeObject.tsx +++ b/src/components/Canvas/BarcodeObject.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from "react"; import { useNodeDraggable } from "./hooks/useNodeDraggable"; import { Image as KImage, Group, Rect, Shape, Text } from "react-konva"; import { HriOverlay } from "./hriOverlayNode"; -import { BARCODE_1D_TYPES, ObjectRegistry, objectResolvesCtrl } from "@zplab/core/registry"; +import { BARCODE_1D_TYPES, ObjectRegistry, getEntry, isGs1Active, objectResolvesCtrl } from "@zplab/core/registry"; import { dotsToPx, mmToDots, pxToDots } from "@zplab/core/lib/coordinates"; import { barcodeFtAnchorOffset, qrPrintsAsGraphic, rightAnchorShiftDots } from "@zplab/core/lib/objectBounds"; import { qrByHeight } from "@zplab/core/registry/qrcode"; @@ -132,7 +132,7 @@ export function BarcodeObject({ (obj.props as { moduleWidth?: number }).moduleWidth ?? 2; // GS1-128 shows the parenthesized element string as HRI (firmware prints the // parens it strips for encoding). - const gs1Hri = !!(obj.props as { gs1?: boolean }).gs1; + const gs1Hri = isGs1Active(getEntry(obj.type), obj.props); const contentRaw = getObjectStringContent(obj) ?? ""; // A blank (unconfigured) field renders the symbology's sample (GS1 sample // in GS1 mode): bwip still gets encodable content, so the object keeps its @@ -298,7 +298,7 @@ export function BarcodeObject({ // plain escape the code128 formatter decodes, and prints the element // string as-is. const displayText = stripHriControlBytes( - // Weak-type cast: non-barcode props share none of the optional keys. + // Cast to the HRI slice: the formatter reads only its own keys. (gs1Hri ? undefined : hri?.formatHri?.(rawContent, obj.props as HriFormatProps)) ?? rawContent, ); const isTextAbove = resolveHriAbove(obj); diff --git a/src/lib/designFile.test.ts b/src/lib/designFile.test.ts index cbaa554f2..8dc974669 100644 --- a/src/lib/designFile.test.ts +++ b/src/lib/designFile.test.ts @@ -801,3 +801,22 @@ describe('parseDesignFile', () => { expect(result.value.columnMapping).toBeNull(); }); }); + +describe("Code 128 mode", () => { + const leaf = (mode: string): LabelObject => + ({ + id: "b", type: "code128", x: 0, y: 0, rotation: 0, positionType: "FO", + props: { content: "1234", height: 100, moduleWidth: 2, printInterpretation: false, printInterpretationAbove: false, checkDigit: false, rotation: "N", mode }, + }) as unknown as LabelObject; + const modeOf = (mode: string) => { + const loaded = parseDesignFile(serializeDesign({ widthMm: 50, heightMm: 30, dpmm: 8 }, [{ objects: [leaf(mode)] }], [])); + if (!loaded.ok) throw new Error(loaded.error); + return (loaded.value.pages[0]?.objects[0] as { props: { mode?: string } }).props.mode; + }; + + it("keeps A and drops N and D, which the design stores as the default and as gs1", () => { + expect(modeOf("A")).toBe("A"); + expect(modeOf("N")).toBeUndefined(); + expect(modeOf("D")).toBeUndefined(); + }); +}); diff --git a/src/lib/symbologySwitch.test.ts b/src/lib/symbologySwitch.test.ts index 450648654..792cb58c9 100644 --- a/src/lib/symbologySwitch.test.ts +++ b/src/lib/symbologySwitch.test.ts @@ -176,4 +176,8 @@ describe('convertSymbologyMapper', () => { const text = barcode('text', { content: 'hi' }); expect(convertSymbologyMapper('qrcode' as never)(text)).toBe(text); }); + + it("drops the Code 128 mode on a symbology switch", () => { + expect((convertSymbologyMapper("code39")(barcode("code128", { mode: "A" })) as { props: object }).props).not.toHaveProperty("mode"); + }); }); diff --git a/src/locales/ar.ts b/src/locales/ar.ts index cb82e0d32..aacf8bdfc 100644 --- a/src/locales/ar.ts +++ b/src/locales/ar.ts @@ -1354,7 +1354,7 @@ const ar = { printInterpretation: 'قابل للقراءة البشرية', checkDigit: 'رقم التحقق', gs1Mode: 'وضع GS1', - autoMode: 'مجموعة فرعية تلقائية (أرقام مضغوطة)', + autoMode: 'مجموعة فرعية تلقائية', moduleWidth: 'عرض الوحدة', }, box: { diff --git a/src/locales/bg.ts b/src/locales/bg.ts index 1b84f1782..b4d62f376 100644 --- a/src/locales/bg.ts +++ b/src/locales/bg.ts @@ -1354,7 +1354,7 @@ const bg = { printInterpretation: 'Четливо от хора', checkDigit: 'Контролна цифра', gs1Mode: 'Режим GS1', - autoMode: 'Автоматичен набор (компактни цифри)', + autoMode: 'Автоматично подмножество', moduleWidth: 'Ширина на модула', }, box: { diff --git a/src/locales/ca.ts b/src/locales/ca.ts index 1cc171b7c..8613f7cf9 100644 --- a/src/locales/ca.ts +++ b/src/locales/ca.ts @@ -1410,7 +1410,7 @@ const ca = { "printInterpretation": "Text llegible", "checkDigit": "Dígit de control", "gs1Mode": "Mode GS1", - "autoMode": "Subconjunt automàtic (dígits compactes)", + "autoMode": "Subconjunt automàtic", "moduleWidth": "Amplada del mòdul" }, "box": { diff --git a/src/locales/cs.ts b/src/locales/cs.ts index b2aa569a5..c2ba9f376 100644 --- a/src/locales/cs.ts +++ b/src/locales/cs.ts @@ -1354,7 +1354,7 @@ const cs = { printInterpretation: 'Čitelný pro člověka', checkDigit: 'Kontrolní číslice', gs1Mode: 'Režim GS1', - autoMode: 'Automatická sada (kompaktní číslice)', + autoMode: 'Automatická podmnožina', moduleWidth: 'Šířka modulu', }, box: { diff --git a/src/locales/da.ts b/src/locales/da.ts index d2e307761..dca7f8982 100644 --- a/src/locales/da.ts +++ b/src/locales/da.ts @@ -1354,7 +1354,7 @@ const da = { printInterpretation: 'Læsbar for mennesker', checkDigit: 'Kontrolciffer', gs1Mode: 'GS1-tilstand', - autoMode: 'Automatisk delsæt (kompakte cifre)', + autoMode: 'Automatisk delsæt', moduleWidth: 'Modulbredde', }, box: { diff --git a/src/locales/de.ts b/src/locales/de.ts index a33a2a864..1dc4c150b 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1419,7 +1419,7 @@ const de = { printInterpretation: 'Klartext', checkDigit: 'Prüfziffer', gs1Mode: 'GS1-Modus', - autoMode: 'Auto-Subset (Ziffern kompakt)', + autoMode: 'Automatisches Subset', moduleWidth: 'Modulbreite', }, box: { diff --git a/src/locales/el.ts b/src/locales/el.ts index 9d02a8fd3..d700f8533 100644 --- a/src/locales/el.ts +++ b/src/locales/el.ts @@ -1354,7 +1354,7 @@ const el = { printInterpretation: 'Αναγνώσιμο από άνθρωπο', checkDigit: 'Ψηφίο ελέγχου', gs1Mode: 'Λειτουργία GS1', - autoMode: 'Αυτόματο υποσύνολο (συμπαγή ψηφία)', + autoMode: 'Αυτόματο υποσύνολο', moduleWidth: 'Πλάτος module', }, box: { diff --git a/src/locales/en.ts b/src/locales/en.ts index baac39840..0fbe16332 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1419,7 +1419,7 @@ const en = { printInterpretation: 'Human readable', checkDigit: 'Check digit', gs1Mode: 'GS1 mode', - autoMode: 'Auto subset (compact digits)', + autoMode: 'Automatic subset', moduleWidth: 'Module width', }, box: { diff --git a/src/locales/es.ts b/src/locales/es.ts index 6de1fe88f..bb6e0c690 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -1354,7 +1354,7 @@ const es = { printInterpretation: 'Legible por humanos', checkDigit: 'Dígito de control', gs1Mode: 'Modo GS1', - autoMode: 'Subconjunto automático (dígitos compactos)', + autoMode: 'Subconjunto automático', moduleWidth: 'Ancho de módulo', }, box: { diff --git a/src/locales/et.ts b/src/locales/et.ts index ae3997329..f823eb492 100644 --- a/src/locales/et.ts +++ b/src/locales/et.ts @@ -1354,7 +1354,7 @@ const et = { printInterpretation: 'Inimesele loetav', checkDigit: 'Kontrollnumber', gs1Mode: 'GS1-režiim', - autoMode: 'Automaatne alamhulk (kompaktsed numbrid)', + autoMode: 'Automaatne alamhulk', moduleWidth: 'Mooduli laius', }, box: { diff --git a/src/locales/fa.ts b/src/locales/fa.ts index 3ad9f506d..5d93c8965 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -1354,7 +1354,7 @@ const fa = { printInterpretation: 'قابل خواندن توسط انسان', checkDigit: 'رقم کنترل', gs1Mode: 'حالت GS1', - autoMode: 'زیرمجموعه خودکار (ارقام فشرده)', + autoMode: 'زیرمجموعه خودکار', moduleWidth: 'عرض ماژول', }, box: { diff --git a/src/locales/fi.ts b/src/locales/fi.ts index 50aee77fe..8123a5af5 100644 --- a/src/locales/fi.ts +++ b/src/locales/fi.ts @@ -1354,7 +1354,7 @@ const fi = { printInterpretation: 'Ihmisluettava', checkDigit: 'Tarkistusnumero', gs1Mode: 'GS1-tila', - autoMode: 'Automaattinen osajoukko (tiiviit numerot)', + autoMode: 'Automaattinen osajoukko', moduleWidth: 'Moduulin leveys', }, box: { diff --git a/src/locales/fr.ts b/src/locales/fr.ts index b0b815d58..afa6a7ef5 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -1354,7 +1354,7 @@ const fr = { printInterpretation: 'Lisible par l\'homme', checkDigit: 'Chiffre de contrôle', gs1Mode: 'Mode GS1', - autoMode: 'Sous-ensemble auto (chiffres compacts)', + autoMode: 'Sous-ensemble automatique', moduleWidth: 'Largeur de module', }, box: { diff --git a/src/locales/he.ts b/src/locales/he.ts index d8886c153..a5e75f2f5 100644 --- a/src/locales/he.ts +++ b/src/locales/he.ts @@ -1354,7 +1354,7 @@ const he = { printInterpretation: 'קריא לאדם', checkDigit: 'ספרת ביקורת', gs1Mode: 'מצב GS1', - autoMode: 'תת-קבוצה אוטומטית (ספרות דחוסות)', + autoMode: 'תת-קבוצה אוטומטית', moduleWidth: 'רוחב מודול', }, box: { diff --git a/src/locales/hr.ts b/src/locales/hr.ts index edb23f105..06c0fd66d 100644 --- a/src/locales/hr.ts +++ b/src/locales/hr.ts @@ -1354,7 +1354,7 @@ const hr = { printInterpretation: 'Čitljivo za čovjeka', checkDigit: 'Kontrolna znamenka', gs1Mode: 'GS1 način', - autoMode: 'Automatski podskup (kompaktne znamenke)', + autoMode: 'Automatski podskup', moduleWidth: 'Širina modula', }, box: { diff --git a/src/locales/hu.ts b/src/locales/hu.ts index 5acece513..006ff252b 100644 --- a/src/locales/hu.ts +++ b/src/locales/hu.ts @@ -1354,7 +1354,7 @@ const hu = { printInterpretation: 'Ember által olvasható', checkDigit: 'Ellenőrzőjegy', gs1Mode: 'GS1 mód', - autoMode: 'Automatikus készlet (tömör számjegyek)', + autoMode: 'Automatikus részhalmaz', moduleWidth: 'Modulszélesség', }, box: { diff --git a/src/locales/it.ts b/src/locales/it.ts index d07b8041f..7365ffaea 100644 --- a/src/locales/it.ts +++ b/src/locales/it.ts @@ -1354,7 +1354,7 @@ const it = { printInterpretation: "Leggibile dall'uomo", checkDigit: 'Cifra di controllo', gs1Mode: 'Modalità GS1', - autoMode: 'Sottoinsieme automatico (cifre compatte)', + autoMode: 'Sottoinsieme automatico', moduleWidth: 'Larghezza modulo', }, box: { diff --git a/src/locales/ja.ts b/src/locales/ja.ts index 5ad15634a..0d7abd9a9 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -1354,7 +1354,7 @@ const ja = { printInterpretation: '人間可読', checkDigit: 'チェックデジット', gs1Mode: 'GS1モード', - autoMode: '自動サブセット(数字を圧縮)', + autoMode: '自動サブセット', moduleWidth: 'モジュール幅', }, box: { diff --git a/src/locales/ko.ts b/src/locales/ko.ts index 2592f0a3d..d7a09669c 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -1354,7 +1354,7 @@ const ko = { printInterpretation: '사람이 읽을 수 있는', checkDigit: '체크 숫자', gs1Mode: 'GS1 모드', - autoMode: '자동 서브셋 (숫자 압축)', + autoMode: '자동 서브셋', moduleWidth: '모듈 너비', }, box: { diff --git a/src/locales/lt.ts b/src/locales/lt.ts index 864f5b61a..1c05a9895 100644 --- a/src/locales/lt.ts +++ b/src/locales/lt.ts @@ -1354,7 +1354,7 @@ const lt = { printInterpretation: 'Žmogui skaitomas', checkDigit: 'Tikrinimo skaitmuo', gs1Mode: 'GS1 režimas', - autoMode: 'Automatinis poaibis (kompaktiški skaitmenys)', + autoMode: 'Automatinis poaibis', moduleWidth: 'Modulio plotis', }, box: { diff --git a/src/locales/lv.ts b/src/locales/lv.ts index c1d99109c..f0ee1ecf0 100644 --- a/src/locales/lv.ts +++ b/src/locales/lv.ts @@ -1354,7 +1354,7 @@ const lv = { printInterpretation: 'Cilvēkam lasāms', checkDigit: 'Pārbaudes cipars', gs1Mode: 'GS1 režīms', - autoMode: 'Automātiska apakškopa (kompakti cipari)', + autoMode: 'Automātiska apakškopa', moduleWidth: 'Moduļa platums', }, box: { diff --git a/src/locales/nl.ts b/src/locales/nl.ts index be2052390..5814e0f9b 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -1354,7 +1354,7 @@ const nl = { printInterpretation: 'Leesbaar voor mensen', checkDigit: 'Controlegetal', gs1Mode: 'GS1-modus', - autoMode: 'Automatische subset (compacte cijfers)', + autoMode: 'Automatische subset', moduleWidth: 'Modulebreedte', }, box: { diff --git a/src/locales/no.ts b/src/locales/no.ts index b4f682ac1..ad976ad3f 100644 --- a/src/locales/no.ts +++ b/src/locales/no.ts @@ -1354,7 +1354,7 @@ const no = { printInterpretation: 'Lesbar for mennesker', checkDigit: 'Kontrollsiffer', gs1Mode: 'GS1-modus', - autoMode: 'Automatisk delsett (kompakte sifre)', + autoMode: 'Automatisk delsett', moduleWidth: 'Modulbredde', }, box: { diff --git a/src/locales/pl.ts b/src/locales/pl.ts index 9764c68cf..8f17cf46f 100644 --- a/src/locales/pl.ts +++ b/src/locales/pl.ts @@ -1354,7 +1354,7 @@ const pl = { printInterpretation: 'Czytelny dla człowieka', checkDigit: 'Cyfra kontrolna', gs1Mode: 'Tryb GS1', - autoMode: 'Automatyczny podzbiór (zwarte cyfry)', + autoMode: 'Automatyczny podzbiór', moduleWidth: 'Szerokość modułu', }, box: { diff --git a/src/locales/pt.ts b/src/locales/pt.ts index d8cb33311..906f36c9a 100644 --- a/src/locales/pt.ts +++ b/src/locales/pt.ts @@ -1354,7 +1354,7 @@ const pt = { printInterpretation: 'Legível por humanos', checkDigit: 'Dígito de verificação', gs1Mode: 'Modo GS1', - autoMode: 'Subconjunto automático (dígitos compactos)', + autoMode: 'Subconjunto automático', moduleWidth: 'Largura do módulo', }, box: { diff --git a/src/locales/ro.ts b/src/locales/ro.ts index 5bbc1c756..dacc4d725 100644 --- a/src/locales/ro.ts +++ b/src/locales/ro.ts @@ -1354,7 +1354,7 @@ const ro = { printInterpretation: 'Lizibil de om', checkDigit: 'Cifră de control', gs1Mode: 'Mod GS1', - autoMode: 'Subset automat (cifre compacte)', + autoMode: 'Subset automat', moduleWidth: 'Lățime modul', }, box: { diff --git a/src/locales/sk.ts b/src/locales/sk.ts index 2c0e594ff..c8075f371 100644 --- a/src/locales/sk.ts +++ b/src/locales/sk.ts @@ -1354,7 +1354,7 @@ const sk = { printInterpretation: 'Čitateľný pre človeka', checkDigit: 'Kontrolná číslica', gs1Mode: 'Režim GS1', - autoMode: 'Automatická sada (kompaktné číslice)', + autoMode: 'Automatická podmnožina', moduleWidth: 'Šírka modulu', }, box: { diff --git a/src/locales/sl.ts b/src/locales/sl.ts index 068a250e7..a5c056345 100644 --- a/src/locales/sl.ts +++ b/src/locales/sl.ts @@ -1354,7 +1354,7 @@ const sl = { printInterpretation: 'Berljivo za človeka', checkDigit: 'Kontrolna številka', gs1Mode: 'Način GS1', - autoMode: 'Samodejna podmnožica (strnjene števke)', + autoMode: 'Samodejna podmnožica', moduleWidth: 'Širina modula', }, box: { diff --git a/src/locales/sr.ts b/src/locales/sr.ts index e0c6e7c43..3fb052e4a 100644 --- a/src/locales/sr.ts +++ b/src/locales/sr.ts @@ -1354,7 +1354,7 @@ const sr = { printInterpretation: 'Čitljivo za čoveka', checkDigit: 'Kontrolna cifra', gs1Mode: 'GS1 режим', - autoMode: 'Automatski podskup (kompaktne cifre)', + autoMode: 'Automatski podskup', moduleWidth: 'Širina modula', }, box: { diff --git a/src/locales/sv.ts b/src/locales/sv.ts index 1fb2d5394..83a974d85 100644 --- a/src/locales/sv.ts +++ b/src/locales/sv.ts @@ -1354,7 +1354,7 @@ const sv = { printInterpretation: 'Läsbar för människa', checkDigit: 'Kontrollsiffra', gs1Mode: 'GS1-läge', - autoMode: 'Automatisk delmängd (kompakta siffror)', + autoMode: 'Automatisk delmängd', moduleWidth: 'Modulbredd', }, box: { diff --git a/src/locales/tr.ts b/src/locales/tr.ts index d170e4dae..b25637f59 100644 --- a/src/locales/tr.ts +++ b/src/locales/tr.ts @@ -1354,7 +1354,7 @@ const tr = { printInterpretation: 'İnsan tarafından okunabilir', checkDigit: 'Kontrol hanesi', gs1Mode: 'GS1 modu', - autoMode: 'Otomatik alt küme (sıkıştırılmış rakamlar)', + autoMode: 'Otomatik alt küme', moduleWidth: 'Modül genişliği', }, box: { diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts index 09952663d..5568bd6bc 100644 --- a/src/locales/zh-hans.ts +++ b/src/locales/zh-hans.ts @@ -1354,7 +1354,7 @@ const zhHans = { printInterpretation: '人工可读', checkDigit: '校验位', gs1Mode: 'GS1 模式', - autoMode: '自动子集(数字紧凑)', + autoMode: '自动子集', moduleWidth: '模块宽度', }, box: { diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts index 7825f2268..c11ec4132 100644 --- a/src/locales/zh-hant.ts +++ b/src/locales/zh-hant.ts @@ -1354,7 +1354,7 @@ const zhHant = { printInterpretation: '人工可讀', checkDigit: '校驗碼', gs1Mode: 'GS1 模式', - autoMode: '自動子集(數字緊湊)', + autoMode: '自動子集', moduleWidth: '模組寬度', }, box: { diff --git a/src/registry/barcode1d.panel.tsx b/src/registry/barcode1d.panel.tsx index 2e7fb898e..271528631 100644 --- a/src/registry/barcode1d.panel.tsx +++ b/src/registry/barcode1d.panel.tsx @@ -31,7 +31,7 @@ export interface BarcodeLocale { placeholder?: string; /** GS1 mode toggle label; only needed by a `gs1Capable` symbology. */ gs1Mode?: string; - /** Automatic subset (^BC m=A) toggle label; only needed with `autoModeCapable`. */ + /** Toggle label for ^BC m=A. */ autoMode?: string; } @@ -52,7 +52,7 @@ export interface Barcode1DPanelConfig { /** Symbology offers a GS1 mode: renders the toggle + builder and swaps in the * GS1 charset while on. */ gs1Capable?: boolean; - /** Symbology offers the automatic subset mode (Code 128 m=A). */ + /** Symbology offers ^BC m=A: renders the toggle, hidden while GS1 is on. */ autoModeCapable?: boolean; } diff --git a/src/registry/code128.panel.tsx b/src/registry/code128.panel.tsx index 6ed9dab42..bddff2291 100644 --- a/src/registry/code128.panel.tsx +++ b/src/registry/code128.panel.tsx @@ -8,5 +8,5 @@ export const code128Panel = createBarcode1DPanel({ hriAboveConfigurable: true, // Single-source the capability from the core config so emit + UI never skew. gs1Capable: code128CoreConfig.gs1Capable, - autoModeCapable: true, + autoModeCapable: code128CoreConfig.autoModeCapable, }); diff --git a/src/registry/hriFormatters.test.ts b/src/registry/hriFormatters.test.ts index 9d3171f7c..a7058fc2b 100644 --- a/src/registry/hriFormatters.test.ts +++ b/src/registry/hriFormatters.test.ts @@ -8,6 +8,7 @@ import { formatCode11Hri, formatCode39Hri, formatInterleaved2of5Hri, formatMsiHri, formatLogmarsHri, formatUpcEanExtensionHri, + formatCode128Hri, } from '@zplab/core/registry/hriFormatters'; describe('HRI formatters', () => { @@ -135,4 +136,11 @@ describe('HRI formatters', () => { expect(formatUpcEanExtensionHri('5-1-9-9-9')).toBe('51999'); }); }); + + describe('formatCode128Hri', () => { + it('prints the Table-2 read under m=N and the payload as typed under m=A', () => { + expect(formatCode128Hri('>;123')).toBe('12'); + expect(formatCode128Hri('>;123', { mode: 'A' })).toBe('>;123'); + }); + }); });