From efc0db7a7983c5cf927d9defcb877228f05f48c6 Mon Sep 17 00:00:00 2001 From: tuhm1 <50200070+tuhm1@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:41:29 +0000 Subject: [PATCH] perf: embed only used fonts --- src/embed-webfonts.ts | 38 +++++++- test/resources/fonts/web-fonts/empty.html | 1 + test/spec/webfont.spec.ts | 107 ++++++++++++++++++++++ 3 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 test/spec/webfont.spec.ts diff --git a/src/embed-webfonts.ts b/src/embed-webfonts.ts index 42c73da6..e39c6643 100644 --- a/src/embed-webfonts.ts +++ b/src/embed-webfonts.ts @@ -202,16 +202,46 @@ async function parseWebFontRules( return getWebFontRules(cssRules) } +function normalizeFontFamily(font: string) { + return font.trim().replace(/["']/g, '') +} + +function getUsedFonts(node: HTMLElement) { + const fonts = new Set() + function traverse(node: HTMLElement) { + const fontFamily = + node.style.fontFamily || getComputedStyle(node).fontFamily + fontFamily.split(',').forEach((font) => { + fonts.add(normalizeFontFamily(font)) + }) + + Array.from(node.children).forEach((child) => { + if (child instanceof HTMLElement) { + traverse(child) + } + }) + } + traverse(node) + return fonts +} + export async function getWebFontCSS( node: T, options: Options, ): Promise { const rules = await parseWebFontRules(node, options) + const usedFonts = getUsedFonts(node) const cssTexts = await Promise.all( - rules.map((rule) => { - const baseUrl = rule.parentStyleSheet ? rule.parentStyleSheet.href : null - return embedResources(rule.cssText, baseUrl, options) - }), + rules + .filter((rule) => + usedFonts.has(normalizeFontFamily(rule.style.fontFamily)), + ) + .map((rule) => { + const baseUrl = rule.parentStyleSheet + ? rule.parentStyleSheet.href + : null + return embedResources(rule.cssText, baseUrl, options) + }), ) return cssTexts.join('\n') diff --git a/test/resources/fonts/web-fonts/empty.html b/test/resources/fonts/web-fonts/empty.html index e69de29b..8b2ae22f 100644 --- a/test/resources/fonts/web-fonts/empty.html +++ b/test/resources/fonts/web-fonts/empty.html @@ -0,0 +1 @@ +
diff --git a/test/spec/webfont.spec.ts b/test/spec/webfont.spec.ts new file mode 100644 index 00000000..5be025d2 --- /dev/null +++ b/test/spec/webfont.spec.ts @@ -0,0 +1,107 @@ +import * as htmlToImage from '../../src' +import { getSvgDocument } from './helper' + +describe('font embedding', () => { + describe('should embed only used fonts', () => { + it('should embed 1 font when use 1', async () => { + const root = document.createElement('div') + document.body.append(root) + try { + root.innerHTML = ` + +

Hello world

+ ` + const svg = await htmlToImage.toSvg(root) + const doc = await getSvgDocument(svg) + const [style] = Array.from(doc.getElementsByTagName('style')) + expect(style.textContent).toContain('Font 1') + expect(style.textContent).not.toContain('Font 0') + expect(style.textContent).not.toContain('Font 2') + } finally { + root.remove() + } + }) + it('should embed 2 fonts when use 2', async () => { + const root = document.createElement('div') + document.body.append(root) + try { + root.innerHTML = ` + +

Hello world

+

Hello world

+ ` + const svg = await htmlToImage.toSvg(root) + const doc = await getSvgDocument(svg) + const [style] = Array.from(doc.getElementsByTagName('style')) + expect(style.textContent).toContain('Font 0') + expect(style.textContent).toContain('Font 2') + expect(style.textContent).not.toContain('Font 1') + } finally { + root.remove() + } + }) + it('should embed font used by deeply nested child', async () => { + const root = document.createElement('div') + document.body.append(root) + try { + root.innerHTML = ` + +
+
+
+
Hello world
+
+
+
+ ` + const svg = await htmlToImage.toSvg(root) + const doc = await getSvgDocument(svg) + const [style] = Array.from(doc.getElementsByTagName('style')) + expect(style.textContent).toContain('Font 1') + expect(style.textContent).not.toContain('Font 0') + expect(style.textContent).not.toContain('Font 2') + } finally { + root.remove() + } + }) + }) +})