1// font-kit/src/matching.rs
2//
3// Copyright © 2018 The Pathfinder Project Developers.
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11//! Determines the closest font matching a description per the CSS Fonts Level 3 specification.
12
13use float_ord::FloatOrd;
14
15use crate::error::SelectionError;
16use crate::properties::{Properties, Stretch, Style, Weight};
17
18/// This follows CSS Fonts Level 3 § 5.2 [1].
19///
20/// https://drafts.csswg.org/css-fonts-3/#font-style-matching
21pub fn find_best_match(
22 candidates: &[Properties],
23 query: &Properties,
24) -> Result<usize, SelectionError> {
25 // Step 4.
26 let mut matching_set: Vec<usize> = (0..candidates.len()).collect();
27 if matching_set.is_empty() {
28 return Err(SelectionError::NotFound);
29 }
30
31 // Step 4a (`font-stretch`).
32 let matching_stretch = if matching_set
33 .iter()
34 .any(|&index| candidates[index].stretch == query.stretch)
35 {
36 // Exact match.
37 query.stretch
38 } else if query.stretch <= Stretch::NORMAL {
39 // Closest width, first checking narrower values and then wider values.
40 match matching_set
41 .iter()
42 .filter(|&&index| candidates[index].stretch < query.stretch)
43 .min_by_key(|&&index| FloatOrd(query.stretch.0 - candidates[index].stretch.0))
44 {
45 Some(&matching_index) => candidates[matching_index].stretch,
46 None => {
47 let matching_index = *matching_set
48 .iter()
49 .min_by_key(|&&index| FloatOrd(candidates[index].stretch.0 - query.stretch.0))
50 .unwrap();
51 candidates[matching_index].stretch
52 }
53 }
54 } else {
55 // Closest width, first checking wider values and then narrower values.
56 match matching_set
57 .iter()
58 .filter(|&&index| candidates[index].stretch > query.stretch)
59 .min_by_key(|&&index| FloatOrd(candidates[index].stretch.0 - query.stretch.0))
60 {
61 Some(&matching_index) => candidates[matching_index].stretch,
62 None => {
63 let matching_index = *matching_set
64 .iter()
65 .min_by_key(|&&index| FloatOrd(query.stretch.0 - candidates[index].stretch.0))
66 .unwrap();
67 candidates[matching_index].stretch
68 }
69 }
70 };
71 matching_set.retain(|&index| candidates[index].stretch == matching_stretch);
72
73 // Step 4b (`font-style`).
74 let style_preference = match query.style {
75 Style::Italic => [Style::Italic, Style::Oblique, Style::Normal],
76 Style::Oblique => [Style::Oblique, Style::Italic, Style::Normal],
77 Style::Normal => [Style::Normal, Style::Oblique, Style::Italic],
78 };
79 let matching_style = *style_preference
80 .iter()
81 .filter(|&query_style| {
82 matching_set
83 .iter()
84 .any(|&index| candidates[index].style == *query_style)
85 })
86 .next()
87 .unwrap();
88 matching_set.retain(|&index| candidates[index].style == matching_style);
89
90 // Step 4c (`font-weight`).
91 //
92 // The spec doesn't say what to do if the weight is between 400 and 500 exclusive, so we
93 // just use 450 as the cutoff.
94 let matching_weight = if matching_set
95 .iter()
96 .any(|&index| candidates[index].weight == query.weight)
97 {
98 query.weight
99 } else if query.weight >= Weight(400.0)
100 && query.weight < Weight(450.0)
101 && matching_set
102 .iter()
103 .any(|&index| candidates[index].weight == Weight(500.0))
104 {
105 // Check 500 first.
106 Weight(500.0)
107 } else if query.weight >= Weight(450.0)
108 && query.weight <= Weight(500.0)
109 && matching_set
110 .iter()
111 .any(|&index| candidates[index].weight == Weight(400.0))
112 {
113 // Check 400 first.
114 Weight(400.0)
115 } else if query.weight <= Weight(500.0) {
116 // Closest weight, first checking thinner values and then fatter ones.
117 match matching_set
118 .iter()
119 .filter(|&&index| candidates[index].weight <= query.weight)
120 .min_by_key(|&&index| FloatOrd(query.weight.0 - candidates[index].weight.0))
121 {
122 Some(&matching_index) => candidates[matching_index].weight,
123 None => {
124 let matching_index = *matching_set
125 .iter()
126 .min_by_key(|&&index| FloatOrd(candidates[index].weight.0 - query.weight.0))
127 .unwrap();
128 candidates[matching_index].weight
129 }
130 }
131 } else {
132 // Closest weight, first checking fatter values and then thinner ones.
133 match matching_set
134 .iter()
135 .filter(|&&index| candidates[index].weight >= query.weight)
136 .min_by_key(|&&index| FloatOrd(candidates[index].weight.0 - query.weight.0))
137 {
138 Some(&matching_index) => candidates[matching_index].weight,
139 None => {
140 let matching_index = *matching_set
141 .iter()
142 .min_by_key(|&&index| FloatOrd(query.weight.0 - candidates[index].weight.0))
143 .unwrap();
144 candidates[matching_index].weight
145 }
146 }
147 };
148 matching_set.retain(|&index| candidates[index].weight == matching_weight);
149
150 // Step 4d concerns `font-size`, but fonts in `font-kit` are unsized, so we ignore that.
151
152 // Return the result.
153 matching_set
154 .into_iter()
155 .next()
156 .ok_or(SelectionError::NotFound)
157}
158