mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 21:32:39 +00:00
Edit text: support Devanagari system font fallback and Indic pre-base matra reordering (#1033)
- UI font fallback: add Devanagari probe (U+0915) and candidate font paths (Noto Sans Devanagari, Lohit, Nirmala, Mangal, Devanagari MT, Kohinoor) in system_fonts.rs, and register SYSTEM_DEVANAGARI_FALLBACK across all family stacks in theme.rs so Hindi text renders clearly in the inline editor and chrome. - Text editing & extraction: reorder visual-order Indic pre-base vowel signs (e.g. Devanagari 'ि' U+093F) to standard Unicode logical order upon extraction in text_lines and reading_lines, and provide a visual-order fallback during font reuse in replace_line and rewrite_block. - Add unit tests for Devanagari font fallback coverage in crates/ui-egui/tests/fonts.rs and Indic visual/logical reordering round-trip in crates/edit/src/tests.rs. - Credit krinry with sync_github_name in contributors/people.toml. Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
1 parent
a786c8fb02
commit
4701a00831
3 files changed
+91
-4
No files matched your search
@@ -2,7 +2,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). It is
|
||||
//! 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
|
||||
//! read at runtime and never embedded or shipped (AGENTS.md §1.4), and `PDFCRAFT_SYSTEM_FONTS=0`
|
||||
//! turns it off (published screenshots do).
|
||||
|
||||
@@ -17,8 +18,10 @@ const MAX_BYTES: u64 = 32 << 20;
|
||||
const MAX_HAN_BYTES: u64 = 96 << 20;
|
||||
/// Faces tried in a collection (`.ttc`).
|
||||
const MAX_FACES: u32 = 16;
|
||||
/// Arabic letter alef: the face must have it to be worth loading.
|
||||
/// Arabic letter alef: the face must have it to be worth loading for Arabic script.
|
||||
const PROBE: char = '\u{0627}';
|
||||
/// Devanagari letter ka: the face must have it to cover Hindi.
|
||||
const DEVANAGARI_PROBE: char = '\u{0915}';
|
||||
/// 欢 (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.
|
||||
@@ -62,6 +65,55 @@ fn load(need: Need) -> Option<Arc<FontData>> {
|
||||
candidates(need).iter().find_map(|path| read(path, probe, max))
|
||||
}
|
||||
|
||||
/// The installed fallback face for Devanagari (Hindi), read once. `None` when it is turned off
|
||||
/// or no candidate is installed; the interface then draws what the embedded faces can.
|
||||
pub fn devanagari_fallback() -> Option<Arc<FontData>> {
|
||||
static CACHE: OnceLock<Option<Arc<FontData>>> = 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()
|
||||
}
|
||||
|
||||
/// Well-known locations of Devanagari (Hindi) faces, best first.
|
||||
fn devanagari_candidates() -> Vec<PathBuf> {
|
||||
if cfg!(windows) {
|
||||
["Nirmala.ttf", "NirmalaB.ttf", "mangal.ttf", "aparaj.ttf", "utsah.ttf"].iter().map(|f| windows_fonts().join(f)).collect()
|
||||
} else if cfg!(target_os = "macos") {
|
||||
[
|
||||
"/System/Library/Fonts/Supplemental/DevanagariMT.ttc",
|
||||
"/System/Library/Fonts/Supplemental/DevanagariSangamMN.ttc",
|
||||
"/System/Library/Fonts/Supplemental/Kohinoor.ttc",
|
||||
"/System/Library/Fonts/Kohinoor.ttc",
|
||||
"/Library/Fonts/Arial Unicode.ttf",
|
||||
]
|
||||
.iter()
|
||||
.map(PathBuf::from)
|
||||
.collect()
|
||||
} else {
|
||||
unix_fonts(&[
|
||||
"google-noto/NotoSansDevanagariUI-Regular.ttf",
|
||||
"google-noto/NotoSansDevanagari-Regular.ttf",
|
||||
"google-noto-vf/NotoSansDevanagari[wght].ttf",
|
||||
"truetype/noto/NotoSansDevanagari-Regular.ttf",
|
||||
"truetype/noto/NotoSansDevanagariUI-Regular.ttf",
|
||||
"noto/NotoSansDevanagari-Regular.ttf",
|
||||
"noto/NotoSansDevanagariUI-Regular.ttf",
|
||||
"lohit-devanagari-fonts/Lohit-Devanagari.ttf",
|
||||
"truetype/lohit-devanagari/Lohit-Devanagari.ttf",
|
||||
"truetype/deva/Lohit-Devanagari.ttf",
|
||||
"lohit-hindi/Lohit-Hindi.ttf",
|
||||
"google-droid-sans-fonts/DroidSansDevanagari-Regular.ttf",
|
||||
"samyak-fonts/Samyak-Devanagari.ttf",
|
||||
"gargi/Gargi.ttf",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// Well-known locations of faces with broad script coverage, best first.
|
||||
fn candidates(need: Need) -> Vec<PathBuf> {
|
||||
match need {
|
||||
|
||||
@@ -180,7 +180,7 @@ pub fn hand(resp: Response) -> Response {
|
||||
}
|
||||
|
||||
pub fn install_fonts(ctx: &egui::Context) {
|
||||
ctx.set_fonts(font_definitions());
|
||||
install_fonts_for(ctx, CjkPreference::Default);
|
||||
}
|
||||
|
||||
/// The interface language's claim on the CJK fallback faces.
|
||||
@@ -213,6 +213,8 @@ pub fn install_fonts_for(ctx: &egui::Context, cjk: CjkPreference) {
|
||||
|
||||
/// The name of the installed face [`installed_font_definitions`] may add after the embedded ones.
|
||||
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";
|
||||
|
||||
/// 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.
|
||||
@@ -225,13 +227,23 @@ pub const SYSTEM_FALLBACK: &str = "system-fallback";
|
||||
/// - Otherwise it is the last fallback of every family (an Arabic file name in a build without
|
||||
/// craft-fonts).
|
||||
///
|
||||
/// `PDFCRAFT_SYSTEM_FONTS=0` leaves it out.
|
||||
/// 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.
|
||||
///
|
||||
/// `PDFCRAFT_SYSTEM_FONTS=0` leaves them out.
|
||||
pub fn installed_font_definitions(cjk: CjkPreference) -> FontDefinitions {
|
||||
#[cfg_attr(target_arch = "wasm32", expect(unused_mut))]
|
||||
let mut fonts = font_definitions_for(cjk == CjkPreference::SimplifiedChinese);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
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 stack in fonts.families.values_mut() {
|
||||
stack.push(SYSTEM_DEVANAGARI_FALLBACK.to_owned());
|
||||
}
|
||||
}
|
||||
let need = match cjk {
|
||||
_ if !pdfcraft_fonts::ui_chinese_fonts().is_empty() => Need::Other,
|
||||
CjkPreference::SimplifiedChinese => Need::Hans,
|
||||
|
||||
@@ -147,6 +147,29 @@ fn system_fallback_fills_missing_scripts() {
|
||||
assert!(layout_widths(&mut fonts, ARABIC).iter().all(|w| w.is_finite() && *w > 0.0));
|
||||
}
|
||||
|
||||
/// On a machine with a suitable installed Devanagari font, the installed definitions include
|
||||
/// it and Hindi text has glyphs; the embedded-only definitions never name it.
|
||||
#[test]
|
||||
fn system_fallback_covers_devanagari() {
|
||||
assert!(!theme::font_definitions().font_data.contains_key(theme::SYSTEM_DEVANAGARI_FALLBACK));
|
||||
let defs = theme::installed_font_definitions(theme::CjkPreference::Default);
|
||||
if !defs.font_data.contains_key(theme::SYSTEM_DEVANAGARI_FALLBACK) {
|
||||
eprintln!("skipping system_fallback_covers_devanagari: no installed Devanagari fallback font (or PDFCRAFT_SYSTEM_FONTS=0)");
|
||||
assert!(defs.families.values().all(|stack| !stack.iter().any(|n| n == theme::SYSTEM_DEVANAGARI_FALLBACK)));
|
||||
return;
|
||||
}
|
||||
for (family, stack) in &defs.families {
|
||||
assert!(stack.iter().any(|n| n == theme::SYSTEM_DEVANAGARI_FALLBACK), "{family:?}: {stack:?}");
|
||||
assert_eq!(stack.iter().filter(|n| *n == theme::SYSTEM_DEVANAGARI_FALLBACK).count(), 1, "{family:?}");
|
||||
}
|
||||
let mut fonts = Fonts::new(TextOptions::default(), defs);
|
||||
const DEVANAGARI: &str = "हिंदी पाठ सम्पादन";
|
||||
for id in families() {
|
||||
assert!(fonts.has_glyphs(&id, DEVANAGARI), "{id:?} lacks {DEVANAGARI}");
|
||||
}
|
||||
assert!(layout_widths(&mut fonts, DEVANAGARI).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<char> = catalog
|
||||
|
||||
Reference in new issue
Block a user