rsnext/packages/next/server/font-utils.ts
Hannes Bornö 05498a0988
Local font files adjust fallbacks (#41180)
When using `@next/font/local` we can't have pre calculated metrics for the font. This PR uses fontkit to read metadata about the imported local font in order to generate a fallback.

Also removes some props from the api that's better done automatically in the future.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
2022-10-10 18:54:34 +00:00

205 lines
5 KiB
TypeScript

import * as Log from '../build/output/log'
import {
GOOGLE_FONT_PROVIDER,
DEFAULT_SERIF_FONT,
DEFAULT_SANS_SERIF_FONT,
} from '../shared/lib/constants'
const googleFontsMetrics = require('./google-font-metrics.json')
const https = require('https')
const CHROME_UA =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'
const IE_UA = 'Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko'
export type FontManifest = Array<{
url: string
content: string
}>
export type FontConfig = boolean
function isGoogleFont(url: string): boolean {
return url.startsWith(GOOGLE_FONT_PROVIDER)
}
function getFontForUA(url: string, UA: string): Promise<String> {
return new Promise((resolve, reject) => {
let rawData: any = ''
https
.get(
url,
{
headers: {
'user-agent': UA,
},
},
(res: any) => {
res.on('data', (chunk: any) => {
rawData += chunk
})
res.on('end', () => {
resolve(rawData.toString('utf8'))
})
}
)
.on('error', (e: Error) => {
reject(e)
})
})
}
export async function getFontDefinitionFromNetwork(
url: string
): Promise<string> {
let result = ''
/**
* The order of IE -> Chrome is important, other wise chrome starts loading woff1.
* CSS cascading 🤷‍♂️.
*/
try {
if (isGoogleFont(url)) {
result += await getFontForUA(url, IE_UA)
}
result += await getFontForUA(url, CHROME_UA)
} catch (e) {
Log.warn(
`Failed to download the stylesheet for ${url}. Skipped optimizing this font.`
)
return ''
}
return result
}
export function getFontDefinitionFromManifest(
url: string,
manifest: FontManifest
): string {
return (
manifest.find((font) => {
if (font && font.url === url) {
return true
}
return false
})?.content || ''
)
}
function parseGoogleFontName(css: string): Array<string> {
const regex = /font-family: ([^;]*)/g
const matches = css.matchAll(regex)
const fontNames = new Set<string>()
for (let font of matches) {
const fontFamily = font[1].replace(/^['"]|['"]$/g, '')
fontNames.add(fontFamily)
}
return [...fontNames]
}
function formatOverrideValue(val: number) {
return Math.abs(val * 100).toFixed(2)
}
export function calculateOverrideValues(fontMetrics: any) {
let { category, ascent, descent, lineGap, unitsPerEm } = fontMetrics
const fallbackFont =
category === 'serif' ? DEFAULT_SERIF_FONT : DEFAULT_SANS_SERIF_FONT
ascent = formatOverrideValue(ascent / unitsPerEm)
descent = formatOverrideValue(descent / unitsPerEm)
lineGap = formatOverrideValue(lineGap / unitsPerEm)
return {
ascent,
descent,
lineGap,
fallbackFont: fallbackFont.name,
}
}
export function calculateSizeAdjustValues(fontMetrics: any) {
let { category, ascent, descent, lineGap, unitsPerEm, xAvgCharWidth } =
fontMetrics
const fallbackFont =
category === 'serif' ? DEFAULT_SERIF_FONT : DEFAULT_SANS_SERIF_FONT
let sizeAdjust = xAvgCharWidth
? xAvgCharWidth / fallbackFont.xAvgCharWidth
: 1
ascent = formatOverrideValue(ascent / (unitsPerEm * sizeAdjust))
descent = formatOverrideValue(descent / (unitsPerEm * sizeAdjust))
lineGap = formatOverrideValue(lineGap / (unitsPerEm * sizeAdjust))
return {
ascent,
descent,
lineGap,
fallbackFont: fallbackFont.name,
sizeAdjust: formatOverrideValue(sizeAdjust),
}
}
function calculateOverrideCSS(font: string, fontMetrics: any) {
const fontName = font.trim()
const { ascent, descent, lineGap, fallbackFont } = calculateOverrideValues(
fontMetrics[fontName]
)
return `
@font-face {
font-family: "${fontName} Fallback";
ascent-override: ${ascent}%;
descent-override: ${descent}%;
line-gap-override: ${lineGap}%;
src: local("${fallbackFont}");
}
`
}
function calculateSizeAdjustCSS(font: string, fontMetrics: any) {
const fontName = font.trim()
const { ascent, descent, lineGap, fallbackFont, sizeAdjust } =
calculateSizeAdjustValues(fontMetrics[fontName])
return `
@font-face {
font-family: "${fontName} Fallback";
ascent-override: ${ascent}%;
descent-override: ${descent}%;
line-gap-override: ${lineGap}%;
size-adjust: ${sizeAdjust}%;
src: local("${fallbackFont}");
}
`
}
export function getFontOverrideCss(
url: string,
css: string,
useSizeAdjust = false
) {
if (!isGoogleFont(url)) {
return ''
}
const calcFn = useSizeAdjust ? calculateSizeAdjustCSS : calculateOverrideCSS
try {
const fontNames = parseGoogleFontName(css)
const fontMetrics = googleFontsMetrics
const fontCss = fontNames.reduce((cssStr, fontName) => {
cssStr += calcFn(fontName, fontMetrics)
return cssStr
}, '')
return fontCss
} catch (e) {
console.log('Error getting font override values - ', e)
return ''
}
}