An open, unfixed upstream panic in a shaping library isn’t someone else’s problem once you ship on top of it — a font you don’t control can take down your process.
Today we released OxiText 0.2.2 — a shaping-correctness release that vendors its own fork of swash, oxitext-swash, to fix a Devanagari reph-duplication bug and an out-of-bounds panic that has been open on upstream since April 2025 with no fix, no PR, and no assignee. It also finishes removing the png/flate2/miniz_oxide dependency chain from the workspace and fixes color-glyph format detection for sbix/CBDT/SVG fonts.
No C. No Fortran. No png crate anywhere in the graph anymore. OxiText compiles to a single static binary (or WASM) with no system libraries and no build-time C toolchain — Pure Rust from font bytes to rendered pixel.
Why OxiText 0.2.2 is a game changer
Complex-script shaping bugs are the kind that don’t show up in a Latin-only test suite and then take down production the day real-world text arrives:
- Upstream
swash’sreorder_complexmarks a syllable’s rephignoredand re-emits it later — but for a single-base syllable, where the first base and the last base are the same glyph, the re-emit hook never fires. The scratchorderbuffer it writes into is never cleared byState::reset()and only ever grows, so the unwritten tail silently held indices from a previous shaping call.स्वर्ग(a brand-new shaper, Noto Sans Devanagari) shaped to[256, 84, 58, 58]instead of[256, 84, 58, 506]— the real reph replaced by a duplicate of its neighbour. - The same stale-
ordertail, when it held an out-of-range index, indexed past the slice — dfrg/swash#93 “Panic While Shaping”, open since 2025-04-20, reported via parley/vello_editor onबर्नार्ड. In a WASM build — a browser map, a canvas app — that panic takes down the whole surface, not just one glyph. detect_color_glyph_typegated itssbix/SVG/CBDT branches on a ttf-parser API that, as of 0.25, reports COLR coverage only — every pure-sbixor CBDT emoji font (no COLR table) was silently rendered as a monochrome outline.SwashShaper::shape_with_variationsdidlet _ = variations;and shaped the font’s default instance regardless of the caller’s requested variation axes — a documented no-op resting on an incorrect premise about the API.
OxiText 0.2.2 ends all of that.
- Both Devanagari bugs are fixed in a new vendored fork,
oxitext-swash— 61 source files, 22,865 SLoC, forked fromswash0.2.10 (upstream commit7773843). A 440,000-string randomized stress run went from 12 panics to 0 with the fix armed; a 21-case font×script A/B sweep shows exactly 6 lines changed, every one a fix, with every other script (Latin, Cyrillic, Greek, Arabic, Hebrew, Thai, CJK, Hangul, Myanmar, Tamil, Telugu, Gujarati, Gurmukhi, Malayalam, Kannada, Sinhala, Javanese) byte-identical to upstream’s output. order[..len]is now provably a permutation of0..len— every emission is guarded, the remaining hole is closed explicitly, and adebug_assert_eq!keeps a future logic leak loud in dev/test while the release path degrades to an identity fallback instead of a panic.- Color-glyph detection now probes the real per-glyph data the renderer will use (
sbix.best_strike(ppem).get(gid),glyph_svg_image,glyph_raster_image) instead of a COLR-only API, so detection and rendering can no longer disagree. shape_with_variationsactually threads variation-axis arguments through toswash’sShaperBuilder::variations(...), which was already available — the premise that it wasn’t was simply wrong.yaziandpngare both gone from the workspace, not just off the default path. The vendored fork’s own PNG-strike decoder now runs onoxiarc-deflate, andoxitext-coregained a newpng_decodemodule sooxitext-raster’s PNG-compressed bitmap-glyph path no longer needspng→flate2→miniz_oxideat all.- 845 tests passing with
--all-features(743 with default features), 80 doctests, zero clippy/compiler/rustdoc warnings.
Technical Deep Dive: the fork, the decoder, and where each fix lives
- The vendored shaping engine (
oxitext-swash). A drop-in fork via a Cargopackage =rename (swash = { package = "oxitext-swash", ... }), so every existinguse swash::...call site across the workspace is byte-unchanged. 37 of 61 files are byte-identical to upstream; every modified file carries anOXITEXT MODIFICATIONheader per Apache-2.0 §4(b), andPROVENANCE.mdrecords the full per-file divergence table. - The PNG decoder (
oxitext-core::png_decode). Mirrors 0.2.1’spng_encode, built on the sameoxiarc_deflate/oxiarc_corestack. Normalizes every still-image PNG to straight-alpha RGBA8 across all standard color types and bit depths, rejects a declaredIHDRsize unless the compressed payload could plausibly reach it at deflate’s maximum expansion ratio, and was cross-validated against 235 real-world PNGs (byte-identical to Pillow’s decode) plus a 13-million-execution fuzz run with zero crashes. - Color-glyph rendering (
oxitext-raster,oxitextfacade).Pipeline::render/render_to_imagenow reach CBDT/CBLC andsbixstrikes in the default build, andSVGglyphs behind a new opt-insvg-glyphsfeature — all three were previously either misdetected or had no code path to actually render once detected. - Fuzz coverage. Two new cargo-fuzz targets,
sdf_atlas_from_bytesandcbdt_bitmap, join the workspace’s existing fuzz suite, targeting the byte-slice parsers that read untrusted font and image data directly.
Getting Started
cargo add oxitext
The published, compile-checked quick-start example (crates/oxitext/examples/quick_start.rs, new in this release):
use oxitext::Pipeline;
let font_data = std::fs::read("MyFont.ttf")?;
let mut pipeline = Pipeline::from_bytes(&font_data, 0)?;
let metrics = pipeline.measure("Hello, OxiText!", 32.0)?;
println!("{}x{}", metrics.total_width, metrics.total_height);
let bitmap = pipeline.render_to_image(
"Hello, OxiText!",
32.0,
[255, 255, 255, 255], // background
[0, 0, 0, 255], // text color
)?;
# Ok::<(), Box<dyn std::error::Error>>(())
Shaping Devanagari or any other complex script directly? The fix lives entirely inside oxitext-swash — no API changes are needed on your side to pick it up; a version bump is the whole migration.
What’s New in 0.2.2
- Fixed: Devanagari (and every
EngineMode::Complexscript) reph-duplication bug, and the upstream out-of-bounds shaping panic (dfrg/swash#93) — both fixed in the new vendoredoxitext-swashfork. - Fixed:
oxitext-rastercolor-glyph misdetection forsbix/CBDT/SVG fonts (previously gated on a COLR-only ttf-parser API);oxitext-layouttab stops on bidi/RTL lines (a logical-vs-visual-order bug the 0.2.1 tab-stop fix didn’t cover);oxitext-shape’sshape_with_variationsnow actually applies its variation-axis arguments instead of silently ignoring them. - Added: the
oxitext-swashcrate itself (first publish);oxitext-core::png_decode;oxitext-raster::detect_color_glyph_type_at(ppem-aware color-glyph detection); runnable examples for the five crates that had none; two new fuzz targets (sdf_atlas_from_bytes,cbdt_bitmap); workspace-rootrustfmt.toml/clippy.toml. - Changed:
yaziremoved from the workspace entirely (the vendored fork’s PNG-strike decoder now usesoxiarc-deflate); thepngcrate removed from the workspace entirely (oxitext-raster’spng-bitmapfeature now maps tooxitext-core/png-decode);tiny-skia’spng-formatdefault feature dropped;oxifontecosystem 0.2.1 → 0.2.2; docs-truth reconciliation correcting stale “0.2.2 already shipped” status language and a README Quick Start example that no longer matched the realPipelineAPI.
Tips
- If you shape Devanagari, Bengali, Oriya, or any other Indic script with a reph, re-verify your output. Before this release, single-base syllables (a common case) could silently substitute the reph with a duplicate of a neighbouring glyph — visually wrong but not a crash, so it’s easy to have shipped unnoticed.
- If you shape untrusted or arbitrary user-supplied fonts, this release closes a real DoS vector. The upstream panic (dfrg/swash#93) is reachable from ordinary text, not just adversarial input —
बर्नार्डwas enough. Upgrade before shaping anything you don’t control. swash-backend/png-bitmap-enabled builds get smaller, not bigger. Bothyaziandpng(and theirflate2/miniz_oxidechain) are now completely absent from the dependency graph regardless of feature combination — verify withcargo tree -i yazi/cargo tree -i pngif you maintain your owndeny.toml.oxitext-swashis a transparent rename, not a new dependency to learn. Everyuse swash::...call site in downstream code keeps compiling unchanged; only theCargo.tomlentry needs to resolveswash = { package = "oxitext-swash", ... }via the workspace, which OxiText’s ownCargo.tomlalready does for you.- The five new runnable examples double as living API documentation —
cargo run --example quick_start -p oxitext,shape_text -p oxitext-shape,rasterize_glyph -p oxitext-raster,cldr_text_processing -p oxitext-icu, andbuild_text_styles -p oxitext-coreall compile and run against real fixtures.
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. A release that removes an unfixed upstream panic from the dependency graph, rather than working around it downstream, is exactly what that initiative is for. It pairs with OxiFont for font parsing and discovery, and sits underneath OxiUI (every widget, 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 Japanese subtitle rendering.
Repository: https://github.com/cool-japan/oxitext
Star the repo if you want a text stack where an unfixed upstream panic gets absorbed and fixed, not shipped through to your users.
The era of trusting a shaping library’s happy path because your test suite is Latin-only is over. Pure Rust typography — sovereign, safe, and now provably free of dfrg/swash#93 — is here.
— KitaSan at COOLJAPAN OÜ August 6, 2026