@@ -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;