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.