From 6b26950bdc7e2f567083375e572ae3143a00a389 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 15 Jul 2026 17:54:52 +0200 Subject: [PATCH] fix emojis --- render-wasm/src/shapes/text_vertical.rs | 73 ++++++++++++++++++++----- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/render-wasm/src/shapes/text_vertical.rs b/render-wasm/src/shapes/text_vertical.rs index 8d2ef5011b..6275c6f72d 100644 --- a/render-wasm/src/shapes/text_vertical.rs +++ b/render-wasm/src/shapes/text_vertical.rs @@ -30,8 +30,8 @@ // Upright CJK <-> rotated alphanumeric boundaries additionally receive the // conventional quarter-em inter-script gap. // -// Deferred to a later phase (documented in the phase handoff): emoji -// overlays, inner shadows and block-axis vertical-align. +// Deferred to a later phase (documented in the phase handoff): PDF/vector +// emoji overlays, inner shadows and block-axis vertical-align. use skia_safe::{ self as skia, @@ -47,6 +47,7 @@ use skia_safe::{ use crate::get_render_state; use crate::math::Rect; +use crate::render::DEFAULT_EMOJI_FONT; use crate::shapes::japanese::{classify, pair_rule, JapaneseClass}; use crate::shapes::kinsoku::{forbidden_at_line_end, forbidden_at_line_start}; use crate::shapes::{ @@ -56,11 +57,33 @@ use crate::shapes::{ }; use crate::utils::get_fallback_fonts; +/// Emoji ranges recognized by the frontend font-loader. Emoji use their +/// intrinsic upright presentation in vertical flow instead of rotating like +/// Latin text under `text-orientation: mixed`. +fn is_emoji_char(c: char) -> bool { + matches!(u32::from(c), + 0x2300..=0x23FF + | 0x2600..=0x27BF + | 0x2B00..=0x2BFF + | 0x1F000..=0x1FAFF + ) +} + +fn span_font_families(span: &TextSpan, fallback_families: &[String]) -> Vec { + let mut families = vec![ + format!("{}", span.font_family), + DEFAULT_EMOJI_FONT.to_string(), + ]; + families.extend(fallback_families.iter().cloned()); + families +} + /// Characters that stay upright in vertical flow: kana, kanji, CJK -/// punctuation and full-width forms. Everything else rotates sideways +/// punctuation, full-width forms and emoji. Everything else rotates sideways /// under `text-orientation: mixed`. pub fn is_upright_char(c: char) -> bool { - matches!(u32::from(c), + is_emoji_char(c) + || matches!(u32::from(c), 0x2E80..=0x2FDF // CJK radicals, Kangxi radicals | 0x3000..=0x303F // CJK symbols and punctuation | 0x3040..=0x30FF // hiragana, katakana @@ -75,7 +98,7 @@ pub fn is_upright_char(c: char) -> bool { | 0xFF00..=0xFF60 // full-width forms | 0xFFE0..=0xFFE6 // full-width signs | 0x20000..=0x2FA1F // CJK extensions B..F - ) + ) } /// Characters whose horizontal glyph needs a vertical alternate. `vert` / @@ -139,8 +162,18 @@ pub fn segment_by_orientation(text: &str, orientation: TextOrientation) -> Vec last.text.push(c), + Some(last) + if last.upright == upright + && last.text.chars().next_back().is_some_and(is_emoji_char) == emoji => + { + last.text.push(c) + } _ => segments.push(Segment { text: c.to_string(), utf16_start: utf16_offset, @@ -1681,8 +1714,7 @@ pub fn layout_vertical( // provider does no per-character fallback, so pick the first // family that covers the whole segment (falling back to one that // covers its first character). - let mut families = vec![format!("{}", span.font_family)]; - families.extend(fallback_families.iter().cloned()); + let families = span_font_families(span, fallback_families); let orientation = span.text_orientation; let letter_spacing = span.letter_spacing; @@ -2360,8 +2392,7 @@ pub fn layout_vertical( let ruby_font_size = span.font_size * span.ruby_size.scale(); let probe = ruby_text.chars().next().unwrap_or(' '); - let mut families = vec![format!("{}", span.font_family)]; - families.extend(fallback_families.iter().cloned()); + let families = span_font_families(span, fallback_families); let typeface = families .iter() .filter_map(|family| { @@ -2487,8 +2518,7 @@ pub fn layout_vertical( continue; }; let mark_font_size = span.font_size * EMPHASIS_FONT_SCALE; - let mut families = vec![format!("{}", span.font_family)]; - families.extend(fallback_families.iter().cloned()); + let families = span_font_families(span, fallback_families); let typeface = families .iter() .filter_map(|family| { @@ -3113,8 +3143,7 @@ pub fn paint_horizontal_ruby( continue; } let ruby_font_size = span.font_size * span.ruby_size.scale(); - let mut families = vec![format!("{}", span.font_family)]; - families.extend(fallback_families.iter().cloned()); + let families = span_font_families(span, &fallback_families); let paint = merge_fills(&span.fills, bounds); let shifted_range = span_range; let rects = laid_out.get_rects_for_range( @@ -4130,6 +4159,22 @@ mod tests { assert!(!is_upright_char('.')); } + #[test] + fn emoji_stay_upright_in_vertical_text() { + for emoji in ['πŸ˜†', 'πŸ’¦', 'πŸ™‡'] { + assert!(is_upright_char(emoji)); + } + + let segments = segment_by_orientation("δΉ…πŸ˜†ε…ƒπŸ’¦πŸ™‡", TextOrientation::Mixed); + assert_eq!(segments.len(), 4); + assert!(segments.iter().all(|segment| segment.upright)); + assert_eq!(segments[1].text, "πŸ˜†"); + assert_eq!(segments[3].text, "πŸ’¦πŸ™‡"); + + let families = span_font_families(&make_span("πŸ˜†"), &[]); + assert_eq!(families[1], DEFAULT_EMOJI_FONT); + } + #[test] fn transformed_japanese_punctuation_has_a_rotated_font_fallback() { for character in [