# oxideav-scribe
Pure-Rust **vector** font shaper + line layout for the
[oxideav](https://github.com/OxideAV) framework. Parses TTF / OTF
tables (via [`oxideav-ttf`](https://github.com/OxideAV/oxideav-ttf)
+ [`oxideav-otf`](https://github.com/OxideAV/oxideav-otf)) and emits
positioned glyphs as [`oxideav-core`](https://github.com/OxideAV/oxideav-core)
`Node` vectors ready for the rasterizer in
[`oxideav-raster`](https://github.com/OxideAV/oxideav-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:
```rust
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);
```
## Capabilities
### Outlines and rendering
- **Outline access** — `Face::glyph_path(gid)` returns a Y-up
`oxideav_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-ready `Node::Path` (or `Node::Image`
for CBDT colour glyphs).
- **Vector text API** — `Shaper::shape_to_paths` returns one
`(face_idx, Node, Transform2D)` per visible glyph. Each node is
wrapped in an `oxideav_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.italic` synthesises 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_idx` tells the consumer which face owns each glyph.
- **CBDT/CBLC colour bitmaps** — Noto Color Emoji and friends decode to
`Node::Image` carrying a `VideoFrame`; resampling to the requested
size happens in scribe (bilinear, straight-alpha).
### Shaping (GSUB / GPOS)
- **GSUB substitution** — LookupType 1 (single), 2 (multiple), 3
(alternate), 4 (ligature) are applied through the caller-driven
`Face::shape_text(text, features)` surface; contextual / chained /
reverse-chained (types 5 / 6 / 8) flow through `Shaper::shape` /
`FaceChain::shape`. The required-feature `ccmp` runs as a pre-ligature
pass and `calt` as 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.
- **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), and mark-to-mark
stacking (type 6). Together this is enough for Latin / Cyrillic /
Greek / basic CJK / Vietnamese / polytonic Greek.
- **Feature-tag introspection** — `Face::gsub_features_for_script` /
`has_gsub_feature` report the feature tags the active face publishes
under an OpenType script tag, for higher-level APIs that gate on
feature presence. (A GPOS introspection mirror is a follow-up.)
- **Explicit-script + alternate-index shaping** —
`shape_text_with_script` resolves features against one named script
tag (no priority walk, avoiding cross-script collisions like `liga`
under both `latn` and `arab`); `shape_text_with_alternates` /
`shape_text_with_script_and_alternates` name the `AlternateSet` entry
the Type-3 walker picks. The auto-probe `shape_text` walks a broad
script-tag priority list (Latin / Cyrillic / Greek / DFLT / Arabic /
Hebrew / Thai / Lao / the Indic v1+v2 scripts / Khmer / Myanmar /
Hangul / Han / Kana).
### Complex-script shaping
- **Arabic contextual joining** — `shaping::arabic` picks `isol` /
`init` / `medi` / `fina` per character via the joining-class state
machine; `FaceChain::shape` rewrites 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::indic` classifies
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 via `rphf`, and wires the
cluster-position-aware GSUB features (`half`, `pref` / `blwf` /
`abvf` / `pstf`, the presentation features `pres` / `psts` / `abvs` /
`blws`, and the context-aware `locl` / `nukt` / `akhn` / `cjct` /
`init` / `haln`). Per-script reorder rules are exposed as
`DEVANAGARI_RULES` / `BENGALI_RULES` / … / `BURMESE_RULES` for callers
reusing the cluster machine. Coverage misses pass through unchanged.
### Variable fonts
- **Outline interpolation** — `Face::set_variation_coords` /
`variation_axes` / `named_instances` / `is_variable` surface the
`fvar` declarations 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 emitted
`Path` carries the blended deltas. CFF2 variable charstrings (the
`blend` operator) are not yet emitted — scribe parses the CFF2 INDEX
for table presence / axis count / glyph count via `Face::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), and
`Face::stat()` / `stat_axes()` / `stat_axis_values()` (Style
Attributes) all resolve at the current variation coords. They share an
`ItemVariationStore` + `DeltaSetIndexMap` parser in `crate::variations`.
- **`name`-id resolution** — `Face::name_id(nid)` returns the
highest-ranked Unicode string for a `name`-table id, resolving
`axis_name_id` / `subfamily_name_id` / `value_name_id`.
### Layout and bidirectional text
- **Line layout** — line measurement + word-wrap.
- **High-level bidi bridge** — `layout::reorder_line_visual(text,
base_level) -> VisualLine` drives 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. `VisualLine`
publishes `visual: Vec<char>` (L4-mirrored, render order), the
`logical_to_visual` / `visual_to_logical` permutation pair (the latter
precomputed for O(1) cursor hit-testing), and the resolved
`base_level`. `base_level: Option<u8>` is the HL1 override.
- **Whole-text / paragraph drivers** — `bidi::process_text(text,
base_level) -> TextBidi` splits a document into paragraphs (P1) and
resolves each independently; `bidi::process_paragraph(text,
base_level)` and `process_paragraph_with_brackets(..)` (N0 wired in
between W7 and N1) compose the per-rule passes into one
`ParagraphBidi` carrier with `reorder_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` (full Unicode 16.0 `DerivedBidiClass.txt`
coverage, data-driven from the UCD snapshots under
`docs/text/unicode-bidi/`), `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, full `BidiBrackets.txt`),
`resolve_neutral_types` (N1/N2), `resolve_implicit_levels` (I1/I2),
`reset_trailing_levels` / `reorder_line` (L1/L2),
`reorder_combining_marks` (L3), and `mirrored_glyph` /
`apply_mirroring` (L4, full `BidiMirroring.txt`).
## Out of scope
- **Pixel work** — bitmap rasterisation, alpha compositing, synthetic
bold dilation, stroke dilation. All in
[`oxideav-raster`](https://github.com/OxideAV/oxideav-raster).
- **Bidi HL1..HL6 higher-level-protocol overrides** — the rule pipeline
itself is complete; HL overrides remain caller responsibility.
- **CFF2 variable charstrings** — the `blend` operator 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.
## Test fixtures
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`.
## License
MIT — see [`LICENSE`](LICENSE).