diff --git a/frontend/src/views/Print.vue b/frontend/src/views/Print.vue index 9d12c5f..e6ccd2b 100644 --- a/frontend/src/views/Print.vue +++ b/frontend/src/views/Print.vue @@ -50,34 +50,39 @@ - +
@@ -137,7 +142,9 @@
Label
- {{ tape.mediaWidthMm }} mm tape{{ textSizesSummary }} + {{ tape.mediaWidthMm }} mm tape{{ + textSizesSummary + }}

@@ -249,8 +256,8 @@

+ :recent-template-ids="recentTemplateIds" + @input="selectedTemplate = $event">
@@ -267,9 +274,28 @@ import LabelLayoutPreview from "@/components/LabelLayoutPreview.vue"; import printerManager from "@/printerManager.js"; import {MultiPrinterBlob, canvasToBitmap, bitmapToCanvas} from "../../vendor/weblabel.js"; -import {tapeFromStatus, drawLabel, drawFallbackLabel, buildLabelContent, buildLabelFields, preloadQrEncoder} from "@/label.js"; +import { + tapeFromStatus, + drawLabel, + drawFallbackLabel, + buildLabelContent, + buildLabelFields, + preloadQrEncoder +} from "@/label.js"; import {LABEL_TEMPLATES, BASE_VARS, DERIVED_VARS, withDerivedVars, templateContent} from "@/label-layouts.js"; import {shortenedRoute} from "@/router"; +import {loadFreeType, loadFontFace, drawMonoText} from "@/freetype-preview.js"; + +// URLs (not raw bytes - fetched lazily, see drawOptimalFontPreviews) for each font optimal_fonts +// can pick, fed to freetype-preview.js's loadFontFace. Silkscreen and Inter each need their own TTF +// copy (see pixel-candidates/LICENSE.md) since this build of FreeType can't parse WOFF2 (no brotli +// support - confirmed: FT error 2 on both production pixel/Silkscreen-Regular.woff2 and +// label/Inter-Regular.woff2). +import pixelonFontUrl from "@/assets/fonts/pixel-candidates/Pixelon.ttf?url"; +import chavaFontUrl from "@/assets/fonts/pixel-candidates/Chava.ttf?url"; +import terminusFontUrl from "@/assets/fonts/pixel-candidates/Terminus.ttf?url"; +import silkscreenCandidateFontUrl from "@/assets/fonts/pixel-candidates/Silkscreen.ttf?url"; +import interCandidateFontUrl from "@/assets/fonts/pixel-candidates/Inter.ttf?url"; // Print.vue-local calculated fields on top of label-layouts.js's DERIVED_VARS. See // docs/implementation.md#calculated-short-link-fields. @@ -297,10 +323,14 @@ const RULER_TIERS = [ //after manually comparing all fonts at all sizes, it was decides that in some cases it's more readable to round down the font size to a more readable // this is a lookup table for these optimal settings const optimal_fonts = (size) => { - if (size > 20) + if (size > 32) return {family: "Inter", url: interCandidateFontUrl, size: size}; + if (size >= 28) + return {family: "Terminus", url: terminusFontUrl, size: 28}; // would even support bold mode >=14px + if (size >= 24) + return {family: "Terminus", url: terminusFontUrl, size: 24}; // would even support bold mode >=14px if (size >= 12) - return {family: "Terminus", url: terminusFontUrl, size: (size - (size % 2))}; + return {family: "Terminus", url: terminusFontUrl, size: (size - (size % 2))}; // would even support bold mode >=14px if (size >= 10) return {family: "Pixelon", url: pixelonFontUrl, size: 10}; if (size >= 8) @@ -372,21 +402,19 @@ export default { brotherCliExample: "brother_ql --model QL-000 --printer usb://0000:0000 print --label 00 label.png", niimbotCliExample: "niimprint --model b00 --conn usb print --density 3 --image label.png", - // Ten pixel/bitmap font candidates for legibility testing only (see the disabled card above); not used by label.js's real FONT_TIERS. - candidateFonts: [ - {family: "Pixelon"}, - {family: "Pixelbasel"}, - {family: "Chava"}, - {family: "CodersCrux"}, - {family: "712Serif"}, - {family: "6pxNormal"}, - {family: "6pxExpert"}, - {family: "Jersey10"}, - {family: "Jersey15"}, - {family: "Terminus"}, - ], - candidateSizes: [5, 6, 7, 8, 10, 12, 16, 20], - candidateZoom: 4, + // Nominal sizes previewed by the "optimal font by size" card - 5px to 30px inclusive. + previewSizes: Array.from({length: 36}, (_, i) => i + 5), + previewZoom: 4, + // Keyed by family, populated lazily the first time drawOptimalFontPreviews loads a font - + // avoids refetching the same bytes on every varValues-triggered redraw. + freetypeFontBytes: {}, + // Set if FreeType itself (not a specific font) fails to load - shown once above the grid + // instead of failing every cell silently. + freetypeError: null, + // Keyed by nominal size - each cell's effective line height in px (ft_get_line_height at + // the actually-drawn size), same metric prototypes/freetype-wasm's own index.html demo + // shows below its canvas. + freetypeLineHeights: {}, }; }, computed: { @@ -482,7 +510,7 @@ export default { } else { this.redrawFallback(); } - this.drawCandidateFontTests(); + this.drawOptimalFontPreviews(); }, deep: true, }, @@ -750,48 +778,52 @@ export default { }); }, - // Works around Vue's refInFor array-collecting behavior for :ref in v-for, same as LabelLayoutPreview.vue's setTemplateCanvasRef. - setCandidateCanvasRef(key, el) { + + // Looks up the (family, url, effective size) optimal_fonts picks for a nominal size - + // exposed as a method (not a computed) since the template calls it per-size inside a v-for. + optimalFontFor(size) { + return optimal_fonts(size); + }, + + setPreviewCanvasRef(size, el) { if (el) { - this.candidateCanvases[key] = el; + this.freetypeCanvases[size] = el; } else { - delete this.candidateCanvases[key]; + delete this.freetypeCanvases[size]; } }, - // Renders the "Text" field at exact size/family with no layout snapping, so the font itself is judged; ink-centered vertically (like label.js's drawTextLeaf) so unreliable metrics don't clip. Thresholded through canvasToBitmap/bitmapToCanvas afterwards, same as redraw()'s real label preview, so a candidate is judged on the same 1-bit dots the printer would actually fire, not the anti-aliased canvas render. - drawCandidateCell(canvas, family, fontPx) { - if (!canvas) { + // Draws every previewSizes cell - see the card in the template. For each nominal size, looks + // up the font optimal_fonts actually picks and draws through libfreetype-wasm's own + // FT_LOAD_TARGET_MONO rasterizer - a true 1-bit render, not canvas fillText's grayscale AA + // thresholded after the fact. Font bytes are cached per family (see freetypeFontBytes) since + // several sizes can share the same font. + async drawOptimalFontPreviews() { + let ft; + try { + ft = await loadFreeType(); + } catch (e) { + this.freetypeError = e.message; return; } - const text = this.varValues.text; - const ctx = canvas.getContext("2d"); - ctx.font = `${fontPx}px "${family}"`; - const {width, actualBoundingBoxAscent: up, actualBoundingBoxDescent: down} = ctx.measureText(text); - canvas.width = Math.ceil(width) + 4; - canvas.height = Math.ceil(up + down) + 8; - // Re-set font/alignment - changing canvas.width/height above resets the 2D context. - ctx.font = `${fontPx}px "${family}"`; - ctx.textAlign = "left"; - ctx.textBaseline = "alphabetic"; - ctx.fillStyle = "#fff"; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = "#000"; - ctx.fillText(text, 2, (canvas.height + up - down) / 2); - bitmapToCanvas(canvas, canvasToBitmap(canvas)); - canvas.style.width = `${canvas.width * this.candidateZoom}px`; - canvas.style.height = `${canvas.height * this.candidateZoom}px`; - }, - - // Draws every (candidateFonts x candidateSizes) cell - see the card in the template. - async drawCandidateFontTests() { - await Promise.all(this.candidateFonts - .map(({family}) => document.fonts.load(`20px "${family}"`).catch(() => {}))); - for (const {family} of this.candidateFonts) { - for (const size of this.candidateSizes) { - const canvas = this.candidateCanvases[`${family}-${size}`]; - this.drawCandidateCell(canvas, family, size); + this.freetypeError = null; + for (const size of this.previewSizes) { + const cfg = optimal_fonts(size); + const canvas = this.freetypeCanvases[size]; + if (!cfg || !canvas) { + continue; } + let bytes = this.freetypeFontBytes[cfg.family]; + if (!bytes) { + const res = await fetch(cfg.url); + bytes = new Uint8Array(await res.arrayBuffer()); + this.freetypeFontBytes[cfg.family] = bytes; + } + if (loadFontFace(ft, bytes) !== 0) { + continue; // fails to parse for FreeType too - leave that cell blank. + } + this.freetypeLineHeights[size] = + drawMonoText(ft, canvas, cfg.size, this.varValues.text, this.previewZoom); } }, }, @@ -799,11 +831,11 @@ export default { this.blob = null; this.resizeObserver = null; this.resizeTimer = null; - this.candidateCanvases = {}; + this.freetypeCanvases = {}; this.recentTemplateIds = this.loadRecentTemplateIds(); }, async mounted() { - this.drawCandidateFontTests(); + this.drawOptimalFontPreviews(); // Not awaited: resolving late just means shortUrl briefly shows "—" instead of blocking the unrelated printer/wasm setup below. this.fetchIdMap().catch(e => { this.error = e.message;