diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7179a672e..e1d1d3c73 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -165,6 +165,7 @@ export { export { FONT_WEIGHT_NAMES, FontManager, + chooseLocalFontMatch, fontManager, styleToWeight, weightToFigmaStyle, diff --git a/packages/core/src/text/fonts.ts b/packages/core/src/text/fonts.ts index 28888480a..8638dd059 100644 --- a/packages/core/src/text/fonts.ts +++ b/packages/core/src/text/fonts.ts @@ -22,6 +22,44 @@ export interface DownloadedFontCache { type FindLocalFontOptions = { allowVariable?: boolean } +type LocalFontMatch = Pick + +export function chooseLocalFontMatch( + fonts: T[], + family: string, + style?: string +): T | undefined { + const families = [family] + const normalized = normalizeFontFamily(family) + if (normalized !== family) families.push(normalized) + const requested = parseFontStyle(style) + + for (const f of families) { + const exact = style ? fonts.find((x) => x.family === f && x.style === style) : undefined + if (exact) return exact + + const candidates = fonts.filter((x) => x.family === f) + const sameStyle = candidates.find((x) => { + const parsed = parseFontStyle(x.style) + return parsed.weight === requested.weight && parsed.italic === requested.italic + }) + if (sameStyle) return sameStyle + + const sameSlant = candidates.filter((x) => parseFontStyle(x.style).italic === requested.italic) + if (sameSlant.length > 0) { + return sameSlant.sort( + (a, b) => + Math.abs(parseFontStyle(a.style).weight - requested.weight) - + Math.abs(parseFontStyle(b.style).weight - requested.weight) + )[0] + } + + if (!style && candidates.length > 0) return candidates[0] + } + + return undefined +} + const BUNDLED_FONTS: Record = { 'Inter|Regular': '/Inter-Regular.ttf', 'Inter|Medium': '/Inter-Medium.ttf', @@ -448,17 +486,7 @@ export class FontManager { if (!IS_BROWSER || !window.queryLocalFonts) return null try { const fonts = await window.queryLocalFonts() - const families = [family] - const normalized = normalizeFontFamily(family) - if (normalized !== family) families.push(normalized) - - let match: (typeof fonts)[number] | undefined - for (const f of families) { - match = style ? fonts.find((x) => x.family === f && x.style === style) : undefined - match ??= fonts.find((x) => x.family === f) - if (match) break - } - + const match = chooseLocalFontMatch(fonts, family, style) if (!match) return null const blob: Blob = await match.blob() const buffer = await blob.arrayBuffer() diff --git a/tests/engine/text/fonts/loading.test.ts b/tests/engine/text/fonts/loading.test.ts index a4bd0fa25..f53ddacad 100644 --- a/tests/engine/text/fonts/loading.test.ts +++ b/tests/engine/text/fonts/loading.test.ts @@ -3,6 +3,7 @@ import { describe, test, expect } from 'bun:test' import type { CanvasKit, TypefaceFontProvider } from 'canvaskit-wasm' import { + chooseLocalFontMatch, fontManager, isVariableFont, normalizeFontFamily, @@ -91,6 +92,24 @@ function createRecordingProvider() { return { provider, registrations } } +describe('chooseLocalFontMatch', () => { + const fonts = [ + { family: 'Inter', style: 'Italic' }, + { family: 'Inter', style: 'Regular' }, + { family: 'Inter', style: 'Semi Bold' }, + { family: 'Inter', style: 'Bold Italic' } + ] + + test('prefers parsed weight and slant over first family match', () => { + expect(chooseLocalFontMatch(fonts, 'Inter', 'SemiBold')?.style).toBe('Semi Bold') + expect(chooseLocalFontMatch(fonts, 'Inter', 'Regular')?.style).toBe('Regular') + }) + + test('does not fall back from upright requests to italic when upright faces exist', () => { + expect(chooseLocalFontMatch(fonts, 'Inter', 'Bold')?.style).toBe('Semi Bold') + }) +}) + describe('FontManager loaded font cache', () => { test('marks and checks font', () => { const family = `TestFont_${Date.now()}`