Skip to content

Commit 2561203

Browse files
authored
Fix/webkit fr custom font (#396)
* fix(desktop): allow data: fonts in the CSP WKWebView enforces font-src on FontFace loads that Chromium exempts, so custom-font uploads failed on macOS with 'font file could not be loaded'. * fix(canvas): render custom TTFs at their real weight Forcing bold on an uploaded face without one triggers synthetic bold, which WebKit multi-passes; under the reverse path's 'difference' composite that self-cancels into hollow glyphs (discussion #356). The style now derives from the font source in TextRenderMetrics, shared by render and measure.
1 parent 68a4ce2 commit 2561203

6 files changed

Lines changed: 57 additions & 17 deletions

File tree

‎packages/core/src/lib/fontCache.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ function dataUrlBytes(dataUrl: string): Uint8Array | undefined {
6565
/** Resolves true when the face registered, false when the bytes were rejected
6666
* (invalid font, unsupported outlines) or the API is unavailable. Callers on
6767
* the interactive upload path surface the failure; background paths ignore it.
68-
* Binary source, NOT `url(data:...)`: the url form is a font-src fetch, which
69-
* the desktop CSP blocks (no `data:` there, deliberately). */
68+
* Binary source, NOT `url(data:...)`: WKWebView enforces font-src even on
69+
* FontFace loads, and byte registration plus `font-src data:` is the combo
70+
* that works across engines (#356). */
7071
async function registerFontFace(entry: CachedFont): Promise<boolean> {
7172
try {
7273
const bytes = dataUrlBytes(entry.dataUrl);

‎packages/core/src/lib/labelGeometry/textRenderMetrics.ts‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export interface TextRenderMetrics {
2424
/** Canvas-only resolved bitmap device font (A-H); single source for
2525
* consumers that need the id beside the metrics (line pitch, bounds). */
2626
deviceFontId?: string;
27+
/** Style the render and the measure share. Bold only for the PrintLab
28+
* Font-0 substitute (real bold face, calibrated so); device substitutes
29+
* and custom TTFs render at their real weight like the printer, and
30+
* synthetic bold hollows out under WebKit's 'difference' (#356). */
31+
fontStyle: "normal" | "bold";
2732
}
2833

2934
/** Parser feeds these (no obj at parse time). */
@@ -40,9 +45,6 @@ export interface TextMetricsInput {
4045
/** Canvas-only bitmap device-font size/scale (A-H snap to magnifications). */
4146
fontSizeDots?: number;
4247
scaleXOverride?: number;
43-
/** Canvas-only: device fonts paint at this weight, so inkWidth must measure
44-
* it too (emit/parse omit it and keep the default bold). */
45-
measureFontStyle?: string;
4648
/** Canvas-only device-font inter-char spacing folded into inkWidth so the
4749
* reverse-box / ^FPR width matches the letter-spaced render. */
4850
letterSpacingDots?: number;
@@ -62,16 +64,27 @@ export function computeTextRenderMetrics(input: TextMetricsInput): TextRenderMet
6264
input.scaleXOverride ?? (fontWidth > 0 ? fontWidth / fontHeight : 1);
6365
const measureSizeDots =
6466
input.fontSizeDots ?? fontHeight / ZPL_FONT_HEIGHT_TO_CSS_RATIO;
67+
// Style follows the RESOLVED face, so a referenced-but-unloaded TTF that
68+
// falls back to the substitute keeps its calibrated bold (metrics feed the
69+
// anchor math); a real face renders and measures at its own weight.
70+
const fontStyle: "normal" | "bold" = fontFamily === DEFAULT_FONT_FAMILY ? "bold" : "normal";
6571
const glyphWidthDots = measureInkWidthPx(
6672
content,
6773
measureSizeDots,
6874
fontFamily,
69-
input.measureFontStyle,
75+
fontStyle,
7076
);
7177
const spacingDots =
7278
(input.letterSpacingDots ?? 0) * Math.max(0, content.length - 1);
7379
const inkWidthDots = (glyphWidthDots + spacingDots) * fontScaleX;
74-
return { content, fontFamily, fontScaleX, inkWidthDots, fontSizeDots: input.fontSizeDots };
80+
return {
81+
content,
82+
fontFamily,
83+
fontScaleX,
84+
inkWidthDots,
85+
fontSizeDots: input.fontSizeDots,
86+
fontStyle,
87+
};
7588
}
7689

7790
/** Ink extent feeding the FO/I and FO/B anchor shift: cell grid for bitmap
@@ -136,7 +149,6 @@ export function getTextRenderMetrics(
136149
fontFamilyOverride,
137150
fontSizeDots: device?.fontSizeDots,
138151
scaleXOverride: device?.scaleX,
139-
measureFontStyle: device ? "normal" : undefined,
140152
letterSpacingDots: device?.letterSpacingDots,
141153
}),
142154
yOffsetDots: device?.yOffsetDots,

‎src-tauri/tauri.conf.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
}
2323
],
2424
"security": {
25-
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' ipc: http://ipc.localhost http://127.0.0.1:*; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'",
26-
"devCsp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' ipc: http://ipc.localhost http://127.0.0.1:* http://localhost:5173 ws://localhost:5173; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'"
25+
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost http://127.0.0.1:*; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'",
26+
"devCsp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost http://127.0.0.1:* http://localhost:5173 ws://localhost:5173; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'"
2727
}
2828
},
2929
"plugins": {

‎src/components/Canvas/KonvaObject.tsx‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -613,10 +613,7 @@ function KonvaObjectInner({
613613
if (obj.type === "text" && textMetrics) {
614614
const p = obj.props;
615615
const { content, fontFamily, fontScaleX, fontSizePx } = textMetrics;
616-
// Bitmap device fonts (A-H) carry their own weight via the substitute
617-
// family (Vera Mono / Bold, OCR); forcing bold there faux-bolds them
618-
// thicker than Labelary. Font 0 / custom uploads stay bold.
619-
const fontStyle = textMetrics.fontSizeDots != null ? "normal" : "bold";
616+
const fontStyle = textMetrics.fontStyle;
620617
// Device-font position trims (cap-top / left-bearing) vs Labelary.
621618
const deviceXOffPx = dotsToPx(textMetrics.xOffsetDots ?? 0, scale, dpmm);
622619
const deviceYOffPx = dotsToPx(textMetrics.yOffsetDots ?? 0, scale, dpmm);

‎src/lib/fontCache.test.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ describe('fontCache', () => {
4141
// ── loadFontFile ─────────────────────────────────────────────────────────────
4242

4343
it('registers the FontFace from bytes, never a url() source', async () => {
44-
// Regression (#356 comment): a url(data:...) source is a font-src fetch,
45-
// which the desktop CSP blocks; every upload failed there.
44+
// Regression #356; the why lives at registerFontFace's doc header.
4645
const sources: unknown[] = [];
4746
const Original = globalThis.FontFace;
4847
class CapturingFontFace {

‎src/lib/labelGeometry/textRenderMetrics.test.ts‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from "vitest";
2-
import { getTextRenderMetrics } from "@zplab/core/lib/labelGeometry/textRenderMetrics";
2+
import { computeTextRenderMetrics, getTextRenderMetrics } from "@zplab/core/lib/labelGeometry/textRenderMetrics";
33
import type { LabelObject } from "@zplab/core/types/Group";
44
import type { LabelConfig } from "@zplab/core/types/LabelConfig";
55

@@ -74,3 +74,34 @@ describe("getTextRenderMetrics — device font resolution", () => {
7474
expect(m!.content).toBe("Abc");
7575
});
7676
});
77+
78+
describe("computeTextRenderMetrics fontStyle", () => {
79+
it("derives the style from the RESOLVED face", async () => {
80+
// Only the PrintLab substitute is bold (calibrated so); see #356.
81+
expect(computeTextRenderMetrics({ content: "X", fontHeight: 30, fontWidth: 0 }).fontStyle)
82+
.toBe("bold");
83+
// Referenced but NOT loaded: falls back to the substitute, which must
84+
// keep its calibrated bold or the anchor metrics measure the wrong face.
85+
expect(
86+
computeTextRenderMetrics({
87+
content: "X", fontHeight: 30, fontWidth: 0, printerFontName: "MISSING.TTF",
88+
}).fontStyle,
89+
).toBe("bold");
90+
const { loadFontBytes, removeFont } = await import("@zplab/core/lib/fontCache");
91+
await loadFontBytes(new Uint8Array([1, 2, 3]), "DINALTERNATE.TTF");
92+
try {
93+
expect(
94+
computeTextRenderMetrics({
95+
content: "X", fontHeight: 30, fontWidth: 0, printerFontName: "DINALTERNATE.TTF",
96+
}).fontStyle,
97+
).toBe("normal");
98+
} finally {
99+
removeFont("DINALTERNATE.TTF");
100+
}
101+
expect(
102+
computeTextRenderMetrics({
103+
content: "X", fontHeight: 30, fontWidth: 0, fontFamilyOverride: "'Vera Mono'",
104+
}).fontStyle,
105+
).toBe("normal");
106+
});
107+
});

0 commit comments

Comments
 (0)