Skip to content

Commit

Permalink
perf: embed only used fonts
Browse files Browse the repository at this point in the history
  • Loading branch information
tuhm1 committed Sep 27, 2024
1 parent 128dc3e commit efc0db7
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 4 deletions.
38 changes: 34 additions & 4 deletions src/embed-webfonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,16 +202,46 @@ async function parseWebFontRules<T extends HTMLElement>(
return getWebFontRules(cssRules)
}

function normalizeFontFamily(font: string) {
return font.trim().replace(/["']/g, '')
}

function getUsedFonts(node: HTMLElement) {
const fonts = new Set<string>()
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<T extends HTMLElement>(
node: T,
options: Options,
): Promise<string> {
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)),
)

Check warning on line 238 in src/embed-webfonts.ts

View check run for this annotation

Codecov / codecov/patch

src/embed-webfonts.ts#L238

Added line #L238 was not covered by tests
.map((rule) => {
const baseUrl = rule.parentStyleSheet
? rule.parentStyleSheet.href
: null
return embedResources(rule.cssText, baseUrl, options)
}),
)

return cssTexts.join('\n')
Expand Down
1 change: 1 addition & 0 deletions test/resources/fonts/web-fonts/empty.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div style="font-family: Font1"></div>
107 changes: 107 additions & 0 deletions test/spec/webfont.spec.ts
Original file line number Diff line number Diff line change
@@ -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 = `
<style>
@font-face {
font-family: 'Font 0';
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
}
@font-face {
font-family: 'Font 1';
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
}
@font-face {
font-family: 'Font 2';
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
}
</style>
<p style="font-family: 'Font 1'">Hello world</p>
`
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 = `
<style>
@font-face {
font-family: 'Font 0';
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
}
@font-face {
font-family: 'Font 1';
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
}
@font-face {
font-family: 'Font 2';
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
}
</style>
<p style="font-family: 'Font 0'">Hello world</p>
<p style="font-family: 'Font 2'">Hello world</p>
`
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 = `
<style>
@font-face {
font-family: 'Font 0';
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
}
@font-face {
font-family: 'Font 1';
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
}
@font-face {
font-family: 'Font 2';
src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2');
}
</style>
<div>
<div>
<div>
<div style="font-family: 'Font 1'">Hello world</div>
</div>
</div>
</div>
`
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()
}
})
})
})

0 comments on commit efc0db7

Please sign in to comment.