From 7de473782031d00244a11f62f6845a435086f0c6 Mon Sep 17 00:00:00 2001 From: u8array Date: Wed, 22 Apr 2026 00:32:22 +0200 Subject: [PATCH 1/3] Refactor barcode positioning and sizing This commit refactors the positioning and sizing logic for barcode objects. It introduces more accurate calculations for 2D barcodes and corrects baseline corrections for various barcode types, including specific adjustments for QR codes. Additionally, it clarifies the `buildBwipOptions` function by assigning options to a variable before returning. --- .zed/settings.json | 0 src/components/Canvas/BarcodeObject.tsx | 160 ++++++++++++++---------- src/components/Canvas/KonvaObject.tsx | 29 ++++- 3 files changed, 122 insertions(+), 67 deletions(-) create mode 100644 .zed/settings.json diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Canvas/BarcodeObject.tsx b/src/components/Canvas/BarcodeObject.tsx index 9e01228..207c32e 100644 --- a/src/components/Canvas/BarcodeObject.tsx +++ b/src/components/Canvas/BarcodeObject.tsx @@ -76,19 +76,21 @@ function eanCheckDigit(digits: string, w0: number, w1: number): string { */ function toCode128BRaw(text: string): string | null { if (!text) return null; - const parts = ['^104']; // Start B + const parts = ["^104"]; // Start B for (const ch of text) { const code = ch.charCodeAt(0); if (code < 32 || code > 126) return null; - parts.push(`^${String(code - 32).padStart(3, '0')}`); + parts.push(`^${String(code - 32).padStart(3, "0")}`); } - return parts.join(''); + return parts.join(""); } function buildBwipOptions(obj: LabelObject): Record | null { const bcid = BCID[obj.type]; if (!bcid) return null; + let opts: Record | null = null; + switch (obj.type) { case "ean13": case "ean8": @@ -102,14 +104,19 @@ function buildBwipOptions(obj: LabelObject): Record | null { } else { text = p.content || "0"; } - return { bcid, text, scale: BWIP_SCALE, height: 10 }; + opts = { bcid, text, scale: BWIP_SCALE, height: 10 }; + break; } case "code128": { const p = obj.props; const text = p.content || "0"; const rawB = toCode128BRaw(text); - if (rawB) return { bcid, text: rawB, raw: true, scale: BWIP_SCALE, height: 10 }; - return { bcid, text, scale: BWIP_SCALE, height: 10 }; + if (rawB) { + opts = { bcid, text: rawB, raw: true, scale: BWIP_SCALE, height: 10 }; + } else { + opts = { bcid, text, scale: BWIP_SCALE, height: 10 }; + } + break; } case "code39": case "interleaved2of5": @@ -121,44 +128,48 @@ function buildBwipOptions(obj: LabelObject): Record | null { case "msi": case "plessey": { const p = obj.props; - return { + opts = { bcid, text: p.content || "0", scale: BWIP_SCALE, height: 10, }; + break; } case "postal": { const p = obj.props; - return { + opts = { bcid, text: p.content || "0", scale: BWIP_SCALE, height: 10, }; + break; } case "logmars": { // LOGMARS is Code 39 with mandatory MOD 43 check digit const p = obj.props; - return { + opts = { bcid, text: p.content || "0", scale: BWIP_SCALE, height: 10, includecheck: true, }; + break; } case "gs1databar": { // bwip-js requires (01) AI prefix for GS1 DataBar const p = obj.props; const raw = (p.content || "0").replace(/\D/g, ""); const padded = raw.padStart(13, "0").slice(0, 14); - return { + opts = { bcid, text: `(01)${padded}`, scale: BWIP_SCALE, height: 10, }; + break; } case "planet": { // USPS PLANET requires 11 or 13 digits (excl. check digit) @@ -166,17 +177,18 @@ function buildBwipOptions(obj: LabelObject): Record | null { let raw = (p.content || "0").replace(/\D/g, ""); if (raw.length < 11) raw = raw.padStart(11, "0"); else if (raw.length === 12) raw = raw.padStart(13, "0"); - return { + opts = { bcid, text: raw, scale: BWIP_SCALE, height: 10, includecheck: true, }; + break; } case "pdf417": { const p = obj.props; - return { + opts = { bcid, text: p.content || " ", scale: BWIP_SCALE, @@ -187,35 +199,39 @@ function buildBwipOptions(obj: LabelObject): Record | null { columns: p.columns || 0, eclevel: String(p.securityLevel), }; + break; } case "qrcode": { const p = obj.props; - return { + opts = { bcid, text: p.content || " ", scale: BWIP_SCALE, eclevel: p.errorCorrection, }; + break; } case "datamatrix": { const p = obj.props; - return { + opts = { bcid, text: p.content || " ", scale: BWIP_SCALE, }; + break; } case "aztec": { const p = obj.props; - return { + opts = { bcid, text: p.content || " ", scale: BWIP_SCALE, }; + break; } case "micropdf417": { const p = obj.props; - return { + opts = { bcid, text: p.content || " ", scale: BWIP_SCALE, @@ -224,10 +240,11 @@ function buildBwipOptions(obj: LabelObject): Record | null { Math.round(p.rowHeight / Math.max(p.moduleWidth, 1)), ), }; + break; } case "codablock": { const p = obj.props; - return { + opts = { bcid, text: p.content || " ", scale: BWIP_SCALE, @@ -236,10 +253,13 @@ function buildBwipOptions(obj: LabelObject): Record | null { Math.round(p.rowHeight / Math.max(p.moduleWidth, 1)), ), }; + break; } default: return null; } + + return opts; } // Compute Konva display dimensions from the rendered bwip canvas and object props. @@ -287,19 +307,19 @@ function getDisplaySize( return { w: canvas.width * ratio, h: canvas.height * ratio }; } case "qrcode": { - // canvas.width / BWIP_SCALE = number of modules; each module = magnification dots + // canvas.width / (BWIP_SCALE * 2) = number of modules; bwip-js applies an implicit 2x scale for 2D barcodes const modulePx = dotsToPx(obj.props.magnification, scale, dpmm); - const size = (canvas.width / BWIP_SCALE) * modulePx; + const size = (canvas.width / (BWIP_SCALE * 2)) * modulePx; return { w: size, h: size }; } case "datamatrix": { const modulePx = dotsToPx(obj.props.dimension, scale, dpmm); - const size = (canvas.width / BWIP_SCALE) * modulePx; + const size = (canvas.width / (BWIP_SCALE * 2)) * modulePx; return { w: size, h: size }; } case "aztec": { const modulePx = dotsToPx(obj.props.magnification, scale, dpmm); - const size = (canvas.width / BWIP_SCALE) * modulePx; + const size = (canvas.width / (BWIP_SCALE * 2)) * modulePx; return { w: size, h: size }; } case "micropdf417": @@ -323,35 +343,6 @@ export function BarcodeObject({ onChange, snap, }: Props) { - // Apply ^FT baseline correction (same logic as KonvaObjectInner) - const displayX = obj.x; - let displayY = obj.y; - if (obj.positionType === "FT") { - if (BARCODE_1D_TYPES.has(obj.type)) { - const p = obj.props as { height: number }; - displayY -= p.height; - } else if ( - obj.type === "pdf417" || - obj.type === "micropdf417" || - obj.type === "codablock" - ) { - const p = obj.props as { rowHeight: number }; - displayY -= p.rowHeight * 10; - } else if (obj.type === "qrcode") { - const p = obj.props as { magnification: number }; - displayY -= p.magnification * 25; - } else if (obj.type === "aztec") { - const p = obj.props as { magnification: number }; - displayY -= p.magnification * 25; - } else if (obj.type === "datamatrix") { - const p = obj.props as { dimension: number }; - displayY -= p.dimension * 20; - } - } - - const x = offsetX + dotsToPx(displayX, scale, dpmm); - const y = offsetY + dotsToPx(displayY, scale, dpmm); - // bwip-js is synchronous — compute canvas directly in render (no async flash on resize) const { barcodeCanvas, errorMsg } = useMemo(() => { const opts = buildBwipOptions(obj); @@ -371,6 +362,40 @@ export function BarcodeObject({ } }, [obj]); + let displayW = 0; + let displayH = 0; + if (barcodeCanvas) { + const size = getDisplaySize(obj, barcodeCanvas, scale, dpmm); + displayW = size.w; + displayH = size.h; + } + + // Apply ^FT baseline correction (same logic as KonvaObjectInner) + const displayX = obj.x; + let displayY = obj.y; + if (obj.positionType === "FT") { + if (barcodeCanvas) { + displayY -= pxToDots(displayH, scale, dpmm); + } else if (BARCODE_1D_TYPES.has(obj.type)) { + displayY -= (obj.props as { height: number }).height; + } + if (obj.type === "qrcode") { + // Zebra firmware artifact: ^FT for QR codes shifts the symbol up by exactly + // 3 modules (= 3 * magnification dots), independent of dpmm or content. + // Verified against Labelary API across magnifications 4–10 at 8 and 12 dpmm. + // Leading theory: the firmware reserves a dummy text-interpretation bounding + // box (as for 1D barcodes) even though QR codes have no human-readable text. + displayY -= 3 * (obj.props as { magnification: number }).magnification; + } + } else if (obj.type === "qrcode") { + // Zebra firmware artifact: ^FO QR codes are rendered with a hardcoded +10 dot + // Y-offset, independent of magnification and dpmm. Verified against Labelary. + displayY += 10; + } + + const x = offsetX + dotsToPx(displayX, scale, dpmm); + const y = offsetY + dotsToPx(displayY, scale, dpmm); + const snapPos = (sx: number, sy: number) => ({ x: offsetX + @@ -385,17 +410,32 @@ export function BarcodeObject({ }; const handleDragEnd = (e: Konva.KonvaEventObject) => { + let finalY = pxToDots(e.target.y() - offsetY, scale, dpmm); + if (obj.positionType === "FT") { + if (barcodeCanvas) { + finalY += pxToDots(displayH, scale, dpmm); + } else if (BARCODE_1D_TYPES.has(obj.type)) { + finalY += (obj.props as { height: number }).height; + } + if (obj.type === "qrcode") { + finalY += 3 * (obj.props as { magnification: number }).magnification; + } + } else if (obj.type === "qrcode") { + finalY -= 10; + } onChange({ x: pxToDots(e.target.x() - offsetX, scale, dpmm), - y: pxToDots(e.target.y() - offsetY, scale, dpmm), + y: finalY, }); }; if (barcodeCanvas) { - const { w, h } = getDisplaySize(obj, barcodeCanvas, scale, dpmm); + const w = displayW; + const h = displayH; const printInterp = !!(obj.props as { printInterpretation?: boolean }) .printInterpretation; - const moduleWidth = (obj.props as { moduleWidth?: number }).moduleWidth ?? 2; + const moduleWidth = + (obj.props as { moduleWidth?: number }).moduleWidth ?? 2; const textFontSize = Math.max(dotsToPx(moduleWidth * 10, scale, dpmm), 6); const textGap = Math.max(dotsToPx(5, scale, dpmm), 3); const rawContent = (obj.props as { content?: string }).content ?? ""; @@ -691,12 +731,7 @@ export function BarcodeObject({ onDragMove={(e) => e.target.position(snapPos(e.target.x(), e.target.y())) } - onDragEnd={(e) => - onChange({ - x: pxToDots(e.target.x() - offsetX, scale, dpmm), - y: pxToDots(e.target.y() - offsetY, scale, dpmm), - }) - } + onDragEnd={handleDragEnd} > e.target.position(snapPos(e.target.x(), e.target.y())) } - onDragEnd={(e) => - onChange({ - x: pxToDots(e.target.x() - offsetX, scale, dpmm), - y: pxToDots(e.target.y() - offsetY, scale, dpmm), - }) - } + onDragEnd={handleDragEnd} > ) => { + let finalX = pxToDots(e.target.x() - offsetX, scale, dpmm); + let finalY = pxToDots(e.target.y() - offsetY, scale, dpmm); + + if (obj.type === "text" || obj.type === "serial") { + const p = obj.props as { fontHeight: number; rotation: string }; + const ROTATION_OFFSET = 15; + if (p.rotation === "I") { + finalY += ROTATION_OFFSET; + } else if (p.rotation === "R") { + finalX += ROTATION_OFFSET; + } else if (p.rotation === "B") { + finalX -= ROTATION_OFFSET; + } + } + + if (obj.positionType === "FT") { + if (obj.type === "box" || obj.type === "ellipse") { + const p = obj.props as { height: number; thickness: number }; + finalY += p.height; + } else if (obj.type === "datamatrix") { + const p = obj.props as { dimension: number }; + finalY += p.dimension * 20; + } + } + onChange({ - x: pxToDots(e.target.x() - offsetX, scale, dpmm), - y: pxToDots(e.target.y() - offsetY, scale, dpmm), + x: finalX, + y: finalY, }); }; From 05b9efd7f1bb5b099950d96848edbfbaee6e49c7 Mon Sep 17 00:00:00 2001 From: u8array Date: Wed, 22 Apr 2026 00:36:54 +0200 Subject: [PATCH 2/3] Refactor 2D barcode sizing and offsets Introduce constants for fixed scaling and offsets related to bwip-js and Zebra firmware artifacts. This clarifies the calculations for 2D barcode sizes and vertical positioning, improving maintainability and readability. --- src/components/Canvas/BarcodeObject.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/Canvas/BarcodeObject.tsx b/src/components/Canvas/BarcodeObject.tsx index 207c32e..69332b9 100644 --- a/src/components/Canvas/BarcodeObject.tsx +++ b/src/components/Canvas/BarcodeObject.tsx @@ -52,6 +52,9 @@ const BCID: Partial> = { }; const BWIP_SCALE = 2; // px per module — fixed render resolution +const BWIP_2D_INTERNAL_SCALE = 2; // bwip-js renders 2D matrix codes as 2×2 units/module (PostScript rounding artifact) +const QR_FO_Y_OFFSET_DOTS = 10; // Zebra firmware artifact: ^FO QR adds hardcoded 10-dot Y offset +const QR_FT_MODULE_OFFSET = 3; // Zebra firmware artifact: ^FT QR shifts symbol up by 3 modules // EAN/UPC barcodes: digits are rendered manually via Konva Text nodes. // Other 1D types: text is a separate ZPL ^FT field. @@ -307,19 +310,19 @@ function getDisplaySize( return { w: canvas.width * ratio, h: canvas.height * ratio }; } case "qrcode": { - // canvas.width / (BWIP_SCALE * 2) = number of modules; bwip-js applies an implicit 2x scale for 2D barcodes + // canvas.width / (BWIP_SCALE * BWIP_2D_INTERNAL_SCALE) = number of modules const modulePx = dotsToPx(obj.props.magnification, scale, dpmm); - const size = (canvas.width / (BWIP_SCALE * 2)) * modulePx; + const size = (canvas.width / (BWIP_SCALE * BWIP_2D_INTERNAL_SCALE)) * modulePx; return { w: size, h: size }; } case "datamatrix": { const modulePx = dotsToPx(obj.props.dimension, scale, dpmm); - const size = (canvas.width / (BWIP_SCALE * 2)) * modulePx; + const size = (canvas.width / (BWIP_SCALE * BWIP_2D_INTERNAL_SCALE)) * modulePx; return { w: size, h: size }; } case "aztec": { const modulePx = dotsToPx(obj.props.magnification, scale, dpmm); - const size = (canvas.width / (BWIP_SCALE * 2)) * modulePx; + const size = (canvas.width / (BWIP_SCALE * BWIP_2D_INTERNAL_SCALE)) * modulePx; return { w: size, h: size }; } case "micropdf417": @@ -385,12 +388,12 @@ export function BarcodeObject({ // Verified against Labelary API across magnifications 4–10 at 8 and 12 dpmm. // Leading theory: the firmware reserves a dummy text-interpretation bounding // box (as for 1D barcodes) even though QR codes have no human-readable text. - displayY -= 3 * (obj.props as { magnification: number }).magnification; + displayY -= QR_FT_MODULE_OFFSET * (obj.props as { magnification: number }).magnification; } } else if (obj.type === "qrcode") { // Zebra firmware artifact: ^FO QR codes are rendered with a hardcoded +10 dot // Y-offset, independent of magnification and dpmm. Verified against Labelary. - displayY += 10; + displayY += QR_FO_Y_OFFSET_DOTS; } const x = offsetX + dotsToPx(displayX, scale, dpmm); @@ -418,10 +421,10 @@ export function BarcodeObject({ finalY += (obj.props as { height: number }).height; } if (obj.type === "qrcode") { - finalY += 3 * (obj.props as { magnification: number }).magnification; + finalY += QR_FT_MODULE_OFFSET * (obj.props as { magnification: number }).magnification; } } else if (obj.type === "qrcode") { - finalY -= 10; + finalY -= QR_FO_Y_OFFSET_DOTS; } onChange({ x: pxToDots(e.target.x() - offsetX, scale, dpmm), From 99541280952ece69bf79b9f3fac7a594b78fa99b Mon Sep 17 00:00:00 2001 From: u8array Date: Wed, 22 Apr 2026 00:39:55 +0200 Subject: [PATCH 3/3] Remove unnecessary position calculation --- src/components/Canvas/KonvaObject.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx index 32f0dc7..8cb2937 100644 --- a/src/components/Canvas/KonvaObject.tsx +++ b/src/components/Canvas/KonvaObject.tsx @@ -470,16 +470,6 @@ function KonvaObjectInner({ } } - if (obj.positionType === "FT") { - if (obj.type === "box" || obj.type === "ellipse") { - const p = obj.props as { height: number; thickness: number }; - finalY += p.height; - } else if (obj.type === "datamatrix") { - const p = obj.props as { dimension: number }; - finalY += p.dimension * 20; - } - } - onChange({ x: finalX, y: finalY,