Tools to manipulate font files
—
Build and manipulate variable fonts using design space documents, supporting multi-axis font variations and instance generation. Variable fonts allow a single font file to contain multiple styles and weights along continuous axes.
Core functions for building variable fonts from master sources and design space documents.
def build(designspace, master_finder=lambda s: s, exclude=[], optimize=True, **kwargs):
"""
Build variable font from designspace document.
Parameters:
- designspace: str or DesignSpaceDocument, path to .designspace file or document object
- master_finder: callable, function to locate master font files
- exclude: List[str], OpenType tables to exclude from variation
- optimize: bool, enable variation optimization
- kwargs: additional build options
Returns:
TTFont: Variable font with variation tables
"""
def build_many(designspace, master_finder=lambda s: s, exclude=[], **kwargs):
"""
Build multiple variable fonts from designspace v5 document.
Parameters:
- designspace: str or DesignSpaceDocument, designspace file or document
- master_finder: callable, function to locate master font files
- exclude: List[str], tables to exclude
- kwargs: additional build options
Returns:
List[TTFont]: List of built variable fonts
"""
def load_designspace(designspace):
"""
Parse and validate designspace document.
Parameters:
- designspace: str, path to designspace file
Returns:
DesignSpaceDocument: Parsed and validated document
"""
def load_masters(designspace, master_finder):
"""
Load master fonts referenced in designspace.
Parameters:
- designspace: DesignSpaceDocument, design space document
- master_finder: callable, function to resolve master font paths
Returns:
Dict[str, TTFont]: Master fonts keyed by source identifier
"""from fontTools.varLib import build
from fontTools.designspaceLib import DesignSpaceDocument
# Build from designspace file
variable_font = build("MyFont.designspace")
variable_font.save("MyFont-VF.ttf")
# Build with custom master finder
def find_masters(source_path):
"""Custom logic to locate master fonts."""
return f"masters/{source_path}"
variable_font = build("MyFont.designspace", master_finder=find_masters)# Build with optimization and exclusions
variable_font = build(
"MyFont.designspace",
exclude=['DSIG', 'hdmx'], # Exclude problematic tables
optimize=True, # Enable delta optimization
overlap_tolerance=0.5, # Overlap detection tolerance
cff_version=2 # Use CFF2 for PostScript outlines
)
# Build multiple fonts from v5 designspace
fonts = build_many("MyFamily.designspace")
for i, font in enumerate(fonts):
font.save(f"MyFamily-VF-{i}.ttf")Classes for creating and manipulating design space documents that define variable font parameters.
class DesignSpaceDocument:
def __init__(self):
"""Initialize empty design space document."""
def read(self, path):
"""
Read designspace from file.
Parameters:
- path: str, path to .designspace file
"""
def write(self, path):
"""
Write designspace to file.
Parameters:
- path: str, output file path
"""
def addAxisDescriptor(self, descriptor):
"""
Add variation axis definition.
Parameters:
- descriptor: AxisDescriptor, axis definition
"""
def addSourceDescriptor(self, descriptor):
"""
Add master font source.
Parameters:
- descriptor: SourceDescriptor, source definition
"""
def addInstanceDescriptor(self, descriptor):
"""
Add named instance definition.
Parameters:
- descriptor: InstanceDescriptor, instance definition
"""
def addRuleDescriptor(self, descriptor):
"""
Add conditional substitution rule.
Parameters:
- descriptor: RuleDescriptor, rule definition
"""from fontTools.designspaceLib import (
DesignSpaceDocument, AxisDescriptor, SourceDescriptor, InstanceDescriptor
)
# Create new design space
doc = DesignSpaceDocument()
# Define weight axis
weight_axis = AxisDescriptor()
weight_axis.name = "Weight"
weight_axis.tag = "wght"
weight_axis.minimum = 100
weight_axis.default = 400
weight_axis.maximum = 900
doc.addAxisDescriptor(weight_axis)
# Define width axis
width_axis = AxisDescriptor()
width_axis.name = "Width"
width_axis.tag = "wdth"
width_axis.minimum = 75
width_axis.default = 100
width_axis.maximum = 125
doc.addAxisDescriptor(width_axis)
# Add master sources
light_source = SourceDescriptor()
light_source.filename = "MyFont-Light.ufo"
light_source.location = {"Weight": 100, "Width": 100}
doc.addSourceDescriptor(light_source)
regular_source = SourceDescriptor()
regular_source.filename = "MyFont-Regular.ufo"
regular_source.location = {"Weight": 400, "Width": 100}
regular_source.copyInfo = True # Copy font info from this master
doc.addSourceDescriptor(regular_source)
bold_source = SourceDescriptor()
bold_source.filename = "MyFont-Bold.ufo"
bold_source.location = {"Weight": 900, "Width": 100}
doc.addSourceDescriptor(bold_source)
# Add named instances
regular_instance = InstanceDescriptor()
regular_instance.name = "Regular"
regular_instance.location = {"Weight": 400, "Width": 100}
doc.addInstanceDescriptor(regular_instance)
bold_instance = InstanceDescriptor()
bold_instance.name = "Bold"
bold_instance.location = {"Weight": 700, "Width": 100}
doc.addInstanceDescriptor(bold_instance)
# Save design space
doc.write("MyFont.designspace")Classes that define the components of a design space document.
class AxisDescriptor:
def __init__(self):
"""Initialize axis descriptor."""
name: str # Human-readable axis name
tag: str # 4-character axis tag
minimum: float # Minimum axis value
default: float # Default axis value
maximum: float # Maximum axis value
hidden: bool # Hide axis from user interfaces
labelNames: Dict # Localized axis names
class SourceDescriptor:
def __init__(self):
"""Initialize source descriptor."""
filename: str # Path to master font file
location: Dict # Axis coordinates for this master
copyInfo: bool # Copy font info from this master
copyGroups: bool # Copy glyph groups
copyLib: bool # Copy font lib
copyFeatures: bool # Copy OpenType features
muteKerning: bool # Disable kerning from this master
mutedGlyphNames: List # Glyphs to exclude from this master
class InstanceDescriptor:
def __init__(self):
"""Initialize instance descriptor."""
name: str # Instance name
location: Dict # Axis coordinates
styleName: str # Style name for naming table
familyName: str # Family name override
postScriptFontName: str # PostScript name
styleMapFamilyName: str # Style map family name
styleMapStyleName: str # Style map style name
class RuleDescriptor:
def __init__(self):
"""Initialize rule descriptor."""
name: str # Rule name
conditionSets: List[Dict] # Conditions when rule applies
subs: List[Tuple[str, str]] # Glyph substitutions (from, to)Utility functions for variable font manipulation and optimization.
def set_default_weight_width_slant(font, axes_config=None):
"""
Set default axis values in STAT table.
Parameters:
- font: TTFont, variable font
- axes_config: Dict, axis configuration overrides
"""
def drop_implied_oncurve_points(font):
"""
Optimize TrueType curves by dropping implied on-curve points.
Parameters:
- font: TTFont, font to optimize
"""
def addGSUBFeatureVariations(font, conditional_substitutions):
"""
Add GSUB feature variations for conditional substitutions.
Parameters:
- font: TTFont, variable font
- conditional_substitutions: Dict, feature variation definitions
"""from fontTools.varLib import set_default_weight_width_slant, drop_implied_oncurve_points
# Optimize variable font
variable_font = build("MyFont.designspace")
# Set default axes values for better browser support
set_default_weight_width_slant(variable_font, {
'wght': 400, # Regular weight as default
'wdth': 100 # Normal width as default
})
# Optimize TrueType curves
drop_implied_oncurve_points(variable_font)
variable_font.save("MyFont-VF-Optimized.ttf")Utilities for locating and validating master font files.
class MasterFinder:
def __init__(self, template):
"""
Template-based master font finder.
Parameters:
- template: str, path template with placeholders
"""
def __call__(self, source_path):
"""
Find master font file.
Parameters:
- source_path: str, source file path from designspace
Returns:
str: Resolved master font path
"""from fontTools.varLib import MasterFinder
# Template-based finder
finder = MasterFinder("masters/{stem}.ufo")
# Custom finder function
def custom_finder(source_path):
"""Custom logic for finding masters."""
import os
# Look in multiple directories
search_paths = ["masters/", "sources/", "fonts/"]
for search_dir in search_paths:
full_path = os.path.join(search_dir, source_path)
if os.path.exists(full_path):
return full_path
raise FileNotFoundError(f"Master not found: {source_path}")
# Use with build
variable_font = build("MyFont.designspace", master_finder=custom_finder)Support for conditional OpenType feature substitutions in variable fonts.
# Add feature variations for different weights
feature_variations = {
'liga': [
{
'conditions': [{'wght': (400, 900)}], # Bold weights
'substitutions': [('f_i', 'f_i.bold')] # Use bold ligature
}
]
}
addGSUBFeatureVariations(variable_font, feature_variations)Variable font building may raise various exceptions:
def build_font_family_variable(designspace_path, output_dir):
"""Build variable font family with proper naming."""
import os
from pathlib import Path
# Load and validate designspace
doc = load_designspace(designspace_path)
# Build variable font
vf = build(doc, optimize=True)
# Set up proper naming
name_table = vf['name']
family_name = doc.default.familyName or "Variable Font"
# Update name table entries
name_table.setName(family_name, 1, 3, 1, 0x409) # Family name
name_table.setName("Variable", 2, 3, 1, 0x409) # Subfamily name
# Save with proper filename
output_path = Path(output_dir) / f"{family_name.replace(' ', '')}-VF.ttf"
vf.save(output_path)
return output_path
def validate_masters_compatibility(designspace_path):
"""Validate that masters are compatible for interpolation."""
doc = load_designspace(designspace_path)
masters = load_masters(doc, lambda s: s)
# Check glyph compatibility
reference_glyphs = None
for source, font in masters.items():
glyph_set = set(font.getGlyphOrder())
if reference_glyphs is None:
reference_glyphs = glyph_set
elif glyph_set != reference_glyphs:
missing = reference_glyphs - glyph_set
extra = glyph_set - reference_glyphs
print(f"Master {source} compatibility issues:")
if missing:
print(f" Missing glyphs: {missing}")
if extra:
print(f" Extra glyphs: {extra}")
return len(masters)Install with Tessl CLI
npx tessl i tessl/pypi-fonttools