mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 18:46:57 -06:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
662fca6e97 | ||
|
|
f2db8f30e7 | ||
|
|
ce5a88b785 | ||
|
|
3e62501373 |
@@ -79,6 +79,10 @@ pub struct FontFace {
|
|||||||
/// values).
|
/// values).
|
||||||
pub cap_height: f64,
|
pub cap_height: f64,
|
||||||
pub x_height: f64,
|
pub x_height: f64,
|
||||||
|
/// Where the baseline sits in a line, as a share of the line's height from its top: the
|
||||||
|
/// Windows ascent over the Windows ascent plus descent (OS/2 `usWinAscent`/`usWinDescent`),
|
||||||
|
/// the split PowerPoint uses (computed by `line_split`).
|
||||||
|
pub line_split: f64,
|
||||||
pub shaper: harfrust::ShaperData,
|
pub shaper: harfrust::ShaperData,
|
||||||
/// Variable fonts: the named instance's axis settings (user units; empty = default instance),
|
/// Variable fonts: the named instance's axis settings (user units; empty = default instance),
|
||||||
/// the normalised location and the shaper's view of it.
|
/// the normalised location and the shaper's view of it.
|
||||||
@@ -436,6 +440,23 @@ pub(crate) fn last_resort_face() -> Arc<FontFace> {
|
|||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The share of a line above the baseline (`FontFace::line_split`): OS/2 `usWinAscent` over
|
||||||
|
/// `usWinAscent + usWinDescent`, else the ascent over ascent plus descent. Fonts whose Windows
|
||||||
|
/// extent is huge (Gabriola, Cambria Math: over 1.5 em) get 0.81, where PowerPoint puts their
|
||||||
|
/// baseline (observed).
|
||||||
|
fn line_split(f: &skrifa::FontRef, ascent: f64, descent: f64, upem: f64) -> f64 {
|
||||||
|
use skrifa::raw::TableProvider;
|
||||||
|
let (a, d) = match f.os2() {
|
||||||
|
Ok(os2) if os2.us_win_ascent() > 0 => (os2.us_win_ascent() as f64, os2.us_win_descent() as f64),
|
||||||
|
_ => (ascent, descent),
|
||||||
|
};
|
||||||
|
let total = a + d;
|
||||||
|
if !total.is_finite() || total <= 0.0 || a < 0.0 || d < 0.0 {
|
||||||
|
return 0.8;
|
||||||
|
}
|
||||||
|
if total > upem * 1.5 { 0.81 } else { (a / total).clamp(0.5, 1.0) }
|
||||||
|
}
|
||||||
|
|
||||||
fn make_face(bytes: FontBytes, index: u32, family: String, style: String, coords: Vec<([u8; 4], f32)>) -> Option<FontFace> {
|
fn make_face(bytes: FontBytes, index: u32, family: String, style: String, coords: Vec<([u8; 4], f32)>) -> Option<FontFace> {
|
||||||
let data: &[u8] = match &bytes {
|
let data: &[u8] = match &bytes {
|
||||||
FontBytes::Static(b) => b,
|
FontBytes::Static(b) => b,
|
||||||
@@ -467,6 +488,7 @@ fn make_face(bytes: FontBytes, index: u32, family: String, style: String, coords
|
|||||||
descent: -(m.descent as f64),
|
descent: -(m.descent as f64),
|
||||||
cap_height: m.cap_height.map(|v| v as f64).filter(|v| *v > 0.0).unwrap_or(m.ascent as f64 * 0.72),
|
cap_height: m.cap_height.map(|v| v as f64).filter(|v| *v > 0.0).unwrap_or(m.ascent as f64 * 0.72),
|
||||||
x_height: m.x_height.map(|v| v as f64).filter(|v| *v > 0.0).unwrap_or(m.ascent as f64 * 0.5),
|
x_height: m.x_height.map(|v| v as f64).filter(|v| *v > 0.0).unwrap_or(m.ascent as f64 * 0.5),
|
||||||
|
line_split: line_split(&f, m.ascent as f64, -(m.descent as f64), m.units_per_em.max(1) as f64),
|
||||||
shaper,
|
shaper,
|
||||||
coords,
|
coords,
|
||||||
location,
|
location,
|
||||||
|
|||||||
@@ -143,6 +143,17 @@ mod tests {
|
|||||||
assert!(f.upem > 0.0);
|
assert!(f.upem > 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_split_follows_the_windows_ascent_and_descent() {
|
||||||
|
use skrifa::raw::TableProvider;
|
||||||
|
let f = fontdb::last_resort_face();
|
||||||
|
let font = skrifa::FontRef::new(f.data()).unwrap();
|
||||||
|
let os2 = font.os2().unwrap();
|
||||||
|
let (a, d) = (os2.us_win_ascent() as f64, os2.us_win_descent() as f64);
|
||||||
|
assert!((f.line_split - a / (a + d)).abs() < 1e-9, "{} vs {}", f.line_split, a / (a + d));
|
||||||
|
assert!(f.line_split > 0.5 && f.line_split < 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn some_face_always_resolves() {
|
fn some_face_always_resolves() {
|
||||||
let db = FontDb::with_font_dirs(Vec::new());
|
let db = FontDb::with_font_dirs(Vec::new());
|
||||||
|
|||||||
+13
-2
@@ -702,9 +702,20 @@ fn measure_str(face: &FontFace, size: f64, s: &str) -> (f64, Vec<(u32, f64)>) {
|
|||||||
(x, out)
|
(x, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Single line spacing as a multiple of the font size. PowerPoint spaces single-spaced lines 1.2
|
||||||
|
/// times the size apart in every font (observed on twenty Latin, CJK, Thai and Indic fonts at 18
|
||||||
|
/// and 40 pt), whatever the font's own ascent and descent.
|
||||||
|
///
|
||||||
|
/// Caveat: this was measured on one Windows install of desktop PowerPoint. It may be a Windows or
|
||||||
|
/// machine setting; PowerPoint on macOS or the web may space lines by the font's own metrics.
|
||||||
|
/// Keep it a single tunable value, and re-measure on the other platform before changing it.
|
||||||
|
const LINE_PITCH: f64 = 1.2;
|
||||||
|
|
||||||
|
/// A line's height above and below the baseline at `size`: [`LINE_PITCH`] × size, split where
|
||||||
|
/// the font's Windows ascent and descent put the baseline.
|
||||||
fn line_metrics(face: &FontFace, size: f64) -> (f64, f64) {
|
fn line_metrics(face: &FontFace, size: f64) -> (f64, f64) {
|
||||||
let k = size / face.upem.max(1.0);
|
let h = size * LINE_PITCH;
|
||||||
(face.ascent * k, face.descent * k)
|
(h * face.line_split, h * (1.0 - face.line_split))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Break opportunities: allowed[i] = a line may start at char i.
|
/// Break opportunities: allowed[i] = a line may start at char i.
|
||||||
|
|||||||
@@ -489,3 +489,34 @@ fn an_ordinary_hang_still_puts_the_text_at_the_margin() {
|
|||||||
assert!((first - rest).abs() < 0.5, "first {first} rest {rest}");
|
assert!((first - rest).abs() < 0.5, "first {first} rest {rest}");
|
||||||
assert!((rest - bx.unwrap() - 36.0).abs() < 0.5);
|
assert!((rest - bx.unwrap() - 36.0).abs() < 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_spaced_lines_are_1_2_times_the_size_apart_whatever_the_font() {
|
||||||
|
// PowerPoint spaces single-spaced lines 1.2 × the font size apart in every font, with the
|
||||||
|
// baseline where the font's Windows ascent/descent split puts it (observed on twenty fonts).
|
||||||
|
for size in [18.0, 40.0] {
|
||||||
|
let mut run = Run::new("HHH");
|
||||||
|
run.props.size = Some(size);
|
||||||
|
let (p, mut sh) = bidi_setup(vec![run], 2000.0, false, Align::Left);
|
||||||
|
let t = sh.text.as_mut().unwrap();
|
||||||
|
t.body.autofit = Some(AutoFit::None);
|
||||||
|
let second = t.paragraphs[0].clone();
|
||||||
|
t.paragraphs.push(second.clone());
|
||||||
|
t.paragraphs.push(second);
|
||||||
|
for para in &mut t.paragraphs {
|
||||||
|
para.props.line_spacing = Some(Spacing::Pct(1.0));
|
||||||
|
para.props.space_before = Some(Spacing::Pts(0.0));
|
||||||
|
para.props.space_after = Some(Spacing::Pts(0.0));
|
||||||
|
}
|
||||||
|
let l = lay(&p, &sh, 2000.0, 600.0);
|
||||||
|
assert_eq!(l.lines.len(), 3);
|
||||||
|
// Each line is 1.2 × the size tall (the face is not compared across lines: another
|
||||||
|
// test's thread can load the family between them, see #252).
|
||||||
|
for line in &l.lines {
|
||||||
|
assert!((line.bottom - line.top - size * LINE_PITCH).abs() < 1e-6, "{size}: {}", line.bottom - line.top);
|
||||||
|
}
|
||||||
|
let face = &l.runs[0].face;
|
||||||
|
let first = &l.lines[0];
|
||||||
|
assert!((first.baseline - first.top - size * LINE_PITCH * face.line_split).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user