@@ -289,12 +289,8 @@ export function buildBwipOptions(
289289 const bcid = BCID [ obj . type ] ;
290290 if ( ! bcid ) return null ;
291291
292- // bwip-js takes the same N/R/I/B letters ZPL does for symbol orientation;
293- // emitting it post-build means the produced bitmap is already rotated and
294- // its dimensions are the post-rotation extents — no Konva-side rotation math
295- // needed.
296- const rotation = objectRotation ( obj . props ) ;
297-
292+ // bwip always renders upright; visual rotation is handled in the
293+ // Konva renderer via an inner rotated Group (rotatedGroupTransform).
298294 // Declared without an initializer because every reachable case
299295 // assigns before `break` and the `default` arm returns early — the
300296 // previous `= null` initializer is what ESLint 10's
@@ -352,8 +348,9 @@ export function buildBwipOptions(
352348 // HRI digits sit ABOVE the bars per Zebra firmware. Rendered
353349 // as a separate Konva Text overlay (same pattern as logmars)
354350 // so all four rotations land at the firmware-correct anchor
355- // via getRotatedTextAnchor; bwip's own includetext would
356- // bake the text into the bitmap and rotate with it.
351+ // via the inner rotated Group in BarcodeObject; bwip's own
352+ // includetext would bake the text into the bitmap and rotate
353+ // with it.
357354 const text = p . content || "00000" ;
358355 const variantBcid = text . length === 2 ? "ean2" : "ean5" ;
359356 opts = {
@@ -539,16 +536,10 @@ export function buildBwipOptions(
539536 return null ;
540537 }
541538
542- if ( rotation !== "N" ) {
543- // ZPL uses N/R/I/B (B = 270° CW). bwip-js uses N/R/I/L (L = 90° CCW =
544- // 270° CW). The other three letters mean the same thing in both.
545- opts . rotate = rotation === "B" ? "L" : rotation ;
546- // HRI text is handled as a Konva overlay in BarcodeObject (same as for
547- // upright barcodes). Using bwip's includetext would embed text into the
548- // bitmap at bwip's internal scale, making the bitmap taller/wider than the
549- // bar-only dimensions that getDisplaySize computes — causing the KImage to
550- // stretch the bitmap incorrectly and appear blurry/distorted.
551- }
539+ // Visual rotation is Konva's job (wraps the KImage in a rotated Group
540+ // via rotatedGroupTransform). Asking bwip to rotate would produce
541+ // bitmaps in mismatched orientations across types — keep them all
542+ // upright so the renderer can rotate them uniformly.
552543 return opts ;
553544}
554545
@@ -573,6 +564,18 @@ export interface BarcodeDisplaySize {
573564 barH : number ;
574565 barLeftPx : number ;
575566 barTopPx : number ;
567+ /** Upright (rotation=N) view of the same layout. Renderers that draw
568+ * inside an inner rotated Group consume this so their geometry stays
569+ * rotation-independent — the inner group's transform handles the
570+ * visual rotation. Always present alongside the rotated fields. */
571+ upright : {
572+ w : number ;
573+ h : number ;
574+ barW : number ;
575+ barH : number ;
576+ barLeftPx : number ;
577+ barTopPx : number ;
578+ } ;
576579 /** Sub-rect of the bwip-js canvas to render (in source pixel coords).
577580 * Lets the renderer skip bwip's internal padding, e.g. the
578581 * paddingheight pad on GS1 DataBar that would otherwise leave the
@@ -581,60 +584,6 @@ export interface BarcodeDisplaySize {
581584 bitmapCrop ?: { x : number ; y : number ; width : number ; height : number } ;
582585}
583586
584- /** Anchor coordinates for a Konva Text node that is rotated alongside a
585- * 1D barcode. Exactly one field is meaningful per rotation: `sideX` for
586- * R / B, `topY` for I. The other is set to 0 and ignored by the caller. */
587- export interface RotatedTextAnchor {
588- sideX : number ;
589- topY : number ;
590- }
591-
592- /**
593- * Where to place the rotated HRI text node so it sits `textGap` dots away
594- * from the bars on the firmware-correct side.
595- *
596- * Naive sideX = -textGap / w + textGap anchors against the bbox edge,
597- * which double-counts the firmware text zone (EAN/UPC: 13 dots, logmars:
598- * 20 dots). Anchoring against the bar sub-rectangle (`barLeftPx`/`bw` for
599- * R/B, `barTopPx`/`bh` for I) keeps the gap at exactly `textGap`
600- * regardless of which side the text zone sits on.
601- *
602- * Konva rotates around the node origin, so the anchor accounts for the
603- * text glyph extending in the rotation-opposite direction. For R (CW 90)
604- * with text on the right, the glyph extends LEFT of `sideX` by
605- * `textFontSize`, so we offset by +textFontSize. Mirror for B (CCW 90)
606- * and I (180).
607- */
608- export function getRotatedTextAnchor (
609- rotation : "R" | "B" | "I" ,
610- isTextAbove : boolean ,
611- dim : Pick < BarcodeDisplaySize , "barLeftPx" | "barTopPx" | "barW" | "barH" > ,
612- textGap : number ,
613- textFontSize : number ,
614- ) : RotatedTextAnchor {
615- const { barLeftPx : btX , barTopPx : btY , barW : bw , barH : bh } = dim ;
616- if ( rotation === "R" ) {
617- return {
618- sideX : isTextAbove ? btX + bw + textGap + textFontSize : btX - textGap ,
619- topY : 0 ,
620- } ;
621- }
622- if ( rotation === "B" ) {
623- return {
624- sideX : isTextAbove ? btX - textGap - textFontSize : btX + bw + textGap ,
625- topY : 0 ,
626- } ;
627- }
628- // I (180°): text glyph extends UP from the origin, so a text-below-in-
629- // upright glyph (now top after flip) anchors at btY - textGap; a
630- // text-above-in-upright glyph (now bottom) anchors at btY + bh +
631- // textGap + textFontSize.
632- return {
633- sideX : 0 ,
634- topY : isTextAbove ? btY + bh + textGap + textFontSize : btY - textGap ,
635- } ;
636- }
637-
638587/** Firmware-reserved text-zone height in dots, keyed by symbology. The
639588 * zone sits below the bars in upright orientation; rotation maps it to
640589 * another side of the bbox in getDisplaySize. Types not listed have no
@@ -656,19 +605,18 @@ export function getDisplaySize(
656605 if ( ! canvas ) {
657606 return {
658607 w : 0 , h : 0 , barW : 0 , barH : 0 , barLeftPx : 0 , barTopPx : 0 ,
608+ upright : { w : 0 , h : 0 , barW : 0 , barH : 0 , barLeftPx : 0 , barTopPx : 0 } ,
659609 } ;
660610 }
661611
662- // For 90°/270° rotations, bwip-js produces a bitmap whose width and height
663- // are swapped relative to the upright form. Compute size as if upright ( the
664- // existing per-symbology formulas all assume that), then swap at the end .
612+ // bwip-js now always renders upright (the BarcodeObject renderer
613+ // handles visual rotation via a rotated Konva Group), so the canvas
614+ // dimensions are the upright dimensions directly .
665615 const rotation = objectRotation ( obj . props ) ;
666616 const isQuarter = rotation === "R" || rotation === "B" ;
667- const cw = isQuarter ? canvas . height : canvas . width ;
668- const ch = isQuarter ? canvas . width : canvas . height ;
669- const upright = getUprightDisplaySize ( obj , cw , ch , scale , dpmm ) ;
617+ const upright = getUprightDisplaySize ( obj , canvas . width , canvas . height , scale , dpmm ) ;
670618
671- // Bbox after rotation.
619+ // Bbox after rotation: R/B swap upright w/h; N/I keep them .
672620 const w = isQuarter ? upright . h : upright . w ;
673621 const h = isQuarter ? upright . w : upright . h ;
674622
@@ -725,39 +673,41 @@ export function getDisplaySize(
725673 }
726674
727675 // GS1 DataBar opts include `paddingheight: N`, which adds whitespace
728- // rows on top and bottom of the bwip canvas. Without cropping them out,
729- // the bitmap drawn at displayH leaves the bars proportionally shorter
730- // than the spec-correct height. Zebra firmware fills the full reserved
731- // height with bars; mirror that by cropping the source bitmap to the
732- // bar-only rows.
733- //
734- // Rotation flips which axis the padding sits on. For N / I the padding
735- // is on top/bottom of the bitmap as bwip produced it; for R / B (bwip
736- // rotated 90° CW / CCW respectively) the same rows end up on the
737- // left/right edges, so the crop must run along the x-axis instead.
676+ // rows on top and bottom of the upright bwip canvas. Without cropping
677+ // them out, the bitmap drawn at displayH leaves the bars
678+ // proportionally shorter than the spec-correct height. Zebra firmware
679+ // fills the full reserved height with bars; mirror that by cropping
680+ // the source bitmap to the bar-only rows. Always y-axis now because
681+ // bwip renders upright.
738682 let bitmapCrop : BarcodeDisplaySize [ "bitmapCrop" ] ;
739683 if ( obj . type === "gs1databar" ) {
740684 const bwipSc = get1DBwipScale ( obj . props . moduleWidth , scale , dpmm ) ;
741685 const padPx = GS1_DATABAR_PADDING_ROWS * bwipSc ;
742- const axisDim = isQuarter ? canvas . width : canvas . height ;
743- if ( axisDim > 2 * padPx ) {
744- bitmapCrop = isQuarter
745- ? {
746- x : padPx ,
747- y : 0 ,
748- width : canvas . width - 2 * padPx ,
749- height : canvas . height ,
750- }
751- : {
752- x : 0 ,
753- y : padPx ,
754- width : canvas . width ,
755- height : canvas . height - 2 * padPx ,
756- } ;
686+ if ( canvas . height > 2 * padPx ) {
687+ bitmapCrop = {
688+ x : 0 ,
689+ y : padPx ,
690+ width : canvas . width ,
691+ height : canvas . height - 2 * padPx ,
692+ } ;
757693 }
758694 }
759695
760- return { w, h, barW, barH, barLeftPx, barTopPx, bitmapCrop } ;
696+ // Upright view (rotation=N) of the same layout. Text zone sits above
697+ // the bars when isTextAbove (logmars), otherwise implicitly below
698+ // (barTopPx=0, barH = h - textZonePx). Bars span the full width.
699+ // The inner-rotated-Group renderer pattern consumes this so its
700+ // geometry stays rotation-independent.
701+ const uprightView = {
702+ w : upright . w ,
703+ h : upright . h ,
704+ barLeftPx : 0 ,
705+ barTopPx : isTextAbove && textZonePx > 0 ? textZonePx : 0 ,
706+ barW : upright . w ,
707+ barH : textZonePx > 0 ? upright . h - textZonePx : upright . h ,
708+ } ;
709+
710+ return { w, h, barW, barH, barLeftPx, barTopPx, upright : uprightView , bitmapCrop } ;
761711}
762712
763713function getUprightDisplaySize (
0 commit comments