oxitext-sdf — Signed-distance-field glyph atlas generation for OxiText

oxitext-sdf turns glyph coverage bitmaps and outlines into GPU-ready signed-distance-field (SDF) texture atlases. It powers resolution-independent text rendering: a single small atlas can be sampled in a fragment shader to draw crisp glyphs at any magnification, with cheap effects such as outlines, glows, and soft shadows.
The crate is 100% Pure Rust — no C/C++ dependencies. It implements the Felzenszwalb–Huttenlocher Euclidean distance transform (EDT) for single-channel SDFs, a Chlumsky-style edge-coloring pipeline for multi-channel SDFs (MSDF/MTSDF) generated directly from outlines, an analytic per-pixel SDF generator, a pseudo-distance (PSDF) variant, three bin-packing algorithms (shelf, MaxRects, skyline) with single- and multi-page atlases, a self-describing binary atlas format, and a backend-agnostic GPU descriptor that uploads to wgpu/Vulkan/Metal/OpenGL without depending on any of them. Outline parsing uses ttf-parser; packing parallelizes with rayon.
Installation
[dependencies]
oxitext-sdf = "0.1.0"
Feature flags
oxitext-sdf = { version = "0.1.0", features = ["simd"] }
| Feature |
Default |
Effect |
sdf |
no |
Marker feature for the public SDF API. All symbols are public by default; enabling it makes intent explicit. |
simd |
no |
Enables wide-based SIMD lanes in the distance-transform hot loop. No API change. |
Quick Start
Single-channel SDF from a coverage bitmap, packed into an atlas:
use oxitext_sdf::{compute_sdf, SdfAtlas, SdfTile};
let coverage = vec![255u8; 32 * 32];
let sdf = compute_sdf(&coverage, 32, 32, 8.0, 0)?;
assert_eq!(sdf.len(), 32 * 32);
let tile = SdfTile {
glyph_id: 0,
width: 32,
height: 32,
data: sdf,
bearing_x: 0,
bearing_y: 0,
advance_x: 32.0,
};
let atlas = SdfAtlas::pack(&[tile]);
assert!(atlas.uv_map.contains_key(&0));
# Ok::<(), oxitext_sdf::SdfError>(())
Multi-channel SDF straight from a font outline, then GPU upload:
use oxitext_sdf::{glyph_to_msdf_tile, MsdfAtlas, GpuAtlasFormat};
let font = std::fs::read("font.ttf")?;
let tile = glyph_to_msdf_tile(&font, 36, 48.0, 64, 64, 4.0, 2)?
.expect("glyph has an outline");
let atlas = MsdfAtlas::pack(&[tile], 512);
let desc = atlas.to_gpu_descriptor();
assert_eq!(desc.format, GpuAtlasFormat::Rgb8Unorm);
# Ok::<(), Box<dyn std::error::Error>>(())
API Overview
Single-channel SDF (coverage-based)
| Item |
Kind |
Description |
compute_sdf(coverage, width, height, spread, padding) |
fn |
Felzenszwalb–Huttenlocher EDT over a u8 coverage bitmap → Vec<u8> SDF (<128 outside, ≈128 on the outline, >128 inside). |
edt_2d(grid, width, height) |
fn |
Raw 2D Euclidean distance transform over an f32 seed grid (squared distances). |
glyph_to_sdf_tile(coverage, src_width, src_height, tile_size) |
fn |
Compute an SDF at source resolution then bilinearly resample to tile_size × tile_size. Default spread 8.0. |
bitmap_to_sdf_tile(bitmap, glyph_id, bearing_x, bearing_y, advance_x, spread) |
fn |
Convert an oxitext-core coverage Bitmap into an SdfTile (None for empty bitmaps). |
Outline-based SDF generators
| Item |
Kind |
Description |
glyph_to_sdf_tile_analytic(face_data, glyph_id, px_size, tile_size, spread) |
fn |
Analytic per-pixel true SDF rendered directly from the outline → Option<SdfTile>. |
glyph_to_psdf_tile(face_data, glyph_id, px_size, tile_size, spread) |
fn |
Pseudo-distance-field tile (perpendicular distance) → Option<PsdfTile>. |
extract_glyph_shape(face_data, glyph_id) |
fn |
Parse a glyph outline into a GlyphShape (Option). |
color_edges(&mut shape) |
fn |
Assign Chlumsky edge colors to a GlyphShape in place (call before compute_msdf). |
Multi-channel SDF (MSDF / MTSDF)
| Item |
Kind |
Description |
compute_msdf(shape, width, height, spread, scale, offset_x, offset_y) |
fn |
3-channel RGB MSDF from a colored GlyphShape → Vec<u8> (w·h·3). |
compute_mtsdf(shape, width, height, spread, scale, offset_x, offset_y) |
fn |
4-channel RGBA MTSDF: RGB from MSDF, alpha from a true SDF → Vec<u8> (w·h·4). |
glyph_to_msdf_tile(face_data, glyph_id, px_size, tile_width, tile_height, spread, padding) |
fn |
End-to-end MSDF tile from a font → Option<MsdfTile>. |
glyph_to_mtsdf_tile(face_data, glyph_id, px_size, tile_width, tile_height, spread, padding) |
fn |
End-to-end MTSDF tile from a font → Option<MtsdfTile>. |
EdgeColor |
struct |
Bit-flag edge color (RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA, WHITE) with has_red/has_green/has_blue. |
GlyphShape |
struct |
Decomposed outline (contours, units_per_em) used by all outline generators. |
MsdfTile |
struct |
glyph_id, width, height, data (RGB), bearing_x, bearing_y, advance_x. |
MtsdfTile |
struct |
As MsdfTile but data is RGBA (w·h·4). |
Tiles and atlases
| Item |
Kind |
Description |
SdfTile |
struct |
A single single-channel tile: glyph_id, width, height, data, bearing_x/y, advance_x. |
SdfTile::from_coverage(glyph_id, coverage, width, height, spread, bearing_x, bearing_y, advance_x) |
fn |
Build a tile from an f32 coverage bitmap (values in [0, 1]). |
SdfAtlas |
struct |
Single-channel atlas: width, height, texture (u8), uv_map: HashMap<u16, UvRect>. |
SdfAtlas::new(width, height) / with_capacity(...) |
fn |
Construct a blank / capacity-hinted atlas. |
SdfAtlas::pack(tiles) |
fn |
Power-of-two shelf-pack a tile set. |
SdfAtlas::pack_with_options(tiles, options) |
fn |
Pack with AtlasOptions; returns (SdfAtlas, AtlasStats). |
SdfAtlas::pack_growing(tiles, initial_size, max_size) |
fn |
Pack with dynamic atlas growth; returns (SdfAtlas, AtlasStats). |
SdfAtlas::add_tile(&mut self, tile) / remove_tile(glyph_id) |
fn |
Incrementally insert (Option<UvRect>) or remove (bool) a tile. |
SdfAtlas::to_bytes() / from_bytes(data) / from_static(data) |
fn |
Serialize to / load from the SDFA binary atlas format. |
SdfAtlas::export_png(path) |
fn |
Write the atlas texture as a greyscale PNG. |
MsdfAtlas |
struct |
Multi-channel atlas; MsdfAtlas::pack(tiles, atlas_size), export_png(path). |
MultiPageAtlas |
struct |
Multi-page packer for tile sets that exceed one texture; pack(tiles, page_size, padding), lookup(glyph_id) -> Option<(page, &UvRect)>. |
pack_growing(tiles, initial_size, max_size, padding) |
fn |
Free-function growing packer. |
UvRect |
struct |
u_min, v_min, u_max, v_max (all in [0, 1]). |
AtlasOptions |
struct |
atlas_size, padding, max_size, algorithm. |
AtlasStats |
struct |
tiles_packed, tiles_dropped, utilization, wasted_pixels. |
PackingAlgorithm |
enum |
Shelf (default), MaxRects, Skyline. |
GPU descriptors
| Item |
Kind |
Description |
SdfAtlas::to_gpu_descriptor() / MsdfAtlas::to_gpu_descriptor() |
fn |
Produce a backend-agnostic GpuAtlasDescriptor. |
GpuAtlasDescriptor |
struct |
width, height, format, data, uv_map: HashMap<u16, NormalizedUvRect>, glyph_metrics. |
GpuAtlasFormat |
enum |
R8Unorm (single-channel) or Rgb8Unorm (MSDF). |
NormalizedUvRect |
struct |
UV rectangle in normalized [0, 1] coordinates. |
AtlasGlyphMetrics |
struct |
bearing_x, bearing_y, advance_x, width_px, height_px. |
Build-time helpers
| Item |
Kind |
Description |
generate_atlas_binary(font_data, glyph_ids, px_size, tile_size, spread, atlas_size, output_path) |
fn |
Render a glyph set to a .bin atlas on disk (errors if tiles overflow). |
generate_ascii_atlas(font_data, px_size, output_path) |
fn |
Convenience: pre-bake the printable-ASCII range (64×64 tiles, spread 4, 512×512 atlas). |
Error type
| Variant |
Description |
SdfError::InvalidInput(String) |
Coverage slice length does not match width × height. |
SdfError::ZeroSize |
Width or height was zero. |
SdfError::InvalidFont |
Font bytes could not be parsed by ttf-parser. |
SdfError::InvalidData(String) |
Binary atlas data is malformed (bad magic, version mismatch, truncated). |
SdfError::Io(String) |
I/O error (e.g. during PNG/atlas export). |
Cross-references
License
Apache-2.0 — COOLJAPAN OU (Team Kitasan)