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 .find(|&query_style| {
82 matching_set
83 .iter()
84 .any(|&index| candidates[index].style == *query_style)
85 })
86 .unwrap();
87 matching_set.retain(|&index| candidates[index].style == matching_style);
88
89 // Step 4c (`font-weight`).
90 //
91 // The spec doesn't say what to do if the weight is between 400 and 500 exclusive, so we
92 // just use 450 as the cutoff.
93 let matching_weight = if matching_set
94 .iter()
95 .any(|&index| candidates[index].weight == query.weight)
96 {
97 query.weight
98 } else if query.weight >= Weight(400.0)
99 && query.weight < Weight(450.0)
100 && matching_set
101 .iter()
102 .any(|&index| candidates[index].weight == Weight(500.0))
103 {
104 // Check 500 first.
105 Weight(500.0)
106 } else if query.weight >= Weight(450.0)
107 && query.weight <= Weight(500.0)
108 && matching_set
109 .iter()
110 .any(|&index| candidates[index].weight == Weight(400.0))
111 {
112 // Check 400 first.
113 Weight(400.0)
114 } else if query.weight <= Weight(500.0) {
115 // Closest weight, first checking thinner values and then fatter ones.
116 match matching_set
117 .iter()
118 .filter(|&&index| candidates[index].weight <= query.weight)
119 .min_by_key(|&&index| FloatOrd(query.weight.0 - candidates[index].weight.0))
120 {
121 Some(&matching_index) => candidates[matching_index].weight,
122 None => {
123 let matching_index = *matching_set
124 .iter()
125 .min_by_key(|&&index| FloatOrd(candidates[index].weight.0 - query.weight.0))
126 .unwrap();
127 candidates[matching_index].weight
128 }
129 }
130 } else {
131 // Closest weight, first checking fatter values and then thinner ones.
132 match matching_set
133 .iter()
134 .filter(|&&index| candidates[index].weight >= query.weight)
135 .min_by_key(|&&index| FloatOrd(candidates[index].weight.0 - query.weight.0))
136 {
137 Some(&matching_index) => candidates[matching_index].weight,
138 None => {
139 let matching_index = *matching_set
140 .iter()
141 .min_by_key(|&&index| FloatOrd(query.weight.0 - candidates[index].weight.0))
142 .unwrap();
143 candidates[matching_index].weight
144 }
145 }
146 };
147 matching_set.retain(|&index| candidates[index].weight == matching_weight);
148
149 // Step 4d concerns `font-size`, but fonts in `font-kit` are unsized, so we ignore that.
150
151 // Return the result.
152 matching_set
153 .into_iter()
154 .next()
155 .ok_or(SelectionError::NotFound)
156}
157