Tools to manipulate font files
—
Standardized drawing interface for glyph construction, manipulation, and rendering with specialized pen implementations for different use cases. The pen protocol provides a consistent API for drawing vector graphics that can be rendered to various outputs.
Core pen protocol and base implementations that define the standard drawing interface.
class AbstractPen:
"""Abstract base class defining the pen protocol."""
def moveTo(self, pt):
"""
Move to point without drawing.
Parameters:
- pt: Tuple[float, float], (x, y) coordinates
"""
def lineTo(self, pt):
"""
Draw line from current point to specified point.
Parameters:
- pt: Tuple[float, float], (x, y) coordinates
"""
def curveTo(self, *points):
"""
Draw cubic Bezier curve.
Parameters:
- points: Tuple[float, float], sequence of control points and end point
Last point is curve endpoint, others are control points
"""
def qCurveTo(self, *points):
"""
Draw quadratic Bezier curve(s).
Parameters:
- points: Tuple[float, float], sequence of control points and optional end point
If no end point given, curves to next oncurve point
"""
def closePath(self):
"""Close current path with line back to start point."""
def endPath(self):
"""End current path without closing."""
def addComponent(self, glyphName, transformation):
"""
Add component reference to another glyph.
Parameters:
- glyphName: str, name of referenced glyph
- transformation: Transform, transformation matrix
"""
class BasePen(AbstractPen):
def __init__(self, glyphSet=None):
"""
Base pen implementation with validation.
Parameters:
- glyphSet: GlyphSet, glyph set for component validation
"""
def moveTo(self, pt):
"""Move to point with validation."""
def lineTo(self, pt):
"""Draw line with validation."""
def curveTo(self, *points):
"""Draw cubic curve with validation."""
def qCurveTo(self, *points):
"""Draw quadratic curve with validation."""
class NullPen(AbstractPen):
"""No-operation pen for measurement and testing."""
def moveTo(self, pt): pass
def lineTo(self, pt): pass
def curveTo(self, *points): pass
def qCurveTo(self, *points): pass
def closePath(self): pass
def endPath(self): passfrom fontTools.pens.basePen import BasePen
class DebugPen(BasePen):
"""Custom pen that prints drawing operations."""
def __init__(self):
super().__init__()
self.operations = []
def moveTo(self, pt):
self.operations.append(f"moveTo{pt}")
print(f"Move to {pt}")
def lineTo(self, pt):
self.operations.append(f"lineTo{pt}")
print(f"Line to {pt}")
def curveTo(self, *points):
self.operations.append(f"curveTo{points}")
print(f"Curve to {points}")
def closePath(self):
self.operations.append("closePath")
print("Close path")
# Use the pen
pen = DebugPen()
pen.moveTo((100, 100))
pen.lineTo((200, 100))
pen.lineTo((200, 200))
pen.lineTo((100, 200))
pen.closePath()Pens for creating TrueType glyph data with proper curve conversion and hinting.
class TTGlyphPen(BasePen):
def __init__(self, glyphSet):
"""
Pen for creating TrueType glyph objects.
Parameters:
- glyphSet: GlyphSet, glyph set for component resolution
"""
def glyph(self):
"""
Get the constructed glyph object.
Returns:
Glyph: TrueType glyph with quadratic curves
"""
class T2CharStringPen(BasePen):
def __init__(self, width, glyphSet):
"""
Pen for creating CFF CharString objects.
Parameters:
- width: int, glyph advance width
- glyphSet: GlyphSet, glyph set for component resolution
"""
def getCharString(self):
"""
Get the constructed CharString.
Returns:
CharString: CFF CharString with cubic curves
"""from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.pens.t2CharStringPen import T2CharStringPen
# Create TrueType glyph (letter A)
tt_pen = TTGlyphPen(None)
# Draw letter A outline
tt_pen.moveTo((200, 0)) # Bottom left
tt_pen.lineTo((100, 700)) # Top left
tt_pen.lineTo((300, 700)) # Top right
tt_pen.lineTo((400, 0)) # Bottom right
tt_pen.closePath()
# Add crossbar
tt_pen.moveTo((175, 350))
tt_pen.lineTo((325, 350))
tt_pen.lineTo((325, 400))
tt_pen.lineTo((175, 400))
tt_pen.closePath()
# Get the glyph
glyph_a = tt_pen.glyph()
# Create CFF CharString (letter O)
cff_pen = T2CharStringPen(500, None)
# Draw letter O outline (with curves)
cff_pen.moveTo((250, 0))
cff_pen.curveTo((100, 0), (50, 100), (50, 350)) # Left curve
cff_pen.curveTo((50, 600), (100, 700), (250, 700)) # Top curve
cff_pen.curveTo((400, 700), (450, 600), (450, 350)) # Right curve
cff_pen.curveTo((450, 100), (400, 0), (250, 0)) # Bottom curve
cff_pen.closePath()
# Inner counter
cff_pen.moveTo((250, 100))
cff_pen.curveTo((350, 100), (350, 150), (350, 350))
cff_pen.curveTo((350, 550), (350, 600), (250, 600))
cff_pen.curveTo((150, 600), (150, 550), (150, 350))
cff_pen.curveTo((150, 150), (150, 100), (250, 100))
cff_pen.closePath()
char_string_o = cff_pen.getCharString()Pens for analyzing glyph properties without rendering.
class BoundsPen(BasePen):
def __init__(self, glyphSet):
"""
Calculate glyph bounding box.
Parameters:
- glyphSet: GlyphSet, glyph set for component resolution
"""
@property
def bounds(self):
"""
Get calculated bounds.
Returns:
Tuple[float, float, float, float]: (xMin, yMin, xMax, yMax) or None
"""
class AreaPen(BasePen):
def __init__(self, glyphSet=None):
"""
Calculate glyph area.
Parameters:
- glyphSet: GlyphSet, glyph set for component resolution
"""
@property
def area(self):
"""
Get calculated area.
Returns:
float: Glyph area (positive for clockwise, negative for counter-clockwise)
"""
class StatisticsPen(BasePen):
def __init__(self, glyphSet=None):
"""
Gather glyph statistics.
Parameters:
- glyphSet: GlyphSet, glyph set for component resolution
"""
@property
def area(self):
"""Get glyph area."""
@property
def length(self):
"""Get total path length."""
@property
def moments(self):
"""Get statistical moments."""from fontTools.pens.boundsPen import BoundsPen
from fontTools.pens.areaPen import AreaPen
from fontTools.pens.statisticsPen import StatisticsPen
from fontTools.ttLib import TTFont
# Load font and get glyph
font = TTFont("font.ttf")
glyph_set = font.getGlyphSet()
glyph = glyph_set['A']
# Calculate bounds
bounds_pen = BoundsPen(glyph_set)
glyph.draw(bounds_pen)
bounds = bounds_pen.bounds
print(f"Glyph bounds: {bounds}") # (xMin, yMin, xMax, yMax)
# Calculate area
area_pen = AreaPen(glyph_set)
glyph.draw(area_pen)
area = area_pen.area
print(f"Glyph area: {area}")
# Gather statistics
stats_pen = StatisticsPen(glyph_set)
glyph.draw(stats_pen)
print(f"Area: {stats_pen.area}")
print(f"Length: {stats_pen.length}")
print(f"Moments: {stats_pen.moments}")Pens for applying geometric transformations to glyph data.
class TransformPen(BasePen):
def __init__(self, otherPen, transformation):
"""
Apply transformation to pen operations.
Parameters:
- otherPen: AbstractPen, target pen to receive transformed operations
- transformation: Transform, transformation matrix to apply
"""
class ReversedContourPen(BasePen):
def __init__(self, otherPen):
"""
Reverse contour direction.
Parameters:
- otherPen: AbstractPen, target pen to receive reversed operations
"""from fontTools.pens.transformPen import TransformPen
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.misc.transform import Transform
# Create base glyph
base_pen = TTGlyphPen(None)
base_pen.moveTo((100, 100))
base_pen.lineTo((200, 100))
base_pen.lineTo((150, 200))
base_pen.closePath()
# Create transformed version (scaled and rotated)
target_pen = TTGlyphPen(None)
transform = Transform()
transform = transform.scale(1.5, 1.5) # Scale 150%
transform = transform.rotate(math.radians(45)) # Rotate 45 degrees
transform_pen = TransformPen(target_pen, transform)
# Draw original shape through transform pen
transform_pen.moveTo((100, 100))
transform_pen.lineTo((200, 100))
transform_pen.lineTo((150, 200))
transform_pen.closePath()
transformed_glyph = target_pen.glyph()Pens for capturing and replaying drawing operations.
class RecordingPen(BasePen):
def __init__(self):
"""Pen that records all drawing operations."""
@property
def value(self):
"""
Get recorded operations.
Returns:
List[Tuple]: List of (operation, args) tuples
"""
def replay(self, pen):
"""
Replay recorded operations to another pen.
Parameters:
- pen: AbstractPen, target pen for replay
"""
class DecomposingRecordingPen(RecordingPen):
def __init__(self, glyphSet):
"""
Recording pen that decomposes components.
Parameters:
- glyphSet: GlyphSet, glyph set for component decomposition
"""from fontTools.pens.recordingPen import RecordingPen
# Record drawing operations
recording_pen = RecordingPen()
recording_pen.moveTo((0, 0))
recording_pen.lineTo((100, 0))
recording_pen.lineTo((100, 100))
recording_pen.lineTo((0, 100))
recording_pen.closePath()
# Get recorded operations
operations = recording_pen.value
print("Recorded operations:")
for op, args in operations:
print(f" {op}{args}")
# Replay to different pen
target_pen = TTGlyphPen(None)
recording_pen.replay(target_pen)
replayed_glyph = target_pen.glyph()Pens for generating various output formats from glyph data.
class SVGPathPen(BasePen):
def __init__(self, glyphSet=None):
"""
Generate SVG path data.
Parameters:
- glyphSet: GlyphSet, glyph set for component resolution
"""
def getCommands(self):
"""
Get SVG path commands.
Returns:
List[str]: SVG path command strings
"""
def d(self):
"""
Get SVG path 'd' attribute value.
Returns:
str: Complete SVG path data
"""from fontTools.pens.svgPathPen import SVGPathPen
# Create SVG path from glyph
svg_pen = SVGPathPen()
# Draw a simple shape
svg_pen.moveTo((100, 100))
svg_pen.curveTo((150, 50), (250, 50), (300, 100))
svg_pen.curveTo((350, 150), (350, 250), (300, 300))
svg_pen.curveTo((250, 350), (150, 350), (100, 300))
svg_pen.curveTo((50, 250), (50, 150), (100, 100))
svg_pen.closePath()
# Get SVG path data
path_data = svg_pen.d()
print(f"SVG path: {path_data}")
# Use in SVG
svg_content = f'''
<svg viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
<path d="{path_data}" fill="black"/>
</svg>
'''class PenError(Exception):
"""Base pen exception."""
class OpenContourError(PenError):
"""Raised when path operations are invalid due to open contour."""class CustomPen(BasePen):
"""Example custom pen implementation."""
def __init__(self):
super().__init__()
self.paths = []
self.current_path = []
def moveTo(self, pt):
if self.current_path:
self.paths.append(self.current_path)
self.current_path = [('moveTo', pt)]
def lineTo(self, pt):
self.current_path.append(('lineTo', pt))
def curveTo(self, *points):
self.current_path.append(('curveTo', points))
def closePath(self):
self.current_path.append(('closePath',))
self.paths.append(self.current_path)
self.current_path = []
def endPath(self):
if self.current_path:
self.paths.append(self.current_path)
self.current_path = []
# Usage patterns for drawing glyphs
def draw_rectangle(pen, x, y, width, height):
"""Helper function to draw rectangle."""
pen.moveTo((x, y))
pen.lineTo((x + width, y))
pen.lineTo((x + width, y + height))
pen.lineTo((x, y + height))
pen.closePath()
def draw_circle(pen, cx, cy, radius, segments=32):
"""Helper function to draw circle approximation."""
import math
# Calculate points for circle
points = []
for i in range(segments):
angle = 2 * math.pi * i / segments
x = cx + radius * math.cos(angle)
y = cy + radius * math.sin(angle)
points.append((x, y))
pen.moveTo(points[0])
for point in points[1:]:
pen.lineTo(point)
pen.closePath()Install with Tessl CLI
npx tessl i tessl/pypi-fonttools