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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/zpl-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ What's supported, what's next, what's planned.
- [x] `^B5` — Planet Code
- [x] `^BZ` — POSTNET
- [x] `^BS` — UPC/EAN 2- or 5-digit supplement
- [x] `^B4` — Code 49
- [x] `^BQ` — QR Code
- [x] `^BX` — DataMatrix
- [x] `^B7` — PDF417
Expand Down Expand Up @@ -112,7 +113,6 @@ What's supported, what's next, what's planned.

### Barcodes

- [ ] `^B4` — Code 49
- [ ] `^BD` — UPS MaxiCode
- [ ] `^BT` — TLC39

Expand Down
1 change: 1 addition & 0 deletions src/components/Canvas/KonvaObject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ const BARCODE_TYPES = new Set([
"micropdf417",
"codablock",
"upcEanExtension",
"code49",
]);

export function KonvaObject(props_: Props) {
Expand Down
45 changes: 45 additions & 0 deletions src/components/Canvas/bwipHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const BCID: Partial<Record<LabelObject["type"], string>> = {
// Placeholder — actual bcid (ean2 vs ean5) is resolved from the
// content length in the per-type switch in buildBwipOptions.
upcEanExtension: "ean5",
code49: "code49",
};

export const BWIP_SCALE = 2;
Expand Down Expand Up @@ -317,6 +318,29 @@ export function buildBwipOptions(
opts = { bcid, text, scale, height: 10 };
break;
}
case "code49": {
const p = obj.props;
const scale = bwipScale1D(p.moduleWidth, renderScale, renderDpmm);
// Stacked 1D, bwip auto-picks row count. Clamp rowheight to
// bwip's 8..50 range — defensive net for JSON loads that
// bypass the registry's commitTransform/normalize clamps.
const rawRow = Math.round(p.height / Math.max(p.moduleWidth, 1));
const rowheight = Math.min(50, Math.max(8, rawRow));
opts = {
bcid,
text: p.content || "0",
scale,
rowheight,
};
// bwip's mode is numeric 0-5; 'A' (auto) is the no-option case.
if (p.mode !== "A") {
const m = parseInt(p.mode, 10);
if (Number.isInteger(m) && m >= 0 && m <= 5) {
(opts as Record<string, unknown>).mode = m;
}
}
break;
}
case "upcEanExtension": {
const p = obj.props;
const scale = bwipScale1D(p.moduleWidth, renderScale, renderDpmm);
Expand Down Expand Up @@ -858,6 +882,27 @@ function getUprightDisplaySize(
const h = dotsToPx(obj.props.height + LOGMARS_TEXT_ZONE_DOTS, scale, dpmm);
return { w, h };
}
case "code49": {
// Stacked 1D. Labelary's emulator only renders the HRI line
// for ^B4 (not the bars), so bwip-js is the ground truth;
// bbox math is not Labelary-cross-validated. Same as ^BB.
const p = obj.props;
// Mirror buildBwipOptions's 8..50 clamp so numRows recovery
// matches what bwip actually drew.
const rawRow = Math.round(p.height / Math.max(p.moduleWidth, 1));
const rowheightUnits = Math.min(50, Math.max(8, rawRow));
const modulePx = dotsToPx(p.moduleWidth, scale, dpmm);
const bwipSc = get1DBwipScale(p.moduleWidth, scale, dpmm);
// numRows recovery uses bwipSc (matches the scale bwip was
// called with), not BWIP_SCALE — otherwise the row count is
// wrong whenever rendering at a non-default scale/dpmm.
const numRows = Math.max(1, Math.round(ch / (rowheightUnits * bwipSc)));
const w = (cw / bwipSc) * modulePx;
// Bbox uses the clamped row height so the preview matches the
// rendered bars when raw h is outside bwip's range.
const h = numRows * dotsToPx(rowheightUnits * p.moduleWidth, scale, dpmm);
return { w, h };
}
case "code39":
case "interleaved2of5":
case "industrial2of5":
Expand Down
2 changes: 1 addition & 1 deletion src/lib/zplCommandSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const ZPL_COMMANDS: readonly ZplCommandInfo[] = [
{ cmd: 'B1', status: 'supported', description: 'Code 11 barcode' },
{ cmd: 'B2', status: 'supported', description: 'Interleaved 2 of 5 barcode' },
{ cmd: 'B3', status: 'supported', description: 'Code 39 barcode' },
{ cmd: 'B4', status: 'unsupported', description: 'Code 49 barcode' },
{ cmd: 'B4', status: 'supported', description: 'Code 49 barcode' },
{ cmd: 'B5', status: 'supported', description: 'Planet Code barcode' },
{ cmd: 'B7', status: 'supported', description: 'PDF417 barcode' },
{ cmd: 'B8', status: 'supported', description: 'EAN-8 barcode' },
Expand Down
43 changes: 43 additions & 0 deletions src/lib/zplGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,49 @@ describe('generateZPL — parse/generate roundtrip', () => {
expect(props(ext).moduleWidth).toBe(3);
});

it('round-trips a ^B4 Code 49 with default mode A', () => {
const original = parseZPL('^XA^FO10,10^B4N,20,Y,A^FDCODE49^FS^XZ', 8);
const regenerated = generateZPL(BASE_LABEL, original.objects);
const reparsed = parseZPL(regenerated, 8);
const bc = defined(reparsed.objects.find((o) => o.type === 'code49'));
expect(props(bc).content).toBe('CODE49');
expect(props(bc).height).toBe(20);
expect(props(bc).printInterpretation).toBe(true);
expect(props(bc).mode).toBe('A');
});

it('round-trips ^B4 explicit mode + rotation + moduleWidth', () => {
const original = parseZPL('^XA^BY3^FO10,10^B4R,30,N,2^FD12345^FS^XZ', 8);
const regenerated = generateZPL(BASE_LABEL, original.objects);
const reparsed = parseZPL(regenerated, 8);
const bc = defined(reparsed.objects.find((o) => o.type === 'code49'));
expect(props(bc).rotation).toBe('R');
expect(props(bc).moduleWidth).toBe(3);
expect(props(bc).mode).toBe('2');
expect(props(bc).printInterpretation).toBe(false);
});

it('falls back to mode A when ^B4 receives an unknown mode', () => {
const r = parseZPL('^XA^FO10,10^B4N,20,Y,X^FDCODE49^FS^XZ', 8);
const bc = defined(r.objects.find((o) => o.type === 'code49'));
expect(props(bc).mode).toBe('A');
});

it('does not leak ^B4 mode from one symbol to the next', () => {
// Two B4 fields back-to-back: first explicit mode=3, second omits
// the mode parameter. The second must default to 'A' even though
// the parser variable still holds '3' from the previous handler
// run — the handler resets it on each B4 via the `?? "A"` fallback.
const r = parseZPL(
'^XA^FO10,10^B4N,20,Y,3^FDONE^FS^FO10,200^B4N,20,Y^FDTWO^FS^XZ',
8,
);
const codes = r.objects.filter((o) => o.type === 'code49');
expect(codes).toHaveLength(2);
expect(props(codes[0]!).mode).toBe('3');
expect(props(codes[1]!).mode).toBe('A');
});

it('preserves printer params through generate -> parse', () => {
const label: LabelConfig = {
...BASE_LABEL,
Expand Down
32 changes: 32 additions & 0 deletions src/lib/zplParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { ImageProps } from "../registry/image";
import type { Barcode1DProps } from "../registry/barcode1d";
import type { Gs1DatabarProps } from "../registry/gs1databar";
import type { Pdf417Props } from "../registry/pdf417";
import type { Code49Props } from "../registry/code49";
import type { SerialProps } from "../registry/serial";
import { isZplRotation, type ZplRotation } from "../registry/rotation";
import type { AztecProps } from "../registry/aztec";
Expand Down Expand Up @@ -556,6 +557,7 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL {
let bcInterp = true;
let bcCheck = false;
let bcRotation: ZplRotation = "N";
let bcCode49Mode: Code49Props["mode"] = "A";
let gsSymbology: Gs1DatabarProps["symbology"] = 1;
let gsSegments: number | undefined = undefined;
// ^BY barcode defaults
Expand Down Expand Up @@ -901,6 +903,25 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL {
),
);
break;
case "code49":
objects.push(
makeObj(
"code49",
x,
y,
{
content,
height: bcHeight,
moduleWidth: byModuleWidth,
printInterpretation: bcInterp,
mode: bcCode49Mode,
rotation: bcRotation,
} satisfies Code49Props,
posType,
comment,
),
);
break;
case "aztec":
objects.push(
makeObj(
Expand Down Expand Up @@ -1152,6 +1173,17 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL {
B5: mkBarcode("planet", 1, 2), // ^B5N,h,i,N
BZ: mkBarcode("postal", 1, 2), // ^BZN,h,i,N
BS: mkBarcode("upcEanExtension", 1, 2), // ^BSo,h,f (UPC/EAN 2- or 5-digit supplement)
B4: (p) => {
// ^B4o,h,f,m — Code 49. Custom handler for the extra `m`.
fieldType = "code49";
bcRotation = readRotation(p[0]);
bcHeight = int(p[1], byHeight || 20);
bcInterp = (p[2] ?? "N") === "Y";
const m = (p[3] ?? "A").toUpperCase();
bcCode49Mode = /^[A0-5]$/.test(m)
? (m as Code49Props["mode"])
: "A";
},

// MSI: check logic is "any letter except N" (not simple "Y") — keep inline
// ^BMN,{checkType},{height},{interp},N (checkType: A/B/C/D=enabled, N=none)
Expand Down
9 changes: 9 additions & 0 deletions src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const ar = {
ean8: 'EAN-8',
upce: 'UPC-E',
upcEanExtension: 'امتداد UPC/EAN',
code49: 'Code 49',
interleaved2of5: 'Interleaved 2 من 5',
code93: 'Code 93',
pdf417: 'PDF417',
Expand Down Expand Up @@ -341,6 +342,14 @@ const ar = {
moduleWidth: 'عرض الوحدة',
placeholder: '2 أو 5 أرقام',
},
code49: {
content: 'المحتوى',
height: 'الارتفاع (نقاط)',
moduleWidth: 'عرض الوحدة',
printInterpretation: 'قابل للقراءة',
mode: 'الوضع',
modeAuto: 'تلقائي',
},
interleaved2of5: {
content: 'المحتوى',
height: 'الارتفاع (نقاط)',
Expand Down
9 changes: 9 additions & 0 deletions src/locales/bg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const bg = {
ean8: 'EAN-8',
upce: 'UPC-E',
upcEanExtension: 'Разширение UPC/EAN',
code49: 'Code 49',
interleaved2of5: 'Interleaved 2 от 5',
code93: 'Code 93',
pdf417: 'PDF417',
Expand Down Expand Up @@ -341,6 +342,14 @@ const bg = {
moduleWidth: 'Ширина на модула',
placeholder: '2 или 5 цифри',
},
code49: {
content: 'Съдържание',
height: 'Височина (точки)',
moduleWidth: 'Ширина на модула',
printInterpretation: 'Четим',
mode: 'Режим',
modeAuto: 'авто',
},
interleaved2of5: {
content: 'Съдържание',
height: 'Височина (точки)',
Expand Down
9 changes: 9 additions & 0 deletions src/locales/cs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const cs = {
ean8: 'EAN-8',
upce: 'UPC-E',
upcEanExtension: 'Rozšíření UPC/EAN',
code49: 'Code 49',
interleaved2of5: 'Interleaved 2 z 5',
code93: 'Code 93',
pdf417: 'PDF417',
Expand Down Expand Up @@ -341,6 +342,14 @@ const cs = {
moduleWidth: 'Šířka modulu',
placeholder: '2 nebo 5 číslic',
},
code49: {
content: 'Obsah',
height: 'Výška (body)',
moduleWidth: 'Šířka modulu',
printInterpretation: 'Čitelné',
mode: 'Režim',
modeAuto: 'auto',
},
interleaved2of5: {
content: 'Obsah',
height: 'Výška (body)',
Expand Down
9 changes: 9 additions & 0 deletions src/locales/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const da = {
ean8: 'EAN-8',
upce: 'UPC-E',
upcEanExtension: 'UPC/EAN-tillæg',
code49: 'Code 49',
interleaved2of5: 'Interleaved 2 af 5',
code93: 'Code 93',
pdf417: 'PDF417',
Expand Down Expand Up @@ -341,6 +342,14 @@ const da = {
moduleWidth: 'Modulbredde',
placeholder: '2 eller 5 cifre',
},
code49: {
content: 'Indhold',
height: 'Højde (punkter)',
moduleWidth: 'Modulbredde',
printInterpretation: 'Læsbar',
mode: 'Tilstand',
modeAuto: 'auto',
},
interleaved2of5: {
content: 'Indhold',
height: 'Højde (punkter)',
Expand Down
9 changes: 9 additions & 0 deletions src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const de = {
ean8: 'EAN-8',
upce: 'UPC-E',
upcEanExtension: 'UPC/EAN-Erweiterung',
code49: 'Code 49',
interleaved2of5: 'Interleaved 2 of 5',
code93: 'Code 93',
pdf417: 'PDF417',
Expand Down Expand Up @@ -362,6 +363,14 @@ const de = {
moduleWidth: 'Modulbreite',
placeholder: '2 oder 5 Ziffern',
},
code49: {
content: 'Inhalt',
height: 'Höhe (Punkte)',
moduleWidth: 'Modulbreite',
printInterpretation: 'Klartext',
mode: 'Modus',
modeAuto: 'auto',
},
interleaved2of5: {
content: 'Inhalt',
height: 'Höhe (Punkte)',
Expand Down
9 changes: 9 additions & 0 deletions src/locales/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const el = {
ean8: 'EAN-8',
upce: 'UPC-E',
upcEanExtension: 'Επέκταση UPC/EAN',
code49: 'Κωδικός 49',
interleaved2of5: 'Interleaved 2 από 5',
code93: 'Code 93',
pdf417: 'PDF417',
Expand Down Expand Up @@ -341,6 +342,14 @@ const el = {
moduleWidth: 'Πλάτος μονάδας',
placeholder: '2 ή 5 ψηφία',
},
code49: {
content: 'Περιεχόμενο',
height: 'Ύψος (κουκκίδες)',
moduleWidth: 'Πλάτος μονάδας',
printInterpretation: 'Αναγνώσιμο',
mode: 'Λειτουργία',
modeAuto: 'αυτόματο',
},
interleaved2of5: {
content: 'Περιεχόμενο',
height: 'Ύψος (κουκκίδες)',
Expand Down
9 changes: 9 additions & 0 deletions src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const en = {
ean8: 'EAN-8',
upce: 'UPC-E',
upcEanExtension: 'UPC/EAN extension',
code49: 'Code 49',
interleaved2of5: 'Interleaved 2 of 5',
code93: 'Code 93',
pdf417: 'PDF417',
Expand Down Expand Up @@ -362,6 +363,14 @@ const en = {
moduleWidth: 'Module width',
placeholder: '2 or 5 digits',
},
code49: {
content: 'Content',
height: 'Height (dots)',
moduleWidth: 'Module width',
printInterpretation: 'Human readable',
mode: 'Mode',
modeAuto: 'auto',
},
interleaved2of5: {
content: 'Content',
height: 'Height (dots)',
Expand Down
9 changes: 9 additions & 0 deletions src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const es = {
ean8: 'EAN-8',
upce: 'UPC-E',
upcEanExtension: 'Extensión UPC/EAN',
code49: 'Code 49',
interleaved2of5: 'Intercalado 2 de 5',
code93: 'Code 93',
pdf417: 'PDF417',
Expand Down Expand Up @@ -341,6 +342,14 @@ const es = {
moduleWidth: 'Ancho de módulo',
placeholder: '2 o 5 dígitos',
},
code49: {
content: 'Contenido',
height: 'Altura (puntos)',
moduleWidth: 'Ancho de módulo',
printInterpretation: 'Legible',
mode: 'Modo',
modeAuto: 'auto',
},
interleaved2of5: {
content: 'Contenido',
height: 'Altura (puntos)',
Expand Down
Loading