@@ -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";
2223import {
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 >
0 commit comments