StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 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 | |
| 9 | use std::ffi::c_int; |
| 10 | use std::{collections::HashMap, ffi::CString}; |
| 11 | |
| 12 | use super::{ |
| 13 | font_handle::{Error as FontDataError, FontHandle}, |
| 14 | FontFamily, ValidateFontSupportsEn, |
| 15 | }; |
| 16 | use crate::fonts::{FontInfo, Properties, Style, Weight}; |
| 17 | |
| 18 | use 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 | }; |
| 25 | use 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 |
| 32 | pub struct FontconfigLoader { |
| 33 | fc: Fontconfig, |
| 34 | } |
| 35 | |
| 36 | impl 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 | |
| 197 | impl 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. |
| 259 | pub(super) struct FamilyHandle { |
| 260 | name: String, |
| 261 | fonts: Vec<FontHandle>, |
| 262 | } |
| 263 | |
| 264 | impl 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)] |
| 317 | pub 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 | |
| 350 | fn 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 | |
| 364 | fn 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 |