Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/windowing/winit/fonts/linux.rs
1//! Loads fonts on linux.
2//!
3//! Handles discovering and loading fonts on linux systems.
4//! Leverages the fontconfig crate to detect all fonts
5//! available on the user's device, creating handles for the fonts.
6//! Handles can be converted to owned_ttf_parser::OwnedFace objects
7//! by loading the fonts into memory.
8 
9use std::ffi::c_int;
10use std::{collections::HashMap, ffi::CString};
11 
12use super::{
13 font_handle::{Error as FontDataError, FontHandle},
14 FontFamily, ValidateFontSupportsEn,
15};
16use crate::fonts::{FontInfo, Properties, Style, Weight};
17 
18use fontconfig::{
19 list_fonts, sort_fonts, FontSet, Fontconfig, ObjectSet, Pattern, FC_FAMILY, FC_FILE,
20 FC_FONTFORMAT, FC_FULLNAME, FC_INDEX, FC_LANG, FC_MONO, FC_SLANT, FC_SLANT_ITALIC,
21 FC_SLANT_ROMAN, FC_SPACING, FC_WEIGHT, FC_WEIGHT_BLACK, FC_WEIGHT_BOLD, FC_WEIGHT_EXTRABOLD,
22 FC_WEIGHT_EXTRALIGHT, FC_WEIGHT_LIGHT, FC_WEIGHT_MEDIUM, FC_WEIGHT_NORMAL, FC_WEIGHT_SEMIBOLD,
23 FC_WEIGHT_THIN,
24};
25use itertools::Itertools;
26 
27/// Manages font detection and handle generation.
28///
29/// Contains our font loading object, wrapping around fontconfig::FontConfig
30/// to query the available fonts on the system and return handles grouped into
31/// families
32pub struct FontconfigLoader {
33 fc: Fontconfig,
34}
35 
36impl FontconfigLoader {
37 /// Creates a new FontLoader instance.
38 ///
39 /// # Errors
40 ///
41 /// Will return an Error::Init if the underlying FFI wrapper
42 /// for Fontconfig fails to initialize
43 pub fn new() -> Result<Self, Error> {
44 if let Some(fc) = Fontconfig::new() {
45 Ok(Self { fc })
46 } else {
47 Err(Error::Init)
48 }
49 }
50 
51 /// Gets a handle for a single font family.
52 ///
53 /// Looks up all fonts in the font family specified by `family_name`.
54 /// Returns a FamilyHandle for those fonts
55 ///
56 /// # Errors
57 /// If there are zero valid fonts within the family, this will error with
58 /// Error::FamilyHasNoFonts
59 ///
60 /// Additionally, passing a malformed CString name (ex: a string w/ a null terminator)
61 /// can trigger an Error::InvalidFontName.
62 pub(super) fn get_family(&self, family_name: &str) -> Result<FamilyHandle, Error> {
63 let fonts = self.query_fonts(Some(family_name))?;
64 let mut family = FamilyHandle::new(family_name);
65 let mut errors = Vec::<Error>::new();
66 for pattern in fonts.iter() {
67 match Self::parse_font(pattern, ValidateFontSupportsEn::Yes) {
68 Ok(font) => family.add_font(font),
69 Err(err) => errors.push(err),
70 }
71 }
72 if !family.fonts.is_empty() {
73 Ok(family)
74 } else {
75 Err(Error::FamilyHasNoFonts(family_name.to_string(), errors))
76 }
77 }
78 
79 // Gets handles for all font families present on the device.
80 //
81 // Searches for all available fonts on the device, and returns
82 // font families for all valid results. A font is considered valid if:
83 //
84 // * It has a valid family_name, filename, and face_index
85 // * It supports the language 'en'.
86 // * It has a TTF or CFF format
87 //
88 // Any invalid fonts are skipped over, with logging explaining why it was skipped
89 pub(super) fn get_all_families(&self) -> Result<Vec<FamilyHandle>, Error> {
90 let fonts = self.query_fonts(None)?;
91 
92 let mut family_map = HashMap::new();
93 for pattern in fonts.iter() {
94 let font_name = pattern.name().unwrap_or("unknown");
95 let Some(family_name) = pattern.get_string(FC_FAMILY).map(|name| name.to_string())
96 else {
97 log::warn!("could not parse font_family for font {font_name}",);
98 continue;
99 };
100 
101 let font_handle = match Self::parse_font(pattern, ValidateFontSupportsEn::Yes) {
102 Ok(handle) => handle,
103 Err(_) => continue,
104 };
105 family_map
106 .entry(family_name.to_string())
107 .or_insert_with(|| FamilyHandle::new(&family_name))
108 .add_font(font_handle);
109 }
110 let mut results = family_map.into_values().collect::<Vec<_>>();
111 results.sort_by(|a, b| a.name.cmp(&b.name));
112 Ok(results)
113 }
114 
115 /// Convenience function to parse a font from a pattern, and log appropriately
116 /// if the parsing fails.
117 /// If `validate` is set to [`ValidateFontSupportsEn::Yes`] an error is returned if the font does not support
118 /// english.
119 fn parse_font(
120 pattern: Pattern<'_>,
121 validate: ValidateFontSupportsEn,
122 ) -> Result<FontHandle, Error> {
123 FontHandle::try_from_pattern(&pattern, validate).map_err(|err| {
124 let font_name = pattern.name().unwrap_or("unknown");
125 match &err {
126 Error::InvalidFontFormat(_) | Error::DoesNotSupportEn => {
127 log::debug!("skipping font {font_name} because of error: {err:#}")
128 }
129 _ => {
130 log::warn!("could not parse font {font_name}: {err:#}");
131 }
132 };
133 err
134 })
135 }
136 
137 /// Returns a list of fallback fonts that match the `family_name` and given `properties`, in order of closeness.
138 pub fn fallback_fonts(
139 &self,
140 family_name: &str,
141 properties: Properties,
142 ) -> Result<Vec<FontHandle>, Error> {
143 let mut pattern = Pattern::new(&self.fc);
144 // Though unlikely, return an `Error` if the requested family name has a null character in it.
145 let name = CString::new(family_name)
146 .map_err(|_| Error::InvalidFontName(family_name.to_string()))?;
147 pattern.add_string(FC_FAMILY, &name);
148 pattern.add_integer(FC_WEIGHT, to_fontconfig_weight(properties.weight));
149 pattern.add_integer(FC_SLANT, to_fontconfig_style(properties.style));
150 
151 let mut object_set = ObjectSet::new(&self.fc);
152 object_set.add(FC_FAMILY);
153 object_set.add(FC_FULLNAME);
154 object_set.add(FC_FILE);
155 object_set.add(FC_INDEX);
156 
157 // By setting trim to true, we omit fonts that have a unicode range covered by prior fonts in chain. Doing this
158 // reduces the overall set of fallback fonts we need to load.
159 let sort_fonts = sort_fonts(&pattern, true /* trim */);
160 
161 // Skip the first font, since this is considered the primary "font" we're trying to match.
162 let fallback_fonts = sort_fonts
163 .iter()
164 .skip(1)
165 .filter_map(|pattern| {
166 // Fallback fonts we load aren't guaranteed to support english.
167 // Also, parse_font already has logging for parsing, so we log there.
168 Self::parse_font(pattern, ValidateFontSupportsEn::No).ok()
169 })
170 .collect_vec();
171 
172 Ok(fallback_fonts)
173 }
174 
175 fn query_fonts(&self, family_name: Option<&str>) -> Result<FontSet<'_>, Error> {
176 let mut pattern = Pattern::new(&self.fc);
177 if let Some(name) = family_name {
178 // Very unlikely that someone is going to pass a font name with a \0 in,
179 // but covering just in case w/ an error
180 let name = CString::new(name).map_err(|_| Error::InvalidFontName(name.to_string()))?;
181 pattern.add_string(FC_FAMILY, &name)
182 }
183 
184 let mut object_set = ObjectSet::new(&self.fc);
185 object_set.add(FC_FAMILY);
186 object_set.add(FC_FULLNAME);
187 object_set.add(FC_FILE);
188 object_set.add(FC_INDEX);
189 object_set.add(FC_SPACING);
190 object_set.add(FC_LANG);
191 object_set.add(FC_FONTFORMAT);
192 
193 Ok(list_fonts(&pattern, Some(&object_set)))
194 }
195}
196 
197impl FontHandle {
198 /// Attempts to generate a FontHandle from a Fontconfig Pattern.
199 ///
200 /// In order to properly parse out a FontHandle, the pattern needs to have
201 ///
202 /// * A filename
203 /// * a face_index
204 ///
205 /// If either of these fields are missing, an Error::MissingMetadataField will
206 /// be returned
207 ///
208 /// Additionally, will return the following errors:
209 ///
210 /// * Error::DoesNotSupportEn: if the pattern is missing en as a supported language and `validate_fonts_support_en`
211 /// is set to [`ValidateFontSupportsEn::Yes`].
212 /// * Error::InvalidFontFormat: if the pattern's font format is not TTF or CFF.
213 fn try_from_pattern(
214 value: &Pattern<'_>,
215 validate_font_supports_en: ValidateFontSupportsEn,
216 ) -> Result<Self, Error> {
217 let file_path = value
218 .filename()
219 .ok_or_else(|| Error::MissingMetadataField("filename".to_owned()))?;
220 
221 let index = value
222 .face_index()
223 .ok_or_else(|| Error::MissingMetadataField("face_index".to_owned()))?
224 as u32;
225 
226 if matches!(validate_font_supports_en, ValidateFontSupportsEn::Yes)
227 && !value
228 .lang_set()
229 .is_some_and(|lang_set| lang_set.into_iter().any(|lang| lang == "en"))
230 {
231 return Err(Error::DoesNotSupportEn);
232 }
233 
234 if !matches!(
235 value.format(),
236 Ok(fontconfig::FontFormat::TrueType) | Ok(fontconfig::FontFormat::CFF)
237 ) {
238 // NOTE: fontconfig::FontFormat does not impl Debug or any mapping to strings,
239 // so for debugging purposes we pull the underlying string field the
240 // enum is computed from.
241 let font_format_str = value.get_string(FC_FONTFORMAT).unwrap_or_default();
242 return Err(Error::InvalidFontFormat(font_format_str.to_string()));
243 }
244 
245 let spacing = value.get_int(FC_SPACING);
246 
247 Ok(FontHandle::new(
248 file_path,
249 index,
250 match spacing {
251 None => false,
252 Some(v) => v == FC_MONO,
253 },
254 ))
255 }
256}
257 
258/// A handle containing information necessary to load all font faces in a family.
259pub(super) struct FamilyHandle {
260 name: String,
261 fonts: Vec<FontHandle>,
262}
263 
264impl FamilyHandle {
265 fn new(name: &str) -> Self {
266 Self {
267 name: name.to_string(),
268 fonts: vec![],
269 }
270 }
271 
272 pub fn name(&self) -> &str {
273 &self.name
274 }
275 
276 fn add_font(&mut self, font: FontHandle) {
277 self.fonts.push(font);
278 }
279 
280 /// Consumes the Family Handle into a FontFamily object.
281 pub fn into_family(self) -> Result<FontFamily, Error> {
282 self.into_info_and_family().map(|(_, family)| family)
283 }
284 
285 /// Converts the [`FamilyHandle`] into a [`FontInfo`], [`FontFamily`] pair.
286 pub fn into_info_and_family(self) -> Result<(FontInfo, FontFamily), Error> {
287 let mut fonts = Vec::<FontHandle>::with_capacity(self.fonts.len());
288 let mut errors = Vec::<Error>::new();
289 let mut is_monospace = false;
290 let name = self.name;
291 
292 for handle in self.fonts {
293 match handle.validate_font_data() {
294 Ok(_) => {
295 is_monospace |= handle.is_monospace();
296 fonts.push(handle);
297 }
298 Err(err) => errors.push(Error::FontData(err)),
299 }
300 }
301 if !fonts.is_empty() {
302 Ok((
303 FontInfo {
304 family_name: name.clone(),
305 is_monospace,
306 },
307 FontFamily { fonts, name },
308 ))
309 } else {
310 Err(Error::FamilyHasNoFonts(name, errors))
311 }
312 }
313}
314 
315/// Errors associated with loading fonts.
316#[derive(Debug, thiserror::Error)]
317pub enum Error {
318 /// The FontLoader cannot be initialized b/c the underlying
319 /// Fontconfig ffi handle failed to init.
320 #[error("Failed to initialize Fontconfig ffi handle")]
321 Init,
322 
323 /// The user has passed a malformed CString font name.
324 #[error("Invalid Font Name {0}")]
325 InvalidFontName(String),
326 
327 /// A font could not be parsed into a handle b/c it is missing
328 /// an important metadata field.
329 #[error("Could not parse font, missing metadata field {0}")]
330 MissingMetadataField(String),
331 
332 /// A font does not have a valid font format (either TTF or CFF)
333 #[error("Invalid font format '{0}'")]
334 InvalidFontFormat(String),
335 
336 /// A font does not support the language en
337 #[error("Font does not support language en")]
338 DoesNotSupportEn,
339 
340 /// A font family has been requested, but there are no valid
341 /// fonts for that family
342 #[error("Font family {0} does not contain any valid fonts")]
343 FamilyHasNoFonts(String, Vec<Error>),
344 
345 // When the underlying font handle has trouble loading data.
346 #[error("Failed to load font data")]
347 FontData(#[from] FontDataError),
348}
349 
350fn to_fontconfig_weight(weight: Weight) -> c_int {
351 match weight {
352 Weight::Thin => FC_WEIGHT_THIN,
353 Weight::ExtraLight => FC_WEIGHT_EXTRALIGHT,
354 Weight::Light => FC_WEIGHT_LIGHT,
355 Weight::Normal => FC_WEIGHT_NORMAL,
356 Weight::Medium => FC_WEIGHT_MEDIUM,
357 Weight::Semibold => FC_WEIGHT_SEMIBOLD,
358 Weight::Bold => FC_WEIGHT_BOLD,
359 Weight::ExtraBold => FC_WEIGHT_EXTRABOLD,
360 Weight::Black => FC_WEIGHT_BLACK,
361 }
362}
363 
364fn to_fontconfig_style(style: Style) -> c_int {
365 match style {
366 Style::Normal => FC_SLANT_ROMAN,
367 Style::Italic => FC_SLANT_ITALIC,
368 }
369}
370