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
76 changes: 60 additions & 16 deletions src/components/Canvas/BarcodeObject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getDisplaySize,
get1DBwipScale,
getEanUpcLayout,
renderEanUpcRawCanvas,
renderTlc39Canvas,
type BarcodeDisplaySize,
type EanUpcType,
Expand All @@ -22,6 +23,7 @@ import { buildEanUpcDigitOverlay } from "./eanUpcDigitNodes";
import {
COURIER_BOLD_EM_BOTTOM_PAD,
COURIER_BOLD_INK_TO_EM,
EAN_TEXT_ZONE_DOTS,
EAN_UPC_TYPES,
QR_FO_Y_OFFSET_DOTS,
QR_FT_MODULE_OFFSET,
Expand Down Expand Up @@ -75,13 +77,43 @@ export function BarcodeObject({
}
}, []);

// Cross-type props read once so the canvas builder and the HRI overlay
// share one source of truth. Single-field casts at the boundary instead
// of switch-narrowing keep the cross-type read flat (2D types without
// moduleWidth fall through with the default; their branches don't
// render HRI text anyway).
const moduleWidth =
(obj.props as { moduleWidth?: number }).moduleWidth ?? 2;
const rawContent = (obj.props as { content?: string }).content ?? "";
const printInterpEnabled =
!ObjectRegistry[obj.type]?.interpretationLocked &&
!!(obj.props as { printInterpretation?: boolean }).printInterpretation;

