Skip to main content

Rule-driven anchor placement for UFO fonts

Project description

AnchorsFactory

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.

Install

pip install anchorsfactory
# or, from a checkout:
pip install -e .

Requires Python 3.10+, fontParts and fontTools.

Quick start

# 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 default

By default the source UFO is never overwritten — output goes to *_anchored.ufo unless you pass --in-place.

The rule language

Rules are stacked: define reusable labels (@, a list of anchors) and variables (&, one axis's value), then mark glyphs with them, mixing them with one-off anchors. An anchor is a name and a parenthesised X Y placement.

# a variable (reuse one axis value) and a label (reuse a set of anchors)
&mid = capHeight*1/2+xHeight*1/2
@ = 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
  • frame.position works on both axes: width.* (advance), box.* (bounding box) or outline.* (the contour, sampled on a scanline at the other coordinate, with .first/.last to pick a stem). On X the words are left/center/right, on Y bottom/middle/top; box*1/3 is a fractional position (same *n/m as capHeight*2/3) and outline.centroid the area centre of mass.
  • Y may also be a number, a font metric (capHeight, xHeight, …), or a reference glyph ($H, $H.bottom, $H*5/6); box.top reads this glyph's own height.
  • Selectors: name, U+XXXX, range U+A..U+B, glob *.sc, category {Lu}.
  • = replace · += add · -= remove; !extends default inherits a ruleset.
  • Terms combine with +/- (on either axis) for a base position plus a bias — outline.centroid-25 nudges a slanted mark off the optical centre.
  • &name names a reusable X/Y value; @name a reusable set of anchors. Both are late-bound, so an extending file can override either.

Full reference: docs/anchor-rules.md.

Presets and migration

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

Library API

from 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 without mutating (preview)

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.

Development

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 + push

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

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

anchorsfactory-0.4.1.tar.gz (61.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

anchorsfactory-0.4.1-py3-none-any.whl (42.6 kB view details)

Uploaded Python 3

File details

Details for the file anchorsfactory-0.4.1.tar.gz.

File metadata

  • Download URL: anchorsfactory-0.4.1.tar.gz
  • Upload date:
  • Size: 61.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.9 {"installer":{"name":"uv","version":"0.9.9"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"44","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for anchorsfactory-0.4.1.tar.gz
Algorithm Hash digest
SHA256 b4d47c08e91fdffe57cd99603d43f78f234380816f4b4de86d29ce981caf3598
MD5 497618f252a6d33b7f7a486bd911cad4
BLAKE2b-256 4b3cd50cecc3a564bba08c0d04a35c629a6cf84eca59bbf8823cff41ef234e8d

See more details on using hashes here.

File details

Details for the file anchorsfactory-0.4.1-py3-none-any.whl.

File metadata

  • Download URL: anchorsfactory-0.4.1-py3-none-any.whl
  • Upload date:
  • Size: 42.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.9 {"installer":{"name":"uv","version":"0.9.9"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"44","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for anchorsfactory-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 3f1c245b8427288f9c25e5b9fb3b19180a677350fd741dfe57425c342442b6c2
MD5 2a0659df5e3dfc5cd8e8b1992cacb1b1
BLAKE2b-256 fd59de4cc2ee72eee221273012a62519b32026ba9ab6e5267c9042744aa4aa69

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page