Skip to content

Commit f9518ba

Browse files
committed
feat(barcode): EAN/UPC extended guard tails via bwip-raw canvas
1 parent de70131 commit f9518ba

2 files changed

Lines changed: 112 additions & 16 deletions

File tree

‎src/components/Canvas/BarcodeObject.tsx‎

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getDisplaySize,
1313
get1DBwipScale,
1414
getEanUpcLayout,
15+
renderEanUpcRawCanvas,
1516
renderTlc39Canvas,
1617
type BarcodeDisplaySize,
1718
type EanUpcType,
@@ -22,6 +23,7 @@ import { buildEanUpcDigitOverlay } from "./eanUpcDigitNodes";
2223
import {
2324
COURIER_BOLD_EM_BOTTOM_PAD,
2425
COURIER_BOLD_INK_TO_EM,
26+
EAN_TEXT_ZONE_DOTS,
2527
EAN_UPC_TYPES,
2628
QR_FO_Y_OFFSET_DOTS,
2729
QR_FT_MODULE_OFFSET,
@@ -75,13 +77,43 @@ export function BarcodeObject({
7577
}
7678
}, []);
7779

80+
// Cross-type props read once so the canvas builder and the HRI overlay
81+
// share one source of truth. Single-field casts at the boundary instead
82+
// of switch-narrowing keep the cross-type read flat (2D types without
83+
// moduleWidth fall through with the default; their branches don't
84+
// render HRI text anyway).
85+
const moduleWidth =
86+
(obj.props as { moduleWidth?: number }).moduleWidth ?? 2;
87+
const rawContent = (obj.props as { content?: string }).content ?? "";
88+
const printInterpEnabled =
89+
!ObjectRegistry[obj.type]?.interpretationLocked &&
90+
!!(obj.props as { printInterpretation?: boolean }).printInterpretation;
91+
7892
let barcodeCanvas: HTMLCanvasElement | null = null;
7993
let errorMsg: string | null = null;
8094
if (obj.type === "tlc39") {
8195
// bwip-js has no native tlc39 encoder, render the Code 39 + MicroPDF417
8296
// composite ourselves. Goes straight to canvas, bypassing buildBwipOptions.
8397
barcodeCanvas = renderTlc39Canvas(obj.props, scale, dpmm);
8498
if (!barcodeCanvas) errorMsg = "TLC39 render failed";
99+
} else if (EAN_UPC_TYPES.has(obj.type)) {
100+
// EAN/UPC use bwip.raw + own fillRect pass so guard tails extend in
101+
// the same canvas as the bars (no overlay seam). Guard tails only
102+
// extend when HRI is on, matching Zebra firmware.
103+
const eanHeight = (obj.props as { height: number }).height;
104+
const modulePxInt = get1DBwipScale(moduleWidth, scale, dpmm);
105+
const barHeightPx = dotsToPx(eanHeight, scale, dpmm);
106+
const tailHeightPx = printInterpEnabled
107+
? dotsToPx(EAN_TEXT_ZONE_DOTS, scale, dpmm)
108+
: 0;
109+
barcodeCanvas = renderEanUpcRawCanvas(
110+
obj.type as EanUpcType,
111+
rawContent,
112+
modulePxInt,
113+
barHeightPx,
114+
tailHeightPx,
115+
);
116+
if (!barcodeCanvas) errorMsg = "EAN/UPC encode failed";
85117
} else {
86118
const opts = buildBwipOptions(obj, scale, dpmm);
87119
if (opts) {
@@ -193,20 +225,7 @@ export function BarcodeObject({
193225
// carries printInterpretation: true.
194226
const rotation = objectRotation(obj.props);
195227
const isUpright = rotation === "N";
196-
const printInterpEnabled =
197-
!ObjectRegistry[obj.type]?.interpretationLocked &&
198-
!!(obj.props as { printInterpretation?: boolean }).printInterpretation;
199-
// Cross-type access: this block runs for every barcode and reads moduleWidth
200-
// generically (textFontSize is computed for HRI text rendering downstream).
201-
// Unlike buildBwipOptions, this function isn't switch-structured by obj.type,
202-
// so per-case narrowing would require restructuring the whole block. The
203-
// cast + fallback stays as the documented boundary between cross-type code
204-
// and the per-type required-typed schemas (2D types without moduleWidth fall
205-
// through with the default; their branches don't render HRI text anyway).
206-
const moduleWidth =
207-
(obj.props as { moduleWidth?: number }).moduleWidth ?? 2;
208228
const textGap = Math.max(dotsToPx(5, scale, dpmm), 3);
209-
const rawContent = (obj.props as { content?: string }).content ?? "";
210229

211230
const hri = ObjectRegistry[obj.type]?.hri;
212231
// Function-form fontDots returns ink dots; inflate to Konva em.
@@ -356,12 +375,37 @@ export function BarcodeObject({
356375
image={barcodeCanvas}
357376
crop={bitmapCrop}
358377
width={ub.barW}
359-
height={ub.barH}
378+
height={isEanUpc
379+
? ub.barH + dotsToPx(EAN_TEXT_ZONE_DOTS, scale, dpmm)
380+
: ub.barH}
381+
// Clip Transformer bbox to the bar area so resize handles
382+
// ignore the guard tails.
383+
ref={isEanUpc ? (node) => {
384+
if (node) {
385+
const w = ub.barW;
386+
const h = ub.barH;
387+
node.getSelfRect = () => ({ x: 0, y: 0, width: w, height: h });
388+
}
389+
} : undefined}
360390
imageSmoothingEnabled={false}
361-
stroke={isSelected ? colors.selection : undefined}
362-
strokeWidth={isSelected ? 2 : 0}
391+
// EAN/UPC draws its selection stroke as the Rect below so
392+
// the highlight skips the guard tails.
393+
stroke={!isEanUpc && isSelected ? colors.selection : undefined}
394+
strokeWidth={!isEanUpc && isSelected ? 2 : 0}
363395
strokeScaleEnabled={false}
364396
/>
397+
{isEanUpc && isSelected && (
398+
<Rect
399+
x={ub.barLeftPx}
400+
y={ub.barTopPx}
401+
width={ub.barW}
402+
height={ub.barH}
403+
stroke={colors.selection}
404+
strokeWidth={2}
405+
strokeScaleEnabled={false}
406+
listening={false}
407+
/>
408+
)}
365409
<Group ref={excludeGroupFromBbox}>{overlayContent}</Group>
366410
</Group>
367411
</Group>

‎src/components/Canvas/bwipHelpers.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,58 @@ function bwipScale1D(
197197

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

200+
/** Sub-pixel overlap so adjacent module rects don't leave hairline gaps. */
201+
const RAW_BAR_SEAM = 0.4;
202+
203+
interface BwipRawLinear {
204+
sbs?: number[];
205+
bbs?: number[];
206+
}
207+
208+
/** Bars + extended guard tails in one fillRect pass via bwip raw
209+
* geometry. `includetext: true` makes bwip mark guards with `bbs<0`;
210+
* we never draw text it would emit. */
211+
export function renderEanUpcRawCanvas(
212+
type: EanUpcType,
213+
text: string,
214+
modulePxInt: number,
215+
barHeightPx: number,
216+
tailHeightPx: number,
217+
): HTMLCanvasElement | null {
218+
if (modulePxInt <= 0 || barHeightPx <= 0) return null;
219+
let stack: BwipRawLinear[];
220+
try {
221+
stack = bwipjs.raw({ bcid: type, text, includetext: true } as never) as BwipRawLinear[];
222+
} catch {
223+
return null;
224+
}
225+
const g = stack?.[0];
226+
if (!g?.sbs) return null;
227+
let totalModules = 0;
228+
for (const w of g.sbs) totalModules += w;
229+
const canvas = document.createElement("canvas");
230+
canvas.width = totalModules * modulePxInt;
231+
canvas.height = barHeightPx + Math.max(0, tailHeightPx);
232+
const ctx = canvas.getContext("2d");
233+
if (!ctx) return null;
234+
ctx.fillStyle = "#000000";
235+
let cx = 0;
236+
let isBar = true;
237+
let barIdx = 0;
238+
for (const w of g.sbs) {
239+
const wPx = w * modulePxInt;
240+
if (isBar) {
241+
const isGuard = (g.bbs?.[barIdx] ?? 0) < 0;
242+
const h = isGuard ? canvas.height : barHeightPx;
243+
ctx.fillRect(cx, 0, wPx + RAW_BAR_SEAM, h);
244+
barIdx++;
245+
}
246+
cx += wPx;
247+
isBar = !isBar;
248+
}
249+
return canvas;
250+
}
251+
200252
// Forcing Code B keeps module count in sync with Labelary (^BC default).
201253
// Returns null for chars outside ASCII 32..126.
202254
function toCode128BRaw(text: string): string | null {

0 commit comments

Comments
 (0)