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