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:
parent
e0fb78a230
commit
1602b37ec9
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue