Skip to content

Commit 6aa45ad

Browse files
authored
Merge pull request #102 from u8array/feat/maxicode-bv
feat(registry): add Maxicode (^BV) 2D barcode support
2 parents dd79a64 + d1bda98 commit 6aa45ad

42 files changed

Lines changed: 597 additions & 19 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎src/components/Canvas/BarcodeObject.tsx‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useColorScheme } from "../../lib/useColorScheme";
88
import { selectionHandlers, type KonvaObjectProps } from "./konvaObjectProps";
99
import {
1010
buildBwipOptions,
11+
cleanBwipError,
1112
getDisplaySize,
1213
get1DBwipScale,
1314
getEanUpcLayout,
@@ -74,11 +75,7 @@ export function BarcodeObject({
7475
bwipjs.toCanvas(canvas, opts as unknown as Parameters<typeof bwipjs.toCanvas>[1]);
7576
barcodeCanvas = canvas;
7677
} catch (e) {
77-
const raw = e instanceof Error ? e.message : String(e);
78-
// bwip-js prefixes its errors like `bwip-js: bwipp.code39badCharacter: ...`
79-
// Strip the library/identifier head so the placeholder shows only the
80-
// human-readable reason.
81-
errorMsg = raw.replace(/^bwip-js:\s*/i, '').replace(/^bwipp\.[^:]+:\s*/i, '');
78+
errorMsg = cleanBwipError(e);
8279
}
8380
}
8481

‎src/components/Canvas/KonvaObject.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ const BARCODE_TYPES = new Set([
229229
"planet",
230230
"postal",
231231
"aztec",
232+
"maxicode",
232233
"micropdf417",
233234
"codablock",
234235
"upcEanExtension",

‎src/components/Canvas/bwipConstants.ts‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,27 @@ export const PLESSEY_BWIP_TO_ZEBRA_WIDTH_RATIO = 49 / 82;
5858
* with the same module count but at different transition positions). UI
5959
* surfaces a hint when one is selected so users know the layout is correct
6060
* even when the visual rendering is approximate. */
61-
export const BWIP_VISUAL_APPROX_TYPES = new Set<string>([
62-
"code93",
63-
"code11",
64-
"plessey",
65-
"gs1databar",
61+
/** Severity of visual-render approximation per symbology. One
62+
* axis with two levels:
63+
* - "approx": bitmap approximate, dimensions spec-correct
64+
* (we compute the bbox against the ZPL formula)
65+
* - "unverified": bitmap approximate AND dimensions not cross-
66+
* checkable against Labelary because Labelary
67+
* returns the raw text payload instead of a real
68+
* symbol render. The printed footprint may differ
69+
* from the canvas bbox.
70+
* The UI picks a per-severity hint string; absence means the
71+
* canvas render is trusted. */
72+
export type BwipApproxSeverity = "approx" | "unverified";
73+
74+
export const BWIP_APPROX_SEVERITY: ReadonlyMap<string, BwipApproxSeverity> = new Map([
75+
["code93", "approx"],
76+
["code11", "approx"],
77+
["plessey", "approx"],
78+
["gs1databar", "approx"],
79+
["code49", "unverified"],
80+
["codablock", "unverified"],
81+
["maxicode", "unverified"],
6682
]);
6783

6884
// Per-symbology spec module heights for GS1 DataBar. bwip-js renders most

‎src/components/Canvas/bwipHelpers.ts‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* bwipHelpers.test.ts ensures every BCID-registered type has a case.
1313
*/
1414

15+
import bwipjs from "bwip-js/browser";
1516
import { ObjectRegistry, type LeafObject } from "../../registry";
1617
import type { LabelObject } from "../../types/Group";
1718
import type { Gs1DatabarProps } from "../../registry/gs1databar";
@@ -92,6 +93,7 @@ const BCID: Partial<Record<LabelObject["type"], string>> = {
9293
qrcode: "qrcode",
9394
datamatrix: "datamatrix",
9495
aztec: "azteccodecompact",
96+
maxicode: "maxicode",
9597
micropdf417: "micropdf417",
9698
codablock: "codablockf",
9799
// Placeholder — actual bcid (ean2 vs ean5) is resolved from the
@@ -103,6 +105,48 @@ const BCID: Partial<Record<LabelObject["type"], string>> = {
103105
export const BWIP_SCALE = 2;
104106
const BWIP_2D_INTERNAL_SCALE = 2;
105107

108+
/** Reused unattached canvas for dry-run encoding validators (e.g.
109+
* `validateMaxicodeBwip`). Cheaper than creating a fresh element
110+
* per call and we don't need its pixel output, just the success
111+
* /throw signal from bwip-js. Initialised lazily so SSR-style
112+
* imports don't break — `document` exists only in the browser. */
113+
let _validationCanvas: HTMLCanvasElement | null = null;
114+
function getValidationCanvas(): HTMLCanvasElement {
115+
if (!_validationCanvas) _validationCanvas = document.createElement("canvas");
116+
return _validationCanvas;
117+
}
118+
119+
/** Strip the `bwip-js: bwipp.symbology:` prefixes that bwip-js
120+
* decorates every encoder error with, so the bare diagnostic
121+
* ("Expected postcode followed by group separator character",
122+
* etc.) can be shown as-is in the UI. Shared between the canvas
123+
* render (BarcodeObject) and dry-run validators below. */
124+
export function cleanBwipError(e: unknown): string {
125+
const raw = e instanceof Error ? e.message : String(e);
126+
return raw.replace(/^bwip-js:\s*/i, "").replace(/^bwipp\.[^:]+:\s*/i, "");
127+
}
128+
129+
/** Dry-run a Maxicode encode against the shared validation canvas
130+
* to check whether the (content, mode) combination would succeed
131+
* in the real render path. Returns null on success or the cleaned
132+
* bwip-js error message on failure (e.g. SCM-format mismatch for
133+
* modes 2/3). Uses the same option shape `buildBwipOptions`
134+
* produces so canvas render and panel-side validation can't drift. */
135+
export function validateMaxicodeBwip(content: string, mode: number): string | null {
136+
try {
137+
const opts = {
138+
bcid: "maxicode",
139+
text: content || " ",
140+
scale: BWIP_SCALE,
141+
mode,
142+
} as unknown as Parameters<typeof bwipjs.toCanvas>[1];
143+
bwipjs.toCanvas(getValidationCanvas(), opts);
144+
return null;
145+
} catch (e) {
146+
return cleanBwipError(e);
147+
}
148+
}
149+
106150
/**
107151
* Estimate the number of columns ZPL/Labelary would choose for PDF417 when
108152
* columns=0 (auto). Zebra uses a heuristic to keep the symbol somewhat square.
@@ -506,6 +550,14 @@ export function buildBwipOptions(
506550
opts = { bcid, text: p.content || " ", scale: BWIP_SCALE };
507551
break;
508552
}
553+
case "maxicode": {
554+
const p = obj.props;
555+
// bwip-js maxicode uses `mode` (number) directly; modes 2/3
556+
// would also accept a structured carrier message via `parse`,
557+
// but we don't expose SCM editing yet so pass content as-is.
558+
opts = { bcid, text: p.content || " ", scale: BWIP_SCALE, mode: p.mode };
559+
break;
560+
}
509561
case "micropdf417": {
510562
const p = obj.props;
511563
opts = {
@@ -902,6 +954,15 @@ function getUprightDisplaySize(
902954
(cw / (BWIP_SCALE * BWIP_2D_INTERNAL_SCALE)) * modulePx;
903955
return { w: size, h: size };
904956
}
957+
case "maxicode": {
958+
// Fixed physical size — no module-magnification prop. bwip-js
959+
// emits the canonical maxicode bitmap at BWIP_SCALE internal
960+
// pixels per dot, so converting cw/ch back to dots and then
961+
// to stage px places the symbol at its true printed size.
962+
const w = dotsToPx(cw / BWIP_SCALE, scale, dpmm);
963+
const h = dotsToPx(ch / BWIP_SCALE, scale, dpmm);
964+
return { w, h };
965+
}
905966
case "micropdf417": {
906967
const p = obj.props;
907968
// bwip-js ignores rowheight for micropdf417 and always uses 2 internal pixels per row.

‎src/components/Properties/PropertiesPanel.tsx‎

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { LabelCanvasHandle } from "../Canvas/LabelCanvas";
55
import type { AlignAxis } from "../../lib/alignment";
66
import { ObjectRegistry } from "../../registry";
77
import { canGroupSelection, findObjectById, isGroup } from "../../types/Group";
8-
import { BWIP_VISUAL_APPROX_TYPES } from "../Canvas/bwipConstants";
8+
import { BWIP_APPROX_SEVERITY } from "../Canvas/bwipConstants";
99
import { stripZplCommandChars } from "../../registry/zplHelpers";
1010
import { dotsToMm, mmToDots } from "../../lib/coordinates";
1111
import {
@@ -29,6 +29,26 @@ import {
2929
stripDrivePrefix,
3030
} from "../../lib/customFonts";
3131

32+
/** Tooltip-icon flagging that the canvas render is approximate for
33+
* the given object type. Two severities collapse into one icon
34+
* with a per-severity hint string. Returns null when the type's
35+
* render is fully trusted (no entry in BWIP_APPROX_SEVERITY). */
36+
function BwipApproxIcon({ type }: { type: string }) {
37+
const t = useT();
38+
const severity = BWIP_APPROX_SEVERITY.get(type);
39+
if (!severity) return null;
40+
const title =
41+
severity === "approx"
42+
? t.properties.visualApproxHint
43+
: t.properties.visualApproxUnverifiedHint;
44+
return (
45+
<InformationCircleIcon
46+
className="w-3.5 h-3.5 text-muted/60 cursor-help"
47+
title={title}
48+
/>
49+
);
50+
}
51+
3252
/** POSITION section header with centre-on-label tools docked right —
3353
* same Figma/Sketch/Affinity convention used across leaf, group and
3454
* multi-select panels. `unitSuffix` shows only when X/Y inputs sit
@@ -152,12 +172,8 @@ export function PropertiesPanel({ canvasRef }: PropertiesPanelProps) {
152172
<span className="text-xs font-medium text-text">
153173
{typeLabel}
154174
</span>
155-
{BWIP_VISUAL_APPROX_TYPES.has(obj.type) && (
156-
<InformationCircleIcon
157-
className="w-3.5 h-3.5 text-muted/60 cursor-help"
158-
title={t.properties.visualApproxHint}
159-
/>
160-
)}
175+
<BwipApproxIcon type={obj.type} />
176+
161177
<span className="font-mono text-[10px] text-muted ml-auto truncate">
162178
{obj.id.slice(0, 8)}
163179
</span>

‎src/lib/zplParser.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { DEFAULT_GS_SYMBOL, GS_SYMBOL_CODES, type SymbolProps } from "../registr
3333
import type { SerialProps } from "../registry/serial";
3434
import { isZplRotation, type ZplRotation } from "../registry/rotation";
3535
import type { AztecProps } from "../registry/aztec";
36+
import type { MaxicodeProps } from "../registry/maxicode";
3637
import type { MicroPdf417Props } from "../registry/micropdf417";
3738
import type { CodablockProps } from "../registry/codablock";
3839
import { unzlibSync } from "fflate";
@@ -687,6 +688,9 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL {
687688
// Aztec pending parameters
688689
let aztecMag = 4;
689690

691+
// Maxicode pending parameters
692+
let maxicodeMode: MaxicodeProps["mode"] = 4;
693+
690694
// MicroPDF417 pending parameters
691695
let mpdfRowHeight = 10;
692696

@@ -1118,6 +1122,22 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL {
11181122
),
11191123
);
11201124
break;
1125+
case "maxicode":
1126+
objects.push(
1127+
makeObj(
1128+
"maxicode",
1129+
x,
1130+
y,
1131+
{
1132+
content,
1133+
mode: maxicodeMode,
1134+
rotation: bcRotation,
1135+
} satisfies MaxicodeProps,
1136+
posType,
1137+
comment,
1138+
),
1139+
);
1140+
break;
11211141
case "micropdf417":
11221142
objects.push(
11231143
makeObj(
@@ -1492,6 +1512,17 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL {
14921512
// ^B0N,{magnification},... / ^BON,... — Aztec (^B0 and ^BO are synonyms)
14931513
B0: handleAztec,
14941514
BO: handleAztec,
1515+
// ^BVo,{mode},{symbolNumber},{totalSymbols} — Maxicode (fixed
1516+
// physical size, no magnification). symbolNumber/totalSymbols
1517+
// describe structured-append composition; we don't expose that
1518+
// in the editor, so the params are read but the emitted form
1519+
// pins them to (1, 1).
1520+
BV(p) {
1521+
fieldType = "maxicode";
1522+
bcRotation = readRotation(p[0]);
1523+
const m = int(p[1], 4);
1524+
maxicodeMode = (m >= 2 && m <= 6 ? m : 4) as MaxicodeProps["mode"];
1525+
},
14951526
// ^BFN,{rowHeight} — MicroPDF417
14961527
BF(p) {
14971528
fieldType = "micropdf417";

‎src/locales/ar.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const ar = {
4242
planet: 'Planet Code',
4343
postal: 'POSTNET',
4444
aztec: 'Aztec',
45+
maxicode: 'ماكسي كود',
4546
micropdf417: 'MicroPDF417',
4647
codablock: 'CODABLOCK',
4748
group: 'مجموعة',
@@ -61,6 +62,7 @@ const ar = {
6162
multipleSelectedHint: 'استخدم أسهم الاتجاه للتحريك',
6263
groupSelection: 'تجميع التحديد',
6364
visualApproxHint: 'العرض المرئي تقريبي؛ الأبعاد تطابق طباعة ZPL',
65+
visualApproxUnverifiedHint: 'العرض البصري تقريبي؛ قد تختلف الأبعاد عن طباعة ZPL',
6466
alignCenterH: 'توسيط أفقياً على الملصق',
6567
alignCenterV: 'توسيط رأسياً على الملصق',
6668
alignCenterBoth: 'توسيط على الملصق',
@@ -464,6 +466,11 @@ const ar = {
464466
magnification: 'الحجم (1–10)',
465467
ecLevel: 'تصحيح الأخطاء (0 = تلقائي)',
466468
},
469+
maxicode: {
470+
content: 'المحتوى',
471+
mode: 'الوضع',
472+
mode6Advisory: 'الوضع 6 يقوم بترميز أوامر تكوين القارئ، وليس بيانات المستخدم',
473+
},
467474
micropdf417: {
468475
content: 'المحتوى',
469476
rowHeight: 'ارتفاع الصف (نقاط)',

‎src/locales/bg.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const bg = {
4242
planet: 'Planet Code',
4343
postal: 'POSTNET',
4444
aztec: 'Aztec',
45+
maxicode: 'Maxicode',
4546
micropdf417: 'MicroPDF417',
4647
codablock: 'CODABLOCK',
4748
group: 'Група',
@@ -61,6 +62,7 @@ const bg = {
6162
multipleSelectedHint: 'със стрелките местиш',
6263
groupSelection: 'Групирай селекцията',
6364
visualApproxHint: 'Визуалното изобразяване е приблизително; размерите съответстват на ZPL отпечатъка',
65+
visualApproxUnverifiedHint: 'Визуалното изобразяване е приблизително; размерите може да се различават от ZPL отпечатъка',
6466
alignCenterH: 'Хоризонтално центриране върху етикета',
6567
alignCenterV: 'Вертикално центриране върху етикета',
6668
alignCenterBoth: 'Центриране върху етикета',
@@ -464,6 +466,11 @@ const bg = {
464466
magnification: 'Размер (1–10)',
465467
ecLevel: 'Корекция на грешки (0 = auto)',
466468
},
469+
maxicode: {
470+
content: 'Съдържание',
471+
mode: 'Режим',
472+
mode6Advisory: 'Режим 6 кодира команди за конфигурация на скенера, а не потребителски данни',
473+
},
467474
micropdf417: {
468475
content: 'Съдържание',
469476
rowHeight: 'Височина на ред (точки)',

‎src/locales/cs.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const cs = {
4242
planet: 'Planet Code',
4343
postal: 'POSTNET',
4444
aztec: 'Aztec',
45+
maxicode: 'Maxicode',
4546
micropdf417: 'MicroPDF417',
4647
codablock: 'CODABLOCK',
4748
group: 'Skupina',
@@ -61,6 +62,7 @@ const cs = {
6162
multipleSelectedHint: 'šipkami posunete',
6263
groupSelection: 'Seskupit výběr',
6364
visualApproxHint: 'Vizuální zobrazení je přibližné; rozměry odpovídají tisku ZPL',
65+
visualApproxUnverifiedHint: 'Vizuální vykreslení je přibližné; rozměry se mohou lišit od tisku ZPL',
6466
alignCenterH: 'Vystředit vodorovně na štítku',
6567
alignCenterV: 'Vystředit svisle na štítku',
6668
alignCenterBoth: 'Vystředit na štítku',
@@ -464,6 +466,11 @@ const cs = {
464466
magnification: 'Velikost (1–10)',
465467
ecLevel: 'Korekce chyb (0 = auto)',
466468
},
469+
maxicode: {
470+
content: 'Obsah',
471+
mode: 'Režim',
472+
mode6Advisory: 'Režim 6 kóduje konfigurační příkazy pro čtečku, nikoli uživatelská data',
473+
},
467474
micropdf417: {
468475
content: 'Obsah',
469476
rowHeight: 'Výška řádku (body)',

‎src/locales/da.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const da = {
4242
planet: 'Planet Code',
4343
postal: 'POSTNET',
4444
aztec: 'Aztec',
45+
maxicode: 'Maxicode',
4546
micropdf417: 'MicroPDF417',
4647
codablock: 'CODABLOCK',
4748
group: 'Gruppe',
@@ -61,6 +62,7 @@ const da = {
6162
multipleSelectedHint: 'piletaster flytter',
6263
groupSelection: 'Gruppér valg',
6364
visualApproxHint: 'Visuel gengivelse er omtrentlig; dimensionerne svarer til ZPL-udskriften',
65+
visualApproxUnverifiedHint: 'Visuel gengivelse er omtrentlig; mål kan afvige fra ZPL-udskriften',
6466
alignCenterH: 'Centrer vandret på label',
6567
alignCenterV: 'Centrer lodret på label',
6668
alignCenterBoth: 'Centrer på label',
@@ -464,6 +466,11 @@ const da = {
464466
magnification: 'Størrelse (1–10)',
465467
ecLevel: 'Fejlkorrektion (0 = auto)',
466468
},
469+
maxicode: {
470+
content: 'Indhold',
471+
mode: 'Tilstand',
472+
mode6Advisory: 'Tilstand 6 koder læserkonfigurationskommandoer, ikke brugerdata',
473+
},
467474
micropdf417: {
468475
content: 'Indhold',
469476
rowHeight: 'Rækkehøjde (punkter)',

0 commit comments

Comments
 (0)