Skip to content

Commit 09bee44

Browse files
authored
perf: embed only used fonts (#476)
1 parent 4b4a362 commit 09bee44

File tree

3 files changed

+142
-4
lines changed

3 files changed

+142
-4
lines changed

src/embed-webfonts.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,16 +202,46 @@ async function parseWebFontRules<T extends HTMLElement>(
202202
return getWebFontRules(cssRules)
203203
}
204204

205+
function normalizeFontFamily(font: string) {
206+
return font.trim().replace(/["']/g, '')
207+
}
208+
209+
function getUsedFonts(node: HTMLElement) {
210+
const fonts = new Set<string>()
211+
function traverse(node: HTMLElement) {
212+
const fontFamily =
213+
node.style.fontFamily || getComputedStyle(node).fontFamily
214+
fontFamily.split(',').forEach((font) => {
215+
fonts.add(normalizeFontFamily(font))
216+
})
217+
218+
Array.from(node.children).forEach((child) => {
219+
if (child instanceof HTMLElement) {
220+
traverse(child)
221+
}
222+
})
223+
}
224+
traverse(node)
225+
return fonts
226+
}
227+
205228
export async function getWebFontCSS<T extends HTMLElement>(
206229
node: T,
207230
options: Options,
208231
): Promise<string> {
209232
const rules = await parseWebFontRules(node, options)
233+
const usedFonts = getUsedFonts(node)
210234
const cssTexts = await Promise.all(
211-
rules.map((rule) => {
212-
const baseUrl = rule.parentStyleSheet ? rule.parentStyleSheet.href : null
213-
return embedResources(rule.cssText, baseUrl, options)
214-
}),
235+
rules
236+
.filter((rule) =>
237+
usedFonts.has(normalizeFontFamily(rule.style.fontFamily)),
238+
)
239+
.map((rule) => {
240+
const baseUrl = rule.parentStyleSheet
241+
? rule.parentStyleSheet.href
242+
: null
243+
return embedResources(rule.cssText, baseUrl, options)
244+
}),
215245
)
216246

217247
return cssTexts.join('\n')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div style="font-family: Font1"></div>

test/spec/webfont.spec.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import * as htmlToImage from '../../src'
2+
import { getSvgDocument } from './helper'
3+
4+
describe('font embedding', () => {
5+
describe('should embed only used fonts', () => {
6+
it('should embed 1 font when use 1', async () => {
7+
const root = document.createElement('div')
8+
document.body.append(root)
9+
try {
10+
root.innerHTML = `
11+
<style>
12+
@font-face {
13+
font-family: 'Font 0';
14+
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
15+
}
16+
@font-face {
17+
font-family: 'Font 1';
18+
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
19+
}
20+
@font-face {
21+
font-family: 'Font 2';
22+
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
23+
}
24+
</style>
25+
<p style="font-family: 'Font 1'">Hello world</p>
26+
`
27+
const svg = await htmlToImage.toSvg(root)
28+
const doc = await getSvgDocument(svg)
29+
const [style] = Array.from(doc.getElementsByTagName('style'))
30+
expect(style.textContent).toContain('Font 1')
31+
expect(style.textContent).not.toContain('Font 0')
32+
expect(style.textContent).not.toContain('Font 2')
33+
} finally {
34+
root.remove()
35+
}
36+
})
37+
it('should embed 2 fonts when use 2', async () => {
38+
const root = document.createElement('div')
39+
document.body.append(root)
40+
try {
41+
root.innerHTML = `
42+
<style>
43+
@font-face {
44+
font-family: 'Font 0';
45+
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
46+
}
47+
@font-face {
48+
font-family: 'Font 1';
49+
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
50+
}
51+
@font-face {
52+
font-family: 'Font 2';
53+
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
54+
}
55+
</style>
56+
<p style="font-family: 'Font 0'">Hello world</p>
57+
<p style="font-family: 'Font 2'">Hello world</p>
58+
`
59+
const svg = await htmlToImage.toSvg(root)
60+
const doc = await getSvgDocument(svg)
61+
const [style] = Array.from(doc.getElementsByTagName('style'))
62+
expect(style.textContent).toContain('Font 0')
63+
expect(style.textContent).toContain('Font 2')
64+
expect(style.textContent).not.toContain('Font 1')
65+
} finally {
66+
root.remove()
67+
}
68+
})
69+
it('should embed font used by deeply nested child', async () => {
70+
const root = document.createElement('div')
71+
document.body.append(root)
72+
try {
73+
root.innerHTML = `
74+
<style>
75+
@font-face {
76+
font-family: 'Font 0';
77+
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
78+
}
79+
@font-face {
80+
font-family: 'Font 1';
81+
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
82+
}
83+
@font-face {
84+
font-family: 'Font 2';
85+
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
86+
}
87+
</style>
88+
<div>
89+
<div>
90+
<div>
91+
<div style="font-family: 'Font 1'">Hello world</div>
92+
</div>
93+
</div>
94+
</div>
95+
`
96+
const svg = await htmlToImage.toSvg(root)
97+
const doc = await getSvgDocument(svg)
98+
const [style] = Array.from(doc.getElementsByTagName('style'))
99+
expect(style.textContent).toContain('Font 1')
100+
expect(style.textContent).not.toContain('Font 0')
101+
expect(style.textContent).not.toContain('Font 2')
102+
} finally {
103+
root.remove()
104+
}
105+
})
106+
})
107+
})

0 commit comments

Comments
 (0)