Subsetting

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.FlagDropLayoutTables

Web font (keep layout tables for browser shaping):

input.Flags = subset.FlagNoHinting

Retain glyph IDs (when external references depend on stable GIDs):

input.Flags = subset.FlagRetainGIDs

Table 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)
}