zng_ext_font/
query_util.rs

1#[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
2pub use desktop::*;
3
4#[cfg(target_arch = "wasm32")]
5pub use wasm::*;
6
7#[cfg(target_os = "android")]
8pub use android::*;
9
10#[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
11mod desktop {
12    use std::{
13        path::{Path, PathBuf},
14        sync::Arc,
15    };
16
17    use parking_lot::Mutex;
18    use zng_var::ResponseVar;
19
20    use crate::{FontBytes, FontLoadingError, FontName, FontStretch, FontStyle, FontWeight, GlyphLoadingError, WeakFontBytes};
21
22    static DATA_CACHE: Mutex<Vec<(PathBuf, WeakFontBytes)>> = Mutex::new(vec![]);
23
24    pub fn system_all() -> ResponseVar<Vec<FontName>> {
25        zng_task::wait_respond(|| {
26            font_kit::source::SystemSource::new()
27                .all_families()
28                .unwrap_or_default()
29                .into_iter()
30                .map(FontName::from)
31                .collect()
32        })
33    }
34
35    pub fn best(
36        font_name: &FontName,
37        style: FontStyle,
38        weight: FontWeight,
39        stretch: FontStretch,
40    ) -> Result<Option<(FontBytes, u32)>, FontLoadingError> {
41        if font_name == "Ubuntu"
42            && let Ok(Some(h)) = workaround_ubuntu(style, weight, stretch)
43        {
44            return Ok(Some(h));
45        }
46
47        let family_name = font_kit::family_name::FamilyName::from(font_name.clone());
48        match font_kit::source::SystemSource::new().select_best_match(
49            &[family_name],
50            &font_kit::properties::Properties {
51                style: style.into(),
52                weight: weight.into(),
53                stretch: stretch.into(),
54            },
55        ) {
56            Ok(handle) => {
57                let r = load_handle(&handle)?;
58                Ok(Some(r))
59            }
60            Err(font_kit::error::SelectionError::NotFound) => {
61                tracing::debug!(target: "font_loading", "system font not found\nquery: {:?}", (font_name, style, weight, stretch));
62                Ok(None)
63            }
64            Err(font_kit::error::SelectionError::CannotAccessSource { reason }) => {
65                Err(FontLoadingError::Io(Arc::new(std::io::Error::other(reason.unwrap_or_default()))))
66            }
67        }
68    }
69
70    // see https://github.com/servo/font-kit/issues/245
71    fn workaround_ubuntu(style: FontStyle, weight: FontWeight, stretch: FontStretch) -> Result<Option<(FontBytes, u32)>, FontLoadingError> {
72        let source = font_kit::source::SystemSource::new();
73        let ubuntu = match source.select_family_by_name("Ubuntu") {
74            Ok(u) => u,
75            Err(e) => {
76                return match e {
77                    font_kit::error::SelectionError::NotFound => Ok(None),
78                    font_kit::error::SelectionError::CannotAccessSource { reason } => {
79                        Err(FontLoadingError::Io(Arc::new(std::io::Error::other(reason.unwrap_or_default()))))
80                    }
81                };
82            }
83        };
84        for handle in ubuntu.fonts() {
85            let font = handle.load()?;
86            let name = match font.postscript_name() {
87                Some(n) => n,
88                None => continue,
89            };
90
91            // Ubuntu-ExtraBold
92            // Ubuntu-Condensed
93            // Ubuntu-CondensedLight
94            // Ubuntu-CondensedBold
95            // Ubuntu-CondensedMedium
96            // Ubuntu-CondensedExtraBold
97            // UbuntuItalic-CondensedLightItalic
98            // UbuntuItalic-CondensedItalic
99            // UbuntuItalic-CondensedMediumItalic
100            // UbuntuItalic-CondensedBoldItalic
101            // UbuntuItalic-CondensedExtraBoldItalic
102            // Ubuntu-Italic
103            // UbuntuItalic-ThinItalic
104            // UbuntuItalic-LightItalic
105            // UbuntuItalic-Italic
106            // UbuntuItalic-MediumItalic
107            // UbuntuItalic-BoldItalic
108            // UbuntuItalic-ExtraBoldItalic
109            // UbuntuItalic-CondensedThinItalic
110            // Ubuntu-Thin
111            // Ubuntu-Regular
112            // Ubuntu-Light
113            // Ubuntu-Bold
114            // Ubuntu-Medium
115            // Ubuntu-CondensedThin
116
117            if (style == FontStyle::Italic) != name.contains("Italic") {
118                continue;
119            }
120
121            if (FontWeight::MEDIUM..FontWeight::SEMIBOLD).contains(&weight) != name.contains("Medium") {
122                continue;
123            }
124            if (weight >= FontWeight::EXTRA_BOLD) != name.contains("ExtraBold") {
125                continue;
126            }
127            if (FontWeight::SEMIBOLD..FontWeight::EXTRA_BOLD).contains(&weight) != name.contains("Bold") {
128                continue;
129            }
130
131            if (FontWeight::EXTRA_LIGHT..FontWeight::LIGHT).contains(&weight) != name.contains("Light") {
132                continue;
133            }
134            if (weight < FontWeight::EXTRA_LIGHT) != name.contains("Thin") {
135                continue;
136            }
137
138            if (stretch <= FontStretch::CONDENSED) != name.contains("Condensed") {
139                continue;
140            }
141
142            return Ok(Some(load_handle(handle)?));
143        }
144        Ok(None)
145    }
146
147    fn load_handle(handle: &font_kit::handle::Handle) -> Result<(FontBytes, u32), FontLoadingError> {
148        match handle {
149            font_kit::handle::Handle::Path { path, font_index } => {
150                let mut path = path.clone();
151                // try replacing type1 fonts with OpenType
152                // RustyBuzz does not support type1 (neither does Harfbuzz, it is obsolete)
153                //
154                // Example case from default Ubuntu fonts:
155                // /usr/share/fonts/type1/urw-base35/Z003-MediumItalic.t1
156                // /usr/share/fonts/opentype/urw-base35/Z003-MediumItalic.otf
157                if let Ok(base) = path.strip_prefix("/usr/share/fonts/type1/")
158                    && let Some(name) = base.file_name()
159                    && let Some(name) = name.to_str()
160                    && name.ends_with(".t1")
161                {
162                    let rep = Path::new("/usr/share/fonts/opentype/").join(base.with_extension("otf"));
163                    if rep.exists() {
164                        tracing::debug!("replaced `{name}` with .otf of same name");
165                        path = rep;
166                    }
167                }
168
169                for (k, data) in DATA_CACHE.lock().iter() {
170                    if *k == *path
171                        && let Some(data) = data.upgrade()
172                    {
173                        return Ok((data, *font_index));
174                    }
175                }
176
177                let data = FontBytes::from_file(path.clone())?;
178
179                let mut cache = DATA_CACHE.lock();
180                cache.retain(|(_, v)| v.strong_count() > 0);
181                cache.push((path.clone(), data.downgrade()));
182
183                Ok((data, *font_index))
184            }
185            font_kit::handle::Handle::Memory { bytes, font_index } => Ok((FontBytes::from_arc(bytes.clone()), *font_index)),
186        }
187    }
188
189    impl From<font_kit::error::FontLoadingError> for FontLoadingError {
190        fn from(ve: font_kit::error::FontLoadingError) -> Self {
191            match ve {
192                font_kit::error::FontLoadingError::UnknownFormat => Self::UnknownFormat,
193                font_kit::error::FontLoadingError::NoSuchFontInCollection => Self::NoSuchFontInCollection,
194                font_kit::error::FontLoadingError::Parse => Self::Parse(ttf_parser::FaceParsingError::MalformedFont),
195                font_kit::error::FontLoadingError::NoFilesystem => Self::NoFilesystem,
196                font_kit::error::FontLoadingError::Io(e) => Self::Io(Arc::new(e)),
197            }
198        }
199    }
200
201    impl From<FontName> for font_kit::family_name::FamilyName {
202        fn from(font_name: FontName) -> Self {
203            use font_kit::family_name::FamilyName::*;
204            match font_name.name() {
205                "serif" => Serif,
206                "sans-serif" => SansSerif,
207                "monospace" => Monospace,
208                "cursive" => Cursive,
209                "fantasy" => Fantasy,
210                _ => Title(font_name.txt.into()),
211            }
212        }
213    }
214
215    impl From<FontStretch> for font_kit::properties::Stretch {
216        fn from(value: FontStretch) -> Self {
217            font_kit::properties::Stretch(value.0)
218        }
219    }
220
221    impl From<FontStyle> for font_kit::properties::Style {
222        fn from(value: FontStyle) -> Self {
223            use font_kit::properties::Style::*;
224            match value {
225                FontStyle::Normal => Normal,
226                FontStyle::Italic => Italic,
227                FontStyle::Oblique => Oblique,
228            }
229        }
230    }
231
232    impl From<FontWeight> for font_kit::properties::Weight {
233        fn from(value: FontWeight) -> Self {
234            font_kit::properties::Weight(value.0)
235        }
236    }
237
238    impl From<font_kit::error::GlyphLoadingError> for GlyphLoadingError {
239        fn from(value: font_kit::error::GlyphLoadingError) -> Self {
240            use GlyphLoadingError::*;
241            match value {
242                font_kit::error::GlyphLoadingError::NoSuchGlyph => NoSuchGlyph,
243                font_kit::error::GlyphLoadingError::PlatformError => PlatformError,
244            }
245        }
246    }
247}
248
249#[cfg(target_arch = "wasm32")]
250mod wasm {
251    use zng_var::ResponseVar;
252
253    use crate::{FontBytes, FontLoadingError, FontName, FontStretch, FontStyle, FontWeight};
254
255    pub fn system_all() -> ResponseVar<Vec<FontName>> {
256        zng_var::response_done_var(vec![])
257    }
258
259    pub fn best(
260        font_name: &FontName,
261        style: FontStyle,
262        weight: FontWeight,
263        stretch: FontStretch,
264    ) -> Result<Option<(FontBytes, u32)>, FontLoadingError> {
265        let _ = (font_name, style, weight, stretch);
266        Err(FontLoadingError::NoFilesystem)
267    }
268}
269
270#[cfg(target_os = "android")]
271mod android {
272    // font-kit does not cross compile for Android because of a dependency,
273    // so we reimplement/copy some of their code here.
274
275    use std::{borrow::Cow, path::PathBuf, sync::Arc};
276
277    use zng_var::ResponseVar;
278
279    use crate::{FontBytes, FontLoadingError, FontName, FontStretch, FontStyle, FontWeight};
280
281    pub fn system_all() -> ResponseVar<Vec<FontName>> {
282        zng_task::wait_respond(|| {
283            let mut prev = None;
284            cached_system_all()
285                .iter()
286                .flat_map(|(k, _)| {
287                    if prev == Some(k) {
288                        None
289                    } else {
290                        prev = Some(k);
291                        Some(k)
292                    }
293                })
294                .cloned()
295                .collect()
296        })
297    }
298
299    fn cached_system_all() -> parking_lot::MappedRwLockReadGuard<'static, Vec<(FontName, PathBuf)>> {
300        let lock = SYSTEM_ALL.read();
301        if !lock.is_empty() {
302            return lock;
303        }
304
305        drop(lock);
306        let mut lock = SYSTEM_ALL.write();
307        if lock.is_empty() {
308            for entry in ["/system/fonts/", "/system/font/", "/data/fonts/", "/system/product/fonts/"]
309                .iter()
310                .flat_map(std::fs::read_dir)
311                .flatten()
312                .flatten()
313            {
314                let entry = entry.path();
315                let ext = entry.extension().and_then(|e| e.to_str()).unwrap_or_default().to_ascii_lowercase();
316                if ["ttf", "otf"].contains(&ext.as_str()) && entry.is_file() {
317                    if let Ok(bytes) = FontBytes::from_file(entry.clone()) {
318                        match crate::FontFace::load(bytes, 0) {
319                            Ok(f) => {
320                                lock.push((f.family_name().clone(), entry));
321                            }
322                            Err(e) => tracing::error!("error parsing '{}', {e}", entry.display()),
323                        }
324                    }
325                }
326            }
327            lock.sort_by(|a, b| a.0.cmp(&b.0));
328        }
329        drop(lock);
330        SYSTEM_ALL.read()
331    }
332
333    pub fn best(
334        font_name: &FontName,
335        style: FontStyle,
336        weight: FontWeight,
337        stretch: FontStretch,
338    ) -> Result<Option<(FontBytes, u32)>, FontLoadingError> {
339        let lock = cached_system_all();
340        let lock = &*lock;
341
342        // special names
343        // source: https://android.googlesource.com/platform/frameworks/base/+/master/data/fonts/fonts.xml
344        let font_name = match font_name.name() {
345            "sans-serif" => Cow::Owned(FontName::new("Roboto")),
346            "serif" | "fantasy" => Cow::Owned(FontName::new("Noto Serif")),
347            "cursive" => Cow::Owned(FontName::new("Dancing Script")),
348            "monospace" => Cow::Owned(FontName::new("Droid Sans Mono")),
349            _ => Cow::Borrowed(font_name),
350        };
351        let font_name = &*font_name;
352
353        let mut start_i = match lock.binary_search_by(|a| a.0.cmp(font_name)) {
354            Ok(i) => i,
355            Err(_) => {
356                tracing::debug!(target: "font_loading", "system font not found\nquery: {:?}", (font_name, style, weight, stretch));
357                return Ok(None);
358            }
359        };
360        while start_i > 0 && &lock[start_i - 1].0 == font_name {
361            start_i -= 1
362        }
363        let mut end_i = start_i;
364        while end_i + 1 < lock.len() && &lock[end_i + 1].0 == font_name {
365            end_i += 1
366        }
367
368        let family_len = end_i - start_i;
369        let mut options = Vec::with_capacity(family_len);
370        let mut candidates = Vec::with_capacity(family_len);
371
372        for (_, path) in &lock[start_i..=end_i] {
373            if let Ok(bytes) = FontBytes::from_file(path.clone()) {
374                if let Ok(f) = ttf_parser::Face::parse(&bytes, 0) {
375                    candidates.push(matching::Properties {
376                        style: f.style(),
377                        weight: f.weight(),
378                        stretch: f.width(),
379                    });
380                    options.push(bytes);
381                }
382            }
383        }
384
385        match matching::find_best_match(
386            &candidates,
387            &matching::Properties {
388                style: style.into(),
389                weight: weight.into(),
390                stretch: stretch.into(),
391            },
392        ) {
393            Ok(i) => {
394                let bytes = options.swap_remove(i);
395                Ok(Some((bytes, 0)))
396            }
397            Err(FontLoadingError::NoSuchFontInCollection) => {
398                tracing::debug!(target: "font_loading", "system font not found\nquery: {:?}", (font_name, style, weight, stretch));
399                Ok(None)
400            }
401            Err(e) => Err(FontLoadingError::Io(Arc::new(std::io::Error::other(e)))),
402        }
403    }
404
405    zng_app_context::app_local! {
406        static SYSTEM_ALL: Vec<(FontName, PathBuf)> = vec![];
407    }
408
409    mod matching {
410        // font-kit/src/matching.rs
411        //
412        // Copyright © 2018 The Pathfinder Project Developers.
413        //
414        // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
415        // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
416        // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
417        // option. This file may not be copied, modified, or distributed
418        // except according to those terms.
419
420        //! Determines the closest font matching a description per the CSS Fonts Level 3 specification.
421
422        use ttf_parser::{Style, Weight, Width as Stretch};
423
424        use crate::FontLoadingError;
425
426        pub struct Properties {
427            pub style: Style,
428            pub weight: Weight,
429            pub stretch: Stretch,
430        }
431
432        /// This follows CSS Fonts Level 3 § 5.2 [1].
433        ///
434        /// https://drafts.csswg.org/css-fonts-3/#font-style-matching
435        pub fn find_best_match(candidates: &[Properties], query: &Properties) -> Result<usize, FontLoadingError> {
436            // Step 4.
437            let mut matching_set: Vec<usize> = (0..candidates.len()).collect();
438            if matching_set.is_empty() {
439                return Err(FontLoadingError::NoSuchFontInCollection);
440            }
441
442            // Step 4a (`font-stretch`).
443            let matching_stretch = if matching_set.iter().any(|&index| candidates[index].stretch == query.stretch) {
444                // Exact match.
445                query.stretch
446            } else if query.stretch <= Stretch::Normal {
447                // Closest width, first checking narrower values and then wider values.
448                match matching_set
449                    .iter()
450                    .filter(|&&index| candidates[index].stretch < query.stretch)
451                    .min_by_key(|&&index| query.stretch.to_number() - candidates[index].stretch.to_number())
452                {
453                    Some(&matching_index) => candidates[matching_index].stretch,
454                    None => {
455                        let matching_index = *matching_set
456                            .iter()
457                            .min_by_key(|&&index| candidates[index].stretch.to_number() - query.stretch.to_number())
458                            .unwrap();
459                        candidates[matching_index].stretch
460                    }
461                }
462            } else {
463                // Closest width, first checking wider values and then narrower values.
464                match matching_set
465                    .iter()
466                    .filter(|&&index| candidates[index].stretch > query.stretch)
467                    .min_by_key(|&&index| candidates[index].stretch.to_number() - query.stretch.to_number())
468                {
469                    Some(&matching_index) => candidates[matching_index].stretch,
470                    None => {
471                        let matching_index = *matching_set
472                            .iter()
473                            .min_by_key(|&&index| query.stretch.to_number() - candidates[index].stretch.to_number())
474                            .unwrap();
475                        candidates[matching_index].stretch
476                    }
477                }
478            };
479            matching_set.retain(|&index| candidates[index].stretch == matching_stretch);
480
481            // Step 4b (`font-style`).
482            let style_preference = match query.style {
483                Style::Italic => [Style::Italic, Style::Oblique, Style::Normal],
484                Style::Oblique => [Style::Oblique, Style::Italic, Style::Normal],
485                Style::Normal => [Style::Normal, Style::Oblique, Style::Italic],
486            };
487            let matching_style = *style_preference
488                .iter()
489                .find(|&query_style| matching_set.iter().any(|&index| candidates[index].style == *query_style))
490                .unwrap();
491            matching_set.retain(|&index| candidates[index].style == matching_style);
492
493            // Step 4c (`font-weight`).
494            //
495            // The spec doesn't say what to do if the weight is between 400 and 500 exclusive, so we
496            // just use 450 as the cutoff.
497            let matching_weight = if matching_set.iter().any(|&index| candidates[index].weight == query.weight) {
498                query.weight
499            } else if query.weight.to_number() >= 400
500                && query.weight.to_number() < 450
501                && matching_set.iter().any(|&index| candidates[index].weight == Weight::from(500))
502            {
503                // Check 500 first.
504                Weight::from(500)
505            } else if query.weight.to_number() >= 450
506                && query.weight.to_number() <= 500
507                && matching_set.iter().any(|&index| candidates[index].weight.to_number() == 400)
508            {
509                // Check 400 first.
510                Weight::from(400)
511            } else if query.weight.to_number() <= 500 {
512                // Closest weight, first checking thinner values and then fatter ones.
513                match matching_set
514                    .iter()
515                    .filter(|&&index| candidates[index].weight.to_number() <= query.weight.to_number())
516                    .min_by_key(|&&index| query.weight.to_number() - candidates[index].weight.to_number())
517                {
518                    Some(&matching_index) => candidates[matching_index].weight,
519                    None => {
520                        let matching_index = *matching_set
521                            .iter()
522                            .min_by_key(|&&index| candidates[index].weight.to_number() - query.weight.to_number())
523                            .unwrap();
524                        candidates[matching_index].weight
525                    }
526                }
527            } else {
528                // Closest weight, first checking fatter values and then thinner ones.
529                match matching_set
530                    .iter()
531                    .filter(|&&index| candidates[index].weight.to_number() >= query.weight.to_number())
532                    .min_by_key(|&&index| candidates[index].weight.to_number() - query.weight.to_number())
533                {
534                    Some(&matching_index) => candidates[matching_index].weight,
535                    None => {
536                        let matching_index = *matching_set
537                            .iter()
538                            .min_by_key(|&&index| query.weight.to_number() - candidates[index].weight.to_number())
539                            .unwrap();
540                        candidates[matching_index].weight
541                    }
542                }
543            };
544            matching_set.retain(|&index| candidates[index].weight == matching_weight);
545
546            // Step 4d concerns `font-size`, but fonts in `font-kit` are unsized, so we ignore that.
547
548            // Return the result.
549            matching_set.into_iter().next().ok_or(FontLoadingError::NoSuchFontInCollection)
550        }
551    }
552}