/** * Barcode / QR / DataMatrix rasteriser for the Label Designer. * * Wraps `bwip-js` (1D symbologies + QR + DataMatrix). Renders a code element to * a PNG data URL via an offscreen canvas — the SAME output is used by the * on-screen SVG (``) and the PDF compiler (`doc.addImage`), so screen and * print never diverge. Client-only: bwip-js is dynamically imported behind an * `import.meta.client` guard and never touches the SSR bundle. * * Results are memoised by their visual inputs (value + symbology + colours + * dpi), so panning/selecting doesn't re-rasterise. */ import type { LdBarcodeElement, LdQrElement, LdDataMatrixElement } from './useLabelDesign' export type LdCodeElement = LdBarcodeElement | LdQrElement | LdDataMatrixElement export interface BarcodeRaster { dataUrl: string; wPx: number; hPx: number } // 1×1 transparent PNG — SSR / pre-render / error fallback. const TRANSPARENT_PX = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==' let _bwip: any = null const _cache = new Map() const stripHash = (c?: string) => (c || '').replace('#', '').slice(0, 8) const SYMBOLOGY_BCID: Record = { code128: 'code128', code39: 'code39', ean13: 'ean13', ean8: 'ean8', upca: 'upca', itf14: 'itf14', 'gs1-128': 'gs1-128' } export function bcidFor (el: LdCodeElement): string { if (el.type === 'qr') return 'qrcode' if (el.type === 'datamatrix') return 'datamatrix' return SYMBOLOGY_BCID[(el as LdBarcodeElement).symbology] || 'code128' } /** True for square 2D codes — renderers should preserve aspect (letterbox) rather than stretch. */ export function isSquareCode (el: LdCodeElement): boolean { return el.type === 'qr' || el.type === 'datamatrix' } const cacheKey = (el: LdCodeElement, dpi: number): string => { const parts: any[] = [el.type, bcidFor(el), el.value, el.barColor, el.bgColor, dpi] if (el.type === 'barcode') parts.push((el as LdBarcodeElement).showText ? 1 : 0) if (el.type === 'qr') parts.push((el as LdQrElement).eccLevel) return parts.join('|') } async function getBwip (): Promise { if (_bwip) return _bwip // Explicit browser entry: exports `toCanvas` as a named export and never // pulls the Node `canvas` build. (The bare 'bwip-js' specifier resolves to // the Node build under SSR, which has no toCanvas.) const mod: any = await import('bwip-js/browser') _bwip = (mod && typeof mod.toCanvas === 'function') ? mod : (mod?.default && typeof mod.default.toCanvas === 'function') ? mod.default : (mod?.default ?? mod) return _bwip } export function useBarcode () { /** * Rasterise a code element. Returns null on failure (e.g. invalid data for * the chosen symbology) so callers can show an "invalid" placeholder. */ async function render (el: LdCodeElement, opts: { dpi?: number } = {}): Promise { if (!import.meta.client) return null const value = String(el.value ?? '') if (!value) return null const dpi = opts.dpi ?? 300 const key = cacheKey(el, dpi) const cached = _cache.get(key) if (cached) return cached try { const bwip = await getBwip() const canvas = document.createElement('canvas') // device pixels per module — scaled to the requested dpi for crisp print. const scale = Math.min(8, Math.max(2, Math.round(dpi / 96) + 1)) const bwipOpts: any = { bcid: bcidFor(el), text: value, scale, // quiet zones left at bwip-js defaults — needed for reliable scanning; // fine-tune per symbology/printer during real-label testing. barcolor: stripHash(el.barColor) || '000000' } if (el.bgColor && el.bgColor !== 'transparent') { bwipOpts.backgroundcolor = stripHash(el.bgColor) } if (el.type === 'barcode') { bwipOpts.height = 10 // mm — fixed aspect; the box scales it on screen/PDF if ((el as LdBarcodeElement).showText) { bwipOpts.includetext = true bwipOpts.textxalign = 'center' } } else if (el.type === 'qr') { bwipOpts.eclevel = (el as LdQrElement).eccLevel || 'M' } bwip.toCanvas(canvas, bwipOpts) const raster: BarcodeRaster = { dataUrl: canvas.toDataURL('image/png'), wPx: canvas.width || 1, hPx: canvas.height || 1 } _cache.set(key, raster) return raster } catch (err) { console.warn('[label-designer] barcode render failed:', err) return null } } return { render, bcidFor, isSquareCode, TRANSPARENT_PX } }