let barcodeCanvas: HTMLCanvasElement | null = null;
let errorMsg: string | null = null;
if (obj.type === "tlc39") {
// bwip-js has no native tlc39 encoder, render the Code 39 + MicroPDF417
// composite ourselves. Goes straight to canvas, bypassing buildBwipOptions.
barcodeCanvas = renderTlc39Canvas(obj.props, scale, dpmm);
if (!barcodeCanvas) errorMsg = "TLC39 render failed";
} else if (EAN_UPC_TYPES.has(obj.type)) {
// EAN/UPC use bwip.raw + own fillRect pass so guard tails extend in
// the same canvas as the bars (no overlay seam). Guard tails only
// extend when HRI is on, matching Zebra firmware.
const eanHeight = (obj.props as { height: number }).height;
const modulePxInt = get1DBwipScale(moduleWidth, scale, dpmm);
const barHeightPx = dotsToPx(eanHeight, scale, dpmm);
const tailHeightPx = printInterpEnabled
? dotsToPx(EAN_TEXT_ZONE_DOTS, scale, dpmm)
: 0;
barcodeCanvas = renderEanUpcRawCanvas(
obj.type as EanUpcType,
rawContent,
modulePxInt,
barHeightPx,
tailHeightPx,
);
Comment on lines +103 to +115

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When printInterpretation is disabled (printInterpEnabled is false), the EAN/UPC layout still reserves the 13-dot text zone at the bottom of the barcode to match the firmware-reserved footprint. However, passing tailHeightPx = 0 to renderEanUpcRawCanvas results in a canvas height of only barHeightPx. Since the KImage height is unconditionally set to ub.barH + EAN_TEXT_ZONE_DOTS, the canvas image gets vertically stretched by ~13%, causing blurry rendering and overlapping the reserved text zone.

To fix this, we should always pass the full EAN_TEXT_ZONE_DOTS height as tailHeightPx to ensure the canvas height matches the KImage height, and pass printInterpEnabled as a boolean flag to control whether the guard tails actually extend into this zone.

    const eanHeight = (obj.props as { height: number }).height;
    const modulePxInt = get1DBwipScale(moduleWidth, scale, dpmm);
    const barHeightPx = dotsToPx(eanHeight, scale, dpmm);
    const tailHeightPx = dotsToPx(EAN_TEXT_ZONE_DOTS, scale, dpmm);
    barcodeCanvas = renderEanUpcRawCanvas(
      obj.type as EanUpcType,
      rawContent,
      modulePxInt,
      barHeightPx,
      tailHeightPx,
      printInterpEnabled,
    );

if (!barcodeCanvas) errorMsg = "EAN/UPC encode failed";
} else {
const opts = buildBwipOptions(obj, scale, dpmm);
if (opts) {
Expand Down Expand Up @@ -193,20 +225,7 @@ export function BarcodeObject({
// carries printInterpretation: true.
const rotation = objectRotation(obj.props);
const isUpright = rotation === "N";
const printInterpEnabled =
!ObjectRegistry[obj.type]?.interpretationLocked &&
!!(obj.props as { printInterpretation?: boolean }).printInterpretation;
// Cross-type access: this block runs for every barcode and reads moduleWidth
// generically (textFontSize is computed for HRI text rendering downstream).
// Unlike buildBwipOptions, this function isn't switch-structured by obj.type,
// so per-case narrowing would require restructuring the whole block. The
// cast + fallback stays as the documented boundary between cross-type code
// and the per-type required-typed schemas (2D types without moduleWidth fall
// through with the default; their branches don't render HRI text anyway).
const moduleWidth =
(obj.props as { moduleWidth?: number }).moduleWidth ?? 2;
const textGap = Math.max(dotsToPx(5, scale, dpmm), 3);
const rawContent = (obj.props as { content?: string }).content ?? "";

const hri = ObjectRegistry[obj.type]?.hri;
// Function-form fontDots returns ink dots; inflate to Konva em.
Expand Down Expand Up @@ -356,12 +375,37 @@ export function BarcodeObject({
image={barcodeCanvas}
crop={bitmapCrop}
width={ub.barW}
height={ub.barH}
height={isEanUpc
? ub.barH + dotsToPx(EAN_TEXT_ZONE_DOTS, scale, dpmm)
: ub.barH}
// Clip Transformer bbox to the bar area so resize handles
// ignore the guard tails.
ref={isEanUpc ? (node) => {
if (node) {
const w = ub.barW;
const h = ub.barH;
node.getSelfRect = () => ({ x: 0, y: 0, width: w, height: h });
}
} : undefined}
imageSmoothingEnabled={false}
stroke={isSelected ? colors.selection : undefined}
strokeWidth={isSelected ? 2 : 0}
// EAN/UPC draws its selection stroke as the Rect below so
// the highlight skips the guard tails.
stroke={!isEanUpc && isSelected ? colors.selection : undefined}
strokeWidth={!isEanUpc && isSelected ? 2 : 0}
strokeScaleEnabled={false}
/>
{isEanUpc && isSelected && (
<Rect
x={ub.barLeftPx}
y={ub.barTopPx}
width={ub.barW}
height={ub.barH}
stroke={colors.selection}
strokeWidth={2}
strokeScaleEnabled={false}
listening={false}
/>
)}
<Group ref={excludeGroupFromBbox}>{overlayContent}</Group>
</Group>
</Group>
Expand Down
52 changes: 52 additions & 0 deletions src/components/Canvas/bwipHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,58 @@ function bwipScale1D(

export { eanCheckDigit, upceCheckDigit } from "../../lib/barcodeCheckDigits";

/** Sub-pixel overlap so adjacent module rects don't leave hairline gaps. */
const RAW_BAR_SEAM = 0.4;

interface BwipRawLinear {
sbs?: number[];
bbs?: number[];
}

/** Bars + extended guard tails in one fillRect pass via bwip raw
* geometry. `includetext: true` makes bwip mark guards with `bbs<0`;
* we never draw text it would emit. */
export function renderEanUpcRawCanvas(
type: EanUpcType,
text: string,
modulePxInt: number,
barHeightPx: number,
tailHeightPx: number,
): HTMLCanvasElement | null {
if (modulePxInt <= 0 || barHeightPx <= 0) return null;
let stack: BwipRawLinear[];
try {
stack = bwipjs.raw({ bcid: type, text, includetext: true } as never) as BwipRawLinear[];
} catch {
return null;
}
const g = stack?.[0];
if (!g?.sbs) return null;
let totalModules = 0;
for (const w of g.sbs) totalModules += w;
const canvas = document.createElement("canvas");
canvas.width = totalModules * modulePxInt;
canvas.height = barHeightPx + Math.max(0, tailHeightPx);
const ctx = canvas.getContext("2d");
if (!ctx) return null;
ctx.fillStyle = "#000000";
let cx = 0;
let isBar = true;
let barIdx = 0;
for (const w of g.sbs) {
const wPx = w * modulePxInt;
if (isBar) {
const isGuard = (g.bbs?.[barIdx] ?? 0) < 0;
const h = isGuard ? canvas.height : barHeightPx;
ctx.fillRect(cx, 0, wPx + RAW_BAR_SEAM, h);
barIdx++;
}
cx += wPx;
isBar = !isBar;
}
return canvas;
}
Comment on lines +211 to +250

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Update renderEanUpcRawCanvas to accept the extendGuards boolean flag to control guard tail extension. Additionally, round barHeightPx and tailHeightPx to integers to prevent sub-pixel rendering (anti-aliasing) which causes blurry barcode edges and scanning failures. Also, add a defensive check to ensure totalModules is greater than 0 before creating the canvas.

export function renderEanUpcRawCanvas(
  type: EanUpcType,
  text: string,
  modulePxInt: number,
  barHeightPx: number,
  tailHeightPx: number,
  extendGuards: boolean,
): HTMLCanvasElement | null {
  const barH = Math.round(barHeightPx);
  const tailH = Math.round(tailHeightPx);
  if (modulePxInt <= 0 || barH <= 0) return null;
  let stack: BwipRawLinear[];
  try {
    stack = bwipjs.raw({ bcid: type, text, includetext: true } as never) as BwipRawLinear[];
  } catch {
    return null;
  }
  const g = stack?.[0];
  if (!g?.sbs) return null;
  let totalModules = 0;
  for (const w of g.sbs) totalModules += w;
  if (totalModules <= 0) return null;
  const canvas = document.createElement("canvas");
  canvas.width = totalModules * modulePxInt;
  canvas.height = barH + Math.max(0, tailH);
  const ctx = canvas.getContext("2d");
  if (!ctx) return null;
  ctx.fillStyle = "#000000";
  let cx = 0;
  let isBar = true;
  let barIdx = 0;
  for (const w of g.sbs) {
    const wPx = w * modulePxInt;
    if (isBar) {
      const isGuard = extendGuards && (g.bbs?.[barIdx] ?? 0) < 0;
      const h = isGuard ? canvas.height : barH;
      ctx.fillRect(cx, 0, wPx + RAW_BAR_SEAM, h);
      barIdx++;
    }
    cx += wPx;
    isBar = !isBar;
  }
  return canvas;
}


// Forcing Code B keeps module count in sync with Labelary (^BC default).
// Returns null for chars outside ASCII 32..126.
function toCode128BRaw(text: string): string | null {
Expand Down