Software-design

Architecture

The main idea of the design of this software, is for us to be able to create countless transformations of fonts the way we want it. To achieve this I think, at this moment at least, that a pipeline architecture is going to be the best to achieve this process.

Awesome Font -> Parse(bytes) -> [Ordered transformations] -> font.Write() -> Awesomer Font

For the transformations, what I have in mind is a main Modify() function that will take care of all the modifications of the font, and just make smaller scoped functions that just modify some parameters.

This is done this way as there is an order needed for you to apply your transformations into the font. This is an initial design and will start as tight as we can make it, but as the concepts grow and everything is clearer I will be able to liberate your design skills and let you create your own transformations and process pipelines.

Font Representation

The in-memory Font struct holds all TTF tables as typed Go structs, with glyphs decoded into explicit contours and control points:

Font
├── head, hhea, maxp, OS/2, name, cmap, post, loca, hmtx, kern
└── Glyphs: []Glyph
        └── Contours: []Contour
                └── Points: []Point  { X, Y, OnCurve }

Glyphs are not stored as raw bytes — every control point is decoded and addressable. This makes plugins format-agnostic: when CFF/OTF support is added, the parser produces the same Glyph struct and all existing plugins work unchanged.

FontPatch

Every transformation produces a FontPatch — a description of what should change — and hands it to Modify(), the single point of mutation. Plugins never touch the font directly.

type FontPatch struct {
    OS2  *OS2Patch
    Hhea *HheaPatch
    Hmtx *HmtxPatch
    // ... other tables

    // nil = leave all glyphs unchanged
    GlyphTransform func(glyph *Glyph)
}

Field patches use pointer fields — nil means “don’t touch this.” The glyph transform is a function value applied to every glyph in the font. A plugin that only changes metrics sets no GlyphTransform; a plugin that reshapes outlines provides one.

Plugin Registry

Each transformation is a pure function. Each plugin decodes its own typed parameter struct from the JSON payload:

type Plugin func(*Font, json.RawMessage) (FontPatch, error)

var Registry = map[string]Plugin{
    "bold":      bold.Apply,
    "monospace": monospace.Apply,
    "width":     width.Apply,
    "bounce":    bounce.Apply,
}

Plugins are registered by name. Adding a new transformation means writing one function and adding one registry entry — nothing else changes.

Pipeline

A pipeline is a JSON file: an ordered list of { name, params } objects. The engine reads the file, looks up each name in the registry, hands the raw params to the plugin (which deserializes them into its typed struct), and calls Modify() in sequence.

[{ "bold": { "amount": 50 } }, { "monospace": { "target_width": 600 } }]

The font state flows through each transformation in order. The final state is serialized by font.Write().

CLI

handyman run pipeline.json input.ttf output.ttf

Transformation Categories

Two categories of transformation differ in complexity:

Metrics-only (e.g. monospace) — set specific table fields. Only hmtx advance widths change; no outline work needed.

Outline-level (e.g. bold, width, bounce) — apply a geometric operation to every glyph’s control points. These provide a GlyphTransform function in their patch.

Both go through the same Modify() call. The distinction is internal to the plugin.


Design Constraints

Zero external dependencies for the core engine. Pure Go standard library. Build with go build.

Round-trip correctness first. The parser and writer are validated against real TTF files before any transformation is written. If Parse → Write does not produce a byte-identical result, no transformation can be trusted. In Go this means: use slices (not maps) for any ordered table, watch alignment in binary.Write, and avoid gob.

Format-agnostic plugins. The Glyph struct is the common language between the parser and the plugins. A CFF parser that produces the same struct makes all existing plugins work for OTF files with no changes.