diff --git a/contributors/people.toml b/contributors/people.toml index f7cb39292..b258f4d3c 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -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" diff --git a/crates/ui-egui/src/system_fonts.rs b/crates/ui-egui/src/system_fonts.rs index 606fa923d..18692de9d 100644 --- a/crates/ui-egui/src/system_fonts.rs +++ b/crates/ui-egui/src/system_fonts.rs @@ -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> { 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> { + static CACHE: OnceLock>> = OnceLock::new(); + CACHE.get_or_init(|| script_fallback(&hangul_candidates(), HANGUL_PROBE)).clone() +} + fn script_fallback(candidates: &[PathBuf], probe: char) -> Option> { 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 { } } +/// 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 { + 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:?}"); diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 3f43a03dc..b064b2907 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -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); diff --git a/crates/ui-egui/tests/fonts.rs b/crates/ui-egui/tests/fonts.rs index 9cfa36c8a..5157c86c5 100644 --- a/crates/ui-egui/tests/fonts.rs +++ b/crates/ui-egui/tests/fonts.rs @@ -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)); +}