CJK 1.5: fonts read vertical metrics

FontFace::v_advance, v_origin and em_box read vmtx/vhea, VORG (else the
glyph top plus its top side bearing) and the ideographic em box (BASE
ideo/idtp, else OS/2 typo metrics, else ascender/descender, centred on one
em); fonts without them fall back to the em box. shape_vertical shapes
top to bottom with those metrics (vert, and vkrn/vpal when asked).
This commit is contained in:
aiongg 2026-10-06 14:37:44 -04:00
parent e0fb78a230
commit 1602b37ec9
5 changed files with 537 additions and 22 deletions

View File

@ -113,6 +113,8 @@ pub struct FontFace {
pub instance: Option<harfrust::ShaperInstance>,
/// Basic Multilingual Plane coverage bitset, built on first use.
bmp: std::sync::OnceLock<Box<[u64]>>,
/// The ideographic em box, read on first use.
em: std::sync::OnceLock<(f64, f64)>,
}
/// A cheap `Copy` handle to a face. Faces are never unloaded, so the handle lives for the rest of
@ -241,6 +243,22 @@ impl FontFace {
.map(|a| a as f64)
.unwrap_or(self.upem * 0.5)
}
/// Vertical advance (down the line) of glyph `gid` in font units: `vmtx`, else one em.
pub fn v_advance(&self, gid: u32) -> f64 {
crate::vertical::VMetrics::new(self).advance(GlyphId::new(gid))
}
/// Height of glyph `gid`'s vertical origin above its baseline in font units: `VORG`, else its
/// top plus its `vmtx` top side bearing, else the em box top. Across, the origin is at half the
/// horizontal advance. Upright glyphs in vertical text hang from it on the line's centre.
pub fn v_origin(&self, gid: u32) -> f64 {
crate::vertical::VMetrics::new(self).origin(GlyphId::new(gid))
}
/// The ideographic em box (top, bottom) in font units, y up (Japanese fonts: typically 880,
/// -120): `BASE` `idtp`/`ideo`, else the OS/2 typo ascender/descender, else the ascender and
/// descender, the last two centred on one em.
pub fn em_box(&self) -> (f64, f64) {
*self.em.get_or_init(|| crate::vertical::em_box(self))
}
/// Glyph id for `c` (0 = .notdef).
pub fn glyph_for(&self, c: char) -> u32 {
self.skrifa().and_then(|f| f.charmap().map(c)).map(|g| g.to_u32()).unwrap_or(0)
@ -625,6 +643,7 @@ fn make_face(bytes: FontBytes, source: FontSource, index: u32, family: String, s
bytes,
index,
bmp: std::sync::OnceLock::new(),
em: std::sync::OnceLock::new(),
})
}
@ -1176,3 +1195,7 @@ mod tests_fallback;
#[cfg(not(target_arch = "wasm32"))]
#[path = "tests_docfonts.rs"]
mod tests_docfonts;
#[cfg(test)]
#[path = "tests_vertical.rs"]
mod tests_vertical;

View File

