From 22c60c64b6e6f5500c20d85a93711cfb07136609 Mon Sep 17 00:00:00 2001 From: JamesGoslings <3248175240@qq.com> Date: Thu, 14 May 2026 17:36:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(fonts):=20restore=20legacy=20local=20font?= =?UTF-8?q?=20names=20via=20metric-compatible=20ali=E2=80=A6=20(#3057)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(fonts): restore legacy local font names via metric-compatible aliases Closes #2989. In v5.0.x the Puppeteer renderer resolved fonts like 'Times New Roman' or 'Arial' through the browser's font stack. The v5.1 migration to @react-pdf/renderer requires every font to be Font.register()-ed; the legacy local-font names were not carried over, so resumes upgraded from v5.0.x had their typography silently replaced with IBM Plex Serif, changing line breaks, page counts and overall layout. This adds a render-time alias layer mapping the old names to metric-compatible web fonts already shipped in the webfont list: Times New Roman → Tinos Cambria → Tinos Arial → Arimo Garamond → EB Garamond Calibri → Source Sans 3 - packages/fonts: - new `legacyFontAliases` map and `resolveLegacyFontAlias` helper. - `getFont` falls back to the alias map when the direct lookup misses, so any caller that asked 'is this a known family?' now answers truthfully for the legacy names. - `getFontDisplayName` is intentionally unchanged: the typography sidebar keeps showing the user's original choice ('Times New Roman'), while the renderer transparently swaps in the alias target. - packages/pdf/use-register-fonts: - `resolvePdfFontFamily` returns the alias target when one applies, so `Font.register` runs against the right web font and templates receive a family name they can actually render. Backwards compatible: families that were never aliased (Roboto, IBM Plex Serif, the standard PDF fonts, ...) take exactly the same code path as before. The CJK glyph fallback added in #2986 / PR #3013 continues to apply on top of the resolved primary family. * fix(fonts): use Carlito (not Source Sans 3) as Calibri alias Per maintainer review feedback: Carlito is metric-compatible with Calibri, while Source Sans 3 only matches visually. Switching gives upgraded resumes the same line widths, line breaks and page counts they had under v5.0.x. - packages/fonts/webfontlist.json: add Carlito (Google Fonts, weights 400/700 + italics) so it's a registerable target. - packages/scripts/fonts/generate.ts: add a getMetricCompatibleFonts helper and merge it into the output, mirroring how Computer Modern fonts are appended. This way regenerating the list (`pnpm generate`) re-emits Carlito automatically and dedupes if it ever enters the Google Fonts popularity slice. - packages/fonts/src/index.ts: alias `Calibri → Carlito`. - packages/fonts/src/index.test.ts: update alias test cases. --- packages/fonts/src/index.test.ts | 43 ++++++++++++++++++++ packages/fonts/src/index.ts | 22 +++++++++- packages/fonts/src/webfontlist.json | 13 ++++++ packages/pdf/src/hooks/use-register-fonts.ts | 9 +++- packages/scripts/fonts/generate.ts | 37 ++++++++++++++++- 5 files changed, 120 insertions(+), 4 deletions(-) diff --git a/packages/fonts/src/index.test.ts b/packages/fonts/src/index.test.ts index f7ab41b23..d0521fc43 100644 --- a/packages/fonts/src/index.test.ts +++ b/packages/fonts/src/index.test.ts @@ -11,6 +11,7 @@ import { getWebFont, getWebFontSource, isStandardPdfFontFamily, + resolveLegacyFontAlias, standardFontList, webFontList, webFontMap, @@ -257,3 +258,45 @@ describe("buildResumeFontFamily", () => { expect(result).toContain("Bob\\'s Font"); }); }); + +describe("legacy font compatibility (#2989)", () => { + it.each([ + ["Times New Roman", "Tinos"], + ["Arial", "Arimo"], + ["Garamond", "EB Garamond"], + ["Calibri", "Carlito"], + ["Cambria", "Tinos"], + ])("aliases %s → %s", (legacy, target) => { + expect(resolveLegacyFontAlias(legacy)).toBe(target); + }); + + it("returns null for non-aliased families", () => { + expect(resolveLegacyFontAlias("Roboto")).toBeNull(); + expect(resolveLegacyFontAlias("IBM Plex Serif")).toBeNull(); + expect(resolveLegacyFontAlias("UnknownFont")).toBeNull(); + }); + + it("every alias target is actually registered as a known font", () => { + const aliasTargets = ["Tinos", "Arimo", "EB Garamond", "Carlito"]; + for (const target of aliasTargets) { + expect(getFont(target), `alias target ${target} must be a known font`).toBeDefined(); + } + }); + + it("getFont resolves a legacy family to its alias target", () => { + const tnr = getFont("Times New Roman"); + expect(tnr).toBeDefined(); + expect(tnr?.family).toBe("Tinos"); + }); + + it("getFont still returns the direct font when both legacy and direct lookup would succeed", () => { + // Sanity check: for non-aliased families the direct path is used. + expect(getFont("Roboto")?.family).toBe("Roboto"); + }); + + it("getFontDisplayName preserves the legacy family name (UI is not rewritten)", () => { + // Users who picked "Times New Roman" should keep seeing that label + // in the typography sidebar — the alias is a render-time concern only. + expect(getFontDisplayName("Times New Roman")).toBe("Times New Roman"); + }); +}); diff --git a/packages/fonts/src/index.ts b/packages/fonts/src/index.ts index 38e5e382d..77c061716 100644 --- a/packages/fonts/src/index.ts +++ b/packages/fonts/src/index.ts @@ -123,8 +123,28 @@ for (const font of fontList) { fontMap.set(font.family, font); } +// Compatibility aliases for fonts that v5.0.x resolved via the browser +// (Arial, Times New Roman, ...) but that aren't registered with +// @react-pdf/renderer in v5.1+. Targets are metric-compatible web fonts +// already shipped in webfontlist (#2989). +const legacyFontAliases: Record = { + Arial: "Arimo", + Cambria: "Tinos", + Calibri: "Carlito", + Garamond: "EB Garamond", + "Times New Roman": "Tinos", +}; + +export function resolveLegacyFontAlias(family: string): string | null { + return legacyFontAliases[family] ?? null; +} + export function getFont(family: string) { - return fontMap.get(family); + const direct = fontMap.get(family); + if (direct) return direct; + + const alias = legacyFontAliases[family]; + return alias ? fontMap.get(alias) : undefined; } function getFontCategory(family: string): FontCategory | null { diff --git a/packages/fonts/src/webfontlist.json b/packages/fonts/src/webfontlist.json index 4e1b20bb5..a521c9dfe 100644 --- a/packages/fonts/src/webfontlist.json +++ b/packages/fonts/src/webfontlist.json @@ -7509,5 +7509,18 @@ "weights": ["400"], "preview": "https://fonts.gstatic.com/s/secularone/v14/8QINdiTajsj_87rMuMdKyqDkOO0.ttf", "files": { "400": "https://fonts.gstatic.com/s/secularone/v14/8QINdiTajsj_87rMuMdKypDlMul7LJpK.ttf" } + }, + { + "type": "web", + "category": "sans-serif", + "family": "Carlito", + "weights": ["400", "700"], + "preview": "https://fonts.gstatic.com/s/carlito/v4/3Jn9SDPw3m-pk039DDeBSQ.ttf", + "files": { + "400": "https://fonts.gstatic.com/s/carlito/v4/3Jn9SDPw3m-pk039DDeBSQ.ttf", + "700": "https://fonts.gstatic.com/s/carlito/v4/3Jn4SDPw3m-pk039BIykWXolVg.ttf", + "400italic": "https://fonts.gstatic.com/s/carlito/v4/3Jn_SDPw3m-pk039DDKxTl0F.ttf", + "700italic": "https://fonts.gstatic.com/s/carlito/v4/3Jn6SDPw3m-pk039DDK59XgVUcBD.ttf" + } } ] diff --git a/packages/pdf/src/hooks/use-register-fonts.ts b/packages/pdf/src/hooks/use-register-fonts.ts index 39aa89ad9..e05d293ef 100644 --- a/packages/pdf/src/hooks/use-register-fonts.ts +++ b/packages/pdf/src/hooks/use-register-fonts.ts @@ -8,6 +8,7 @@ import { getPdfCjkFallbackFontFamily, getWebFontSource, isStandardPdfFontFamily, + resolveLegacyFontAlias, sortFontWeights, } from "@reactive-resume/fonts"; import { isCJKLocale } from "@reactive-resume/utils/locale"; @@ -51,8 +52,14 @@ const toFontWeight = (weight: number): FontWeight => { return "900"; }; +// Resolves the user-stored family to the one we hand to Font.register: +// direct match → legacy alias (#2989) → IBM Plex Serif fallback. const resolvePdfFontFamily = (family: string) => { - return getFont(family) ? family : fallbackFontFamily; + if (getFont(family)) { + const alias = resolveLegacyFontAlias(family); + return alias ?? family; + } + return fallbackFontFamily; }; const resolvePdfTypography = (typography: Typography): Typography => { diff --git a/packages/scripts/fonts/generate.ts b/packages/scripts/fonts/generate.ts index 05fcae6a6..05d1834c2 100644 --- a/packages/scripts/fonts/generate.ts +++ b/packages/scripts/fonts/generate.ts @@ -134,6 +134,32 @@ function getComputerModernWebFonts(): WebFont[] { ]; } +/** + * Helper: Additional metric-compatible web fonts that aren't in the + * popularity-sorted Google Fonts slice but are needed as render targets + * for legacy local-font aliases (#2989). Carlito is the open-source + * metric-compatible equivalent of Calibri. + */ +function getMetricCompatibleFonts(): WebFont[] { + const CDN = "https://fonts.gstatic.com/s/carlito/v4"; + + return [ + { + type: "web", + category: "sans-serif", + family: "Carlito", + weights: ["400", "700"], + preview: `${CDN}/3Jn9SDPw3m-pk039DDeBSQ.ttf`, + files: { + "400": `${CDN}/3Jn9SDPw3m-pk039DDeBSQ.ttf`, + "700": `${CDN}/3Jn4SDPw3m-pk039BIykWXolVg.ttf`, + "400italic": `${CDN}/3Jn_SDPw3m-pk039DDKxTl0F.ttf`, + "700italic": `${CDN}/3Jn6SDPw3m-pk039DDK59XgVUcBD.ttf`, + }, + }, + ]; +} + export async function generateFonts() { const response = await getGoogleFontsJSON(); console.log(`Found ${response.items.length} fonts in total (Google Fonts).`); @@ -168,10 +194,17 @@ export async function generateFonts() { // Manually append Computer Modern web fonts const computerModernFonts = getComputerModernWebFonts(); - const allWebFonts: WebFont[] = [...computerModernFonts, ...googleFontResults]; + // Manually append metric-compatible fonts not covered by the popularity slice + const metricCompatFonts = getMetricCompatibleFonts(); + // De-duplicate against the Google Fonts slice in case a manual entry + // later enters the popularity top-N. + const googleFontFamilies = new Set(googleFontResults.map((f) => f.family)); + const filteredMetricCompat = metricCompatFonts.filter((f) => !googleFontFamilies.has(f.family)); + + const allWebFonts: WebFont[] = [...computerModernFonts, ...googleFontResults, ...filteredMetricCompat]; console.log( - `Added ${computerModernFonts.length} Computer Modern Web Fonts. Total output: ${allWebFonts.length} web fonts.`, + `Added ${computerModernFonts.length} Computer Modern Web Fonts and ${filteredMetricCompat.length} metric-compatible fonts. Total output: ${allWebFonts.length} web fonts.`, ); const jsonString = argCompress ? JSON.stringify(allWebFonts) : JSON.stringify(allWebFonts, null, 2);