From 705b688f2b07038d50c0cc6fed8fd4c42ee915de Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 15:48:12 -0800 Subject: [PATCH] Type: Insert Aki keeps a widened glyph's own width and an optical pair's letter A glyph that justification widens to fill the line keeps its Insert Aki before it at its width, and its own width, which ligature carets stand in, now starts where the glyph is drawn. Optical kerning measures a pair from the letter of a cluster rather than from a combining mark whose advance holds the cluster's Aki. The SVG test also checks centered and right-aligned lines with Insert Aki. --- crates/svg/tests/it/text_layout.rs | 9 +++++++-- crates/text/src/layout.rs | 15 ++++++++++++++- crates/text/src/layout/flow.rs | 12 ++++++------ crates/text/src/shape.rs | 3 ++- crates/text/src/tests/aki.rs | 15 +++++++++++++++ 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/crates/svg/tests/it/text_layout.rs b/crates/svg/tests/it/text_layout.rs index 534d24ebcc..25d4d27d68 100644 --- a/crates/svg/tests/it/text_layout.rs +++ b/crates/svg/tests/it/text_layout.rs @@ -164,14 +164,19 @@ fn the_space_before_the_first_character_on_a_path_is_its_dx() { } /// Insert Aki: each character the layout moves by its Aki starts a `` where the character is -/// drawn, so a reader draws the text where the canvas does, with fewer ``s asked for too. -/// On a path the type is outlined. +/// drawn, so a reader draws the text where the canvas does, on left-aligned, centered and +/// right-aligned lines and with fewer ``s asked for too. On a path the type is outlined. #[test] fn insert_aki_places_each_moved_character() { // 250/1000 em of 12 pt before the O (3 pt) and 500/1000 after it (6 pt), then a run with Aki // after every character, and a second line. let spaced = CharStyle { aki_left: 250.0, aki_right: 500.0, ..style(12.0) }; let runs = vec![run("H", style(12.0)), run("O", spaced), run("H ", style(12.0)), run("OHO\nHO", CharStyle { aki_right: 500.0, ..style(12.0) })]; + // A centered or right-aligned line whose characters the Aki moves is placed segment by + // segment, not anchored. + for justify in [Justify::Center, Justify::Right] { + assert_drawn_in_place(&area_doc(runs.clone(), 200.0, justify)); + } let d = area_doc(runs, 200.0, Justify::Left); assert_drawn_in_place(&d); let plain = tspans(&export(&area_doc(vec![run("HOH OHO\nHO", style(12.0))], 200.0, Justify::Left), &ExportOptions::default())).len(); diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 208871b300..cbe7aaf394 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -337,6 +337,19 @@ impl Ctx<'_> { n } + /// Does glyph `g`'s own width start where its placement sets it ([`PositionedGlyph::width_start`])? + /// Not for an inline graphic, nor in vertical type for an upright or tate-chu-yoko glyph, which + /// start their cell at the start of their advance. + fn places_own_width(&self, g: &SGlyph) -> bool { + g.inline.is_none() && !(self.vertical && (g.tcy.is_some() || (takes_cell(g) && stands_upright(g)))) + } + + /// How far glyph `g`'s own width moves when the glyph is drawn `shift` further along its + /// advance: `shift` when its placement sets where that width starts, otherwise 0. + fn own_width_shift(&self, g: &SGlyph, shift: f64) -> f64 { + if self.places_own_width(g) { shift } else { 0.0 } + } + fn emit(&mut self, g: &SGlyph, pre: Affine, origin: Point, angle: f64, advance: f64, line: usize) { // Only the glyph right after an upright glyph can continue its cluster. let cluster = self.upright_cluster.take(); @@ -540,7 +553,7 @@ impl Ctx<'_> { let [a, b, ..] = pre.as_coeffs(); a.hypot(b) }; - let width_start = if g.inline.is_none() && !upright && !(self.vertical && g.tcy.is_some()) { + let width_start = if self.places_own_width(g) { Some((g.kern_shift + g.dx - g.lead) * along).filter(|s| s.is_finite()).unwrap_or(0.0) } else if upright { Some(g.kern_shift * along).filter(|s| s.is_finite()).unwrap_or(0.0) diff --git a/crates/text/src/layout/flow.rs b/crates/text/src/layout/flow.rs index d72dd246ef..21ab10c6e6 100644 --- a/crates/text/src/layout/flow.rs +++ b/crates/text/src/layout/flow.rs @@ -1031,12 +1031,9 @@ fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData advance += kashida.unwrap_or(0.0); let y = line.position.baseline - align_shift(glyph, cx.style_at(glyph.byte).char_align, line_em, cx.vertical); // A glyph scaled to fill the line keeps its Insert Aki before it at its width. - let transform = if scale != 1.0 { - let aki = if glyph.aki_shift.is_finite() { glyph.aki_shift * (1.0 - scale) } else { 0.0 }; - Affine::translate((x + aki, y)) * Affine::scale_non_uniform(scale, 1.0) - } else { - Affine::translate((x, y)) - }; + let aki = if scale != 1.0 && glyph.aki_shift.is_finite() { glyph.aki_shift * (1.0 - scale) } else { 0.0 }; + let transform = + if scale != 1.0 { Affine::translate((x + aki, y)) * Affine::scale_non_uniform(scale, 1.0) } else { Affine::translate((x, y)) }; if glyph_index < paragraph.marker { let at = cx.out.glyphs.len(); marker_glyphs = Some(marker_glyphs.map_or(at..at + 1, |range: Range| range.start.min(at)..range.end.max(at + 1))); @@ -1045,8 +1042,11 @@ fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData } } cx.emit(glyph, transform, Point::new(x, y), 0.0, advance, line_index); + // Its own width starts where the glyph is drawn, with the Aki kept at its width. + let own_shift = cx.own_width_shift(glyph, aki); if let Some(out) = cx.out.glyphs.last_mut() { out.scale = paragraph.flex.get(glyph_index).map_or(1.0, |flex| flex.scale) * scale; + out.width_start += own_shift; } if !leader.is_empty() { // The leader belongs to the tab's glyph: the gap stays one character for the diff --git a/crates/text/src/shape.rs b/crates/text/src/shape.rs index 83ff986bd3..c8c460aa3b 100644 --- a/crates/text/src/shape.rs +++ b/crates/text/src/shape.rs @@ -807,7 +807,8 @@ fn optical_pairs(db: &FontDb, text: &str, glyphs: &mut [SGlyph], runs: &[(Range< let mut cache = HashMap::new(); let mut corrections: HashMap<[u64; 15], Option> = HashMap::new(); let mut references: HashMap> = HashMap::new(); - let shaped_advances: Vec = glyphs.iter().map(|g| g.adv).collect(); + // The Insert Aki a cluster's last glyph holds does not make a mark its base. + let shaped_advances: Vec = glyphs.iter().map(|g| g.adv - g.aki).collect(); for i in 0..glyphs.len().saturating_sub(1) { let Some((a, b)) = glyphs.get(i).zip(glyphs.get(i + 1)) else { continue }; if a.level != b.level { diff --git a/crates/text/src/tests/aki.rs b/crates/text/src/tests/aki.rs index 9eac47e560..eab94cb2b1 100644 --- a/crates/text/src/tests/aki.rs +++ b/crates/text/src/tests/aki.rs @@ -66,6 +66,12 @@ fn optical_kerning_spaces_the_pair_as_without_the_aki() { assert!(near(&moved, &[0.0, 25.0, 25.0]), "{moved:?}"); let moved = moves(&optical, &with(&optical, 0, |s| s.aki_right = 250.0), &[0, 1, 2]); assert!(near(&moved, &[0.0, 25.0, 25.0]), "{moved:?}"); + // A letter with a combining mark: the pair is measured from the letter, not from the mark + // whose advance holds the cluster's Aki. + let marked = point("H\u{301}V", CharStyle { optical_kerning: true, ..style(100.0) }); + let after = "H\u{301}".len(); + let moved = moves(&marked, &with(&marked, 0, |s| s.aki_right = 1000.0), &[0, after]); + assert!(near(&moved, &[0.0, 100.0]), "{moved:?}"); } #[test] @@ -112,6 +118,15 @@ fn justification_does_not_stretch_or_narrow_the_aki() { assert!(near(&moved, &[0.0, 25.0, 25.0]), "{moved:?}"); let moved = moves(&scaled, &with(&scaled, 1, |s| s.aki_left = 250.0), &[1, 2]); assert!(near(&moved, &[25.0, 25.0]), "{moved:?}"); + // A glyph that justification widens to fill the line keeps the Aki before it at its width, + // and its own width, which ligature carets stand in, starts where the glyph is drawn. + let mut filled = area("HOH", style(100.0), Rect::new(0.0, 0.0, 600.0, 400.0), Justify::JustifyAll); + filled.para.justification.glyph_scaling = [100.0, 100.0, 300.0]; + let l = layout(db(), &with(&filled, 1, |s| s.aki_left = 250.0)); + let g = l.glyphs.iter().find(|g| g.byte == 1).unwrap(); + assert!(g.scale > 1.0 + 1e-6, "the glyph is widened: {}", g.scale); + let drawn = g.xf.as_coeffs()[4]; + assert!((g.origin.x + g.width_start - drawn).abs() < 1e-6, "{} {} {drawn}", g.origin.x, g.width_start); let mut spaced = point("H H", style(100.0)); spaced.para.justification.word_spacing = [200.0, 200.0, 200.0]; let moved = moves(&spaced, &with(&spaced, 1, |s| s.aki_right = 250.0), &[0, 2]);