MCPcopy Index your code
hub / github.com/eliheuer/img2bez

github.com/eliheuer/img2bez @main

Chat with this repo
repository ↗ · DeepWiki ↗ · + Follow
818 symbols 2,087 edges 75 files 404 documented · 49%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

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.

Read the blog post for the design rationale, the eval harness, and an interactive in-browser demo.

A low-resolution pixelated "a" with the cubic outline img2bez traces from it: green smooth on-curve points, orange corner points, and purple off-curve handles.

It takes scanned or rendered letterforms and produces cubic outlines ready for editing or compiling into fonts, for use in AI type-design pipelines. Most autotracers reproduce a glyph's silhouette and leave the outline structure to a human; img2bez aims for the structure a font source needs: points at extrema with horizontal or vertical 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 design rationale).

Built on the Linebender ecosystem: kurbo for bezier curve math, and norad for UFO reading and writing.

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

An eval-harness specimen: the reference outlines (top row) against the traced output (bottom row), with point structure, rendered previews, and overlay diffs. Generated with ./eval-harness/render-specimen.sh --text a.

Installation

Install the CLI:

cargo install img2bez

Or use img2bez as a library:

cargo add img2bez

The default build is batteries-included (CLI, UFO output, PNG rendering, parallelism). If you are embedding img2bez as a library and want a leaner build (fewer dependencies, faster compiles, smaller binary), disable the defaults and re-add only what you need:

# core tracing only; add `ufo` for UFO/GLIF output, `render` for
# PNG rendering + raster IoU, `parallel` for per-contour rayon
cargo add img2bez --no-default-features --features ufo

Quick start

# Basic usage: trace a glyph image and insert it into a UFO source
# (the target UFO is created if it doesn't exist; --unicode is hex)
img2bez --input glyph.png --output MyFont.ufo --name A --unicode 0041

# With font metrics: --target-height is ascender minus descender,
# --y-offset is the descender, --grid snaps coordinates to a 2-unit grid
img2bez --input glyph.png --output MyFont.ufo --name A --unicode 0041 \
  --target-height 1024 --y-offset -256 --grid 2

# 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 1088 Target height in font units (ascender - descender)
--y-offset -256 Y offset after scaling (typically the descender)
--grid 2 Grid size for coordinate snapping (0 = off)
--profile wild Source-class preset: wild (unknown raster), clean (font render), or photo (soft scan of printed type — forces the de-texture pre-blur)
--accuracy profile Curve fitting accuracy in font units (smaller = tighter)
--pre-blur auto Image pre-blur sigma; soft/low-contrast sources are auto-detected and blurred (--no-auto-pre-blur disables)
--smoothing 1.0 Pre-fit smoothing multiplier (raise for noisy/blurry sources)
--corner-threshold 12 Corner turn-angle threshold in degrees
--mode default Output-shape constraint: default, smooth (all-curves), or line (all-straight)
--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
--format ufo Output format: ufo, glif, json, or svg

These tuning flags are global, so they also apply to the subcommands (img2bez masters … --profile photo). Run img2bez --help or img2bez <command> --help for the always-current, authoritative list, including the masters (multi-master variable-font) and new-font subcommands.

Library usage

use std::path::Path;

use img2bez::{trace_glyph_path, FontMetrics, Profile, TraceOptions};

let opts = TraceOptions::for_profile(Profile::Clean)
    .with_em_height(1088.0)
    .with_grid(2);
let metrics = FontMetrics::from_target_height(1088.0, -256.0)
    .with_advance(600.0);

let glyph = trace_glyph_path(
    Path::new("glyph.png"),
    "A",
    &['A'],
    &opts,
    &metrics,
)?;

// The same Glyph model serializes to JSON, GLIF, or SVG.
std::fs::write("A.glif", glyph.to_glif())?;
let json = serde_json::to_string_pretty(&glyph)?;
let svg_path = glyph.outline.to_svg_path();

# Ok::<(), Box<dyn std::error::Error>>(())

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 ─── io/bitmap.rs
      └── nearest-upscaled sources downscaled to true resolution
      |
 1. Marching-squares iso-contours at the threshold level
    ─── pipeline/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; bounded
    minimal-count subdivision fallback)
 5. Corner reconstruction, G1 align, raster-loss refinement, G2
    harmonization
    ─── pipeline/vectorize/fit/
    ─── pipeline/vectorize/refine/
      |
 6. Post-processing ─── pipeline/cleanup/
      ├── Contour direction (CCW outer, CW counter)
      ├── Grid snapping
      ├── H/V handle correction
      └── Optional chamfer insertion
      |
 7. Reposition + advance width ─── pipeline/placement.rs
      |
 8. UFO output ─── io/ufo.rs

Tracing quality

The eval-harness 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.964, with 12 glyphs reproducing their reference exactly (score 1.000) and all 62 passing the stress gate. To evaluate img2bez on your own images, use the interactive demo.

Full basic Latin sweep

