1use std::collections::HashSet;
36use std::path::PathBuf;
37use std::sync::{Arc, OnceLock};
38
39use fontdb::{Database, Family, Query, Source};
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum FontStyle {
44 Sans,
45 Serif,
46}
47
48#[non_exhaustive]
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum FontRegion {
52 Korean,
53 Japanese,
54 SimplifiedChinese,
55 TraditionalChinese,
56 Cyrillic,
57 Latin,
58 Unknown,
59}
60
61#[non_exhaustive]
63#[derive(Clone, Debug)]
64pub enum FontPreset {
65 Latin,
66 Korean,
67 SimplifiedChinese,
68 TraditionalChinese,
69 Japanese,
70 Cyrillic,
71 Custom(Vec<String>),
73}
74
75#[derive(Clone, Debug)]
81pub struct FoundFont {
82 pub family: String,
83 pub key: String,
84 pub source: FoundFontSource,
85}
86
87#[derive(Clone, Debug)]
92pub enum FoundFontSource {
93 Path(PathBuf),
94 Bytes(Arc<[u8]>),
95}
96
97pub fn system_locale() -> Option<String> {
107 sys_locale::get_locale()
108}
109
110pub fn region_from_locale(locale: &str) -> FontRegion {
126 let mut s = locale.trim().to_ascii_lowercase().replace('_', "-");
127 if let Some((head, _)) = s.split_once('.') {
128 s = head.to_string();
129 }
130
131 if s.contains("-cyrl") {
132 return FontRegion::Cyrillic;
133 }
134 if s.contains("-latn") {
135 return FontRegion::Latin;
136 }
137
138 if s.starts_with("ko") {
139 return FontRegion::Korean;
140 }
141 if s.starts_with("ja") {
142 return FontRegion::Japanese;
143 }
144 if s.starts_with("zh") {
145 if s.contains("hant") || s.contains("-tw") || s.contains("-hk") || s.contains("-mo") {
146 return FontRegion::TraditionalChinese;
147 }
148 return FontRegion::SimplifiedChinese;
149 }
150
151 if s.starts_with("ru")
152 || s.starts_with("uk")
153 || s.starts_with("be")
154 || s.starts_with("bg")
155 || s.starts_with("mk")
156 || s.starts_with("sr")
157 || s.starts_with("kk")
158 || s.starts_with("ky")
159 || s.starts_with("tg")
160 || s.starts_with("mn")
161 {
162 return FontRegion::Cyrillic;
163 }
164
165 if s.starts_with("en") || s.starts_with("fr") || s.starts_with("de") {
166 return FontRegion::Latin;
167 }
168
169 FontRegion::Unknown
170}
171
172pub fn presets_for_region(region: FontRegion) -> Vec<FontPreset> {
185 match region {
186 FontRegion::Korean => vec![
187 FontPreset::Korean,
188 FontPreset::Japanese,
189 FontPreset::SimplifiedChinese,
190 FontPreset::TraditionalChinese,
191 FontPreset::Latin,
192 ],
193 FontRegion::Japanese => vec![
194 FontPreset::Japanese,
195 FontPreset::Korean,
196 FontPreset::SimplifiedChinese,
197 FontPreset::TraditionalChinese,
198 FontPreset::Latin,
199 ],
200 FontRegion::SimplifiedChinese => vec![
201 FontPreset::SimplifiedChinese,
202 FontPreset::TraditionalChinese,
203 FontPreset::Korean,
204 FontPreset::Japanese,
205 FontPreset::Latin,
206 ],
207 FontRegion::TraditionalChinese => vec![
208 FontPreset::TraditionalChinese,
209 FontPreset::SimplifiedChinese,
210 FontPreset::Korean,
211 FontPreset::Japanese,
212 FontPreset::Latin,
213 ],
214 FontRegion::Cyrillic => vec![
215 FontPreset::Cyrillic,
216 FontPreset::Latin,
217 FontPreset::Korean,
218 FontPreset::SimplifiedChinese,
219 FontPreset::TraditionalChinese,
220 FontPreset::Japanese,
221 ],
222 FontRegion::Latin | FontRegion::Unknown => vec![
223 FontPreset::Latin,
224 FontPreset::Cyrillic,
225 FontPreset::Korean,
226 FontPreset::SimplifiedChinese,
227 FontPreset::TraditionalChinese,
228 FontPreset::Japanese,
229 ],
230 }
231}
232
233pub fn find_from_presets<I>(presets_in_priority: I, style: FontStyle) -> Vec<FoundFont>
253where
254 I: IntoIterator<Item = FontPreset>,
255{
256 let db = font_db();
257
258 let mut targets: Vec<String> = Vec::new();
259 for preset in presets_in_priority {
260 match style {
261 FontStyle::Serif => {
262 targets.extend(preset_targets_serif(&preset));
263 targets.extend(preset_targets_sans(&preset));
264 }
265 FontStyle::Sans => {
266 targets.extend(preset_targets_sans(&preset));
267 }
268 }
269 }
270
271 let mut seen_family = HashSet::<String>::new();
272 let mut out = Vec::<FoundFont>::new();
273
274 for (i, family_name) in targets.into_iter().enumerate() {
275 if !seen_family.insert(family_name.clone()) {
276 continue;
277 }
278
279 if let Some(found) = resolve_one_family(db, &family_name, i) {
280 out.push(found);
281 }
282 }
283
284 out
285}
286
287pub fn find_for_locale(locale: &str, style: FontStyle) -> (FontRegion, Vec<FoundFont>) {
298 let region = region_from_locale(locale);
299 let presets = presets_for_region(region);
300 (region, find_from_presets(presets, style))
301}
302
303pub fn find_for_system_locale(style: FontStyle) -> (Option<String>, FontRegion, Vec<FoundFont>) {
316 let locale = system_locale();
317 let (region, fonts) = match locale.as_deref() {
318 Some(loc) if !loc.trim().is_empty() => find_for_locale(loc, style),
319 _ => {
320 let fallback = "en-US";
321 find_for_locale(fallback, style)
322 }
323 };
324 (locale, region, fonts)
325}
326
327static FONT_DB: OnceLock<Database> = OnceLock::new();
328
329fn font_db() -> &'static Database {
330 FONT_DB.get_or_init(|| {
331 let mut db = Database::new();
332 db.load_system_fonts();
333 db
334 })
335}
336
337fn resolve_one_family(db: &Database, family_name: &str, uniq: usize) -> Option<FoundFont> {
338 let families = [Family::Name(family_name)];
339 let query = Query {
340 families: &families,
341 ..Default::default()
342 };
343
344 let id = db.query(&query)?;
345 let face = db.face(id)?;
346
347 let source = match &face.source {
348 Source::File(path) => FoundFontSource::Path(path.to_path_buf()),
349 Source::Binary(bytes) => {
350 let v: Vec<u8> = bytes.as_ref().as_ref().to_vec();
351 FoundFontSource::Bytes(Arc::from(v.into_boxed_slice()))
352 }
353 _ => return None,
354 };
355
356 let key = format!("system:{}:{}", family_name, uniq);
357
358 Some(FoundFont {
359 family: family_name.to_string(),
360 key,
361 source,
362 })
363}
364
365fn preset_targets_sans(p: &FontPreset) -> Vec<String> {
366 match p {
367 FontPreset::Latin => vec![
368 "Noto Sans".into(),
369 "Segoe UI".into(),
370 "Arial".into(),
371 "SF Pro Text".into(),
372 "Helvetica Neue".into(),
373 "DejaVu Sans".into(),
374 "Liberation Sans".into(),
375 "Roboto".into(),
376 ],
377 FontPreset::Korean => vec![
378 "Noto Sans KR".into(),
379 "Noto Sans CJK KR".into(),
380 "Malgun Gothic".into(),
381 "Apple SD Gothic Neo".into(),
382 "NanumGothic".into(),
383 ],
384 FontPreset::SimplifiedChinese => vec![
385 "Noto Sans SC".into(),
386 "Noto Sans CJK SC".into(),
387 "Microsoft YaHei".into(),
388 "PingFang SC".into(),
389 "SimHei".into(),
390 "SimSun".into(),
391 ],
392 FontPreset::TraditionalChinese => vec![
393 "Noto Sans TC".into(),
394 "Noto Sans CJK TC".into(),
395 "Microsoft JhengHei".into(),
396 "PingFang TC".into(),
397 ],
398 FontPreset::Japanese => vec![
399 "Noto Sans JP".into(),
400 "Noto Sans CJK JP".into(),
401 "Yu Gothic".into(),
402 "Hiragino Sans".into(),
403 "Meiryo".into(),
404 ],
405 FontPreset::Cyrillic => vec![
406 "Noto Sans".into(),
407 "DejaVu Sans".into(),
408 "Segoe UI".into(),
409 "Arial".into(),
410 "Tahoma".into(),
411 "Times New Roman".into(),
412 ],
413 FontPreset::Custom(list) => list.clone(),
414 }
415}
416
417fn preset_targets_serif(p: &FontPreset) -> Vec<String> {
418 match p {
419 FontPreset::Latin => vec![
420 "Noto Serif".into(),
421 "Times New Roman".into(),
422 "Georgia".into(),
423 "Liberation Serif".into(),
424 "DejaVu Serif".into(),
425 "Times".into(),
426 ],
427 FontPreset::Korean => vec![
428 "Noto Serif KR".into(),
429 "Noto Serif CJK KR".into(),
430 "Batang".into(),
431 "AppleMyungjo".into(),
432 "NanumMyeongjo".into(),
433 ],
434 FontPreset::SimplifiedChinese => vec![
435 "Noto Serif SC".into(),
436 "Noto Serif CJK SC".into(),
437 "Songti SC".into(),
438 "SimSun".into(),
439 ],
440 FontPreset::TraditionalChinese => vec![
441 "Noto Serif TC".into(),
442 "Noto Serif CJK TC".into(),
443 "Songti TC".into(),
444 "PMingLiU".into(),
445 ],
446 FontPreset::Japanese => vec![
447 "Noto Serif JP".into(),
448 "Noto Serif CJK JP".into(),
449 "Yu Mincho".into(),
450 "Hiragino Mincho ProN".into(),
451 "MS Mincho".into(),
452 ],
453 FontPreset::Cyrillic => vec![
454 "Noto Serif".into(),
455 "Times New Roman".into(),
456 "Georgia".into(),
457 "Liberation Serif".into(),
458 "DejaVu Serif".into(),
459 ],
460 FontPreset::Custom(list) => list.clone(),
461 }
462}