fontconfig-rs 0.1.1

Safe, higher-level wrapper around the fontconfig library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
#![deny(missing_docs)]

//! A wrapper around [freedesktop.org's Fontconfig library][homepage], for locating fonts on a UNIX
//! like systems such as Linux and FreeBSD. Requires Fontconfig to be installed. Alternatively,
//! set the environment variable `RUST_FONTCONFIG_DLOPEN=on` or enable the `dlopen` Cargo feature
//! to load the library at runtime rather than link at build time (useful for cross compiling).
//!
//! See the [Fontconfig developer reference][1] for more information.
//!
//! [1]: http://www.freedesktop.org/software/fontconfig/fontconfig-devel/t1.html
//! [homepage]: https://www.freedesktop.org/wiki/Software/fontconfig/
//!
//! Dependencies
//! ============
//!
//! * Arch Linux: `fontconfig`
//! * Debian-based systems: `libfontconfig1-dev`
//! * FreeBSD: `fontconfig`
//! * Void Linux: `fontconfig-devel`
//!
//! Usage
//! -----
//!
//! ```
//! use fontconfig::FontConfig;
//!
//! let mut config = FontConfig::default();
//! // `FontConfig::find()` returns `Option` (will rarely be `None` but still could be)
//! let font = config.find("freeserif".to_string(), None).unwrap();
//! // `name` is a `String`, `path` is a `Path`
//! println!("Name: {}\nPath: {}", font.name, font.path.display());
//! ```
//!
//! ### Cargo Features
//!
//! | Feature       | Description                       | Default Enabled | Extra Dependencies    |
//! |---------------|-----------------------------------|:---------------:|-----------------------|
//! | `dlopen`      | [dlopen] libfontconfig at runtime |        ❌       |                       |
//!
//! The `dlopen` feature enables building this crate without dynamically linking to the Fontconfig C
//! library at link time. Instead, Fontconfig will be dynamically loaded at runtime with the
//! [dlopen] function. This can be useful in cross-compiling situations as you don't need to have a
//! version of Fontcofig available for the target platform available at compile time.
//!
//! [dlopen]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/dlopen.html

pub use fontconfig_sys as sys;
use sys::ffi_dispatch;

#[cfg(feature = "dlopen")]
use sys::statics::{LIB, LIB_RESULT};
#[cfg(not(feature = "dlopen"))]
use sys::*;

use std::path::PathBuf;
use std::ptr::{self, NonNull};
use std::str::FromStr;
use std::sync::{Arc, Mutex};

pub use sys::constants::*;
use sys::FcBool;

pub mod blanks;
pub mod charset;
pub mod fontset;
pub mod langset;
pub mod matrix;
pub mod objectset;
pub mod pattern;
pub mod strings;
pub mod stringset;

#[allow(deprecated)]
pub use blanks::Blanks;
pub use charset::{CharSet, OwnedCharSet};
pub use fontset::FontSet;
pub use langset::{LangSet, LangSetCmp};
pub use matrix::Matrix;
pub use objectset::ObjectSet;
pub use pattern::{
    properties, properties::Property, properties::PropertyType, OwnedPattern, Pattern,
};
pub use strings::FcStr;
pub use stringset::StringSet;

#[allow(non_upper_case_globals)]
const FcTrue: FcBool = 1;
#[allow(non_upper_case_globals, dead_code)]
const FcFalse: FcBool = 0;

type Result<T> = std::result::Result<T, Error>;

static INITIALIZED: once_cell::sync::Lazy<Arc<Mutex<usize>>> =
    once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(0)));

///
#[derive(Debug, thiserror::Error)]
pub enum Error {
    /// The format is not known.
    /// Error type returned from Pattern::format.
    // The error holds the name of the unknown format.
    #[error("Unknown format {0}")]
    UnknownFontFormat(String),
    /// Out of memory error.
    #[error("malloc failed")]
    OutOfMemory,
    /// Object exists, but has fewer values than specified
    #[error("Object exists, but has fewer values than specified")]
    NoId,
    /// Object exists, but the type doesn't match.
    #[error("Object exists, but the type doesn't match")]
    TypeMismatch,
    /// Object doesn't exist at all.
    #[error("Object doesn't exist at all")]
    NoMatch,
}

trait ToResult {
    fn ok(&self) -> Result<()>;

    fn opt(&self) -> Option<()> {
        self.ok().ok()
    }
}

impl ToResult for sys::FcResult {
    fn ok(&self) -> Result<()> {
        match *self {
            sys::FcResultMatch => Ok(()),
            sys::FcResultNoMatch => Err(Error::NoMatch),
            sys::FcResultTypeMismatch => Err(Error::TypeMismatch),
            sys::FcResultNoId => Err(Error::NoId),
            _ => unreachable!(),
        }
    }
}

///
pub enum MatchKind {
    /// Tagged as pattern operations are applied
    Pattern,
    /// Tagged as font operations are applied and `pat` is used for <test> elements with target=pattern
    Font,
    ///
    Scan,
}

