Font Format Overview
| Format | Extension | Typical Use |
|---|---|---|
| TrueType Font | .ttf |
Windows, macOS, Android |
| OpenType Font | .otf |
Professional typography, advanced features |
| Web Open Font Format | .woff |
Web (zlib compression) |
| WOFF2 | .woff2 |
Modern web (Brotli compression, 30% smaller) |
| Embedded OpenType | .eot |
IE6-IE8 (obsolete) |
| TrueType Collection | .ttc |
Multiple fonts in one file |
Installation
pip install fonttools brotli zopfli
# brotli and zopfli are required for WOFF2 and optimized WOFF
Basic Conversion with fonttools
from fontTools.ttLib import TTFont
from fontTools import subset
from pathlib import Path
def ttf_to_woff2(ttf_path, woff2_output=None):
"""
Convert a TTF/OTF font to WOFF2 (modern web format).
WOFF2 uses Brotli compression and is 30-40% smaller than WOFF.
"""
font = TTFont(str(ttf_path))
if woff2_output is None:
woff2_output = Path(ttf_path).with_suffix('.woff2')
font.flavor = 'woff2'
font.save(str(woff2_output))
orig = Path(ttf_path).stat().st_size / 1024
new_sz = Path(woff2_output).stat().st_size / 1024
reduction = (1 - new_sz / orig) * 100
print(f"TTF→WOFF2: {woff2_output} ({orig:.0f}KB → {new_sz:.0f}KB, -{reduction:.0f}%)")
return str(woff2_output)
def ttf_to_woff(ttf_path, woff_output=None):
"""Convert TTF/OTF to WOFF (zlib compression, compatible with IE9+)."""
font = TTFont(str(ttf_path))
if woff_output is None:
woff_output = Path(ttf_path).with_suffix('.woff')
font.flavor = 'woff'
font.save(str(woff_output))
orig = Path(ttf_path).stat().st_size / 1024
new_sz = Path(woff_output).stat().st_size / 1024
print(f"TTF→WOFF: {woff_output} ({orig:.0f}KB → {new_sz:.0f}KB)")
return str(woff_output)
def woff2_to_ttf(woff2_path, ttf_output=None):
"""Convert WOFF2 back to TTF (for editing with design tools)."""
font = TTFont(str(woff2_path))
font.flavor = None
if ttf_output is None:
ttf_output = Path(woff2_path).with_suffix('.ttf')
font.save(str(ttf_output))
print(f"WOFF2→TTF: {ttf_output}")
return str(ttf_output)
# Examples
ttf_to_woff2('Inter-Regular.ttf')
ttf_to_woff('Inter-Regular.ttf')
woff2_to_ttf('Inter-Regular.woff2')
Subsetting: Include Only Needed Characters
Subsetting is the most effective technique for reducing web font size. Instead of including 65,000+ glyphs from a complete font, you only include the characters your site actually uses.
from fontTools import subset as ft_subset
def create_font_subset(font_input, output, text_or_unicode=None,
language='en', fmt='woff2'):
"""
Create a font subset including only the needed characters.
text_or_unicode: string of characters to include, or None to
use the basic character set for the language.
fmt: 'ttf', 'woff', 'woff2'
"""
if text_or_unicode is None:
if language == 'en':
text_or_unicode = (
'abcdefghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
'0123456789'
'.,;:!?()-–—"\' '
)
elif language == 'es':
text_or_unicode = (
'abcdefghijklmnñopqrstuvwxyz'
'ABCDEFGHIJKLMNÑOPQRSTUVWXYZ'
'0123456789áéíóúüÁÉÍÓÚÜ¡¿.,;:!?()-–—"\'«»… '
)
unicodes = {ord(c) for c in text_or_unicode}
options = ft_subset.Options()
options.flavor = fmt if fmt in ('woff', 'woff2') else None
options.drop_tables += ['DSIG', 'GDEF', 'GPOS', 'GSUB', 'kern']
options.layout_features = []
options.name_IDs = [0, 1, 2, 4, 5, 6]
subsetter = ft_subset.Subsetter(options=options)
font = TTFont(str(font_input))
subsetter.populate(unicodes=unicodes)
subsetter.subset(font)
font.save(str(output))
orig = Path(font_input).stat().st_size / 1024
new_sz = Path(output).stat().st_size / 1024
reduction = (1 - new_sz / orig) * 100
print(f"Subset ({len(unicodes)} glyphs): {output} "
f"({orig:.0f}KB → {new_sz:.0f}KB, -{reduction:.0f}%)")
return str(output)
# English website subset
create_font_subset('Inter-Regular.ttf', 'Inter-Regular-subset.woff2',
language='en', fmt='woff2')
# Custom subset for an e-commerce store
store_text = (
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
'0123456789$.,/:%-+()'
)
create_font_subset('Roboto-Regular.ttf', 'Roboto-store.woff2',
text_or_unicode=store_text, fmt='woff2')
Inspecting Font Metadata
def inspect_font(font_path):
"""Display detailed information about a font file."""
font = TTFont(str(font_path))
print(f"=== {Path(font_path).name} ===")
name_table = font['name']
def get_name(name_id):
for record in name_table.names:
if record.nameID == name_id:
try:
return record.toUnicode()
except Exception:
pass
return None
print(f"Family: {get_name(1)}")
print(f"Style: {get_name(2)}")
print(f"Full name: {get_name(4)}")
print(f"Version: {get_name(5)}")
print(f"Designer: {get_name(9)}")
print(f"License: {get_name(13)}")
head = font['head']
hhea = font['hhea']
os2 = font['OS/2']
print(f"\nUnits/EM: {head.unitsPerEm}")
print(f"Ascent: {hhea.ascent}")
print(f"Descent: {hhea.descent}")
print(f"Weight: {os2.usWeightClass}")
print(f"Width: {os2.usWidthClass}")
print(f"Tables: {sorted(font.keys())}")
print(f"Total glyphs: {len(font.getGlyphSet())}")
cmap = font.getBestCmap()
if cmap:
print(f"Unicode range: U+{min(cmap):04X}-U+{max(cmap):04X}")
return font
inspect_font('Inter-Regular.ttf')
Auto-generating @font-face CSS
def generate_fontface_css(family_name, fonts, relative_path='fonts/'):
"""
Generate @font-face CSS blocks for a font family.
fonts = [
{'file': 'Inter-Regular', 'weight': 400, 'style': 'normal'},
{'file': 'Inter-Bold', 'weight': 700, 'style': 'normal'},
{'file': 'Inter-Italic', 'weight': 400, 'style': 'italic'},
]
"""
css_blocks = []
for f in fonts:
name = f['file']
weight = f.get('weight', 400)
style = f.get('style', 'normal')
formats = []
for ext, fmt in [('.woff2', 'woff2'), ('.woff', 'woff'), ('.ttf', 'truetype')]:
if Path(f"{name}{ext}").exists():
formats.append(f"url('{relative_path}{name}{ext}') format('{fmt}')")
if not formats:
print(f"⚠️ No files found for {name}")
continue
src = ',\n '.join(formats)
css_blocks.append(f'''@font-face {{
font-family: '{family_name}';
font-weight: {weight};
font-style: {style};
font-display: swap;
src: {src};
}}''')
css = '\n\n'.join(css_blocks)
print(css)
return css
css = generate_fontface_css('Inter', [
{'file': 'Inter-Regular', 'weight': 400, 'style': 'normal'},
{'file': 'Inter-Bold', 'weight': 700, 'style': 'normal'},
])
Path('fonts.css').write_text(css, encoding='utf-8')
Batch Font Conversion
def batch_convert_fonts(directory, output_format='woff2', subset_en=True):
"""Convert all fonts in a folder to the specified format."""
folder = Path(directory)
fonts = list(folder.glob('*.ttf')) + list(folder.glob('*.otf'))
print(f"Converting {len(fonts)} fonts to {output_format}...")
for font_path in sorted(fonts):
output = font_path.with_suffix(f'.{output_format}')
try:
if subset_en:
create_font_subset(font_path, output,
language='en', fmt=output_format)
elif output_format == 'woff2':
ttf_to_woff2(font_path, output)
elif output_format == 'woff':
ttf_to_woff(font_path, output)
except Exception as e:
print(f" ✗ Error in {font_path.name}: {e}")
print("Batch conversion complete.")
batch_convert_fonts('original_fonts/', output_format='woff2')
Conclusion
fonttools is the de facto library for font manipulation in Python. Its primary use cases are: converting TTF/OTF to WOFF/WOFF2 for the web (30-50% size reduction), creating subsets with only the necessary characters (70-90% reduction for large fonts), and inspecting typographic metadata. For production web projects, always combine WOFF2 conversion with subsetting: a font that weighs 500 KB as TTF can end up under 30 KB as WOFF2 with subsetting.
Related conversions
Frequent conversions across the catalogue: