fix(markdown): embed used @font-face rules in exported SVGs
The exported SVG carries font-family="cmr7"/"cmsy7"/… attributes but no @font-face, so standalone viewers fall back to system fonts. That scatters TikZ glyphs (precomputed x positions assume Computer Modern metrics) and turns cmsy7 minus signs into ¡ (custom symbol encoding). Inline only the rules for font-families the SVG actually uses, and await document.fonts.ready before rasterizing to PNG.
This commit is contained in:
@@ -3,6 +3,55 @@ const SVG_NS = "http://www.w3.org/2000/svg"
|
||||
|
||||
const TABLER_DOWNLOAD_ICON = `<svg xmlns="${SVG_NS}" class="icon icon-tabler icon-tabler-download" width="14" height="14" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2"/><path d="M7 11l5 5l5 -5"/><path d="M12 4l0 12"/></svg>`
|
||||
|
||||
const stripQuotes = (s: string): string => s.trim().replace(/^["']|["']$/g, "")
|
||||
|
||||
const collectFontFamilies = (svg: SVGSVGElement): Set<string> => {
|
||||
const families = new Set<string>()
|
||||
const collect = (raw: string | null) => {
|
||||
if (!raw) return
|
||||
raw.split(",").forEach((part) => {
|
||||
const cleaned = stripQuotes(part)
|
||||
if (cleaned) families.add(cleaned)
|
||||
})
|
||||
}
|
||||
collect(svg.getAttribute("font-family"))
|
||||
svg.querySelectorAll<Element>("[font-family]").forEach((el) =>
|
||||
collect(el.getAttribute("font-family"))
|
||||
)
|
||||
return families
|
||||
}
|
||||
|
||||
const isFontFaceRule = (rule: CSSRule): boolean => {
|
||||
if (typeof CSSFontFaceRule !== "undefined" && rule instanceof CSSFontFaceRule)
|
||||
return true
|
||||
return rule.cssText.trimStart().startsWith("@font-face")
|
||||
}
|
||||
|
||||
const collectFontFaceCss = (families: Set<string>): string => {
|
||||
if (families.size === 0) return ""
|
||||
const parts: string[] = []
|
||||
for (const sheet of Array.from(document.styleSheets)) {
|
||||
let rules: CSSRuleList | null = null
|
||||
try {
|
||||
rules = sheet.cssRules
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if (!rules) continue
|
||||
for (const rule of Array.from(rules)) {
|
||||
if (!isFontFaceRule(rule)) continue
|
||||
const styleRule = rule as CSSFontFaceRule
|
||||
const family = stripQuotes(
|
||||
styleRule.style?.getPropertyValue("font-family") ?? ""
|
||||
)
|
||||
if (family && families.has(family)) {
|
||||
parts.push(rule.cssText)
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts.join("\n")
|
||||
}
|
||||
|
||||
const getSvgPixelSize = (svg: SVGSVGElement): { width: number; height: number } => {
|
||||
const viewBox = svg.viewBox?.baseVal
|
||||
if (viewBox && viewBox.width > 0 && viewBox.height > 0) {
|
||||
@@ -36,6 +85,14 @@ const buildExportableSvgString = (svg: SVGSVGElement): string => {
|
||||
bg.setAttribute("fill", "#ffffff")
|
||||
clone.insertBefore(bg, clone.firstChild)
|
||||
|
||||
const fontFaceCss = collectFontFaceCss(collectFontFamilies(svg))
|
||||
if (fontFaceCss) {
|
||||
const style = document.createElementNS(SVG_NS, "style")
|
||||
style.setAttribute("type", "text/css")
|
||||
style.textContent = fontFaceCss
|
||||
clone.insertBefore(style, clone.firstChild)
|
||||
}
|
||||
|
||||
return new XMLSerializer().serializeToString(clone)
|
||||
}
|
||||
|
||||
@@ -67,6 +124,9 @@ const downloadAsPng = async (
|
||||
const svgUrl = URL.createObjectURL(svgBlob)
|
||||
|
||||
try {
|
||||
if (document.fonts?.ready) {
|
||||
await document.fonts.ready
|
||||
}
|
||||
const img = new Image()
|
||||
img.crossOrigin = "anonymous"
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
|
||||
Reference in New Issue
Block a user