sigilbuzz is a text shaping engine written in pure Rust. You give it a font and a string. It gives you back glyph IDs and positions, ready to draw. The API follows the HarfBuzz model (blob, face, font, buffer, shape), so it will feel familiar if you have used HarfBuzz or rustybuzz.
The core crate has no runtime dependencies, builds under no_std, and returns the
same output for the same input every time.
The current release is 0.23.0. sigilbuzz is still pre-1.0, so a minor release can change the API. The names exported from the crate root are the ones I intend to keep stable. docs/STABILITY.md lists them.
- OpenType layout: every GSUB and GPOS lookup type, GDEF, feature variations, and the
legacy
kerntable. - AAT
morxandkerx, used when a font has no GSUB or GPOS. - Complex scripts: Arabic, Syriac, Hebrew, Devanagari and the rest of the Indic family, Khmer, Myanmar, Thai, Lao, Tibetan, Mongolian, N'Ko, Old Hangul, plus every other script HarfBuzz gives the Universal Shaping Engine (Adlam, Balinese, Brahmi, Chakma, Javanese, Kaithi, Takri, and many more).
- Mixed-script runs, bidi (UAX 9 with paired brackets), and vertical text.
- Variable fonts:
fvar,avar,gvar,HVAR,VVAR,MVAR, and VARC composite glyphs. - Glyph outlines from TrueType
glyf, CFF, and CFF2. - Color fonts: COLRv0, COLRv1, CPAL, SVG-in-OT, CBDT/CBLC, sbix, and EBDT/EBLC.
- TrueType Collections (
.ttc), plus thename,BASE, andMATHtables.
Script shaping is checked against rustybuzz on real fonts (Open Sans, Amiri, and the
Noto families under tests/fonts/).
The repository is a Cargo workspace. The shaping engine is the root crate. Everything
else is optional and lives under crates/.
| Crate | What it does |
|---|---|
sigilbuzz |
Parses fonts, shapes text, and exposes outlines and font tables. |
sigilbuzz-render |
CPU rasterizer for outlines, color glyphs, SVG-in-OT, and embedded bitmaps. Includes a PNG encoder. |
sigilbuzz-paint |
Walks a COLRv1 paint tree and emits a flat list of draw commands. |
sigilbuzz-gpu |
Encodes outlines for GPU rendering with the Slug algorithm. |
sigilbuzz-subset |
Font subsetter and variable-font instancer, similar to hb-subset. |
sigilbuzz-woff |
WOFF1 and WOFF2 wrap and unwrap. |
sigilbuzz-svg |
Writes glyph outlines and COLRv1 glyphs as SVG. |
sigilbuzz-pdf |
Emits Type 3, Type 1, and embedded OpenType fonts for PDF. |
sigilbuzz-text-layout |
Line breaking (UAX 14), word wrap, and word boundaries. |
sigilbuzz-hyphen |
Liang hyphenation with bundled US English patterns. |
sigilbuzz-capi |
A C library that exports HarfBuzz's hb_* symbols, so C code can link it in place of HarfBuzz. |
sigilbuzz-cli |
The sigilbuzz command-line tool, similar to hb-shape and hb-subset. |
Each crate has its own README with an example.
[dependencies]
sigilbuzz = "0.23"use sigilbuzz::{feature, shape, Blob, Buffer, Face, Feature, Font};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let blob = Blob::from_path("OpenSans-Regular.ttf")?;
let face = Face::parse(&blob, 0)?;
let font = Font::new(face, 16.0);
let mut buffer = Buffer::new();
buffer.push_str("Hello, world");
let features = [Feature { tag: feature::LIGA, value: 1 }];
let run = shape(&font, &buffer, &features)?;
for glyph in &run.glyphs {
println!("gid {} advance {} cluster {}", glyph.glyph_id, glyph.x_advance, glyph.cluster);
}
Ok(())
}A few things you will likely need next:
- Right-to-left text: call
buffer.set_direction(Direction::Rtl). As in HarfBuzz, the glyphs come back in visual order (leftmost glyph first), ready to draw left to right. - Text in several scripts: with no script set,
shapesplits the text into script runs and shapes each with its own shaper, so every script gets its own shaping. To shape the buffer the way HarfBuzz does afterhb_buffer_guess_segment_properties, with one shaper for the whole buffer, callbuffer.guess_segment_properties()first, or set the script yourself withbuffer.set_script. - Mixed-direction text: build a
BidiParagraphfrom the text. It runs the Unicode bidi algorithm, shapes each run of one embedding level in logical order and in its own direction (the way HarfBuzz callers do), and puts the runs in visual order withparagraph.shape(&font, &buffer, &features). Glyph clusters stay byte offsets into your text. Text with several paragraphs splits at paragraph separators, and each paragraph gets its own direction. For wrapped text,line_runsandshape_lineorder each line on its own. - Variable fonts:
font.with_coords(&coords)shapes at a given set of normalized axis coordinates. - Font collections: pass the member index to
Face::parse.fonts_in_collectiontells you how many members a.ttcfile has. - A face you can cache or share across threads:
OwnedFaceowns its bytes and has no lifetime parameter.
- Pure Rust. No C, no FFI, no C toolchain. It builds anywhere stable Rust builds,
including
wasm32-unknown-unknown. - No runtime dependencies in the core crate. A few companion crates take one where writing our own made no sense (Brotli for WOFF2, zlib for WOFF1 and PNG, clap for the CLI). docs/deps.md explains each one.
no_stdsupport. The default build usesstd, but the shaping path runs underno_stdwithalloc.- Deterministic output. The same font, text, features, and direction produce the same glyphs, byte for byte. That matters for replays, lockstep networking, and golden-file tests.
- Keep up with current HarfBuzz, including the newer pieces like GPU outline encoding, color paint, and PDF and SVG output.
- The Rust API follows HarfBuzz's structure with Rust types and ownership. It does not
mirror the C API. C callers can use
sigilbuzz-capi, which exports thehb_*symbols. - sigilbuzz does not aim for bug-for-bug compatibility with older HarfBuzz releases. Where HarfBuzz keeps a behavior for historical reasons, sigilbuzz picks the simpler rule.
I needed a text shaper for a Rust rendering project, and the Rust options had stalled. When I started in early 2026, rustybuzz had not published a release since November 2024. harfbuzz-rs had barely changed since 2021 and still targeted HarfBuzz 2.x. The 2026 HarfBuzz release added a GPU rasterizer, COLR paint, and PDF and SVG output, and none of it was reachable from Rust. So I wrote a shaper that covers it. I test sigilbuzz against real text rendering workloads, and those workloads decide what gets built next.
- CHANGELOG.md: what shipped in each release.
- docs/ROADMAP.md: what comes next.
- docs/STABILITY.md: which APIs are stable before 1.0.
- docs/PERFORMANCE.md: benchmark numbers against rustybuzz.
- docs/deps.md: every external dependency and why it is there.
- docs/RELEASING.md: how a release is cut and published.
- fuzz/README.md: the fuzz targets and how to run them.
- agent.md: contribution rules.
After cloning, install the pre-push hook. It runs the same checks as CI:
scripts/install-hooks.shThe hook runs cargo fmt --all --check, clippy with and without default features,
cargo test --workspace --all-features, and the no_std build. CI runs the same checks on pushes
and pull requests to main and release/** branches, but the hook catches problems
first. Don't bypass it with --no-verify.
Where things live:
src/: the shaping core.tables/holds the font table parsers,ot/the OpenType layout engine and script shapers,unicode/the Unicode property data.crates/: the companion crates.tests/: integration and parity tests. Fonts live intests/fixtures/andtests/fonts/.benches/: Criterion benchmarks that run sigilbuzz and rustybuzz side by side.
The minimum supported Rust version is 1.81.
