Pure-Rust vector font shaper + line layout for the
oxideav framework. Parses TTF / OTF
tables (via oxideav-ttf
oxideav-otf) and emits positioned glyphs asoxideav-coreNodevectors ready for the rasterizer inoxideav-raster.
Scribe contains no pixel kernel: outline flattening, scanline AA,
alpha compositing, synthetic bold and stroke dilation all live in
oxideav-raster. Producing a rasterised text run is a two-step pipeline:
use oxideav_core::{Group, Node, VectorFrame};
use oxideav_raster::Renderer;
use oxideav_scribe::{Face, FaceChain, Shaper};
let bytes = std::fs::read("DejaVuSans.ttf")?;
let face = Face::from_ttf_bytes(bytes)?;
let chain = FaceChain::new(face);
// 1. Shape: emit positioned vector glyph nodes.
let placed = Shaper::shape_to_paths(&chain, "Hello, world!", 16.0);
// 2. Wrap into a VectorFrame + render via oxideav-raster.
let mut root = Group::default();
for (_face_idx, glyph_node, transform) in placed {
root.children.push(Node::Group(Group {
transform,
children: vec![glyph_node],
..Group::default()
}));
}
let mut frame = VectorFrame::new(400.0, 80.0);
frame.root = root;
let rgba: oxideav_core::VideoFrame = Renderer::new(400, 80).render(&frame);- Outline access —
Face::glyph_path(gid)returns a Y-upoxideav_core::Path(MoveTo/LineTo/QuadCurveTo/CubicCurveTo/Close). TT outlines decode quadratics; CFF charstrings decode cubics 1:1.Face::glyph_node(gid, size_px)bakes the Y-flip + scale into a render-readyNode::Path(orNode::Imagefor CBDT colour glyphs). - PostScript glyph names —
Face::glyph_name(gid)(and the lower levelFace::post()/crate::post::PostTable) resolve a glyph ID to itspost-table PostScript name. The 258 standard Macintosh names are carried asSTANDARD_MAC_GLYPH_NAMES; the parser handlespostformats 1.0 (implied standard ordering), 2.0 (per-glyph standard + custom Pascal strings), and the deprecated 2.5 (signed delta into the standard set). Format 3.0 reports no names. - Vector text API —
Shaper::shape_to_pathsreturns one(face_idx, Node, Transform2D)per visible glyph. Each node is wrapped in anoxideav_core::Group { cache_key: Some(_), .. }so the downstream rasterizer's bitmap cache memoises the rendered glyph across renders, frames, and renderer instances. - Italic synthesis —
style.italicsynthesises a 12° forward shear when the face is upright; falls back to the font's own slant when one is present. Bold synthesis is deferred to consumer code (or a real Bold face). - Face chain — multi-face fallback for missing codepoints; per-glyph
face_idxtells the consumer which face owns each glyph. - CBDT/CBLC colour bitmaps — Noto Color Emoji and friends decode to
Node::Imagecarrying aVideoFrame; resampling to the requested size happens in scribe (bilinear, straight-alpha).
- GSUB substitution — LookupType 1 (single), 2 (multiple), 3
(alternate), 4 (ligature), 5 (contextual), 6 (chained-contextual), and
8 (reverse-chaining contextual single) are all applied through the
caller-driven
Face::shape_text(text, features)surface as well as through the auto-probeShaper::shape/FaceChain::shapepath. The contextual types dispatch each rule's nestedSequenceLookupRecordsub-lookups; type 8 is processed right-to-left per the GSUB chapter's reverse-processing requirement. The required-featureccmpruns as a pre-ligature pass andcaltas a post-ligature pass for every Latin / Cyrillic / Greek / DFLT run. Worked examples:face.shape_text("fi", &[*b"liga"])returns a single fi-ligature glyph;face.shape_text("Hi", &[*b"smcp"])returns small-caps where the font ships them; afrac/caltcontextual rule the caller requests now fires instead of passing through. - GPOS positioning — single adjustment (type 1), pair kerning
(type 2), cursive attachment (type 3, flag-clear semantics),
mark-to-base (type 4), mark-to-ligature (type 5), mark-to-mark
stacking (type 6), and contextual / chained-contextual positioning
(types 7 / 8). The contextual pass runs last so its nested per-glyph
adjustments layer on the post-kern / post-mark / post-cursive
geometry; it accumulates the
PosRecorddeltas the GPOS lookup dispatches and is gated so plain Latin faces pay one lookup-list scan. Together this is enough for Latin / Cyrillic / Greek / basic CJK / Vietnamese / polytonic Greek. - Feature-tag introspection —
Face::gsub_features_for_script/has_gsub_featureand their GPOS mirrorgpos_features_for_script/has_gpos_featurereport the substitution / positioning feature tags the active face publishes under an OpenType script tag, for higher-level APIs that gate on feature presence (iskernthere? does the font shipcpspcapital-spacing?).layout_features_for_scriptreturns the de-duplicated GSUB ∪ GPOS union so a "which features can I toggle for this script?" surface need not care which table realises a tag. - Explicit-script + alternate-index shaping —
shape_text_with_scriptresolves features against one named script tag (no priority walk, avoiding cross-script collisions likeligaunder bothlatnandarab);shape_text_with_alternates/shape_text_with_script_and_alternatesname theAlternateSetentry the Type-3 walker picks. The auto-probeshape_textwalks a broad script-tag priority list (Latin / Cyrillic / Greek / DFLT / Arabic / Hebrew / Thai / Lao / the Indic v1+v2 scripts / Khmer / Myanmar / Hangul / Han / Kana). - Positioned caller-feature shaping —
Face::position_text(text, size_px, features)(and the explicit-scriptposition_text_with_scriptmirror, plusFaceChain::shape_with_featuresfor multi-face fallback) run the caller's requested optional/discretionary features (smcp,frac,sups/subs,onum/lnum/pnum/tnum,zero, stylistic sets, …) through GSUB substitution and then the full GPOS positioning pass, returning render-readyPositionedGlyphs with per-glyph advances and offsets. Previously the caller-feature surface stopped at GSUB and handed back bare glyph IDs; the substituted run now gets pair kerning, SinglePos, mark-to-base / mark-to-mark / mark-to-ligature attachment, cursive attachment, and contextual positioning. Ligature component counts are tracked through the Type-4 collapse so mark-to-ligature attachment targets the right component. - Itemised mixed-script shaping —
Face::position_text_itemized(text, size_px, features)runs thescriptsegmenter (below) over the input, resolves each run's Unicode script to the OpenType tag the font registers (Face::resolve_ot_script_tag: modern "v.2" tag preferred, legacy tag fallback, e.g.dev2on a modern font butdevaon a legacy-only one), and positions each run under that tag through the GSUB-feature + GPOS pipeline, concatenating the per-run glyphs in logical order. A"Hello हि"string selectslatnfor the Latin run and the Devanagari tag for the second, so a feature published under one script does not leak into the other. For a single-script input the result is identical toposition_text_with_scriptunder the resolved tag.
- Arabic contextual joining —
shaping::arabicpicksisol/init/medi/finaper character via the joining-class state machine;FaceChain::shaperewrites Arabic letters into their Presentation Forms-B equivalents before cmap so cmap-only fonts render the correct contextual shapes (including LAM-ALEF ligatures). - Indic + Brahmic non-Indic shaping —
shaping::indicclassifies Devanagari, Bengali, Tamil, Gurmukhi, Gujarati, Telugu, Kannada, Malayalam, Oriya, Sinhala, Khmer, Thai, Lao, and Myanmar / Burmese, segments runs into orthographic clusters, applies per-script pre-base matra reorder, identifies reph (or the Burmese kinzi), rewrites the leading RA to its reph form viarphf, and wires the cluster-position-aware GSUB features (half,pref/blwf/abvf/pstf, the presentation featurespres/psts/abvs/blws, and the context-awarelocl/nukt/akhn/cjct/init/haln). Per-script reorder rules are exposed asDEVANAGARI_RULES/BENGALI_RULES/ … /BURMESE_RULESfor callers reusing the cluster machine. Coverage misses pass through unchanged. - Universal cluster model —
shaping::clusterimplements the script-agnostic combining-character-sequence cluster of UAX #24 §5.2 (base +Mn/Mc/Memarks + ZWJ/ZWNJ join controls) for every script without a dedicated machine:cluster_category,universal_cluster_boundaries(half-open spans tiling the input), andcluster_script(the §5.2 first-real-script resolution).FaceChain's font fallback is cluster-atomic on top of it: a combining mark is re-sourced from its cluster's base face whenever that face covers it, so GPOS mark attachment never has to cross faces; marks the base face lacks degrade to the per-character scan. The per-script syllable grammars of the OpenType Universal Shaping Engine (USE) model — reordering classes, syllable machines for e.g. Balinese / Javanese / Cham / Tai Tham — need the per-script script-development specs, which are not in the staged docs set.
- Unicode script → OpenType tag —
script::ot_script_tag(s)/ot_script_tags(s)map a UnicodeScript(from theintlUCD tables) to its OpenTypeScriptListtag(s). The Indic scripts that register both a legacy and a "v.2" shaping tag return the pair modern-first (deva→[dev2, deva],taml→[tml2, taml], …) so a shaper can prefer the v.2 form and fall back for older fonts. The tables are transcribed from the OpenType Script Tags registry (docs/text/opentype/registries/script-tags.html, CC-BY-4.0);Common/Inherited/Unknownresolve to the Default tagDFLT. - Script-run segmentation (full UAX #24 §5) —
script::script_runs(chars)/script_runs_str(text)itemise a string into maximal same-scriptScriptRuns (char-index ranges + resolvedScript), on top of the per-character resolverscript::resolve_scripts. The resolution implements the complete UAX #24 §5 rule set:Inheritedcombining marks join the open run unconditionally (§5.2);Commonpunctuation / digits / spaces join the open run (a leadingCommonspan back-fills onto the first real script), so"abc, def"and"123abc"are each one Latin run; characters with a limitedScript_Extensionsset continue a run only when the run's script is a member (§5.3 —"アー"is one Katakana run but"abcー"splits U+30FC out of Latin, and U+060C ARABIC COMMA joins the Arabic side), with consecutive constrained characters intersecting their sets; and paired brackets resolve the closing element to the same script as its opener — the enclosing text — via the vendoredBidiBrackets.txttable (§5.1 —"abc (Ψα) def"keeps both parentheses in the Latin runs). The output is a gap-free partition. - Font-aware itemised shaping —
Face::resolve_ot_script_tag(script)picks the tag the font actually registers (v.2 preferred, legacy fallback);Face::script_run_tags(text)pairs eachScriptRunwith that resolved tag;Face::shape_text_itemized(text, features)(gids) andFace::position_text_itemized(text, size_px, features)(render-ready glyphs) shape a mixed-script string run-by-run under the resolved tags.
- Outline interpolation —
Face::set_variation_coords/variation_axes/named_instances/is_variablesurface thefvardeclarations and let callers shape against a custom axis-coord vector.Shaper::with_variation_coords(..)is the per-call override path. Glyph outlines flow through the gvar interpolator so the emittedPathcarries the blended deltas. CFF2 variable charstrings (theblendoperator) are not yet emitted — scribe parses the CFF2 INDEX for table presence / axis count / glyph count viaFace::cff2()only. - Metric-variation tables —
Face::mvar()/metric_delta(tag)(global metrics),Face::hvar()/h_advance_delta(gid)(horizontal advance),Face::vvar()/v_advance_delta(gid)(vertical), andFace::stat()/stat_axes()/stat_axis_values()(Style Attributes) all resolve at the current variation coords. They share anItemVariationStore+DeltaSetIndexMapparser incrate::variations. name-id resolution —Face::name_id(nid)returns the highest-ranked Unicode string for aname-table id, resolvingaxis_name_id/subfamily_name_id/value_name_id.
- Baseline metrics —
Face::has_base_table/Face::baseline_coord( axis, script_tag, baseline_tag)surface theBASEtable's per-script design-unit baseline offsets (romanromn, ideographicideo, hanginghang,math, …) so a layout engine can sit a Latin run and a CJK run on a common line.BaselineAxis::{Horizontal, Vertical}selects the Y (horizontal-layout) or X (vertical-layout) axis; variable TTF faces resolve the coordinate at the current instance through theBASEItemVariationStore.default_baseline_tag/default_baseline_coordresolve the baseline a script declares as its own default (BaseScript::defaultBaselineIndex), so a caller can align to the font's intent without hard-codingromn. Works for both TTF and OTF/CFF faces;Nonewhen the table, axis, script, or baseline tag is absent. - Line layout — line measurement + word-wrap.
layout::wrap_linesbreaks logical text to a pixel width;layout::wrap_and_shape_lines( chain, text, size_px, max_width, base_level)is the one-call path that wraps and shapes each produced line into aShapedVisualLine(bidi-ordered, render-ready), one per display line top-to-bottom. - Document layout —
layout::shape_paragraphs(chain, text, size_px, max_width, base_level)is the multi-paragraph driver: it splits the text on UAX #9 bidi-class-Bparagraph separators (LF, CR, CRLF, NELU+0085,U+2029), resolves each paragraph's own base direction (P1 / P2 / P3, unless a uniformbase_levelis forced), and wraps + bidi-shapes every paragraph independently — returning oneShapedParagraph { lines, base_level }per source paragraph. (LINE SEPARATORU+2028is classWS, a line break within a paragraph, so it does not start a new one.) - Bidi-shaped visual line —
layout::shape_visual_line(chain, text, size_px, base_level) -> ShapedVisualLineis the join between the UAX #9 reordering pipeline and the OpenType shaper. It partitions the line into bidi level runs, shapes each run's logical substring through the face chain (so ligatures / Arabic joining / Indic clustering see the natural character sequence), reverses each RTL run's glyph sequence, and concatenates the runs in §3.4 L2 visual order. The result is aVec<PositionedGlyph>a renderer paints left-to-right with the pen advancing normally — correct mixed-direction layout without the caller hand-rolling the run arrangement.ShapedVisualLine::width()reports the laid-out advance. - High-level bidi bridge —
layout::reorder_line_visual(text, base_level) -> VisualLinedrives the complete UAX #9 pipeline over one display line (class assignment → P → X → W → N0 → N1/N2 → I → L1 → L2 → L3 → L4) and returns the characters in left-to-right visual order ready to feed glyph-by-glyph into the shaper.VisualLinepublishesvisual: Vec<char>(L4-mirrored, render order), thelogical_to_visual/visual_to_logicalpermutation pair (the latter precomputed for O(1) cursor hit-testing), and the resolvedbase_level.base_level: Option<u8>is the HL1 override. - Whole-text / paragraph drivers —
bidi::process_text(text, base_level) -> TextBidisplits a document into paragraphs (P1) and resolves each independently;bidi::process_paragraph(text, base_level)andprocess_paragraph_with_brackets(..)(N0 wired in between W7 and N1) compose the per-rule passes into oneParagraphBidicarrier withreorder_paragraph()/reorder_line_range(start..end)helpers. - Per-rule UAX #9 surface — the complete rule pipeline is also
exposed as individual public functions for callers needing finer
control:
bidi_class(fullBidi_Classcoverage via theintlcrate's compiled UCD tables, plus the UAX #9 §3.2 unassigned-block defaults),paragraph_level/split_paragraphs(P1/P2/P3),resolve_explicit_levels(X1..X9 stack),level_runs/isolating_run_sequences(X10 BD7/BD13 partition + sos/eos),resolve_weak_types(W1..W7),paired_bracket/bracket_pairs/resolve_bracket_pairs(N0, fullBidiBrackets.txt),resolve_neutral_types(N1/N2),resolve_implicit_levels(I1/I2),reset_trailing_levels/reorder_line(L1/L2),reorder_combining_marks(L3), andmirrored_glyph/apply_mirroring(L4,Bidi_Mirroring_Glyphvia theintlcrate).
- Pixel work — bitmap rasterisation, alpha compositing, synthetic
bold dilation, stroke dilation. All in
oxideav-raster. - Bidi HL1..HL6 higher-level-protocol overrides — the rule pipeline itself is complete; HL overrides remain caller responsibility.
- CFF2 variable charstrings — the
blendoperator is not yet emitted (the INDEX walker is parsed for table metadata only). - TrueType bytecode hinting, subpixel LCD filtering, and the
GPOS cursive attachment RIGHT_TO_LEFT flag-set variant (needs
lookup-flag exposure in
oxideav-ttf's public GPOS API) — deferred.
Reuses crates/oxideav-ttf/tests/fixtures/DejaVuSans.ttf plus
DejaVuSansMono.ttf (Bitstream Vera license),
crates/oxideav-otf/tests/fixtures/SourceSans3-Regular.otf (SIL OFL),
and a vendored copy of InterVariable.ttf (SIL OFL — see
tests/fixtures/INTER-OFL-LICENSE.txt) for the variable-font suite.
Network-gated emoji/CJK fixtures fetch on demand; see
tests/font_fixtures/ and run with OXIDEAV_NETWORK_TESTS=1.
MIT — see LICENSE.