Skip to content

eliheuer/img2bez

Repository files navigation

img2bez

CI Apache 2.0 or MIT license. Linebender Zulip chat.

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.

Autoresearch specimen for "a": the reference outline structure, rendered preview, and debug overlay (top row) next to the traced output (bottom row)

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.

Installation

Install the CLI:

cargo install --git https://github.com/eliheuer/img2bez

Or use img2bez as a library by adding it to your Cargo.toml:

[dependencies]
img2bez = { git = "https://github.com/eliheuer/img2bez" }

Quick start

# 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

CLI options

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

Library usage

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>

Pipeline overview

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

Tracing quality

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.

Full basic Latin sweep

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

Features

  • 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_opt handles 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

Cargo features

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

Debug environment variables

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

Autoresearch

An overnight research loop that autonomously improves img2bez by tracing hand-drawn reference glyphs and comparing raster + vector quality metrics. Inspired by karpathy/autoresearch.

How it works

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)

Rendering specimen sheets

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

The 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 4

Edit autoresearch/render_specimen.py to tune the DrawBot layout, grid, colors, typography, and technical footer.

Running a session

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

Current baseline

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

Switching reference fonts

# 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

Files

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

Minimum supported Rust version (MSRV)

img2bez has been verified to compile with Rust 1.88 and later.

Community

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.

License

Licensed under either of

at your option.

Contribution

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.

About

Trace raster images to font-ready bezier outlines — Rust library and CLI tool.

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors