From 49f3125e9e657586fc5ef31cebdd342aaba9e3fe Mon Sep 17 00:00:00 2001 From: sanches Date: Tue, 13 Oct 2026 21:58:57 +0300 Subject: [PATCH] Restore the Telugu system-font fallback the i18n rebase dropped The rebase onto main resolved the theme.rs and system_fonts.rs conflicts in favour of the new Hangul code and deleted upstream's Telugu fallback (SYSTEM_TELUGU_FALLBACK, telugu_fallback, telugu_candidates, script_fallback and their tests), while tests/fonts.rs kept the Telugu tests, so the build failed. Keep both: Telugu restored as it is on main, Hangul added alongside it through the same script_fallback helper. --- crates/ui-egui/src/system_fonts.rs | 86 ++++++++++++++++++++++++------ crates/ui-egui/src/theme.rs | 26 ++++----- 2 files changed, 82 insertions(+), 30 deletions(-) diff --git a/crates/ui-egui/src/system_fonts.rs b/crates/ui-egui/src/system_fonts.rs index 6d530e6639..db654e6b00 100644 --- a/crates/ui-egui/src/system_fonts.rs +++ b/crates/ui-egui/src/system_fonts.rs @@ -3,7 +3,8 @@ //! 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 -//! second installed face, [`devanagari_fallback`], draws Hindi when one is installed. It is +//! second, a third and a fourth installed face, [`devanagari_fallback`], [`telugu_fallback`] and +//! [`hangul_fallback`], draw Hindi, Telugu 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). @@ -22,6 +23,8 @@ const MAX_FACES: u32 = 16; const PROBE: char = '\u{0627}'; /// Devanagari letter ka: the face must have it to cover Hindi. const DEVANAGARI_PROBE: char = '\u{0915}'; +/// Telugu letter ka: the face must have it to cover Telugu. +const TELUGU_PROBE: char = '\u{0C15}'; /// Hangul syllable han (한, U+D55C): the face must have it to cover Korean. const HANGUL_PROBE: char = '\u{D55C}'; /// 欢 (U+6B22): Simplified only, absent from the Japanese craft-fonts faces. @@ -71,14 +74,14 @@ fn load(need: Need) -> Option> { /// or no candidate is installed; the interface then draws what the embedded faces can. pub fn devanagari_fallback() -> Option> { static CACHE: OnceLock>> = OnceLock::new(); - CACHE - .get_or_init(|| { - if std::env::var_os("PDFCRAFT_SYSTEM_FONTS").is_some_and(|v| v == "0") { - return None; - } - devanagari_candidates().iter().find_map(|path| read(path, DEVANAGARI_PROBE, MAX_BYTES)) - }) - .clone() + CACHE.get_or_init(|| script_fallback(&devanagari_candidates(), DEVANAGARI_PROBE, MAX_BYTES)).clone() +} + +/// The installed fallback face for Telugu, read once. `None` when it is turned off or no +/// candidate is installed; the interface then draws what the embedded faces can. +pub fn telugu_fallback() -> Option> { + static CACHE: OnceLock>> = OnceLock::new(); + CACHE.get_or_init(|| script_fallback(&telugu_candidates(), TELUGU_PROBE, MAX_BYTES)).clone() } /// The installed fallback face for Hangul (Korean), read once. `None` when it is turned off or no @@ -86,14 +89,14 @@ pub fn devanagari_fallback() -> Option> { /// has Hangul, so Korean text shows replacement boxes). pub fn hangul_fallback() -> Option> { static CACHE: OnceLock>> = OnceLock::new(); - CACHE - .get_or_init(|| { - if std::env::var_os("PDFCRAFT_SYSTEM_FONTS").is_some_and(|v| v == "0") { - return None; - } - hangul_candidates().iter().find_map(|path| read(path, HANGUL_PROBE, MAX_HAN_BYTES)) - }) - .clone() + CACHE.get_or_init(|| script_fallback(&hangul_candidates(), HANGUL_PROBE, MAX_HAN_BYTES)).clone() +} + +fn script_fallback(candidates: &[PathBuf], probe: char, max: u64) -> Option> { + if std::env::var_os("PDFCRAFT_SYSTEM_FONTS").is_some_and(|v| v == "0") { + return None; + } + candidates.iter().find_map(|path| read(path, probe, max)) } /// Well-known locations of Korean system faces, best first. Noto CJK and Source Han are left out @@ -155,6 +158,37 @@ fn devanagari_candidates() -> Vec { } } +/// Well-known locations of Telugu faces, best first. +fn telugu_candidates() -> Vec { + if cfg!(windows) { + ["Nirmala.ttc", "Nirmala.ttf", "gautami.ttf"].iter().map(|f| windows_fonts().join(f)).collect() + } else if cfg!(target_os = "macos") { + [ + "/System/Library/Fonts/KohinoorTelugu.ttc", + "/System/Library/Fonts/Supplemental/Telugu Sangam MN.ttc", + "/System/Library/Fonts/Supplemental/Telugu MN.ttc", + "/Library/Fonts/Arial Unicode.ttf", + ] + .iter() + .map(PathBuf::from) + .collect() + } else { + unix_fonts(&[ + "google-noto/NotoSansTeluguUI-Regular.ttf", + "google-noto/NotoSansTelugu-Regular.ttf", + "google-noto-vf/NotoSansTelugu[wght].ttf", + "google-noto-vf/NotoSansTelugu-UI[wght].ttf", + "truetype/noto/NotoSansTelugu-Regular.ttf", + "truetype/noto/NotoSansTeluguUI-Regular.ttf", + "noto/NotoSansTelugu-Regular.ttf", + "noto/NotoSansTeluguUI-Regular.ttf", + "lohit-telugu-fonts/Lohit-Telugu.ttf", + "truetype/lohit-telugu/Lohit-Telugu.ttf", + "truetype/fonts-telu-extra/Pothana2000.ttf", + ]) + } +} + /// Well-known locations of faces with broad script coverage, best first. fn candidates(need: Need) -> Vec { match need { @@ -285,6 +319,24 @@ mod tests { } } + #[test] + fn a_script_fallback_is_the_first_candidate_with_the_probe() { + let inter = PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/Inter-Regular.ttf")); + let candidates = [PathBuf::from("definitely/not/here.ttf"), inter]; + assert!(script_fallback(&candidates, 'A', MAX_BYTES).is_some()); + assert!(script_fallback(&candidates, TELUGU_PROBE, MAX_BYTES).is_none()); + assert!(script_fallback(&candidates, DEVANAGARI_PROBE, MAX_BYTES).is_none()); + } + + #[test] + fn script_candidates_are_absolute_font_files() { + for list in [devanagari_candidates(), telugu_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:?}"); + } + } + #[test] fn hangul_candidates_are_font_files_without_adobe_designs() { let list = hangul_candidates(); diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index a91fa8cc1f..94c6019be1 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -218,6 +218,8 @@ pub fn install_fonts_for(ctx: &egui::Context, cjk: CjkPreference) { pub const SYSTEM_FALLBACK: &str = "system-fallback"; /// The name of the installed Devanagari (Hindi) face [`installed_font_definitions`] may add. pub const SYSTEM_DEVANAGARI_FALLBACK: &str = "system-devanagari-fallback"; +/// The name of the installed Telugu face [`installed_font_definitions`] may add. +pub const SYSTEM_TELUGU_FALLBACK: &str = "system-telugu-fallback"; /// The name of the installed Hangul (Korean) face [`installed_font_definitions`] may add. pub const SYSTEM_HANGUL_FALLBACK: &str = "system-hangul-fallback"; @@ -232,9 +234,9 @@ pub const SYSTEM_HANGUL_FALLBACK: &str = "system-hangul-fallback"; /// - Otherwise it is the last fallback of every family (an Arabic file name in a build without /// craft-fonts). /// -/// An installed Devanagari face, when there is one, ends every family just before that one, so -/// Hindi text draws in a build without a Devanagari face. An installed Hangul face does the same -/// for Korean: no craft-fonts face has Hangul yet. +/// An installed Devanagari face, an installed Telugu face and an installed Hangul face, when +/// there are, end every family just before that one, so Hindi, Telugu and Korean text draw in a +/// build without such faces (#1531); no craft-fonts face has Hangul yet. /// /// `PDFCRAFT_SYSTEM_FONTS=0` leaves them out. pub fn installed_font_definitions(cjk: CjkPreference) -> FontDefinitions { @@ -244,17 +246,15 @@ pub fn installed_font_definitions(cjk: CjkPreference) -> FontDefinitions { { use crate::system_fonts::{Need, fallback}; // Before the general installed face, which stays the last fallback outside Chinese. - if let Some(data) = crate::system_fonts::devanagari_fallback() { - fonts.font_data.insert(SYSTEM_DEVANAGARI_FALLBACK.to_owned(), data); + for (name, data) in [ + (SYSTEM_DEVANAGARI_FALLBACK, crate::system_fonts::devanagari_fallback()), + (SYSTEM_TELUGU_FALLBACK, crate::system_fonts::telugu_fallback()), + (SYSTEM_HANGUL_FALLBACK, crate::system_fonts::hangul_fallback()), + ] { + let Some(data) = data else { continue }; + fonts.font_data.insert(name.to_owned(), data); for stack in fonts.families.values_mut() { - stack.push(SYSTEM_DEVANAGARI_FALLBACK.to_owned()); - } - } - // No embedded face has Hangul, so Korean text relies on an installed one. - if let Some(data) = crate::system_fonts::hangul_fallback() { - fonts.font_data.insert(SYSTEM_HANGUL_FALLBACK.to_owned(), data); - for stack in fonts.families.values_mut() { - stack.push(SYSTEM_HANGUL_FALLBACK.to_owned()); + stack.push(name.to_owned()); } } let need = match cjk {