From 01db64abaf619bd56157670eb32da07e3db34a6f Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 27 Oct 2026 16:59:16 +0000 Subject: [PATCH] :bug: Keep Pinyin readings on one baseline over horizontal text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each ruby reading attached its lowest ink to the base, which suits kana but not Latin letters: the descender of "y" lifted "yǒu" above "liáo" over 友聊, and under ruby moved with ascenders and tone marks. Western letters in a reading now take the font's descent (over) or cap height (under) instead of their ink, so Pinyin and romaji readings share one baseline and descenders stay clear of the base. Upright glyphs keep the ink edge. AI-assisted-by: claude-opus-5-5 --- .../src/shapes/text_horizontal/ruby.rs | 15 ++++- .../src/shapes/text_horizontal/tests.rs | 64 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/render-wasm/src/shapes/text_horizontal/ruby.rs b/render-wasm/src/shapes/text_horizontal/ruby.rs index 4869c9f3a0..60942d6142 100644 --- a/render-wasm/src/shapes/text_horizontal/ruby.rs +++ b/render-wasm/src/shapes/text_horizontal/ruby.rs @@ -384,11 +384,22 @@ pub(super) fn horizontal_ruby_pieces( /// Ink edge shared by the glyphs of one ruby line: the lowest ink bottom for /// over ruby, the highest ink top for under ruby. One edge for the whole line /// keeps marks whose ink sits mid-height, such as ー and ・, on the line's -/// baseline instead of pulling each of them onto the base. +/// baseline instead of pulling each of them onto the base. Western letters +/// (Pinyin, romaji) take the font's descent and cap height instead of their +/// ink, so every reading keeps one baseline whatever its descenders, ascenders +/// or tone marks. pub(super) fn ruby_line_ink_edge(shaped: &[ShapedRun], glyphs: &[RubyGlyphRef], over: bool) -> f32 { - let edges = glyphs.iter().map(|(run_index, glyph, _, _)| { + let edges = glyphs.iter().map(|(run_index, glyph, _, western)| { let run = &shaped[*run_index]; let (_, metrics) = run.font.metrics(); + if *western { + if over { + return metrics.descent; + } + if metrics.cap_height > 0.0 { + return -metrics.cap_height; + } + } let fallback = if over { metrics.descent } else { diff --git a/render-wasm/src/shapes/text_horizontal/tests.rs b/render-wasm/src/shapes/text_horizontal/tests.rs index 4b9d30047f..1cf4136f76 100644 --- a/render-wasm/src/shapes/text_horizontal/tests.rs +++ b/render-wasm/src/shapes/text_horizontal/tests.rs @@ -1341,6 +1341,70 @@ fn horizontal_ruby_glyphs_share_one_baseline() { ); } +#[test] +fn latin_readings_share_one_baseline_whatever_their_letters() { + // Pinyin over 友聊: the descender of "yu" lifted it above "lia" when each + // reading attached its own ink bottom to the base. + use crate::shapes::text_vertical::test_support::{provider, EM}; + use crate::shapes::{FontFamily, FontStyle}; + use skia_safe::FontMgr; + + let font_provider = provider(include_bytes!("../../fonts/sourcesanspro-regular.ttf")); + let family = format!("{}", FontFamily::new(Uuid::nil(), 400, FontStyle::Normal)); + let size = EM / 2.0; + let shape = |text: &str| { + shape_segment_with_fallbacks( + text, + size, + &[family.clone()], + &font_provider, + false, + FontFeatures::None, + &FontMgr::from(font_provider.clone()), + "zh-Hans", + ) + }; + let base = skia::Rect::from_xywh(0.0, 0.0, EM, EM); + for side in [RubySide::Over, RubySide::Under] { + let baseline = |text: &str| { + let span = TextSpan { + ruby: text.to_string(), + ruby_side: side, + font_size: EM, + ..make_span("友", 0.0) + }; + let shaped = shape(text); + let glyphs = ruby_glyph_refs(text, &shaped, size); + assert!(glyphs.iter().all(|(_, _, _, western)| *western)); + let line = RubyLine { + span: &span, + shaped: &shaped, + glyphs: &glyphs, + base, + leading_gap: 0.0, + room: (0.0, 0.0), + }; + (line.baseline(0.0), shaped) + }; + let (descender, shaped) = baseline("yu"); + let (plain, _) = baseline("lia"); + assert!( + (descender - plain).abs() < 0.01, + "{side:?}: yu at {descender}, lia at {plain}" + ); + if side == RubySide::Over { + let mut bounds = [skia::Rect::default()]; + shaped[0] + .font + .get_bounds(&shaped[0].glyphs[..1], &mut bounds, None); + assert!( + descender + bounds[0].bottom <= horizontal_annotation_over_top(base, EM) + 0.01, + "the descender of y reaches into the base" + ); + } + } +} + #[test] fn emphasis_position_overrides_the_language_and_stacks_on_its_own_side() { let placement = super::emphasis::HorizontalEmphasisPlacement {