#[doc(hidden)]
impl From<sys::FcMatchKind> for MatchKind {
    fn from(kind: sys::FcMatchKind) -> Self {
        match kind {
            sys::FcMatchPattern => MatchKind::Pattern,
            sys::FcMatchFont => MatchKind::Font,
            sys::FcMatchScan => MatchKind::Scan,
            _ => unreachable!(),
        }
    }
}

#[doc(hidden)]
impl From<MatchKind> for sys::FcMatchKind {
    fn from(kind: MatchKind) -> sys::FcMatchKind {
        match kind {
            MatchKind::Pattern => sys::FcMatchPattern,
            MatchKind::Font => sys::FcMatchFont,
            MatchKind::Scan => sys::FcMatchScan,
        }
    }
}

/// The format of a font matched by Fontconfig.
#[derive(Eq, PartialEq)]
#[allow(missing_docs)]
pub enum FontFormat {
    TrueType,
    Type1,
    BDF,
    PCF,
    Type42,
    CIDType1,
    CFF,
    PFR,
    WindowsFNT,
}

/// Handle obtained after Fontconfig has been initialised.
#[doc(alias = "FcConfig")]
pub struct FontConfig {
    cfg: Option<NonNull<sys::FcConfig>>,
}

///
impl FontConfig {
    /// Create a configuration
    // pub fn new() -> Option<Self> {
    //     #[cfg(feature = "dlopen")]
    //     if LIB_RESULT.is_err() {
    //         return None;
    //     }
    //     let cfg = unsafe { ffi_dispatch!(LIB, FcConfigCreate,) };
    //     Some(FontConfig {
    //         cfg: Some(NonNull::new(cfg)?),
    //     })
    // }

    /// Set configuration as default.
    ///
    /// Sets the current default configuration to config.
    /// Implicitly calls FcConfigBuildFonts if necessary,
    /// and FcConfigReference() to inrease the reference count in config since 2.12.0,
    /// returning FcFalse if that call fails.
    // pub fn set_current(config: &mut FontConfig) {
    //     //
    // }

    /// Execute substitutions
    ///
    /// Calls FcConfigSubstituteWithPat setting p_pat to NULL.
    /// Returns false if the substitution cannot be performed (due to allocation failure).
    /// Otherwise returns true.
    pub fn substitute(&mut self, pat: &mut Pattern, kind: MatchKind) {
        let ret = unsafe {
            ffi_dispatch!(
                LIB,
                FcConfigSubstitute,
                self.as_mut_ptr(),
                pat.as_mut_ptr(),
                kind.into()
            )
        };
        assert_eq!(ret, FcTrue);
    }

    /// Return the best font from a set of font sets
    ///
    /// Finds the font in sets most closely matching pattern and
    /// returns the result of FcFontRenderPrepare for that font and the provided pattern.
    /// This function should be called only after FcConfigSubstitute and FcDefaultSubstitute have been called for pattern;
    /// otherwise the results will not be correct.
    /// If config is NULL, the current configuration is used.
    /// Returns NULL if an error occurs during this process.
    #[doc(alias = "FcFontSetMatch")]
    pub fn fontset_match(&mut self, sets: &mut [FontSet], pat: &mut Pattern) -> OwnedPattern {
        // pat.default_substitute();
        // self.substitute(pat, MatchKind::Font);
        let mut result = sys::FcResultNoMatch;
        let pat = unsafe {
            ffi_dispatch!(
                LIB,
                FcFontSetMatch,
                self.as_mut_ptr(),
                &mut sets.as_mut_ptr().cast(),
                sets.len() as i32,
                pat.as_mut_ptr(),
                &mut result
            )
        };
        result.ok().unwrap();
        OwnedPattern {
            pat: NonNull::new(pat).unwrap(),
        }
    }

    fn as_mut_ptr(&mut self) -> *mut sys::FcConfig {
        if let Some(ref mut cfg) = self.cfg {
            cfg.as_ptr()
        } else {
            ptr::null_mut()
        }
    }

    #[allow(dead_code)]
    fn as_ptr(&self) -> *const sys::FcConfig {
        if let Some(ref cfg) = self.cfg {
            cfg.as_ptr()
        } else {
            ptr::null_mut()
        }
    }

    /// Find a font of the given `family` (e.g. Dejavu Sans, FreeSerif),
    /// optionally filtering by `style`. Both fields are case-insensitive.
    pub fn find(&mut self, family: String, style: Option<String>) -> Option<Font> {
        let mut pat = OwnedPattern::new();
        pat.add(&properties::FC_FAMILY, family);

        if let Some(style) = style {
            let style = style;
            pat.add(&properties::FC_STYLE, style);
        }

        let font_match = pat.font_match(self);

        font_match.name().and_then(|name| {
            font_match.filename().map(|filename| Font {
                name: name.to_owned(),
                path: PathBuf::from(filename),
            })
        })
    }
}

