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:
Brandon Thomas
2026-10-13 06:48:43 -04:00
co-authored by Claude Opus 5.5
parent f3f959cabf
commit 9e2f7b96dc
2 changed files with 47 additions and 3 deletions
+15 -3
View File
@@ -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, &params, |ch| if upper { ch.to_uppercase().next().unwrap_or(ch) } else { ch });
let glyphs = deckcraft_fonts::shape_range(face, text, b0..b1, &params, |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);
+32
View File
@@ -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");
}
}