fontconfig/
lib.rs

1#![deny(missing_docs)]
2
3//! A wrapper around [freedesktop.org's Fontconfig library][homepage], for locating fonts on a UNIX
4//! like systems such as Linux and FreeBSD. Requires Fontconfig to be installed. Alternatively,
5//! set the environment variable `RUST_FONTCONFIG_DLOPEN=on` or enable the `dlopen` Cargo feature
6//! to load the library at runtime rather than link at build time (useful for cross compiling).
7//!
8//! See the [Fontconfig developer reference][1] for more information.
9//!
10//! [1]: http://www.freedesktop.org/software/fontconfig/fontconfig-devel/t1.html
11//! [homepage]: https://www.freedesktop.org/wiki/Software/fontconfig/
12//!
13//! Dependencies
14//! ============
15//!
16//! * Arch Linux: `fontconfig`
17//! * Debian-based systems: `libfontconfig1-dev`
18//! * FreeBSD: `fontconfig`
19//! * Void Linux: `fontconfig-devel`
20//!
21//! Usage
22//! -----
23//!
24//! ```
25//! use fontconfig::FontConfig;
26//!
27//! let mut config = FontConfig::default();
28//! // `FontConfig::find()` returns `Option` (will rarely be `None` but still could be)
29//! let font = config.find("freeserif".to_string(), None).unwrap();
30//! // `name` is a `String`, `path` is a `Path`
31//! println!("Name: {}\nPath: {}", font.name, font.path.display());
32//! ```
33//!
34//! ### Cargo Features
35//!
36//! | Feature       | Description                       | Default Enabled | Extra Dependencies    |
37//! |---------------|-----------------------------------|:---------------:|-----------------------|
38//! | `dlopen`      | [dlopen] libfontconfig at runtime |        ❌       |                       |
39//!
40//! The `dlopen` feature enables building this crate without dynamically linking to the Fontconfig C
41//! library at link time. Instead, Fontconfig will be dynamically loaded at runtime with the
42//! [dlopen] function. This can be useful in cross-compiling situations as you don't need to have a
43//! version of Fontcofig available for the target platform available at compile time.
44//!
45//! [dlopen]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/dlopen.html
46
47pub use fontconfig_sys as sys;
48use sys::ffi_dispatch;
49
50#[cfg(feature = "dlopen")]
51use sys::statics::{LIB, LIB_RESULT};
52#[cfg(not(feature = "dlopen"))]
53use sys::*;
54
55use std::path::PathBuf;
56use std::ptr::{self, NonNull};
57use std::str::FromStr;
58use std::sync::{Arc, Mutex};
59
60pub use sys::constants::*;
61use sys::FcBool;
62
63pub mod blanks;
64pub mod charset;
65pub mod fontset;
66pub mod langset;
67pub mod matrix;
68pub mod objectset;
69pub mod pattern;
70pub mod strings;
71pub mod stringset;
72
73#[allow(deprecated)]
74pub use blanks::Blanks;
75pub use charset::{CharSet, OwnedCharSet};
76pub use fontset::FontSet;
77pub use langset::{LangSet, LangSetCmp};
78pub use matrix::Matrix;
79pub use objectset::ObjectSet;
80pub use pattern::{
81    properties, properties::Property, properties::PropertyType, OwnedPattern, Pattern,
82};
83pub use strings::FcStr;
84pub use stringset::StringSet;
85
86#[allow(non_upper_case_globals)]
87const FcTrue: FcBool = 1;
88#[allow(non_upper_case_globals, dead_code)]
89const FcFalse: FcBool = 0;
90
91type Result<T> = std::result::Result<T, Error>;
92
93static INITIALIZED: once_cell::sync::Lazy<Arc<Mutex<usize>>> =
94    once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(0)));
95
96///
97#[derive(Debug, thiserror::Error)]
98pub enum Error {
99    /// The format is not known.
100    /// Error type returned from Pattern::format.
101    // The error holds the name of the unknown format.
102    #[error("Unknown format {0}")]
103    UnknownFontFormat(String),
104    /// Out of memory error.
105    #[error("malloc failed")]
106    OutOfMemory,
107    /// Object exists, but has fewer values than specified
108    #[error("Object exists, but has fewer values than specified")]
109    NoId,
110    /// Object exists, but the type doesn't match.
111    #[error("Object exists, but the type doesn't match")]
112    TypeMismatch,
113    /// Object doesn't exist at all.
114    #[error("Object doesn't exist at all")]
115    NoMatch,
116}
117
118trait ToResult {
119    fn ok(&self) -> Result<()>;
120
121    fn opt(&self) -> Option<()> {
122        self.ok().ok()
123    }
124}
125
126impl ToResult for sys::FcResult {
127    fn ok(&self) -> Result<()> {
128        match *self {
129            sys::FcResultMatch => Ok(()),
130            sys::FcResultNoMatch => Err(Error::NoMatch),
131            sys::FcResultTypeMismatch => Err(Error::TypeMismatch),
132            sys::FcResultNoId => Err(Error::NoId),
133            _ => unreachable!(),
134        }
135    }
136}
137
138///
139pub enum MatchKind {
140    /// Tagged as pattern operations are applied
141    Pattern,
142    /// Tagged as font operations are applied and `pat` is used for <test> elements with target=pattern
143    Font,
144    ///
145    Scan,
146}
147
148#[doc(hidden)]
149impl From<sys::FcMatchKind> for MatchKind {
150    fn from(kind: sys::FcMatchKind) -> Self {
151        match kind {
152            sys::FcMatchPattern => MatchKind::Pattern,
153            sys::FcMatchFont => MatchKind::Font,
154            sys::FcMatchScan => MatchKind::Scan,
155            _ => unreachable!(),
156        }
157    }
158}
159
160#[doc(hidden)]
161impl From<MatchKind> for sys::FcMatchKind {
162    fn from(kind: MatchKind) -> sys::FcMatchKind {
163        match kind {
164            MatchKind::Pattern => sys::FcMatchPattern,
165            MatchKind::Font => sys::FcMatchFont,
166            MatchKind::Scan => sys::FcMatchScan,
167        }
168    }
169}
170
171/// The format of a font matched by Fontconfig.
172#[derive(Eq, PartialEq)]
173#[allow(missing_docs)]
174pub enum FontFormat {
175    TrueType,
176    Type1,
177    BDF,
178    PCF,
179    Type42,
180    CIDType1,
181    CFF,
182    PFR,
183    WindowsFNT,
184}
185
186/// Handle obtained after Fontconfig has been initialised.
187#[doc(alias = "FcConfig")]
188pub struct FontConfig {
189    cfg: Option<NonNull<sys::FcConfig>>,
190}
191
192///
193impl FontConfig {
194    /// Create a configuration
195    // pub fn new() -> Option<Self> {
196    //     #[cfg(feature = "dlopen")]
197    //     if LIB_RESULT.is_err() {
198    //         return None;
199    //     }
200    //     let cfg = unsafe { ffi_dispatch!(LIB, FcConfigCreate,) };
201    //     Some(FontConfig {
202    //         cfg: Some(NonNull::new(cfg)?),
203    //     })
204    // }
205
206    /// Set configuration as default.
207    ///
208    /// Sets the current default configuration to config.
209    /// Implicitly calls FcConfigBuildFonts if necessary,
210    /// and FcConfigReference() to inrease the reference count in config since 2.12.0,
211    /// returning FcFalse if that call fails.
212    // pub fn set_current(config: &mut FontConfig) {
213    //     //
214    // }
215
216    /// Execute substitutions
217    ///
218    /// Calls FcConfigSubstituteWithPat setting p_pat to NULL.
219    /// Returns false if the substitution cannot be performed (due to allocation failure).
220    /// Otherwise returns true.
221    pub fn substitute(&mut self, pat: &mut Pattern, kind: MatchKind) {
222        let ret = unsafe {
223            ffi_dispatch!(
224                LIB,
225                FcConfigSubstitute,
226                self.as_mut_ptr(),
227                pat.as_mut_ptr(),
228                kind.into()
229            )
230        };
231        assert_eq!(ret, FcTrue);
232    }
233
234    /// Return the best font from a set of font sets
235    ///
236    /// Finds the font in sets most closely matching pattern and
237    /// returns the result of FcFontRenderPrepare for that font and the provided pattern.
238    /// This function should be called only after FcConfigSubstitute and FcDefaultSubstitute have been called for pattern;
239    /// otherwise the results will not be correct.
240    /// If config is NULL, the current configuration is used.
241    /// Returns NULL if an error occurs during this process.
242    #[doc(alias = "FcFontSetMatch")]
243    pub fn fontset_match(&mut self, sets: &mut [FontSet], pat: &mut Pattern) -> OwnedPattern {
244        // pat.default_substitute();
245        // self.substitute(pat, MatchKind::Font);
246        let mut result = sys::FcResultNoMatch;
247        let pat = unsafe {
248            ffi_dispatch!(
249                LIB,
250                FcFontSetMatch,
251                self.as_mut_ptr(),
252                &mut sets.as_mut_ptr().cast(),
253                sets.len() as i32,
254                pat.as_mut_ptr(),
255                &mut result
256            )
257        };
258        result.ok().unwrap();
259        OwnedPattern {
260            pat: NonNull::new(pat).unwrap(),
261        }
262    }
263
264    fn as_mut_ptr(&mut self) -> *mut sys::FcConfig {
265        if let Some(ref mut cfg) = self.cfg {
266            cfg.as_ptr()
267        } else {
268            ptr::null_mut()
269        }
270    }
271
272    #[allow(dead_code)]
273    fn as_ptr(&self) -> *const sys::FcConfig {
274        if let Some(ref cfg) = self.cfg {
275            cfg.as_ptr()
276        } else {
277            ptr::null_mut()
278        }
279    }
280
281    /// Find a font of the given `family` (e.g. Dejavu Sans, FreeSerif),
282    /// optionally filtering by `style`. Both fields are case-insensitive.
283    pub fn find(&mut self, family: String, style: Option<String>) -> Option<Font> {
284        let mut pat = OwnedPattern::new();
285        pat.add(&properties::FC_FAMILY, family);
286
287        if let Some(style) = style {
288            let style = style;
289            pat.add(&properties::FC_STYLE, style);
290        }
291
292        let font_match = pat.font_match(self);
293
294        font_match.name().and_then(|name| {
295            font_match.filename().map(|filename| Font {
296                name: name.to_owned(),
297                path: PathBuf::from(filename),
298            })
299        })
300    }
301}
302
303impl Default for FontConfig {
304    /// Initialise fontconfig and returns the default config.
305    ///
306    /// **PANIC** : If fontconfig fails to initialise
307    fn default() -> Self {
308        let mut guard = INITIALIZED.lock().unwrap();
309        #[cfg(feature = "dlopen")]
310        if LIB_RESULT.is_err() {
311            panic!("Failed to load fontconfig library");
312        }
313        assert_eq!(FcTrue, unsafe { ffi_dispatch!(LIB, FcInit,) });
314        *guard += 1;
315        FontConfig { cfg: None }
316    }
317}
318
319impl Drop for FontConfig {
320    fn drop(&mut self) {
321        let guard = INITIALIZED.lock().unwrap();
322        if guard.checked_sub(1).unwrap_or_default() == 0 {
323            unsafe { ffi_dispatch!(LIB, FcFini,) };
324        }
325    }
326}
327
328/// A very high-level view of a font, only concerned with the name and its file location.
329///
330/// ##Example
331/// ```rust
332/// use fontconfig::{Font, FontConfig};
333///
334/// let mut config = FontConfig::default();
335/// let font = config.find("sans-serif".to_string(), Some("italic".to_string())).unwrap();
336/// println!("Name: {}\nPath: {}", font.name, font.path.display());
337/// ```
338pub struct Font {
339    /// The true name of this font
340    pub name: String,
341    /// The location of this font on the filesystem.
342    pub path: PathBuf,
343}
344
345impl Font {
346    #[allow(dead_code)]
347    fn print_debug(&self) {
348        println!("Name: {}\nPath: {}", self.name, self.path.display());
349    }
350}
351
352impl FromStr for FontFormat {
353    type Err = Error;
354
355    fn from_str(s: &str) -> Result<Self> {
356        match s {
357            "TrueType" => Ok(FontFormat::TrueType),
358            "Type 1" => Ok(FontFormat::Type1),
359            "BDF" => Ok(FontFormat::BDF),
360            "PCF" => Ok(FontFormat::PCF),
361            "Type 42" => Ok(FontFormat::Type42),
362            "CID Type 1" => Ok(FontFormat::CIDType1),
363            "CFF" => Ok(FontFormat::CFF),
364            "PFR" => Ok(FontFormat::PFR),
365            "Windows FNT" => Ok(FontFormat::WindowsFNT),
366            _ => Err(Error::UnknownFontFormat(s.to_string())),
367        }
368    }
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372/// library version number
373pub struct Version {
374    /// major version number
375    pub major: u32,
376    /// minor version number
377    pub minor: u32,
378    /// micro version number
379    pub revision: u32,
380}
381
382impl std::fmt::Display for Version {
383    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
384        write!(f, "{}.{}.{}", self.major, self.minor, self.revision)
385    }
386}
387
388/// Returns the version number of the library.
389pub fn version() -> Version {
390    let version = unsafe { ffi_dispatch!(LIB, FcGetVersion,) as u32 };
391    let major = version / 10000;
392    let version = version % 10000;
393    let minor = version / 100;
394    let revision = version % 100;
395
396    Version {
397        major,
398        minor,
399        revision,
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    #[test]
408    fn it_works() {
409        FontConfig::default();
410    }
411
412    #[test]
413    fn find_font() {
414        let mut config = FontConfig::default();
415        config
416            .find("dejavu sans".to_string(), None)
417            .unwrap()
418            .print_debug();
419        config
420            .find("dejavu sans".to_string(), Some("oblique".to_string()))
421            .unwrap()
422            .print_debug();
423    }
424
425    #[test]
426    fn iter_and_print() {
427        let mut config = FontConfig::default();
428        let pat = OwnedPattern::new();
429        let fontset = pat.font_list(&mut config, None);
430        for pattern in fontset.iter() {
431            println!("{:?}", pattern.name());
432        }
433
434        // Ensure that the set can be iterated again
435        assert!(fontset.iter().count() > 0);
436    }
437
438    #[test]
439    fn iter_lang_set() {
440        let mut config = FontConfig::default();
441        let mut pat = OwnedPattern::new();
442        let family = "dejavu sans".to_string();
443        pat.add(&properties::FC_FAMILY, family);
444        let pattern = pat.font_match(&mut config);
445        for lang in pattern.lang_set().unwrap().langs().iter() {
446            println!("{:?}", lang);
447        }
448
449        // Test find
450        assert!(pattern
451            .lang_set()
452            .unwrap()
453            .langs()
454            .iter()
455            .any(|lang| lang == "za"));
456
457        // Test collect
458        let langs = pattern
459            .lang_set()
460            .unwrap()
461            .langs()
462            .iter()
463            .map(ToString::to_string)
464            .collect::<Vec<_>>();
465        assert!(langs.iter().any(|l| l == "ie"));
466    }
467
468    #[test]
469    fn iter_font_sort() {
470        let mut config = FontConfig::default();
471        let mut pat = OwnedPattern::new();
472        let family = "dejavu sans".to_owned();
473        pat.add(&properties::FC_FAMILY, family);
474        pat.default_substitute();
475        config.substitute(&mut pat, MatchKind::Pattern);
476        let font_set = pat.font_sort(&mut config, false).unwrap();
477
478        for font in font_set.iter() {
479            println!("{:?}", font.name());
480        }
481        assert!(font_set.iter().count() > 1);
482        assert!(font_set.iter().next().unwrap().name().unwrap() == "DejaVu Sans");
483    }
484}