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. The tracer aims for production-quality type design structure: points at extrema with H/V handles, straight segments as lines, points at inflections, G2-harmonized smooth joins, and the minimum number of points needed to render the shape (see docs/autotracing-research.md for the research and design rationale).
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.
An autoresearch specimen: the reference outlines (top row) against the traced
output (bottom row), with point structure, rendered previews, and overlay
diffs. Generated with ./render-specimen.sh --text a.
Install the CLI:
cargo install --git https://github.com/eliheuer/img2bezOr use img2bez as a library by adding it 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 (created if it doesn't 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 |
2.0 | Curve fitting accuracy in font units (smaller = tighter) |
--chamfer |
0 | Chamfer size (0 = off) |
--threshold |
Otsu | Fixed brightness threshold (0-255), overrides Otsu |
--invert |
false | Invert image before tracing |
--no-refine |
false | Disable raster-loss refinement (junction flats + curve merging + handle polish, scored against the source image) |
--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>img2bez traces the grayscale image directly, using the anti-aliasing at the ink boundary for sub-pixel accuracy. Inputs are expected to carry that anti-aliasing (rendered glyphs, grayscale scans); nearest-neighbor upscaled sources are detected and downscaled to their true resolution first.
Input image (grayscale)
|
0. Load + threshold ─── bitmap.rs
└── nearest-upscaled sources downscaled to true resolution
|
1. Marching-squares iso-contours at the threshold level
─── vectorize/subpixel.rs
2. Resample + adaptive smoothing
3. Curvature features: corners, straights, tangent points,
extrema, inflections, chamfers
4. Constrained cubic fitting
(H/V tangents at extrema; inflection split; kurbo fallback)
5. Corner reconstruction, G1 align, raster-loss refinement, G2
harmonization
─── vectorize/typefit.rs
─── vectorize/rasterfit.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
The autoresearch stress gate traces clean renders of a reference font and compares the result against the original UFO outlines: point count and placement, line/curve structure, H/V handles, and matched reference points. Across basic Latin (a-z, A-Z, 0-9) the mean structural score is 0.967, with 14 glyphs reproducing their reference exactly (score 1.000) and all 62 passing the stress gate. Per-glyph structure details are in docs/quality.md. Updated 2026-06-10.
| Glyph | Score | Glyph | Score | Glyph | Score | Glyph | Score |
|---|---|---|---|---|---|---|---|
| 1 | 1.000 | X | 0.990 | 9 | 0.973 | D | 0.946 |
| E | 1.000 | K | 0.989 | 6 | 0.969 | 8 | 0.941 |
| F | 1.000 | k | 0.989 | p | 0.967 | Q | 0.941 |
| H | 1.000 | j | 0.987 | U | 0.964 | r | 0.940 |
| I | 1.000 | P | 0.986 | C | 0.963 | N | 0.937 |
| L | 1.000 | f | 0.984 | c | 0.963 | l | 0.934 |
| M | 1.000 | J | 0.982 | h | 0.962 | R | 0.933 |
| O | 1.000 | W | 0.982 | u | 0.962 | S | 0.927 |
| T | 1.000 | a | 0.982 | 3 | 0.960 | m | 0.923 |
| V | 1.000 | t | 0.982 | G | 0.958 | 0 | 0.915 |
| i | 1.000 | 4 | 0.981 | Y | 0.958 | B | 0.915 |
| o | 1.000 | x | 0.981 | n | 0.956 | 2 | 0.912 |
| v | 1.000 | s | 0.977 | e | 0.954 | 7 | 0.857 |
| z | 1.000 | b | 0.975 | Z | 0.950 | y | 0.854 |
| w | 0.991 | d | 0.975 | g | 0.950 | ||
| A | 0.990 | q | 0.975 | 5 | 0.949 |
- Sub-pixel boundary extraction: marching squares on the grayscale at the threshold iso-level keeps the ~0.1px accuracy that anti-aliasing carries, instead of quantizing to binary first
- Type-design structure detection: on-curve points are placed where a designer puts them — extrema (H/V handles), tangent points, inflections, corners, chamfers — and nowhere else
- Constrained cubic fitting: tangent directions are fixed (exactly H/V
at extrema) and only handle lengths are solved; kurbo's near-optimal
fit_to_bezpath_opthandles sections that genuinely need subdivision - G2 harmonization: smooth joins get the Glyphs/FontLab equal-curvature treatment after G1 alignment
- Raster-loss refinement: in the spirit of differentiable rasterization (diffvg), candidate structures are scored against the source grayscale — junction crotches are rebuilt as the tiny axis-aligned flats references draw, over-subdivided curves are merged when one cubic reproduces the image, and handle lengths are re-optimized — with handle directions and on-curve points otherwise held to type-design structure
- Corner reconstruction: rasterization-rounded corners between straight edges are rebuilt as sharp line intersections
- 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 and render) |
parallel |
yes | Per-contour parallelism via rayon |
render |
yes | PNG rendering and raster IoU evaluation via tiny-skia |
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_TYPEFIT |
Print sub-pixel pipeline splits, line sections, and fit errors |
IMG2BEZ_DEBUG_FLATS |
Print per-candidate junction-flat decisions in raster-loss refinement |
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)
Specimen rendering uses a repo-local Python venv and the
eliheuer/drawbot-skia fork to
render glyphs and specimen images. The venv is created at
./.venv-autoresearch:
./autoresearch/setup.shThe trace stress gate writes a 2048x2048 specimen image with two rows
(input/reference and output/trace) and three columns (outline structure,
rendered preview, and red overlay):
./render-specimen.sh
./render-specimen.sh --text aRE
./render-specimen.sh --chars "ش"
./render-specimen.sh --text "&" -- --accuracy 4Edit autoresearch/render_specimen.py to tune the DrawBot layout, grid,
colors, typography, and technical footer.
# 1. Create a research branch
git checkout -b autoresearch/$(date +%b%d | tr A-Z a-z)
# 2. Prepare the repo-local Python venv
./autoresearch/setup.sh
# 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"The autoresearch scripts create and reuse ./.venv-autoresearch by default.
Dependencies are installed from autoresearch/requirements.txt, including
drawbot-skia from eliheuer/drawbot-skia. Override the venv location with
IMG2BEZ_AUTORESEARCH_VENV=/path/to/venv if needed.
Reference font: Virtua Grotesk Regular (autoresearch/reference.ufo →
symlink to a local UFO source, gitignored). Current per-glyph quality is
tracked in the Tracing quality section above and in
docs/quality.md; results are 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/ensure_venv.sh |
Repo-local Python venv bootstrap for autoresearch scripts |
autoresearch/requirements.txt |
Python dependencies for rendering and reports |
autoresearch/run_experiment.sh |
Experiment harness: render → trace → evaluate → report |
autoresearch/render_glyph.py |
Render a .glif to PNG via drawbot-skia |
autoresearch/render_specimen.py |
Render 2048x2048 DrawBot specimen/debug sheets |
autoresearch/run_trace_stress_gate.sh |
Composite glyph stress gate and specimen generator |
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) |
render-specimen.sh |
Root-level wrapper for rendering and opening specimen sheets |
img2bez has been verified to compile with Rust 1.88 and later.
img2bez is not a Linebender project, but it is built on Linebender crates and follows their conventions. Discussion of curve and font tooling happens in the Linebender Zulip (all public content can be read without logging in). Issues and pull requests are welcome on GitHub.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.
