labels: one module knows how to draw a code, seven views stop guessing
Three core pages and four plugin pages each imported qrcode and jsbarcode directly, and each carried its own answer to the same questions: what margin, what width, which error correction, how big a module must 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 on a label that is the difference between a sticker that scans and one that does not. frontend/src/utils/codes.js owns it now: the label-stock presets, the quiet-zone and margin defaults, CODE128 with no printed value, and the printer-resolution arithmetic that only the Tech Tools generator had. A view passes what is specific to its own label and nothing else - MachineBadge still asks for CODE39, because the badge readers predate the shop-floor scanners and decode nothing else, and that is exactly the kind of thing a call site should say out loud. views/print/qrLogo.js is folded in rather than left as a second half-shared helper that only some of the pages reached into. The check script now fails a build that imports either library outside that module. Without it this re-forks within a month: the next label page starts by copying the nearest existing one, which is how it happened the first time. Tests cover the part no amount of looking at a screen verifies - a QR that looks fine at 96 dpi on a monitor can be unreadable at 203 dpi on half-inch stock.
This commit is contained in:
202
frontend/src/utils/codes.js
Normal file
202
frontend/src/utils/codes.js
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
// 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 <img> 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 <img src=png>.
|
||||||
|
*/
|
||||||
|
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 <img>. */
|
||||||
|
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32.5 32"><path d="M19.8915 11.8362C19.8915 10.0196 21.1404 8.25119 21.826 8.5888C22.6014 8.97061 21.2424 10.6868 19.8915 11.8362ZM11.3823 12.4994C11.3823 11.0364 12.8475 8.25521 13.7453 8.54861C14.8023 8.89425 12.8679 11.6996 11.3823 12.4994ZM9.89679 22.9611C9.2234 22.9932 8.77447 22.5672 8.77447 21.8558C8.77447 19.9508 11.4558 18.1301 13.4841 17.1535C13.125 19.8141 12.2108 22.8525 9.90087 22.957M22.279 16.7516C20.7486 16.7516 19.5773 17.8608 19.5773 19.1912C19.5773 20.3004 20.2507 21.1846 21.1526 21.1846C21.4668 21.1846 21.7811 21.0078 21.7811 20.6099C21.7811 20.0352 21.0057 19.8945 21.0669 19.0304C21.1036 18.4637 21.6505 18.0819 22.1892 18.0819C23.2707 18.0819 23.7768 19.1148 23.7768 20.1758C23.7319 21.8156 22.5075 22.957 21.0669 22.957C19.1773 22.957 17.9611 21.1846 17.9611 19.2756C17.9611 16.4381 19.8507 15.3328 20.8424 15.0676C20.8547 15.0676 23.4299 15.5217 23.3483 14.4004C23.3156 13.9101 22.5688 13.7212 22.03 13.6971C21.4301 13.6729 20.8302 13.886 20.8302 13.886C20.5159 13.7292 20.2996 13.4238 20.165 13.0701C22.0096 11.6956 23.3156 10.3652 23.3156 8.85808C23.3156 8.0623 22.7769 7.35092 21.7403 7.35092C19.8956 7.35092 18.4998 9.65386 18.4998 11.7398C18.4998 12.0934 18.4998 12.4511 18.5896 12.7606C17.4183 13.6046 16.5491 14.1271 14.9737 15.0555C14.9737 14.8626 15.0145 14.3602 15.1492 13.7131C15.6879 13.1384 16.4307 12.2743 16.4307 11.6112C16.4307 11.3017 16.2511 11.0364 15.892 11.0364C14.9941 11.0364 14.3167 12.3667 14.1371 13.2952C13.7331 13.7815 12.9209 14.4044 12.2475 14.4044C11.7088 14.4044 11.5292 13.9141 11.4803 13.7413C13.1903 13.1625 15.3084 10.8596 15.3084 8.77769C15.3084 8.33559 15.1288 7.35895 13.778 7.35895C11.7537 7.35895 10.0437 10.3291 10.0437 12.632C9.32134 12.632 9.05607 11.8764 9.05607 11.3017C9.05607 10.727 9.28053 10.1482 9.28053 9.97136C9.28053 9.79452 9.19075 9.57347 8.92139 9.57347C8.248 9.57347 7.83989 10.4617 7.83989 11.4785C7.88478 12.8973 8.83161 13.7855 10.0886 13.8739C10.2682 14.7179 11.0354 15.5137 11.9782 15.5137C12.5659 15.5137 13.2841 15.3369 13.778 14.8947C13.7331 15.2042 13.6882 15.4695 13.6433 15.7388C11.6639 16.7596 10.2233 17.467 8.91731 18.6204C7.88478 19.5529 7.29709 20.7908 7.29709 21.7674C7.29709 23.0977 8.15005 24.3356 9.90903 24.3356C11.9782 24.3356 13.5535 22.6958 14.3208 20.4371C14.6799 19.372 14.8268 17.8247 14.9166 16.4059C16.9857 15.2565 17.9693 14.5853 19.0467 13.8337C19.1814 14.0548 19.3202 14.2316 19.4956 14.3642C18.5529 14.8505 16.3001 16.2251 16.3001 19.4604C16.3001 21.7674 17.8754 24.3356 20.9812 24.3356C23.5482 24.3356 25.3031 22.2537 25.3031 20.2602C25.3031 18.4436 24.2665 16.7596 22.2872 16.7596M30.025 20.5657C30.025 20.5657 29.9924 20.6019 29.9434 20.5818C29.9067 20.5697 29.8944 20.5496 29.8944 20.5255C29.8944 20.4974 30.4372 18.9219 30.4331 17.1133C30.429 15.164 29.621 13.9663 28.5884 13.9663C27.96 13.9663 27.5069 14.4084 27.5069 15.0756C27.5069 16.2733 28.9925 16.3617 28.9925 18.9781C28.9925 20.0432 28.768 21.06 28.4089 22.1693C26.7438 27.7076 21.4301 30.2798 16.2593 30.2798C13.8718 30.2798 12.1781 29.7975 11.6721 29.5765C11.6517 29.5684 11.6354 29.5283 11.6517 29.4881C11.6639 29.4559 11.6966 29.4358 11.717 29.4439C11.921 29.5242 13.378 29.9744 15.1778 29.9744C17.1571 29.9744 18.3284 29.1786 18.3284 28.202C18.3284 27.583 17.8346 27.0967 17.202 27.0967C15.9859 27.0967 15.8961 28.6039 13.2882 28.6039C12.1618 28.6039 11.1742 28.3828 10.0029 28.0291C4.41988 26.3451 1.76306 21.1605 1.76714 16.0161C1.76714 13.5122 2.48134 11.5187 2.49358 11.4986C2.50174 11.4866 2.53439 11.4705 2.5752 11.4866C2.61602 11.4986 2.62418 11.5348 2.62418 11.5428C2.55888 11.7518 2.08547 13.1786 2.08547 14.951C2.08547 16.9003 2.89353 18.0538 3.93015 18.0538C4.51783 18.0538 5.01165 17.6117 5.01165 16.9887C5.01165 15.791 3.52611 15.6584 3.52611 13.0862C3.52611 11.9769 3.75058 11.0043 4.10972 9.85079C5.80747 4.34464 11.0722 1.7684 16.2471 1.72821C18.6509 1.70811 20.7567 2.41949 20.8383 2.47978C20.8506 2.49184 20.8669 2.52399 20.8506 2.56016C20.8343 2.60035 20.8057 2.60839 20.7935 2.60437C20.769 2.60437 19.3977 2.03768 17.3286 2.03768C15.3941 2.03768 14.1779 2.83346 14.1779 3.85431C14.1779 4.42904 14.6268 4.91535 15.3043 4.91535C16.5205 4.91535 16.6103 3.4524 19.2181 3.4524C20.3445 3.4524 21.3322 3.67345 22.5035 4.02713C28.1314 5.71113 30.6902 10.94 30.7392 15.996C30.7637 18.5843 30.025 20.5456 30.0169 20.5576M16.2471 0.75157C7.69705 0.75157 0.763175 7.58001 0.763175 16C0.763175 24.42 7.69705 31.2444 16.2471 31.2444C24.7971 31.2444 31.7269 24.42 31.7269 16C31.7269 7.58001 24.7971 0.75157 16.2471 0.75157ZM16.2471 32C7.28893 32 0 24.8661 0 16C0 7.13389 7.28893 0 16.2471 0C25.2052 0 32.4941 7.18212 32.4941 16C32.4941 24.8179 25.2011 32 16.2471 32Z" fill="black"/></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 ''
|
||||||
|
}
|
||||||
|
}
|
||||||
93
frontend/src/utils/codes.spec.js
Normal file
93
frontend/src/utils/codes.spec.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// The arithmetic in codes.js decides whether a printed label scans, and it is
|
||||||
|
// the part no amount of looking at a screen will verify - a QR that is fine at
|
||||||
|
// 96 dpi on a monitor can be unreadable at 203 dpi on 0.5in stock.
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
|
||||||
|
import {
|
||||||
|
LABEL_PRESETS, BARCODE_DEFAULTS, qrModuleCount, qrFit, svgDataUri,
|
||||||
|
} from './codes'
|
||||||
|
|
||||||
|
describe('qrModuleCount', () => {
|
||||||
|
it('grows with the content', () => {
|
||||||
|
const short = qrModuleCount('AP-01')
|
||||||
|
const long = qrModuleCount('AP-01/'.repeat(20))
|
||||||
|
expect(short).toBeGreaterThan(0)
|
||||||
|
expect(long).toBeGreaterThan(short)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('grows with error correction, because the redundancy is stored in modules', () => {
|
||||||
|
expect(qrModuleCount('WKSTN0042', 'H')).toBeGreaterThanOrEqual(
|
||||||
|
qrModuleCount('WKSTN0042', 'L'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 0 rather than throwing on empty content', () => {
|
||||||
|
expect(qrModuleCount('')).toBe(0)
|
||||||
|
expect(qrModuleCount(null)).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('qrFit', () => {
|
||||||
|
it('calls a code too small when a module gets under two printer dots', () => {
|
||||||
|
// A long payload squeezed onto the 1.00 x 0.50in stock: the case the Tech
|
||||||
|
// Tools generator exists to warn about before 300 labels are printed.
|
||||||
|
const fit = qrFit('https://shopdb.example.net/assets/1234?from=label&sig=abcdef123456', {
|
||||||
|
sizeInches: 0.4286, dpi: 203, errorCorrection: 'H',
|
||||||
|
})
|
||||||
|
expect(fit.tooSmall).toBe(true)
|
||||||
|
expect(fit.dotsPerModule).toBeLessThan(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports a whole number of dots per module as even', () => {
|
||||||
|
const modules = qrModuleCount('WKSTN0042', 'M')
|
||||||
|
// Choose the size that divides exactly at 4 dots per module.
|
||||||
|
const fit = qrFit('WKSTN0042', {
|
||||||
|
sizeInches: (modules * 4) / 203, dpi: 203, errorCorrection: 'M',
|
||||||
|
})
|
||||||
|
expect(fit.wholeDots).toBe(4)
|
||||||
|
expect(fit.even).toBe(true)
|
||||||
|
expect(fit.snapped).toBeCloseTo(fit.snapped, 5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('offers the nearest evenly-dividing size when the chosen one is uneven', () => {
|
||||||
|
const fit = qrFit('WKSTN0042', { sizeInches: 0.9, dpi: 203, errorCorrection: 'M' })
|
||||||
|
expect(fit.even).toBe(false)
|
||||||
|
expect(fit.snapped).toBeLessThanOrEqual(0.9)
|
||||||
|
expect(fit.snapped * 203 / fit.modules).toBeCloseTo(fit.wholeDots, 6)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is null when there is nothing to measure', () => {
|
||||||
|
expect(qrFit('', { sizeInches: 1 })).toBeNull()
|
||||||
|
expect(qrFit('WKSTN0042', {})).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('shared defaults', () => {
|
||||||
|
it('barcodes are CODE128 with no margin and no printed text', () => {
|
||||||
|
// The label draws its own caption, and the quiet zone is blank label. Both
|
||||||
|
// had drifted per-view before this module existed.
|
||||||
|
expect(BARCODE_DEFAULTS.format).toBe('CODE128')
|
||||||
|
expect(BARCODE_DEFAULTS.margin).toBe(0)
|
||||||
|
expect(BARCODE_DEFAULTS.displayValue).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('every label preset is a complete, usable stock definition', () => {
|
||||||
|
expect(LABEL_PRESETS.length).toBeGreaterThan(0)
|
||||||
|
for (const preset of LABEL_PRESETS) {
|
||||||
|
expect(preset.id).toBeTruthy()
|
||||||
|
expect(preset.labelwidth).toBeGreaterThan(0)
|
||||||
|
expect(preset.labelheight).toBeGreaterThan(0)
|
||||||
|
// A code has to fit inside its label with its quiet zone on both sides.
|
||||||
|
expect(preset.codesize + 2 * preset.quiet).toBeLessThanOrEqual(preset.labelwidth)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('svgDataUri', () => {
|
||||||
|
it('escapes markup so the URI survives being put in a src attribute', () => {
|
||||||
|
const uri = svgDataUri('<svg><rect fill="#000"/></svg>')
|
||||||
|
expect(uri.startsWith('data:image/svg+xml;charset=utf-8,')).toBe(true)
|
||||||
|
expect(uri).not.toContain('<')
|
||||||
|
expect(uri).not.toContain('"')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -128,8 +128,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import JsBarcode from 'jsbarcode'
|
import { renderQrDataUrl, barcodeInto } from '@/utils/codes'
|
||||||
import { renderQrDataUrl } from './qrLogo'
|
|
||||||
import { getSetting } from '@/utils/siteSettings'
|
import { getSetting } from '@/utils/siteSettings'
|
||||||
import { withBase } from '@/utils/basePath'
|
import { withBase } from '@/utils/basePath'
|
||||||
import {
|
import {
|
||||||
@@ -208,9 +207,7 @@ async function renderCode() {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
if (!barcodeEl.value) return
|
if (!barcodeEl.value) return
|
||||||
try {
|
try {
|
||||||
JsBarcode(barcodeEl.value, text, {
|
barcodeInto(barcodeEl.value, text, { height: 70 })
|
||||||
format: 'CODE128', displayValue: false, width: 2, height: 70, margin: 0,
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Barcode error:', err)
|
console.error('Barcode error:', err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,8 +140,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import JsBarcode from 'jsbarcode'
|
import { renderQrDataUrl, barcodeInto } from '@/utils/codes'
|
||||||
import { renderQrDataUrl } from './qrLogo'
|
|
||||||
import { getSetting } from '@/utils/siteSettings'
|
import { getSetting } from '@/utils/siteSettings'
|
||||||
import {
|
import {
|
||||||
TYPE_CONFIG, hasLocationType, resolveDefaultEncodes,
|
TYPE_CONFIG, hasLocationType, resolveDefaultEncodes,
|
||||||
@@ -273,10 +272,7 @@ function renderBarcodes() {
|
|||||||
const text = codeMap.value[asset.assetid]
|
const text = codeMap.value[asset.assetid]
|
||||||
if (!el || !text) continue
|
if (!el || !text) continue
|
||||||
try {
|
try {
|
||||||
JsBarcode(el, text, {
|
barcodeInto(el, text, { width, height, background: 'transparent' })
|
||||||
format: 'CODE128', displayValue: false, width, height, margin: 0,
|
|
||||||
background: 'transparent',
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Barcode error:', asset.assetnumber, err)
|
console.error('Barcode error:', asset.assetnumber, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { useRoute } from 'vue-router'
|
|||||||
import { machinesApi } from '../../api'
|
import { machinesApi } from '../../api'
|
||||||
import { getBadgeLogo } from '@/utils/siteSettings'
|
import { getBadgeLogo } from '@/utils/siteSettings'
|
||||||
import { withBase } from '@/utils/basePath'
|
import { withBase } from '@/utils/basePath'
|
||||||
import JsBarcode from 'jsbarcode'
|
import { barcodeInto } from '@/utils/codes'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -75,12 +75,10 @@ onMounted(async () => {
|
|||||||
function generateBarcode() {
|
function generateBarcode() {
|
||||||
if (!barcodeEl.value || !machine.value) return
|
if (!barcodeEl.value || !machine.value) return
|
||||||
try {
|
try {
|
||||||
JsBarcode(barcodeEl.value, machine.value.assetnumber, {
|
// CODE39, not the CODE128 default: the badge readers predate the
|
||||||
format: 'CODE39',
|
// shop-floor scanners and only decode CODE39.
|
||||||
displayValue: false,
|
barcodeInto(barcodeEl.value, machine.value.assetnumber, {
|
||||||
width: 2,
|
format: 'CODE39', height: 70,
|
||||||
height: 70,
|
|
||||||
margin: 0
|
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Barcode generation error:', e)
|
console.error('Barcode generation error:', e)
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
// Shared GE monogram + QR-with-logo rendering for the printer QR label pages.
|
|
||||||
// Both PrinterQRBatch and PrinterQRSingle render each QR to a data-URL image
|
|
||||||
// (canvases print unreliably) with the GE monogram composited in the center.
|
|
||||||
import QRCode from 'qrcode'
|
|
||||||
import { getQrLogo } from '@/utils/siteSettings'
|
|
||||||
|
|
||||||
const GE_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32.5 32"><path d="M19.8915 11.8362C19.8915 10.0196 21.1404 8.25119 21.826 8.5888C22.6014 8.97061 21.2424 10.6868 19.8915 11.8362ZM11.3823 12.4994C11.3823 11.0364 12.8475 8.25521 13.7453 8.54861C14.8023 8.89425 12.8679 11.6996 11.3823 12.4994ZM9.89679 22.9611C9.2234 22.9932 8.77447 22.5672 8.77447 21.8558C8.77447 19.9508 11.4558 18.1301 13.4841 17.1535C13.125 19.8141 12.2108 22.8525 9.90087 22.957M22.279 16.7516C20.7486 16.7516 19.5773 17.8608 19.5773 19.1912C19.5773 20.3004 20.2507 21.1846 21.1526 21.1846C21.4668 21.1846 21.7811 21.0078 21.7811 20.6099C21.7811 20.0352 21.0057 19.8945 21.0669 19.0304C21.1036 18.4637 21.6505 18.0819 22.1892 18.0819C23.2707 18.0819 23.7768 19.1148 23.7768 20.1758C23.7319 21.8156 22.5075 22.957 21.0669 22.957C19.1773 22.957 17.9611 21.1846 17.9611 19.2756C17.9611 16.4381 19.8507 15.3328 20.8424 15.0676C20.8547 15.0676 23.4299 15.5217 23.3483 14.4004C23.3156 13.9101 22.5688 13.7212 22.03 13.6971C21.4301 13.6729 20.8302 13.886 20.8302 13.886C20.5159 13.7292 20.2996 13.4238 20.165 13.0701C22.0096 11.6956 23.3156 10.3652 23.3156 8.85808C23.3156 8.0623 22.7769 7.35092 21.7403 7.35092C19.8956 7.35092 18.4998 9.65386 18.4998 11.7398C18.4998 12.0934 18.4998 12.4511 18.5896 12.7606C17.4183 13.6046 16.5491 14.1271 14.9737 15.0555C14.9737 14.8626 15.0145 14.3602 15.1492 13.7131C15.6879 13.1384 16.4307 12.2743 16.4307 11.6112C16.4307 11.3017 16.2511 11.0364 15.892 11.0364C14.9941 11.0364 14.3167 12.3667 14.1371 13.2952C13.7331 13.7815 12.9209 14.4044 12.2475 14.4044C11.7088 14.4044 11.5292 13.9141 11.4803 13.7413C13.1903 13.1625 15.3084 10.8596 15.3084 8.77769C15.3084 8.33559 15.1288 7.35895 13.778 7.35895C11.7537 7.35895 10.0437 10.3291 10.0437 12.632C9.32134 12.632 9.05607 11.8764 9.05607 11.3017C9.05607 10.727 9.28053 10.1482 9.28053 9.97136C9.28053 9.79452 9.19075 9.57347 8.92139 9.57347C8.248 9.57347 7.83989 10.4617 7.83989 11.4785C7.88478 12.8973 8.83161 13.7855 10.0886 13.8739C10.2682 14.7179 11.0354 15.5137 11.9782 15.5137C12.5659 15.5137 13.2841 15.3369 13.778 14.8947C13.7331 15.2042 13.6882 15.4695 13.6433 15.7388C11.6639 16.7596 10.2233 17.467 8.91731 18.6204C7.88478 19.5529 7.29709 20.7908 7.29709 21.7674C7.29709 23.0977 8.15005 24.3356 9.90903 24.3356C11.9782 24.3356 13.5535 22.6958 14.3208 20.4371C14.6799 19.372 14.8268 17.8247 14.9166 16.4059C16.9857 15.2565 17.9693 14.5853 19.0467 13.8337C19.1814 14.0548 19.3202 14.2316 19.4956 14.3642C18.5529 14.8505 16.3001 16.2251 16.3001 19.4604C16.3001 21.7674 17.8754 24.3356 20.9812 24.3356C23.5482 24.3356 25.3031 22.2537 25.3031 20.2602C25.3031 18.4436 24.2665 16.7596 22.2872 16.7596M30.025 20.5657C30.025 20.5657 29.9924 20.6019 29.9434 20.5818C29.9067 20.5697 29.8944 20.5496 29.8944 20.5255C29.8944 20.4974 30.4372 18.9219 30.4331 17.1133C30.429 15.164 29.621 13.9663 28.5884 13.9663C27.96 13.9663 27.5069 14.4084 27.5069 15.0756C27.5069 16.2733 28.9925 16.3617 28.9925 18.9781C28.9925 20.0432 28.768 21.06 28.4089 22.1693C26.7438 27.7076 21.4301 30.2798 16.2593 30.2798C13.8718 30.2798 12.1781 29.7975 11.6721 29.5765C11.6517 29.5684 11.6354 29.5283 11.6517 29.4881C11.6639 29.4559 11.6966 29.4358 11.717 29.4439C11.921 29.5242 13.378 29.9744 15.1778 29.9744C17.1571 29.9744 18.3284 29.1786 18.3284 28.202C18.3284 27.583 17.8346 27.0967 17.202 27.0967C15.9859 27.0967 15.8961 28.6039 13.2882 28.6039C12.1618 28.6039 11.1742 28.3828 10.0029 28.0291C4.41988 26.3451 1.76306 21.1605 1.76714 16.0161C1.76714 13.5122 2.48134 11.5187 2.49358 11.4986C2.50174 11.4866 2.53439 11.4705 2.5752 11.4866C2.61602 11.4986 2.62418 11.5348 2.62418 11.5428C2.55888 11.7518 2.08547 13.1786 2.08547 14.951C2.08547 16.9003 2.89353 18.0538 3.93015 18.0538C4.51783 18.0538 5.01165 17.6117 5.01165 16.9887C5.01165 15.791 3.52611 15.6584 3.52611 13.0862C3.52611 11.9769 3.75058 11.0043 4.10972 9.85079C5.80747 4.34464 11.0722 1.7684 16.2471 1.72821C18.6509 1.70811 20.7567 2.41949 20.8383 2.47978C20.8506 2.49184 20.8669 2.52399 20.8506 2.56016C20.8343 2.60035 20.8057 2.60839 20.7935 2.60437C20.769 2.60437 19.3977 2.03768 17.3286 2.03768C15.3941 2.03768 14.1779 2.83346 14.1779 3.85431C14.1779 4.42904 14.6268 4.91535 15.3043 4.91535C16.5205 4.91535 16.6103 3.4524 19.2181 3.4524C20.3445 3.4524 21.3322 3.67345 22.5035 4.02713C28.1314 5.71113 30.6902 10.94 30.7392 15.996C30.7637 18.5843 30.025 20.5456 30.0169 20.5576M16.2471 0.75157C7.69705 0.75157 0.763175 7.58001 0.763175 16C0.763175 24.42 7.69705 31.2444 16.2471 31.2444C24.7971 31.2444 31.7269 24.42 31.7269 16C31.7269 7.58001 24.7971 0.75157 16.2471 0.75157ZM16.2471 32C7.28893 32 0 24.8661 0 16C0 7.13389 7.28893 0 16.2471 0C25.2052 0 32.4941 7.18212 32.4941 16C32.4941 24.8179 25.2011 32 16.2471 32Z" fill="black"/></svg>`
|
|
||||||
|
|
||||||
let logoImage = null
|
|
||||||
|
|
||||||
// Load the configured QR overlay image. On any load failure, fall back to the
|
|
||||||
// built-in GE monogram (last-resort constant above).
|
|
||||||
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(GE_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 background behind the monogram
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render a QR for `url` to a PNG data URL with the configured logo composited
|
|
||||||
// in the center. Empty qr_logo setting = no overlay. Returns '' on failure.
|
|
||||||
export async function renderQrDataUrl(url) {
|
|
||||||
const qrLogoUrl = await getQrLogo()
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
try {
|
|
||||||
await QRCode.toCanvas(canvas, url, { width: 144, margin: 0, 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 ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import QRCode from 'qrcode'
|
import { qrPngDataUrl } from '@/utils/codes'
|
||||||
import { printedpartsApi } from '@/api'
|
import { printedpartsApi } from '@/api'
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
@@ -123,9 +123,7 @@ watch(printLabels, async labels => {
|
|||||||
await Promise.all(labels.map(async (label, index) => {
|
await Promise.all(labels.map(async (label, index) => {
|
||||||
// margin 2 = quiet zone (scannability); EC 'M' keeps modules big for the
|
// margin 2 = quiet zone (scannability); EC 'M' keeps modules big for the
|
||||||
// short payload on tiny media; no logo overlay on a 0.36in code.
|
// short payload on tiny media; no logo overlay on a 0.36in code.
|
||||||
urls[index] = await QRCode.toDataURL(labelPayload(label), {
|
urls[index] = await qrPngDataUrl(labelPayload(label), { margin: 2, width: 160 })
|
||||||
margin: 2, errorCorrectionLevel: 'M', width: 160
|
|
||||||
})
|
|
||||||
}))
|
}))
|
||||||
qrDataUrls.value = urls
|
qrDataUrls.value = urls
|
||||||
}, { deep: true, immediate: true })
|
}, { deep: true, immediate: true })
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||||
import { printersApi } from '@/api'
|
import { printersApi } from '@/api'
|
||||||
import { renderQrDataUrl } from '@/views/print/qrLogo'
|
import { renderQrDataUrl } from '@/utils/codes'
|
||||||
import { buildQrUrl } from '@/utils/qrTarget'
|
import { buildQrUrl } from '@/utils/qrTarget'
|
||||||
|
|
||||||
const printers = ref([])
|
const printers = ref([])
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { printersApi } from '@/api'
|
import { printersApi } from '@/api'
|
||||||
import { renderQrDataUrl } from '@/views/print/qrLogo'
|
import { renderQrDataUrl } from '@/utils/codes'
|
||||||
import { buildQrUrl } from '@/utils/qrTarget'
|
import { buildQrUrl } from '@/utils/qrTarget'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|||||||
@@ -163,20 +163,14 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||||
import QRCode from 'qrcode'
|
import { LABEL_PRESETS, qrModuleCount, qrFit, qrSvgDataUri, barcodeSvgDataUri }
|
||||||
import JsBarcode from 'jsbarcode'
|
from '@/utils/codes'
|
||||||
|
|
||||||
// Rendering thousands of codes locks the tab, so stop at a number a person
|
// Rendering thousands of codes locks the tab, so stop at a number a person
|
||||||
// would actually feed a label printer in one run and SAY what was dropped.
|
// would actually feed a label printer in one run and SAY what was dropped.
|
||||||
const MAX_LABELS = 500
|
const MAX_LABELS = 500
|
||||||
|
|
||||||
const PRESETS = [
|
const PRESETS = 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 },
|
|
||||||
]
|
|
||||||
|
|
||||||
const source = ref('single')
|
const source = ref('single')
|
||||||
const codetype = ref('qr')
|
const codetype = ref('qr')
|
||||||
@@ -326,25 +320,20 @@ const visibleLabels = computed(() =>
|
|||||||
const qrModules = computed(() => {
|
const qrModules = computed(() => {
|
||||||
const content = labels.value[0]?.content
|
const content = labels.value[0]?.content
|
||||||
if (!content || codetype.value !== 'qr') return 0
|
if (!content || codetype.value !== 'qr') return 0
|
||||||
try {
|
return qrModuleCount(content, errorcorrection.value)
|
||||||
return QRCode.create(content, { errorCorrectionLevel: errorcorrection.value }).modules.size
|
|
||||||
} catch {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const fitAdvice = computed(() => {
|
const fitAdvice = computed(() => {
|
||||||
const modules = qrModules.value
|
const fit = qrFit(labels.value[0]?.content, {
|
||||||
if (!modules) return ''
|
sizeInches: codesize.value, dpi: dpi.value, errorCorrection: errorcorrection.value,
|
||||||
const dotsPerModule = (codesize.value * dpi.value) / modules
|
})
|
||||||
const whole = Math.floor(dotsPerModule)
|
if (!fit || codetype.value !== 'qr') return ''
|
||||||
if (whole < 2) {
|
const { modules, dotsPerModule, wholeDots: whole, snapped, moduleMm } = fit
|
||||||
|
if (fit.tooSmall) {
|
||||||
return `${modules} modules at ${dotsPerModule.toFixed(2)} dots each - too small to scan `
|
return `${modules} modules at ${dotsPerModule.toFixed(2)} dots each - too small to scan `
|
||||||
+ `reliably. Shorten the content, drop error correction, or use bigger stock.`
|
+ `reliably. Shorten the content, drop error correction, or use bigger stock.`
|
||||||
}
|
}
|
||||||
const snapped = (modules * whole) / dpi.value
|
if (fit.even) {
|
||||||
const moduleMm = (whole / dpi.value) * 25.4
|
|
||||||
if (Math.abs(snapped - codesize.value) < 0.002) {
|
|
||||||
return `${modules} modules at exactly ${whole} dots each (${moduleMm.toFixed(3)} mm). Good.`
|
return `${modules} modules at exactly ${whole} dots each (${moduleMm.toFixed(3)} mm). Good.`
|
||||||
}
|
}
|
||||||
return `${modules} modules at ${dotsPerModule.toFixed(2)} dots each. Uneven - `
|
return `${modules} modules at ${dotsPerModule.toFixed(2)} dots each. Uneven - `
|
||||||
@@ -361,30 +350,13 @@ const fitAdvice = computed(() => {
|
|||||||
//
|
//
|
||||||
// margin 0 on the QR: the quiet zone is blank label supplied by --tool-quiet,
|
// margin 0 on the QR: the quiet zone is blank label supplied by --tool-quiet,
|
||||||
// so none of the code box is spent on white we cannot then adjust.
|
// so none of the code box is spent on white we cannot then adjust.
|
||||||
function svgDataUri(svg) {
|
|
||||||
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renderOne(row) {
|
async function renderOne(row) {
|
||||||
const text = row.content
|
const text = row.content
|
||||||
if (!text) return ''
|
if (!text) return ''
|
||||||
if (codetype.value === 'qr') {
|
if (codetype.value === 'qr') {
|
||||||
const svg = await QRCode.toString(text, {
|
return qrSvgDataUri(text, { errorCorrection: errorcorrection.value })
|
||||||
type: 'svg',
|
|
||||||
errorCorrectionLevel: errorcorrection.value,
|
|
||||||
margin: 0,
|
|
||||||
})
|
|
||||||
return svgDataUri(svg)
|
|
||||||
}
|
}
|
||||||
const element = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
return barcodeSvgDataUri(text)
|
||||||
JsBarcode(element, text, {
|
|
||||||
format: 'CODE128',
|
|
||||||
displayValue: false,
|
|
||||||
margin: 0,
|
|
||||||
width: 2,
|
|
||||||
height: 100,
|
|
||||||
})
|
|
||||||
return svgDataUri(new XMLSerializer().serializeToString(element))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let renderToken = 0
|
let renderToken = 0
|
||||||
|
|||||||
@@ -83,8 +83,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||||
import { usbApi } from '@/api'
|
import { usbApi } from '@/api'
|
||||||
import JsBarcode from 'jsbarcode'
|
import { barcodeInto, qrPngDataUrl } from '@/utils/codes'
|
||||||
import QRCode from 'qrcode'
|
|
||||||
import { getSetting } from '@/utils/siteSettings'
|
import { getSetting } from '@/utils/siteSettings'
|
||||||
import { buildQrUrl } from '@/utils/qrTarget'
|
import { buildQrUrl } from '@/utils/qrTarget'
|
||||||
|
|
||||||
@@ -180,11 +179,8 @@ async function generateQrImages() {
|
|||||||
serialnumber: item.device_id || '',
|
serialnumber: item.device_id || '',
|
||||||
alias: item.device_desc || '',
|
alias: item.device_desc || '',
|
||||||
})
|
})
|
||||||
next[`${pageIdx}-${cellIdx + 1}-${miniIdx}`] = await QRCode.toDataURL(url, {
|
next[`${pageIdx}-${cellIdx + 1}-${miniIdx}`] =
|
||||||
margin: 0,
|
await qrPngDataUrl(url, { width: 150 })
|
||||||
width: 150,
|
|
||||||
errorCorrectionLevel: 'M',
|
|
||||||
})
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('QR error:', item.device_id, e)
|
console.error('QR error:', item.device_id, e)
|
||||||
}
|
}
|
||||||
@@ -210,13 +206,8 @@ function generateBarcodes() {
|
|||||||
const el = barcodeRefs.value[`${pageIdx}-${cellNum}-${miniIdx}`]
|
const el = barcodeRefs.value[`${pageIdx}-${cellNum}-${miniIdx}`]
|
||||||
if (!el) return
|
if (!el) return
|
||||||
try {
|
try {
|
||||||
JsBarcode(el, item.device_id, {
|
barcodeInto(el, item.device_id, {
|
||||||
format: 'CODE128',
|
width: 1, height: 22, background: 'transparent',
|
||||||
width: 1,
|
|
||||||
height: 22,
|
|
||||||
displayValue: false,
|
|
||||||
margin: 0,
|
|
||||||
background: 'transparent'
|
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Barcode error:', item.device_id, e)
|
console.error('Barcode error:', item.device_id, e)
|
||||||
|
|||||||
@@ -140,6 +140,24 @@ fi
|
|||||||
# cannot see and did not choose. Use a setting with a NEUTRAL default, a
|
# cannot see and did not choose. Use a setting with a NEUTRAL default, a
|
||||||
# site-namespaced directory (scripts/site_imports/<site>/), or seed data.
|
# site-namespaced directory (scripts/site_imports/<site>/), or seed data.
|
||||||
#
|
#
|
||||||
|
# Code generation lives in one module. Seven views were importing qrcode and
|
||||||
|
# jsbarcode directly, each with its own margin, width and error correction, and
|
||||||
|
# the answers had already drifted - which on a label means one page scans and
|
||||||
|
# another does not. frontend/src/utils/codes.js owns that knowledge now; a view
|
||||||
|
# passes what is specific to its label and nothing else.
|
||||||
|
echo "==> Checking that only the shared module imports the code libraries..."
|
||||||
|
CODE_LIB_IMPORTS=$(grep -rn "from ['\"]\(qrcode\|jsbarcode\)['\"]" \
|
||||||
|
--include='*.vue' --include='*.js' \
|
||||||
|
frontend/src plugins/ 2>/dev/null \
|
||||||
|
| grep -v 'frontend/src/utils/codes\.js' \
|
||||||
|
| grep -v '\.plugins-staged/' || true)
|
||||||
|
if [ -n "$CODE_LIB_IMPORTS" ]; then
|
||||||
|
echo "FAIL: import qrcode/jsbarcode through @/utils/codes, not directly:"
|
||||||
|
echo "$CODE_LIB_IMPORTS"
|
||||||
|
echo
|
||||||
|
VIOLATIONS=$((VIOLATIONS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
# ENFORCING. It was report-only while the backlog was worked off, and the hit
|
# ENFORCING. It was report-only while the backlog was worked off, and the hit
|
||||||
# count then did not move for weeks - a rule that only prints is read as no rule.
|
# count then did not move for weeks - a rule that only prints is read as no rule.
|
||||||
# Set SITE_LITERALS_ENFORCE=0 to drop back to reporting for a local run.
|
# Set SITE_LITERALS_ENFORCE=0 to drop back to reporting for a local run.
|
||||||
|
|||||||
Reference in New Issue
Block a user