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