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 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 getSvgPixelSize = (svg: SVGSVGElement): { width: number; height: number } => {
|
||||||
const viewBox = svg.viewBox?.baseVal
|
const viewBox = svg.viewBox?.baseVal
|
||||||
if (viewBox && viewBox.width > 0 && viewBox.height > 0) {
|
if (viewBox && viewBox.width > 0 && viewBox.height > 0) {
|
||||||
@@ -36,6 +85,14 @@ const buildExportableSvgString = (svg: SVGSVGElement): string => {
|
|||||||
bg.setAttribute("fill", "#ffffff")
|
bg.setAttribute("fill", "#ffffff")
|
||||||
clone.insertBefore(bg, clone.firstChild)
|
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)
|
return new XMLSerializer().serializeToString(clone)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +124,9 @@ const downloadAsPng = async (
|
|||||||
const svgUrl = URL.createObjectURL(svgBlob)
|
const svgUrl = URL.createObjectURL(svgBlob)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (document.fonts?.ready) {
|
||||||
|
await document.fonts.ready
|
||||||
|
}
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
img.crossOrigin = "anonymous"
|
img.crossOrigin = "anonymous"
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user