Files
skills/optimize-fonts/scripts/font-metrics.py
T

95 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Print metric-override fallback @font-face blocks for a web font.
The blocks make a local system font (Arial, Roboto) occupy the same space as the
web font, so switching from the fallback to the web font on load does not shift
layout (CLS).
Usage:
python3 font-metrics.py <webfont.woff2> [--weight 500] [--name Montserrat] [--weights "400 800"]
Requires Python 3 with fonttools and brotli: pip install fonttools brotli
"""
import argparse
import sys
from fontTools.ttLib import TTFont
# Advance-width metrics of common fallback fonts, measured over the same
# character set as the web font (a-z A-Z 0-9 space), normalised to unitsPerEm.
# The ascent/descent/lineGap values are not used: only the average width matters
# for size-adjust. Keys are the local() src names.
FALLBACKS = {
'Arial': {'local': 'Arial', 'suffix': '', 'avg': 0.5742885044642857},
'Roboto': {'local': 'Roboto', 'suffix': ' Android', 'avg': 0.5608646453373016},
}
CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 '
def web_metrics(path, weight):
font = TTFont(path)
if weight is not None and 'fvar' in font:
from fontTools.varLib import instancer
font = instancer.instantiateVariableFont(font, {'wght': weight}, inplace=False)
head = font['head']
hhea = font['hhea']
cmap = font.getBestCmap()
hmtx = font['hmtx']
upm = head.unitsPerEm
widths = [hmtx[cmap[ord(c)]][0] / upm for c in CHARS if ord(c) in cmap]
return {
'ascent': hhea.ascent / upm,
'descent': abs(hhea.descent) / upm,
'lineGap': hhea.lineGap / upm,
'avg': sum(widths) / len(widths),
}
def family_name(path):
font = TTFont(path, lazy=True)
for id_ in (16, 1):
name = font['name'].getDebugName(id_)
if name:
return name.split()[0]
return 'WebFont'
def pct(value):
text = f'{value:.2f}'.rstrip('0').rstrip('.')
return f'{text}%'
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('webfont', help='path to the web font file (woff2/ttf/otf)')
parser.add_argument('--weight', type=int, default=500, help='weight to measure variable fonts at (default: 500)')
parser.add_argument('--name', default=None, help='font family name used in the fallback family names')
parser.add_argument('--weights', default='400 800', help='font-weight value for the fallback rules (default: "400 800")')
args = parser.parse_args()
web = web_metrics(args.webfont, args.weight)
name = args.name or family_name(args.webfont)
for key, fb in FALLBACKS.items():
size_adjust = web['avg'] / fb['avg']
if not size_adjust:
print(f'warning: empty width for {key}', file=sys.stderr)
continue
print('@font-face {')
print(f' ascent-override: {pct(web["ascent"] / size_adjust * 100)};')
print(f' descent-override: {pct(web["descent"] / size_adjust * 100)};')
print(f" font-family: '{name} Fallback{fb['suffix']}';")
print(f' font-weight: {args.weights};')
print(f' line-gap-override: {pct(web["lineGap"] / size_adjust * 100)};')
print(f' size-adjust: {pct(size_adjust * 100)};')
print(f" src: local('{fb['local']}');")
print('}')
print()
if __name__ == '__main__':
main()