CJK: ICF top and bottom from the font's BASE table for grid and character alignment

This commit is contained in:
aiongg
2026-10-10 16:02:52 +00:00
parent d9bc53d7f8
commit b720ff6147
6 changed files with 105 additions and 18 deletions
+5 -8
View File
@@ -16,10 +16,6 @@ use crate::shape::Glyph;
/// Most grid lines one line or paragraph takes (input-derived).
pub(crate) const MAX_GYOUDORI: u32 = 100;
/// The ideographic character face's inset from the em box, top and bottom, as a share of the em.
/// An approximation: fonts with a `BASE` table give `icft` / `icfb`, which aren't read yet.
const ICF_INSET: f64 = 0.05;
#[derive(Clone, Copy, Debug, PartialEq)]
enum Anchor {
First,
@@ -36,21 +32,22 @@ fn anchor(r: Reference) -> Anchor {
}
/// The line's grid reference point below its baseline (negative = above), from its largest
/// character's em box ([`crate::cjk_em_box`], as the leading model measures it).
/// character's em box ([`crate::cjk_em_box`], as the leading model measures it) or ICF box
/// ([`crate::cjk_icf_box`]).
pub(crate) fn reference_offset(line: &[Glyph], r: Reference) -> f64 {
if r == Reference::Baseline {
return 0.0;
}
let Some(g) = line.iter().filter(|g| g.adv > 0.0).max_by(|a, b| a.size.total_cmp(&b.size)) else { return 0.0 };
let (top, bottom) = crate::cjk_em_box(g);
let inset = (bottom - top) * ICF_INSET;
let (icf_top, icf_bottom) = crate::cjk_icf_box(g);
match r {
Reference::Baseline => 0.0,
Reference::EmTop => top,
Reference::EmCenter => (top + bottom) / 2.0,
Reference::EmBottom => bottom,
Reference::IcfTop => top + inset,
Reference::IcfBottom => bottom - inset,
Reference::IcfTop => icf_top,
Reference::IcfBottom => icf_bottom,
}
}
+13 -2
View File
@@ -2087,6 +2087,17 @@ fn cjk_em_box(g: &Glyph) -> (f64, f64) {
let top = -height * g.ascent / (g.ascent + g.descent).max(1e-9);
(top, top + height)
}
/// The ideographic character face (ICF) box relative to the baseline: the em box of
/// [`cjk_em_box`] inset by the font's own ICF insets (`BASE` `icft`/`icfb`, else 5% of the em).
fn cjk_icf_box(g: &Glyph) -> (f64, f64) {
let (top, bottom) = cjk_em_box(g);
let (em_top, em_bottom) = g.face.em_box();
let (icf_top, icf_bottom) = g.face.icf_box();
let em = (em_top - em_bottom).max(1e-9);
let height = bottom - top;
let inset = |v: f64| if v.is_finite() { v.clamp(0.0, 0.5) } else { 0.05 };
(top + inset((em_top - icf_top) / em) * height, bottom - inset((icf_bottom - em_bottom) / em) * height)
}
fn cjk_alignment_shift(g: &Glyph, reference: &Glyph) -> f64 {
use designcraft_doc::cjk::CharacterAlignment as A;
let (t, b) = cjk_em_box(g);
@@ -2096,8 +2107,8 @@ fn cjk_alignment_shift(g: &Glyph, reference: &Glyph) -> f64 {
A::EmTop => t - rt,
A::EmCenter => (t + b - rt - rb) / 2.0,
A::EmBottom => b - rb,
A::IcfTop => reference.ascent - g.ascent,
A::IcfBottom => g.descent - reference.descent,
A::IcfTop => cjk_icf_box(g).0 - cjk_icf_box(reference).0,
A::IcfBottom => cjk_icf_box(g).1 - cjk_icf_box(reference).1,
}
}
fn cjk_line_reference(line: &[Glyph]) -> f64 {
+8
View File
@@ -119,6 +119,8 @@ pub struct FontFace {
bmp: std::sync::OnceLock<Box<[u64]>>,
/// The ideographic em box, read on first use.
em: std::sync::OnceLock<(f64, f64)>,
/// The ideographic character face box, read on first use.
icf: std::sync::OnceLock<(f64, f64)>,
/// The font menu group and native family name, read on first use.
group: std::sync::OnceLock<(FontGroup, Option<String>)>,
}
@@ -265,6 +267,11 @@ impl FontFace {
pub fn em_box(&self) -> (f64, f64) {
*self.em.get_or_init(|| crate::vertical::em_box(self))
}
/// The ideographic character face (ICF) box (top, bottom) in font units, y up: `BASE`
/// `icft`/`icfb`, else the em box inset by 5% of the em at the top and bottom.
pub fn icf_box(&self) -> (f64, f64) {
*self.icf.get_or_init(|| crate::vertical::icf_box(self))
}
/// The font menu group the font is for and, for a CJK group, its family name in that language
/// (see [`crate::FamilyInfo`]); read from the font once.
pub fn menu_group(&self) -> (FontGroup, Option<&str>) {
@@ -708,6 +715,7 @@ fn make_face(bytes: FontBytes, source: FontSource, index: u32, family: String, s
index,
bmp: std::sync::OnceLock::new(),
em: std::sync::OnceLock::new(),
icf: std::sync::OnceLock::new(),
group: std::sync::OnceLock::new(),
})
}
+30
View File
@@ -162,6 +162,36 @@ pub fn with_vorg(font: &[u8], default: i16, overrides: &[(u16, i16)]) -> Option<
with_tables(font, vec![(*b"VORG", vorg)])
}
/// `font` with a `BASE` table whose horizontal axis gives the default script the `baselines`
/// (tag, coordinate in font units). `None` if `font` can't be read.
pub fn with_base(font: &[u8], baselines: &[([u8; 4], i16)]) -> Option<Vec<u8>> {
let mut lines = baselines.to_vec();
lines.sort_unstable_by_key(|l| l.0);
lines.dedup_by_key(|l| l.0);
let n = u16::try_from(lines.len()).ok()?;
let u16s = |out: &mut Vec<u8>, vs: &[u16]| vs.iter().for_each(|v| out.extend_from_slice(&v.to_be_bytes()));
// Header (version 1.0, horizontal axis at 8), the axis (tag list at 4, script list after it),
// the tag list, one `DFLT` script record, its BaseScript (values at 6) and the BaseValues
// with one format-1 BaseCoord per baseline.
let mut base = Vec::new();
u16s(&mut base, &[1, 0, 8, 0]);
u16s(&mut base, &[4, 4 + 2 + 4 * n]);
u16s(&mut base, &[n]);
lines.iter().for_each(|(tag, _)| base.extend_from_slice(tag));
u16s(&mut base, &[1]);
base.extend_from_slice(b"DFLT");
u16s(&mut base, &[8, 6, 0, 0]);
u16s(&mut base, &[0, n]);
for k in 0..n {
u16s(&mut base, &[4 + 2 * n + 4 * k]);
}
for (_, y) in &lines {
u16s(&mut base, &[1]);
base.extend_from_slice(&y.to_be_bytes());
}
with_tables(font, vec![(*b"BASE", base)])
}
/// `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>> {
+17 -1
View File
@@ -6,7 +6,7 @@
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::testing::{font_mapping, font_with, with_base, with_table_len, with_table_u16, with_vmtx, with_vorg};
use crate::{ShapeContext, feature, shape_vertical, shape_with_context};
/// Text in Japanese.
@@ -121,6 +121,20 @@ fn fonts_without_vmtx_use_the_em_box() {
assert!((top + bottom - (face.ascent - face.descent)).abs() < 1e-9, "{top} {bottom}");
}
#[test]
fn icf_box_comes_from_base_else_an_inset_em_box() {
let font = font_with("ICF", &['一']).unwrap();
let face = face_of(with_base(&font, &[(*b"ideo", -120), (*b"idtp", 880), (*b"icfb", -80), (*b"icft", 840)]).unwrap());
assert_eq!((face.em_box(), face.icf_box()), ((880.0, -120.0), (840.0, -80.0)));
// One of the two: mirrored inside the em box.
let face = face_of(with_base(&font, &[(*b"ideo", -120), (*b"icfb", -90)]).unwrap());
assert_eq!(face.icf_box(), (850.0, -90.0));
// No ICF baselines (Source Sans 3's BASE has only ideo and romn): the em box inset by 5% of the em.
let face = face_of(font);
assert_eq!(face.em_box(), (830.0, -170.0));
assert_eq!(face.icf_box(), (780.0, -120.0));
}
#[test]
fn broken_vertical_tables_do_not_panic() {
let mut fonts = vec![("synthetic", vertical_font())];
@@ -146,6 +160,8 @@ fn broken_vertical_tables_do_not_panic() {
let face = face_of(bytes);
let (top, bottom) = face.em_box();
assert!(top > bottom && (top - bottom - face.upem).abs() < 1e-9, "{what}: {top} {bottom}");
let (icf_top, icf_bottom) = face.icf_box();
assert!(icf_top > icf_bottom && icf_top.is_finite() && icf_bottom.is_finite(), "{what}: ICF {icf_top} {icf_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}");
+32 -7
View File
@@ -74,13 +74,42 @@ pub(crate) fn em_box(face: &FontFace) -> (f64, f64) {
.unwrap_or((upem * 0.88, upem * -0.12))
}
/// The ideographic character face (ICF) box (top, bottom) in font units, y up: `BASE`
/// `icft`/`icfb` (one of them alone is mirrored inside the em box), else the em box inset by 5% of
/// the em at the top and bottom.
pub(crate) fn icf_box(face: &FontFace) -> (f64, f64) {
let (top, bottom) = face.em_box();
let font = face.skrifa();
let base = match font.as_ref().and_then(|f| base_box(f, b"icfb", b"icft")) {
Some((Some(b), Some(t))) => Some((t, b)),
Some((Some(b), None)) => Some((top - (b - bottom), b)),
Some((None, Some(t))) => Some((t, bottom + (top - t))),
_ => None,
};
base.filter(|(t, b)| t.is_finite() && b.is_finite() && t > b).unwrap_or_else(|| {
let inset = (top - bottom) * 0.05;
(top - inset, bottom + inset)
})
}
/// 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)> {
match base_box(font, b"ideo", b"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 `BASE` horizontal axis's `bottom` and `top` baselines in font units from the Han script's
/// values, else the default script's, else the first script's that has either.
fn base_box(font: &skrifa::FontRef<'_>, bottom_tag: &[u8; 4], top_tag: &[u8; 4]) -> Option<(Option<f64>, Option<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"));
let (ideo, idtp) = (index(bottom_tag), index(top_tag));
if ideo.is_none() && idtp.is_none() {
return None;
}
@@ -92,12 +121,8 @@ fn base_em_box(font: &skrifa::FontRef<'_>, upem: f64) -> Option<(f64, f64)> {
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,
}
let pair = (coord(ideo), coord(idtp));
(pair.0.is_some() || pair.1.is_some()).then_some(pair)
})
}