mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 05:07:51 -06:00
Merge #631: Type: Character Alignment on the ICF top and bottom (ideographic character face)
# Conflicts: # ROADMAP.md # crates/text/src/layout.rs
This commit is contained in:
@@ -79,6 +79,19 @@ pub struct FontFace {
|
||||
pub(crate) instance: Option<harfrust::ShaperInstance>,
|
||||
/// [`Self::ideographic_centre`], read once: layout asks for it per glyph.
|
||||
ideographic_centre: std::sync::OnceLock<f64>,
|
||||
/// [`Self::icf_margins`], read once.
|
||||
icf_margins: std::sync::OnceLock<IcfMargins>,
|
||||
}
|
||||
|
||||
/// Where the ideographic character face (ICF) lies inside the ideographic em box: its distance
|
||||
/// from each edge of the em box, in ems. `top` and `bottom` along a horizontal line, `right` and
|
||||
/// `left` across a vertical one.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct IcfMargins {
|
||||
pub top: f64,
|
||||
pub bottom: f64,
|
||||
pub right: f64,
|
||||
pub left: f64,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FontFace {
|
||||
@@ -177,6 +190,69 @@ impl FontFace {
|
||||
gid.and_then(|g| self.vertical_glyph(g)).map_or(0.38, |(advance, origin)| (origin - advance * 0.5) / self.upem)
|
||||
})
|
||||
}
|
||||
/// The ideographic character face as margins inside the em box ([`IcfMargins`]): from the
|
||||
/// font's BASE table as the OpenType baseline tags define it (`icfb`, `icft` against `ideo`,
|
||||
/// `idtp`), else, as that definition allows, from the bounds of some ideographs and kana
|
||||
/// averaged, else none (the ICF is the em box).
|
||||
/// <https://learn.microsoft.com/en-us/typography/opentype/spec/baselinetags>
|
||||
pub fn icf_margins(&self) -> IcfMargins {
|
||||
*self.icf_margins.get_or_init(|| self.icf_from_base().or_else(|| self.icf_from_glyphs()).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// [`Self::icf_margins`] from the BASE table: the ICF's bottom edge `icfb` (top edge `icft`, else
|
||||
/// as far below the em box top) against the em box `ideo` .. `idtp` (else the OS/2 typographic
|
||||
/// descender, and one em above it); across vertical lines, `icfb` and `icft` of the vertical
|
||||
/// axis against 0 .. `idtp` (else the horizontal margin, and one em).
|
||||
pub(crate) fn icf_from_base(&self) -> Option<IcfMargins> {
|
||||
use skrifa::raw::TableProvider;
|
||||
let f = self.skrifa()?;
|
||||
let base = f.base().ok()?;
|
||||
let em = self.upem;
|
||||
let h = base_values(base.horiz_axis()?.ok()?)?;
|
||||
let icfb = *h.get(b"icfb")?;
|
||||
let em_bottom = match h.get(b"ideo") {
|
||||
Some(v) => *v,
|
||||
None => f64::from(f.os2().ok()?.s_typo_descender()),
|
||||
};
|
||||
let em_top = h.get(b"idtp").copied().unwrap_or(em_bottom + em);
|
||||
let margin = icfb - em_bottom;
|
||||
let icf_top = h.get(b"icft").copied().unwrap_or(em_top - margin);
|
||||
let v = base.vert_axis().and_then(Result::ok).and_then(base_values).unwrap_or_default();
|
||||
let icf_left = v.get(b"icfb").copied().unwrap_or(margin);
|
||||
let em_right = v.get(b"idtp").copied().unwrap_or(em);
|
||||
let icf_right = v.get(b"icft").copied().unwrap_or(em_right - icf_left);
|
||||
let m = IcfMargins { top: (em_top - icf_top) / em, bottom: margin / em, right: (em_right - icf_right) / em, left: icf_left / em };
|
||||
// Inside the em box, short of its middle: anything else is a damaged table.
|
||||
[m.top, m.bottom, m.right, m.left].iter().all(|x| (0.0..0.5).contains(x)).then_some(m)
|
||||
}
|
||||
|
||||
/// [`Self::icf_margins`] from the ink of some ideographs and kana: each edge's margin to the em
|
||||
/// box (its centre [`Self::ideographic_centre`], one em high and wide), averaged.
|
||||
pub(crate) fn icf_from_glyphs(&self) -> Option<IcfMargins> {
|
||||
let f = self.skrifa()?;
|
||||
let metrics = f.glyph_metrics(Size::unscaled(), self.location());
|
||||
let em = self.upem;
|
||||
let (em_bottom, em_top) = ((self.ideographic_centre() - 0.5) * em, (self.ideographic_centre() + 0.5) * em);
|
||||
let boxes: Vec<_> = ['国', '東', '永', '書', 'あ', 'ア']
|
||||
.into_iter()
|
||||
.map(|c| self.glyph_for(c))
|
||||
.filter(|g| *g != 0)
|
||||
.filter_map(|g| metrics.bounds(GlyphId::new(g)))
|
||||
.collect();
|
||||
if boxes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let n = boxes.len() as f64;
|
||||
let mean = |v: &dyn Fn(&skrifa::metrics::BoundingBox) -> f32| boxes.iter().map(|b| f64::from(v(b))).sum::<f64>() / n;
|
||||
let m = IcfMargins {
|
||||
top: (em_top - mean(&|b| b.y_max)) / em,
|
||||
bottom: (mean(&|b| b.y_min) - em_bottom) / em,
|
||||
right: (em - mean(&|b| b.x_max)) / em,
|
||||
left: mean(&|b| b.x_min) / em,
|
||||
};
|
||||
[m.top, m.bottom, m.right, m.left].iter().all(|x| (0.0..0.5).contains(x)).then_some(m)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
@@ -926,9 +1002,28 @@ fn make_face(bytes: FontBytes, index: u32, spec: FaceStyle, path: Option<std::pa
|
||||
index,
|
||||
path,
|
||||
ideographic_centre: std::sync::OnceLock::new(),
|
||||
icf_margins: std::sync::OnceLock::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// An axis of a BASE table: its default baseline values for Han ideographs (`hani`), else kana
|
||||
/// (`kana`), else its first script, by tag, in font units.
|
||||
fn base_values(axis: skrifa::raw::tables::base::Axis<'_>) -> Option<HashMap<[u8; 4], f64>> {
|
||||
let tags = axis.base_tag_list()?.ok()?;
|
||||
let list = axis.base_script_list().ok()?;
|
||||
let records = list.base_script_records();
|
||||
let record =
|
||||
[b"hani", b"kana"].into_iter().find_map(|t| records.iter().find(|r| r.base_script_tag() == Tag::new(t))).or_else(|| records.first())?;
|
||||
let values = record.base_script(list.offset_data()).ok()?.base_values()?.ok()?;
|
||||
let mut out = HashMap::new();
|
||||
for (tag, coord) in tags.baseline_tags().iter().zip(values.base_coords().iter()) {
|
||||
if let Ok(c) = coord {
|
||||
out.insert(tag.get().to_be_bytes(), f64::from(c.coordinate()));
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// `s` lowercased, without anything but letters and digits.
|
||||
fn norm_chars(s: &str) -> impl Iterator<Item = char> + '_ {
|
||||
s.chars().filter(|c| c.is_alphanumeric()).flat_map(char::to_lowercase)
|
||||
|
||||
@@ -430,19 +430,23 @@ fn glyph_em(g: &SGlyph) -> f64 {
|
||||
|
||||
/// How far up (line space) Character Alignment `a` moves glyph `g` on a line whose largest em is
|
||||
/// `line_em`: the glyph's em box top, centre or bottom onto the line's (the em box running from
|
||||
/// its centre less half an em to its centre plus half an em above the baseline). Nothing on the
|
||||
/// Roman baseline, or for the line's largest characters.
|
||||
/// its centre less half an em to its centre plus half an em above the baseline), or its ICF's top
|
||||
/// or bottom (the em box's less the face's [`crate::IcfMargins`]; across a vertical line, its
|
||||
/// right and left). Nothing on the Roman baseline, or for the line's largest characters.
|
||||
///
|
||||
/// An inline graphic has its run's em (whatever its scale): it counts as one of the line's
|
||||
/// largest characters only when its run's size is, and moves with the text of its run (its art
|
||||
/// stays centred on that text's cap height).
|
||||
fn align_shift(g: &SGlyph, a: vectorcraft_doc::CharAlign, line_em: f64) -> f64 {
|
||||
fn align_shift(g: &SGlyph, a: vectorcraft_doc::CharAlign, line_em: f64, vertical: bool) -> f64 {
|
||||
use vectorcraft_doc::CharAlign;
|
||||
let icf = || g.face.icf_margins();
|
||||
let k = match a {
|
||||
CharAlign::RomanBaseline => return 0.0,
|
||||
CharAlign::EmBoxTop => 0.5,
|
||||
CharAlign::EmBoxCenter => 0.0,
|
||||
CharAlign::EmBoxBottom => -0.5,
|
||||
CharAlign::IcfTop => 0.5 - if vertical { icf().right } else { icf().top },
|
||||
CharAlign::IcfBottom => -0.5 + if vertical { icf().left } else { icf().bottom },
|
||||
};
|
||||
(g.face.ideographic_centre() + k) * (line_em - glyph_em(g)).max(0.0)
|
||||
}
|
||||
@@ -1534,7 +1538,7 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, regions: Optio
|
||||
adv += per_gap;
|
||||
}
|
||||
}
|
||||
let y = baseline - align_shift(g, cx.style_at(g.byte).char_align, line_em);
|
||||
let y = baseline - align_shift(g, cx.style_at(g.byte).char_align, line_em, cx.vertical);
|
||||
cx.emit(g, Affine::translate((x, y)), Point::new(x, y), 0.0, adv, li);
|
||||
x += adv;
|
||||
if j < trimmed {
|
||||
|
||||
@@ -25,7 +25,8 @@ pub mod thread;
|
||||
pub use craft_fonts::{CRAFT_FONTS, CraftFont};
|
||||
pub use features::{LIGATURE_TRACKING_LIMITS, OtFeatures, explicit_ligatures, ligatures_suppressed_by};
|
||||
pub use fontdb::{
|
||||
FALLBACK_FAMILY, FontClass, FontDb, FontFace, FontMatch, FontTraits, PlatformFontFiles, set_platform_font_files, style_weight, system_font_dirs,
|
||||
FALLBACK_FAMILY, FontClass, FontDb, FontFace, FontMatch, FontTraits, IcfMargins, PlatformFontFiles, set_platform_font_files, style_weight,
|
||||
system_font_dirs,
|
||||
};
|
||||
use kurbo::{Affine, BezPath, Point, Rect, Vec2};
|
||||
pub use layout::{layout, layout_with};
|
||||
|
||||
@@ -647,6 +647,42 @@ fn character_alignment_lines_small_characters_up_with_the_largest_em_box() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Character Alignment on the ICF: a 20 pt character next to a 40 pt one lines its ideographic
|
||||
/// character face's top (right, vertical) or bottom (left) up with the big one's, which lies inside
|
||||
/// the em box by the face's ICF margins. A face without ideographs has none (the ICF is the em
|
||||
/// box). The Japanese faces need craft-fonts (skipped without them).
|
||||
#[test]
|
||||
fn character_alignment_on_the_icf_lines_small_characters_up_with_the_largest_face() {
|
||||
use vectorcraft_doc::{CharAlign, TextRun};
|
||||
let place = |family: &str, a: CharAlign, vertical_type: bool| {
|
||||
let st = |size: f64| CharStyle { font_family: family.into(), ..style(size) };
|
||||
let mut t = point("", st(40.0));
|
||||
t.vertical = vertical_type;
|
||||
t.runs = vec![TextRun { text: "大".into(), style: st(40.0) }, TextRun { text: "小".into(), style: CharStyle { char_align: a, ..st(20.0) } }];
|
||||
let l = layout(db(), &t);
|
||||
let (big, small) = (l.glyphs[0].origin, l.glyphs[1].origin);
|
||||
if vertical_type { small.x - big.x } else { big.y - small.y }
|
||||
};
|
||||
let near = |a: f64, b: f64| (a - b).abs() < 0.01;
|
||||
// No ideographs: the ICF is the em box.
|
||||
assert_eq!(db().face("Source Sans 3", "Regular").unwrap().icf_margins(), crate::IcfMargins::default());
|
||||
let Some(face) = db().face("Shippori Mincho", "Regular").filter(|f| f.family == "Shippori Mincho") else {
|
||||
return; // no Japanese font here
|
||||
};
|
||||
let m = face.icf_margins();
|
||||
assert!([m.top, m.bottom, m.right, m.left].iter().all(|x| *x > 0.01 && *x < 0.2), "{m:?}");
|
||||
for vertical_type in [false, true] {
|
||||
let (top, bottom) = if vertical_type { (m.right, m.left) } else { (m.top, m.bottom) };
|
||||
// The big character's ICF top is `top` ems below its em box top: 20 pt of difference.
|
||||
let em_top = place("Shippori Mincho", CharAlign::EmBoxTop, vertical_type);
|
||||
let icf_top = place("Shippori Mincho", CharAlign::IcfTop, vertical_type);
|
||||
assert!(near(em_top - icf_top, top * 20.0), "vertical {vertical_type}: {em_top} {icf_top} {m:?}");
|
||||
let em_bottom = place("Shippori Mincho", CharAlign::EmBoxBottom, vertical_type);
|
||||
let icf_bottom = place("Shippori Mincho", CharAlign::IcfBottom, vertical_type);
|
||||
assert!(near(icf_bottom - em_bottom, bottom * 20.0), "vertical {vertical_type}: {em_bottom} {icf_bottom} {m:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Burasagari leaves Latin punctuation alone: in a measure of exactly "abcd", the full stop of
|
||||
/// "abcd. ef" doesn't hang with Standard or Forced (the line breaks as it does with None), and a
|
||||
/// Latin line ending in a full stop isn't shortened by Forced.
|
||||
|
||||
Reference in New Issue
Block a user