1use std::path::PathBuf;
14use std::sync::Arc;
15
16#[cfg(not(target_arch = "wasm32"))]
17use fontdb::{Database, Family, Query, Source};
18#[cfg(not(target_arch = "wasm32"))]
19use std::collections::HashSet;
20#[cfg(not(target_arch = "wasm32"))]
21use std::sync::OnceLock;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum FontStyle {
26 Sans,
27 Serif,
28}
29
30#[non_exhaustive]
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum FontRegion {
34 Korean,
35 Japanese,
36 SimplifiedChinese,
37 TraditionalChinese,
38 Cyrillic,
39 Latin,
40 Unknown,
41}
42
43#[non_exhaustive]
45#[derive(Clone, Debug)]
46pub enum FontPreset {
47 Latin,
48 Korean,
49 SimplifiedChinese,
50 TraditionalChinese,
51 Japanese,
52 Cyrillic,
53 Custom(Vec<String>),
55}
56
57#[derive(Clone, Debug)]
63pub struct FoundFont {
64 pub family: String,
65 pub key: String,
66 pub source: FoundFontSource,
67}
68
69#[derive(Clone, Debug)]
74pub enum FoundFontSource {
75 Path(PathBuf),
76 Bytes(Arc<[u8]>),
77}
78
79#[cfg(not(target_arch = "wasm32"))]
83pub fn system_locale() -> Option<String> {
84 sys_locale::get_locale()
85}
86
87#[cfg(target_arch = "wasm32")]
88pub fn system_locale() -> Option<String> {
89 web_sys::window()
90 .and_then(|w| w.navigator().language())
91}
92
93pub fn region_from_locale(locale: &str) -> FontRegion {
105 let mut s = locale.trim().to_ascii_lowercase().replace('_', "-");
106 if let Some((head, _)) = s.split_once('.') {
107 s = head.to_string();
108 }
109
110 if s.contains("-cyrl") {
111 return FontRegion::Cyrillic;
112 }
113 if s.contains("-latn") {
114 return FontRegion::Latin;
115 }
116
117 if s.starts_with("ko") {
118 return FontRegion::Korean;
119 }
120 if s.starts_with("ja") {
121 return FontRegion::Japanese;
122 }
123 if s.starts_with("zh") {
124 if s.contains("hant") || s.contains("-tw") || s.contains("-hk") || s.contains("-mo") {
125 return FontRegion::TraditionalChinese;
126 }
127 return FontRegion::SimplifiedChinese;
128 }
129
130 if s.starts_with("ru")
131 || s.starts_with("uk")
132 || s.starts_with("be")
133 || s.starts_with("bg")
134 || s.starts_with("mk")
135 || s.starts_with("sr")
136 || s.starts_with("kk")
137 || s.starts_with("ky")
138 || s.starts_with("tg")
139 || s.starts_with("mn")
140 {
141 return FontRegion::Cyrillic;
142 }
143
144 if s.starts_with("en") || s.starts_with("fr") || s.starts_with("de") {
145 return FontRegion::Latin;
146 }
147
148 FontRegion::Unknown
149}
150
151pub fn presets_for_region(region: FontRegion) -> Vec<FontPreset> {
160 match region {
161 FontRegion::Korean => vec![
162 FontPreset::Korean,
163 FontPreset::Japanese,
164 FontPreset::SimplifiedChinese,
165 FontPreset::TraditionalChinese,
166 FontPreset::Latin,
167 ],
168 FontRegion::Japanese => vec![
169 FontPreset::Japanese,
170 FontPreset::Korean,
171 FontPreset::SimplifiedChinese,
172 FontPreset::TraditionalChinese,
173 FontPreset::Latin,
174 ],
175 FontRegion::SimplifiedChinese => vec![
176 FontPreset::SimplifiedChinese,
177 FontPreset::TraditionalChinese,
178 FontPreset::Korean,
179 FontPreset::Japanese,
180 FontPreset::Latin,
181 ],
182 FontRegion::TraditionalChinese => vec![
183 FontPreset::TraditionalChinese,
184 FontPreset::SimplifiedChinese,
185 FontPreset::Korean,
186 FontPreset::Japanese,
187 FontPreset::Latin,
188 ],
189 FontRegion::Cyrillic => vec![
190 FontPreset::Cyrillic,
191 FontPreset::Latin,
192 FontPreset::Korean,
193 FontPreset::SimplifiedChinese,
194 FontPreset::TraditionalChinese,
195 FontPreset::Japanese,
196 ],
197 FontRegion::Latin | FontRegion::Unknown => vec![
198 FontPreset::Latin,
199 FontPreset::Cyrillic,
200 FontPreset::Korean,
201 FontPreset::SimplifiedChinese,
202 FontPreset::TraditionalChinese,
203 FontPreset::Japanese,
204 ],
205 }
206}
207
208#[cfg(not(target_arch = "wasm32"))]
219pub fn find_from_presets<I>(presets_in_priority: I, style: FontStyle) -> Vec<FoundFont>
220where
221 I: IntoIterator<Item = FontPreset>,
222{
223 let db = font_db();
224
225 let mut targets: Vec<String> = Vec::new();
226 for preset in presets_in_priority {
227 match style {
228 FontStyle::Serif => {
229 targets.extend(preset_targets_serif(&preset));
230 targets.extend(preset_targets_sans(&preset));
231 }
232 FontStyle::Sans => {
233 targets.extend(preset_targets_sans(&preset));
234 }
235 }
236 }
237
238 let mut seen_family = HashSet::<String>::new();
239 let mut out = Vec::<FoundFont>::new();
240
241 for (i, family_name) in targets.into_iter().enumerate() {
242 if !seen_family.insert(family_name.clone()) {
243 continue;
244 }
245
246 if let Some(found) = resolve_one_family(db, &family_name, i) {
247 out.push(found);
248 }
249 }
250
251 out
252}
253
254#[cfg(target_arch = "wasm32")]
255pub fn find_from_presets<I>(_presets_in_priority: I, _style: FontStyle) -> Vec<FoundFont>
256where
257 I: IntoIterator<Item = FontPreset>,
258{
259 vec![]
260}
261
262#[cfg(not(target_arch = "wasm32"))]
271pub fn find_for_locale(locale: &str, style: FontStyle) -> (FontRegion, Vec<FoundFont>) {
272 let region = region_from_locale(locale);
273 let presets = presets_for_region(region);
274 (region, find_from_presets(presets, style))
275}
276
277#[cfg(target_arch = "wasm32")]
278pub fn find_for_locale(locale: &str, _style: FontStyle) -> (FontRegion, Vec<FoundFont>) {
279 (region_from_locale(locale), vec![])
280}
281
282#[cfg(not(target_arch = "wasm32"))]
291pub fn find_for_system_locale(style: FontStyle) -> (Option<String>, FontRegion, Vec<FoundFont>) {
292 let locale = system_locale();
293 let (region, fonts) = match locale.as_deref() {
294 Some(loc) if !loc.trim().is_empty() => find_for_locale(loc, style),
295 _ => {
296 let fallback = "en-US";
297 find_for_locale(fallback, style)
298 }
299 };
300 (locale, region, fonts)
301}
302
303#[cfg(target_arch = "wasm32")]
304pub fn find_for_system_locale(_style: FontStyle) -> (Option<String>, FontRegion, Vec<FoundFont>) {
305 (None, FontRegion::Unknown, vec![])
306}
307
308#[cfg(not(target_arch = "wasm32"))]
309static FONT_DB: OnceLock<Database> = OnceLock::new();
310
311#[cfg(not(target_arch = "wasm32"))]
312fn font_db() -> &'static Database {
313 FONT_DB.get_or_init(|| {
314 let mut db = Database::new();
315 db.load_system_fonts();
316 db
317 })
318}
319
320#[cfg(not(target_arch = "wasm32"))]
321fn resolve_one_family(db: &Database, family_name: &str, uniq: usize) -> Option<FoundFont> {
322 let families = [Family::Name(family_name)];
323 let query = Query {
324 families: &families,
325 ..Default::default()
326 };
327
328 let id = db.query(&query)?;
329 let face = db.face(id)?;
330
331 let source = match &face.source {
332 Source::File(path) => FoundFontSource::Path(path.to_path_buf()),
333 Source::Binary(bytes) => {
334 let v: Vec<u8> = bytes.as_ref().as_ref().to_vec();
335 FoundFontSource::Bytes(Arc::from(v.into_boxed_slice()))
336 }
337 _ => return None,
338 };
339
340 let key = format!("system:{}:{}", family_name, uniq);
341
342 Some(FoundFont {
343 family: family_name.to_string(),
344 key,
345 source,
346 })
347}
348
349#[cfg(not(target_arch = "wasm32"))]
350fn preset_targets_sans(p: &FontPreset) -> Vec<String> {
351 match p {
352 FontPreset::Latin => vec![
353 "Noto Sans".into(),
354 "Segoe UI".into(),
355 "Arial".into(),
356 "SF Pro Text".into(),
357 "Helvetica Neue".into(),
358 "DejaVu Sans".into(),
359 "Liberation Sans".into(),
360 "Roboto".into(),
361 ],
362 FontPreset::Korean => vec![
363 "Noto Sans KR".into(),
364 "Noto Sans CJK KR".into(),
365 "Malgun Gothic".into(),
366 "Apple SD Gothic Neo".into(),
367 "NanumGothic".into(),
368 ],
369 FontPreset::SimplifiedChinese => vec![
370 "Noto Sans SC".into(),
371 "Noto Sans CJK SC".into(),
372 "Microsoft YaHei".into(),
373 "PingFang SC".into(),
374 "SimHei".into(),
375 "SimSun".into(),
376 ],
377 FontPreset::TraditionalChinese => vec![
378 "Noto Sans TC".into(),
379 "Noto Sans CJK TC".into(),
380 "Microsoft JhengHei".into(),
381 "PingFang TC".into(),
382 ],
383 FontPreset::Japanese => vec![
384 "Noto Sans JP".into(),
385 "Noto Sans CJK JP".into(),
386 "Yu Gothic".into(),
387 "Hiragino Sans".into(),
388 "Meiryo".into(),
389 ],
390 FontPreset::Cyrillic => vec![
391 "Noto Sans".into(),
392 "DejaVu Sans".into(),
393 "Segoe UI".into(),
394 "Arial".into(),
395 "Tahoma".into(),
396 "Times New Roman".into(),
397 ],
398 FontPreset::Custom(list) => list.clone(),
399 }
400}
401
402#[cfg(not(target_arch = "wasm32"))]
403fn preset_targets_serif(p: &FontPreset) -> Vec<String> {
404 match p {
405 FontPreset::Latin => vec![
406 "Noto Serif".into(),
407 "Times New Roman".into(),
408 "Georgia".into(),
409 "Liberation Serif".into(),
410 "DejaVu Serif".into(),
411 "Times".into(),
412 ],
413 FontPreset::Korean => vec![
414 "Noto Serif KR".into(),
415 "Noto Serif CJK KR".into(),
416 "Batang".into(),
417 "AppleMyungjo".into(),
418 "NanumMyeongjo".into(),
419 ],
420 FontPreset::SimplifiedChinese => vec![
421 "Noto Serif SC".into(),
422 "Noto Serif CJK SC".into(),
423 "Songti SC".into(),
424 "SimSun".into(),
425 ],
426 FontPreset::TraditionalChinese => vec![
427 "Noto Serif TC".into(),
428 "Noto Serif CJK TC".into(),
429 "Songti TC".into(),
430 "PMingLiU".into(),
431 ],
432 FontPreset::Japanese => vec![
433 "Noto Serif JP".into(),
434 "Noto Serif CJK JP".into(),
435 "Yu Mincho".into(),
436 "Hiragino Mincho ProN".into(),
437 "MS Mincho".into(),
438 ],
439 FontPreset::Cyrillic => vec![
440 "Noto Serif".into(),
441 "Times New Roman".into(),
442 "Georgia".into(),
443 "Liberation Serif".into(),
444 "DejaVu Serif".into(),
445 ],
446 FontPreset::Custom(list) => list.clone(),
447 }
448}