// One place that knows how to draw a barcode or a QR code for a label. // // Seven views were generating codes - three in core, four in plugins - each // importing qrcode/jsbarcode directly and each carrying its own answer to the // same questions: what margin, what width, which error correction, how big a // module has to be before a scanner can read it. The answers had already // drifted (margin 0 in one place and 2 in another, width 150 against 160), and // none of the label pages knew the printer-resolution arithmetic the Tech Tools // generator worked out. // // So the knowledge lives here and the views pass what they need. Nothing about // a specific label belongs in this file; nothing about how to render a code // belongs anywhere else. // // WHY SVG, MOSTLY. A code renders to an SVG data URI rather than a canvas or a // PNG for two reasons found the hard way on the label pages: an prints // reliably where a live canvas or inline SVG does not, and SVG rasterises at // the printer's resolution with hard module edges. A PNG gets downscaled to the // label size and smears exactly the edges a scanner reads. The one exception is // a QR with a logo composited into it, which needs a canvas to composite on. import QRCode from 'qrcode' import JsBarcode from 'jsbarcode' import { getQrLogo } from '@/utils/siteSettings' // Label stock actually loaded in the printers, with the code size, padding and // quiet zone that fit each one. Sizes are inches, because that is what the // stock is sold as and what @page takes. export const LABEL_PRESETS = [ { id: 'zebra1x05', name: '1.00 x 0.50 in (Zebra gap)', labelwidth: 1.0, labelheight: 0.5, codesize: 0.4286, padding: 0, quiet: 0.035, labelfont: 7 }, { id: 'zebra2x1', name: '2.00 x 1.00 in', labelwidth: 2.0, labelheight: 1.0, codesize: 0.85, padding: 0.03, quiet: 0.05, labelfont: 10 }, { id: 'zebra225x125', name: '2.25 x 1.25 in', labelwidth: 2.25, labelheight: 1.25, codesize: 1.05, padding: 0.04, quiet: 0.06, labelfont: 11 }, { id: 'zebra4x6', name: '4.00 x 6.00 in (shipping)', labelwidth: 4.0, labelheight: 6.0, codesize: 3.0, padding: 0.15, quiet: 0.12, labelfont: 20 }, { id: 'badge', name: '2.13 x 3.38 in (badge)', labelwidth: 2.13, labelheight: 3.38, codesize: 1.5, padding: 0.15, quiet: 0.1, labelfont: 12 }, ] // CODE128 is what the scanners on the floor are configured for, and a label // carries its text separately, so the barcode never renders its own. export const BARCODE_DEFAULTS = { format: 'CODE128', displayValue: false, margin: 0, width: 2, height: 100, } export function svgDataUri(svg) { return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg) } /** * Modules across a QR for this content and error correction, quiet zone * excluded. 0 when the content cannot be encoded at all. */ export function qrModuleCount(text, errorCorrection = 'M') { if (!text) return 0 try { return QRCode.create(text, { errorCorrectionLevel: errorCorrection }).modules.size } catch { return 0 } } /** * Whether a QR of `sizeInches` can actually be printed at `dpi`. * * A scanner reads modules, not inches. Below about two printer dots per module * the edges blur into each other and the code stops scanning reliably - and a * fractional dots-per-module means some modules are a dot wider than others, * which is the difference between a label that always works and one that works * on most printers. `snapped` is the nearest size that divides evenly. */ export function qrFit(text, { sizeInches, dpi = 203, errorCorrection = 'M' } = {}) { const modules = qrModuleCount(text, errorCorrection) if (!modules || !sizeInches) return null const dotsPerModule = (sizeInches * dpi) / modules const whole = Math.floor(dotsPerModule) return { modules, dotsPerModule, wholeDots: whole, snapped: whole > 0 ? (modules * whole) / dpi : 0, moduleMm: whole > 0 ? (whole / dpi) * 25.4 : 0, // Below two dots per module the code is not reliably scannable, whatever // it looks like on screen. tooSmall: whole < 2, even: whole > 0 && Math.abs((modules * whole) / dpi - sizeInches) < 0.002, } } /** * A QR as an SVG data URI. * * margin 0 by default: on a label the quiet zone is blank label supplied by the * layout, so spending the code box on white the page cannot then adjust makes * the printed code smaller for no benefit. A caller rendering to a screen, or * onto something with no controlled surround, should pass a margin. */ export async function qrSvgDataUri(text, { errorCorrection = 'M', margin = 0 } = {}) { const svg = await QRCode.toString(text, { type: 'svg', errorCorrectionLevel: errorCorrection, margin, }) return svgDataUri(svg) } /** * A QR as a PNG data URL. Use only where a raster is genuinely needed - a * composited logo, or an existing page built around . */ export async function qrPngDataUrl(text, { width = 160, margin = 0, errorCorrection = 'M' } = {}) { return QRCode.toDataURL(text, { width, margin, errorCorrectionLevel: errorCorrection }) } /** * Render a barcode into an existing SVG or canvas element. * * Kept as a thin pass-through because several pages hold an element ref and * want JsBarcode to draw straight into it; the value here is the shared * defaults, not the indirection. */ export function barcodeInto(element, text, options = {}) { JsBarcode(element, text, { ...BARCODE_DEFAULTS, ...options }) return element } /** A barcode as an SVG data URI, for pages that print an . */ export function barcodeSvgDataUri(text, options = {}) { const element = document.createElementNS('http://www.w3.org/2000/svg', 'svg') barcodeInto(element, text, options) return svgDataUri(new XMLSerializer().serializeToString(element)) } // -- QR with a logo composited in the middle --------------------------------- // // Moved here from views/print/qrLogo.js, which core label pages and the printer // QR pages both reached into. The overlay image is a site setting; the monogram // below is the last-resort fallback when that setting is empty or unreachable, // so a label still carries a mark rather than a hole. const FALLBACK_LOGO_SVG = `` let logoImage = null function loadLogo(url) { if (logoImage) return Promise.resolve(logoImage) return new Promise(resolve => { const img = new Image() img.onload = () => { logoImage = img; resolve(img) } img.onerror = () => { const fallback = new Image() fallback.onload = () => { logoImage = fallback; resolve(fallback) } fallback.onerror = () => resolve(null) fallback.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(FALLBACK_LOGO_SVG) } img.src = url }) } function drawLogoOverlay(canvas, logo) { const canvasContext = canvas.getContext('2d') const size = canvas.width const logoSize = Math.round(size * 0.22) const x = (size - logoSize) / 2 const y = (size - logoSize) / 2 // White circle behind the mark, so it sits on the code rather than in it. canvasContext.beginPath() canvasContext.arc(size / 2, size / 2, logoSize / 2 + 4, 0, Math.PI * 2) canvasContext.fillStyle = '#fff' canvasContext.fill() if (logo) { canvasContext.drawImage(logo, x, y, logoSize, logoSize) } } /** * A QR with the configured logo composited in the centre, as a PNG data URL. * * Error correction H, because up to 30% of the code can be obscured and still * decode - which is what makes covering the middle of it survivable at all. * Empty qr_logo setting means no overlay. Returns '' on failure: a label with * no code is better than a page that throws while printing a batch. */ export async function renderQrDataUrl(url, { width = 144, margin = 0 } = {}) { const qrLogoUrl = await getQrLogo() const canvas = document.createElement('canvas') try { await QRCode.toCanvas(canvas, url, { width, margin, errorCorrectionLevel: 'H' }) if (qrLogoUrl) { const logo = await loadLogo(qrLogoUrl) drawLogoOverlay(canvas, logo) } return canvas.toDataURL('image/png') } catch (err) { console.error('QR error:', err) return '' } }