Merge PR #2872: fix(ui): Korean (Hangul) file names and tab titles draw as boxes

This commit is contained in:
aiongg
2026-10-22 01:49:37 +00:00
4 changed files with 73 additions and 6 deletions
+5
View File
@@ -20,6 +20,11 @@
[people.YouseifUS]
sync_github_name = true
[people.brian-moon]
real_name = "Il Min Brian Moon"
display_name = "Il Min Brian Moon"
sync_github_name = false
[people.echelon]
real_name = "Brandon Thomas"
display_name = "Brandon Thomas"
+38 -3
View File
@@ -3,8 +3,9 @@
//! The embedded faces (Inter, egui's defaults, craft-fonts) come first in every family; this one
//! only draws characters none of them has, such as an Arabic file name in a build without
//! craft-fonts, or the Simplified Chinese interface in a build without a `Hans` face (#826). A
//! few more installed faces, [`devanagari_fallback`], [`telugu_fallback`], [`sinhala_fallback`]
//! and [`tamil_fallback`], draw Hindi, Telugu, Sinhala and Tamil when one is installed. It is
//! few more installed faces, [`devanagari_fallback`], [`telugu_fallback`], [`sinhala_fallback`],
//! [`tamil_fallback`] and [`hangul_fallback`], draw Hindi, Telugu, Sinhala, Tamil and Korean when
//! one is installed. It is
//! read at runtime and never embedded or shipped (AGENTS.md §1.4), and `PDFCRAFT_SYSTEM_FONTS=0`
//! turns it off (published screenshots do).
@@ -32,6 +33,9 @@ const TELUGU_PROBE: char = '\u{0C15}';
const SINHALA_PROBE: char = '\u{0D9A}';
/// Tamil letter ka (க): the face must have it to cover Tamil.
const TAMIL_PROBE: char = '\u{0B95}';
/// Hangul syllable hih (힣, U+D7A3), the last of the 11,172 modern syllables: a face with it has
/// the whole block. Droid Sans Fallback, for one, has 가 but few other syllables.
const HANGUL_PROBE: char = '\u{D7A3}';
/// 欢 (U+6B22): Simplified only, absent from the Japanese craft-fonts faces.
const HANS_PROBE: char = '\u{6B22}';
/// 說 (U+8AAA): Traditional only, absent from the Japanese craft-fonts faces.
@@ -103,6 +107,13 @@ pub fn tamil_fallback() -> Option<Arc<FontData>> {
CACHE.get_or_init(|| script_fallback(&tamil_candidates(), TAMIL_PROBE)).clone()
}
/// The installed fallback face for Hangul (Korean), read once. `None` when it is turned off or no
/// candidate is installed; the interface then draws what the embedded faces can.
pub fn hangul_fallback() -> Option<Arc<FontData>> {
static CACHE: OnceLock<Option<Arc<FontData>>> = OnceLock::new();
CACHE.get_or_init(|| script_fallback(&hangul_candidates(), HANGUL_PROBE)).clone()
}
fn script_fallback(candidates: &[PathBuf], probe: char) -> Option<Arc<FontData>> {
if std::env::var_os("PDFCRAFT_SYSTEM_FONTS").is_some_and(|v| v == "0") {
return None;
@@ -288,6 +299,30 @@ fn han_candidates(traditional: bool) -> Vec<PathBuf> {
}
}
/// Well-known locations of Korean faces, best first. Noto CJK and Source Han are left out, as for
/// Chinese (AGENTS.md §1.1).
fn hangul_candidates() -> Vec<PathBuf> {
if cfg!(windows) {
["malgun.ttf", "gulim.ttc", "batang.ttc"].iter().map(|f| windows_fonts().join(f)).collect()
} else if cfg!(target_os = "macos") {
["/System/Library/Fonts/AppleSDGothicNeo.ttc", "/System/Library/Fonts/Supplemental/AppleGothic.ttf", "/Library/Fonts/Arial Unicode.ttf"]
.iter()
.map(PathBuf::from)
.collect()
} else {
unix_fonts(&[
"truetype/nanum/NanumGothic.ttf",
"nanum/NanumGothic.ttf",
"naver-nanum/NanumGothic.ttf",
"nhn-nanum/NanumGothic.ttf",
"truetype/unfonts-core/UnDotum.ttf",
"un-core/UnDotum.ttf",
"truetype/baekmuk/dotum.ttf",
"baekmuk-ttf-dotum-fonts/dotum.ttf",
])
}
}
fn windows_fonts() -> PathBuf {
let dir = std::env::var_os("WINDIR").or_else(|| std::env::var_os("SystemRoot")).map_or_else(|| PathBuf::from(r"C:\Windows"), PathBuf::from);
dir.join("Fonts")
@@ -411,7 +446,7 @@ mod tests {
#[test]
fn script_candidates_are_absolute_font_files() {
for list in [devanagari_candidates(), telugu_candidates(), sinhala_candidates(), tamil_candidates()] {
for list in [devanagari_candidates(), telugu_candidates(), sinhala_candidates(), tamil_candidates(), hangul_candidates()] {
assert!(!list.is_empty());
assert!(list.iter().all(|p| p.is_absolute() || cfg!(windows)), "{list:?}");
assert!(list.iter().all(|p| p.extension().is_some_and(|e| e == "ttf" || e == "ttc")), "{list:?}");
+6 -3
View File
@@ -224,6 +224,8 @@ pub const SYSTEM_TELUGU_FALLBACK: &str = "system-telugu-fallback";
pub const SYSTEM_SINHALA_FALLBACK: &str = "system-sinhala-fallback";
/// The name of the installed Tamil face [`installed_font_definitions`] may add.
pub const SYSTEM_TAMIL_FALLBACK: &str = "system-tamil-fallback";
/// The name of the installed Hangul (Korean) face [`installed_font_definitions`] may add.
pub const SYSTEM_HANGUL_FALLBACK: &str = "system-hangul-fallback";
/// What [`install_fonts_for`] installs: [`font_definitions_for`], then, on desktop, one face
/// already installed on this machine. It only draws characters no embedded face has.
@@ -236,9 +238,9 @@ pub const SYSTEM_TAMIL_FALLBACK: &str = "system-tamil-fallback";
/// - Otherwise it is the last fallback of every family (an Arabic file name in a build without
/// craft-fonts).
///
/// Installed Devanagari, Telugu, Sinhala and Tamil faces, when there are, end every family just
/// before that one, so Hindi, Telugu (#1531), Sinhala and Tamil (#1688) text, such as a file
/// name, draws in a build without such faces.
/// Installed Devanagari, Telugu, Sinhala, Tamil and Hangul faces, when there are, end every family
/// just before that one, so Hindi, Telugu (#1531), Sinhala and Tamil (#1688) and Korean text, such
/// as a file name or a tab title, draws in a build without such faces.
///
/// `PDFCRAFT_SYSTEM_FONTS=0` leaves them out.
pub fn installed_font_definitions(cjk: CjkPreference) -> FontDefinitions {
@@ -253,6 +255,7 @@ pub fn installed_font_definitions(cjk: CjkPreference) -> FontDefinitions {
(SYSTEM_TELUGU_FALLBACK, crate::system_fonts::telugu_fallback()),
(SYSTEM_SINHALA_FALLBACK, crate::system_fonts::sinhala_fallback()),
(SYSTEM_TAMIL_FALLBACK, crate::system_fonts::tamil_fallback()),
(SYSTEM_HANGUL_FALLBACK, crate::system_fonts::hangul_fallback()),
] {
let Some(data) = data else { continue };
fonts.font_data.insert(name.to_owned(), data);
+24
View File
@@ -337,3 +337,27 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() {
}
}
}
/// On a machine with an installed Korean font, the installed definitions include it and Hangul has
/// glyphs, so a Korean file name in a tab title is not boxes. macOS always ships Apple SD Gothic
/// Neo and Windows Malgun Gothic, so there the test never skips.
#[test]
fn system_fallback_covers_hangul() {
const HANGUL: &str = "성가대 찬양곡 목록";
assert!(!theme::font_definitions().font_data.contains_key(theme::SYSTEM_HANGUL_FALLBACK));
let defs = theme::installed_font_definitions(theme::CjkPreference::Default);
let off = std::env::var_os("PDFCRAFT_SYSTEM_FONTS").is_some_and(|v| v == "0");
if !defs.font_data.contains_key(theme::SYSTEM_HANGUL_FALLBACK) && (off || cfg!(all(unix, not(target_os = "macos")))) {
eprintln!("skipping system_fallback_covers_hangul: no installed Korean fallback font (or PDFCRAFT_SYSTEM_FONTS=0)");
assert!(defs.families.values().all(|stack| !stack.iter().any(|n| n == theme::SYSTEM_HANGUL_FALLBACK)));
return;
}
for (family, stack) in &defs.families {
assert_eq!(stack.iter().filter(|n| *n == theme::SYSTEM_HANGUL_FALLBACK).count(), 1, "{family:?}: {stack:?}");
}
let mut fonts = Fonts::new(TextOptions::default(), defs);
for id in families() {
assert!(fonts.has_glyphs(&id, HANGUL), "{id:?} lacks {HANGUL}");
}
assert!(layout_widths(&mut fonts, HANGUL).iter().all(|w| w.is_finite() && *w > 0.0));
}