font-kit 0.1.0

A cross-platform font loading library
Documentation
// font-kit/src/matching.rs
//
// Copyright © 2018 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! Determines the closest font matching a description per the CSS Fonts Level 3 specification.

use float_ord::FloatOrd;

use error::SelectionError;
use properties::{Properties, Stretch, Style, Weight};

pub struct Description {
    pub family_name: String,
    pub properties: Properties,
}

/// This follows CSS Fonts Level 3 § 5.2 [1].
///
/// https://drafts.csswg.org/css-fonts-3/#font-style-matching
pub fn find_best_match(candidates: &[Description], query: &Properties)
                       -> Result<usize, SelectionError> {
    // Step 4.
    let mut matching_set: Vec<usize> = (0..candidates.len()).collect();
    if matching_set.is_empty() {
        return Err(SelectionError::NotFound)
    }

    // Step 4a (`font-stretch`).
    let matching_stretch =
        if matching_set.iter().any(|&index| {
                candidates[index].properties.stretch == query.stretch
            }) {
            // Exact match.
            query.stretch
        } else if query.stretch <= Stretch::NORMAL {
            // Closest width, first checking narrower values and then wider values.
            match matching_set.iter()
                              .filter(|&&index| {
                                  candidates[index].properties.stretch < query.stretch
                              })
                              .min_by_key(|&&index| {
                                FloatOrd(query.stretch.0 - candidates[index].properties.stretch.0)
                              }) {
                Some(&matching_index) => candidates[matching_index].properties.stretch,
                None => {
                    let matching_index =
                        *matching_set.iter()
                                     .min_by_key(|&&index| {
                                         FloatOrd(candidates[index].properties.stretch.0 -
                                                  query.stretch.0)
                                     })
                                     .unwrap();
                    candidates[matching_index].properties.stretch
                }
            }
        } else {
            // Closest width, first checking wider values and then narrower values.
            match matching_set.iter()
                              .filter(|&&index| {
                                  candidates[index].properties.stretch > query.stretch
                              })
                              .min_by_key(|&&index| {
                                FloatOrd(candidates[index].properties.stretch.0 - query.stretch.0)
                              }) {
                Some(&matching_index) => candidates[matching_index].properties.stretch,
                None => {
                    let matching_index =
                        *matching_set.iter()
                                     .min_by_key(|&&index| {
                                        FloatOrd(query.stretch.0 -
                                                 candidates[index].properties.stretch.0)
                                     })
                                     .unwrap();
                    candidates[matching_index].properties.stretch
                }
            }
        };
    matching_set.retain(|&index| candidates[index].properties.stretch == matching_stretch);

    // Step 4b (`font-style`).
    let style_preference = match query.style {
        Style::Italic => [Style::Italic, Style::Oblique, Style::Normal],
        Style::Oblique => [Style::Oblique, Style::Italic, Style::Normal],
        Style::Normal => [Style::Normal, Style::Oblique, Style::Italic],
    };
    let matching_style = *style_preference.iter().filter(|&query_style| {
        matching_set.iter().any(|&index| candidates[index].properties.style == *query_style)
    }).next().unwrap();
    matching_set.retain(|&index| candidates[index].properties.style == matching_style);

    // Step 4c (`font-weight`).
    //
    // The spec doesn't say what to do if the weight is between 400 and 500 exclusive, so we
    // just use 450 as the cutoff.
    let matching_weight =
        if query.weight >= Weight(400.0) && query.weight < Weight(450.0) &&
                matching_set.iter()
                            .any(|&index| candidates[index].properties.weight == Weight(500.0)) {
            // Check 500 first.
            Weight(500.0)
        } else if query.weight >= Weight(450.0) && query.weight <= Weight(500.0) &&
                matching_set.iter()
                            .any(|&index| candidates[index].properties.weight == Weight(400.0)) {
            // Check 400 first.
            Weight(400.0)
        } else if query.weight <= Weight(500.0) {
            // Closest weight, first checking thinner values and then fatter ones.
            match matching_set.iter()
                              .filter(|&&index| {
                                  candidates[index].properties.weight <= query.weight
                              })
                              .min_by_key(|&&index| {
                                FloatOrd(query.weight.0 - candidates[index].properties.weight.0)
                              }) {
                Some(&matching_index) => candidates[matching_index].properties.weight,
                None => {
                    let matching_index =
                        *matching_set.iter()
                                     .min_by_key(|&&index| {
                                        FloatOrd(candidates[index].properties.weight.0 -
                                                 query.weight.0)
                                     })
                                     .unwrap();
                    candidates[matching_index].properties.weight
                }
            }
        } else {
            // Closest weight, first checking fatter values and then thinner ones.
            match matching_set.iter()
                              .filter(|&&index| {
                                  candidates[index].properties.weight >= query.weight
                              })
                              .min_by_key(|&&index| {
                                FloatOrd(candidates[index].properties.weight.0 - query.weight.0)
                              }) {
                Some(&matching_index) => candidates[matching_index].properties.weight,
                None => {
                    let matching_index =
                        *matching_set.iter()
                                     .min_by_key(|&&index| {
                                        FloatOrd(query.weight.0 -
                                                 candidates[index].properties.weight.0)
                                     })
                                     .unwrap();
                    candidates[matching_index].properties.weight
                }
            }
        };
    matching_set.retain(|&index| candidates[index].properties.weight == matching_weight);

    // Step 4d concerns `font-size`, but fonts in `font-kit` are unsized, so we ignore that.

    // Return the result.
    matching_set.into_iter().next().ok_or(SelectionError::NotFound)
}