From 2761bd67159fc916e6f3c19ce51aa070bc910344 Mon Sep 17 00:00:00 2001 From: Santhi Prakash <38608178+santhiprakash@users.noreply.github.com> Date: Thu, 27 Aug 2026 06:45:31 +0530 Subject: [PATCH] fix(fonts): register Vazirmatn in webfont catalog for JSON imports (#3331) * fix(fonts): register Vazirmatn in webfont catalog for JSON imports Imported resumes can set typography.fontFamily to Vazirmatn, but the popularity-sorted Google Fonts slice omits it so PDF registration fell back to IBM Plex Serif and Persian/Arabic glyphs stacked or tofu (#3098). Add Vazirmatn as a locale-coverage manual entry (same pattern as Carlito) and cover catalog resolution with unit tests. * test(pdf): keep Vazirmatn as primary family for fa-IR registration Prove JSON-imported Vazirmatn is handed to Font.register instead of being rewritten to IBM Plex Serif (#3098). * fix(fonts): address CodeRabbit review on Vazirmatn catalog Assert getWebFontSource resolves files["400"] for Vazirmatn instead of only matching the preview fallback, and split the font-generation log line to satisfy the 120-column Biome limit. --- packages/fonts/src/index.test.ts | 17 +++++++ packages/fonts/src/webfontlist.json | 18 +++++++ .../pdf/src/hooks/use-register-fonts.test.ts | 19 +++++++ tooling/fonts/generate.ts | 50 +++++++++++++++++-- 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/packages/fonts/src/index.test.ts b/packages/fonts/src/index.test.ts index 861540e7d..1ed4fd63d 100644 --- a/packages/fonts/src/index.test.ts +++ b/packages/fonts/src/index.test.ts @@ -289,3 +289,20 @@ describe("legacy font compatibility (#2989)", () => { expect(getFontDisplayName("Times New Roman")).toBe("Times New Roman"); }); }); + +describe("locale coverage fonts (#3098)", () => { + it("resolves Vazirmatn from the webfont catalog", () => { + expect(getFont("Vazirmatn")?.family).toBe("Vazirmatn"); + expect(getWebFont("Vazirmatn")?.category).toBe("sans-serif"); + }); + + it("returns a gstatic source for Vazirmatn regular so PDF registration can load it", () => { + const regularSource = getWebFont("Vazirmatn")?.files["400"]; + expect(regularSource).toBeDefined(); + + const source = getWebFontSource("Vazirmatn", "400"); + expect(source).toBe(regularSource); + expect(source).toEqual(expect.stringContaining("fonts.gstatic.com")); + expect(source).toEqual(expect.stringMatching(/\.ttf(\?|$)/)); + }); +}); diff --git a/packages/fonts/src/webfontlist.json b/packages/fonts/src/webfontlist.json index a521c9dfe..fc36587b0 100644 --- a/packages/fonts/src/webfontlist.json +++ b/packages/fonts/src/webfontlist.json @@ -7522,5 +7522,23 @@ "400italic": "https://fonts.gstatic.com/s/carlito/v4/3Jn_SDPw3m-pk039DDKxTl0F.ttf", "700italic": "https://fonts.gstatic.com/s/carlito/v4/3Jn6SDPw3m-pk039DDK59XgVUcBD.ttf" } + }, + { + "type": "web", + "category": "sans-serif", + "family": "Vazirmatn", + "weights": ["100", "200", "300", "400", "500", "600", "700", "800", "900"], + "preview": "https://fonts.gstatic.com/s/vazirmatn/v16/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklWgzCRCT60LA7Qdazg.ttf", + "files": { + "100": "https://fonts.gstatic.com/s/vazirmatn/v16/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklWgyCRCT60LA7Qdazg.ttf", + "200": "https://fonts.gstatic.com/s/vazirmatn/v16/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklegzCRCT60LA7Qdazg.ttf", + "300": "https://fonts.gstatic.com/s/vazirmatn/v16/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklTYzCRCT60LA7Qdazg.ttf", + "400": "https://fonts.gstatic.com/s/vazirmatn/v16/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklWgzCRCT60LA7Qdazg.ttf", + "500": "https://fonts.gstatic.com/s/vazirmatn/v16/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklVozCRCT60LA7Qdazg.ttf", + "600": "https://fonts.gstatic.com/s/vazirmatn/v16/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklbY0CRCT60LA7Qdazg.ttf", + "700": "https://fonts.gstatic.com/s/vazirmatn/v16/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklY80CRCT60LA7Qdazg.ttf", + "800": "https://fonts.gstatic.com/s/vazirmatn/v16/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRkleg0CRCT60LA7Qdazg.ttf", + "900": "https://fonts.gstatic.com/s/vazirmatn/v16/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklcE0CRCT60LA7Qdazg.ttf" + } } ] diff --git a/packages/pdf/src/hooks/use-register-fonts.test.ts b/packages/pdf/src/hooks/use-register-fonts.test.ts index 5824d9587..fc99dda32 100644 --- a/packages/pdf/src/hooks/use-register-fonts.test.ts +++ b/packages/pdf/src/hooks/use-register-fonts.test.ts @@ -299,6 +299,25 @@ describe("registerFonts", () => { expect(registerSpy).not.toHaveBeenCalledWith(expect.objectContaining({ family: "Lato", fontWeight: 600 })); expect(registerSpy).toHaveBeenCalledWith(expect.objectContaining({ family: "Lato", fontWeight: 700 })); }); + + it("keeps Vazirmatn as the primary PDF family instead of substituting IBM Plex Serif (#3098)", async () => { + const registerSpy = vi.spyOn(Font, "register").mockImplementation(() => {}); + vi.spyOn(Font, "registerHyphenationCallback").mockImplementation(() => {}); + const { registerFonts } = await import("./use-register-fonts"); + + const vazirTypography = { + ...typography, + body: { ...typography.body, fontFamily: "Vazirmatn", fontWeights: ["400"] }, + heading: { ...typography.heading, fontFamily: "Vazirmatn", fontWeights: ["700"] }, + } satisfies Typography; + + const pdfTypography = registerFonts(vazirTypography, "fa-IR"); + + expect(pdfTypography.body.fontFamily).toEqual(["Vazirmatn", "Noto Sans Arabic", "Noto Sans"]); + expect(pdfTypography.heading.fontFamily).toEqual(["Vazirmatn", "Noto Sans Arabic", "Noto Sans"]); + expect(registerSpy).toHaveBeenCalledWith(expect.objectContaining({ family: "Vazirmatn", fontWeight: 400 })); + expect(registerSpy).toHaveBeenCalledWith(expect.objectContaining({ family: "Vazirmatn", fontWeight: 700 })); + }); }); describe("resumeContentContainsCJK", () => { diff --git a/tooling/fonts/generate.ts b/tooling/fonts/generate.ts index fdf9fa86b..6c09203d5 100644 --- a/tooling/fonts/generate.ts +++ b/tooling/fonts/generate.ts @@ -160,6 +160,37 @@ function getMetricCompatibleFonts(): WebFont[] { ]; } +/** + * Helper: locale-coverage web fonts that aren't in the popularity-sorted + * Google Fonts slice but are stored as `typography.*.fontFamily` in imported + * resume JSON. Without a catalog entry, PDF registration silently substitutes + * IBM Plex Serif and Persian/Arabic glyphs stack or tofu (#3098). + */ +function getLocaleCoverageFonts(): WebFont[] { + const CDN = "https://fonts.gstatic.com/s/vazirmatn/v16"; + + return [ + { + type: "web", + category: "sans-serif", + family: "Vazirmatn", + weights: ["100", "200", "300", "400", "500", "600", "700", "800", "900"], + preview: `${CDN}/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklWgzCRCT60LA7Qdazg.ttf`, + files: { + "100": `${CDN}/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklWgyCRCT60LA7Qdazg.ttf`, + "200": `${CDN}/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklegzCRCT60LA7Qdazg.ttf`, + "300": `${CDN}/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklTYzCRCT60LA7Qdazg.ttf`, + "400": `${CDN}/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklWgzCRCT60LA7Qdazg.ttf`, + "500": `${CDN}/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklVozCRCT60LA7Qdazg.ttf`, + "600": `${CDN}/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklbY0CRCT60LA7Qdazg.ttf`, + "700": `${CDN}/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklY80CRCT60LA7Qdazg.ttf`, + "800": `${CDN}/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRkleg0CRCT60LA7Qdazg.ttf`, + "900": `${CDN}/Dxx78j6PP2D_kU2muijPEe1n2vVbfJRklcE0CRCT60LA7Qdazg.ttf`, + }, + }, + ]; +} + async function generateFonts() { const response = await getGoogleFontsJSON(); console.log(`Found ${response.items.length} fonts in total (Google Fonts).`); @@ -196,16 +227,27 @@ async function generateFonts() { const computerModernFonts = getComputerModernWebFonts(); // Manually append metric-compatible fonts not covered by the popularity slice const metricCompatFonts = getMetricCompatibleFonts(); + const localeCoverageFonts = getLocaleCoverageFonts(); // 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 filteredLocaleCoverage = localeCoverageFonts.filter((f) => !googleFontFamilies.has(f.family)); - const allWebFonts: WebFont[] = [...computerModernFonts, ...googleFontResults, ...filteredMetricCompat]; + const allWebFonts: WebFont[] = [ + ...computerModernFonts, + ...googleFontResults, + ...filteredMetricCompat, + ...filteredLocaleCoverage, + ]; - console.log( - `Added ${computerModernFonts.length} Computer Modern Web Fonts and ${filteredMetricCompat.length} metric-compatible fonts. Total output: ${allWebFonts.length} web fonts.`, - ); + const manualFontSummary = [ + `${computerModernFonts.length} Computer Modern Web Fonts`, + `${filteredMetricCompat.length} metric-compatible fonts`, + `and ${filteredLocaleCoverage.length} locale-coverage fonts`, + ].join(", "); + + console.log(`Added ${manualFontSummary}. Total output: ${allWebFonts.length} web fonts.`); const jsonString = argCompress ? JSON.stringify(allWebFonts) : JSON.stringify(allWebFonts, null, 2); await mkdir(FONTS_DIR, { recursive: true });