impl Default for FontConfig {
    /// Initialise fontconfig and returns the default config.
    ///
    /// **PANIC** : If fontconfig fails to initialise
    fn default() -> Self {
        let mut guard = INITIALIZED.lock().unwrap();
        #[cfg(feature = "dlopen")]
        if LIB_RESULT.is_err() {
            panic!("Failed to load fontconfig library");
        }
        assert_eq!(FcTrue, unsafe { ffi_dispatch!(LIB, FcInit,) });
        *guard += 1;
        FontConfig { cfg: None }
    }
}

impl Drop for FontConfig {
    fn drop(&mut self) {
        let guard = INITIALIZED.lock().unwrap();
        if guard.checked_sub(1).unwrap_or_default() == 0 {
            unsafe { ffi_dispatch!(LIB, FcFini,) };
        }
    }
}

/// A very high-level view of a font, only concerned with the name and its file location.
///
/// ##Example
/// ```rust
/// use fontconfig::{Font, FontConfig};
///
/// let mut config = FontConfig::default();
/// let font = config.find("sans-serif".to_string(), Some("italic".to_string())).unwrap();
/// println!("Name: {}\nPath: {}", font.name, font.path.display());
/// ```
pub struct Font {
    /// The true name of this font
    pub name: String,
    /// The location of this font on the filesystem.
    pub path: PathBuf,
}

impl Font {
    #[allow(dead_code)]
    fn print_debug(&self) {
        println!("Name: {}\nPath: {}", self.name, self.path.display());
    }
}

impl FromStr for FontFormat {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        match s {
            "TrueType" => Ok(FontFormat::TrueType),
            "Type 1" => Ok(FontFormat::Type1),
            "BDF" => Ok(FontFormat::BDF),
            "PCF" => Ok(FontFormat::PCF),
            "Type 42" => Ok(FontFormat::Type42),
            "CID Type 1" => Ok(FontFormat::CIDType1),
            "CFF" => Ok(FontFormat::CFF),
            "PFR" => Ok(FontFormat::PFR),
            "Windows FNT" => Ok(FontFormat::WindowsFNT),
            _ => Err(Error::UnknownFontFormat(s.to_string())),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// library version number
pub struct Version {
    /// major version number
    pub major: u32,
    /// minor version number
    pub minor: u32,
    /// micro version number
    pub revision: u32,
}

impl std::fmt::Display for Version {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}.{}.{}", self.major, self.minor, self.revision)
    }
}

/// Returns the version number of the library.
pub fn version() -> Version {
    let version = unsafe { ffi_dispatch!(LIB, FcGetVersion,) as u32 };
    let major = version / 10000;
    let version = version % 10000;
    let minor = version / 100;
    let revision = version % 100;

    Version {
        major,
        minor,
        revision,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        FontConfig::default();
    }

    #[test]
    fn find_font() {
        let mut config = FontConfig::default();
        config
            .find("dejavu sans".to_string(), None)
            .unwrap()
            .print_debug();
        config
            .find("dejavu sans".to_string(), Some("oblique".to_string()))
            .unwrap()
            .print_debug();
    }

    #[test]
    fn iter_and_print() {
        let mut config = FontConfig::default();
        let pat = OwnedPattern::new();
        let fontset = pat.font_list(&mut config, None);
        for pattern in fontset.iter() {
            println!("{:?}", pattern.name());
        }

        // Ensure that the set can be iterated again
        assert!(fontset.iter().count() > 0);
    }

    #[test]
    fn iter_lang_set() {
        let mut config = FontConfig::default();
        let mut pat = OwnedPattern::new();
        let family = "dejavu sans".to_string();
        pat.add(&properties::FC_FAMILY, family);
        let pattern = pat.font_match(&mut config);
        for lang in pattern.lang_set().unwrap().langs().iter() {
            println!("{:?}", lang);
        }

        // Test find
        assert!(pattern
            .lang_set()
            .unwrap()
            .langs()
            .iter()
            .any(|lang| lang == "za"));

        // Test collect
        let langs = pattern
            .lang_set()
            .unwrap()
            .langs()
            .iter()
            .map(ToString::to_string)
            .collect::<Vec<_>>();
        assert!(langs.iter().any(|l| l == "ie"));
    }

    #[test]
    fn iter_font_sort() {
        let mut config = FontConfig::default();
        let mut pat = OwnedPattern::new();
        let family = "dejavu sans".to_owned();
        pat.add(&properties::FC_FAMILY, family);
        pat.default_substitute();
        config.substitute(&mut pat, MatchKind::Pattern);
        let font_set = pat.font_sort(&mut config, false).unwrap();

        for font in font_set.iter() {
            println!("{:?}", font.name());
        }
        assert!(font_set.iter().count() > 1);
        assert!(font_set.iter().next().unwrap().name().unwrap() == "DejaVu Sans");
    }
}