| name | rust-fontations-patterns |
|---|---|
| description | Idioms for reading, manipulating and writing OpenType fonts in Rust with the fontations crates (read-fonts, skrifa, write-fonts and fontdrasil). Use this when writing any Rust code that parses font tables, draws outlines, measures glyphs, works with design-space coordinates/locations (including avar), edits layout tables (GSUB/GPOS/GDEF/STAT) or builds/modifies font binaries. No project-specific abstractions — only the public crate APIs. |
Idioms for the fontations family of crates, as used in real-world font tooling:
| Crate | Version | Role |
|---|---|---|
read-fonts |
0.43.3 | Low-level, zero-copy reading of font tables (FontRef, TableProvider, raw table structs). |
skrifa |
0.46.2 | High-level API: outlines, metrics, variation, glyph names, charmaps. Re-exports read-fonts as skrifa::raw. |
write-fonts |
0.52.0 | Building and modifying font binaries (FontBuilder, owned table structs). |
fontdrasil |
1.0.0 | Design-space coordinates, CoordConverter (piecewise-linear maps, avar-style). |
font-types |
0.12.5 | Shared scalar types (Tag, Fixed, GlyphId, BigEndian, NameId…). |
font-types is the foundation: read-fonts::types and write-fonts::types are
the same crate re-exported twice. skrifa::raw is read-fonts.
Choose crates by task:
- Read a raw table / field →
read-fonts(FontRef+TableProvider). - Draw, measure, get metrics/glyph-names/charmap, resolve variation →
skrifa. - Build or modify a font →
write-fonts(andToOwnedTableto go from read to write). - Nonlinear coordinate math / avar-style maps →
fontdrasil.
// read-fonts — raw access
use read_fonts::{FontRef, TableProvider, TopLevelTable, ReadError, FontData, FontRead};
use read_fonts::{ArrayOfOffsets, ArrayOfNullableOffsets, Offset, ResolveOffset};
use read_fonts::types::{Tag, Fixed, GlyphId, GlyphId16, BigEndian, NameId, LongDateTime, F2Dot14};
use read_fonts::tables::{gsub::..., gpos::..., gdef::..., stat::..., head::..., os2::...};
use read_fonts::tables::name::NameString; // etc.
// skrifa — high-level (FontRef and raw types are re-exported here too)
use skrifa::raw::{ReadError, TableProvider}; // == read_fonts
use skrifa::{GlyphId, GlyphId16, Tag, GlyphNames, MetadataProvider};
use skrifa::string::StringId;
use skrifa::setting::{Setting, VariationSetting};
use skrifa::prelude::{LocationRef, NormalizedCoord, Size};
use skrifa::outline::{DrawSettings, OutlinePen, OutlineGlyph, OutlineGlyphCollection};
use skrifa::{Axis, AxisCollection, NamedInstance, NamedInstanceCollection};
// write-fonts — building / writing
use write_fonts::FontBuilder;
use write_fonts::from_obj::ToOwnedTable;
use write_fonts::tables::{name::{Name, NameRecord}, cmap::Cmap, os2::Os2, head::Head};
use write_fonts::types::{Tag, Fixed, GlyphId, BigEndian, NameId};
use write_fonts::validate::Validate;
use write_fonts::{FontWrite, TableWriter, dump_table, OffsetMarker, NullableOffsetMarker};
use write_fonts::BuilderError;
// fontdrasil — design space / coordinates
use fontdrasil::coords::{
Coord, CoordConverter, DesignCoord, UserCoord, NormalizedCoord,
DesignSpace, UserSpace, NormalizedSpace,
Location, DesignLocation, UserLocation, NormalizedLocation, ConvertSpace,
};
use fontdrasil::types::{Axes, Axis};FontRef is the same type everywhere: read_fonts::FontRef, skrifa::FontRef,
and skrifa::font::FontRef all resolve to read_fonts::FontRef.
read_fonts::types == write_fonts::types == font_types. There is exactly one
Tag, one Fixed, one GlyphId — no cross-crate conversions needed.
| Type | Backing | Notes |
|---|---|---|
Tag |
[u8; 4] |
Tag::new(b"OS/2") (panics if not 4 bytes), Tag::new_checked(bytes) -> Result, FromStr ("wght".parse()), .to_be_bytes(), Display |
GlyphId |
u32 |
general-purpose id used by skrifa's high-level API |
GlyphId16 |
u16 |
id as stored in binary tables (coverage, class defs, GSUB/GPOS output) |
GlyphId24 |
24-bit | rare; some table formats |
Fixed |
16.16 | Fixed::from_f64(12.0), .to_f32(), .to_f64(), .to_bits(), .from_bits() |
F2Dot14 |
2.14 | normalized coordinates; this is skrifa's NormalizedCoord |
F26Dot6, F48Dot16 |
— | other fixed-point formats |
BigEndian<T> |
scalar | wraps a scalar in big-endian byte order; seen as &[BigEndian<Tag>]; unwrap with .get(), wrap with .new() / From<T> |
NameId |
u16 |
name-table id, NameId::new(u16); constants e.g. NameId::FAMILY_NAME |
LongDateTime |
i64 |
seconds since 1904-01-01 |
Offset16/24/32 |
— | offsets; .get() / ResolveOffset |
Nullable<T> |
— | nullable offset/marker |
GlyphId::from(gid16) // GlyphId16 -> GlyphId (From, infallible)
gid16.into() // same
GlyphId::from(u32) // also From<u16> and From<GlyphId24>
GlyphId16::try_from(gid) // GlyphId -> GlyphId16 (TryFrom, fallible)
gid16.to_u32() // raw valueGlyphId implements PartialEq<GlyphId16> and PartialOrd<GlyphId16> (and vice
versa), so you can compare the two directly. Read glyph ids out of raw subtables
as GlyphId16; convert to GlyphId when calling skrifa's outline/metric APIs.
let font = FontRef::new(&bytes)?; // single font (sfnt)
let glyph_count: usize = font.maxp()?.num_glyphs().into();For font collections (.ttc):
// iterate every font in a collection
for result in FontRef::fonts(&bytes) {
let font = result?;
}
// or pick one by index
let font = FontRef::from_index(&bytes, 0)?;FontRef::fonts works for single fonts too (returns one item). FileRef is the
lower-level entry point if you need to inspect the collection wrapper itself.
read-fonts gives you raw, zero-copy table structs through the TableProvider
trait. skrifa layers a semantic API on top through MetadataProvider.
use read_fonts::TableProvider; // raw: font.os2(), font.head(), font.gsub() ...
use skrifa::MetadataProvider; // high-level: font.axes(), font.charmap(), font.glyph_metrics() ...TableProvider methods (each returns Result<T, ReadError>, mostly):
os2() head() hhea() hmtx() maxp() name() post() cmap() glyf() loca() gvar() fvar() avar() gsub() gpos() gdef() stat() base() cff() cff2() colr() gasp() kern() vhea() vmtx() ...
MetadataProvider methods:
font.axes() // AxisCollection (high-level variation axes)
font.named_instances() // NamedInstanceCollection
font.localized_strings(id) // LocalizedStrings (name table, localized)
font.metrics(size, loc) // global metrics
font.glyph_metrics(size, loc)// per-glyph metrics
font.charmap() // Charmap
font.outline_glyphs() // OutlineGlyphCollection
font.attributes() // Attributes (strikeout, underline, etc.)Table presence / raw bytes:
font.has_table(b"STAT") // bool (b"..." is &[u8; 4])
font.table_data(Tag::new(b"fvar")) // Option<&[u8]>
font.table_directory.table_records() // iterate raw table recordsRaw tables expose offsets and arrays through a small set of wrapper types. Learn these and most traversal code becomes mechanical.
| Type | .iter() yields |
.get(idx) |
|---|---|---|
ArrayOfOffsets<'a, T> |
Result<T, ReadError> |
Result<T, ReadError> |
ArrayOfNullableOffsets<'a, T> |
Option<Result<T, ReadError>> |
Option<Result<T, ReadError>> |
VarLenArray<'a, T> |
Result<T, ReadError> |
Option<Result<T, ReadError>> |
ComputedArray<'a, T> |
Result<T, ReadError> |
Result<T, ReadError> |
The single most common idiom is .iter().flatten() to collapse the
Result/Option layers in one pass:
// ArrayOfOffsets<Lookup>
for lookup in gpos.lookup_list()?.lookups().iter().flatten() { /* Lookup */ }
// ArrayOfNullableOffsets<AxisValue>
for axis_value in stat.offset_to_axis_values()?.axis_values().iter().flatten() { /* AxisValue */ }
// zip coverage (Vec<GlyphId16>) with a parallel array of offsets
for (old_gid, seq) in coverage.iter().zip(sequences.iter().flatten()) { /* ... */ }Offsets are read with ? (they return Result) or via .get() for nullable
offsets. A nullable offset field is typically Option<Result<T, ReadError>> or
returned through a method that itself returns Option<Result<...>>:
if let Some(Ok(subtable)) = stat.offset_to_axis_values() { ... }coverage() tables (CoverageTable) iterate as GlyphId16. BigEndian<T>
wraps a scalar in big-endian bytes and is unwrapped with .get() (e.g.
&[BigEndian<GlyphId16>] → glyph_id.get()).
use skrifa::GlyphNames;
let names = GlyphNames::new(&font);
names.get(gid) // Option<GlyphName>; may be synthesized
names.iter() // Iterator over (GlyphId, GlyphName)GlyphName::as_str() gives the name; GlyphName::is_synthesized() tells you
whether it came from a post/CFF table or was fabricated (gidNNNN). The
typical "only trust real names" pattern:
names.get(gid).and_then(|n| (!n.is_synthesized()).then(|| n.as_str().to_string()))use skrifa::MetadataProvider;
font.charmap().map('A') // Option<GlyphId> (char -> glyph)
font.charmap().map(0x00A0_u32) // also From<u32> / From<char>
// all (codepoint, glyph) mappings
for (codepoint, gid) in font.charmap().mappings() { /* u32, GlyphId */ }
// reverse: glyph -> codepoint
let reverse: HashMap<GlyphId, u32> = font.charmap().mappings().map(|(cp, g)| (g, cp)).collect();To inspect the raw cmap table directly, use font.cmap()? (read_fonts::tables::cmap::Cmap),
which exposes encoding_records() / subtables — but the high-level charmap()
already deduplicates and handles all encodings, so prefer it.
skrifa draws glyphs through the OutlinePen trait, decoupling geometry from the
binary outline format (glyf / CFF). Implement the five methods:
use skrifa::outline::OutlinePen;
impl OutlinePen for MyPen {
fn move_to(&mut self, x: f32, y: f32) {}
fn line_to(&mut self, x: f32, y: f32) {}
fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {}
fn close(&mut self) {}
}Draw a glyph:
use skrifa::prelude::Size;
use skrifa::outline::DrawSettings;
let glyph = font.outline_glyphs().get(gid).unwrap();
let location = font.axes().location(settings); // see §11
let draw = DrawSettings::unhinted(Size::unscaled(), &location);
glyph.draw(draw, &mut pen)?;DrawSettings controls hinting and size; DrawSettings::unhinted(size, location)
is the common choice for geometry checks. Size::unscaled() is used for
font-unit work; use Size::new(ppem) for device work.
Common pens to write: an area/bounds pen, a contour-count pen, a "has ink" pen,
or a converter to another path type (e.g. kurbo::BezPath). Each curve_to
call means TrueType quadratic vs cubic depending on the outline source, which is
why the trait surfaces both quad_to and curve_to.
use skrifa::prelude::{LocationRef, Size};
use skrifa::MetadataProvider;
let metrics = font.glyph_metrics(Size::unscaled(), LocationRef::default());
metrics.advance(gid) // Option<GlyphAdvance> (width/height)
metrics.bounds(gid) // Option<BoundingBox> (x_min/y_min/x_max/y_max)
let global = font.metrics(Size::unscaled(), LocationRef::default());
global.cap_height; global.x_height; global.ascent; global.descent; // etc.LocationRef::default() (or LocationRef::new(&[])) is the default instance.
Pass a resolved Location (see §11) to measure at another point in design space.
Walk LookupList -> Lookup -> subtables and match on the subtable format. The
Subtables wrapper type is not re-exported by read-fonts, so alias it locally:
use read_fonts::tables::{
gsub::{SubstitutionSubtables, ExtensionSubstFormat1, SingleSubst, MultipleSubstFormat1,
AlternateSubstFormat1, LigatureSubstFormat1, ReverseChainSingleSubstFormat1},
layout::Subtables,
};
// read-fonts doesn't export this alias, so copy it:
type SubSubtables<'a, T> = Subtables<'a, T, ExtensionSubstFormat1<'a, T>>;
let gsub = font.gsub()?;
for lookup in gsub.lookup_list()?.lookups().iter().flatten() {
let subtables = lookup.subtables()?; // SubstitutionSubtables
match subtables {
SubstitutionSubtables::Single(st) => { /* st.iter() -> SingleSubst */ }
SubstitutionSubtables::Multiple(st) => { /* ... */ }
SubstitutionSubtables::Alternate(st) => { /* ... */ }
SubstitutionSubtables::Ligature(st) => { /* ... */ }
SubstitutionSubtables::Reverse(st) => { /* ... */ }
SubstitutionSubtables::Contextual(_) | SubstitutionSubtables::ChainContextual(_) => { /* ... */ }
SubstitutionSubtables::Extension(_) => { /* ... */ }
}
}Inside SingleSubst, for example:
match subtable {
SingleSubst::Format1(t) => {
let delta = t.delta_glyph_id() as i32;
// t.coverage()?.iter() yields GlyphId16
for old_gid in t.coverage()?.iter() {
let new_gid = GlyphId16::from((old_gid.to_u32() as i32 + delta) as u16);
}
}
SingleSubst::Format2(t) => {
// zip coverage with substitute_glyph_ids()
for (old, new) in t.coverage()?.iter().zip(t.substitute_glyph_ids().iter()) {
// new is BigEndian<GlyphId16>; use new.get()
}
}
}Glyph-id arrays in raw subtables (substitute_glyph_ids, alternate_glyph_ids,
component_glyph_ids, second_glyph) are BigEndian<GlyphId16> — call
.get(). Coverage tables yield plain GlyphId16.
Same lookup/list structure, but the subtable enum is PositionSubtables:
use read_fonts::tables::gpos::{PositionSubtables, PairPos, PairPosFormat1, PairPosFormat2};
for lookup in gpos.lookup_list()?.lookups().iter().flatten() {
let subtables = lookup.subtables()?; // PositionSubtables
match subtables {
PositionSubtables::Pair(p) => {
for pp in p.iter().flatten() {
match pp {
PairPos::Format1(f) => { /* f.coverage()?.iter() zip f.pair_sets().iter() */ }
PairPos::Format2(f) => { /* f.class_def1()? / class_def2()? + class1_records() */ }
}
}
}
_ => {}
}
}PairPos Format 1 pairs each covered glyph with a PairSet of second-glyph
records; Format 2 uses class definitions — build HashMap<class, HashSet<GlyphId>>
from class_def1()/class_def2() (skipping class 0), then iterate
class1_records() × class2_records().
use read_fonts::tables::layout::{Feature, FeatureRecord};
// Feature lists live on both GSUB and GPOS
let gsub_features = font.gsub().ok().and_then(|g| g.feature_list().ok());
if let Some(list) = gsub_features {
for record in list.feature_records().iter() {
let tag = record.feature_tag(); // e.g. "kern", "tnum"
let feature: Feature = record.feature(list.offset_data())?;
for index in feature.lookup_list_indices().iter() {
let lookup = lookup_list.lookups().get(index.get() as usize)?;
}
}
}use read_fonts::tables::gdef::GlyphClassDef;
let class_def = font.gdef()?.glyph_class_def(); // Option<Result<ClassDef>>
// map GlyphId16 -> class; missing glyphs are GlyphClassDef::Unknown
let lig_carets = font.gdef()?.lig_caret_list(); // Option<Result<LigCaretList>>STAT is awkward because of its four AxisValue formats. The recurring pattern:
use read_fonts::tables::stat::AxisValue;
let stat = font.stat()?;
let axes = stat.design_axes()?; // ArrayOfOffsets<AxisRecord>
let opsz_index = axes.iter().position(|a| a.axis_tag() == "opsz").map(|i| i as u16);
if let Some(Ok(subtable)) = stat.offset_to_axis_values() {
for axis_value in subtable.axis_values().iter().flatten() {
match axis_value {
AxisValue::Format1(v) => { v.axis_index(); v.value(); v.value_name_id(); }
AxisValue::Format2(v) => { v.axis_index(); v.nominal_value(); v.value_name_id(); }
AxisValue::Format3(v) => { v.axis_index(); v.value(); v.value_name_id(); }
AxisValue::Format4(v) => { for av in v.axis_values() { av.axis_index(); } }
}
}
}Notes:
design_axes()returns raw axis records referenced by index (u16), not tag.axis_value.flags().contains(AxisValueTableFlags::ELIDABLE_AXIS_VALUE_NAME).- Format 1/3 use
value(); Format 2 usesnominal_value()andrange_min_value()/range_max_value(); Format 4 is a set of sub-AxisValueRecords.
This is the subtlest area: two coordinate systems and two "Axis" types, plus an
overloaded name (NormalizedCoord) that means different things in each crate.
use skrifa::setting::{Setting, VariationSetting}; // { selector: Tag, value: f32 } (user coords)
use skrifa::prelude::{LocationRef, NormalizedCoord, Size}; // NormalizedCoord == F2Dot14
// High-level axes (skrifa::Axis, from axes()):
for axis in font.axes().iter() {
axis.tag(); // Tag
axis.min_value(); // f32
axis.default_value(); // f32
axis.max_value(); // f32
axis.name_id(); // StringId
axis.is_hidden(); // bool
axis.normalize(x); // f32 -> NormalizedCoord (F2Dot14)
}
// A location from user-space settings:
let location = font.axes().location(settings); // settings: IntoIterator<Item: Into<VariationSetting>>
// location: skrifa::instance::Location = Vec<F2Dot14>
// Borrowed default location:
LocationRef::default();
LocationRef::new(&[]);axes().location(settings) resolves user coords to normalized coords (applying
fvar defaults and clamping); NamedInstance::location() does the same for a
named instance. NamedInstance (from font.named_instances()) also has
subfamily_name_id(), postscript_name_id() -> Option<StringId>, and
user_coords() -> Iterator<f32>.
font.fvar()?.axes()?.iter() yields the raw fvar::Axis, whose accessors
return Fixed and use axis_tag() (not tag()):
axis.axis_tag(); // Tag
axis.min_value(); // Fixed
axis.default_value(); // Fixed
axis.max_value(); // Fixed
axis.axis_name_id(); // NameIdDo not confuse skrifa::Axis (from axes(), returns f32) with the raw
read_fonts::tables::fvar::Axis (returns Fixed).
fontdrasil models three spaces — user, design, normalized — and a
piecewise-linear converter between them (exactly what avar segment maps are):
use fontdrasil::coords::{Coord, CoordConverter, UserCoord, NormalizedCoord, DesignCoord,
UserLocation, NormalizedLocation, DesignLocation};
use fontdrasil::types::Axes;
// A converter is a piecewise-linear map user <-> normalized (design is optional):
let converter = CoordConverter::default_normalization(min, default, max); // linear
let converter = CoordConverter::new(mapping, default_idx); // piecewise (avar)
// Locations are Vec<(Tag, Coord<Space>)>:
let norm: NormalizedLocation = /* ... */;
let user: UserLocation = norm.to_user(&axes)?; // normalized -> user
let norm2: NormalizedLocation = user.to_normalized(&axes)?;
let design: DesignLocation = norm.to_design(&axes)?;
// A single coordinate:
let user = NormalizedCoord::new(0.5).to_user(&converter); // Coord<UserSpace>fontdrasil::types::Axis has fields converter: CoordConverter, tag: Tag,
min/default/max: UserCoord, name, hidden, localized_names. Axes is a
collection of these with .iter(), and provides the to_user/to_normalized/ to_design conversion methods on locations.
To get well-typed design-space math from a real font, reconstruct fontdrasil
Axes from fvar (+ avar if present):
use fontdrasil::coords::{CoordConverter, DesignCoord, NormalizedCoord, UserCoord};
use fontdrasil::types::Axis;
// Collect avar segment maps once. Avar::axis_segment_maps() returns a
// VarLenArray<SegmentMaps>, whose .iter() yields Result<SegmentMaps, ReadError>.
let per_axis_maps: Vec<_> = match font.avar() {
Ok(avar) => avar.axis_segment_maps().iter().collect::<Result<Vec<_>, _>>()?,
Err(_) => vec![],
};
let axes: Axes = font.axes().iter().enumerate()
.map(|(ix, a)| {
let (min, default, max) = (
UserCoord::new(a.min_value() as f64),
UserCoord::new(a.default_value() as f64),
UserCoord::new(a.max_value() as f64),
);
let mut fd_axis = Axis {
converter: CoordConverter::default_normalization(min, default, max),
tag: a.tag(),
name: Default::default(),
hidden: a.is_hidden(),
min, default, max,
localized_names: Default::default(),
};
// Optionally apply avar segment maps (per-axis index):
if let Some(map) = per_axis_maps.get(ix) {
let mapping: Vec<(UserCoord, DesignCoord)> = map
.axis_value_maps
.iter()
.map(|m| {
// avar maps normalized->normalized; convert 'from' back to user
// space, and treat 'to' as design space (a common convention).
let user = NormalizedCoord::new(m.from_coordinate().to_f32() as f64)
.to_user(&fd_axis.converter);
let design = DesignCoord::new(m.to_coordinate().to_f32() as f64);
(user, design)
})
.collect();
let default_idx = mapping.iter().position(|(_, d)| d.to_f64() == 0.0).unwrap_or(0);
fd_axis.converter = CoordConverter::new(mapping, default_idx)
.unwrap_or_else(|_| CoordConverter::default_normalization(min, default, max));
}
fd_axis
})
.collect();Round-trip a normalized location through fontdrasil (for denormalization) and
back into skrifa VariationSettings:
let user = normalized_location.to_user(&axes)?; // fontdrasil
let settings: Vec<VariationSetting> = user.iter()
.map(|(tag, value)| VariationSetting {
selector: Tag::new(&tag.to_be_bytes()),
value: value.to_f64() as f32,
})
.collect();
// now pass `settings` to font.axes().location(..), glyph.draw(..), etc.VariationSetting is just Setting<f32> ({ selector: Tag, value: f32 }); any
&[VariationSetting] (or iterator of Into<VariationSetting>) can be passed to
axes().location, draw_glyph, etc.
use write_fonts::{from_obj::ToOwnedTable, FontBuilder};
use read_fonts::TableProvider;
let mut os2: write_fonts::tables::os2::Os2 = font.os2()?.to_owned_table();
os2.us_weight_class = 700;
let new_bytes = FontBuilder::new()
.add_table(&os2)? // replaces/compiles the OS/2 table
.copy_missing_tables(font) // copies every table not already added
.build();ToOwnedTable converts a read-fonts table into its owned write-fonts equivalent
— always prefer it over hand-copying fields. It is implemented for every table
that has a write-fonts representation.
The three write-fonts traits:
FontWrite— the owned type knows how to serialize itself.Validate— the owned type can check itself for invariants.TopLevelTable(from read-fonts) — it is a top-level table with a 4-byte tag.
FontBuilder::add_table requires T: FontWrite + Validate + TopLevelTable.
For tables with no write-fonts representation, or surgical remove/add, iterate
the raw table directory and use add_raw:
let mut builder = FontBuilder::new();
for record in font.table_directory.table_records() {
let tag = record.tag.get();
if tag != tag_to_remove {
if let Some(data) = font.table_data(tag) {
builder.add_raw(tag, data); // copy raw bytes verbatim
}
}
}
builder.add_raw(new_tag, &[0u8; 4]); // add raw/dummy bytes
let new_bytes = builder.build();use write_fonts::tables::name::{Name, NameRecord};
use write_fonts::tables::cmap::Cmap;
use write_fonts::tables::os2::Os2;
use write_fonts::types::{Fixed, NameId, Tag};
// name table
let records = vec![NameRecord::new(3, 1, 1033, NameId::new(1), "Family".to_string().into())];
let name = Name::new(records); // or Name::default(); name.name_record = records
// cmap
let cmap = Cmap::from_mappings(
mappings.into_iter().map(|(cp, gid)| (char::from_u32(cp).unwrap_or('\0'), gid)),
)?;
// OS/2
let os2 = Os2 { us_weight_class: 700, ..Default::default() };Cmap::from_mappings accepts (char, GlyphId) pairs and picks the optimal
subtable format(s) automatically.
use write_fonts::tables::stat::{AxisRecord, AxisValue, AxisValueTableFlags, Stat};
use write_fonts::types::{Fixed, NameId, Tag};
let stat = Stat::new(
vec![
AxisRecord::new(Tag::new(b"wght"), NameId::new(256), 0),
AxisRecord::new(Tag::new(b"opsz"), NameId::new(257), 1),
],
vec![AxisValue::format_1(
1, // axis index
AxisValueTableFlags::ELIDABLE_AXIS_VALUE_NAME,
NameId::new(258),
Fixed::from_f64(12.0),
)],
NameId::new(259), // elided fallback name id
);The BASE table is deeply nested. The write-fonts construction is:
Base::new(Option<Axis>, Option<Axis>) → each Axis::new(Option<BaseTagList>, BaseScriptList) → BaseScriptList::new(Vec<BaseScriptRecord>) →
BaseScriptRecord::new(tag, BaseScript::new(Option<BaseValues>, Option<BaseMinMax>, Vec<BaseLangSysRecord>)) → BaseValues::new(default_index, Vec<BaseCoord>) →
BaseCoord::Format1(BaseCoordFormat1::new(i16)).
use write_fonts::tables::base as base;
let horiz = base::Axis::new(
Some(base::BaseTagList::new(vec![Tag::new(b"romn"), Tag::new(b"ideo")])),
base::BaseScriptList::new(vec![
base::BaseScriptRecord::new(
Tag::new(b"latn"),
base::BaseScript::new(
Some(base::BaseValues::new(
0,
vec![
base::BaseCoord::Format1(base::BaseCoordFormat1::new(0)),
base::BaseCoord::Format1(base::BaseCoordFormat1::new(800)),
],
)),
None,
vec![],
),
),
]),
);
let table = base::Base::new(Some(horiz), None);For most "difficult" tables (BASE, COLR, GDEF with mark/ligature classes,
STAT, GSUB/GPOS), the read side is one match over a big enum; the write
side is one nested ::new(...) call. Use cargo doc -p write-fonts (or read
write-fonts's tables/ source) to find the exact constructor signatures.
let mut builder = FontBuilder::new();
builder.add_table(&Maxp::default()).unwrap(); // required for the result to parse as a font
builder.add_table(&name).unwrap();
let bytes = builder.build();A font without maxp will not parse back with FontRef::new.
-
read_fonts::types==write_fonts::types==font_types. One scalar type system;ToOwnedTablehandles whole-table conversion. -
Two
Axistypes. High-levelskrifa::Axis(axes(),f32,tag()) vs rawread_fonts::tables::fvar::Axis(fvar()?.axes(),Fixed,axis_tag()). -
NormalizedCoordname collision.skrifa::prelude::NormalizedCoordisF2Dot14(one number);fontdrasil::coords::NormalizedCoordisCoord<NormalizedSpace>(number + space type). They are unrelated. -
GlyphId(u32) vsGlyphId16(u16). Raw subtable arrays/covers yieldGlyphId16; skrifa high-level APIs takeGlyphId.GlyphId16 -> GlyphIdisFrom;GlyphId -> GlyphId16isTryFrom. -
BigEndian<T>unwraps with.get(). Raw glyph-id arrays (substitute_glyph_ids,alternate_glyph_ids, etc.) areBigEndian<GlyphId16>; coverage tables yield plainGlyphId16. -
?on empty lookups can fail.lookup.subtables()returnsErrfor a lookup with zero subtables (read-fontsOutOfBounds). Useif let Ok(subtables) = lookup.subtables()when a lookup might legitimately be empty. -
Tag::newpanics on non-4-byte input. UseTag::new_checked(bytes)for untrusted/unknown lengths,Tag::from_str("wght")for literal strings, and.to_be_bytes()when crossing into fontdrasil (Tag::new(&tag.to_be_bytes())). -
.iter().flatten()collapsesResult/Optionlayers on offset arrays and nullable offsets — the standard read-fonts iteration idiom. -
Nullable offsets are
Option<Result<T, ReadError>>. Unwrap the outerOptionwithif let Some(Ok(x)), not?. -
copy_missing_tablesonly copies tables you didn'tadd_table. Adding a table you want to keep recompiles it (that's the "replace this table" behavior); to drop a table, just don't add it and don't copy it. -
Font collections. Use
FontRef::from_index/FontRef::fontsfor.ttc;FontRef::newis for single fonts.