Glyph Score Glyph Score Glyph Score Glyph Score
i 1.000 j 0.987 6 0.967 D 0.946
o 1.000 q 0.987 9 0.967 S 0.943
v 1.000 z 0.986 B 0.965 b 0.939
E 1.000 P 0.986 g 0.964 p 0.938
F 1.000 w 0.982 U 0.964 l 0.934
H 1.000 J 0.982 n 0.960 2 0.934
I 1.000 W 0.982 h 0.958 Y 0.931
L 1.000 x 0.981 u 0.958 s 0.927
O 1.000 R 0.981 X 0.958 m 0.922
T 1.000 C 0.978 5 0.958 y 0.918
V 1.000 a 0.972 d 0.957 0 0.915
1 1.000 4 0.971 Q 0.957 e 0.909
8 0.993 A 0.969 c 0.954 r 0.863
f 0.991 3 0.968 N 0.950 7 0.779
t 0.991 k 0.967 Z 0.950
M 0.990 K 0.967 G 0.948

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 with 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; sections that genuinely need subdivision get a bounded minimal-segment-count fit (fewest cubics that meet tolerance, tangent-continuous at the joints)
  • G2 harmonization: smooth joins get the equal-curvature editor 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, while handle directions and on-curve points are 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 via clap + serde_json (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_FIT=1 img2bez --input glyph.png --output MyFont.ufo --name A
Variable Effect
IMG2BEZ_DEBUG_FIT Print sub-pixel pipeline splits, line sections, fit errors, and refine-pass decisions
IMG2BEZ_DEBUG_CORNERS Print per-candidate corner decisions and (with the site head on) zone features and scores
IMG2BEZ_DEBUG_FLATS Print per-candidate junction-flat decisions in raster-loss refinement
IMG2BEZ_DEBUG_SIMPLIFY Print point-merge candidates and accept/reject reasons in cleanup
IMG2BEZ_DEBUG_TANGENT Print tangent-point handle decisions
IMG2BEZ_DEBUG_WELD Print convex-tip weld decisions
IMG2BEZ_DEBUG_ARCCHAIN Print arc-chain straight-run drops
IMG2BEZ_DEBUG_NO_CLEANUP Skip all post-processing
IMG2BEZ_DEBUG_PIXELDIFF Save 1:1 pixel diff image
IMG2BEZ_DEBUG_JOINT Print joint masters features, alignment columns, and section decisions
IMG2BEZ_DEBUG_JOINT_EXTREMA Print dense-extremum candidate rejections in joint tracing
IMG2BEZ_PHOTO_FAIR_DEV Override the photo-profile deviation-fairing threshold (font units)
IMG2BEZ_LOG Append a JSONL record of each trace (features + settings + output stats) to the given path
IMG2BEZ_LOG_DECISIONS Log per-candidate pipeline decisions (features + outcome) as JSONL for training data

Two more variables are opt-ins rather than debug switches: IMG2BEZ_CORNER_HEAD=1 enables the experimental learned corner gate — a tiny embedded decision-tree ensemble that refines corner detection — and IMG2BEZ_SITE_HEAD=1 enables the learned zone classifier that replaces the per-candidate corner gate (the default pipeline is fully procedural). The wasm bindings expose the same switches as cornerHead and siteHead in the trace config.

Eval harness (development)

The tracer is developed against an evaluation loop that renders a hand-drawn reference font to bitmaps, traces them back, and scores raster overlap and structural match, so changes can be pushed toward measurable goals. The harness lives in the repository (not in the published crate): see eval-harness/.

structure-compare (QA tool)

Renders every glyph of a source UFO, traces the renders with the img2bez library in-process, and writes side-by-side outline-structure sheets — the source font is ground truth for what the trace should be, so each sheet is a lead on a tracer improvement. Install it from a checkout (requires a sibling designbot checkout at ../designbot for the sheet drawing):

cargo install --path eval-harness/structure-compare

Then point it at any font:

structure-compare path/to/Font.ufo            # a-z A-Z by default
structure-compare Font.ufo --glyphs a,g,s,R   # or a subset

Sheets and a summary.tsv (point counts, kinks, magic-triangle/reach scans) land in

Core symbols most depended-on inside this repo

get
called by 98
src/ml/judge.rs
point
called by 33
eval-harness/structure-compare/src/main.rs
height
called by 20
src/pipeline/placement.rs
start
called by 16
src/font/compat.rs
band_loss
called by 15
src/pipeline/vectorize/refine/mod.rs
stroke
called by 15
eval-harness/render_specimen.py
line_seg
called by 14
src/model/geom.rs
eval
called by 14
src/font/compat.rs

Shape

Function 647
Method 75
Class 73
Enum 23

Languages

Rust76%
Python24%

Modules by API surface

eval-harness/render_specimen.py72 symbols
src/font/compat.rs47 symbols
src/main.rs39 symbols
eval-harness/structural_report.py38 symbols
eval-harness/trace_stress_gate.py35 symbols
src/pipeline/vectorize/joint.rs31 symbols
src/io/eval.rs30 symbols
src/model/config.rs28 symbols
eval-harness/structure-compare/src/main.rs27 symbols
src/pipeline/vectorize/refine/mod.rs26 symbols
src/model/outline.rs26 symbols
src/font/refit.rs23 symbols

For agents

$ claude mcp add img2bez \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact