mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-27 19:56:51 -06:00
Merge feat/layout-parity: Layout: only CJK fonts take the em box top from a one-em typo box
Conflict: crates/compose/src/tests.rs: next's baseline-fit and keep tests, then the branch's OS/2 offset doc and code-page constant Conflict: crates/fonts/src/tests_vertical.rs: testing imports from both (with_base, with_code_pages)
This commit is contained in:
@@ -2243,9 +2243,11 @@ fn satisfiable_keep_chain_still_moves_together_to_the_next_frame() {
|
||||
assert_eq!(cs.frame(second).unwrap().lines.iter().map(|l| l.para).collect::<Vec<_>>(), [1, 2, 3]);
|
||||
}
|
||||
|
||||
/// OS/2 field offsets: sTypoAscender, sxHeight, sCapHeight.
|
||||
/// OS/2 field offsets: sTypoAscender, sTypoDescender, the high half of ulCodePageRange1, sxHeight,
|
||||
/// sCapHeight.
|
||||
const TYPO_ASCENDER: usize = 68;
|
||||
const TYPO_DESCENDER: usize = 70;
|
||||
const CODE_PAGES_HIGH: usize = 78;
|
||||
const X_HEIGHT: usize = 86;
|
||||
const CAP_HEIGHT: usize = 88;
|
||||
|
||||
@@ -2367,8 +2369,12 @@ fn em_box_first_baseline_follows_base_then_the_typo_box_or_the_cap_height() {
|
||||
db.add_font(b);
|
||||
db.face(family, "Regular").declared_em_box()
|
||||
};
|
||||
// Typo ascender − descender = 1 em: the typo ascender is the top.
|
||||
assert_eq!(add("EmBoxTypo 850", false, &[(TYPO_ASCENDER, 850), (TYPO_DESCENDER, -150)]), Some((850.0, -150.0)));
|
||||
// Typo ascender − descender = 1 em in a CJK font (JIS code page, bit 17): the typo ascender is
|
||||
// the top.
|
||||
let typo = [(TYPO_ASCENDER, 850), (TYPO_DESCENDER, -150)];
|
||||
assert_eq!(add("EmBoxTypo 850", false, &[typo[0], typo[1], (CODE_PAGES_HIGH, 1 << 1)]), Some((850.0, -150.0)));
|
||||
// In a Latin font the cap height decides: 0.5 em + 660 / 2.
|
||||
assert_eq!(add("EmBoxLatn 850", false, &typo), Some((830.0, -170.0)));
|
||||
// Otherwise the em box is centred on half the cap height: 0.5 em + 600 / 2.
|
||||
assert_eq!(add("EmBoxCapH 600", false, &[(TYPO_ASCENDER, 700), (CAP_HEIGHT, 600)]), Some((800.0, -200.0)));
|
||||
// No OS/2 cap height: the H glyph's top, 656.
|
||||
@@ -2391,7 +2397,7 @@ fn em_box_first_baseline_follows_base_then_the_typo_box_or_the_cap_height() {
|
||||
let close = |a: f64, b: f64| (a - b).abs() < 0.01;
|
||||
let asc = first("EmBoxCapH 600", [10.0; 2], FirstBaseline::Ascent);
|
||||
assert!(close(asc, 7.0), "{asc}");
|
||||
for (family, top) in [("EmBoxTypo 850", 8.5), ("EmBoxCapH 600", 8.0), ("EmBoxGlyphH 0", 8.28), ("EmBoxBase 850", 8.3)] {
|
||||
for (family, top) in [("EmBoxTypo 850", 8.5), ("EmBoxLatn 850", 8.3), ("EmBoxCapH 600", 8.0), ("EmBoxGlyphH 0", 8.28), ("EmBoxBase 850", 8.3)] {
|
||||
let emb = first(family, [10.0; 2], FirstBaseline::EmboxHeight);
|
||||
assert!(close(emb, top), "{family}: {emb}");
|
||||
}
|
||||
@@ -2400,6 +2406,36 @@ fn em_box_first_baseline_follows_base_then_the_typo_box_or_the_cap_height() {
|
||||
assert!(close(emb, 16.0), "{emb}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn em_box_first_baseline_in_a_latin_font_with_a_one_em_typo_box_follows_the_cap_height() {
|
||||
use designcraft_doc::FirstBaseline;
|
||||
// DejaVu Serif: 2048 units per em, typo ascender 1556 and descender -492 (one em), cap height
|
||||
// 1493. Read from the system fonts; the test skips where it isn't installed.
|
||||
let Some(bytes) = designcraft_fonts::system_font_dirs().iter().find_map(|dir| {
|
||||
["truetype/dejavu/DejaVuSerif.ttf", "dejavu/DejaVuSerif.ttf", "DejaVuSerif.ttf"].iter().find_map(|p| std::fs::read(dir.join(p)).ok())
|
||||
}) else {
|
||||
eprintln!("skipped: DejaVu Serif isn't installed");
|
||||
return;
|
||||
};
|
||||
let db = designcraft_fonts::FontDb::global();
|
||||
db.add_font(bytes);
|
||||
let face = db.face("DejaVu Serif", "Book");
|
||||
assert_eq!(face.family, "DejaVu Serif");
|
||||
let (mut d, sid, fid) = doc_with("Hxg Hxg", Rect::new(36.0, 36.0, 300.0, 300.0), ParaAttrs::default());
|
||||
let st = d.story_mut(sid).unwrap();
|
||||
let n = st.len();
|
||||
st.format_chars(0..n, |f| {
|
||||
f.over.font_family = Some("DejaVu Serif".into());
|
||||
f.over.font_style = Some("Book".into());
|
||||
f.over.size = Some(10.0);
|
||||
});
|
||||
d.item_mut(fid).unwrap().text_frame_mut().unwrap().options.first_baseline = FirstBaseline::EmboxHeight;
|
||||
let first = compose_story(&d, sid, &ComposeOptions::default()).frames[0].lines[0].baseline - 36.0;
|
||||
// 0.5 em + cap height / 2, not the 1556-unit typo ascender (7.60 pt).
|
||||
let expected = 5.0 + 1493.0 / 2048.0 * 5.0;
|
||||
assert!((first - expected).abs() < 0.01, "{first}, expected {expected}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascent_first_baseline_falls_back_to_the_ascent_without_a_usable_typo_ascender() {
|
||||
let db = designcraft_fonts::FontDb::global();
|
||||
|
||||
@@ -324,8 +324,9 @@ pub enum FirstBaseline {
|
||||
Leading,
|
||||
XHeight,
|
||||
Fixed,
|
||||
/// The top of the font's em box: its `BASE` em box; else its OS/2 typo ascender when the typo
|
||||
/// ascender and descender span one em; else half an em above half the cap height.
|
||||
/// The top of the font's em box: its `BASE` em box; else, for a CJK font, its OS/2 typo
|
||||
/// ascender when the typo ascender and descender span one em; else half an em above half the
|
||||
/// cap height.
|
||||
EmboxHeight,
|
||||
}
|
||||
|
||||
|
||||
@@ -277,9 +277,9 @@ impl FontFace {
|
||||
crate::vertical::VMetrics::new(self).origin(GlyphId::new(gid))
|
||||
}
|
||||
/// The ideographic em box (top, bottom) in font units, y up, one em tall (Japanese fonts:
|
||||
/// typically 880, -120): `BASE` `idtp`/`ideo`; else from the OS/2 typo ascender when the typo
|
||||
/// ascender and descender span one em; else centred on half the cap height (OS/2, else the
|
||||
/// `H` glyph's top); else 0.88 em above the baseline.
|
||||
/// typically 880, -120): `BASE` `idtp`/`ideo`; else, for a CJK font, from the OS/2 typo
|
||||
/// ascender when the typo ascender and descender span one em; else centred on half the cap
|
||||
/// height (OS/2, else the `H` glyph's top); else 0.88 em above the baseline.
|
||||
pub fn em_box(&self) -> (f64, f64) {
|
||||
self.em.get_or_init(|| crate::vertical::em_box(self)).0
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use skrifa::raw::TableProvider;
|
||||
|
||||
use super::*;
|
||||
use crate::testing::{font_mapping, font_with, with_base, with_table_len, with_table_u16, with_vmtx, with_vorg};
|
||||
use crate::testing::{font_mapping, font_with, with_base, with_code_pages, with_table_len, with_table_u16, with_vmtx, with_vorg};
|
||||
use crate::{ShapeContext, feature, shape_vertical, shape_with_context};
|
||||
|
||||
/// Text in Japanese.
|
||||
@@ -116,11 +116,23 @@ fn fonts_without_vmtx_use_the_em_box() {
|
||||
let face = face_of(with_table_len(font_with("No Base", &['一']).unwrap(), b"BASE", 0).unwrap());
|
||||
assert_eq!(face.em_box(), (830.0, -170.0));
|
||||
assert_eq!(face.declared_em_box(), Some((830.0, -170.0)));
|
||||
// Typo ascender and descender one em apart: the typo ascender is the top.
|
||||
let typo = with_table_u16(font_with("Typo Em", &['一']).unwrap(), b"OS/2", 68, 860).unwrap();
|
||||
let typo = with_table_u16(typo, b"OS/2", 70, u16::from_be_bytes((-140i16).to_be_bytes())).unwrap();
|
||||
let face = face_of(with_table_len(typo, b"BASE", 0).unwrap());
|
||||
// Typo ascender and descender one em apart: a CJK font (JIS code page) tops at the typo
|
||||
// ascender; a Latin one (Source Sans 3's own code pages) still centres on the cap height.
|
||||
let typo = |family: &str, code_pages: Option<u32>| {
|
||||
let font = with_table_u16(font_with(family, &['一']).unwrap(), b"OS/2", 68, 860).unwrap();
|
||||
let font = with_table_u16(font, b"OS/2", 70, u16::from_be_bytes((-140i16).to_be_bytes())).unwrap();
|
||||
let font = match code_pages {
|
||||
Some(r1) => with_code_pages(font, r1, 0).unwrap(),
|
||||
None => font,
|
||||
};
|
||||
face_of(with_table_len(font, b"BASE", 0).unwrap())
|
||||
};
|
||||
let face = typo("Typo Em CJK", Some(1 << 17));
|
||||
assert_ne!(face.menu_group().0, crate::FontGroup::Western);
|
||||
assert_eq!(face.em_box(), (860.0, -140.0));
|
||||
let face = typo("Typo Em Latin", None);
|
||||
assert_eq!(face.menu_group().0, crate::FontGroup::Western);
|
||||
assert_eq!(face.em_box(), (830.0, -170.0));
|
||||
// Without OS/2: centred on half the H glyph's height (656).
|
||||
let no_os2 = |font: Vec<u8>| face_of(with_table_len(with_table_len(font, b"BASE", 0).unwrap(), b"OS/2", 0).unwrap());
|
||||
let face = no_os2(font_mapping("No OS2", &[('一', 'X'), ('H', 'H')]).unwrap());
|
||||
|
||||
@@ -54,9 +54,10 @@ impl<'a> VMetrics<'a> {
|
||||
}
|
||||
|
||||
/// The ideographic em box (top, bottom) in font units, y up, one em tall: `BASE` `idtp`/`ideo`;
|
||||
/// else topped by the OS/2 typo ascender when the typo ascender and descender span exactly one em;
|
||||
/// else centred on half the cap height (OS/2 `sCapHeight`, else the `H` glyph's top); else 0.88 em
|
||||
/// above the baseline. The flag is false for that last fallback.
|
||||
/// else, for a CJK font (its [`crate::FontGroup`] isn't Western), topped by the OS/2 typo ascender
|
||||
/// when the typo ascender and descender span exactly one em; else centred on half the cap height
|
||||
/// (OS/2 `sCapHeight`, else the `H` glyph's top); else 0.88 em above the baseline. The flag is
|
||||
/// false for that last fallback.
|
||||
pub(crate) fn em_box(face: &FontFace) -> ((f64, f64), bool) {
|
||||
let upem = face.upem;
|
||||
let fallback = ((upem * 0.88, upem * -0.12), false);
|
||||
@@ -64,7 +65,9 @@ pub(crate) fn em_box(face: &FontFace) -> ((f64, f64), bool) {
|
||||
if let Some(b) = base_em_box(&font, upem) {
|
||||
return (b, true);
|
||||
}
|
||||
let typo_top = font.os2().ok().and_then(|t| {
|
||||
// Latin fonts with a one-em typo box (DejaVu) still centre on the cap height.
|
||||
let cjk = face.menu_group().0 != crate::FontGroup::Western;
|
||||
let typo_top = font.os2().ok().filter(|_| cjk).and_then(|t| {
|
||||
let (asc, desc) = (f64::from(t.s_typo_ascender()), f64::from(t.s_typo_descender()));
|
||||
(asc - desc == upem && asc > 0.0 && asc <= upem).then_some(asc)
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user