fix(host): render Korean (Hangul) text in chrome

Hangul codepoints were routed to the shared CJK typeface, which is
resolved from a Han ideograph (a Chinese font with no Hangul glyphs),
so 한국어 painted as blank .notdef boxes in the locale picker while
Chinese/Japanese/Hindi/Thai/Vietnamese rendered fine. Add a dedicated
`is_hangul_codepoint` split + a cached `korean_typeface` (resolved from
'한' → Apple SD Gothic Neo / Noto Sans KR) and route Hangul there in
both typeface-resolution paths.
This commit is contained in:
Kayshen-X 2026-05-29 21:40:37 +08:00
parent 1d06be7f0d
commit e2f0e4e1a0
3 changed files with 66 additions and 2 deletions

View file

@ -144,6 +144,11 @@ pub struct NativeBackend {
typeface_tried: bool,
cjk_typeface: Option<skia_safe::Typeface>,
cjk_typeface_tried: bool,
/// Korean (Hangul) face — resolved separately from `cjk_typeface`
/// because the Han-ideograph match returns a Chinese font without
/// Hangul coverage.
korean_typeface: Option<skia_safe::Typeface>,
korean_typeface_tried: bool,
/// Default-family per-codepoint typeface cache, keyed by
/// `(codepoint, weight)`.
char_typeface_cache: std::collections::HashMap<(i32, u16), Option<skia_safe::Typeface>>,
@ -221,6 +226,8 @@ impl NativeBackend {
typeface_tried: false,
cjk_typeface: None,
cjk_typeface_tried: false,
korean_typeface: None,
korean_typeface_tried: false,
char_typeface_cache: std::collections::HashMap::new(),
family_typeface_cache: std::collections::HashMap::new(),
image_cache: std::collections::HashMap::new(),
@ -255,7 +262,11 @@ impl NativeBackend {
if c.is_ascii() && weight == 400 {
return self.ensure_typeface().cloned();
}
if font_script::is_east_asian_codepoint(c) {
if font_script::is_hangul_codepoint(c) {
if let Some(tf) = self.ensure_korean_typeface().cloned() {
return Some(tf);
}
} else if font_script::is_east_asian_codepoint(c) {
return self.ensure_cjk_typeface().cloned();
}
let cp = c as i32;
@ -287,7 +298,11 @@ impl NativeBackend {
let Some(primary) = primary_font_family(family) else {
return self.typeface_for_char(c, weight);
};
if font_script::is_east_asian_codepoint(c) {
if font_script::is_hangul_codepoint(c) {
if let Some(tf) = self.ensure_korean_typeface().cloned() {
return Some(tf);
}
} else if font_script::is_east_asian_codepoint(c) {
return self.ensure_cjk_typeface().cloned();
}
let key = (primary.to_string(), c as i32, weight);
@ -358,6 +373,23 @@ impl NativeBackend {
self.cjk_typeface.as_ref()
}
/// Lazy-init the cached Korean (Hangul) typeface, resolved from a
/// Hangul syllable so the OS picks a Hangul-covering face (e.g.
/// Apple SD Gothic Neo / Noto Sans KR) rather than the Chinese
/// font the Han-ideograph match returns.
fn ensure_korean_typeface(&mut self) -> Option<&skia_safe::Typeface> {
if !self.korean_typeface_tried {
self.korean_typeface = self.font_mgr.match_family_style_character(
"",
skia_safe::FontStyle::default(),
&[],
'한' as i32,
);
self.korean_typeface_tried = true;
}
self.korean_typeface.as_ref()
}
/// Convenience constructor for tests and the basic-window demo.
pub fn with_dpi(dpi: f32) -> Self {
Self::new(jian_skia::SkiaBackend::new(), dpi)

View file

@ -1,3 +1,16 @@
/// Hangul (Korean): Jamo, Compatibility Jamo, Jamo Extended-A/B, and
/// the precomposed syllable block. Routed to a dedicated Korean
/// typeface — the shared CJK face is resolved from a Han ideograph
/// (a Chinese font) and does NOT cover Hangul, so without this split
/// Korean text renders as blank `.notdef` boxes.
pub(super) fn is_hangul_codepoint(c: char) -> bool {
let cp = c as u32;
matches!(
cp,
0x1100..=0x11FF | 0x3130..=0x318F | 0xA960..=0xA97F | 0xAC00..=0xD7A3 | 0xD7B0..=0xD7FF
)
}
pub(super) fn is_east_asian_codepoint(c: char) -> bool {
let cp = c as u32;
matches!(

View file

@ -380,3 +380,22 @@ fn image_draw_respects_node_opacity() {
c.b()
);
}
#[test]
fn korean_hangul_resolves_to_a_covering_typeface() {
// Regression: Hangul routed to the shared CJK face (resolved from
// a Chinese ideograph) which lacks Hangul glyphs, so 한국어 painted
// blank. The resolved face for '한' must actually cover '한'.
let mut be = NativeBackend::with_dpi(1.0);
let tf = be.typeface_for_char('한', 400).expect("a typeface for 한");
assert_ne!(
tf.unichar_to_glyph('한' as i32),
0,
"the resolved Hangul face must have a glyph for 한"
);
// Every char of the Korean locale name resolves to a covering face.
for c in "한국어".chars() {
let tf = be.typeface_for_char(c, 400).expect("typeface");
assert_ne!(tf.unichar_to_glyph(c as i32), 0, "missing glyph for {c}");
}
}