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
Empty file added .zed/settings.json
Empty file.
163 changes: 98 additions & 65 deletions src/components/Canvas/BarcodeObject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ const BCID: Partial<Record<LabelObject["type"], string>> = {
};

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.
Expand All @@ -76,19 +79,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<string, unknown> | null {
const bcid = BCID[obj.type];
if (!bcid) return null;

let opts: Record<string, unknown> | null = null;

switch (obj.type) {
case "ean13":
case "ean8":
Expand All @@ -102,14 +107,19 @@ function buildBwipOptions(obj: LabelObject): Record<string, unknown> | 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":
Expand All @@ -121,62 +131,67 @@ function buildBwipOptions(obj: LabelObject): Record<string, unknown> | 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)
const p = obj.props;
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,
Expand All @@ -187,35 +202,39 @@ function buildBwipOptions(obj: LabelObject): Record<string, unknown> | 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,
Expand All @@ -224,10 +243,11 @@ function buildBwipOptions(obj: LabelObject): Record<string, unknown> | 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,
Expand All @@ -236,10 +256,13 @@ function buildBwipOptions(obj: LabelObject): Record<string, unknown> | 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.
Expand Down Expand Up @@ -287,19 +310,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 * BWIP_2D_INTERNAL_SCALE) = number of modules
const modulePx = dotsToPx(obj.props.magnification, scale, dpmm);
const size = (canvas.width / BWIP_SCALE) * 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) * 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) * modulePx;
const size = (canvas.width / (BWIP_SCALE * BWIP_2D_INTERNAL_SCALE)) * modulePx;
return { w: size, h: size };
}
case "micropdf417":
Expand All @@ -323,35 +346,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);
Expand All @@ -371,6 +365,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;
}
Comment thread
u8array marked this conversation as resolved.
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 -= 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 += QR_FO_Y_OFFSET_DOTS;
}

const x = offsetX + dotsToPx(displayX, scale, dpmm);
const y = offsetY + dotsToPx(displayY, scale, dpmm);

const snapPos = (sx: number, sy: number) => ({
x:
offsetX +
Expand All @@ -385,17 +413,32 @@ export function BarcodeObject({
};

const handleDragEnd = (e: Konva.KonvaEventObject<DragEvent>) => {
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 += QR_FT_MODULE_OFFSET * (obj.props as { magnification: number }).magnification;
}
} else if (obj.type === "qrcode") {
finalY -= QR_FO_Y_OFFSET_DOTS;
}
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 ?? "";
Expand Down Expand Up @@ -691,12 +734,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}
>
<KImage
x={0}
Expand Down Expand Up @@ -738,12 +776,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}
>
<KImage
x={0}
Expand Down
19 changes: 17 additions & 2 deletions src/components/Canvas/KonvaObject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -455,9 +455,24 @@ function KonvaObjectInner({
};

const handleDragEnd = (e: Konva.KonvaEventObject<DragEvent>) => {
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;
}
}

onChange({
x: pxToDots(e.target.x() - offsetX, scale, dpmm),
y: pxToDots(e.target.y() - offsetY, scale, dpmm),
x: finalX,
y: finalY,
});
};

Expand Down
Loading