mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
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:
+1
-1
File diff suppressed because one or more lines are too long
@@ -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();
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user