Tools to manipulate font files
—
Build fonts from scratch using a builder pattern, supporting both TrueType and CFF/PostScript font creation with comprehensive table setup methods. FontBuilder provides a structured approach to creating fonts programmatically with proper table dependencies and validation.
The primary class for constructing fonts from scratch using a builder pattern with sequential setup methods.
class FontBuilder:
def __init__(self, unitsPerEm=None, font=None, isTTF=True):
"""
Initialize font builder.
Parameters:
- unitsPerEm: int, units per em (default: 1000)
- font: TTFont, existing font to modify (default: new font)
- isTTF: bool, True for TrueType, False for CFF/PostScript
"""
def setupGlyphOrder(self, glyphOrder):
"""
Set glyph names list (must be called first).
Parameters:
- glyphOrder: List[str], ordered list of glyph names
"""
def setupCharacterMap(self, cmap):
"""
Set character-to-glyph mapping.
Parameters:
- cmap: Dict[int, str], Unicode codepoint to glyph name mapping
"""
def setupGlyf(self, glyphs):
"""
Set TrueType glyph data (for TrueType fonts).
Parameters:
- glyphs: Dict[str, Glyph], glyph name to glyph object mapping
"""
def setupCFF(self, psName, charStrings, charStringsType=2):
"""
Set CFF/PostScript data (for OpenType fonts).
Parameters:
- psName: str, PostScript font name
- charStrings: Dict[str, CharString], glyph name to CharString mapping
- charStringsType: int, CharString format version (1 or 2)
"""
def setupHorizontalMetrics(self, metrics):
"""
Set glyph advance widths and left side bearings.
Parameters:
- metrics: Dict[str, Tuple[int, int]], glyph name to (advance, lsb) mapping
"""
def setupHorizontalHeader(self, ascent=None, descent=None):
"""
Set horizontal metrics header.
Parameters:
- ascent: int, font ascent (default: calculated from metrics)
- descent: int, font descent (default: calculated from metrics)
"""
def setupNameTable(self, nameStrings, mac=True):
"""
Set font naming information.
Parameters:
- nameStrings: Dict[str, str], name ID to string mapping
- mac: bool, include Mac platform names
"""
def setupOS2(self, sTypoAscender=None, sTypoDescender=None, usWinAscent=None, usWinDescent=None):
"""
Set OS/2 table with font metrics and properties.
Parameters:
- sTypoAscender: int, typographic ascender
- sTypoDescender: int, typographic descender
- usWinAscent: int, Windows ascent
- usWinDescent: int, Windows descent
"""
def setupPost(self, keepGlyphNames=True, extraNames=[]):
"""
Set PostScript table.
Parameters:
- keepGlyphNames: bool, include glyph names in font
- extraNames: List[str], additional glyph names beyond standard set
"""from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
# Initialize builder for TrueType font
fb = FontBuilder(unitsPerEm=1000, isTTF=True)
# 1. Set glyph order (must be first)
glyph_order = [".notdef", "A", "B", "space"]
fb.setupGlyphOrder(glyph_order)
# 2. Set character mapping
cmap = {
ord('A'): 'A',
ord('B'): 'B',
ord(' '): 'space'
}
fb.setupCharacterMap(cmap)
# 3. Create glyphs using pen
glyphs = {}
# Create .notdef glyph
pen = TTGlyphPen(None)
pen.moveTo((100, 0))
pen.lineTo((100, 750))
pen.lineTo((400, 750))
pen.lineTo((400, 0))
pen.closePath()
glyphs[".notdef"] = pen.glyph()
# Create letter A
pen = TTGlyphPen(None)
pen.moveTo((200, 0))
pen.lineTo((100, 700))
pen.lineTo((150, 700))
pen.lineTo((300, 700))
pen.lineTo((350, 700))
pen.lineTo((250, 0))
pen.closePath()
# Add crossbar
pen.moveTo((175, 350))
pen.lineTo((275, 350))
pen.lineTo((275, 400))
pen.lineTo((175, 400))
pen.closePath()
glyphs["A"] = pen.glyph()
# Simple rectangle for B
pen = TTGlyphPen(None)
pen.moveTo((100, 0))
pen.lineTo((100, 700))
pen.lineTo((300, 700))
pen.lineTo((300, 0))
pen.closePath()
glyphs["B"] = pen.glyph()
# Empty glyph for space
pen = TTGlyphPen(None)
glyphs["space"] = pen.glyph()
fb.setupGlyf(glyphs)
# 4. Set metrics (advance width, left side bearing)
metrics = {
".notdef": (500, 100),
"A": (450, 100),
"B": (400, 100),
"space": (250, 0)
}
fb.setupHorizontalMetrics(metrics)
# 5. Set font header
fb.setupHorizontalHeader(ascent=800, descent=-200)
# 6. Set font names
names = {
"familyName": "TestFont",
"styleName": "Regular",
"uniqueFontIdentifier": "TestFont-Regular",
"fullName": "TestFont Regular",
"psName": "TestFont-Regular",
"version": "1.0"
}
fb.setupNameTable(names)
# 7. Set OS/2 table
fb.setupOS2(sTypoAscender=800, sTypoDescender=-200, usWinAscent=800, usWinDescent=200)
# 8. Set PostScript table
fb.setupPost()
# Get the built font
font = fb.font
font.save("test_font.ttf")Additional methods for creating variable fonts with multiple axes and instances.
def setupFvar(self, axes, instances):
"""
Set up font variations (variable font axes and instances).
Parameters:
- axes: List[Dict], axis definitions with name, tag, min, default, max values
- instances: List[Dict], named instances with coordinates and names
"""
def setupAvar(self, axes):
"""
Set up axis variation mapping (non-linear axis mapping).
Parameters:
- axes: Dict[str, List[Tuple[float, float]]], axis tag to mapping pairs
"""
def setupGvar(self, variations):
"""
Set up glyph variations (delta coordinates for each axis).
Parameters:
- variations: Dict[str, Dict], glyph name to variation deltas mapping
"""# Build variable font with weight axis
fb = FontBuilder(unitsPerEm=1000, isTTF=True)
# ... basic setup steps as above ...
# Define weight axis
axes = [{
'name': 'Weight',
'tag': 'wght',
'minValue': 100,
'defaultValue': 400,
'maxValue': 900
}]
# Define instances
instances = [{
'name': 'Light',
'coordinates': {'wght': 200}
}, {
'name': 'Regular',
'coordinates': {'wght': 400}
}, {
'name': 'Bold',
'coordinates': {'wght': 700}
}]
fb.setupFvar(axes, instances)
# Set up glyph variations (simplified example)
variations = {
'A': {
'wght': [
# Delta coordinates for different weight values
{'coordinates': [10, 0, 15, 0, 20, 0], 'support': [0, 1, 1]}
]
}
}
fb.setupGvar(variations)Methods for creating color fonts using COLR/CPAL tables.
def setupCOLR(self, colorLayers):
"""
Set up color layer definitions (COLR table).
Parameters:
- colorLayers: Dict[str, List[Tuple[str, int]]], base glyph to layers mapping
"""
def setupCPAL(self, palettes):
"""
Set up color palettes (CPAL table).
Parameters:
- palettes: List[List[Tuple[float, float, float, float]]], color palettes with RGBA values
"""# Create color font with emoji-style glyphs
fb = FontBuilder(unitsPerEm=1000, isTTF=True)
# ... basic setup ...
# Define color layers
color_layers = {
'heart': [
('heart.base', 0), # Base shape uses palette color 0
('heart.highlight', 1) # Highlight uses palette color 1
]
}
fb.setupCOLR(color_layers)
# Define color palette
palettes = [[
(1.0, 0.0, 0.0, 1.0), # Red (RGBA)
(1.0, 0.5, 0.5, 1.0) # Light red
]]
fb.setupCPAL(palettes)FontBuilder automatically handles many validation requirements, but additional checks can be performed:
# Validate required tables are present
font = fb.font
required_tables = ['head', 'hhea', 'hmtx', 'maxp', 'name', 'cmap', 'post']
for table_tag in required_tables:
if table_tag not in font:
print(f"Warning: Missing required table {table_tag}")
# Validate glyph metrics consistency
if 'hmtx' in font and 'hhea' in font:
hmtx = font['hmtx']
hhea = font['hhea']
if hhea.numberOfHMetrics != len(hmtx.metrics):
print("Warning: Inconsistent horizontal metrics count")from fontTools.misc.bezierTools import calcBounds
def build_glyph_from_contours(contours):
"""Build glyph from list of contour point lists."""
pen = TTGlyphPen(None)
for contour in contours:
if not contour:
continue
pen.moveTo(contour[0])
for point in contour[1:]:
pen.lineTo(point)
pen.closePath()
return pen.glyph()
# Calculate bounding box for metrics
def calc_glyph_bounds(glyph):
"""Calculate bounding box for glyph."""
if hasattr(glyph, 'coordinates'):
coords = glyph.coordinates
if coords:
return calcBounds(coords)
return (0, 0, 0, 0)def create_font_family(base_glyphs, family_name, styles):
"""Create multiple fonts in a family."""
fonts = {}
for style_name, style_params in styles.items():
fb = FontBuilder(unitsPerEm=1000, isTTF=True)
# Apply style-specific modifications to glyphs
styled_glyphs = apply_style_to_glyphs(base_glyphs, style_params)
# ... setup steps ...
names = {
"familyName": family_name,
"styleName": style_name,
"fullName": f"{family_name} {style_name}",
# ... other names
}
fb.setupNameTable(names)
fonts[style_name] = fb.font
return fontsFontBuilder methods may raise exceptions for invalid parameters or missing dependencies:
Always ensure proper setup sequence and validate input data before font building operations.
Install with Tessl CLI
npx tessl i tessl/pypi-fonttools