A font-fallback bug that only misdraws text when your primary and fallback fonts number their glyphs differently is the kind of bug that passes every test you happen to run with Noto Sans.
Today we released OxiText 0.2.3 — a shaping-correctness release that fixes a silent glyph-misattribution bug in the .notdef font-fallback path, and adds a small API for checking whether the shaper accepts a given OpenType script tag before you shape anything.
No C. No Fortran. No system font-shaping library anywhere in the graph. OxiText compiles to a single static binary (or WASM) with no build-time C toolchain — Pure Rust from font bytes to rendered pixel.
Why OxiText 0.2.3 is a game changer
Font-fallback bugs are the worst kind to catch in testing, because they depend on which two fonts you happen to pair:
shape_run_with_notdef_fallbackshaped every glyph in a run into a singleShapedRun— andShapedRuncarries exactly onefont_datafield for its entire glyph slice. The first time a fallback font supplied a glyph, that one field got overwritten with the fallback’sfont_data.- Every other glyph in the run — glyphs the primary font had already shaped correctly, with no fallback needed — silently inherited that overwrite. They kept the primary font’s glyph ids, but were now tagged as belonging to the fallback face.
- A caller rasterizing per
(font_data, gid)therefore looked up the primary’s glyph ids inside the fallback’sglyf/CFFtable — drawing whatever glyph happened to sit at that id in the wrong font. - The bug was invisible exactly when it mattered least: whenever the two faces happened to number their glyphs alike for the codepoints in play — true for Noto Sans paired with Meiryo, MS Gothic, MS Mincho, or Yu Gothic on basic Latin text. Ship a less-aligned font pairing, or non-Latin text, and the same code path drew wrong letters.
- A second, related bug rode along in the same function: only the first non-
.notdefglyph a fallback produced for a cluster was kept, so a fallback that shaped a cluster into a base plus a combining mark silently dropped the mark.
OxiText 0.2.3 ends all of that.
shape_run_with_notdef_fallbacknow returns oneShapedRunper font actually used, split at font boundaries in logical order — instead of one run whose singlefont_datafield got clobbered by the last fallback hit.- No caller-side migration needed. Every caller already accepted
&[ShapedRun], because the bidi-reordering path has always been capable of producing more than one run. The fix just makes font-fallback use the plumbing that was already there. - The dropped-mark bug is fixed by the same change — a fallback shaping a base+mark cluster now keeps both glyphs, correctly attributed to the font that actually produced them.
- A new introspection API:
oxitext_shape::{Script, Tag, tag_from_bytes}, re-exported fromoxitext-shapeand again from theoxitextfacade under thepurefeature.Script::from_opentype(tag)round-trips throughScript::to_opentype(), so a caller can check whether the shaper accepts a given OpenType script tag before passing it toShapeRequest::script— no shaping attempt or direct shaper-crate dependency required just to check. - 6 new regression tests (
crates/oxitext/tests/fallback_runs.rs), built entirely on bundled fonts — no system-font dependency, so the regression is deterministic in CI. - 851 tests passing with
--all-features(749 with default features), 80 doctests, zero warnings.
Technical Deep Dive: where the bug lived and how the fix closes it
- The data-model gap.
ShapedRunwas designed around the assumption that one run means one font. Font fallback broke that assumption without changing the type, so the run’s singlefont_datafield became a race between “whichever font supplied the last glyph.” - The fix, structurally.
shape_run_with_notdef_fallbacknow watches for font boundaries as it walks a cluster’s shaped glyphs and emits a newShapedRuneach time the source font changes, preserving logical (not visual) order within the original run. - Why no caller had to change. The bidi-reordering path already produces multiple
ShapedRuns per logical line (one per bidi run), so every downstream consumer — layout, rasterization — already iterates&[ShapedRun]rather than assuming exactly one. Font-fallback splitting into multiple runs is the same shape of change, just from a different trigger. - The script-tag API.
Script/Tag/tag_from_byteslive inoxitext-shapeand are re-exported through theoxitextfacade’spurefeature, so a caller building on the high-levelPipelineAPI doesn’t need a direct dependency on the shaper crate just to validate a script tag ahead of aShapeRequest.
Getting Started
[dependencies]
oxitext = "0.2.3" # latest stable
use oxitext::{Pipeline, prelude::*};
let font_data = std::fs::read("my-font.ttf")?;
let mut pipeline = Pipeline::from_bytes(&font_data)?;
// Measure a string
let metrics = pipeline.measure("Hello, world!", &TextStyle::default())?;
println!("width={:.1} height={:.1}", metrics.total_width, metrics.total_height);
// Shape, lay out, and rasterize to RGBA pixels
let bg = Rgba8 { r: 255, g: 255, b: 255, a: 255 };
let fg = Rgba8 { r: 0, g: 0, b: 0, a: 255 };
let image = pipeline.render_to_image("Hello, OxiText!", &TextStyle::default(), bg, fg)?;
println!("{}x{} RGBA pixels", image.width, image.height);
See crates/oxitext/examples/quick_start.rs for the compile-checked, runnable version of this snippet (cargo run -p oxitext --example quick_start), including the lower-level Pipeline::render call that returns per-line and per-glyph layout data.
What’s New in 0.2.3
- Fixed: a
.notdeffont-fallback hit could misattribute already-correctly-shaped primary-font glyphs to the fallback font, drawing the wrong letters whenever the two fonts didn’t share glyph-id numbering.shape_run_with_notdef_fallbacknow returns oneShapedRunper font actually used. - Fixed: a fallback that shaped a cluster into a base plus a combining mark could silently drop the mark, since only the first non-
.notdefglyph a fallback produced per cluster was kept. - Added:
oxitext_shape::{Script, Tag, tag_from_bytes}, re-exported fromoxitext-shapeand from theoxitextfacade underpure, for checking OpenType script-tag support ahead of aShapeRequest::scriptcall.
Tips
- If you render text with font fallback enabled — any multi-font pipeline that isn’t guaranteed to hit the primary font for every glyph — re-verify affected output. The bug was invisible for font pairings that happen to share glyph-id numbering on the codepoints you tested (Noto Sans + Meiryo/MS Gothic/MS Mincho/Yu Gothic on basic Latin is the common case that hid it); a different font pairing or non-Latin text could have been drawing wrong letters undetected.
- No API changes are needed to pick up the fix.
shape_run_with_notdef_fallback’s return type already flowed through&[ShapedRun]call sites everywhere; a version bump is the whole migration. - If you shape base+mark clusters through a fallback font (combining diacritics, Indic matras via fallback, etc.), this release also recovers marks that were previously silently dropped in that path.
- Use the new
Script/TagAPI to validate script tags before shaping, rather than attempting a shape call and handling failure after the fact — useful when script selection comes from user input or external font metadata you don’t fully trust. - The regression suite (
fallback_runs.rs) uses only bundled fonts, socargo test -p oxitext fallback_runsreproduces and verifies the fix deterministically with no system-font setup.
This is the foundation
OxiText is part of NoFFI — the COOLJAPAN initiative replacing every C/C++/Fortran/-sys dependency in the Rust ecosystem with a clean, memory-safe, Pure-Rust implementation. It pairs with OxiFont for font parsing and discovery, and sits underneath OxiUI (every widget that draws text, via the oxitext-sdf GPU glyph atlas), oximedia (subtitles and captions), and every other COOLJAPAN surface that shapes or rasterizes text — including a caption pipeline that depends on it directly for font-fallback-heavy subtitle rendering, exactly the code path this release fixes.
Repository: https://github.com/cool-japan/oxitext
Star the repo if you want a text stack where a silent, font-pairing-dependent rendering bug gets found and fixed before it reaches your users, not after.
The era of trusting font-fallback rendering because your test fonts happen to share glyph-id numbering is over. Pure Rust typography — sovereign, correct, and now provably right about which font drew which glyph — is here.
— KitaSan at COOLJAPAN OÜ August 12, 2026