@ -10,6 +10,7 @@
mod fontdb;
#[cfg(any(test, feature = "testing"))]
pub mod testing;
mod vertical;
pub use fontdb::{
DOCUMENT_FONTS_FOLDER, DocumentFonts, FALLBACK_FAMILY, FaceRef, FontDb, FontFace, FontSource, MAX_DOCUMENT_FONT_BYTES, MAX_DOCUMENT_FONT_FILES,
@ -57,13 +58,19 @@ pub fn japanese_ui_fonts(bold: bool) -> Vec<&'static CraftFont> {
/// InDesign's default text font is a serif; ours is Source Serif 4.
pub const DEFAULT_FAMILY: &str = "Source Serif 4";
/// One shaped glyph, in font units.
/// One shaped glyph, in font units (y up).
///
/// Horizontal runs ([`shape`]) advance by `x_advance` (`y_advance` is 0) and the offsets move the
/// glyph from its pen position. Vertical runs ([`shape_vertical`]) advance down the line by
/// `-y_advance` (`x_advance` is 0) and the offsets move the glyph from its vertical origin
/// ([`FontFace::v_origin`]) on the line's centre, as `vpal` or `vkrn` place it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ShapedGlyph {
pub gid: u32,
/// Byte offset (in the shaped string) of the cluster this glyph belongs to.
pub cluster: usize,
pub x_advance: i32,
pub y_advance: i32,
pub x_offset: i32,
pub y_offset: i32,
pub safe_tatweel_before: bool,
@ -205,6 +212,7 @@ fn shape_dir(
gid: info.glyph_id,
cluster: info.cluster as usize,
x_advance: pos.x_advance,
y_advance: 0,
x_offset: pos.x_offset,
y_offset: pos.y_offset,
safe_tatweel_before: info.safe_to_insert_tatweel(),
@ -224,6 +232,7 @@ fn shape_dir(
gid: g.to_u32(),
cluster: i,
x_advance: adv.round() as i32,
y_advance: 0,
x_offset: 0,
y_offset: 0,
safe_tatweel_before: false,
@ -234,6 +243,70 @@ fn shape_dir(
out
}
/// Shape upright text top to bottom (`text`, `features`, `map` and `context` as in
/// [`shape_with_context`]): the shaper applies `vert` (vertical forms), and `vkrn` and `vpal` when
/// `features` turn them on. Advances and origins are the face's vertical metrics
/// ([`FontFace::v_advance`], [`FontFace::v_origin`]).
pub fn shape_vertical(face: &FontFace, text: &str, features: &[Feature], map: impl Fn(char) -> char, context: ShapeContext<'_>) -> Vec<ShapedGlyph> {
let mut funcs = vertical::VerticalFuncs { face, metrics: vertical::VMetrics::new(face) };
let mut out = Vec::with_capacity(text.len());
let mut origins = Vec::with_capacity(text.len());
let shaped = face.hb().map(|hb| {
let shaper = face.shaper.shaper(&hb).instance(face.instance.as_ref()).build();
let mut buf = UnicodeBuffer::new();
for (i, c) in text.char_indices() {
buf.add(map(c), i as u32);
}
buf.set_pre_context(context.before);
buf.set_post_context(context.after);
if let Some(language) = context.language.and_then(|t| t.parse::<Language>().ok()) {
buf.set_language(language);
}
buf.guess_segment_properties();
buf.set_direction(Direction::TopToBottom);
let gb = shaper.shape(buf, ShapeOptions::new().features(features).font_funcs(Some(&mut funcs)));
for (info, pos) in gb.glyph_infos().iter().zip(gb.glyph_positions()) {
out.push(ShapedGlyph {
gid: info.glyph_id,
cluster: info.cluster as usize,
x_advance: pos.x_advance,
y_advance: pos.y_advance,
x_offset: pos.x_offset,
y_offset: pos.y_offset,
safe_tatweel_before: false,
rtl: false,
});
}
});
if shaped.is_none()
&& let Some(f) = face.skrifa()
{
let cmap = f.charmap();
for (i, c) in text.char_indices() {
let gid = cmap.map(map(c)).unwrap_or_default().to_u32();
let (x, y) = funcs.origin(gid);
let adv = -(face.v_advance(gid).round() as i32);
out.push(ShapedGlyph {
gid,
cluster: i,
x_advance: 0,
y_advance: adv,
x_offset: -x,
y_offset: -y,
safe_tatweel_before: false,
rtl: false,
});
}
}
// The shaper hangs each glyph from its vertical origin; keep only what GPOS moved it by.
origins.extend(out.iter().map(|g| funcs.origin(g.gid)));
for (g, (x, y)) in out.iter_mut().zip(origins) {
g.x_offset = g.x_offset.saturating_add(x);
g.y_offset = g.y_offset.saturating_add(y);
}
out
}
/// Glyph id of the first of `chars` the face has (0 = .notdef).
pub fn first_glyph(face: &FontFace, chars: &[char]) -> u32 {
chars.iter().map(|c| face.glyph_for(*c)).find(|g| *g != 0).unwrap_or(0)

View File

@ -7,20 +7,21 @@ use skrifa::MetadataProvider;
/// isn't installed (a system CJK font on a machine without it). `None` only if the bundled font
/// can't be read.
pub fn font_with(family: &str, chars: &[char]) -> Option<Vec<u8>> {
let base = *crate::bundled().first()?;
let gid = skrifa::FontRef::new(base).ok()?.charmap().map('X')?.to_u32();
let be16 = |b: &[u8], at: usize| b.get(at..at + 2).and_then(|s| s.try_into().ok()).map(u16::from_be_bytes);
let be32 = |b: &[u8], at: usize| b.get(at..at + 4).and_then(|s| s.try_into().ok()).map(u32::from_be_bytes);
let glyphs: Vec<(char, char)> = chars.iter().map(|c| (*c, 'X')).collect();
font_mapping(family, &glyphs)
}
let mut tables: Vec<([u8; 4], Vec<u8>)> = Vec::new();
for r in 0..usize::from(be16(base, 4)?) {
let rec = 12 + r * 16;
let tag: [u8; 4] = base.get(rec..rec + 4)?.try_into().ok()?;
let (offset, len) = (be32(base, rec + 8)? as usize, be32(base, rec + 12)? as usize);
if &tag != b"name" && &tag != b"cmap" {
tables.push((tag, base.get(offset..offset.checked_add(len)?)?.to_vec()));
}
}
/// [`font_with`], mapping each character of `glyphs` to Source Sans 3's glyph for the character
/// paired with it (`('一', 'X')` draws 一 as X), so characters can have glyphs (and metrics) of
/// their own. `None` if the bundled font can't be read or lacks a glyph asked for.
pub fn font_mapping(family: &str, glyphs: &[(char, char)]) -> Option<Vec<u8>> {
let base = *crate::bundled().first()?;
let base_cmap = skrifa::FontRef::new(base).ok()?.charmap();
let mut map: Vec<(u32, u32)> = glyphs.iter().map(|(c, g)| Some((*c as u32, base_cmap.map(*g)?.to_u32()))).collect::<Option<_>>()?;
map.sort_unstable();
map.dedup_by_key(|m| m.0);
let mut tables = tables_of(base)?;
tables.retain(|(tag, _)| tag != b"name" && tag != b"cmap");
// `name`: format 0, family (1) and subfamily (2), Windows Unicode English.
let strings: Vec<Vec<u8>> = [family, "Regular"].iter().map(|s| s.encode_utf16().flat_map(u16::to_be_bytes).collect()).collect();
@ -39,10 +40,7 @@ pub fn font_with(family: &str, chars: &[char]) -> Option<Vec<u8>> {
tables.push((*b"name", name));
// `cmap`: one format 12 subtable (Windows, full Unicode), one group per character.
let mut cps: Vec<u32> = chars.iter().map(|c| *c as u32).collect();
cps.sort_unstable();
cps.dedup();
let sub_len = u32::try_from(16 + 12 * cps.len()).ok()?;
let sub_len = u32::try_from(16 + 12 * map.len()).ok()?;
let mut cmap = Vec::new();
for v in [0u16, 1, 3, 10] {
cmap.extend_from_slice(&v.to_be_bytes());
@ -50,21 +48,39 @@ pub fn font_with(family: &str, chars: &[char]) -> Option<Vec<u8>> {
cmap.extend_from_slice(&12u32.to_be_bytes());
cmap.extend_from_slice(&12u16.to_be_bytes());
cmap.extend_from_slice(&0u16.to_be_bytes());
for v in [sub_len, 0, u32::try_from(cps.len()).ok()?] {
for v in [sub_len, 0, u32::try_from(map.len()).ok()?] {
cmap.extend_from_slice(&v.to_be_bytes());
}
for cp in cps {
for (cp, gid) in map {
for v in [cp, cp, gid] {
cmap.extend_from_slice(&v.to_be_bytes());
}
}
tables.push((*b"cmap", cmap));
assemble(base, tables)
}
// The file: table directory (sorted by tag), then the tables, each 4-byte aligned.
/// Every table of the font file `font`: (tag, bytes).
fn tables_of(font: &[u8]) -> Option<Vec<([u8; 4], Vec<u8>)>> {
let be16 = |b: &[u8], at: usize| b.get(at..at + 2).and_then(|s| s.try_into().ok()).map(u16::from_be_bytes);
let be32 = |b: &[u8], at: usize| b.get(at..at + 4).and_then(|s| s.try_into().ok()).map(u32::from_be_bytes);
let mut tables = Vec::new();
for r in 0..usize::from(be16(font, 4)?) {
let rec = 12 + r * 16;
let tag: [u8; 4] = font.get(rec..rec + 4)?.try_into().ok()?;
let (offset, len) = (be32(font, rec + 8)? as usize, be32(font, rec + 12)? as usize);
tables.push((tag, font.get(offset..offset.checked_add(len)?)?.to_vec()));
}
Some(tables)
}
/// A font file of `tables`, with the sfnt version of `font`: the table directory (sorted by tag),
/// then the tables, each 4-byte aligned.
fn assemble(font: &[u8], mut tables: Vec<([u8; 4], Vec<u8>)>) -> Option<Vec<u8>> {
tables.sort_by_key(|t| t.0);
let n = u16::try_from(tables.len()).ok()?;
let pow = 1u16 << (15 - n.leading_zeros().min(15));
let mut out = base.get(0..4)?.to_vec();
let mut out = font.get(0..4)?.to_vec();
for v in [n, pow * 16, pow.trailing_zeros() as u16, n * 16 - pow * 16] {
out.extend_from_slice(&v.to_be_bytes());
}
@ -83,6 +99,62 @@ pub fn font_with(family: &str, chars: &[char]) -> Option<Vec<u8>> {
Some(out)
}
/// `font` with `tables` added, each replacing the table of its tag. `None` if `font` can't be read.
fn with_tables(font: &[u8], tables: Vec<([u8; 4], Vec<u8>)>) -> Option<Vec<u8>> {
let mut all = tables_of(font)?;
all.retain(|(tag, _)| !tables.iter().any(|(t, _)| t == tag));
all.extend(tables);
assemble(font, all)
}
/// `font` with vertical metrics: a `vhea` table and a `vmtx` table that give every glyph `default`
/// (advance down the line, top side bearing), except the glyphs `overrides` lists (glyph id,
/// advance, top side bearing). Every glyph has a long metric. `None` if `font` can't be read.
pub fn with_vmtx(font: &[u8], default: (u16, i16), overrides: &[(u16, u16, i16)]) -> Option<Vec<u8>> {
use skrifa::raw::TableProvider;
let glyphs = skrifa::FontRef::new(font).ok()?.maxp().ok()?.num_glyphs();
let mut vmtx = Vec::with_capacity(usize::from(glyphs) * 4);
let mut max_advance = 0;
for gid in 0..glyphs {
let (advance, tsb) = overrides.iter().find(|o| o.0 == gid).map_or(default, |o| (o.1, o.2));
max_advance = max_advance.max(advance);
vmtx.extend_from_slice(&advance.to_be_bytes());
vmtx.extend_from_slice(&tsb.to_be_bytes());
}
// `vhea` 1.1: typo ascender, descender and line gap, advanceHeightMax, minTop/BottomSideBearing,
// yMaxExtent, caret slope rise and run, caret offset, four reserved, metricDataFormat and
// numOfLongVerMetrics (at byte 34).
let mut vhea = 0x0001_1000_u32.to_be_bytes().to_vec();
for v in [500i16, -500, 0] {
vhea.extend_from_slice(&v.to_be_bytes());
}
vhea.extend_from_slice(&max_advance.to_be_bytes());
for v in [0i16, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0] {
vhea.extend_from_slice(&v.to_be_bytes());
}
vhea.extend_from_slice(&glyphs.to_be_bytes());
with_tables(font, vec![(*b"vhea", vhea), (*b"vmtx", vmtx)])
}
/// `font` with a `VORG` table: every glyph's vertical origin is `default` above its baseline,
/// except the glyphs `overrides` lists (glyph id, origin). `None` if `font` can't be read.
pub fn with_vorg(font: &[u8], default: i16, overrides: &[(u16, i16)]) -> Option<Vec<u8>> {
let mut entries = overrides.to_vec();
entries.sort_unstable_by_key(|e| e.0);
entries.dedup_by_key(|e| e.0);
let mut vorg = Vec::new();
for v in [1u16, 0] {
vorg.extend_from_slice(&v.to_be_bytes());
}
vorg.extend_from_slice(&default.to_be_bytes());
vorg.extend_from_slice(&u16::try_from(entries.len()).ok()?.to_be_bytes());
for (gid, y) in entries {
vorg.extend_from_slice(&gid.to_be_bytes());
vorg.extend_from_slice(&y.to_be_bytes());
}
with_tables(font, vec![(*b"VORG", vorg)])
}
/// `font` with its OS/2 `fsType` (embedding licence bits) set to `fs_type`. `None` if the font has
/// no OS/2 table.
pub fn with_fs_type(mut font: Vec<u8>, fs_type: u16) -> Option<Vec<u8>> {
@ -92,3 +164,31 @@ pub fn with_fs_type(mut font: Vec<u8>, fs_type: u16) -> Option<Vec<u8>> {
font.get_mut(offset + 8..offset + 10)?.copy_from_slice(&fs_type.to_be_bytes());
Some(font)
}
/// Offset and length of table `tag` in `font`'s table directory, and where its record is.
fn table(font: &[u8], tag: &[u8; 4]) -> Option<(usize, usize, usize)> {
let tables = usize::from(u16::from_be_bytes(font.get(4..6)?.try_into().ok()?));
let rec = (0..tables).map(|r| 12 + r * 16).find(|rec| font.get(*rec..rec + 4) == Some(tag))?;
let offset = u32::from_be_bytes(font.get(rec + 8..rec + 12)?.try_into().ok()?) as usize;
let len = u32::from_be_bytes(font.get(rec + 12..rec + 16)?.try_into().ok()?) as usize;
Some((rec, offset, len))
}
/// `font` with table `tag` cut to `len` bytes (its directory length; the bytes stay). `None` if
/// the font has no such table.
pub fn with_table_len(mut font: Vec<u8>, tag: &[u8; 4], len: u32) -> Option<Vec<u8>> {
let (rec, _, _) = table(&font, tag)?;
font.get_mut(rec + 12..rec + 16)?.copy_from_slice(&len.to_be_bytes());
Some(font)
}
/// `font` with the big-endian `u16` at byte `at` of table `tag` set to `value`. `None` if the font
/// has no such table or it is too short.
pub fn with_table_u16(mut font: Vec<u8>, tag: &[u8; 4], at: usize, value: u16) -> Option<Vec<u8>> {
let (_, offset, len) = table(&font, tag)?;
if at + 2 > len {
return None;
}
font.get_mut(offset + at..offset + at + 2)?.copy_from_slice(&value.to_be_bytes());
Some(font)
}

View File

@ -0,0 +1,192 @@
//! Vertical metrics (`vmtx`, `vhea`, `VORG`), the ideographic em box and top-to-bottom shaping.
//!
//! The synthetic font (Source Sans 3 with `vhea`/`vmtx`, and `VORG` where asked) runs everywhere;
//! the checks against Shippori Mincho's own tables run when built with craft-fonts.
use skrifa::raw::TableProvider;
use super::*;
use crate::testing::{font_mapping, font_with, with_table_len, with_table_u16, with_vmtx, with_vorg};
use crate::{ShapeContext, feature, shape_vertical, shape_with_context};
/// Text in Japanese.
fn japanese() -> ShapeContext<'static> {
ShapeContext { language: Some("ja"), ..Default::default() }
}
/// The synthetic font's characters and the Source Sans 3 glyphs that draw them.
const GLYPHS: &[(char, char)] = &[('一', 'X'), ('ヸ', 'O'), ('、', ',')];
/// A font with vertical metrics: every glyph one em down the line with a top side bearing of 120,
/// except ヸ (drawn as O): 1.024 em, top side bearing 100.
fn vertical_font() -> Vec<u8> {
let base = font_mapping("Vertical", GLYPHS).unwrap();
let wi = gid_in(&base, 'ヸ');
with_vmtx(&base, (1000, 120), &[(wi, 1024, 100)]).unwrap()
}
fn gid_in(font: &[u8], c: char) -> u16 {
let gid = skrifa::FontRef::new(font).unwrap().charmap().map(c).unwrap().to_u32();
u16::try_from(gid).unwrap()
}
/// Shippori Mincho from craft-fonts, or `None` (with a note) when built without it.
fn shippori() -> Option<Arc<FontFace>> {
if !crate::japanese_fonts().any(|f| f.family == "Shippori Mincho") {
eprintln!("skipped: built without craft-fonts (set CRAFT_FONTS_DIR to a checkout)");
return None;
}
let face = FontDb::global().face("Shippori Mincho", "Regular");
assert_eq!(face.family, "Shippori Mincho");
Some(face)
}
fn face_of(bytes: Vec<u8>) -> FontFace {
make_face(FontBytes::Owned(Arc::new(bytes)), FontSource::Memory, 0, "Test".into(), "Regular".into(), Vec::new()).unwrap()
}
#[test]
fn reads_vmtx() {
let face = face_of(vertical_font());
let font = face.skrifa().unwrap();
let gm = font.glyph_metrics(Size::unscaled(), LocationRef::default());
for (c, advance, tsb) in [('一', 1000.0, 120.0), ('、', 1000.0, 120.0), ('ヸ', 1024.0, 100.0)] {
let gid = face.glyph_for(c);
assert_ne!(gid, 0, "{c}");
assert_eq!(face.v_advance(gid), advance, "{c}");
// No VORG (TrueType outlines): the origin is the glyph's top plus its top side bearing.
let top = f64::from(gm.bounds(GlyphId::new(gid)).unwrap().y_max);
assert_eq!(face.v_origin(gid), top + tsb, "{c}");
}
// The em box doesn't come from vmtx: Source Sans 3's BASE table.
assert_eq!(face.em_box(), (830.0, -170.0));
// Glyphs past the long metrics advance as the last of them.
let face = face_of(with_table_u16(vertical_font(), b"vhea", 34, 1).unwrap());
assert_eq!(face.v_advance(face.glyph_for('ヸ')), 1000.0);
}
#[test]
fn vorg_gives_the_vertical_origin() {
let wi = gid_in(&vertical_font(), 'ヸ');
let face = face_of(with_vorg(&vertical_font(), 880, &[(wi, 900)]).unwrap());
assert_eq!(face.v_origin(face.glyph_for('一')), 880.0);
assert_eq!(face.v_origin(u32::from(wi)), 900.0);
assert_eq!(face.v_advance(u32::from(wi)), 1024.0, "advances still come from vmtx");
// VORG without vmtx: origins from VORG, one em down the line.
let base = font_mapping("Vorg Only", GLYPHS).unwrap();
let face = face_of(with_vorg(&base, 870, &[]).unwrap());
let ichi = face.glyph_for('一');
assert_eq!((face.v_advance(ichi), face.v_origin(ichi)), (1000.0, 870.0));
}
#[test]
fn shippori_reads_vmtx() {
let Some(face) = shippori() else { return };
let font = face.skrifa().unwrap();
let vmtx = font.vmtx().unwrap();
let gm = font.glyph_metrics(Size::unscaled(), LocationRef::default());
for c in ['「', '、', '一', 'ヸ'] {
let gid = face.glyph_for(c);
let g = GlyphId::new(gid);
assert_eq!(face.v_advance(gid), f64::from(vmtx.advance(g).unwrap()), "{c}");
// No VORG (TrueType outlines): the origin is the glyph's top plus its top side bearing.
let top = f64::from(gm.bounds(g).unwrap().y_max) + f64::from(vmtx.side_bearing(g).unwrap());
assert_eq!(face.v_origin(gid), top, "{c}");
}
// No BASE table: the em box is the OS/2 typo ascender and descender (exactly one em here).
assert_eq!(face.em_box(), (880.0, -120.0));
// Ideographs fill the em box; ヸ is taller than it is wide.
let ichi = face.glyph_for('一');
assert_eq!((face.v_advance(ichi), face.v_origin(ichi)), (1000.0, 880.0));
let wi = face.glyph_for('ヸ');
assert_eq!((face.advance(wi), face.v_advance(wi)), (1000.0, 1024.0));
}
#[test]
fn fonts_without_vmtx_use_the_em_box() {
// Source Sans 3 has no vmtx; its BASE table gives the ideographic em box bottom (-170).
let face = face_of(font_with("No Vmtx", &['一']).unwrap());
let gid = face.glyph_for('一');
assert_ne!(gid, 0);
assert_eq!(face.em_box(), (830.0, -170.0));
assert_eq!(face.v_advance(gid), face.upem);
assert_eq!(face.v_origin(gid), 830.0);
// Without BASE either: the OS/2 typo ascender and descender (1000, -326), centred on one em.
let face = face_of(with_table_len(font_with("No Base", &['一']).unwrap(), b"BASE", 0).unwrap());
assert_eq!(face.em_box(), (837.0, -163.0));
// Without OS/2: the ascender and descender, the same way.
let face = face_of(with_table_len(with_table_len(font_with("No OS2", &['一']).unwrap(), b"BASE", 0).unwrap(), b"OS/2", 0).unwrap());
let (top, bottom) = face.em_box();
assert_eq!(top - bottom, face.upem);
assert!((top + bottom - (face.ascent - face.descent)).abs() < 1e-9, "{top} {bottom}");
}
#[test]
fn broken_vertical_tables_do_not_panic() {
let mut fonts = vec![("synthetic", vertical_font())];
if let Some(face) = shippori() {
fonts.push(("Shippori Mincho", face.data().to_vec()));
}
let vorg = with_vorg(&vertical_font(), 880, &[(1, 900), (2, 910)]).unwrap();
let mut cases = vec![
("synthetic: VORG cut short".to_string(), with_table_len(vorg.clone(), b"VORG", 6).unwrap()),
("synthetic: more origins than VORG holds".to_string(), with_table_u16(vorg, b"VORG", 6, u16::MAX).unwrap()),
("BASE cut short".to_string(), with_table_len(crate::bundled()[0].to_vec(), b"BASE", 6).unwrap()),
];
for (name, font) in fonts {
cases.extend([
(format!("{name}: vmtx cut short"), with_table_len(font.clone(), b"vmtx", 2).unwrap()),
(format!("{name}: vmtx empty"), with_table_len(font.clone(), b"vmtx", 0).unwrap()),
(format!("{name}: more metrics than vmtx holds"), with_table_u16(font.clone(), b"vhea", 34, u16::MAX).unwrap()),
(format!("{name}: no long metrics"), with_table_u16(font.clone(), b"vhea", 34, 0).unwrap()),
(format!("{name}: vhea cut short"), with_table_len(font, b"vhea", 10).unwrap()),
]);
}
for (what, bytes) in cases {
let face = face_of(bytes);
let (top, bottom) = face.em_box();
assert!(top > bottom && (top - bottom - face.upem).abs() < 1e-9, "{what}: {top} {bottom}");
for gid in [0, face.glyph_for('一'), face.glyph_for('、'), u32::from(u16::MAX), u32::MAX] {
let (adv, origin) = (face.v_advance(gid), face.v_origin(gid));
assert!(adv.is_finite() && adv >= 0.0 && origin.is_finite(), "{what}: {gid} {adv} {origin}");
}
let g = shape_vertical(&face, "一、ま", &[feature("vpal").unwrap()], |c| c, japanese());
assert_eq!(g.len(), 3, "{what}");
}
}
#[test]
fn upright_text_shapes_top_to_bottom() {
let face = face_of(vertical_font());
let h = shape_with_context(&face, "一ヸ、", &[], |c| c, japanese());
let v = shape_vertical(&face, "一ヸ、", &[], |c| c, japanese());
// This font has no vertical forms (`vert`): the same glyphs.
assert_eq!(v.iter().map(|g| g.gid).collect::<Vec<_>>(), h.iter().map(|g| g.gid).collect::<Vec<_>>());
// Advances run down the line, by vmtx; offsets are what GPOS moves a glyph from its vertical
// origin (nothing here).
let placed: Vec<_> = v.iter().map(|g| (g.x_advance, g.y_advance, g.x_offset, g.y_offset)).collect();
assert_eq!(placed, [(0, -1000, 0, 0), (0, -1024, 0, 0), (0, -1000, 0, 0)]);
// Origins from VORG leave the offsets alone too.
let face = face_of(with_vorg(&vertical_font(), 880, &[(gid_in(&vertical_font(), 'ヸ'), 900)]).unwrap());
let v = shape_vertical(&face, "一ヸ", &[], |c| c, ShapeContext::default());
assert_eq!(v.iter().map(|g| (g.y_advance, g.x_offset, g.y_offset)).collect::<Vec<_>>(), [(-1000, 0, 0), (-1024, 0, 0)]);
}
#[test]
fn shippori_takes_vertical_forms_and_proportional_metrics() {
let Some(face) = shippori() else { return };
let h = shape_with_context(&face, "一、", &[], |c| c, japanese());
let v = shape_vertical(&face, "一、", &[], |c| c, japanese());
assert_eq!(v[0].gid, h[0].gid);
// `vert` without asking: the comma's vertical form.
assert_ne!(v[1].gid, h[1].gid);
assert_eq!(face.v_origin(v[1].gid), 880.0);
// Advances run down the line; offsets are what GPOS moves a glyph from its vertical origin.
for g in &v {
assert_eq!((g.x_advance, g.y_advance, g.x_offset, g.y_offset), (0, -1000, 0, 0), "{g:?}");
}
assert_eq!(shape_vertical(&face, "ヸ", &[], |c| c, ShapeContext::default())[0].y_advance, -1024);
// `vpal` (proportional vertical metrics) shortens kana and moves them along the line.
let ma = shape_vertical(&face, "ま", &[feature("vpal").unwrap()], |c| c, ShapeContext::default())[0];
assert_eq!((ma.y_advance, ma.x_offset, ma.y_offset), (-974, 0, 4));
}

View File

@ -0,0 +1,127 @@
//! Vertical metrics for upright glyphs in vertical text: advances down the line (`vmtx`), vertical
//! origins (`VORG`, else `vmtx` and the glyph's top) and the ideographic em box (`BASE`, else
//! OS/2). Fonts without them fall back to the em box: one em down the line, the origin at its top.
use skrifa::instance::Size;
use skrifa::metrics::GlyphMetrics;
use skrifa::raw::TableProvider;
use skrifa::raw::tables::{vmtx::Vmtx, vorg::Vorg};
use skrifa::raw::types::Tag;
use skrifa::{GlyphId, MetadataProvider};
use crate::FontFace;
/// A face's vertical metrics tables, read once for a run of lookups.
pub(crate) struct VMetrics<'a> {
vmtx: Option<Vmtx<'a>>,
vorg: Option<Vorg<'a>>,
glyphs: Option<GlyphMetrics<'a>>,
upem: f64,
em_top: f64,
}
impl<'a> VMetrics<'a> {
pub(crate) fn new(face: &'a FontFace) -> Self {
let font = face.skrifa();
VMetrics {
vmtx: font.as_ref().and_then(|f| f.vmtx().ok()),
vorg: font.as_ref().and_then(|f| f.vorg().ok()),
glyphs: font.map(|f| f.glyph_metrics(Size::unscaled(), face.location())),
upem: face.upem,
em_top: face.em_box().0,
}
}
/// Advance down the line in font units.
pub(crate) fn advance(&self, gid: GlyphId) -> f64 {
self.vmtx.as_ref().and_then(|t| t.advance(gid)).map_or(self.upem, f64::from)
}
/// Height of the vertical origin above the glyph's baseline in font units.
pub(crate) fn origin(&self, gid: GlyphId) -> f64 {
if let Some(t) = &self.vorg {
return f64::from(t.vertical_origin_y(gid));
}
// TrueType outlines: the glyph's top plus its top side bearing.
if let (Some(t), Some(g)) = (&self.vmtx, &self.glyphs)
&& let (Some(tsb), Some(b)) = (t.side_bearing(gid), g.bounds(gid))
&& t.advance(gid).is_some()
{
return f64::from(b.y_max) + f64::from(tsb);
}
self.em_top
}
}
/// The ideographic em box (top, bottom) in font units, y up: `BASE` `idtp`/`ideo`, else the OS/2
/// typo ascender and descender, else the ascender and descender; the last two centred on one em.
pub(crate) fn em_box(face: &FontFace) -> (f64, f64) {
let upem = face.upem;
let font = face.skrifa();
if let Some(b) = font.as_ref().and_then(|f| base_em_box(f, upem)) {
return b;
}
let centred = |asc: f64, desc: f64| {
let span = asc - desc;
(span > 0.0 && span.is_finite()).then(|| {
let top = asc - (span - upem) / 2.0;
(top, top - upem)
})
};
font.and_then(|f| f.os2().ok())
.and_then(|t| centred(f64::from(t.s_typo_ascender()), f64::from(t.s_typo_descender())))
.or_else(|| centred(face.ascent, -face.descent))
.unwrap_or((upem * 0.88, upem * -0.12))
}
/// The em box from the `BASE` table's horizontal axis: the Han script's values, else the default
/// script's, else the first script's that has them.
fn base_em_box(font: &skrifa::FontRef<'_>, upem: f64) -> Option<(f64, f64)> {
let axis = font.base().ok()?.horiz_axis()?.ok()?;
let tags = axis.base_tag_list()?.ok()?;
let index = |tag: &[u8; 4]| tags.baseline_tags().iter().position(|t| t.get() == Tag::new(tag));
let (ideo, idtp) = (index(b"ideo"), index(b"idtp"));
if ideo.is_none() && idtp.is_none() {
return None;
}
let scripts = axis.base_script_list().ok()?;
let records = scripts.base_script_records();
let rank = |t: Tag| [Tag::new(b"hani"), Tag::new(b"kana"), Tag::new(b"DFLT")].iter().position(|p| *p == t).unwrap_or(3);
let mut order: Vec<_> = records.iter().collect();
order.sort_by_key(|r| rank(r.base_script_tag()));
order.into_iter().find_map(|r| {
let values = r.base_script(scripts.offset_data()).ok()?.base_values()?.ok()?;
let coord = |i: Option<usize>| i.and_then(|i| values.base_coords().get(i).ok()).map(|c| f64::from(c.coordinate()));
match (coord(ideo), coord(idtp)) {
(Some(bottom), Some(top)) => (top > bottom).then_some((top, bottom)),
(Some(bottom), None) => Some((bottom + upem, bottom)),
(None, Some(top)) => Some((top, top - upem)),
(None, None) => None,
}
})
}
/// The shaper's vertical metrics callbacks: ours, so that its vertical advances and origins are
/// the ones glyphs are placed with.
pub(crate) struct VerticalFuncs<'a> {
pub(crate) face: &'a FontFace,
pub(crate) metrics: VMetrics<'a>,
}
impl VerticalFuncs<'_> {
/// The vertical origin (x, y) in whole font units: centred across, [`VMetrics::origin`] up.
pub(crate) fn origin(&self, gid: u32) -> (i32, i32) {
((self.face.advance(gid) / 2.0).round() as i32, self.metrics.origin(GlyphId::new(gid)).round() as i32)
}
}
impl harfrust::font::FontFuncs for VerticalFuncs<'_> {
fn advance_height(&mut self, _: &harfrust::font::BuiltinFontFuncs, glyph: harfrust::GlyphId) -> i32 {
// The shaper's y axis points up: advancing down the line is negative.
-(self.metrics.advance(GlyphId::new(glyph.to_u32())).round() as i32)
}
fn vertical_origin(&mut self, _: &harfrust::font::BuiltinFontFuncs, glyph: harfrust::GlyphId) -> (i32, i32) {
self.origin(glyph.to_u32())
}
}