Skip to content

Commit 009c33e

Browse files
committed
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 af0e53e commit 009c33e

5 files changed

Lines changed: 41 additions & 15 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: 17 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,26 @@ 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+
// Same signals the family resolution reads, so style and face cannot disagree.
68+
const fontStyle: "normal" | "bold" =
69+
input.fontFamilyOverride || effectiveFontName ? "normal" : "bold";
6570
const glyphWidthDots = measureInkWidthPx(
6671
content,
6772
measureSizeDots,
6873
fontFamily,
69-
input.measureFontStyle,
74+
fontStyle,
7075
);
7176
const spacingDots =
7277
(input.letterSpacingDots ?? 0) * Math.max(0, content.length - 1);
7378
const inkWidthDots = (glyphWidthDots + spacingDots) * fontScaleX;
74-
return { content, fontFamily, fontScaleX, inkWidthDots, fontSizeDots: input.fontSizeDots };
79+
return {
80+
content,
81+
fontFamily,
82+
fontScaleX,
83+
inkWidthDots,
84+
fontSizeDots: input.fontSizeDots,
85+
fontStyle,
86+
};
7587
}
7688

7789
/** Ink extent feeding the FO/I and FO/B anchor shift: cell grid for bitmap
@@ -136,7 +148,6 @@ export function getTextRenderMetrics(
136148
fontFamilyOverride,
137149
fontSizeDots: device?.fontSizeDots,
138150
scaleXOverride: device?.scaleX,
139-
measureFontStyle: device ? "normal" : undefined,
140151
letterSpacingDots: device?.letterSpacingDots,
141152
}),
142153
yOffsetDots: device?.yOffsetDots,

‎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: 19 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,21 @@ 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 font source", () => {
80+
// Only the PrintLab substitute is bold; see the fontStyle doc (#356).
81+
expect(computeTextRenderMetrics({ content: "X", fontHeight: 30, fontWidth: 0 }).fontStyle)
82+
.toBe("bold");
83+
expect(
84+
computeTextRenderMetrics({
85+
content: "X", fontHeight: 30, fontWidth: 0, printerFontName: "DINALTERNATE.TTF",
86+
}).fontStyle,
87+
).toBe("normal");
88+
expect(
89+
computeTextRenderMetrics({
90+
content: "X", fontHeight: 30, fontWidth: 0, fontFamilyOverride: "'Vera Mono'",
91+
}).fontStyle,
92+
).toBe("normal");
93+
});
94+
});

0 commit comments

Comments
 (0)