The text in this article's figures isn't an image and it isn't the browser's own type. Every GlyphOne drawn shape in a font. A character and a glyph aren't the same thing: one character can need several glyphs, and one glyph can stand for several characters. is being shaped right now, in your browser, by sigilbuzz compiled to WebAssemblyA compact binary format that browsers run at close to native speed. Languages such as Rust compile to it, so the same code can run natively and on the web., and then drawn on a canvas.
sigilbuzz is a Text shapingTurning a string of characters into the glyphs a font should draw and the position of each one, following the font's rules for the script being written. engine written in RustA programming language known for being fast and for catching whole classes of bugs before a program ever runs.. You give it a font and a string, and it gives you back the glyphs to draw and where to put each one. It covers the same ground as HarfBuzz, the shaper behind Chrome, Firefox, Android and LibreOffice, and its API (application programming interface)The set of requests one program accepts from another. A web API is how apps, scripts and AI agents ask a service to read or change its data. follows HarfBuzzThe open-source text shaping engine, written in C++, that most browsers, operating systems and design tools use to lay out text.'s model, so it will feel familiar if you've used HarfBuzz or rustybuzz. As of this week it's on crates.io.
What a shaper does
A string is a list of characters. A font is a list of glyphs, the drawn shapes. Getting from one to the other is rarely one to one, and the work in between is called shaping:
- In English, "f" followed by "i" can become a single LigatureA single glyph that replaces two or more letters, such as the joined fi in many typefaces. glyph, and KerningAdjusting the space between particular pairs of letters, such as A and V, so they look evenly spaced. pulls "A" and "V" closer together.
- In Arabic, every letter has up to four forms depending on its neighbors, and the text runs right to left.
- In Devanagari, a vowel sign written after its consonant is drawn before it.
- In Korean, letters stack into syllable blocks.
The rules live in the font, in OpenTypeThe standard font format used almost everywhere. Besides the outlines, an OpenType font carries tables of rules for substituting and positioning glyphs.'s GSUB and GPOS tables. The shaper's job is to know which rules apply to which script, apply them in the right order, and hand back glyph IDs with positions. Drawing is somebody else's job.
In code it's the same five steps HarfBuzz uses: load the font
file, parse a face, make a font at a size, fill a buffer, and
call shape.
use sigilbuzz::{shape, Blob, Buffer, Face, Font};
let blob = Blob::from_path("NotoSansKR-VF.otf")?;
let face = Face::parse(&blob, 0)?;
let font = Font::new(face, 16.0);
let mut buffer = Buffer::new();
buffer.push_str("한국어를 공부해요");
let run = shape(&font, &buffer, &[])?;
for glyph in &run.glyphs {
println!("gid {} advance {}", glyph.glyph_id, glyph.x_advance);
}Why I wrote it
I needed a text shaper for the game engine I've been building, and when I started in April the Rust options had stalled. rustybuzz hadn't published a release since November 2024, and harfbuzz-rs still targeted HarfBuzz 2.x. Meanwhile HarfBuzz itself had grown a GPU (graphics processing unit)The chip that trains and runs AI models far faster than a computer's main processor. RasterizerCode that turns vector outlines into pixels., color paint, and PDF and SVGA web image format for drawings, written as text. Because it can also carry scripts, services treat it as potentially active content. output, and none of it was reachable from Rust.
So I wrote one, with a few rules I didn't want to bend:
- Pure Rust. No C, no FFIForeign function interface: how code in one language calls a library written in another, such as Rust calling into C., no C toolchain. It builds anywhere stable Rust builds, WebAssembly included, which is why these figures exist.
- No runtime dependencies in the core crate. It also
builds under
no_std. - Deterministic. The same font, text and features give the same glyphs, byte for byte. My engine runs multiplayer in LockstepA multiplayer model where every player runs the same simulation on the same inputs and must reach exactly the same result each step, so any difference between machines breaks the game., so every player has to lay out text identically.
Today sigilbuzz is twelve crates: the shaper, plus a CPU (central processing unit)A computer's main general-purpose processor.
rasterizer, a COLRv1The OpenType color font format that describes each glyph as layers of shapes filled with solid colors and gradients, which can also vary along a font's axes. paint walker, a GPU outline encoder for the
Slug algorithm, a Font subsetterA tool that cuts a font down to only the glyphs a page or app uses, so it downloads much faster. and variable-font instancer, WOFF and
WOFF2, SVG and PDF output, line breaking, hyphenation, a C
library that exports HarfBuzz's hb_* symbols so C code can link
it in place of HarfBuzz, and a command-line tool.
Hangul, from letters to syllables
I've studied Korean for a while, and I still study it in my free time, so Hangul was the script I most wanted sigilbuzz to get right.
Hangul is an alphabet. Modern Korean uses 24 basic letters, called JamoThe individual letters of Hangul, the Korean alphabet. Consonant and vowel jamo stack into syllable blocks, so ㅎ, ㅏ and ㄴ make 한., but they aren't written in a row: each syllable stacks an initial consonant, a vowel and sometimes a final consonant into one block. ㅎ, ㅏ and ㄴ make 한.
UnicodeThe standard that gives every character in every writing system a number, called a code point, and defines rules for text such as line breaking and bidirectional ordering. gives you two ways to write that. There are 11,172 precomposed syllables, one Code pointThe number Unicode assigns to one character, written like U+D55C. each, and there are the conjoining jamo themselves, one code point per letter. A shaper has to turn either spelling into the same glyph.
That second line is my favorite thing in this article. It's the opening of the Korean-language edition of the Hunminjeongeum, "the speech of our country differs from that of China", and it uses two letters modern Korean has since dropped: the arae-a, a vowel written as a single dot, and the old final ng, a circle with a short stroke on top. Your browser's own fonts most likely can't draw those syllables at all, which is why I've described them here rather than typed them. sigilbuzz's Hangul shaper follows HarfBuzz's: it composes jamo into a syllable when the font has one, decomposes a syllable the font lacks, and hands everything else to the font's jamo features.
세로쓰기
Korean can also be set in vertical columns, read top to bottom and right to left, the way Korean newspapers were printed until the 1990s.
Vertical text needs more than a rotated line. The shaper runs in
a top-to-bottom direction, the vert feature swaps in upright
brackets and punctuation, and the advances and origins come from
the font's vertical metrics tables, vmtx and VORG, rather
than its horizontal ones.
Korean also wraps differently from English. Browsers break lines
between any two syllables by default, but most Korean reads
better broken only at spaces, which CSS calls keep-all.
sigilbuzz-text-layout does both now, and the figures above wrap
with keep-all.
Variable and color fonts
The headline at the top uses a Variable fontA single font file that covers a continuous range of styles, such as every weight from thin to black, by storing how each outline changes along one or more axes.: one file holding every weight from thin to black. The font stores a default outline plus deltas that move its points as you change an axis, and the shaper has to apply the same deltas to advances and positions so the text still fits together at weight 612 or anything else.
Color fonts go further. COLRv1 fonts describe each glyph as a small graph of layers, gradients and transforms, and those can vary too.
The fonts on this page
Noto Sans KR covers every syllable in Korean, so the variable font is 8.7 MB (megabyte)About a million bytes. A typical photo from a phone is a few megabytes.. These figures only use a few dozen characters, so I cut it down with sigilbuzz's own subsetter: 93 KB with the weight axis and the vertical metrics intact, 28 KB as your browser downloaded it. With Nabla and the WebAssembly bundle, the four figures cost about 470 KB, and nothing loads until a figure scrolls into view.
What the demos found
A demo is a good test. Building these four turned up bugs that the test suite hadn't:
- TrueType variable fonts distorted away from their default weight, because sigilbuzz didn't infer deltas for points the font leaves out. At the heaviest weight, 4,823 of Noto Sans KR's 11,172 syllables were wrong.
- Composite glyphs, such as accented letters, ignored the variation entirely.
- CFF2 glyphs left their last contour open, which drew horizontal streaks through 1,440 Korean syllables.
- The subsetter dropped the vertical metrics tables, so a subset font couldn't set vertical text.
- Feature variations, where a font swaps glyphs at certain axis values, weren't implemented.
All of them are fixed in 0.23.0, along with full Unicode line breaking for Korean. Before it shipped, I also had each fix reviewed against HarfBuzz's own source, which caught two more problems in the fixes themselves, so those are fixed too.
The figures kept me looking at variable fonts after that, and 0.23.1, published the day this article went live, closes the rest of the gap with HarfBuzz there:
- Axis settings now turn into the same internal coordinates HarfBuzz computes, over 73,000 settings of seven variable fonts. Before, 12,751 of them differed, enough to move some advances by a unit.
- Vertical origins now move with the axes. Top-to-bottom runs of Noto Sans KR and six other variable fonts match HarfBuzz at every setting tested, where tens of thousands of offsets were off before.
- The subsetter's instancer, which bakes a variable font down to one weight, matches HarfBuzz's instancer on Noto Sans KR, Hahmlet, Rubik and Roboto Flex. Noto Sans KR at weight 700 used to come out with 7,210 glyphs more than a unit off.
The figures on this page now run 0.24, the release after that, described below.
How I check it
Shaping is only useful if it matches what everyone else does, so sigilbuzz is tested against HarfBuzz and rustybuzz on real fonts. Expected outputs come from HarfBuzz 14.5.0, and the complex scripts are checked on large test sets: every one of 7,656 Indic test strings, 97,234 strings across 56 scripts that use the Universal Shaping Engine, and 5,568 mixed Hangul strings at every cluster level. Bidi, line breaking and word boundaries each pass every line of Unicode's own conformance files.
Under that there are more than 3,500 tests and nine fuzz targets, covering font parsing, shaping, subsetting, WOFF, rasterizing and line breaking, and every release has to pass the same gate.
Getting it
[dependencies]
sigilbuzz = "0.24"Every crate is on crates.ioThe public registry where Rust libraries, called crates, are published for anyone to use. under Apache License 2.0A permissive open-source license. You can use, change and redistribute the code, including inside closed-source products and hosted services, as long as you keep the license and copyright notices., and the source is
on GitHub. The command
line tool works like hb-shape and hb-subset:
cargo install sigilbuzz-cli
sigilbuzz shape NotoSansKR-VF.otf "한국어"What it doesn't do yet
- The rasterizer has no hinting and no subpixel positioning.
- The GPU encoder packs outlines for the Slug algorithm, but you bring your own shader.
- Line breaking for Thai, Lao and Khmer needs a dictionary, and sigilbuzz doesn't ship one yet.
0.24, and what's next
0.24 shipped the day after this article went live, and its headline is speed. A font now keeps what HarfBuzz keeps between calls: which lookups can't apply to which glyphs, the language system each script resolves to, and every glyph's varied advance. From the second call on, one Noto Sans KR syllable shapes in about 4 µs (microsecond)A millionth of a second. instead of 62, and one Devanagari character in 13 µs instead of 400. HarfBuzz is still faster, about twice as fast on a hundred characters of Korean, and that's the gap I'm working on next.
0.24 also makes HarfBuzz's default cluster level sigilbuzz's
default, so a combining mark or the rest of an emoji sequence
shares the cluster of the character it belongs to. On 554 cases
built from real projects' fonts and strings, the clusters match
HarfBuzz in every one. Code that wants one cluster per character
asks for it with ClusterLevel::MonotoneCharacters.
If you work with text in Rust, I'd like to hear what breaks.
