mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 11:17:01 -06:00
Upright glyphs in vertical type advanced one em (or their horizontal advance, if wider) and turned about a fixed point 0.38 em above the baseline. Fonts that carry vertical metrics now place them as they ask: - `FontFace::vertical_glyph`: a glyph's advance down the column (`vmtx`) and the height of its vertical origin (`VORG`, else the glyph's top plus its top side bearing, as in TrueType fonts); `None` without `vhea`/`vmtx` or with values outside a few ems. `FontFace::ideographic_centre`: the em box centre from an ideograph's cell, else 0.38. - The layout advances an upright glyph by its vertical advance and hangs it from its vertical origin; tate-chu-yoko blocks and point type's anchor use the font's em box centre. Fonts without vertical metrics are laid out exactly as before. - Hiragino's tables give the values the layout assumed (one em, origin 0.88 em), so Japanese vertical type in it renders pixel for pixel the same; this is the base for proportional vertical metrics (`vpal`) and for fonts whose em box sits elsewhere. - `test_fonts::vertical_font`: Source Sans 3 with `vhea`/`vmtx` (and optionally `VORG`) built in code, one glyph 1.4 em tall hanging from 1.1 em; tested with and without `VORG`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
177 lines
8.1 KiB
Rust
177 lines
8.1 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())
|
||
}
|
||
|
||
/// 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())
|
||
}
|