stash
This commit is contained in:
parent
ed04d98bf1
commit
aa94c92000
10 changed files with 873 additions and 296 deletions
|
|
@ -34,13 +34,24 @@ function isQrLeaf(node) {
|
|||
|
||||
function encodeQr(text, codeType, options) {
|
||||
if (!anyd) {
|
||||
throw new Error("The QR encoder is still loading — try again in a moment.");
|
||||
const err = new Error("The QR encoder is still loading — try again in a moment.");
|
||||
err.short = "loading…";
|
||||
throw err;
|
||||
}
|
||||
// BitMatrix-alike shim over anyd's row-major matrix, matching the old "qrcode" package's
|
||||
// modules.size/get() shape that drawQrLeaf/snapQrToCrispSize expect. See
|
||||
// docs/implementation.md#qr-module-matrix-shim.
|
||||
const {width, height, modules} = anyd.encode(codeType, new TextEncoder().encode(text), options).matrix;
|
||||
return {width, height, get: (row, col) => modules[row * width + col] !== 0};
|
||||
try {
|
||||
const {width, height, modules} = anyd.encode(codeType, new TextEncoder().encode(text), options).matrix;
|
||||
return {width, height, get: (row, col) => modules[row * width + col] !== 0};
|
||||
} catch (e) {
|
||||
// anyd's own errors (e.g. "capacity exceeded: …") have no `short` of their own - every
|
||||
// failure here comes down to the content not fitting the chosen QR variant's capacity.
|
||||
if (e && typeof e === "object" && !("short" in e)) {
|
||||
e.short = "too long";
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const TRAILING_PADDING_PX = 3; /* blank columns after the cut, same idea as the leading margin */
|
||||
|
|
@ -80,8 +91,26 @@ function fontFamilyFor(fontPx) {
|
|||
return PIXEL_FONT_TIERS.find(t => fontPx < t.belowPx) ?? {family: "sans-serif"};
|
||||
}
|
||||
|
||||
// Tom Thumb reads clearly down to 5px per the same real-Chromium testing as PIXEL_FONT_TIERS; below this, drawTextLeaf blanks the field instead of rejecting the whole label.
|
||||
const MIN_READABLE_TEXT_PX = 5;
|
||||
// Below this font size (px) or physical height (mm) - Tom Thumb's real-Chromium-tested legibility
|
||||
// floor - text is a hard failure, the same two-metric shape as MIN_RECOMMENDED_QR_PX_PER_MODULE/
|
||||
// MIN_RECOMMENDED_QR_MODULE_MM below (see checkTextSizes). No longer just silently blanked.
|
||||
const MIN_TEXT_PX = 5;
|
||||
const MIN_TEXT_MM = 0.5;
|
||||
// Below this, still legible but a soft warning - flagged rather than blocking, same idea as
|
||||
// MIN_RECOMMENDED_QR_PX_PER_MODULE/MIN_RECOMMENDED_QR_MODULE_MM.
|
||||
const MIN_RECOMMENDED_TEXT_PX = 8;
|
||||
const MIN_RECOMMENDED_TEXT_MM = 1;
|
||||
|
||||
// Below this many pixels per module, a QR-family code still technically fits (see
|
||||
// snapQrToCrispSize's scale >= 1 hard requirement) but risks blurring together on a real thermal
|
||||
// printer's dot pitch - a soft warning rather than the outright rejection scale < 1 gets.
|
||||
const MIN_RECOMMENDED_QR_PX_PER_MODULE = 3;
|
||||
// Below this per-module physical size (in mm), a QR-family code is a rule-of-thumb risk for a
|
||||
// phone camera to resolve at normal scanning distance even when crisply printed at full
|
||||
// resolution - also a soft warning, independent of the pixels-per-module check above (a printer
|
||||
// can hit that check's px/module floor at any dpi, but only a high enough dpi keeps modules this
|
||||
// physically small still legible).
|
||||
const MIN_RECOMMENDED_QR_MODULE_MM = 0.5;
|
||||
|
||||
function isSplit(node) {
|
||||
return Array.isArray(node);
|
||||
|
|
@ -169,27 +198,102 @@ function positionTree(node, ownAxis, x, y) {
|
|||
}
|
||||
|
||||
// Pins each QR-family leaf's real crisp-pixel box.width/box.height so relation() above starts
|
||||
// treating it as fixed-size. See docs/implementation.md#crisp-qr-sizing.
|
||||
function snapQrToCrispSize(node) {
|
||||
// treating it as fixed-size. Appends a {short, message} entry to `warnings` for each of
|
||||
// MIN_RECOMMENDED_QR_PX_PER_MODULE/MIN_RECOMMENDED_QR_MODULE_MM the leaf falls short of (still
|
||||
// printable, just flagged as a scan-reliability risk), and unconditionally appends its raw
|
||||
// {scale, moduleMm} to `qrInfo` - callers needing to show those numbers regardless of whether
|
||||
// they tripped a threshold (see LabelLayoutPreview.vue's debug line) shouldn't have to re-derive
|
||||
// them. See docs/implementation.md#crisp-qr-sizing.
|
||||
function snapQrToCrispSize(node, pxPerMm, warnings, qrInfo) {
|
||||
if (isSplit(node)) {
|
||||
node.forEach(snapQrToCrispSize);
|
||||
node.forEach(child => snapQrToCrispSize(child, pxPerMm, warnings, qrInfo));
|
||||
return;
|
||||
}
|
||||
if (isQrLeaf(node)) {
|
||||
const {width: modulesW, height: modulesH} = node.qr;
|
||||
const scale = Math.floor(Math.min(node.box.width / modulesW, node.box.height / modulesH));
|
||||
if (!(scale >= 1)) {
|
||||
throw new Error("This text needs a bigger code than the tape allows — "
|
||||
const err = new Error("This text needs a bigger code than the tape allows — "
|
||||
+ "try a shorter value or a wider tape.");
|
||||
err.short = "too big";
|
||||
throw err;
|
||||
}
|
||||
node.crispWidth = modulesW * scale;
|
||||
node.crispHeight = modulesH * scale;
|
||||
|
||||
const moduleMm = scale / pxPerMm;
|
||||
qrInfo.push({scale, moduleMm});
|
||||
|
||||
if (scale < MIN_RECOMMENDED_QR_PX_PER_MODULE) {
|
||||
warnings.push({
|
||||
short: `${scale}px/mod`,
|
||||
message: `This code's modules are only ${scale}px wide - they may blur together `
|
||||
+ "when printed; consider a bigger label or shorter content.",
|
||||
});
|
||||
}
|
||||
if (moduleMm < MIN_RECOMMENDED_QR_MODULE_MM) {
|
||||
warnings.push({
|
||||
short: `${moduleMm.toFixed(2)}mm/mod`,
|
||||
message: `This code's modules are only ${moduleMm.toFixed(2)}mm across - it may `
|
||||
+ "be too small to scan reliably; consider a bigger label or shorter content.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The rendered font size, floored to a whole pixel - same reasoning as snapQrToCrispSize's
|
||||
// integer module scale (rounding up could overflow the box a fractional size would have fit
|
||||
// exactly). checkTextSizes and drawTextLeaf both call this rather than each computing their own
|
||||
// raw fraction, so what gets measured/threshold-checked is exactly what gets drawn.
|
||||
function effectiveFontPx(node, referencePx) {
|
||||
return Math.floor(referencePx * (node.box.height / node.naturalHeight));
|
||||
}
|
||||
|
||||
// Same shape as snapQrToCrispSize but for "text" leaves: throws below MIN_TEXT_PX/MIN_TEXT_MM
|
||||
// (too small to read at all), appends a {short, message} warning to `warnings` for each of
|
||||
// MIN_RECOMMENDED_TEXT_PX/MIN_RECOMMENDED_TEXT_MM it falls short of, and unconditionally appends
|
||||
// its raw {fontPx, fontMm} to `textInfo`. Must run after the tree's *final* layoutTree pass (unlike
|
||||
// snapQrToCrispSize, which runs before solve() re-resolves the tree) - a text leaf's box.height
|
||||
// isn't stable until then, since (unlike a QR leaf's crispWidth/crispHeight) it doesn't feed back
|
||||
// into that re-resolve.
|
||||
function checkTextSizes(node, referencePx, pxPerMm, warnings, textInfo) {
|
||||
if (isSplit(node)) {
|
||||
node.forEach(child => checkTextSizes(child, referencePx, pxPerMm, warnings, textInfo));
|
||||
return;
|
||||
}
|
||||
if (node.type !== "text") {
|
||||
return;
|
||||
}
|
||||
const fontPx = effectiveFontPx(node, referencePx);
|
||||
const fontMm = fontPx / pxPerMm;
|
||||
if (fontPx < MIN_TEXT_PX || fontMm < MIN_TEXT_MM) {
|
||||
const err = new Error(`This text only fits at ${fontPx}px `
|
||||
+ `(${fontMm.toFixed(2)}mm) - too small to read; try a shorter value, a different `
|
||||
+ "layout, or a bigger label.");
|
||||
err.short = "too small";
|
||||
throw err;
|
||||
}
|
||||
textInfo.push({fontPx, fontMm});
|
||||
|
||||
if (fontPx < MIN_RECOMMENDED_TEXT_PX) {
|
||||
warnings.push({
|
||||
short: `${fontPx}px text`,
|
||||
message: `This text renders at only ${fontPx}px - it may be hard to read; `
|
||||
+ "consider a bigger label or shorter content.",
|
||||
});
|
||||
}
|
||||
if (fontMm < MIN_RECOMMENDED_TEXT_MM) {
|
||||
warnings.push({
|
||||
short: `${fontMm.toFixed(2)}mm text`,
|
||||
message: `This text renders at only ${fontMm.toFixed(2)}mm tall - it may be hard to `
|
||||
+ "read; consider a bigger label or shorter content.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Always measures in sans-serif at the reference size, since the eventual font (see
|
||||
// PIXEL_FONT_TIERS) isn't known until layoutTree sizes the box this aspect ratio feeds into; the
|
||||
// resulting mismatch is invisible in practice, and MIN_READABLE_TEXT_PX still catches real failures.
|
||||
// resulting mismatch is invisible in practice, and checkTextSizes still catches real failures.
|
||||
function measureTextBlock(ctx, lines, referencePx) {
|
||||
ctx.font = `${referencePx}px sans-serif`;
|
||||
const width = Math.max(...lines.map(line => ctx.measureText(line).width));
|
||||
|
|
@ -232,25 +336,21 @@ function drawQrLeaf(ctx, node) {
|
|||
}
|
||||
}
|
||||
|
||||
// Returns the effective (un-normalized) font size drawn at, or that would have been if too small
|
||||
// to draw (see below); drawTree collects these into drawLabel/drawFallbackLabel's textSizesPx.
|
||||
// Returns the effective (un-normalized) font size drawn at; drawTree collects these into
|
||||
// drawLabel/drawFallbackLabel's textSizesPx. Always draws - checkTextSizes (run earlier, on the
|
||||
// same tree, before any of this) already rejected anything below MIN_TEXT_PX/MIN_TEXT_MM, so
|
||||
// there's no "too small to draw" case left to special-case here.
|
||||
function drawTextLeaf(ctx, node, referencePx) {
|
||||
const fontPx = referencePx * (node.box.height / node.naturalHeight);
|
||||
// Below the smallest legible size, leave this leaf blank rather than reject the whole label;
|
||||
// its box was already accounted for, so nothing else in the layout shifts.
|
||||
if (fontPx < MIN_READABLE_TEXT_PX) {
|
||||
return fontPx;
|
||||
}
|
||||
const fontPx = effectiveFontPx(node, referencePx);
|
||||
const {family, scale = 1} = fontFamilyFor(fontPx);
|
||||
const isPixelFont = family !== "sans-serif";
|
||||
// Pixel-font glyphs need whole-pixel size/position to stay grid-aligned, since node.box.x/y
|
||||
// are ordinary (fractional) layout math; sans-serif is left exact since anti-aliasing handles
|
||||
// fractional positions fine.
|
||||
// Position (not size - fontPx is already a whole pixel) still needs snapping for pixel fonts
|
||||
// to stay grid-aligned, since node.box.x/y are ordinary (fractional) layout math; sans-serif is
|
||||
// left exact since anti-aliasing handles fractional positions fine.
|
||||
const snap = isPixelFont ? Math.round : (v) => v;
|
||||
const drawFontPx = snap(fontPx);
|
||||
// `scale` (Tom Thumb only, see PIXEL_FONT_TIERS above) corrects the size handed to ctx.font
|
||||
// for its real ink; drawFontPx itself stays the logical size used for centering/stacking math.
|
||||
ctx.font = `${drawFontPx * scale}px "${family}"`;
|
||||
// for its real ink; fontPx itself stays the logical size used for centering/stacking math.
|
||||
ctx.font = `${fontPx * scale}px "${family}"`;
|
||||
// Canvas text silently falls back if drawn before a not-yet-loaded font resolves, unlike DOM
|
||||
// text. See docs/implementation.md#canvas-font-loading.
|
||||
if (isPixelFont) {
|
||||
|
|
@ -292,8 +392,8 @@ function drawDebugBorder(ctx, node) {
|
|||
ctx.restore();
|
||||
}
|
||||
|
||||
// Collects each text leaf's effective font size so callers (Print.vue) can spot a blank-rendered
|
||||
// field (see drawTextLeaf's MIN_READABLE_TEXT_PX check) as suspiciously small rather than silently missing.
|
||||
// Collects each text leaf's effective font size so callers (Print.vue) can spot a suspiciously
|
||||
// small field (checkTextSizes' warnings/textInfo cover the same numbers in more structured form).
|
||||
function drawTree(ctx, node, referencePx, textSizesPx) {
|
||||
if (isSplit(node)) {
|
||||
node.forEach(child => drawTree(ctx, child, referencePx, textSizesPx));
|
||||
|
|
@ -311,8 +411,8 @@ function drawTree(ctx, node, referencePx, textSizesPx) {
|
|||
}
|
||||
|
||||
// Builds, sizes and validates the tree for a fixed dimension plus pxPerMm; runs sizing twice so
|
||||
// QR-family leaves' real crisp size is known before the tree is finally resolved. See
|
||||
// docs/implementation.md#label-content-layout.
|
||||
// QR-family leaves' real crisp size is known before the tree is finally resolved, then checks
|
||||
// every text leaf's final font size. See docs/implementation.md#label-content-layout.
|
||||
function layoutContent(ctx, content, fixedSize, maxLength, referencePx, pxPerMm, orientation) {
|
||||
const tree = buildRenderTree(ctx, content, referencePx);
|
||||
const alongTape = orientation !== "across";
|
||||
|
|
@ -326,29 +426,38 @@ function layoutContent(ctx, content, fixedSize, maxLength, referencePx, pxPerMm,
|
|||
|
||||
let {width, height} = solve();
|
||||
layoutTree(tree, false, width, height, pxPerMm);
|
||||
snapQrToCrispSize(tree);
|
||||
const warnings = [];
|
||||
const qrInfo = [];
|
||||
snapQrToCrispSize(tree, pxPerMm, warnings, qrInfo);
|
||||
|
||||
({width, height} = solve());
|
||||
const length = alongTape ? width : height;
|
||||
if (maxLength !== Infinity && length > maxLength) {
|
||||
throw new Error("This doesn't fit on this tape — "
|
||||
const err = new Error("This doesn't fit on this tape — "
|
||||
+ "try a shorter value, a different layout, or a bigger label.");
|
||||
err.short = "too big";
|
||||
throw err;
|
||||
}
|
||||
layoutTree(tree, false, width, height, pxPerMm);
|
||||
return {tree, length};
|
||||
const textInfo = [];
|
||||
checkTextSizes(tree, referencePx, pxPerMm, warnings, textInfo);
|
||||
return {tree, length, warnings, qrInfo, textInfo};
|
||||
}
|
||||
|
||||
// The tape-fed layout: draws a fully resolved content tree (see templateContent) at the tape's
|
||||
// real pixel dimensions. See docs/implementation.md#tape-fed-label-drawing. Returns
|
||||
// {textSizesPx}: each "text" leaf's effective font size, in the tree's own left-to-right,
|
||||
// top-to-bottom order.
|
||||
// top-to-bottom order; {warnings}: {short, message} scan-reliability entries from
|
||||
// snapQrToCrispSize/checkTextSizes, if any; {qrInfo}: each QR-family leaf's raw
|
||||
// {scale, moduleMm}; {textInfo}: each text leaf's raw {fontPx, fontMm} - both regardless of
|
||||
// whether they tripped a warning.
|
||||
export function drawLabel(canvas, tape, content, orientation = "along") {
|
||||
const maxLength = tape.printLengthPx
|
||||
? tape.printLengthPx - tape.leadPx - TRAILING_PADDING_PX
|
||||
: Infinity;
|
||||
const measureCtx = canvas.getContext("2d");
|
||||
const pxPerMm = tape.dpi / 25.4;
|
||||
const {tree, length: contentLength} = layoutContent(
|
||||
const {tree, length: contentLength, warnings, qrInfo, textInfo} = layoutContent(
|
||||
measureCtx, content, tape.printAreaPx, maxLength, TEXT_REFERENCE_PX, pxPerMm, orientation);
|
||||
|
||||
const printedLength = tape.printLengthPx || Math.ceil(contentLength + tape.leadPx + TRAILING_PADDING_PX);
|
||||
|
|
@ -376,7 +485,7 @@ export function drawLabel(canvas, tape, content, orientation = "along") {
|
|||
positionTree(tree, false, origin, 0);
|
||||
drawTree(ctx, tree, TEXT_REFERENCE_PX, textSizesPx);
|
||||
}
|
||||
return {textSizesPx};
|
||||
return {textSizesPx, warnings, qrInfo, textInfo};
|
||||
}
|
||||
|
||||
const FALLBACK_LABEL_HEIGHT_PX = 200; /* reference height the no-webusb preview/PNG scales from */
|
||||
|
|
@ -384,11 +493,11 @@ const FALLBACK_DPI = 203; /* reference resolution for turning "empty" leaves' m
|
|||
|
||||
// The no-webusb preview/PNG: same layout tree/renderer as drawLabel, scaled from a fixed
|
||||
// reference height instead. See docs/implementation.md#fallback-label-preview. `orientation` and
|
||||
// the {textSizesPx} return, see drawLabel.
|
||||
// the {textSizesPx, warnings, qrInfo, textInfo} return, see drawLabel.
|
||||
export function drawFallbackLabel(canvas, content, orientation = "along") {
|
||||
const measureCtx = canvas.getContext("2d");
|
||||
const pxPerMm = FALLBACK_DPI / 25.4;
|
||||
const {tree, length: contentLength} = layoutContent(
|
||||
const {tree, length: contentLength, warnings, qrInfo, textInfo} = layoutContent(
|
||||
measureCtx, content, FALLBACK_LABEL_HEIGHT_PX, Infinity, TEXT_REFERENCE_PX, pxPerMm, orientation);
|
||||
|
||||
canvas.width = Math.ceil(contentLength);
|
||||
|
|
@ -411,7 +520,7 @@ export function drawFallbackLabel(canvas, content, orientation = "along") {
|
|||
positionTree(tree, false, 0, 0);
|
||||
drawTree(ctx, tree, TEXT_REFERENCE_PX, textSizesPx);
|
||||
}
|
||||
return {textSizesPx};
|
||||
return {textSizesPx, warnings, qrInfo, textInfo};
|
||||
}
|
||||
|
||||
// Turns a {kind, components} prefill (see Print.vue's `prefill` prop) into the literal string a
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue