Merge pull request #1185 from storytold/fix/thai-font-fallback

Type: Thai falls back to a font made for Thai
This commit is contained in:
BFlat
2026-10-11 19:28:58 +07:00
committed by GitHub
4 changed files with 88 additions and 2 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+64
View File
@@ -391,6 +391,36 @@ struct SysFallback {
misses: std::collections::HashSet<char>,
}
/// Fonts made for Thai, tried first (when installed) for Thai the font in use lacks: Windows
/// (Leelawadee UI; Tahoma before Windows 8), macOS (Thonburi, Sukhumvit Set) and Linux (Noto,
/// TLWG).
pub const THAI_FAMILIES: &[&str] = &[
"Leelawadee UI",
"Leelawadee",
"Tahoma",
"Thonburi",
"Sukhumvit Set",
"Noto Sans Thai",
"Noto Sans Thai UI",
"Noto Sans Thai Looped",
"Waree",
"Loma",
"Garuda",
];
/// The families tried first for character `c`, in a script whose text only fonts made for it set
/// well (Thai, Lao, Khmer, Myanmar); none for other characters.
fn script_families(c: char) -> &'static [&'static str] {
use unicode_script::{Script, UnicodeScript};
match c.script() {
Script::Thai => THAI_FAMILIES,
Script::Lao => &["Lao UI", "Leelawadee UI", "Lao Sangam MN", "Noto Sans Lao", "Noto Sans Lao UI", "Phetsarath OT"],
Script::Khmer => &["Khmer UI", "Khmer Sangam MN", "Noto Sans Khmer", "Noto Sans Khmer UI", "Khmer OS"],
Script::Myanmar => &["Myanmar Text", "Myanmar Sangam MN", "Noto Sans Myanmar", "Noto Sans Myanmar UI", "Padauk"],
_ => &[],
}
}
/// Families tried (when installed) for characters the loaded fonts lack: CJK, symbols, emoji.
#[cfg(not(target_arch = "wasm32"))]
const SYSTEM_FALLBACKS: &[&str] = &[
@@ -1917,6 +1947,18 @@ impl FontDb {
/// First face (fallback family first, then load order) that covers `c`; on native, system
/// fonts are loaded lazily the first time no loaded face covers a character.
pub(crate) fn fallback_for(&self, c: char, exclude: u32) -> Option<Arc<FontFace>> {
// Thai (Lao, Khmer, Myanmar) takes a font made for it, when one is installed, rather than
// whichever face covering it happened to load first (a display face, say).
let prefer = script_families(c);
if let Some(f) = self.family_fallback(c, exclude, prefer) {
return Some(f);
}
#[cfg(not(target_arch = "wasm32"))]
if self.load_families(c, prefer)
&& let Some(f) = self.family_fallback(c, exclude, prefer)
{
return Some(f);
}
if let Some(f) = self.loaded_fallback(c, exclude) {
return Some(f);
}
@@ -1934,6 +1976,28 @@ impl FontDb {
self.fallback_for(c, 0)
}
/// The loaded face of the first of `families` that covers `c` (upright, nearest Regular).
fn family_fallback(&self, c: char, exclude: u32, families: &[&str]) -> Option<Arc<FontFace>> {
let faces = self.read_faces();
families.iter().find_map(|fam| {
faces
.iter()
.filter(|f| f.id != exclude && f.family.eq_ignore_ascii_case(fam) && f.covers(c))
.min_by_key(|f| (f.italic, (f.weight - 400.0).abs() as i32))
.cloned()
})
}
/// Load the first of the installed `families` not loaded yet that covers `c` (with system
/// fallback on). Returns whether one was loaded.
#[cfg(not(target_arch = "wasm32"))]
fn load_families(&self, c: char, families: &[&str]) -> bool {
if families.is_empty() || !self.sys.lock().unwrap_or_else(|e| e.into_inner()).enabled {
return false;
}
families.iter().any(|fam| !self.is_loaded(fam) && self.load_cataloged(fam) && self.family_fallback(c, 0, &[fam]).is_some())
}
fn loaded_fallback(&self, c: char, exclude: u32) -> Option<Arc<FontFace>> {
let faces = self.read_faces();
let mut order: Vec<&Arc<FontFace>> = faces.iter().filter(|f| f.id != exclude).collect();
+20
View File
@@ -569,3 +569,23 @@ fn family_display_name_follows_show_font_names_in_english() {
let face = db.face("ヒラギノ角ゴシック", "Regular").unwrap();
assert_eq!(face.family, "Hiragino Sans");
}
/// Thai typed in a font without Thai takes an installed font made for Thai (Leelawadee UI,
/// Thonburi, Noto Sans Thai…), whatever other face covering Thai loaded first; Latin beside it keeps
/// the font chosen. Skipped where none of those families is installed.
#[test]
fn thai_falls_back_to_a_font_made_for_thai() {
let db = FontDb::global();
let Some(thai) = crate::fontdb::THAI_FAMILIES.iter().find(|f| db.has_family(f)) else {
eprintln!("skipped: no Thai font installed");
return;
};
let st = vectorcraft_doc::CharStyle { size: 20.0, ..Default::default() };
let l = layout(db, &TextObject::point(kurbo::Point::ZERO, "Thai ภาษาไทย", st));
let family = |g: &PositionedGlyph| db.face_by_id(g.font_id).map(|f| f.family.clone()).unwrap_or_default();
let (latin, thai_glyphs) = l.glyphs.split_at(5);
assert!(latin.iter().all(|g| family(g) == FALLBACK_FAMILY), "{:?}", latin.iter().map(family).collect::<Vec<_>>());
let first = crate::fontdb::THAI_FAMILIES.iter().find(|f| thai_glyphs.iter().all(|g| family(g).eq_ignore_ascii_case(f)));
assert!(first.is_some(), "Thai drawn in {:?}; {thai} is installed", thai_glyphs.iter().map(family).collect::<Vec<_>>());
assert!(thai_glyphs.iter().all(|g| g.gid != 0), "no .notdef");
}
+3 -1
View File
@@ -1,6 +1,6 @@
# Type parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Proportional Metrics in vertical type, `vpal`, #966; document-point character controls after object/group scaling, #1034) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966) · **Target:** Adobe Illustrator 2026 (30.x)
Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in
[target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the
@@ -27,6 +27,7 @@ against Illustrator's public documentation; **estimated** unless noted.
| Preserve font sizes while scaling a group | missing | Separate enhancement; scaling still scales text with the other artwork. |
| OpenType panel, Glyphs panel, alternates | done | Highlight Alternate Glyphs preference unused (#394) |
| Font menu: samples, live preview, filters, favourites; font matching by any name; Find Font | done | #339 |
| Missing glyphs set in an installed font made for their script (Thai, Lao, Khmer, Myanmar first; then any font that has them) | done | Thai: Leelawadee UI, Tahoma, Thonburi, Sukhumvit Set, Noto Sans Thai, TLWG |
| Variable fonts | partial | named instances as styles; axis sliders missing |
| Colour fonts (OpenType SVG / COLR) | partial | |
| Every-line and Single-line composers | done | tuned by eye, never compared side by side |
@@ -51,6 +52,7 @@ against Illustrator's public documentation; **estimated** unless noted.
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Thai (Lao, Khmer, Myanmar) missing from the font in use falls back to an installed font made for the script instead of whichever face covering it loaded first; score unchanged (estimated) |
| 2026-10-11 | minor | Proportional Metrics done: `palt` in horizontal type, `vpal` in vertical type (#966, #1082); ruby and manual tate-chu-yoko still missing |
| 2026-10-10 | minor | #1034: transformed character controls and regression coverage; preserving group font sizes remains a separate enhancement. Scores/hours unchanged at this rounding. |
| 2026-10-10 | major | First checklist, split out of the parity table's Type core and Type advanced rows |