optimize-fonts@1.0.0
This commit is contained in:
3 files changed
+203
No files matched your search
@@ -0,0 +1,94 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user