Skip to main content

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 zng_task::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(read_fonts::ReadError::MalformedData("")),
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() -> zng_task::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) = skrifa::FontRef::from_index(&bytes, 0) {
375                    use skrifa::MetadataProvider as _;
376                    let a = f.attributes();
377                    candidates.push(matching::Properties {
378                        style: a.style,
379                        weight: a.weight,
380                        stretch: a.stretch,
381                    });
382                    options.push(bytes);
383                }
384            }
385        }
386
387        match matching::find_best_match(
388            &candidates,
389            &matching::Properties {
390                style: style.into(),
391                weight: weight.into(),
392                stretch: stretch.into(),
393            },
394        ) {
395            Ok(i) => {
396                let bytes = options.swap_remove(i);
397                Ok(Some((bytes, 0)))
398            }
399            Err(FontLoadingError::NoSuchFontInCollection) => {
400                tracing::debug!(target: "font_loading", "system font not found\nquery: {:?}", (font_name, style, weight, stretch));
401                Ok(None)
402            }
403            Err(e) => Err(FontLoadingError::Io(Arc::new(std::io::Error::other(e)))),
404        }
405    }
406
407    zng_app_context::app_local! {
408        static SYSTEM_ALL: Vec<(FontName, PathBuf)> = vec![];
409    }
410
411    mod matching {
412        // font-kit/src/matching.rs
413        //
414        // Copyright © 2018 The Pathfinder Project Developers.
415        //
416        // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
417        // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
418        // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
419        // option. This file may not be copied, modified, or distributed
420        // except according to those terms.
421
422        //! Determines the closest font matching a description per the CSS Fonts Level 3 specification.
423
424        use skrifa::attribute::{Stretch, Style, Weight};
425
426        use crate::FontLoadingError;
427
428        pub struct Properties {
429            pub style: Style,
430            pub weight: Weight,
431            pub stretch: Stretch,
432        }
433
434        /// This follows CSS Fonts Level 3 § 5.2 [1].
435        ///
436        /// https://drafts.csswg.org/css-fonts-3/#font-style-matching
437        pub fn find_best_match(candidates: &[Properties], query: &Properties) -> Result<usize, FontLoadingError> {
438            // Step 4.
439            let mut matching_set: Vec<usize> = (0..candidates.len()).collect();
440            if matching_set.is_empty() {
441                return Err(FontLoadingError::NoSuchFontInCollection);
442            }
443
444            fn k(f: f32) -> i32 {
445                (f * 64.0) as i32
446            }
447
448            // Step 4a (`font-stretch`).
449            let matching_stretch = if matching_set.iter().any(|&index| candidates[index].stretch == query.stretch) {
450                // Exact match.
451                query.stretch
452            } else if query.stretch <= Stretch::NORMAL {
453                // Closest width, first checking narrower values and then wider values.
454                match matching_set
455                    .iter()
456                    .filter(|&&index| candidates[index].stretch < query.stretch)
457                    .min_by_key(|&&index| k(query.stretch.ratio() - candidates[index].stretch.ratio()))
458                {
459                    Some(&matching_index) => candidates[matching_index].stretch,
460                    None => {
461                        let matching_index = *matching_set
462                            .iter()
463                            .min_by_key(|&&index| k(candidates[index].stretch.ratio() - query.stretch.ratio()))
464                            .unwrap();
465                        candidates[matching_index].stretch
466                    }
467                }
468            } else {
469                // Closest width, first checking wider values and then narrower values.
470                match matching_set
471                    .iter()
472                    .filter(|&&index| candidates[index].stretch > query.stretch)
473                    .min_by_key(|&&index| k(candidates[index].stretch.ratio() - query.stretch.ratio()))
474                {
475                    Some(&matching_index) => candidates[matching_index].stretch,
476                    None => {
477                        let matching_index = *matching_set
478                            .iter()
479                            .min_by_key(|&&index| k(query.stretch.ratio() - candidates[index].stretch.ratio()))
480                            .unwrap();
481                        candidates[matching_index].stretch
482                    }
483                }
484            };
485            matching_set.retain(|&index| candidates[index].stretch == matching_stretch);
486
487            // Step 4b (`font-style`).
488            let style_preference = match query.style {
489                Style::Italic => [Style::Italic, Style::Oblique(None), Style::Normal],
490                Style::Oblique(_) => [Style::Oblique(None), Style::Italic, Style::Normal],
491                Style::Normal => [Style::Normal, Style::Oblique(None), Style::Italic],
492            };
493            let matching_style = *style_preference
494                .iter()
495                .find(|&query_style| matching_set.iter().any(|&index| candidates[index].style == *query_style))
496                .unwrap();
497            matching_set.retain(|&index| candidates[index].style == matching_style);
498
499            // Step 4c (`font-weight`).
500            //
501            // The spec doesn't say what to do if the weight is between 400 and 500 exclusive, so we
502            // just use 450 as the cutoff.
503            let matching_weight = if matching_set.iter().any(|&index| candidates[index].weight == query.weight) {
504                query.weight
505            } else if query.weight.value() >= 400.0
506                && query.weight.value() < 450.0
507                && matching_set.iter().any(|&index| candidates[index].weight == Weight::new(500.0))
508            {
509                // Check 500 first.
510                Weight::new(500.0)
511            } else if query.weight.value() >= 450.0
512                && query.weight.value() <= 500.0
513                && matching_set.iter().any(|&index| candidates[index].weight.value() == 400.0)
514            {
515                // Check 400 first.
516                Weight::new(400.0)
517            } else if query.weight.value() <= 500.0 {
518                // Closest weight, first checking thinner values and then fatter ones.
519                match matching_set
520                    .iter()
521                    .filter(|&&index| candidates[index].weight.value() <= query.weight.value())
522                    .min_by_key(|&&index| k(query.weight.value() - candidates[index].weight.value()))
523                {
524                    Some(&matching_index) => candidates[matching_index].weight,
525                    None => {
526                        let matching_index = *matching_set
527                            .iter()
528                            .min_by_key(|&&index| k(candidates[index].weight.value() - query.weight.value()))
529                            .unwrap();
530                        candidates[matching_index].weight
531                    }
532                }
533            } else {
534                // Closest weight, first checking fatter values and then thinner ones.
535                match matching_set
536                    .iter()
537                    .filter(|&&index| candidates[index].weight.value() >= query.weight.value())
538                    .min_by_key(|&&index| k(candidates[index].weight.value() - query.weight.value()))
539                {
540                    Some(&matching_index) => candidates[matching_index].weight,
541                    None => {
542                        let matching_index = *matching_set
543                            .iter()
544                            .min_by_key(|&&index| k(query.weight.value() - candidates[index].weight.value()))
545                            .unwrap();
546                        candidates[matching_index].weight
547                    }
548                }
549            };
550            matching_set.retain(|&index| candidates[index].weight == matching_weight);
551
552            // Step 4d concerns `font-size`, but fonts in `font-kit` are unsized, so we ignore that.
553
554            // Return the result.
555            matching_set.into_iter().next().ok_or(FontLoadingError::NoSuchFontInCollection)
556        }
557    }
558}