import postcss from 'postcss'; import { getFontURLs, getWoff2BaseURL } from '../renderer'; import { createElement, decodeFontFamily, fetchWithCache, join, normalizeFontWeightName, splitFontFamily, } from '../utils'; interface FontFaceAttributes { 'font-family': string; src: string; 'font-style': string; 'font-display': string; 'font-weight': string; 'unicode-range': string; } export async function embedFonts(svg: SVGSVGElement, embedResources = true) { // 1. 收集使用到的 font-family const usedFonts = collectUsedFonts(svg); if (usedFonts.size === 0) return; const parsedFontsFaces: FontFaceAttributes[] = []; // 2. 对每个使用到的字体,解析 CSS + 结合 document.fonts 的实际加载子集 await Promise.all( Array.from(usedFonts).map(async (fontFamily) => { const loadedFonts = getActualLoadedFontFace(fontFamily); if (!loadedFonts.length) return; const cssFontFaces = await parseFontFamily(fontFamily); if (!cssFontFaces.length) return; const processed = await Promise.all( cssFontFaces.map(async (rawFace) => { const fontFace = normalizeFontFace(rawFace); const unicodeRange = fontFace['unicode-range'].replace(/\s/g, ''); const subset = loadedFonts.find( (font) => font.unicodeRange && font.unicodeRange.replace(/\s/g, '') === unicodeRange, ); // 如果找不到对应子集,就不处理这个 font-face if (!subset) return null; const baseURL = getWoff2BaseURL( fontFace['font-family'], normalizeFontWeightName(fontFace['font-weight']), ); if (!baseURL) return null; // 更宽松地从 src 中提取 .woff2 URL 片段 const urlMatch = fontFace.src.match( /url\(["']?(.*?\.woff2)[^"']*["']?\)/, ); if (!urlMatch?.[1]) return null; const woff2URL = join(baseURL, urlMatch[1]); if (embedResources) { const woff2DataUrl = await loadWoff2(woff2URL); fontFace.src = `url(${woff2DataUrl}) format('woff2')`; } else { fontFace.src = `url(${woff2URL}) format('woff2')`; } return fontFace; }), ); parsedFontsFaces.push( ...((processed.filter(Boolean) as FontFaceAttributes[]) || []), ); }), ); // 3. 创建 并插入 SVG if (parsedFontsFaces.length > 0) { insertFontStyle(svg, parsedFontsFaces); } } /** * 收集 SVG 中用到的 font-family */ function collectUsedFonts(svg: SVGSVGElement) { const usedFonts = new Set(); const addFamilies = (fontFamilyString: string | null | undefined) => { if (!fontFamilyString) return; splitFontFamily(fontFamilyString).forEach((family) => { const decodedFontFamily = decodeFontFamily(family); if (decodedFontFamily) usedFonts.add(decodedFontFamily); }); }; addFamilies(svg.getAttribute('font-family')); const textElements = svg.querySelectorAll('foreignObject span'); for (const span of textElements) { addFamilies(span.style.fontFamily); } return usedFonts; } /** * 解析给定 font-family 对应的 CSS @font-face */ async function parseFontFamily(fontFamily: string) { const urls = getFontURLs(fontFamily); const fontFaces: Partial[] = []; await Promise.allSettled( urls.map(async (url) => { const cssText = await fetchWithCache(url) .then((res) => res.text()) .catch(() => { console.error(`Failed to fetch font CSS: ${url}`); return null; }); if (!cssText) return; try { const root = postcss.parse(cssText); root.walkAtRules('font-face', (rule) => { const fontFace: Record = {}; rule.walkDecls((decl) => { fontFace[decl.prop] = decl.value; }); fontFaces.push(fontFace as Partial); }); } catch (error) { console.error(`Failed to parse CSS: ${url}`, error); } }), ); return fontFaces; } /** * 从 document.fonts 中获取给定 family 且已加载的 FontFace */ export function getActualLoadedFontFace(fontFamily: string) { const fonts: FontFace[] = []; const families = splitFontFamily(fontFamily).map((family) => decodeFontFamily(family), ); document.fonts.forEach((font) => { if ( families.includes(decodeFontFamily(font.family)) && font.status === 'loaded' ) { fonts.push(font); } }); return fonts; } /** * 将不完整的 FontFaceAttributes 补全为完整结构,给后续逻辑使用 */ function normalizeFontFace( face: Partial, ): FontFaceAttributes { return { 'font-family': face['font-family'] ?? '', src: face.src ?? '', 'font-style': face['font-style'] ?? 'normal', 'font-display': face['font-display'] ?? 'swap', 'font-weight': face['font-weight'] ?? '400', 'unicode-range': face['unicode-range'] ?? 'U+0-FFFF', }; } /** * 将 @font-face 写入