mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Text: small capitals draw at their size, and all caps keep characters like "ß"
Small capitals were drawn at the full size of the run's first character: glyph runs grouped
cells by style and face only, so "HxHx" drew every letter at the capital's size on the small
capitals' narrower advances (letters overlapped). A lowercase letter in small caps now starts a
run of its own at 80 % (PowerPoint's small capital "X" is 0.81 of the capital "H" in Arial).
All caps and small caps mapped "ß" to "S" (the first character of "SS"); PowerPoint leaves a
character whose capital is a sequence as it is ("ß", "fi", "ʼn"), and maps the rest one to one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f3f959cabf
commit
9e2f7b96dc
+15
-3
@@ -647,6 +647,17 @@ fn shape_cells(ctx: &Ctx, text: &str, char_style: &[usize], styles: &[Style], fa
|
||||
cells
|
||||
}
|
||||
|
||||
/// A character in capitals (`cap="all"`/`"small"`): its uppercase when that is one character.
|
||||
/// PowerPoint leaves characters whose capital is a sequence as they are: "ß" stays "ß" (not "SS"),
|
||||
/// "fi" and "ʼn" stay.
|
||||
fn caps_char(ch: char) -> char {
|
||||
let mut up = ch.to_uppercase();
|
||||
match (up.next(), up.next()) {
|
||||
(Some(u), None) => u,
|
||||
_ => ch,
|
||||
}
|
||||
}
|
||||
|
||||
/// Size of superscript and subscript text relative to the run's size.
|
||||
const SUB_SUPER: f64 = 0.66;
|
||||
|
||||
@@ -690,7 +701,7 @@ fn shape_item(
|
||||
let (Some(&b0), Some(&b1)) = (byte_of.get(range.start), byte_of.get(range.end)) else { return };
|
||||
let features = shaping_features(cells.get(range.clone()).unwrap_or(&[]), styles, script);
|
||||
let params = ShapeParams { features: &features, rtl, script, language: lang };
|
||||
let glyphs = deckcraft_fonts::shape_range(face, text, b0..b1, ¶ms, |ch| if upper { ch.to_uppercase().next().unwrap_or(ch) } else { ch });
|
||||
let glyphs = deckcraft_fonts::shape_range(face, text, b0..b1, ¶ms, |ch| if upper { caps_char(ch) } else { ch });
|
||||
let upem = face.upem.max(1.0);
|
||||
let scale_of = |cell: &Cell| {
|
||||
let st = styles.get(cell.style);
|
||||
@@ -1420,7 +1431,7 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
|
||||
}
|
||||
// Group visually consecutive cells by (style, face).
|
||||
let mut cur: Option<GlyphRun> = None;
|
||||
let mut cur_key = (usize::MAX, usize::MAX);
|
||||
let mut cur_key = (usize::MAX, usize::MAX, false);
|
||||
for &k in &place.order {
|
||||
let ci = line.start + k;
|
||||
let Some(cell) = p.cells.get(ci) else { continue };
|
||||
@@ -1429,7 +1440,8 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
|
||||
let base_y = line.baseline - st.baseline * st.size;
|
||||
let size = if st.baseline != 0.0 { st.size * SUB_SUPER } else { st.size };
|
||||
let size = if st.caps == Caps::Small && cell.ch.is_lowercase() { size * 0.8 } else { size };
|
||||
let key = (cell.style, cell.face);
|
||||
// Small capitals are drawn smaller than the capitals beside them: a run of their own.
|
||||
let key = (cell.style, cell.face, st.caps == Caps::Small && cell.ch.is_lowercase());
|
||||
if key != cur_key {
|
||||
if let Some(r) = cur.take() {
|
||||
out.runs.push(r);
|
||||
|
||||
@@ -839,3 +839,35 @@ fn symbol_font_characters_draw_in_the_sym_font() {
|
||||
// The letters around it keep the run's font.
|
||||
assert!(families.iter().all(|(f, c)| c.0 == 1 || !f.eq_ignore_ascii_case(symbol)), "{families:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_capitals_draw_smaller_than_the_capitals_beside_them() {
|
||||
let (p, mut sh) = setup("HxHx", 2000.0, 300.0);
|
||||
let para = &mut sh.text.as_mut().unwrap().paragraphs[0];
|
||||
para.props.bullet = Some(Bullet::None);
|
||||
para.runs[0].props.caps = Some(Caps::Small);
|
||||
para.runs[0].props.size = Some(40.0);
|
||||
let l = lay(&p, &sh, 2000.0, 300.0);
|
||||
let sizes: Vec<(usize, f64)> = l.runs.iter().map(|r| (r.glyphs.len(), r.size)).collect();
|
||||
// H at 40 pt, x as a capital at 80 % (four runs: the size changes at every letter).
|
||||
assert_eq!(sizes, vec![(1, 40.0), (1, 32.0), (1, 40.0), (1, 32.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capitals_keep_characters_whose_capital_is_a_sequence() {
|
||||
assert_eq!(caps_char('a'), 'A');
|
||||
assert_eq!(caps_char('dž'), 'DŽ');
|
||||
// PowerPoint draws "ß", "fi" and "ʼn" unchanged in all caps.
|
||||
assert_eq!(caps_char('ß'), 'ß');
|
||||
assert_eq!(caps_char('\u{FB01}'), '\u{FB01}');
|
||||
assert_eq!(caps_char('ʼn'), 'ʼn');
|
||||
let (p, mut sh) = setup("aß", 2000.0, 300.0);
|
||||
let para = &mut sh.text.as_mut().unwrap().paragraphs[0];
|
||||
para.runs[0].props.caps = Some(Caps::All);
|
||||
let l = lay(&p, &sh, 2000.0, 300.0);
|
||||
let r = l.runs.iter().find(|r| !r.cells.is_empty()).unwrap();
|
||||
let face = &r.face;
|
||||
if face.covers('ß') {
|
||||
assert_eq!(r.glyphs[1].0, face.glyph_for('ß'), "ß is not drawn as S");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user