From fb3c1e78e867a4a904818f5ebb86e76f671be22b Mon Sep 17 00:00:00 2001 From: jedi Date: Wed, 26 Aug 2026 12:13:27 +0200 Subject: [PATCH 1/4] stash --- frontend/src/views/Print.vue | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/frontend/src/views/Print.vue b/frontend/src/views/Print.vue index 21195bc..9d12c5f 100644 --- a/frontend/src/views/Print.vue +++ b/frontend/src/views/Print.vue @@ -294,6 +294,24 @@ const RULER_TIERS = [ {aboveMm: 500, tickMm: 5, majorEveryMm: 25}, ]; +//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) + return {family: "Inter", url: interCandidateFontUrl, size: size}; + if (size >= 12) + return {family: "Terminus", url: terminusFontUrl, size: (size - (size % 2))}; + if (size >= 10) + return {family: "Pixelon", url: pixelonFontUrl, size: 10}; + if (size >= 8) + return {family: "Chava", url: chavaFontUrl, size: 8}; + if (size === 7) + return {family: "Chava", url: chavaFontUrl, size: 7}; + if (size >= 5) + return {family: "Silkscreen", url: silkscreenCandidateFontUrl, size: 8}; //Silkscreen comes out to an effective height of 5px at size 8 + return null; +}; + export default { name: "Print", components: { From 368268d288142e9bddb4bbb6fc1005f60ed48832 Mon Sep 17 00:00:00 2001 From: jedi Date: Wed, 26 Aug 2026 13:17:30 +0200 Subject: [PATCH 2/4] stash --- frontend/src/views/Print.vue | 184 ++++++++++++++++++++--------------- 1 file changed, 108 insertions(+), 76 deletions(-) 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; From 1356aa77493f126546edc00d2f2b6bb4af5d22e3 Mon Sep 17 00:00:00 2001 From: jedi Date: Wed, 26 Aug 2026 14:20:02 +0200 Subject: [PATCH 3/4] stash --- frontend/src/freetype-preview.js | 185 +++++++++++++++++++++++++++++++ frontend/src/views/Print.vue | 38 +++++-- 2 files changed, 211 insertions(+), 12 deletions(-) create mode 100644 frontend/src/freetype-preview.js diff --git a/frontend/src/freetype-preview.js b/frontend/src/freetype-preview.js new file mode 100644 index 0000000..3b0d261 --- /dev/null +++ b/frontend/src/freetype-preview.js @@ -0,0 +1,185 @@ +// FreeType-in-wasm glyph rendering for Print.vue's font-candidate comparison card - draws each +// candidate through FreeType's own monochrome rasterizer (FT_LOAD_TARGET_MONO), the same rendering +// mode ptouch-print.c uses (see weblabel.js's TEXT_COVERAGE comment), rather than the canvas +// fillText-then-threshold approximation drawCandidateCell uses for its own cell. Experimental-only, +// same as the rest of that card - not part of the real label pipeline. See +// prototypes/freetype-wasm/ for the source this wasm build comes from. + +// Served unbundled from public/vendor/, same reasoning as libweblabel.js (see +// docs/implementation.md#libweblabel-served-unbundled) - its emscripten glue resolves +// freetype.wasm relative to its own import.meta.url, so both files must sit together, unhashed. +const FREETYPE_URL = "/vendor/freetype.js"; + +// A literal (or traceably-constant) specifier in `import()` - even dynamic, even @vite-ignore'd - +// gets resolved by Vite's dev server the moment it's requested, and it refuses to serve anything +// under public/ that way ("This file is in /public and will be copied as-is during build without +// going through the plugin transforms, and therefore should not be imported from source code. It +// can only be referenced via HTML tags."). A blob: URL re-export module sidesteps Vite's own +// import analysis (it never sees the blob's contents), but the app's CSP `script-src` allows +// `'unsafe-inline'`, not `blob:`, so a blob-URL module gets blocked at the browser level instead +// ("Failed to fetch dynamically imported module: blob:..."). An actual inline `