From b248dffdff3c60260585df3eb71e8ceeaa11586f Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Tue, 13 Oct 2026 01:04:22 +0200 Subject: [PATCH] Hebrew file names: draw them with an installed Hebrew face when the build has none (part of #586) --- crates/ui-egui/src/system_fonts.rs | 44 ++++++++++++++++++++++++++---- crates/ui-egui/src/theme.rs | 9 ++++-- crates/ui-egui/tests/fonts.rs | 24 ++++++++++++++++ 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/crates/ui-egui/src/system_fonts.rs b/crates/ui-egui/src/system_fonts.rs index 606fa923d..8531ccd67 100644 --- a/crates/ui-egui/src/system_fonts.rs +++ b/crates/ui-egui/src/system_fonts.rs @@ -3,10 +3,10 @@ //! 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 -//! read at runtime and never embedded or shipped (AGENTS.md §1.4), and `PDFCRAFT_SYSTEM_FONTS=0` -//! turns it off (published screenshots do). +//! few more installed faces, [`devanagari_fallback`], [`telugu_fallback`], [`sinhala_fallback`], +//! [`tamil_fallback`] and [`hebrew_fallback`], draw Hindi, Telugu, Sinhala, Tamil and Hebrew 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). use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock}; @@ -32,6 +32,8 @@ 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}'; +/// Hebrew letter alef: the face must have it to cover Hebrew. +const HEBREW_PROBE: char = '\u{05D0}'; /// 欢 (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 +105,13 @@ pub fn tamil_fallback() -> Option> { CACHE.get_or_init(|| script_fallback(&tamil_candidates(), TAMIL_PROBE)).clone() } +/// The installed fallback face for Hebrew, read once. `None` when it is turned off or no +/// candidate is installed; the interface then draws what the embedded faces can. +pub fn hebrew_fallback() -> Option> { + static CACHE: OnceLock>> = OnceLock::new(); + CACHE.get_or_init(|| script_fallback(&hebrew_candidates(), HEBREW_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; @@ -227,6 +236,30 @@ fn tamil_candidates() -> Vec { } } +/// Well-known locations of Hebrew faces, best first. The general fallback does not cover it: +/// SF Arabic, Geeza Pro and Noto Sans Arabic have no Hebrew. +fn hebrew_candidates() -> Vec { + if cfg!(windows) { + ["segoeui.ttf", "arial.ttf", "tahoma.ttf", "david.ttf"].iter().map(|f| windows_fonts().join(f)).collect() + } else if cfg!(target_os = "macos") { + ["/System/Library/Fonts/SFHebrew.ttf", "/System/Library/Fonts/ArialHB.ttc", "/Library/Fonts/Arial Unicode.ttf"] + .iter() + .map(PathBuf::from) + .collect() + } else { + unix_fonts(&[ + "google-noto/NotoSansHebrew-Regular.ttf", + "google-noto-vf/NotoSansHebrew[wght].ttf", + "truetype/noto/NotoSansHebrew-Regular.ttf", + "noto/NotoSansHebrew-Regular.ttf", + "truetype/dejavu/DejaVuSans.ttf", + "TTF/DejaVuSans.ttf", + "dejavu/DejaVuSans.ttf", + "dejavu-sans-fonts/DejaVuSans.ttf", + ]) + } +} + /// Well-known locations of faces with broad script coverage, best first. fn candidates(need: Need) -> Vec { match need { @@ -407,11 +440,12 @@ mod tests { assert!(script_fallback(&candidates, DEVANAGARI_PROBE).is_none()); assert!(script_fallback(&candidates, SINHALA_PROBE).is_none()); assert!(script_fallback(&candidates, TAMIL_PROBE).is_none()); + assert!(script_fallback(&candidates, HEBREW_PROBE).is_none()); } #[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(), hebrew_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 d28a7b658..783737847 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 Hebrew face [`installed_font_definitions`] may add. +pub const SYSTEM_HEBREW_FALLBACK: &str = "system-hebrew-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 Hebrew 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, and Hebrew file names (#586) draw 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_HEBREW_FALLBACK, crate::system_fonts::hebrew_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 258abe89c..402ad4539 100644 --- a/crates/ui-egui/tests/fonts.rs +++ b/crates/ui-egui/tests/fonts.rs @@ -8,6 +8,7 @@ const JAPANESE: &str = "日本語の文字"; const CHINESE: &str = "简体中文欢迎"; const ARABIC: &str = "واحد اثنين"; const TELUGU: &str = "తెలుగు ఫైల్"; +const HEBREW: &str = "ייפוי כח.pdf"; fn families() -> Vec { vec![FontId::proportional(13.0), FontId::monospace(13.0), theme::medium(13.0), theme::semibold(17.0)] @@ -219,6 +220,29 @@ fn system_fallback_covers_sinhala_and_tamil() { } } +/// On a machine with an installed Hebrew font, the installed definitions include it and a +/// Hebrew file name has glyphs in the document tab and the recent files (#586). macOS always +/// ships Arial Hebrew and SF Hebrew, so there the test never skips. +#[test] +fn system_fallback_covers_hebrew() { + assert!(!theme::font_definitions().font_data.contains_key(theme::SYSTEM_HEBREW_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_HEBREW_FALLBACK) && (off || !cfg!(target_os = "macos")) { + eprintln!("skipping system_fallback_covers_hebrew: no installed Hebrew fallback font (or PDFCRAFT_SYSTEM_FONTS=0)"); + assert!(defs.families.values().all(|stack| !stack.iter().any(|n| n == theme::SYSTEM_HEBREW_FALLBACK))); + return; + } + for (family, stack) in &defs.families { + assert_eq!(stack.iter().filter(|n| *n == theme::SYSTEM_HEBREW_FALLBACK).count(), 1, "{family:?}: {stack:?}"); + } + let mut fonts = Fonts::new(TextOptions::default(), defs); + for id in families() { + assert!(fonts.has_glyphs(&id, HEBREW), "{id:?} lacks {HEBREW}"); + } + assert!(layout_widths(&mut fonts, HEBREW).iter().all(|w| w.is_finite() && *w > 0.0)); +} + /// The Han characters of a catalog's translations. fn catalog_han(catalog: &str) -> String { let mut han: Vec = catalog