Trace bitmap glyph images to bezier contours and insert them directly into UFO font sources.
Designed for agentic AI type design pipelines — takes scanned or rendered letterforms and produces cubic outlines ready for editing or compilation into fonts.
Built on the Linebender ecosystem: kurbo for bezier curve math and optimal curve fitting via fit_to_bezpath_opt, and norad for UFO reading/writing.
cargo install --git https://github.com/eliheuer/img2bezThe autoresearch loop generates per-glyph bezier structure comparison images. This requires designbot:
cargo install --git https://github.com/eliheuer/designbot designbot-cliOnce installed, autoresearch/run_experiment.sh will automatically render
viz_uni<XXXX>.png files for the weakest glyphs after each experiment run.
If designbot is not on your PATH the viz step is silently skipped.
Or to use as a library, add to your Cargo.toml:
[dependencies]
img2bez = { git = "https://github.com/eliheuer/img2bez" }# Basic usage: trace a glyph image and insert it into a UFO source
img2bez --input glyph.png \
--output MyFont.ufo \ # target UFO (must already exist)
--name A \ # glyph name in the UFO
--unicode 0041 # Unicode codepoint in hex
# With font metrics: scale and position the outline to match your UPM
img2bez --input glyph.png \
--output MyFont.ufo \
--name A \
--unicode 0041 \
--target-height 1024 \ # ascender - descender in font units
--y-offset -256 \ # shift down by the descender value
--grid 2 # snap coordinates to a 2-unit grid
# Compare the traced output against a reference glyph
img2bez --input glyph.png \
--output MyFont.ufo \
--name A \
--unicode 0041 \
--reference MyFont.ufo/glyphs/A_.glif| Flag | Default | Description |
|---|---|---|
-i, --input |
required | Input image (PNG, JPEG, BMP) |
-o, --output |
required | Output UFO path |
-n, --name |
required | Glyph name |
-u, --unicode |
— | Unicode codepoint (hex, e.g. 0041) |
-w, --width |
auto | Advance width (auto-computed from bbox if omitted) |
--target-height |
1000 | Target height in font units (ascender - descender) |
--y-offset |
0 | Y offset after scaling (typically the descender) |
--grid |
0 | Grid size for coordinate snapping (0 = off) |
--accuracy |
4.0 | Curve fitting accuracy in font units (smaller = tighter) |
--alphamax |
1.0 | Corner detection (0.6-0.8 for geometric type, 1.0 for organic) |
--smooth |
3 | Polygon smoothing iterations (0 = off) |
--chamfer |
0 | Chamfer size (0 = off) |
--threshold |
Otsu | Fixed brightness threshold (0-255), overrides Otsu |
--invert |
false | Invert image before tracing |
--reference |
— | Reference .glif for quality evaluation |
use img2bez::{trace, TracingConfig};
use std::path::Path;
let config = TracingConfig {
target_height: 1088.0,
y_offset: -256.0,
grid: 2,
..TracingConfig::default()
};
let result = trace(Path::new("glyph.png"), &config)?;
// result.paths: Vec<kurbo::BezPath>
// result.advance_width: f64
// result.contour_types: Vec<ContourType> Input image
|
1. Threshold (Otsu or fixed) ─── bitmap.rs
|
2. Pixel-edge contour extraction (dual grid) ─── vectorize/decompose.rs
|
3. Optimal polygon approximation (DP) ─── vectorize/polygon.rs
|
4. Sub-pixel vertex refinement ─── vectorize/polygon.rs
|
5. Corner detection + curve fitting ─── vectorize/curve.rs
|
6. Post-processing ─── cleanup/
├── Contour direction (CCW outer, CW counter)
├── Grid snapping
├── H/V handle correction
└── Optional chamfer insertion
|
7. Reposition + advance width ─── metrics.rs
|
8. UFO output ─── ufo.rs
- Potrace-style pipeline: decompose, polygon, curve — adapted for font outlines
- Extrema-aware splitting: curves are split at bounding-box extrema so handles naturally align H/V
- Grid snapping: on-curve points snap to an integer grid while off-curve handles preserve curve accuracy
- Rayon parallelism: contours are fitted in parallel
- Raster IoU evaluation: compare traced output against a reference with pixel-level accuracy
| Feature | Default | Description |
|---|---|---|
ufo |
yes | UFO output via norad |
cli |
yes | Command-line binary (implies ufo) |
Prefix your command with VAR=1 to enable debug output for a single run:
IMG2BEZ_DEBUG_SPLITS=1 img2bez --input glyph.png --output MyFont.ufo --name A| Variable | Effect |
|---|---|
IMG2BEZ_DEBUG_BITMAP |
Save thresholded bitmap as debug_threshold.png |
IMG2BEZ_DEBUG_PIXELS |
Print raw pixel contour stats |
IMG2BEZ_DEBUG_RAW_CONTOUR |
Skip polygon optimization, use raw pixel points |
IMG2BEZ_DEBUG_NO_ADJUST |
Skip sub-pixel vertex refinement |
IMG2BEZ_DEBUG_POLYGON |
Output polygon as straight lines (no curve fitting) |
IMG2BEZ_DEBUG_SPLITS |
Print split-point analysis per contour |
IMG2BEZ_DEBUG_FIT |
Print per-section curve fitting details |
IMG2BEZ_DEBUG_NO_CLEANUP |
Skip all post-processing |
IMG2BEZ_DEBUG_PIXELDIFF |
Save 1:1 pixel diff image |
An overnight research loop that autonomously improves img2bez by tracing hand-drawn reference glyphs and comparing raster + vector quality metrics. Inspired by karpathy/autoresearch.
reference.ufo (hand-drawn glyphs)
|
render_glyph.py → glyph.png (drawbot-skia renders each .glif to bitmap)
|
img2bez → traced.glif (trace the bitmap back to bezier outlines)
|
--reference flag → metrics (raster IoU + weighted vector quality score)
|
agent loop → keep/discard (Claude proposes changes, keeps if mean_iou improves)
# 1. Create a research branch
git checkout -b autoresearch/$(date +%b%d | tr A-Z a-z)
# 2. Activate the Python venv (needs ufoLib2 + drawbot-skia)
source ~/Py/venvs/basic-fonts/bin/activate
# 3. Run the experiment harness
./autoresearch/run_experiment.sh
# 4. Tell Claude to follow the instructions and run overnight
# "Follow autoresearch/program.md and improve img2bez"Reference font: Virtua Grotesk Regular (autoresearch/reference.ufo →
symlink to ~/GH/repos/virtua-grotesk/sources/VirtuaGrotesk-Regular.ufo)
Glyphs tested: A–Z + a–z (52 glyphs, all with real hand-drawn outlines)
| Metric | Baseline |
|---|---|
| mean_iou | 90.39% |
| mean_score | 0.804 |
| glyphs_ok | 51/52 |
Established on commit 82acffa (2026-03-20). Results logged to
autoresearch/results.tsv (gitignored).
# Point at a different UFO
ln -sf /path/to/other.ufo autoresearch/reference.ufo
# Or override per-run without changing the symlink
REFERENCE_UFO=/path/to/other.ufo ./autoresearch/run_experiment.sh
# Run a focused subset of glyphs
GLYPHS_FILTER="O S G" ./autoresearch/run_experiment.sh| File | Purpose |
|---|---|
autoresearch/program.md |
Full instructions for Claude to follow as the research agent |
autoresearch/run_experiment.sh |
Experiment harness: render → trace → evaluate → report |
autoresearch/render_glyph.py |
Render a .glif to PNG via drawbot-skia |
autoresearch/setup.sh |
One-time setup check |
autoresearch/results.tsv |
Experiment log (gitignored, maintained by the agent) |
autoresearch/reference.ufo |
Symlink to reference UFO (gitignored) |
MIT