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 写入