mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 18:26:50 -06:00
CJK: ICF top and bottom from the font's BASE table for grid and character alignment
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user