// 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 ''
}
}