import type { Locale, Script } from "@reactive-resume/utils/locale"; import { getLocaleScript, isCjkScript } from "@reactive-resume/utils/locale"; // ponytail: inlined from @reactive-resume/utils/field (sole consumer) const unique = (items: T[]): T[] => [...new Set(items)]; import webFontListJSON from "./webfontlist.json"; type FontCategory = "display" | "handwriting" | "monospace" | "serif" | "sans-serif"; export type FontWeight = "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900"; type FontFileWeight = FontWeight | `${FontWeight}italic`; type StandardFont = { type: "standard"; category: FontCategory; family: string; weights: FontWeight[]; }; export type WebFont = { type: "web"; category: FontCategory; family: string; weights: FontWeight[]; preview: string; files: Record; }; type FontRecord = StandardFont | WebFont; const preferredChineseFontFamilies = [ "Noto Sans SC", "Noto Serif SC", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Source Han Serif SC", "Songti SC", "SimSun", "SimHei", "KaiTi", "FangSong", "ZCOOL QingKe HuangYou", ] as const; const standardPdfFontList = [ { type: "standard", category: "sans-serif", family: "Helvetica", weights: ["400", "700"] }, { type: "standard", category: "monospace", family: "Courier", weights: ["400", "700"] }, { type: "standard", category: "serif", family: "Times-Roman", weights: ["400", "700"] }, ] satisfies StandardFont[]; const fontDisplayNames: Partial> = { FangSong: "仿宋", "Hiragino Sans GB": "冬青黑体简体中文", KaiTi: "楷体", "Microsoft YaHei": "微软雅黑", "Noto Sans SC": "思源黑体", "Noto Sans TC": "思源黑体(繁中)", "Noto Serif SC": "思源宋体", "Noto Serif TC": "思源宋体(繁中)", "PingFang SC": "苹方", SimHei: "黑体", SimSun: "宋体", "Songti SC": "华文宋体", "Source Han Sans SC": "思源黑体(本地)", "Source Han Serif SC": "思源宋体(本地)", "ZCOOL QingKe HuangYou": "站酷庆科黄油体", }; // Per-script Noto web font, split by serif/sans category. These match the // actual writing system: Hangul lives only in the KR fonts, Kana only in JP, // Arabic glyphs only in the Arabic fonts, etc. — so a Latin or Simplified- // Chinese font cannot render them and produces tofu. Where Noto ships no serif // variant for a script (Hebrew, Thai), the serif slot reuses the sans font so // serif resumes still render real glyphs instead of nothing. All entries are // present in webfontlist.json. const scriptFonts: Record = { hangul: { serif: "Noto Serif KR", sansSerif: "Noto Sans KR" }, kana: { serif: "Noto Serif JP", sansSerif: "Noto Sans JP" }, "han-traditional": { serif: "Noto Serif TC", sansSerif: "Noto Sans TC" }, "han-simplified": { serif: "Noto Serif SC", sansSerif: "Noto Sans SC" }, arabic: { serif: "Noto Naskh Arabic", sansSerif: "Noto Sans Arabic" }, hebrew: { serif: "Noto Sans Hebrew", sansSerif: "Noto Sans Hebrew" }, thai: { serif: "Noto Sans Thai", sansSerif: "Noto Sans Thai" }, }; export const webFontList = webFontListJSON as WebFont[]; export const webFontMap = new Map(webFontList.map((font) => [font.family, font])); export const standardFontList = standardPdfFontList.filter((font) => !webFontMap.has(font.family)); const fontMap = new Map(); const chinesePrioritySet = new Set(preferredChineseFontFamilies); function orderFonts(fonts: FontRecord[]) { return [...fonts].sort((a, b) => { return a.family.localeCompare(b.family, undefined, { sensitivity: "base" }); }); } export const fontList = orderFonts([...standardFontList, ...webFontList]); 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": "Times-Roman", }; export function resolveLegacyFontAlias(family: string): string | null { return legacyFontAliases[family] ?? null; } export function getFont(family: string) { 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 { return getFont(family)?.category ?? null; } export function getFontDisplayName(family: string) { return fontDisplayNames[family] ?? family; } export function getFontSearchKeywords(family: string) { return unique( [family, fontDisplayNames[family], chinesePrioritySet.has(family) ? "中文" : undefined].filter( (keyword): keyword is string => Boolean(keyword), ), ); } function getScriptFont(script: Script, category: FontCategory | null) { const variants = scriptFonts[script]; return category === "serif" ? variants.serif : variants.sansSerif; } export function isStandardPdfFontFamily(family: string) { return standardFontList.some((font) => font.family === family); } export function getWebFont(family: string) { return webFontMap.get(family); } export function getWebFontSource(family: string, weight: FontWeight = "400", italic = false) { const webFont = getWebFont(family); if (!webFont) return null; const key = `${weight}${italic ? "italic" : ""}` as FontFileWeight; return webFont.files[key] ?? (italic ? webFont.files[weight] : undefined) ?? webFont.preview; } export function sortFontWeights(fontWeights: T[]): T[] { return [...fontWeights].sort((a, b) => Number(a) - Number(b)); } /** * Returns an ordered stack of Noto web fonts to register as glyph-level * fallbacks for PDF rendering. react-pdf resolves the font per-codepoint * left-to-right across the stack, so listing one font per writing system lets * a single resume mix Latin with Hangul, Kana, Han, Arabic, Hebrew or Thai. * * Ordering: the locale's primary script first (the dominant language), then * any other scripts actually detected in the content. When the stack contains * a CJK script, a Simplified Chinese entry is appended as a safety net for * stray CJK-Unified ideographs (preserving prior behavior); non-CJK scripts * get no such net. The result is deduped, has the primary family removed, and * only keeps fonts that exist in the webfontlist. */ export function getPdfFallbackFontFamilies( family: string, options: { locale?: Locale; scripts?: Iterable