Rule-driven anchor placement for UFO fonts. You describe, in a compact text file, where anchors should sit on your glyphs; AnchorsFactory computes the coordinates from each glyph's own geometry and writes the anchors into the font.
It does the pre-marking step of accent handling: place top/bottom/_top/…
anchors consistently across hundreds of glyphs, so a tool like
GlyphConstruction can then
assemble composite glyphs by snapping mark anchors to base anchors.
pip install anchorsfactory
# or, from a checkout:
pip install -e .Requires Python 3.10+, fontParts and fontTools.
# place anchors using the bundled default ruleset, save to font_anchored.ufo
anchorsfactory MyFont.ufo --rules default
# your own rules, overwrite in place, with a backup of existing anchors
anchorsfactory MyFont.ufo --rules my-rules.af --in-place --backup-dir backups/
# a whole folder of UFOs
anchorsfactory masters/ --rules defaultBy default the source UFO is never overwritten — output goes to
*_anchored.ufo unless you pass --in-place.
Rules are stacked: define reusable labels, then mark glyphs with them,
mixing labels and one-off anchors. An anchor is a name and a parenthesised
X Y placement.
# a label
@ = top (box.center capHeight), bottom (box.center 0)
# apply it, by name / unicode / range
A = @, ogonek (outline.right 0)
U+0410..U+044F = @ # all Russian Cyrillic
U+0413 += desc (outline.right 0) # Г also gets a descender anchor
- X is
frame.position:width.*(advance),box.*(bounding box) oroutline.*(the contour at height Y, with.first/.lastto pick a stem). - Y is a number, a font metric (
capHeight,xHeight,ascender, …), or a reference glyph ($H,$H.bottom,$H*5/6). - Selectors: name,
U+XXXX, rangeU+A..U+B, glob*.sc, category{Lu}. =replace ·+=add ·-=remove;!extends defaultinherits a ruleset.
Full reference: docs/anchor-rules.md.
Bundled rulesets default and default-italics are usable by name in
--rules or !extends. Old .txt rule files (see examples/) convert to the
new syntax — verified lossless:
anchorsfactory-convert examples/default-anchors-list.txt -o my-rules.affrom anchorsfactory import process_ufo, load_document, apply_document
process_ufo("MyFont.ufo", "default") # high-level: open, apply, save
from fontParts.world import OpenFont
font = OpenFont("MyFont.ufo")
apply_document(font, load_document("my-rules.af"))
font.save()compute_document is the functional core: it returns the anchors a rule
document would place, keyed by glyph, without touching the font. It owns the
same orchestration as apply_document (suffix expansion, shift_x, rounding,
same-name dedup), so a preview never drifts from what gets written.
from anchorsfactory import compute_document, load_document, accumulate, resolve
doc = load_document("default")
placed = compute_document(font, doc) # {glyph_name: [(anchor, x, y), ...]}
# resolve() is the pure, single-anchor primitive behind it:
specs = accumulate(doc, "A", font["A"].unicodes) # the anchors due on glyph A
x, y = resolve(font, font["A"], specs[0])For an interactive editor, on_error="collect" never raises — it places what
it can and reports the rest as structured diagnostics:
result = compute_document(font, doc, on_error="collect")
for d in result.diagnostics:
# d.severity == "error" -> anchor skipped (geometry raised)
# d.severity == "warning" -> anchor placed via a fallback, but suspect
print(d.severity, d.glyph, d.anchor, d.reason)result is a plain dict subclass (so it works anywhere a dict does) carrying
a .diagnostics list of ComputeDiagnostic(glyph, anchor, reason, severity, rule). The default on_error="raise" is unchanged and keeps .diagnostics
empty.
make venv # create .venv, install the package (editable) + dev deps, via uv
make test # run the test suite
make build # build sdist + wheel into dist/
make release # bump minor, update CHANGELOG, build, upload to PyPI, tag + pushReleases are cut with make release: it bumps the minor version, fills in a
CHANGELOG section from the commit log, publishes to PyPI, and
tags vX.Y.Z. Set UV_PUBLISH_TOKEN first; use make release-test to rehearse
against TestPyPI.
MIT — see LICENSE.