Font Subsetting
Font subsetting reduces a font to only the glyphs needed for a specific text. This is essential for PDF embedding, web fonts, and any context where font file size matters. A typical font contains thousands of glyphs, but a document may use only a few dozen.
The subset package lives in github.com/boxesandglue/textshape/subset.
import (
"github.com/boxesandglue/textshape/ot"
"github.com/boxesandglue/textshape/subset"
)Quick Start
For the common case — subset a font to the characters in a string:
fontData, _ := os.ReadFile("NotoSans-Regular.ttf")
font, _ := ot.ParseFont(fontData, 0)
subsetData, err := subset.SubsetString(font, "Hello, World!")
if err != nil {
log.Fatal(err)
}
os.WriteFile("NotoSans-Subset.ttf", subsetData, 0644)Or with explicit codepoints:
codepoints := []rune{'H', 'e', 'l', 'o', ',', ' ', 'W', 'r', 'd', '!'}
subsetData, err := subset.Subset(font, codepoints)Detailed Workflow
For more control, use the three-step process: Input → Plan → Execute.
1. Create an Input
The Input specifies which glyphs to keep and how the subsetting should behave.
input := subset.NewInput()
// Add characters by Unicode codepoint
input.AddUnicode('A')
input.AddUnicodes('B', 'C', 'D')
input.AddUnicodeRange('a', 'z') // Inclusive range
input.AddString("Hello, World!") // All unique characters in the string
// Or add glyphs by their glyph ID directly
input.AddGlyph(42)
input.AddGlyphs(10, 11, 12, 13)The .notdef glyph (GID 0) is always included automatically.
2. Create a Plan
The plan computes the full set of glyphs needed, including glyphs reached through GSUB/GPOS closure (e.g., ligature components), and builds the old→new glyph ID mapping.
plan, err := subset.CreatePlan(font, input)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Output will have %d glyphs\n", plan.NumOutputGlyphs())You can inspect the glyph mapping:
// Old glyph ID → new glyph ID
newGID, ok := plan.MapGlyph(oldGID)
// New glyph ID → old glyph ID
oldGID, ok := plan.OldGlyph(newGID)
// Set of all old glyph IDs that will be retained
glyphSet := plan.GlyphSet()3. Execute
Execute the plan to produce the subsetted font binary:
subsetData, err := plan.Execute()
if err != nil {
log.Fatal(err)
}
// Write to file
os.WriteFile("subset.ttf", subsetData, 0644)Flags
Flags control subsetting behavior. Set them on the Input before creating a plan:
input := subset.NewInput()
input.AddString("Hello")
input.Flags = subset.FlagNoHinting | subset.FlagDropLayoutTables| Flag | Description |
|---|---|
FlagNoHinting |
Remove TrueType hinting instructions (cvt, fpgm, prep) |
FlagRetainGIDs |
Keep original glyph IDs — unused slots are padded with empty glyphs |
FlagGlyphNames |
Retain PostScript glyph names in the post table |
FlagNotdefOutline |
Retain the .notdef glyph outline (otherwise replaced with empty) |
FlagNoLayoutClosure |
Skip GSUB/GPOS glyph closure — only include explicitly requested glyphs |
FlagPassUnrecognized |
Copy unrecognized tables to the output unchanged |
FlagDropLayoutTables |
Exclude GSUB, GPOS, and GDEF tables entirely |
Common Flag Combinations
PDF embedding (minimal size, no hinting needed):
input.Flags = subset.FlagNoHinting | subset.FlagDropLayoutTablesWeb font (keep layout tables for browser shaping):
input.Flags = subset.FlagNoHintingRetain glyph IDs (when external references depend on stable GIDs):
input.Flags = subset.FlagRetainGIDsTable Management
Dropping Tables
Exclude specific tables from the output:
input.DropTable(ot.MakeTag('G', 'S', 'U', 'B')) // Drop GSUB
input.DropTable(ot.MakeTag('n', 'a', 'm', 'e')) // Drop name table
Passing Tables Through
Copy tables to the output without modification:
input.PassThroughTable(ot.MakeTag('D', 'S', 'I', 'G')) // Keep DSIG as-is
Check Table Handling
input.ShouldDropTable(tag) // Will this table be excluded?
input.ShouldPassThrough(tag) // Will this table be copied unchanged?
Feature Filtering
By default, all OpenType features are retained in the subset. You can restrict which features to keep:
input.KeepFeature(ot.MakeTag('k', 'e', 'r', 'n')) // Keep kerning
input.KeepFeature(ot.MakeTag('l', 'i', 'g', 'a')) // Keep ligatures
// All other features will be removed from GSUB/GPOS
If no features are explicitly kept (the default), all features are retained:
input.HasLayoutFeatures() // false if no features explicitly set
input.ShouldKeepFeature(tag) // true for all tags if no filter is set
Variable Font Instancing
For variable fonts, you can pin variation axes to specific values. This creates a static (non-variable) font from a variable source — useful for reducing file size when you only need one weight/width/style.
Pin Individual Axes
// Create a bold instance
input.PinAxisLocation(ot.MakeTag('w', 'g', 'h', 't'), 700)
// Create a condensed bold instance
input.PinAxisLocation(ot.MakeTag('w', 'g', 'h', 't'), 700)
input.PinAxisLocation(ot.MakeTag('w', 'd', 't', 'h'), 75)Pin to Defaults
// Pin one axis to its default value
input.PinAxisToDefault(font, ot.MakeTag('w', 'g', 'h', 't'))
// Pin ALL axes to their default values (produce a static font)
input.PinAllAxesToDefault(font)Check Instancing State
input.HasPinnedAxes() // Any axes pinned?
input.PinnedAxes() // map[Tag]float32 of pinned values
input.IsFullyInstanced(font) // All axes pinned? (will produce static font)
Example: Instancing a Variable Font
fontData, _ := os.ReadFile("NotoSans-VF.ttf")
font, _ := ot.ParseFont(fontData, 0)
input := subset.NewInput()
input.AddString("The quick brown fox")
// Pin to Bold weight, producing a static Bold font
input.PinAxisLocation(ot.MakeTag('w', 'g', 'h', 't'), 700)
plan, _ := subset.CreatePlan(font, input)
staticBold, _ := plan.Execute()
os.WriteFile("NotoSans-Bold-Subset.ttf", staticBold, 0644)When axes are pinned, the subsetter:
- Applies gvar deltas to glyph outlines (for TrueType) or CFF subroutines (for CFF)
- Applies HVAR deltas to advance widths
- Removes the fvar, avar, gvar, HVAR, and other variation tables from the output
Building Fonts Manually
The FontBuilder can assemble a font from raw table data:
builder := subset.NewFontBuilder()
builder.AddTable(ot.MakeTag('h', 'e', 'a', 'd'), headData)
builder.AddTable(ot.MakeTag('h', 'h', 'e', 'a'), hheaData)
// ... add more tables
fontBytes, err := builder.Build()This is used internally by Plan.Execute() but is also available for custom font construction.
Tables Handled During Subsetting
The subsetter handles the following OpenType tables:
| Category | Tables |
|---|---|
| Required | head, maxp, hhea, hmtx |
| Outlines | glyf + loca (TrueType), CFF (PostScript) |
| Character mapping | cmap (rebuilds format 4 / format 12) |
| Layout | GSUB, GPOS, GDEF |
| Variation | fvar, avar, gvar, HVAR, STAT, MVAR |
| Hinting | cvt, fpgm, prep (removed with FlagNoHinting) |
| Other | OS/2, name, post, gasp |
Error Handling
The subset package defines three error values:
| Error | Description |
|---|---|
ErrNoTables |
No tables to build (empty font) |
ErrMissingTable |
A required table is missing from the source font |
ErrInvalidGlyph |
An invalid glyph reference was encountered |
Complete Example
package main
import (
"log"
"os"
"github.com/boxesandglue/textshape/ot"
"github.com/boxesandglue/textshape/subset"
)
func main() {
// Load font
data, err := os.ReadFile("fonts/NotoSans-Regular.ttf")
if err != nil {
log.Fatal(err)
}
font, err := ot.ParseFont(data, 0)
if err != nil {
log.Fatal(err)
}
// Configure subset
input := subset.NewInput()
input.AddString("The quick brown fox jumps over the lazy dog. 0123456789")
input.Flags = subset.FlagNoHinting
// Only keep kerning and ligatures
input.KeepFeature(ot.MakeTag('k', 'e', 'r', 'n'))
input.KeepFeature(ot.MakeTag('l', 'i', 'g', 'a'))
// Create plan and execute
plan, err := subset.CreatePlan(font, input)
if err != nil {
log.Fatal(err)
}
log.Printf("Subsetting: %d glyphs in source → %d in output",
font.NumGlyphs(), plan.NumOutputGlyphs())
result, err := plan.Execute()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("NotoSans-Subset.ttf", result, 0644); err != nil {
log.Fatal(err)
}
log.Printf("Original: %d bytes, Subset: %d bytes (%.0f%% reduction)",
len(data), len(result),
(1-float64(len(result))/float64(len(data)))*100)
}