norad 0.8.0

Read and write Unified Font Object files.
Documentation
//! Load only requested font data.

use std::path::Path;

/// A type that describes which components of a UFO should be loaded.
///
/// By default, all components of the UFO file are loaded; however, if you only
/// need a subset of them, you can pass this struct to [`Ufo::with_fields`] in
/// order to only load the fields you specify. This can improve performance in
/// large projects.
///
/// # Examples
///
/// A [DataRequest] that excludes all layer, glyph and kerning data:
///
/// ```
/// use norad::DataRequest;
///
/// let datareq = DataRequest::default().layers(false).kerning(false);
/// ```
///
/// A [DataRequest] that excludes all UFO data and images:
///
/// ```
/// use norad::DataRequest;
///
/// let datareq = DataRequest::default().data(false).images(false);
/// ```
///
/// A [DataRequest] that only includes parsed lib.plist data:
///
/// ```
/// use norad::DataRequest;
///
/// let datareq = DataRequest::none().lib(true);
/// ```
///
/// [`Ufo::with_fields`]: struct.Ufo.html#method.with_fields
#[derive(Debug)]
#[non_exhaustive]
pub struct DataRequest<'a> {
    // the layers to load.
    pub(crate) layers: LayerFilter<'a>,
    /// Load parsed lib.plist data
    pub lib: bool,
    /// Load parsed groups.plist data
    pub groups: bool,
    /// Load parsed kerning.plist data
    pub kerning: bool,
    /// Load Adobe .fea format feature file data
    pub features: bool,
    /// Load data
    pub data: bool,
    /// Load images
    pub images: bool,
}

type FilterFn<'a> = dyn Fn(&str, &Path) -> bool + 'a;

/// A type describing which layers to load.
pub(crate) struct LayerFilter<'a> {
    all: bool,
    load_default: bool,
    custom: Option<Box<FilterFn<'a>>>,
}

impl<'a> LayerFilter<'a> {
    fn from_bool(b: bool) -> Self {
        LayerFilter { all: b, ..Default::default() }
    }

    pub(crate) fn should_load(&self, name: &str, path: &Path) -> bool {
        self.all
            || (self.load_default && path == Path::new("glyphs"))
            || self.custom.as_ref().map(|f| f(name, path)).unwrap_or(false)
    }

    /// `true` if this filter includes the default layer
    pub(crate) fn includes_default_layer(&self) -> bool {
        self.all || self.load_default
    }
}

impl<'a> DataRequest<'a> {
    fn from_bool(b: bool) -> Self {
        DataRequest {
            layers: LayerFilter::from_bool(b),
            lib: b,
            groups: b,
            kerning: b,
            features: b,
            data: b,
            images: b,
        }
    }

    /// Returns a [`DataRequest`] requesting all UFO data.
    pub fn all() -> Self {
        DataRequest::from_bool(true)
    }

    /// Returns a [`DataRequest`] requesting no UFO data.
    pub fn none() -> Self {
        DataRequest::from_bool(false)
    }

    /// Request that returned UFO data include layers and their glyph data.
    ///
    /// See also the [`filter_layers`] and [`default_layer`] options.
    ///
    /// [`filter_layers`]: Self::filter_layers
    /// [`default_layer`]: Self::default_layer
    pub fn layers(mut self, b: bool) -> Self {
        self.layers.all = b;
        self
    }

    /// Request to only load the default layer.
    ///
    /// If set, we will ignore the [`layers`] option. For finer-grained control,
    /// see the [`filter_layers`] option.
    ///
    /// [`filter_layers`]: Self::filter_layers
    /// [`layers`]: Self::layers
    pub fn default_layer(mut self, b: bool) -> Self {
        self.layers.load_default = b;
        self.layers.all = false;
        self
    }

    /// Request to load a subset of layers using a closure
    ///
    /// Given the name and directory of a layer, the closure must return `true`
    /// or `false`. Only layers for which the closure returns `true` will be loaded.
    ///
    /// If this is set, it will override the [`layers`] option.
    ///
    /// To only load the default layer, use the [`default_layer`] option.
    ///
    /// # Examples
    ///
    /// To only load the background layer:
    ///
    /// ```no_run
    /// # use norad::{DataRequest, Font};
    /// let to_load = DataRequest::none().filter_layers(|name, _path| name.contains("background"));
    /// let font = Font::load_requested_data("path/to/font.ufo", to_load).unwrap();
    /// ```
    ///
    /// [`default_layer`]: Self::default_layer
    /// [`layers`]: Self::layers
    pub fn filter_layers(mut self, filter: impl Fn(&str, &Path) -> bool + 'a) -> Self {
        self.layers.custom = Some(Box::new(filter));
        self.layers.all = false;
        self
    }

    /// Request that returned UFO data include <lib> sections.
    pub fn lib(mut self, b: bool) -> Self {
        self.lib = b;
        self
    }

    /// Request that returned UFO data include parsed `groups.plist`.
    pub fn groups(mut self, b: bool) -> Self {
        self.groups = b;
        self
    }

    /// Request that returned UFO data include parsed `kerning.plist`.
    pub fn kerning(mut self, b: bool) -> Self {
        self.kerning = b;
        self
    }

    /// Request that returned UFO data include [OpenType Layout features in Adobe
    /// .fea format](https://unifiedfontobject.org/versions/ufo3/features.fea/).
    pub fn features(mut self, b: bool) -> Self {
        self.features = b;
        self
    }

    /// Request that returned UFO data include data.
    pub fn data(mut self, b: bool) -> Self {
        self.data = b;
        self
    }

    /// Request that returned UFO data include images.
    pub fn images(mut self, b: bool) -> Self {
        self.images = b;
        self
    }
}

impl Default for DataRequest<'_> {
    fn default() -> Self {
        DataRequest::from_bool(true)
    }
}

impl Default for LayerFilter<'_> {
    fn default() -> Self {
        Self { all: true, load_default: false, custom: None }
    }
}

impl std::fmt::Debug for LayerFilter<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        f.debug_struct("LayerFilter")
            .field("all", &self.all)
            .field("load_default", &self.load_default)
            .field("custom", &self.custom.is_some())
            .finish()
    }
}

#[cfg(test)]
mod tests {

    use super::*;

    fn all_fields_are_true(dr: &DataRequest) -> bool {
        dr.layers.all && dr.lib && dr.groups && dr.kerning && dr.features && dr.data && dr.images
    }

    fn all_fields_are_false(dr: &DataRequest) -> bool {
        !dr.layers.all
            && !dr.lib
            && !dr.groups
            && !dr.kerning
            && !dr.features
            && !dr.data
            && !dr.images
    }

    #[test]
    fn test_datarequest_default() {
        assert!(all_fields_are_true(&DataRequest::default()));
    }

    #[test]
    fn test_datarequest_all() {
        assert!(all_fields_are_true(&DataRequest::all()));
    }

    #[test]
    fn test_datarequest_none() {
        assert!(all_fields_are_false(&DataRequest::none()));
    }

    #[test]
    fn test_datarequest_builder() {
        let dr = DataRequest::default()
            .layers(false)
            .lib(false)
            .groups(false)
            .kerning(false)
            .features(false)
            .data(false)
            .images(false);

        assert!(all_fields_are_false(&dr));
    }
}