Skip to main content

fontconfig/
lib.rs

1#![deny(missing_docs)]
2
3//! A wrapper around [freedesktop.org's Fontconfig library][homepage], for locating fonts on
4//! UNIX-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 to
6//! 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//! To use this crate, you need to have Fontconfig installed on your system.
17//! For example, install the package:
18//!
19//! * Arch Linux: `fontconfig`
20//! * Debian-based systems: `libfontconfig1-dev`
21//! * FreeBSD: `fontconfig`
22//! * Void Linux: `fontconfig-devel`
23//!
24//! Usage
25//! -----
26//!
27//! ### Example
28//!
29//! ```
30//! use fontconfig::{Fontconfig, FontconfigError};
31//!
32//! fn main() -> Result<(), FontconfigError> {
33//!     let fc = Fontconfig::new().expect("unable to init Fontconfig");
34//!     // `Fontconfig::find()` returns `Result` (will rarely be `Err` but still could be)
35//!     let font = fc.find("freeserif", None)?;
36//!     // `name` is a `String`, `path` is a `Path`
37//!     println!("Name: {}\nPath: {}", font.name, font.path.display());
38//!     Ok(())
39//! }
40//! ```
41//!
42//! For more advanced usage, see [list_fonts] and the [Pattern] type.
43//!
44//! See the [examples directory in the repository](https://github.com/yeslogic/fontconfig-rs/blob/master/examples/fc-list.rs)
45//! for more examples.
46//!
47//! ### Cargo Features
48//!
49//! | Feature       | Description                       | Default Enabled | Extra Dependencies    |
50//! |---------------|-----------------------------------|:---------------:|-----------------------|
51//! | `dlopen`      | [dlopen] libfontconfig at runtime |        ❌       |                       |
52//!
53//! The `dlopen` feature enables building this crate without dynamically linking to the Fontconfig C
54//! library at link time. Instead, Fontconfig will be dynamically loaded at runtime with the
55//! [dlopen] function. This can be useful in cross-compiling situations as you don't need to have a
56//! version of Fontconfig available for the target platform available at compile time. This can also
57//! be enabled by setting the `RUST_FONTCONFIG_DLOPEN` environment variable.
58//!
59//! [dlopen]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/dlopen.html
60//!
61//! ### Thread Safety
62//!
63//! **Note:**
64
65use fontconfig_sys as sys;
66use fontconfig_sys::ffi_dispatch;
67
68#[cfg(feature = "dlopen")]
69use sys::statics::{LIB, LIB_RESULT};
70#[cfg(not(feature = "dlopen"))]
71use sys::*;
72
73use std::ffi::{self, c_char, c_int, CStr, CString};
74use std::marker::PhantomData;
75use std::path::PathBuf;
76use std::str::{self, FromStr};
77use std::{fmt, ptr};
78
79pub use sys::constants::*;
80use sys::{FcBool, FcCharSet, FcPattern, FcResult};
81
82#[allow(non_upper_case_globals)]
83const FcTrue: FcBool = 1;
84#[allow(non_upper_case_globals, dead_code)]
85const FcFalse: FcBool = 0;
86
87/// Handle obtained after Fontconfig has been initialised.
88pub struct Fontconfig {
89    _initialised: (),
90}
91
92/// Fontconfig error
93#[derive(Debug)]
94pub enum FontconfigError {
95    /// A Fontconfig operation failed with FcFalse or NULL pointer.
96    ///
97    /// Fontconfig does not provide more information in these cases.
98    Failed,
99    /// An attempt to convert a Rust string to C string failed due to it containing one or more NUL bytes.
100    NulError,
101    /// An attempt to convert a C string to Rust string failed because it was not valid UTF-8.
102    Utf8Error,
103    // FcResult variants, minus FcResultMatch
104    /// A find/match operation returned no matches.
105    NoMatch,
106    /// There was a type mismatch between expected and supplied data.
107    TypeMismatch,
108    /// There is no item with the supplied id.
109    NoId,
110    /// Out of memory.
111    OutOfMemory,
112}
113
114/// Error type returned from [Pattern::format].
115///
116/// The error holds the name of the unknown format.
117#[derive(Debug)]
118pub struct UnknownFontFormat(pub String);
119
120/// The format of a font matched by Fontconfig.
121#[derive(Eq, PartialEq, Debug)]
122#[allow(missing_docs)]
123pub enum FontFormat {
124    TrueType,
125    Type1,
126    BDF,
127    PCF,
128    Type42,
129    CIDType1,
130    CFF,
131    PFR,
132    WindowsFNT,
133}
134
135/// Flag indicating whether results from [Pattern::sort_fonts] should be trimmed
136/// to exclude fonts that do not cover the Unicode coverage in the pattern.
137#[derive(Eq, PartialEq, Debug)]
138pub enum UnicodeCoverage {
139    /// Trim results that don't include Unicode coverage.
140    Trim,
141    /// Don't trim results.
142    NoTrim,
143}
144
145trait ToResult {
146    fn to_result(self) -> Result<(), FontconfigError>;
147}
148
149impl Fontconfig {
150    /// Initialise Fontconfig and return a handle allowing further interaction with the API.
151    ///
152    /// If Fontconfig fails to initialise, returns `None`.
153    ///
154    /// ## Example
155    ///
156    /// ```
157    /// use fontconfig::{Fontconfig};
158    ///
159    /// let fc = Fontconfig::new().expect("unable to initialise Fontconfig");
160    /// ```
161    #[doc(alias = "FcInit")]
162    pub fn new() -> Option<Self> {
163        #[cfg(feature = "dlopen")]
164        if LIB_RESULT.is_err() {
165            return None;
166        }
167        if unsafe { ffi_dispatch!(LIB, FcInit,) == FcTrue } {
168            Some(Fontconfig { _initialised: () })
169        } else {
170            None
171        }
172    }
173
174    /// Find a font of the given `family` and `style`.
175    ///
176    /// Results can optionally be filtered by `style`. Both fields are case-insensitive.
177    pub fn find(&self, family: &str, style: Option<&str>) -> Result<Font, FontconfigError> {
178        Font::find(self, family, style)
179    }
180}
181
182// There were issues with calling FcFini more than once in Fontconfig versions prior
183// to 2.17.0. As of 14 May 2026 earlier versions are still commonly used, so we don't
184// call it automatically.
185// https://gitlab.freedesktop.org/fontconfig/fontconfig/-/merge_requests/410
186// impl Drop for Fontconfig {
187//     fn drop(&mut self) {
188//         unsafe { ffi_dispatch!(LIB, FcFini,) };
189//     }
190// }
191
192/// A  high-level view of a font, only concerned with the name and its file location.
193///
194/// ## Example
195///
196/// ```rust
197/// use fontconfig::{Font, Fontconfig};
198///
199/// let fc = Fontconfig::new().unwrap();
200/// let font = fc.find("sans-serif", Some("italic")).unwrap();
201/// println!("Name: {}\nPath: {}", font.name, font.path.display());
202/// ```
203pub struct Font {
204    /// The true name of this font.
205    pub name: String,
206    /// The location of this font on the filesystem.
207    pub path: PathBuf,
208    /// The index of the font within the file.
209    pub index: Option<i32>,
210}
211
212impl Font {
213    fn find(fc: &Fontconfig, family: &str, style: Option<&str>) -> Result<Font, FontconfigError> {
214        let mut pat = Pattern::new(fc)?;
215        let family = CString::new(family)?;
216        pat.add_string(FC_FAMILY, &family)?;
217
218        if let Some(style) = style {
219            let style = CString::new(style)?;
220            pat.add_string(FC_STYLE, &style)?;
221        }
222
223        let font_match = pat.font_match()?;
224        let name = font_match.name()?;
225        let filename = font_match.filename()?;
226
227        Ok(Font {
228            name: name.to_owned(),
229            path: PathBuf::from(filename),
230            index: font_match.face_index().ok(),
231        })
232    }
233
234    #[cfg(test)]
235    fn print_debug(&self) {
236        println!(
237            "Name: {}\nPath: {}\nIndex: {:?}",
238            self.name,
239            self.path.display(),
240            self.index
241        );
242    }
243}
244
245/// A safe wrapper around fontconfig's `FcPattern`.
246///
247/// See the [fc-match example](https://github.com/yeslogic/fontconfig-rs/blob/master/examples/fc-match.rs)
248/// and
249/// [fc-list example](https://github.com/yeslogic/fontconfig-rs/blob/master/examples/fc-list.rs)
250/// for an example of using `Pattern`.
251///
252#[repr(C)]
253#[doc(alias = "FcPattern")]
254pub struct Pattern<'fc> {
255    /// Raw pointer to `FcPattern`
256    pat: *mut FcPattern,
257    fc: &'fc Fontconfig,
258}
259
260impl<'fc> Pattern<'fc> {
261    /// Create a new empty `Pattern`.
262    ///
263    /// The [Fontconfig] handle is obtained from [Fontconfig::new].
264    #[doc(alias = "FcPatternCreate")]
265    pub fn new(fc: &Fontconfig) -> Result<Pattern<'_>, FontconfigError> {
266        let pat = unsafe { ffi_dispatch!(LIB, FcPatternCreate,) };
267        is_non_null(pat)
268            .then_some(Pattern { pat, fc })
269            .ok_or(FontconfigError::Failed)
270    }
271
272    /// Create a `Pattern` from a raw fontconfig FcPattern pointer.
273    ///
274    /// The pattern is referenced.
275    ///
276    /// **Safety:** The pattern pointer must be valid/non-null.
277    pub unsafe fn from_pattern(fc: &Fontconfig, pat: *mut FcPattern) -> Pattern<'_> {
278        assert!(is_non_null(pat));
279        ffi_dispatch!(LIB, FcPatternReference, pat);
280
281        Pattern { pat, fc }
282    }
283
284    /// Add a key-value pair of type `String` to this pattern.
285    ///
286    /// See useful keys in the [fontconfig reference][1].
287    ///
288    /// [1]: http://www.freedesktop.org/software/fontconfig/fontconfig-devel/x19.html
289    #[doc(alias = "FcPatternAddString")]
290    pub fn add_string(&mut self, name: &CStr, val: &CStr) -> Result<(), FontconfigError> {
291        unsafe {
292            ffi_dispatch!(
293                LIB,
294                FcPatternAddString,
295                self.pat,
296                name.as_ptr(),
297                val.as_ptr() as *const u8
298            )
299            .to_result()
300        }
301    }
302
303    /// Add a key-value pair of type `Int` to this pattern.
304    ///
305    /// See useful keys in the [fontconfig reference][1].
306    ///
307    /// [1]: http://www.freedesktop.org/software/fontconfig/fontconfig-devel/x19.html
308    #[doc(alias = "FcPatternAddInteger")]
309    pub fn add_integer(&mut self, name: &CStr, val: c_int) -> Result<(), FontconfigError> {
310        unsafe { ffi_dispatch!(LIB, FcPatternAddInteger, self.pat, name.as_ptr(), val).to_result() }
311    }
312
313    /// Add a charset to this pattern.
314    #[doc(alias = "FcPatternAddCharSet")]
315    pub fn add_charset(&mut self, val: CharSet) -> Result<(), FontconfigError> {
316        unsafe {
317            ffi_dispatch!(
318                LIB,
319                FcPatternAddCharSet,
320                self.pat,
321                FC_CHARSET.as_ptr(),
322                val.char_set
323            )
324            .to_result()
325        }
326    }
327
328    /// Get string the value for a key from this pattern.
329    #[doc(alias = "FcPatternGetString")]
330    pub fn get_string<'a>(&'a self, name: &'a CStr) -> Result<&'a str, FontconfigError> {
331        unsafe {
332            let mut ret: *mut sys::FcChar8 = ptr::null_mut();
333            ffi_dispatch!(
334                LIB,
335                FcPatternGetString,
336                self.pat,
337                name.as_ptr(),
338                0,
339                &mut ret as *mut _
340            )
341            .to_result()?;
342            let cstr = CStr::from_ptr(ret as *const c_char);
343            cstr.to_str().map_err(FontconfigError::from)
344        }
345    }
346
347    /// Get the integer value for a key from this pattern.
348    #[doc(alias = "FcPatternGetInteger")]
349    pub fn get_int(&self, name: &CStr) -> Result<i32, FontconfigError> {
350        unsafe {
351            let mut ret: i32 = 0;
352            ffi_dispatch!(
353                LIB,
354                FcPatternGetInteger,
355                self.pat,
356                name.as_ptr(),
357                0,
358                &mut ret as *mut i32
359            )
360            .to_result()?;
361            Ok(ret)
362        }
363    }
364
365    /// Print this pattern to stdout with all its values.
366    #[doc(alias = "FcPatternPrint")]
367    pub fn print(&self) {
368        unsafe {
369            ffi_dispatch!(LIB, FcPatternPrint, &*self.pat);
370        }
371    }
372
373    /// Supplies default values for underspecified font patterns.
374    ///
375    /// * Patterns without a specified style or weight are set to Medium.
376    /// * Patterns without a specified style or slant are set to Roman.
377    /// * Patterns without a specified pixel size are given one computed from any specified point size
378    ///   (default 12), dpi (default 75) and scale (default 1).
379    ///
380    /// *Note:* [font_match][Self::font_match] and [sort_fonts][Self::sort_fonts] call this so you
381    /// don't need to manually call it when using those methods.
382    ///
383    /// [Fontconfig reference](https://www.freedesktop.org/software/fontconfig/fontconfig-devel/fcdefaultsubstitute.html)
384    #[doc(alias = "FcDefaultSubstitute")]
385    pub fn default_substitute(&mut self) {
386        unsafe {
387            ffi_dispatch!(LIB, FcDefaultSubstitute, self.pat);
388        }
389    }
390
391    /// Execute substitutions.
392    ///
393    /// *Note:* [font_match][Self::font_match] and [sort_fonts][Self::sort_fonts] call this so you
394    /// don't need to manually call it when using those methods.
395    ///
396    /// [Fontconfig reference](https://www.freedesktop.org/software/fontconfig/fontconfig-devel/fcconfigsubstitute.html)
397    #[doc(alias = "FcConfigSubstitute")]
398    pub fn config_substitute(&mut self) -> Result<(), FontconfigError> {
399        unsafe {
400            ffi_dispatch!(
401                LIB,
402                FcConfigSubstitute,
403                ptr::null_mut(),
404                self.pat,
405                sys::FcMatchPattern
406            )
407            .to_result()
408        }
409    }
410
411    /// Get the best available match for this pattern, returned as a new pattern.
412    ///
413    /// See [the fc-match example](https://github.com/yeslogic/fontconfig-rs/blob/master/examples/fc-match.rs)
414    /// for an example of using this function.
415    #[doc(alias = "FcFontMatch")]
416    pub fn font_match(&mut self) -> Result<Pattern<'_>, FontconfigError> {
417        self.config_substitute()?;
418        self.default_substitute();
419
420        unsafe {
421            let mut res = sys::FcResultNoMatch;
422            let pattern_ptr = ffi_dispatch!(LIB, FcFontMatch, ptr::null_mut(), self.pat, &mut res);
423            is_non_null(pattern_ptr)
424                .then(|| Pattern::from_pattern(self.fc, pattern_ptr))
425                .ok_or(FontconfigError::Failed)
426        }
427    }
428
429    /// Returns a [`FontSet`] containing fonts sorted by closeness to this pattern.
430    ///
431    /// If `trim` is [UnicodeCoverage::Trim], elements in the list which don't include Unicode
432    /// coverage provided by earlier elements in the list are elided.
433    ///
434    /// See the [Fontconfig reference][1] for more details.
435    ///
436    /// [1]: https://www.freedesktop.org/software/fontconfig/fontconfig-devel/fcfontsort.html
437    #[doc(alias = "FcFontSort")]
438    pub fn sort_fonts(&mut self, trim: UnicodeCoverage) -> Result<FontSet<'fc>, FontconfigError> {
439        // Docs: This function should be called only after FcConfigSubstitute and FcDefaultSubstitute
440        // have been called for p; otherwise the results will not be correct.
441        self.config_substitute()?;
442        self.default_substitute();
443
444        // FcFontSort always returns a (possibly empty) set so we don't need to check this.
445        let mut res = sys::FcResultNoMatch;
446        let unicode_coverage = ptr::null_mut();
447        let config = ptr::null_mut();
448        unsafe {
449            let raw_set = ffi_dispatch!(
450                LIB,
451                FcFontSort,
452                config,
453                self.pat,
454                trim as FcBool,
455                unicode_coverage,
456                &mut res
457            );
458            Ok(FontSet::from_raw(self.fc, raw_set))
459        }
460    }
461
462    /// Get the "fullname" (human-readable name) of this pattern.
463    pub fn name(&self) -> Result<&str, FontconfigError> {
464        self.get_string(FC_FULLNAME)
465    }
466
467    /// Get the "file" (path on the filesystem) of this font pattern.
468    pub fn filename(&self) -> Result<&str, FontconfigError> {
469        self.get_string(FC_FILE)
470    }
471
472    /// Get the "index" (the index of the font within the file) of this pattern.
473    pub fn face_index(&self) -> Result<i32, FontconfigError> {
474        self.get_int(FC_INDEX)
475    }
476
477    /// Get the "slant" (italic, oblique or roman) of this pattern.
478    pub fn slant(&self) -> Result<i32, FontconfigError> {
479        self.get_int(FC_SLANT)
480    }
481
482    /// Get the "weight" (light, medium, demibold, bold or black) of this pattern.
483    pub fn weight(&self) -> Result<i32, FontconfigError> {
484        self.get_int(FC_WEIGHT)
485    }
486
487    /// Get the "width" (condensed, normal or expanded) of this pattern.
488    pub fn width(&self) -> Result<i32, FontconfigError> {
489        self.get_int(FC_WIDTH)
490    }
491
492    /// Get the "fontformat" of this pattern.
493    ///
494    /// If Fontconfig returns an unknown font format, [`UnknownFontFormat`] is returned,
495    /// which contains the format returned by Fontconfig.
496    pub fn format(&self) -> Result<FontFormat, UnknownFontFormat> {
497        self.get_string(FC_FONTFORMAT)
498            .map_err(|_| UnknownFontFormat(String::new()))
499            .and_then(|format| format.parse())
500    }
501
502    /// Get the language set of this pattern.
503    #[doc(alias = "FcPatternGetLangSet")]
504    pub fn lang_set(&self) -> Result<StrList<'_>, FontconfigError> {
505        unsafe {
506            let mut lang_set: *mut sys::FcLangSet = ptr::null_mut();
507            ffi_dispatch!(
508                LIB,
509                FcPatternGetLangSet,
510                self.pat,
511                FC_LANG.as_ptr(),
512                0,
513                &mut lang_set as *mut _
514            )
515            .to_result()?;
516            let ss: *mut sys::FcStrSet = ffi_dispatch!(LIB, FcLangSetGetLangs, lang_set);
517            if ss.is_null() {
518                return Err(FontconfigError::Failed);
519            }
520            let lang_strs: *mut sys::FcStrList = ffi_dispatch!(LIB, FcStrListCreate, ss);
521            if lang_strs.is_null() {
522                return Err(FontconfigError::Failed);
523            }
524            Ok(StrList::from_raw(self.fc, lang_strs))
525        }
526    }
527
528    /// Returns a raw pointer to underlying `FcPattern`.
529    pub fn as_ptr(&self) -> *const FcPattern {
530        self.pat
531    }
532
533    /// Returns a mutable pointer to the underlying `FcPattern`.
534    pub fn as_mut_ptr(&mut self) -> *mut FcPattern {
535        self.pat
536    }
537}
538
539impl<'fc> std::fmt::Debug for Pattern<'fc> {
540    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
541        let fcstr = unsafe { ffi_dispatch!(LIB, FcNameUnparse, self.pat) };
542        let fcstr = unsafe { CStr::from_ptr(fcstr as *const c_char) };
543        let result = write!(f, "{:?}", fcstr);
544        unsafe { ffi_dispatch!(LIB, FcStrFree, fcstr.as_ptr() as *mut u8) };
545        result
546    }
547}
548
549impl<'fc> Clone for Pattern<'fc> {
550    /// Clones a `Pattern` using `FcPatternDuplicate`.
551    ///
552    /// # Panics
553    ///
554    /// Panics if the `Pattern` cannot be allocated.
555    #[doc(alias = "FcPatternDuplicate")]
556    fn clone(&self) -> Self {
557        let clone = unsafe { ffi_dispatch!(LIB, FcPatternDuplicate, self.pat) };
558        if clone.is_null() {
559            panic!("Unable to clone. FcPatternDuplicate returned NULL")
560        }
561
562        Pattern {
563            pat: clone,
564            fc: self.fc,
565        }
566    }
567}
568
569impl<'fc> Drop for Pattern<'fc> {
570    fn drop(&mut self) {
571        unsafe {
572            ffi_dispatch!(LIB, FcPatternDestroy, self.pat);
573        }
574    }
575}
576
577/// Wrapper around `FcStrList`.
578///
579/// The wrapper implements [Iterator] so it can be iterated directly, filtered etc.
580///
581/// **Note:** Any entries in the `StrList` that are not valid UTF-8 will be skipped.
582///
583/// ```
584/// use fontconfig::{Fontconfig, FontconfigError, Pattern};
585///
586/// # fn main() -> Result<(), FontconfigError> {
587/// let fc = Fontconfig::new().expect("unable to init Fontconfig");
588///
589/// // Find fonts that support japanese
590/// let fonts = fontconfig::list_fonts(&Pattern::new(&fc)?, None)?;
591/// let ja_fonts: Vec<_> = fonts
592///     .iter()
593///     .filter(|p| p.lang_set().map_or(false, |mut langs| langs.any(|l| l == "ja")))
594///     .collect();
595/// # Ok(())
596/// # }
597/// ```
598#[doc(alias = "FcStrList")]
599pub struct StrList<'a> {
600    list: *mut sys::FcStrList,
601    _life: PhantomData<&'a sys::FcStrList>,
602}
603
604impl<'a> StrList<'a> {
605    /// Wrap an existing `FcStrSet`.
606    ///
607    /// The returned wrapper assumes ownership of the `FcStrSet`.
608    ///
609    /// **Safety:** The string list pointer must be valid/non-null.
610    unsafe fn from_raw(_: &Fontconfig, raw_list: *mut sys::FcStrSet) -> Self {
611        Self {
612            list: raw_list,
613            _life: PhantomData,
614        }
615    }
616}
617
618impl<'a> Drop for StrList<'a> {
619    fn drop(&mut self) {
620        unsafe { ffi_dispatch!(LIB, FcStrListDone, self.list) };
621    }
622}
623
624impl<'a> Iterator for StrList<'a> {
625    type Item = &'a str;
626
627    fn next(&mut self) -> Option<&'a str> {
628        let lang_str: *mut sys::FcChar8 = unsafe { ffi_dispatch!(LIB, FcStrListNext, self.list) };
629        if lang_str.is_null() {
630            None
631        } else {
632            match unsafe { CStr::from_ptr(lang_str as *const c_char) }.to_str() {
633                Ok(s) => Some(s),
634                _ => self.next(),
635            }
636        }
637    }
638}
639
640/// Wrapper around `FcFontSet` representing a set of fonts.
641///
642/// The `FontSet` can be iterated, yielding the [Pattern]s in the set.
643#[doc(alias = "FcFontSet")]
644pub struct FontSet<'fc> {
645    fcset: *mut sys::FcFontSet,
646    fc: &'fc Fontconfig,
647}
648
649impl<'fc> FontSet<'fc> {
650    /// Create a new, empty `FontSet`.
651    pub fn new(fc: &Fontconfig) -> Result<FontSet<'_>, FontconfigError> {
652        let fcset = unsafe { ffi_dispatch!(LIB, FcFontSetCreate,) };
653        is_non_null(fcset)
654            .then_some(FontSet { fcset, fc })
655            .ok_or(FontconfigError::Failed)
656    }
657
658    /// Wrap an existing `FcFontSet`.
659    ///
660    /// The returned wrapper assumes ownership of the `FcFontSet`.
661    ///
662    /// **Safety:** The font set pointer must be valid/non-null.
663    pub unsafe fn from_raw(fc: &Fontconfig, raw_set: *mut sys::FcFontSet) -> FontSet<'_> {
664        FontSet { fcset: raw_set, fc }
665    }
666
667    /// Add a `Pattern` to this `FontSet`.
668    #[doc(alias = "FcFontSetAdd")]
669    pub fn add_pattern(&mut self, pat: Pattern) -> Result<(), FontconfigError> {
670        unsafe { ffi_dispatch!(LIB, FcFontSetAdd, self.fcset, pat.pat).to_result() }
671    }
672
673    /// Print this `FontSet` to stdout.
674    #[doc(alias = "FcFontSetPrint")]
675    pub fn print(&self) {
676        unsafe { ffi_dispatch!(LIB, FcFontSetPrint, self.fcset) };
677    }
678
679    /// Iterate the fonts (as `Patterns`) in this `FontSet`.
680    pub fn iter(&self) -> impl Iterator<Item = Pattern<'_>> {
681        let patterns = unsafe {
682            let fontset = self.fcset;
683            // The set may be empty, in which case .fonts is NULL, but slices require
684            // the pointers to be non-NULL.
685            if (*fontset).nfont > 0 {
686                std::slice::from_raw_parts((*fontset).fonts, (*fontset).nfont as usize)
687            } else {
688                &[]
689            }
690        };
691        patterns
692            .iter()
693            .map(move |&pat| unsafe { Pattern::from_pattern(self.fc, pat) })
694    }
695}
696
697impl<'fc> Drop for FontSet<'fc> {
698    fn drop(&mut self) {
699        unsafe { ffi_dispatch!(LIB, FcFontSetDestroy, self.fcset) }
700    }
701}
702
703/// Returns a [FontSet] containing [Font]s that match the supplied `pattern` and `objects`.
704///
705/// Creates patterns from those fonts containing only the objects in `objects` and returns
706/// the set of unique such patterns. In other words, `objects` indicates that properties
707/// of the font you want populated. The values of these on the matched fonts can be retrieved
708/// using [Pattern::get_string] or [Pattern::get_int] on the returned patterns as wells as the
709/// convenience functions such as [Pattern::name] and [Pattern::filename].
710///
711/// See [the fc-list example](https://github.com/yeslogic/fontconfig-rs/blob/master/examples/fc-list.rs)
712/// for an example of using this function.
713///
714#[doc(alias = "FcFontList")]
715pub fn list_fonts<'fc>(
716    pattern: &Pattern<'fc>,
717    objects: Option<&ObjectSet>,
718) -> Result<FontSet<'fc>, FontconfigError> {
719    let os = objects.map(|o| o.fcset).unwrap_or(ptr::null_mut());
720    unsafe {
721        let raw_set = ffi_dispatch!(LIB, FcFontList, ptr::null_mut(), pattern.pat, os);
722        is_non_null(raw_set)
723            .then(|| FontSet::from_raw(pattern.fc, raw_set))
724            .ok_or(FontconfigError::Failed)
725    }
726}
727
728/// Wrapper around `FcObjectSet`.
729#[doc(alias = "FcObjectSet")]
730pub struct ObjectSet {
731    fcset: *mut sys::FcObjectSet,
732}
733
734impl ObjectSet {
735    /// Create a new, empty `ObjectSet`.
736    #[doc(alias = "FcObjectSetCreate")]
737    pub fn new(_: &Fontconfig) -> Result<ObjectSet, FontconfigError> {
738        let fcset = unsafe { ffi_dispatch!(LIB, FcObjectSetCreate,) };
739        is_non_null(fcset)
740            .then(|| ObjectSet { fcset })
741            .ok_or(FontconfigError::Failed)
742    }
743
744    /// Wrap an existing `FcObjectSet`.
745    ///
746    /// The `FcObjectSet` must not be null. This method assumes ownership of the `FcObjectSet`.
747    ///
748    /// **Safety:** The object set pointer must be valid/non-null.
749    pub unsafe fn from_raw(_: &Fontconfig, raw_set: *mut sys::FcObjectSet) -> ObjectSet {
750        assert!(!raw_set.is_null());
751        ObjectSet { fcset: raw_set }
752    }
753
754    /// Add a string to the `ObjectSet`.
755    #[doc(alias = "FcObjectSetAdd")]
756    pub fn add(&mut self, name: &CStr) -> Result<(), FontconfigError> {
757        unsafe { ffi_dispatch!(LIB, FcObjectSetAdd, self.fcset, name.as_ptr()).to_result() }
758    }
759}
760
761impl Drop for ObjectSet {
762    fn drop(&mut self) {
763        unsafe { ffi_dispatch!(LIB, FcObjectSetDestroy, self.fcset) }
764    }
765}
766
767impl FromStr for FontFormat {
768    type Err = UnknownFontFormat;
769
770    fn from_str(s: &str) -> Result<Self, Self::Err> {
771        match s {
772            "TrueType" => Ok(FontFormat::TrueType),
773            "Type 1" => Ok(FontFormat::Type1),
774            "BDF" => Ok(FontFormat::BDF),
775            "PCF" => Ok(FontFormat::PCF),
776            "Type 42" => Ok(FontFormat::Type42),
777            "CID Type 1" => Ok(FontFormat::CIDType1),
778            "CFF" => Ok(FontFormat::CFF),
779            "PFR" => Ok(FontFormat::PFR),
780            "Windows FNT" => Ok(FontFormat::WindowsFNT),
781            _ => Err(UnknownFontFormat(s.to_string())),
782        }
783    }
784}
785
786impl fmt::Display for FontFormat {
787    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
788        match self {
789            FontFormat::TrueType => write!(f, "TrueType"),
790            FontFormat::Type1 => write!(f, "Type 1"),
791            FontFormat::BDF => write!(f, "BDF"),
792            FontFormat::PCF => write!(f, "PCF"),
793            FontFormat::Type42 => write!(f, "Type 42"),
794            FontFormat::CIDType1 => write!(f, "CID Type 1"),
795            FontFormat::CFF => write!(f, "CFF"),
796            FontFormat::PFR => write!(f, "PFR"),
797            FontFormat::WindowsFNT => write!(f, "Windows FNT"),
798        }
799    }
800}
801
802/// Wrapper around `FcCharSet`.
803#[repr(C)]
804pub struct CharSet {
805    char_set: *mut FcCharSet,
806}
807
808impl<'fc> CharSet {
809    /// Create a new, empty `CharSet`.
810    #[doc(alias = "FcCharSetCreate")]
811    pub fn new(_: &'fc Fontconfig) -> Result<Self, FontconfigError> {
812        let char_set = unsafe { ffi_dispatch!(LIB, FcCharSetCreate,) };
813        is_non_null(char_set)
814            .then_some(CharSet { char_set })
815            .ok_or(FontconfigError::Failed)
816    }
817
818    /// Add a char to the `CharSet`.
819    #[doc(alias = "FcCharSetAddChar")]
820    pub fn add_char(&mut self, c: char) -> Result<(), FontconfigError> {
821        unsafe { ffi_dispatch!(LIB, FcCharSetAddChar, self.char_set, c as u32).to_result() }
822    }
823}
824
825impl Drop for CharSet {
826    fn drop(&mut self) {
827        unsafe {
828            ffi_dispatch!(LIB, FcCharSetDestroy, self.char_set);
829        }
830    }
831}
832
833impl fmt::Display for FontconfigError {
834    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
835        match self {
836            FontconfigError::Failed => f.write_str("operation failed"),
837            FontconfigError::NulError => f.write_str("string contained NUL byte"),
838            FontconfigError::Utf8Error => f.write_str("string contained invalid UTF-8"),
839            FontconfigError::NoMatch => f.write_str("no matches"),
840            FontconfigError::TypeMismatch => f.write_str("type mismatch"),
841            FontconfigError::NoId => f.write_str("no matching id"),
842            FontconfigError::OutOfMemory => f.write_str("out of memory"),
843        }
844    }
845}
846
847impl std::error::Error for FontconfigError {}
848
849impl From<str::Utf8Error> for FontconfigError {
850    fn from(_err: str::Utf8Error) -> Self {
851        FontconfigError::Utf8Error
852    }
853}
854
855impl From<ffi::NulError> for FontconfigError {
856    fn from(_err: ffi::NulError) -> Self {
857        FontconfigError::NulError
858    }
859}
860
861impl ToResult for FcBool {
862    fn to_result(self) -> Result<(), FontconfigError> {
863        if self == FcTrue {
864            Ok(())
865        } else {
866            Err(FontconfigError::Failed)
867        }
868    }
869}
870
871impl ToResult for FcResult {
872    fn to_result(self) -> Result<(), FontconfigError> {
873        match self {
874            sys::FcResultMatch => Ok(()),
875            sys::FcResultNoMatch => Err(FontconfigError::NoMatch),
876            sys::FcResultTypeMismatch => Err(FontconfigError::TypeMismatch),
877            sys::FcResultNoId => Err(FontconfigError::NoId),
878            sys::FcResultOutOfMemory => Err(FontconfigError::OutOfMemory),
879            _ => Err(FontconfigError::Failed),
880        }
881    }
882}
883
884fn is_non_null<T>(ptr: *mut T) -> bool {
885    !ptr.is_null()
886}
887
888#[cfg(test)]
889mod tests {
890    use super::*;
891
892    use std::sync::Mutex;
893
894    // Shared handle for tests, which run in separate threads.
895    // NOTE: Drop is not called on static items, so valgrind will see this as leaked memory.
896    static FC: Mutex<Option<Fontconfig>> = Mutex::new(None);
897
898    #[test]
899    fn test_find_font() -> Result<(), FontconfigError> {
900        let mut lock = FC.lock().unwrap();
901        let fc = lock.get_or_insert_with(|| Fontconfig::new().expect("FcInit"));
902        fc.find("dejavu sans", None)?.print_debug();
903        fc.find("dejavu sans", Some("oblique"))?.print_debug();
904        Ok(())
905    }
906
907    #[test]
908    fn test_iter_and_print() -> Result<(), FontconfigError> {
909        let mut lock = FC.lock().unwrap();
910        let fc = lock.get_or_insert_with(|| Fontconfig::new().expect("FcInit"));
911
912        let fontset = list_fonts(&Pattern::new(&fc)?, None)?;
913        for pattern in fontset.iter() {
914            println!("{:?}", pattern.name());
915        }
916
917        // Ensure that the set can be iterated again
918        assert!(fontset.iter().count() > 0);
919        Ok(())
920    }
921
922    #[test]
923    fn test_empty_font_set() -> Result<(), FontconfigError> {
924        let mut lock = FC.lock().unwrap();
925        let fc = lock.get_or_insert_with(|| Fontconfig::new().expect("FcInit"));
926
927        let mut pat = Pattern::new(&fc)?;
928        let family = CString::new("xxx yyy zzz does not exist")?;
929        pat.add_string(FC_FAMILY, &family)?;
930
931        let fontset = list_fonts(&pat, None)?;
932        // Prior to a bug fix this would fail when the set was empty due to trying
933        // to create a slice from a NULL pointer (FcSet.fonts).
934        assert_eq!(fontset.iter().count(), 0);
935        Ok(())
936    }
937
938    #[test]
939    fn iter_lang_set() -> Result<(), FontconfigError> {
940        let mut lock = FC.lock().unwrap();
941        let fc = lock.get_or_insert_with(|| Fontconfig::new().expect("FcInit"));
942
943        let mut pat = Pattern::new(&fc)?;
944        let family = CString::new("dejavu sans")?;
945        pat.add_string(FC_FAMILY, &family)?;
946        let pattern = pat.font_match()?;
947        for lang in pattern.lang_set()? {
948            println!("{:?}", lang);
949        }
950
951        // Test find
952        assert!(pattern.lang_set()?.find(|&lang| lang == "za").is_some());
953
954        // Test collect
955        let langs = pattern.lang_set()?.collect::<Vec<_>>();
956        assert!(langs.iter().find(|&&l| l == "ie").is_some());
957        Ok(())
958    }
959
960    #[test]
961    fn test_sort_fonts() -> Result<(), FontconfigError> {
962        let mut lock = FC.lock().unwrap();
963        let fc = lock.get_or_insert_with(|| Fontconfig::new().expect("FcInit"));
964
965        let mut pat = Pattern::new(&fc)?;
966        let family = CString::new("dejavu sans")?;
967        pat.add_string(FC_FAMILY, &family)?;
968
969        let style = CString::new("oblique")?;
970        pat.add_string(FC_STYLE, &style)?;
971
972        let font_set = pat.sort_fonts(UnicodeCoverage::NoTrim)?;
973
974        for pattern in font_set.iter() {
975            println!("{:?}", pattern.name());
976        }
977
978        // Ensure that the set can be iterated again
979        assert!(font_set.iter().count() > 0);
980        Ok(())
981    }
982
983    #[test]
984    fn finds_font_containing_charset() -> Result<(), FontconfigError> {
985        let mut lock = FC.lock().unwrap();
986        let fc = lock.get_or_insert_with(|| Fontconfig::new().expect("FcInit"));
987
988        let mut pat = Pattern::new(&fc)?;
989        let mut char_set = CharSet::new(&fc)?;
990        char_set.add_char('a')?;
991        pat.add_charset(char_set)?;
992        let font_set = list_fonts(&pat, None)?;
993
994        // Should find at least one font
995        assert!(font_set.iter().count() > 0);
996        Ok(())
997    }
998
999    #[test]
1000    fn does_not_find_missing_charset() -> Result<(), FontconfigError> {
1001        let mut lock = FC.lock().unwrap();
1002        let fc = lock.get_or_insert_with(|| Fontconfig::new().expect("FcInit"));
1003
1004        let mut pat = Pattern::new(&fc)?;
1005        let mut char_set = CharSet::new(&fc)?;
1006        // DejaVu Sans does not support CJK so try finding it for the following U+5317
1007        char_set.add_char('北')?;
1008        let family = CString::new("dejavu sans")?;
1009        pat.add_string(FC_FAMILY, &family)?;
1010        pat.add_charset(char_set)?;
1011        let font_set = list_fonts(&pat, None)?;
1012
1013        // Font set should be empty
1014        assert_eq!(font_set.iter().count(), 0);
1015        Ok(())
1016    }
1017}