fix(fonts): fall back to a font made for Malayalam (part of #628)
Malayalam took the first installed file mapping it: on macOS Arial Unicode MS, which has no Malayalam shaping, so conjuncts like ക്ഷ fell apart. Fonts made for Malayalam are tried first, loaded on demand through the CJK chain lookup.
This commit is contained in:
parent
c3859c828b
commit
de31bd5647
|
|
@ -1835,6 +1835,23 @@ fn cjk_fallback_follows_the_language() {
|
|||
assert_eq!(Some(han.family.clone()), db.fallback_for('直', primary.id(), None).map(|f| f.family.clone()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malayalam_falls_back_to_a_font_that_forms_its_conjuncts() {
|
||||
// Run against the installed fonts: on macOS the first file mapping Malayalam is Arial
|
||||
// Unicode MS, which leaves ക്ഷ as three glyphs.
|
||||
let db = designcraft_fonts::FontDb::global();
|
||||
if !["Malayalam Sangam MN", "Malayalam MN", "Nirmala UI", "Noto Sans Malayalam"].iter().any(|f| db.has_family(f)) {
|
||||
return;
|
||||
}
|
||||
let text = "ക്ഷ";
|
||||
let (mut d, sid, _) = doc_with(text, Rect::new(0.0, 0.0, 300.0, 100.0), ParaAttrs::default());
|
||||
d.settings.glyph_fallback = true;
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let glyphs: Vec<_> = all_lines(&cs).iter().flat_map(|l| l.glyphs.iter()).map(|g| (g.face.family.clone(), g.gid)).collect();
|
||||
assert_eq!(glyphs.len(), 1, "one conjunct: {glyphs:?}");
|
||||
assert_ne!(glyphs[0].1, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_glyphs_are_the_fonts_box_unless_fallback_is_on() {
|
||||
use designcraft_fonts::testing::font_with;
|
||||
|
|
|
|||
|
|
@ -435,6 +435,29 @@ const KOREAN_FALLBACKS: &[&str] = &[
|
|||
"Source Han Sans K",
|
||||
];
|
||||
|
||||
/// Fonts made for Malayalam (macOS, Windows, Noto, Linux). A font that only maps the characters
|
||||
/// (Arial Unicode MS, first by path on macOS) sets them without conjuncts or vowel signs.
|
||||
const MALAYALAM_FALLBACKS: &[&str] = &[
|
||||
"Malayalam Sangam MN",
|
||||
"Malayalam MN",
|
||||
"Nirmala UI",
|
||||
"Kartika",
|
||||
"Noto Sans Malayalam",
|
||||
"Noto Serif Malayalam",
|
||||
"Manjari",
|
||||
"Rachana",
|
||||
"Meera",
|
||||
"Lohit Malayalam",
|
||||
];
|
||||
|
||||
/// The fallback chain of a script that only fonts made for it set well, whatever the language.
|
||||
fn script_chain(c: char) -> Option<&'static [&'static str]> {
|
||||
match c as u32 {
|
||||
0x0D00..=0x0D7F => Some(MALAYALAM_FALLBACKS),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The Japanese fallback chain: the craft-fonts Japanese faces' families in document order
|
||||
/// (Mincho first; none without craft-fonts), then [`JAPANESE_FALLBACKS`].
|
||||
fn japanese_chain() -> &'static [&'static str] {
|
||||
|
|
@ -1375,12 +1398,12 @@ impl ScopedFonts<'_> {
|
|||
}
|
||||
|
||||
/// A face other than `exclude` that covers `c` in `language` (a BCP 47 tag, `None` when
|
||||
/// unknown). CJK characters try their language's chain first (see [`cjk_chain`]); then the
|
||||
/// first face (fallback family first, then load order, the document's fonts last) that covers
|
||||
/// `c`. On native, system fonts are cataloged and loaded lazily the first time no loaded face
|
||||
/// covers a character.
|
||||
/// unknown). CJK characters try their language's chain first (see [`cjk_chain`]), Malayalam
|
||||
/// the fonts made for it (see [`script_chain`]); then the first face (fallback family first,
|
||||
/// then load order, the document's fonts last) that covers `c`. On native, system fonts are
|
||||
/// cataloged and loaded lazily the first time no loaded face covers a character.
|
||||
pub fn fallback_for(&self, c: char, exclude: u32, language: Option<&str>) -> Option<Arc<FontFace>> {
|
||||
if let Some(chain) = cjk_chain(c, language)
|
||||
if let Some(chain) = cjk_chain(c, language).or_else(|| script_chain(c))
|
||||
&& let Some(f) = chain.iter().find_map(|family| self.chain_face(family, c, exclude))
|
||||
{
|
||||
return Some(f);
|
||||
|
|
|
|||
|
|
@ -127,3 +127,33 @@ fn chain_fonts_load_from_the_font_folders_when_first_needed() {
|
|||
assert_eq!(family(&db, HAN, Some("ko")).as_deref(), Some(first_loaded("Other CJK")));
|
||||
assert!(!db.is_loaded("AppleMyungjo"));
|
||||
}
|
||||
|
||||
const KA: char = 'ക';
|
||||
const VIRAMA: char = '\u{0D4D}';
|
||||
const AA: char = '\u{0D3E}';
|
||||
|
||||
#[test]
|
||||
fn malayalam_takes_a_font_made_for_it() {
|
||||
// Arial Unicode MS maps Malayalam but can't shape it (no conjuncts); loaded first, it won.
|
||||
let db = db_with(&[("Arial Unicode MS", &[KA, VIRAMA, AA]), ("Malayalam Sangam MN", &[KA, VIRAMA, AA])]);
|
||||
for c in [KA, VIRAMA, AA] {
|
||||
for language in [None, Some("en-US"), Some("ml")] {
|
||||
assert_eq!(family(&db, c, language).as_deref(), Some("Malayalam Sangam MN"), "{c:?} {language:?}");
|
||||
}
|
||||
}
|
||||
// Past the Malayalam fonts, the language-blind search as before.
|
||||
let sangam = db.face("Malayalam Sangam MN", "Regular");
|
||||
assert_eq!(db.fallback_for(KA, sangam.id(), None).map(|f| f.family.clone()).as_deref(), Some("Arial Unicode MS"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malayalam_fonts_load_from_the_font_folders_when_first_needed() {
|
||||
let dir = std::env::temp_dir().join(format!("dc-malayalam-fallback-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("noto.ttf"), font_with("Noto Sans Malayalam", &[KA]).unwrap()).unwrap();
|
||||
let db = FontDb::with_font_dirs(vec![dir]);
|
||||
db.add_font(font_with("Arial Unicode MS", &[KA]).unwrap());
|
||||
assert!(!db.is_loaded("Noto Sans Malayalam"));
|
||||
assert_eq!(family(&db, KA, None).as_deref(), Some("Noto Sans Malayalam"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue