mirror of
https://github.com/penpot/penpot.git
synced 2026-10-04 01:36:15 +00:00
231 lines
8.7 KiB
Rust
231 lines
8.7 KiB
Rust
use std::cell::RefCell;
|
|
use std::collections::HashMap;
|
|
use std::rc::Rc;
|
|
use std::thread::LocalKey;
|
|
|
|
use skia_safe::{Font, GlyphId};
|
|
|
|
use crate::shapes::gpos_vpal::{parse_vpal, VpalDelta};
|
|
|
|
/// Parsed font tables keyed by `SkTypefaceID`. `None` is cached too: a face
|
|
/// without the table never gains it.
|
|
type TableCache<T> = RefCell<HashMap<u32, Option<Rc<T>>>>;
|
|
|
|
thread_local! {
|
|
// Layouts are recomputed on every paint but a face's tables never
|
|
// change, so each table (whose parse copies it) is read once per face.
|
|
static VERTICAL_METRICS: TableCache<VerticalMetrics> = RefCell::default();
|
|
static VPAL_TABLES: TableCache<VpalTable> = RefCell::default();
|
|
static TYPO_METRICS: TableCache<TypoMetrics> = RefCell::default();
|
|
}
|
|
|
|
fn cached_table<T: 'static>(
|
|
cache: &'static LocalKey<TableCache<T>>,
|
|
font: &Font,
|
|
parse: impl FnOnce(&Font) -> Option<T>,
|
|
) -> Option<Rc<T>> {
|
|
let id = font.typeface().unique_id();
|
|
cache.with(|cache| {
|
|
cache
|
|
.borrow_mut()
|
|
.entry(id)
|
|
.or_insert_with(|| parse(font).map(Rc::new))
|
|
.clone()
|
|
})
|
|
}
|
|
|
|
fn units_per_em(font: &Font) -> Option<f32> {
|
|
let units_per_em = font.typeface().units_per_em()? as f32;
|
|
(units_per_em > 0.0).then_some(units_per_em)
|
|
}
|
|
|
|
fn table_data(font: &Font, tag: &[u8; 4]) -> Option<skia_safe::Data> {
|
|
font.typeface().copy_table_data(u32::from_be_bytes(*tag))
|
|
}
|
|
|
|
/// Vertical advances from a font's `vhea`/`vmtx` tables. Upright cells
|
|
/// advance down the column by the glyph's true vertical advance rather
|
|
/// than its shaped horizontal advance: identical for full-width CJK, but
|
|
/// correct for vertical alternates and proportional glyphs whose `vmtx`
|
|
/// differs from `hmtx` (e.g. the vertical kana repeat marks).
|
|
pub(super) struct VerticalMetrics {
|
|
units_per_em: f32,
|
|
/// Advance heights of the first `advances.len()` glyph ids.
|
|
advances: Vec<u16>,
|
|
/// Advance for glyph ids at or beyond the long-metric count.
|
|
last: u16,
|
|
}
|
|
|
|
impl VerticalMetrics {
|
|
/// Parse `vhea`/`vmtx` off the font's typeface. Returns `None` when the
|
|
/// font carries no vertical metrics (the caller then keeps horizontal
|
|
/// advances).
|
|
pub(super) fn from_font(font: &Font) -> Option<Self> {
|
|
let units_per_em = units_per_em(font)?;
|
|
let vhea = table_data(font, b"vhea")?;
|
|
let vmtx = table_data(font, b"vmtx")?;
|
|
let vhea = vhea.as_bytes();
|
|
let vmtx = vmtx.as_bytes();
|
|
// `numberOfLongVerMetrics` is the trailing u16 of the 36-byte header.
|
|
let num_long = u16::from_be_bytes([*vhea.get(34)?, *vhea.get(35)?]) as usize;
|
|
if num_long == 0 {
|
|
return None;
|
|
}
|
|
// Each long metric is { advanceHeight: u16, topSideBearing: i16 }.
|
|
let mut advances = Vec::with_capacity(num_long);
|
|
for i in 0..num_long {
|
|
let o = i * 4;
|
|
let (Some(&hi), Some(&lo)) = (vmtx.get(o), vmtx.get(o + 1)) else {
|
|
break;
|
|
};
|
|
advances.push(u16::from_be_bytes([hi, lo]));
|
|
}
|
|
let last = *advances.last()?;
|
|
Some(Self {
|
|
units_per_em,
|
|
advances,
|
|
last,
|
|
})
|
|
}
|
|
|
|
/// Vertical advance of `glyph` at `font_size`, in pixels at the shaped
|
|
/// size (same units as the horizontal advances).
|
|
pub(super) fn advance(&self, glyph: GlyphId, font_size: f32) -> f32 {
|
|
let raw = self
|
|
.advances
|
|
.get(glyph as usize)
|
|
.copied()
|
|
.unwrap_or(self.last);
|
|
raw as f32 * font_size / self.units_per_em
|
|
}
|
|
}
|
|
|
|
pub(super) fn vertical_metrics(font: &Font) -> Option<Rc<VerticalMetrics>> {
|
|
cached_table(&VERTICAL_METRICS, font, VerticalMetrics::from_font)
|
|
}
|
|
|
|
/// GPOS `vpal` deltas for one typeface, in font units.
|
|
pub(super) struct VpalTable {
|
|
units_per_em: f32,
|
|
deltas: HashMap<u16, VpalDelta>,
|
|
}
|
|
|
|
impl VpalTable {
|
|
fn from_font(font: &Font) -> Option<Self> {
|
|
let units_per_em = units_per_em(font)?;
|
|
let gpos = table_data(font, b"GPOS")?;
|
|
let deltas = parse_vpal(gpos.as_bytes())?;
|
|
Some(Self {
|
|
units_per_em,
|
|
deltas,
|
|
})
|
|
}
|
|
|
|
/// Pixel deltas for a cluster at `font_size`: summed advance delta
|
|
/// (negative when the cell tightens) and the flow-axis shift of the
|
|
/// drawn ink (positive down the column). GPOS `yPlacement` is y-up,
|
|
/// so its sign flips into flow space. `None` when no glyph of the
|
|
/// cluster is covered.
|
|
pub(super) fn cluster_delta(&self, glyphs: &[GlyphId], font_size: f32) -> Option<(f32, f32)> {
|
|
let scale = font_size / self.units_per_em;
|
|
let mut advance = 0.0f32;
|
|
let mut flow_shift = None;
|
|
for glyph in glyphs {
|
|
if let Some(delta) = self.deltas.get(glyph) {
|
|
advance += delta.y_advance as f32 * scale;
|
|
flow_shift.get_or_insert(-(delta.y_placement as f32) * scale);
|
|
}
|
|
}
|
|
flow_shift.map(|shift| (advance, shift))
|
|
}
|
|
}
|
|
|
|
pub(super) fn vpal_table(font: &Font) -> Option<Rc<VpalTable>> {
|
|
cached_table(&VPAL_TABLES, font, VpalTable::from_font)
|
|
}
|
|
|
|
/// OS/2 typographic ascender/descender, normalised to the em (design units /
|
|
/// unitsPerEm). These bound the ideographic em box and are what the browser
|
|
/// uses for the vertical central baseline; `hhea`/`Font::metrics` are oversized
|
|
/// for CJK faces (their ascent exceeds the em) and would push an upright glyph
|
|
/// off the ideographic centre of its cell.
|
|
struct TypoMetrics {
|
|
/// sTypoAscender / unitsPerEm (positive, above the baseline).
|
|
ascender: f32,
|
|
/// sTypoDescender / unitsPerEm (negative, below the baseline).
|
|
descender: f32,
|
|
}
|
|
|
|
impl TypoMetrics {
|
|
fn from_font(font: &Font) -> Option<Self> {
|
|
let units_per_em = units_per_em(font)?;
|
|
let os2 = table_data(font, b"OS/2")?;
|
|
let os2 = os2.as_bytes();
|
|
// sTypoAscender @ 68 (i16), sTypoDescender @ 70 (i16); present in every
|
|
// OS/2 table version.
|
|
let ascender = i16::from_be_bytes([*os2.get(68)?, *os2.get(69)?]) as f32;
|
|
let descender = i16::from_be_bytes([*os2.get(70)?, *os2.get(71)?]) as f32;
|
|
Some(Self {
|
|
ascender: ascender / units_per_em,
|
|
descender: descender / units_per_em,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Ascent/descent for centring an upright cell, in Skia's sign convention
|
|
/// (ascent negative, descent positive) at the font's current size. Prefers the
|
|
/// OS/2 typographic metrics (the ideographic em box, matching the browser's
|
|
/// vertical central baseline used by the SVG/foreignObject export); falls back
|
|
/// to `Font::metrics` when the face carries no OS/2 table.
|
|
pub(super) fn upright_centre_metrics(font: &Font) -> (f32, f32) {
|
|
if let Some(typo) = cached_table(&TYPO_METRICS, font, TypoMetrics::from_font) {
|
|
let size = font.size();
|
|
return (-typo.ascender * size, -typo.descender * size);
|
|
}
|
|
let (_, metrics) = font.metrics();
|
|
(metrics.ascent, metrics.descent)
|
|
}
|
|
|
|
/// Vertical offset from a cell's top edge to the glyph baseline for an upright
|
|
/// cell. Centres the glyph's line box within the em cell (as the Tate-chu-yoko
|
|
/// path does) instead of hanging it from the horizontal ascent: CJK faces have
|
|
/// an ascent larger than the em, so hanging from it pushes every glyph below
|
|
/// its cell and the whole column overflows its bounds.
|
|
pub(super) fn upright_baseline_offset(ascent: f32, descent: f32, font_size: f32) -> f32 {
|
|
font_size / 2.0 - (ascent + descent) / 2.0
|
|
}
|
|
|
|
/// Baseline offset for an upright glyph of `font` in an em cell of `font_size`.
|
|
pub(super) fn upright_baseline(font: &Font, font_size: f32) -> f32 {
|
|
let (ascent, descent) = upright_centre_metrics(font);
|
|
upright_baseline_offset(ascent, descent, font_size)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::super::test_support::{TEST_FONT, VMTX_TEST_FONT};
|
|
use super::*;
|
|
use skia_safe::FontMgr;
|
|
|
|
#[test]
|
|
fn vertical_metrics_parse_vmtx() {
|
|
let font_mgr = FontMgr::new();
|
|
let typeface = font_mgr.new_from_data(VMTX_TEST_FONT, None).unwrap();
|
|
let font = Font::new(typeface, 20.0);
|
|
let vm = VerticalMetrics::from_font(&font).expect("vmtx present");
|
|
// gid 1 = 〱 (2000 units), gid 3+ fall back to the last long metric.
|
|
assert!((vm.advance(1, 20.0) - 40.0).abs() < 0.01);
|
|
assert!((vm.advance(3, 20.0) - 20.0).abs() < 0.01);
|
|
assert!((vm.advance(99, 20.0) - 20.0).abs() < 0.01);
|
|
}
|
|
|
|
#[test]
|
|
fn vertical_metrics_absent_without_vmtx() {
|
|
// The bundled Source Sans face carries no `vmtx`.
|
|
let font_mgr = FontMgr::new();
|
|
let typeface = font_mgr.new_from_data(TEST_FONT, None).unwrap();
|
|
let font = Font::new(typeface, 20.0);
|
|
assert!(VerticalMetrics::from_font(&font).is_none());
|
|
}
|
|
}
|