mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 05:07:51 -06:00
Given a family and no font style, text.create and text.createInPath kept the default Regular, and text.setStyle kept the text's previous style, even when the family has no such style. A family whose styles are numbered (Hiragino W3, W6) has no Regular, so the file named a font that isn't installed and opened with Missing Fonts. They now keep the style when the family has it, else take the family's closest one, as text.setRangeStyle already did. Its rule moves to `textedit::fit_font_style`, which the three commands share; Graph labels styled through text.setStyle follow it too. A style the caller names is kept as given. The test builds a family with W3 and W6 and no Regular in code (`test_fonts::numbered_font`), so no font file is added.
774 lines
38 KiB
Rust
774 lines
38 KiB
Rust
//! Fonts built in code for tests, so no font file is committed: a small variable font (#296).
|
||
//! Compiled for this crate's tests and, with the `test-fonts` feature, for other crates' tests.
|
||
|
||
use skrifa::instance::{LocationRef, Size};
|
||
use skrifa::raw::TableProvider;
|
||
use skrifa::raw::tables::glyf::Glyph;
|
||
use skrifa::{GlyphId, MetadataProvider};
|
||
use write_fonts::FontBuilder;
|
||
use write_fonts::tables::fvar::{AxisInstanceArrays, Fvar, InstanceRecord, VariationAxisRecord};
|
||
use write_fonts::tables::gvar::{GlyphDelta, GlyphDeltas, GlyphVariations, Gvar, Tent};
|
||
use write_fonts::types::{F2Dot14, Fixed, NameId, Tag};
|
||
|
||
/// The family of [`variable_font`].
|
||
pub const VARIABLE_FAMILY: &str = "Varitest Sans";
|
||
|
||
/// The characters [`variable_font`] widens towards its heaviest weight (simple glyphs in Source Sans
|
||
/// 3; the composite ones, like `H`, would need their components varied).
|
||
pub const VARIABLE_CHARS: &str = "lIn";
|
||
|
||
/// How much wider [`VARIABLE_CHARS`] are at the heaviest weight (outlines and advances).
|
||
pub const VARIABLE_WIDEN: f32 = 0.25;
|
||
|
||
/// The bundled Source Sans 3 Regular as a variable font of family [`VARIABLE_FAMILY`]: one weight
|
||
/// axis (400–700, default 400, the face itself being Regular) whose maximum makes the glyphs of
|
||
/// [`VARIABLE_CHARS`] [`VARIABLE_WIDEN`] wider (outlines and advances, through `gvar`), with the
|
||
/// named instances Normal (the default, the face Regular), SemiBold (550) and Bold (700), plus an instance without
|
||
/// a name and one named like another (`BOLD`), which style lists leave out. `None` when it can't
|
||
/// be built.
|
||
pub fn variable_font() -> Option<Vec<u8>> {
|
||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||
let (glyf, loca) = (font.glyf().ok()?, font.loca(None).ok()?);
|
||
let metrics = font.glyph_metrics(Size::unscaled(), LocationRef::default());
|
||
let charmap = font.charmap();
|
||
let widened: Vec<GlyphId> = VARIABLE_CHARS.chars().filter_map(|c| charmap.map(c)).collect();
|
||
let mut variations = Vec::new();
|
||
for g in 0..u32::from(font.maxp().ok()?.num_glyphs()) {
|
||
let gid = write_fonts::types::GlyphId::new(g);
|
||
let id = GlyphId::new(g);
|
||
let deltas = if widened.contains(&id) {
|
||
let Ok(Some(Glyph::Simple(s))) = loca.get_glyf(id, &glyf) else { return None };
|
||
let wider = |x: f32| (x * VARIABLE_WIDEN).round() as i16;
|
||
let mut d: Vec<GlyphDelta> = s.points().map(|p| GlyphDelta::required(wider(f32::from(p.x)), 0)).collect();
|
||
// The phantom points: left side bearing, advance, top, bottom.
|
||
let advance = wider(metrics.advance_width(id)?);
|
||
d.extend([(0, 0), (advance, 0), (0, 0), (0, 0)].map(|(x, y)| GlyphDelta::required(x, y)));
|
||
vec![GlyphDeltas::new(vec![Tent::new(F2Dot14::from_f32(1.0), None)], d)]
|
||
} else {
|
||
vec![]
|
||
};
|
||
variations.push(GlyphVariations::new(gid, deltas));
|
||
}
|
||
let gvar = Gvar::new(variations, 1).ok()?;
|
||
let axis = VariationAxisRecord {
|
||
axis_tag: Tag::new(b"wght"),
|
||
min_value: Fixed::from_i32(400),
|
||
default_value: Fixed::from_i32(400),
|
||
max_value: Fixed::from_i32(700),
|
||
flags: 0,
|
||
axis_name_id: NameId::new(256),
|
||
};
|
||
let instance = |name: u16, wght: i32| InstanceRecord {
|
||
subfamily_name_id: NameId::new(name),
|
||
flags: 0,
|
||
coordinates: vec![Fixed::from_i32(wght)],
|
||
post_script_name_id: None,
|
||
};
|
||
let instances = vec![instance(257, 400), instance(258, 550), instance(259, 700), instance(260, 600), instance(261, 650)];
|
||
let fvar = Fvar::new(AxisInstanceArrays::new(vec![axis], instances));
|
||
let name = name_table(&[
|
||
(1, VARIABLE_FAMILY),
|
||
(2, "Regular"),
|
||
(4, "Varitest Sans Regular"),
|
||
(6, "VaritestSans-Regular"),
|
||
(256, "Weight"),
|
||
(257, "Normal"),
|
||
(258, "SemiBold"),
|
||
(259, "Bold"),
|
||
(261, "BOLD"),
|
||
])?;
|
||
let mut builder = FontBuilder::new();
|
||
builder.add_raw(Tag::new(b"name"), name);
|
||
builder.add_table(&fvar).ok()?.add_table(&gvar).ok()?;
|
||
// The rest of Source Sans 3's tables as they are.
|
||
for r in font.table_directory.table_records() {
|
||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||
if !builder.contains(tag) {
|
||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||
}
|
||
}
|
||
Some(builder.build())
|
||
}
|
||
|
||
/// The family of [`two_axis_font`].
|
||
pub const TWO_AXIS_FAMILY: &str = "Biaxtest Sans";
|
||
|
||
/// How much narrower [`two_axis_font`] makes [`VARIABLE_CHARS`] at its narrowest width.
|
||
pub const TWO_AXIS_NARROW: f32 = 0.2;
|
||
|
||
/// The bundled Source Sans 3 Regular as a variable font of family [`TWO_AXIS_FAMILY`] with two axes:
|
||
/// weight (`wght` 400–700, default 400), whose maximum makes the glyphs of [`VARIABLE_CHARS`]
|
||
/// [`VARIABLE_WIDEN`] wider, and width (`wdth` 75–100, default 100), whose minimum makes them
|
||
/// [`TWO_AXIS_NARROW`] narrower (outlines and advances, through `gvar`). Its named instances are
|
||
/// Regular (the default), Bold (`wght` 700) and Condensed (`wdth` 75). `None` when it can't be
|
||
/// built.
|
||
pub fn two_axis_font() -> Option<Vec<u8>> {
|
||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||
let (glyf, loca) = (font.glyf().ok()?, font.loca(None).ok()?);
|
||
let metrics = font.glyph_metrics(Size::unscaled(), LocationRef::default());
|
||
let charmap = font.charmap();
|
||
let varied: Vec<GlyphId> = VARIABLE_CHARS.chars().filter_map(|c| charmap.map(c)).collect();
|
||
let mut variations = Vec::new();
|
||
for g in 0..u32::from(font.maxp().ok()?.num_glyphs()) {
|
||
let id = GlyphId::new(g);
|
||
let mut deltas = vec![];
|
||
if varied.contains(&id) {
|
||
let Ok(Some(Glyph::Simple(s))) = loca.get_glyf(id, &glyf) else { return None };
|
||
let advance = metrics.advance_width(id)?;
|
||
// At each axis's far end: every point and the advance phantom point moved by `k` of their x.
|
||
let moved = |k: f32| {
|
||
let by = |x: f32| (x * k).round() as i16;
|
||
let mut d: Vec<GlyphDelta> = s.points().map(|p| GlyphDelta::required(by(f32::from(p.x)), 0)).collect();
|
||
d.extend([(0, 0), (by(advance), 0), (0, 0), (0, 0)].map(|(x, y)| GlyphDelta::required(x, y)));
|
||
d
|
||
};
|
||
let peak = |w: f32, d: f32| vec![Tent::new(F2Dot14::from_f32(w), None), Tent::new(F2Dot14::from_f32(d), None)];
|
||
deltas.push(GlyphDeltas::new(peak(1.0, 0.0), moved(VARIABLE_WIDEN)));
|
||
deltas.push(GlyphDeltas::new(peak(0.0, -1.0), moved(-TWO_AXIS_NARROW)));
|
||
}
|
||
variations.push(GlyphVariations::new(write_fonts::types::GlyphId::new(g), deltas));
|
||
}
|
||
let gvar = Gvar::new(variations, 2).ok()?;
|
||
let axis = |tag: &[u8; 4], min: i32, default: i32, max: i32, name: u16| VariationAxisRecord {
|
||
axis_tag: Tag::new(tag),
|
||
min_value: Fixed::from_i32(min),
|
||
default_value: Fixed::from_i32(default),
|
||
max_value: Fixed::from_i32(max),
|
||
flags: 0,
|
||
axis_name_id: NameId::new(name),
|
||
};
|
||
let instance = |name: u16, wght: i32, wdth: i32| InstanceRecord {
|
||
subfamily_name_id: NameId::new(name),
|
||
flags: 0,
|
||
coordinates: vec![Fixed::from_i32(wght), Fixed::from_i32(wdth)],
|
||
post_script_name_id: None,
|
||
};
|
||
let axes = vec![axis(b"wght", 400, 400, 700, 256), axis(b"wdth", 75, 100, 100, 257)];
|
||
let fvar = Fvar::new(AxisInstanceArrays::new(axes, vec![instance(258, 400, 100), instance(259, 700, 100), instance(260, 400, 75)]));
|
||
let name = name_table(&[
|
||
(1, TWO_AXIS_FAMILY),
|
||
(2, "Regular"),
|
||
(4, "Biaxtest Sans Regular"),
|
||
(6, "BiaxtestSans-Regular"),
|
||
(256, "Weight"),
|
||
(257, "Width"),
|
||
(258, "Regular"),
|
||
(259, "Bold"),
|
||
(260, "Condensed"),
|
||
])?;
|
||
let mut builder = FontBuilder::new();
|
||
builder.add_raw(Tag::new(b"name"), name);
|
||
builder.add_table(&fvar).ok()?.add_table(&gvar).ok()?;
|
||
for r in font.table_directory.table_records() {
|
||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||
if !builder.contains(tag) {
|
||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||
}
|
||
}
|
||
Some(builder.build())
|
||
}
|
||
|
||
/// A `name` table of Windows English (US) records (name id, string).
|
||
pub fn name_table(records: &[(u16, &str)]) -> Option<Vec<u8>> {
|
||
let mut records = records.to_vec();
|
||
records.sort_by_key(|r| r.0);
|
||
let n = u16::try_from(records.len()).ok()?;
|
||
let (mut head, mut strings) = (Vec::new(), Vec::new());
|
||
for v in [0, n, 6 + 12 * n] {
|
||
head.extend_from_slice(&v.to_be_bytes());
|
||
}
|
||
for (id, s) in records {
|
||
let bytes: Vec<u8> = s.encode_utf16().flat_map(u16::to_be_bytes).collect();
|
||
for v in [3, 1, 0x0409, id, u16::try_from(bytes.len()).ok()?, u16::try_from(strings.len()).ok()?] {
|
||
head.extend_from_slice(&v.to_be_bytes());
|
||
}
|
||
strings.extend(bytes);
|
||
}
|
||
head.extend(strings);
|
||
Some(head)
|
||
}
|
||
|
||
/// The family of [`vertical_font`].
|
||
pub const VERTICAL_FAMILY: &str = "Vertitest Sans";
|
||
|
||
/// The character whose vertical metrics [`vertical_font`] sets apart: 1.4 em down the column, its
|
||
/// vertical origin 1.1 em above the baseline.
|
||
pub const VERTICAL_TALL: char = '§';
|
||
|
||
/// The bundled Source Sans 3 Regular as family [`VERTICAL_FAMILY`] with vertical metrics (`vhea`,
|
||
/// `vmtx`, and `VORG` when `vorg`): every glyph one em down the column with its vertical origin at
|
||
/// the usual 0.88 em, except [`VERTICAL_TALL`] (1.4 em, origin 1.1 em). Without `VORG` the origin
|
||
/// comes from each glyph's top and top side bearing, as in TrueType fonts. `None` when it can't be
|
||
/// built.
|
||
pub fn vertical_font(vorg: bool) -> Option<Vec<u8>> {
|
||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||
let n = font.maxp().ok()?.num_glyphs();
|
||
let metrics = font.glyph_metrics(Size::unscaled(), LocationRef::default());
|
||
let tall = font.charmap().map(VERTICAL_TALL)?;
|
||
let cell = |g: GlyphId| -> (u16, i16) { if g == tall { (1400, 1100) } else { (1000, 880) } };
|
||
let be16 = |v: &mut Vec<u8>, x: i16| v.extend_from_slice(&x.to_be_bytes());
|
||
// vmtx: (advance height, top side bearing = origin − the glyph's top) for every glyph.
|
||
let mut vmtx = Vec::with_capacity(4 * usize::from(n));
|
||
for g in 0..u32::from(n) {
|
||
let id = GlyphId::new(g);
|
||
let (advance, origin) = cell(id);
|
||
let top = metrics.bounds(id).map_or(0.0, |b| b.y_max).round() as i16;
|
||
vmtx.extend_from_slice(&advance.to_be_bytes());
|
||
be16(&mut vmtx, origin - top);
|
||
}
|
||
// vhea 1.1: ascent/descent of the em box across the column, then the metrics' count.
|
||
let mut vhea = Vec::with_capacity(36);
|
||
vhea.extend_from_slice(&0x0001_1000_u32.to_be_bytes());
|
||
for x in [500, -500, 0] {
|
||
be16(&mut vhea, x);
|
||
}
|
||
vhea.extend_from_slice(&1400_u16.to_be_bytes());
|
||
for x in [0, 0, 1400, 1, 0, 0, 0, 0, 0, 0, 0] {
|
||
be16(&mut vhea, x);
|
||
}
|
||
vhea.extend_from_slice(&n.to_be_bytes());
|
||
let name = name_table(&[(1, VERTICAL_FAMILY), (2, "Regular"), (4, "Vertitest Sans Regular"), (6, "VertitestSans-Regular")])?;
|
||
let mut builder = FontBuilder::new();
|
||
builder.add_raw(Tag::new(b"name"), name);
|
||
builder.add_raw(Tag::new(b"vhea"), vhea);
|
||
builder.add_raw(Tag::new(b"vmtx"), vmtx);
|
||
if vorg {
|
||
// VORG 1.0: the default origin, then the one glyph that differs.
|
||
let mut t = Vec::new();
|
||
for x in [1, 0, 880, 1] {
|
||
be16(&mut t, x);
|
||
}
|
||
t.extend_from_slice(&u16::try_from(tall.to_u32()).ok()?.to_be_bytes());
|
||
be16(&mut t, 1100);
|
||
builder.add_raw(Tag::new(b"VORG"), t);
|
||
}
|
||
for r in font.table_directory.table_records() {
|
||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||
if !builder.contains(tag) {
|
||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||
}
|
||
}
|
||
Some(builder.build())
|
||
}
|
||
|
||
/// The family of [`vertical_punctuation_font`].
|
||
pub const VERTICAL_PUNCTUATION_FAMILY: &str = "Punctuationtest Sans";
|
||
|
||
/// A test-only cmap over bundled Source Sans 3: fullwidth !/? use the ASCII glyphs,
|
||
/// 「 uses L and ー uses a hyphen. Optionally `vert` substitutes I/J for !/?, so tests
|
||
/// exercise both nominal fallbacks and actual vertical substitutions without OS fonts.
|
||
pub fn vertical_punctuation_font(alternates: bool) -> Option<Vec<u8>> {
|
||
use write_fonts::tables::cmap::Cmap;
|
||
use write_fonts::tables::gsub::{Gsub, SingleSubstFormat2, SubstitutionLookup, SubstitutionLookupList};
|
||
use write_fonts::tables::layout::{CoverageFormat1, Feature, FeatureList, FeatureRecord, LangSys, Lookup, Script, ScriptList, ScriptRecord};
|
||
use write_fonts::types::GlyphId16;
|
||
|
||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||
let charmap = font.charmap();
|
||
let mapping = [('!', '!'), ('?', '?'), ('「', 'L'), ('ー', '-'), ('!', '!'), ('?', '?'), ('I', 'I'), ('J', 'J'), ('L', 'L'), ('-', '-')];
|
||
let mappings = mapping.into_iter().map(|(c, source)| Some((c, write_fonts::types::GlyphId::new(charmap.map(source)?.to_u32()))));
|
||
let cmap = Cmap::from_mappings(mappings.collect::<Option<Vec<_>>>()?).ok()?;
|
||
let family = VERTICAL_PUNCTUATION_FAMILY;
|
||
let name = name_table(&[(1, family), (2, "Regular"), (4, "Punctuationtest Sans Regular"), (6, "PunctuationtestSans-Regular")])?;
|
||
let mut builder = FontBuilder::new();
|
||
builder.add_table(&cmap).ok()?;
|
||
builder.add_raw(Tag::new(b"name"), name);
|
||
if alternates {
|
||
let gid = |c| Some(GlyphId16::new(u16::try_from(charmap.map(c)?.to_u32()).ok()?));
|
||
let mut pairs = [(gid('!')?, gid('I')?), (gid('?')?, gid('J')?)];
|
||
pairs.sort_by_key(|p| p.0);
|
||
let coverage = CoverageFormat1::new(pairs.iter().map(|p| p.0).collect());
|
||
let subst = SingleSubstFormat2::new(coverage.into(), pairs.iter().map(|p| p.1).collect());
|
||
let scripts = ScriptList::new(vec![ScriptRecord::new(Tag::new(b"DFLT"), Script::new(Some(LangSys::new(vec![0])), vec![]))]);
|
||
let features = FeatureList::new(vec![FeatureRecord::new(Tag::new(b"vert"), Feature::new(None, vec![0]))]);
|
||
let lookups = SubstitutionLookupList::new(vec![SubstitutionLookup::Single(Lookup::new(Default::default(), vec![subst.into()]))]);
|
||
builder.add_table(&Gsub::new(scripts, features, lookups)).ok()?;
|
||
}
|
||
for r in font.table_directory.table_records() {
|
||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||
if tag != Tag::new(b"GSUB") && !builder.contains(tag) {
|
||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||
}
|
||
}
|
||
Some(builder.build())
|
||
}
|
||
|
||
/// The family of [`half_form_font`].
|
||
pub const HALF_FORM_FAMILY: &str = "Halftest Sans";
|
||
|
||
/// A test-only cmap over bundled Source Sans 3: Devanagari Ka uses K, the halant uses a period, Ssa
|
||
/// uses S and the zero width joiner uses the space. The `GSUB` table lists the `dev2` script either
|
||
/// way. With `half`, its `half` feature substitutes k for K followed by the period (Ka's half form);
|
||
/// without it, the script has no features and Ka has no half form. `None` when it can't be built.
|
||
pub fn half_form_font(half: bool) -> Option<Vec<u8>> {
|
||
use write_fonts::tables::cmap::Cmap;
|
||
use write_fonts::tables::gsub::{Gsub, Ligature, LigatureSet, LigatureSubstFormat1, SubstitutionLookup, SubstitutionLookupList};
|
||
use write_fonts::tables::layout::{CoverageFormat1, Feature, FeatureList, FeatureRecord, LangSys, Lookup, Script, ScriptList, ScriptRecord};
|
||
use write_fonts::types::GlyphId16;
|
||
|
||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||
let charmap = font.charmap();
|
||
let mapping = [('क', 'K'), ('\u{094D}', '.'), ('ष', 'S'), ('\u{200D}', ' ')];
|
||
let mappings = mapping.into_iter().map(|(c, source)| Some((c, write_fonts::types::GlyphId::new(charmap.map(source)?.to_u32()))));
|
||
let cmap = Cmap::from_mappings(mappings.collect::<Option<Vec<_>>>()?).ok()?;
|
||
let family = HALF_FORM_FAMILY;
|
||
let name = name_table(&[(1, family), (2, "Regular"), (4, "Halftest Sans Regular"), (6, "HalftestSans-Regular")])?;
|
||
let mut builder = FontBuilder::new();
|
||
builder.add_table(&cmap).ok()?;
|
||
builder.add_raw(Tag::new(b"name"), name);
|
||
let (features, lookups) = if half {
|
||
let gid = |c| Some(GlyphId16::new(u16::try_from(charmap.map(c)?.to_u32()).ok()?));
|
||
let coverage = CoverageFormat1::new(vec![gid('K')?]);
|
||
let subst = LigatureSubstFormat1::new(coverage.into(), vec![LigatureSet::new(vec![Ligature::new(gid('k')?, vec![gid('.')?])])]);
|
||
let features = FeatureList::new(vec![FeatureRecord::new(Tag::new(b"half"), Feature::new(None, vec![0]))]);
|
||
let lookups = SubstitutionLookupList::new(vec![SubstitutionLookup::Ligature(Lookup::new(Default::default(), vec![subst]))]);
|
||
(features, lookups)
|
||
} else {
|
||
(FeatureList::new(vec![]), SubstitutionLookupList::new(vec![]))
|
||
};
|
||
let langsys = LangSys::new(if half { vec![0] } else { vec![] });
|
||
let scripts = ScriptList::new(vec![ScriptRecord::new(Tag::new(b"dev2"), Script::new(Some(langsys), vec![]))]);
|
||
builder.add_table(&Gsub::new(scripts, features, lookups)).ok()?;
|
||
for r in font.table_directory.table_records() {
|
||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||
if !builder.contains(tag) {
|
||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||
}
|
||
}
|
||
Some(builder.build())
|
||
}
|
||
|
||
/// The family of [`pair_kerning_font`].
|
||
pub const PAIR_KERNING_FAMILY: &str = "Pairtest Sans";
|
||
|
||
/// The bundled Source Sans 3 Regular as family [`PAIR_KERNING_FAMILY`], its `GPOS` replaced by two
|
||
/// `kern` pairs, A followed by V and S followed by S: each takes 60 units off the first glyph's
|
||
/// advance (the pair's first value record) and moves the second 100 units left (its second).
|
||
/// `None` when it can't be built.
|
||
pub fn pair_kerning_font() -> Option<Vec<u8>> {
|
||
use write_fonts::tables::gpos::{Gpos, PairPos, PairSet, PairValueRecord, PositionLookup, PositionLookupList, ValueRecord};
|
||
use write_fonts::tables::layout::{CoverageFormat1, Feature, FeatureList, FeatureRecord, LangSys, Lookup, Script, ScriptList, ScriptRecord};
|
||
use write_fonts::types::GlyphId16;
|
||
|
||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||
let charmap = font.charmap();
|
||
let gid = |c| Some(GlyphId16::new(u16::try_from(charmap.map(c)?.to_u32()).ok()?));
|
||
let pair = |second| PairValueRecord::new(second, ValueRecord::new().with_x_advance(-60), ValueRecord::new().with_x_placement(-100));
|
||
let mut pairs = [(gid('A')?, gid('V')?), (gid('S')?, gid('S')?)];
|
||
pairs.sort_by_key(|p| p.0);
|
||
let coverage = CoverageFormat1::new(pairs.iter().map(|p| p.0).collect());
|
||
let pos = PairPos::format_1(coverage.into(), pairs.iter().map(|p| PairSet::new(vec![pair(p.1)])).collect());
|
||
let scripts = ScriptList::new(vec![ScriptRecord::new(Tag::new(b"DFLT"), Script::new(Some(LangSys::new(vec![0])), vec![]))]);
|
||
let features = FeatureList::new(vec![FeatureRecord::new(Tag::new(b"kern"), Feature::new(None, vec![0]))]);
|
||
let lookups = PositionLookupList::new(vec![PositionLookup::Pair(Lookup::new(Default::default(), vec![pos]))]);
|
||
let family = PAIR_KERNING_FAMILY;
|
||
let name = name_table(&[(1, family), (2, "Regular"), (4, "Pairtest Sans Regular"), (6, "PairtestSans-Regular")])?;
|
||
let mut builder = FontBuilder::new();
|
||
builder.add_table(&Gpos::new(scripts, features, lookups)).ok()?;
|
||
builder.add_raw(Tag::new(b"name"), name);
|
||
for r in font.table_directory.table_records() {
|
||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||
if tag != Tag::new(b"kern") && !builder.contains(tag) {
|
||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||
}
|
||
}
|
||
Some(builder.build())
|
||
}
|
||
|
||
/// The family of [`twin_font`]: two versions of one family and style.
|
||
pub const TWIN_FAMILY: &str = "Twintest Sans";
|
||
|
||
/// The version strings of [`twin_font`]'s older and newer face.
|
||
pub const TWIN_VERSIONS: [&str; 2] = ["Version 1.000", "Version 2.000"];
|
||
|
||
/// A face of family [`TWIN_FAMILY`], style Regular, in version [`TWIN_VERSIONS`]`[0]` (the bundled
|
||
/// Source Sans 3 Regular) or, `newer`, `[1]` (the bundled Inter Regular): one name, two sets of
|
||
/// glyphs, as a font library that keeps an old version beside a new one has. `None` when it can't
|
||
/// be built.
|
||
pub fn twin_font(newer: bool) -> Option<Vec<u8>> {
|
||
const OLDER: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||
const NEWER: &[u8] = include_bytes!("../../../assets/fonts/Inter-Regular.ttf");
|
||
let font = skrifa::FontRef::new(if newer { NEWER } else { OLDER }).ok()?;
|
||
let version = TWIN_VERSIONS[usize::from(newer)];
|
||
let name = name_table(&[(1, TWIN_FAMILY), (2, "Regular"), (4, "Twintest Sans Regular"), (5, version), (6, "TwintestSans-Regular")])?;
|
||
let mut builder = FontBuilder::new();
|
||
builder.add_raw(Tag::new(b"name"), name);
|
||
for r in font.table_directory.table_records() {
|
||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||
if !builder.contains(tag) {
|
||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||
}
|
||
}
|
||
Some(builder.build())
|
||
}
|
||
|
||
/// The family of [`numbered_font`]: styles numbered W3 and W6, and no Regular.
|
||
pub const NUMBERED_FAMILY: &str = "Numtest Mincho";
|
||
|
||
/// A face of family [`NUMBERED_FAMILY`] in `style` (`W3` or `W6`; the bundled Source Sans 3
|
||
/// Regular renamed), as families whose styles are numbered have, with no Regular among them.
|
||
/// `None` when it can't be built.
|
||
pub fn numbered_font(style: &str) -> Option<Vec<u8>> {
|
||
const FACE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||
let font = skrifa::FontRef::new(FACE).ok()?;
|
||
let full = format!("{NUMBERED_FAMILY} {style}");
|
||
let postscript = format!("NumtestMincho-{style}");
|
||
let name = name_table(&[(1, NUMBERED_FAMILY), (2, style), (4, &full), (5, "Version 1.000"), (6, &postscript)])?;
|
||
let mut builder = FontBuilder::new();
|
||
builder.add_raw(Tag::new(b"name"), name);
|
||
for r in font.table_directory.table_records() {
|
||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||
if !builder.contains(tag) {
|
||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||
}
|
||
}
|
||
Some(builder.build())
|
||
}
|
||
|
||
/// The character whose glyph [`colour_bitmap_font`] draws as a colour bitmap.
|
||
pub const COLOUR_CHAR: char = 'E';
|
||
|
||
/// The colour of [`colour_bitmap_font`]'s bitmap in its largest strike (RGBA); the `sbix` font's
|
||
/// smaller strike is [`COLOUR_SMALL`].
|
||
pub const COLOUR_LARGE: [u8; 4] = [255, 0, 0, 255];
|
||
|
||
/// The colour of the `sbix` font's smaller strike.
|
||
pub const COLOUR_SMALL: [u8; 4] = [0, 0, 255, 255];
|
||
|
||
/// The pixels per em of [`colour_bitmap_font`]'s largest strike: its square bitmap is one em wide.
|
||
pub const COLOUR_PPEM: u8 = 16;
|
||
|
||
/// Where [`colour_bitmap_font`]`(false)`'s (`CBDT`) bitmap sits, in pixels: its left edge right of
|
||
/// the glyph's origin, its top edge above the baseline.
|
||
pub const COLOUR_CBDT_BEARINGS: (i8, i8) = (2, 12);
|
||
|
||
/// The family of [`colour_bitmap_font`] (`sbix`, or `CBDT`).
|
||
pub fn colour_bitmap_family(sbix: bool) -> &'static str {
|
||
if sbix { "Sbixtest Emoji" } else { "Cbdttest Emoji" }
|
||
}
|
||
|
||
/// The bundled Source Sans 3 Regular as family [`colour_bitmap_family`], [`COLOUR_CHAR`] a solid
|
||
/// square colour bitmap one em wide: in `sbix` strikes of [`COLOUR_PPEM`] ([`COLOUR_LARGE`]) and
|
||
/// half that ([`COLOUR_SMALL`]), each with no origin offset, or (`sbix` false) in one `CBDT`
|
||
/// strike of [`COLOUR_PPEM`] ([`COLOUR_LARGE`], bearings [`COLOUR_CBDT_BEARINGS`]). Its outline
|
||
/// stays too. Tables written from the OpenType specification. `None` when it can't be built.
|
||
pub fn colour_bitmap_font(sbix: bool) -> Option<Vec<u8>> {
|
||
colour_bitmap_font_with(sbix, &solid_png(COLOUR_PPEM.into(), COLOUR_LARGE))
|
||
}
|
||
|
||
/// [`colour_bitmap_font`] with `png` as the bitmap of its largest strike (a malformed one, say).
|
||
pub fn colour_bitmap_font_with(sbix: bool, png: &[u8]) -> Option<Vec<u8>> {
|
||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||
let glyphs = font.maxp().ok()?.num_glyphs();
|
||
let gid = u16::try_from(font.charmap().map(COLOUR_CHAR)?.to_u32()).ok()?;
|
||
let family = colour_bitmap_family(sbix);
|
||
let ps = family.replace(' ', "");
|
||
let name = name_table(&[(1, family), (2, "Regular"), (4, &format!("{family} Regular")), (6, &format!("{ps}-Regular"))])?;
|
||
let mut builder = FontBuilder::new();
|
||
builder.add_raw(Tag::new(b"name"), name);
|
||
if sbix {
|
||
let small = solid_png((COLOUR_PPEM / 2).into(), COLOUR_SMALL);
|
||
builder.add_raw(Tag::new(b"sbix"), sbix_table(glyphs, gid, &[(COLOUR_PPEM / 2, &small), (COLOUR_PPEM, png)]));
|
||
} else {
|
||
let (cblc, cbdt) = cbdt_tables(gid, png);
|
||
builder.add_raw(Tag::new(b"CBLC"), cblc);
|
||
builder.add_raw(Tag::new(b"CBDT"), cbdt);
|
||
}
|
||
for r in font.table_directory.table_records() {
|
||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||
if !builder.contains(tag) {
|
||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||
}
|
||
}
|
||
Some(builder.build())
|
||
}
|
||
|
||
/// An `sbix` table of `strikes` (ppem, PNG), each holding a PNG for glyph `gid` alone.
|
||
fn sbix_table(glyphs: u16, gid: u16, strikes: &[(u8, &[u8])]) -> Vec<u8> {
|
||
let mut out = Vec::new();
|
||
out.extend(1u16.to_be_bytes()); // version
|
||
out.extend(1u16.to_be_bytes()); // flags
|
||
out.extend((strikes.len() as u32).to_be_bytes());
|
||
let header = 8 + 4 * strikes.len();
|
||
let mut data = Vec::new();
|
||
for (ppem, png) in strikes {
|
||
out.extend(((header + data.len()) as u32).to_be_bytes());
|
||
let offsets = 4 + 4 * (usize::from(glyphs) + 1);
|
||
let mut strike = Vec::new();
|
||
strike.extend(u16::from(*ppem).to_be_bytes());
|
||
strike.extend(72u16.to_be_bytes()); // ppi
|
||
// Glyph data offsets: empty but for `gid`'s (origin offsets 0, `png `, the PNG).
|
||
let record = 8 + png.len();
|
||
for g in 0..=glyphs {
|
||
let at = offsets + if g > gid { record } else { 0 };
|
||
strike.extend((at as u32).to_be_bytes());
|
||
}
|
||
strike.extend([0, 0, 0, 0]);
|
||
strike.extend(b"png ");
|
||
strike.extend(*png);
|
||
data.extend(strike);
|
||
}
|
||
out.extend(data);
|
||
out
|
||
}
|
||
|
||
/// `CBLC` and `CBDT` tables of one strike of [`COLOUR_PPEM`] holding `png` for glyph `gid`
|
||
/// (index subtable format 1, glyph data format 17).
|
||
fn cbdt_tables(gid: u16, png: &[u8]) -> (Vec<u8>, Vec<u8>) {
|
||
let ppem = COLOUR_PPEM;
|
||
let mut cbdt = Vec::new();
|
||
cbdt.extend(3u16.to_be_bytes());
|
||
cbdt.extend(0u16.to_be_bytes());
|
||
// Small glyph metrics: height, width, bearing x, bearing y, advance.
|
||
let (bx, by) = COLOUR_CBDT_BEARINGS;
|
||
cbdt.extend([ppem, ppem, bx as u8, by as u8, ppem]);
|
||
cbdt.extend((png.len() as u32).to_be_bytes());
|
||
cbdt.extend(png);
|
||
let mut cblc = Vec::new();
|
||
cblc.extend(3u16.to_be_bytes());
|
||
cblc.extend(0u16.to_be_bytes());
|
||
cblc.extend(1u32.to_be_bytes()); // numSizes
|
||
let array = 8 + 48;
|
||
let subtable = 8;
|
||
let index_size = subtable + 8 + 2 * 4;
|
||
cblc.extend((array as u32).to_be_bytes());
|
||
cblc.extend((index_size as u32).to_be_bytes());
|
||
cblc.extend(1u32.to_be_bytes()); // numberOfIndexSubTables
|
||
cblc.extend(0u32.to_be_bytes()); // colorRef
|
||
// Horizontal and vertical line metrics.
|
||
let line = [ppem as i8, 0, ppem as i8, 1, 0, 0, 0, 0, 0, 0, 0, 0].map(|v| v as u8);
|
||
cblc.extend(line);
|
||
cblc.extend(line);
|
||
cblc.extend(gid.to_be_bytes());
|
||
cblc.extend(gid.to_be_bytes());
|
||
cblc.extend([ppem, ppem, 32, 1]); // ppem x, ppem y, bit depth, flags (horizontal)
|
||
// The index subtable array, then the subtable: format 1, image format 17, its data at 4.
|
||
cblc.extend(gid.to_be_bytes());
|
||
cblc.extend(gid.to_be_bytes());
|
||
cblc.extend((subtable as u32).to_be_bytes());
|
||
cblc.extend(1u16.to_be_bytes());
|
||
cblc.extend(17u16.to_be_bytes());
|
||
cblc.extend(4u32.to_be_bytes());
|
||
cblc.extend(0u32.to_be_bytes());
|
||
cblc.extend(((cbdt.len() - 4) as u32).to_be_bytes());
|
||
(cblc, cbdt)
|
||
}
|
||
|
||
/// A `side`×`side` PNG of one colour (RGBA), written uncompressed (stored deflate blocks).
|
||
pub fn solid_png(side: u32, rgba: [u8; 4]) -> Vec<u8> {
|
||
fn crc32(bytes: &[u8]) -> u32 {
|
||
let mut c = !0u32;
|
||
for b in bytes {
|
||
c ^= u32::from(*b);
|
||
for _ in 0..8 {
|
||
c = if c & 1 != 0 { 0xedb8_8320 ^ (c >> 1) } else { c >> 1 };
|
||
}
|
||
}
|
||
!c
|
||
}
|
||
fn chunk(out: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
|
||
out.extend((data.len() as u32).to_be_bytes());
|
||
let mut body = kind.to_vec();
|
||
body.extend(data);
|
||
out.extend(&body);
|
||
out.extend(crc32(&body).to_be_bytes());
|
||
}
|
||
// Each row: filter type 0 (none), then its pixels.
|
||
let row: Vec<u8> = std::iter::once(0).chain(rgba.repeat(side as usize)).collect();
|
||
let raw = row.repeat(side as usize);
|
||
let (mut a, mut b) = (1u32, 0u32);
|
||
for v in &raw {
|
||
a = (a + u32::from(*v)) % 65521;
|
||
b = (b + a) % 65521;
|
||
}
|
||
let mut zlib = vec![0x78, 0x01];
|
||
let blocks: Vec<&[u8]> = raw.chunks(0xffff).collect();
|
||
for (i, block) in blocks.iter().enumerate() {
|
||
zlib.push(u8::from(i + 1 == blocks.len()));
|
||
let n = block.len() as u16;
|
||
zlib.extend(n.to_le_bytes());
|
||
zlib.extend((!n).to_le_bytes());
|
||
zlib.extend(*block);
|
||
}
|
||
zlib.extend(((b << 16) | a).to_be_bytes());
|
||
let mut out = b"\x89PNG\r\n\x1a\n".to_vec();
|
||
let mut ihdr = Vec::new();
|
||
ihdr.extend(side.to_be_bytes());
|
||
ihdr.extend(side.to_be_bytes());
|
||
ihdr.extend([8, 6, 0, 0, 0]); // 8 bits, RGBA
|
||
chunk(&mut out, b"IHDR", &ihdr);
|
||
chunk(&mut out, b"IDAT", &zlib);
|
||
chunk(&mut out, b"IEND", &[]);
|
||
out
|
||
}
|
||
|
||
/// The palette of [`colr_font`] (palette 0, RGBA): its indices 0, 1 and 2.
|
||
pub const COLR_RED: [u8; 4] = [255, 0, 0, 255];
|
||
pub const COLR_BLUE: [u8; 4] = [0, 0, 255, 255];
|
||
pub const COLR_GREEN: [u8; 4] = [0, 255, 0, 255];
|
||
|
||
/// The character whose glyph [`colr_font`]`(true)` paints with a sweep gradient.
|
||
pub const COLR_SWEEP_CHAR: char = 'S';
|
||
|
||
/// The family of [`colr_font`] (COLRv1, or v0).
|
||
pub fn colr_family(v1: bool) -> &'static str {
|
||
if v1 { "Colrtest Paints" } else { "Colrtest Layers" }
|
||
}
|
||
|
||
/// The glyph ids of `chars` in the bundled Source Sans 3.
|
||
fn source_sans_gids<const N: usize>(chars: [char; N]) -> Option<[u16; N]> {
|
||
let font = skrifa::FontRef::new(SOURCE_SANS).ok()?;
|
||
let mut gids = [0; N];
|
||
for (gid, ch) in gids.iter_mut().zip(chars) {
|
||
*gid = u16::try_from(font.charmap().map(ch)?.to_u32()).ok()?;
|
||
}
|
||
Some(gids)
|
||
}
|
||
|
||
const SOURCE_SANS: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||
|
||
/// The bundled Source Sans 3 Regular as family [`colr_family`], with `COLR` and `CPAL` tables
|
||
/// written from the OpenType specification, palette 0 [`COLR_RED`], [`COLR_BLUE`], [`COLR_GREEN`].
|
||
/// v0: [`COLOUR_CHAR`] is the outline of `O` in red, then that of `I` in the foreground colour
|
||
/// (palette index 0xFFFF). v1: [`COLOUR_CHAR`] is, in a clip box, `O` filled with a linear
|
||
/// gradient red → blue, `I` scaled by a half about the origin with a radial gradient blue → green,
|
||
/// and `-` in green composited with Multiply over `-` in red; [`COLR_SWEEP_CHAR`] is `O` with a
|
||
/// sweep gradient red → blue. Their outlines stay. `None` when it can't be built.
|
||
pub fn colr_font(v1: bool) -> Option<Vec<u8>> {
|
||
use write_fonts::tables::colr::{
|
||
BaseGlyph, BaseGlyphList, BaseGlyphPaint, Clip, ClipBox, ClipList, ColorLine, ColorStop, Colr, CompositeMode, Extend, Layer, LayerList, Paint,
|
||
};
|
||
use write_fonts::types::{FWord, GlyphId16, UfWord};
|
||
let [base, sweep, o, i, dash] = source_sans_gids([COLOUR_CHAR, COLR_SWEEP_CHAR, 'O', 'I', '-'])?;
|
||
let (base, sweep, o, i, dash) = (GlyphId16::new(base), GlyphId16::new(sweep), GlyphId16::new(o), GlyphId16::new(i), GlyphId16::new(dash));
|
||
let colr = if v1 {
|
||
let one = F2Dot14::from_f32(1.0);
|
||
let line = |from: u16, to: u16| {
|
||
ColorLine::new(Extend::Pad, 2, vec![ColorStop::new(F2Dot14::from_f32(0.0), from, one), ColorStop::new(one, to, one)])
|
||
};
|
||
let w = FWord::new;
|
||
let layers = vec![
|
||
Paint::glyph(Paint::linear_gradient(line(0, 1), w(0), w(0), w(500), w(0), w(0), w(500)), o),
|
||
Paint::scale_uniform(
|
||
Paint::glyph(Paint::radial_gradient(line(1, 2), w(250), w(300), UfWord::new(0), w(250), w(300), UfWord::new(300)), i),
|
||
F2Dot14::from_f32(0.5),
|
||
),
|
||
Paint::composite(Paint::glyph(Paint::solid(2, one), dash), CompositeMode::Multiply, Paint::glyph(Paint::solid(0, one), dash)),
|
||
];
|
||
let mut colr = Colr::new(0, None, None, 0);
|
||
colr.layer_list = Some(LayerList::new(layers.len() as u32, layers)).into();
|
||
let mut glyphs = vec![
|
||
BaseGlyphPaint::new(base, Paint::colr_layers(3, 0)),
|
||
BaseGlyphPaint::new(sweep, Paint::glyph(Paint::sweep_gradient(line(0, 1), w(250), w(300), F2Dot14::from_f32(0.0), one), o)),
|
||
];
|
||
glyphs.sort_by_key(|g| g.glyph_id);
|
||
colr.base_glyph_list = Some(BaseGlyphList::new(glyphs.len() as u32, glyphs)).into();
|
||
let clip = ClipBox::format_1(w(-50), w(-250), w(750), w(800));
|
||
colr.clip_list = Some(ClipList::new(1, 1, vec![Clip::new(base, base, clip)])).into();
|
||
colr
|
||
} else {
|
||
Colr::new(1, Some(vec![BaseGlyph::new(base, 0, 2)]), Some(vec![Layer::new(o, 0), Layer::new(i, 0xFFFF)]), 2)
|
||
};
|
||
colr_font_with(colr_family(v1), &colr)
|
||
}
|
||
|
||
/// The bundled Source Sans 3 Regular as `family` with `colr` and [`colr_font`]'s palette.
|
||
pub fn colr_font_with(family: &str, colr: &write_fonts::tables::colr::Colr) -> Option<Vec<u8>> {
|
||
with_colr(SOURCE_SANS, family_names(family, &[])?, colr)
|
||
}
|
||
|
||
/// The family of [`colr_variable_font`].
|
||
pub const COLR_VARIABLE_FAMILY: &str = "Colrtest Variable";
|
||
|
||
/// [`variable_font`] as family [`COLR_VARIABLE_FAMILY`], with a `COLR` (v0) table in which
|
||
/// [`COLOUR_CHAR`] is the outline of `l` (one of [`VARIABLE_CHARS`]) in red, so its layer widens
|
||
/// with the weight. `None` when it can't be built.
|
||
pub fn colr_variable_font() -> Option<Vec<u8>> {
|
||
use write_fonts::tables::colr::{BaseGlyph, Colr, Layer};
|
||
use write_fonts::types::GlyphId16;
|
||
let [base, l] = source_sans_gids([COLOUR_CHAR, 'l'])?;
|
||
let colr = Colr::new(1, Some(vec![BaseGlyph::new(GlyphId16::new(base), 0, 1)]), Some(vec![Layer::new(GlyphId16::new(l), 0)]), 1);
|
||
let axis_names = [(256, "Weight"), (257, "Normal"), (258, "SemiBold"), (259, "Bold")];
|
||
with_colr(&variable_font()?, family_names(COLR_VARIABLE_FAMILY, &axis_names)?, &colr)
|
||
}
|
||
|
||
/// A `name` table for Regular of `family`, with `more` records.
|
||
fn family_names(family: &str, more: &[(u16, &str)]) -> Option<Vec<u8>> {
|
||
let ps = family.replace(' ', "");
|
||
let (full, postscript) = (format!("{family} Regular"), format!("{ps}-Regular"));
|
||
let mut records = vec![(1, family), (2, "Regular"), (4, full.as_str()), (6, postscript.as_str())];
|
||
records.extend_from_slice(more);
|
||
name_table(&records)
|
||
}
|
||
|
||
/// Font `base` with the `name` table `name`, `colr` and [`colr_font`]'s palette.
|
||
fn with_colr(base: &[u8], name: Vec<u8>, colr: &write_fonts::tables::colr::Colr) -> Option<Vec<u8>> {
|
||
use write_fonts::tables::cpal::{ColorRecord, Cpal};
|
||
let font = skrifa::FontRef::new(base).ok()?;
|
||
let records = [COLR_RED, COLR_BLUE, COLR_GREEN].map(|[r, g, b, a]| ColorRecord::new(b, g, r, a)).to_vec();
|
||
let cpal = Cpal::new(3, 1, 3, Some(records), vec![0]);
|
||
let mut builder = FontBuilder::new();
|
||
builder.add_raw(Tag::new(b"name"), name);
|
||
builder.add_table(colr).ok()?;
|
||
builder.add_table(&cpal).ok()?;
|
||
for r in font.table_directory.table_records() {
|
||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||
if !builder.contains(tag) {
|
||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||
}
|
||
}
|
||
Some(builder.build())
|
||
}
|
||
|
||
/// The character whose glyph [`svg_font`]'s first document draws.
|
||
pub const SVG_CHAR: char = 'E';
|
||
|
||
/// The glyph id of `ch` in the bundled Source Sans 3 (and the fonts made from it).
|
||
pub fn source_sans_gid(ch: char) -> Option<u16> {
|
||
source_sans_gids([ch]).map(|[g]| g)
|
||
}
|
||
|
||
/// The bundled Source Sans 3 Regular as `family` with an `SVG ` table written from the OpenType
|
||
/// specification: `documents` are (first glyph, last glyph, SVG document), in glyph order. Its
|
||
/// outlines stay. `None` when it can't be built.
|
||
pub fn svg_font(family: &str, documents: &[(u16, u16, &str)]) -> Option<Vec<u8>> {
|
||
let font = skrifa::FontRef::new(SOURCE_SANS).ok()?;
|
||
let ps = family.replace(' ', "");
|
||
let name = name_table(&[(1, family), (2, "Regular"), (4, &format!("{family} Regular")), (6, &format!("{ps}-Regular"))])?;
|
||
// The header (version, the document list's offset, reserved), then the document list: its
|
||
// records (first and last glyph, the document's offset from the list and length), then the
|
||
// documents.
|
||
let records = documents.len();
|
||
let mut list = (u16::try_from(records).ok()?).to_be_bytes().to_vec();
|
||
let mut offset = 2 + 12 * records;
|
||
for (first, last, doc) in documents {
|
||
list.extend(first.to_be_bytes());
|
||
list.extend(last.to_be_bytes());
|
||
list.extend((u32::try_from(offset).ok()?).to_be_bytes());
|
||
list.extend((u32::try_from(doc.len()).ok()?).to_be_bytes());
|
||
offset += doc.len();
|
||
}
|
||
for (_, _, doc) in documents {
|
||
list.extend(doc.as_bytes());
|
||
}
|
||
let mut svg = vec![0, 0, 0, 0, 0, 10, 0, 0, 0, 0];
|
||
svg.extend(list);
|
||
let mut builder = FontBuilder::new();
|
||
builder.add_raw(Tag::new(b"name"), name);
|
||
builder.add_raw(Tag::new(b"SVG "), svg);
|
||
for r in font.table_directory.table_records() {
|
||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||
if !builder.contains(tag) {
|
||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||
}
|
||
}
|
||
Some(builder.build())
|
||
}
|