1pub const MAX_FONT_SCALE_KNOTS: usize = 12;
41
42const COLLINEAR_EPSILON_DP: f32 = 1.0e-3;
46
47#[derive(Clone, Copy, Debug, PartialEq)]
55pub struct FontScaleCurve {
56 scale: f32,
57 knots: [(f32, f32); MAX_FONT_SCALE_KNOTS],
58 len: usize,
59 fingerprint: u32,
60}
61
62impl Default for FontScaleCurve {
63 fn default() -> Self {
64 Self::linear(1.0)
65 }
66}
67
68impl FontScaleCurve {
69 pub const fn linear(scale: f32) -> Self {
72 Self {
73 scale,
74 knots: [(0.0, 0.0); MAX_FONT_SCALE_KNOTS],
75 len: 0,
76 fingerprint: 0,
77 }
78 }
79
80 pub fn from_samples(scale: f32, samples: &[(f32, f32)]) -> Self {
90 let Some(kept) = compress(samples) else {
91 return Self::linear(scale);
92 };
93 let mut curve = Self::linear(scale);
94 for (index, knot) in kept.iter().enumerate() {
95 curve.knots[index] = *knot;
96 }
97 curve.len = kept.len();
98 curve.fingerprint = fingerprint(scale, &kept);
99 curve
100 }
101
102 pub fn scale(self) -> f32 {
106 self.scale
107 }
108
109 pub fn is_linear(self) -> bool {
111 self.len == 0
112 }
113
114 pub fn is_identity(self) -> bool {
120 if self.len == 0 {
121 return (self.scale - 1.0).abs() <= f32::EPSILON;
122 }
123 self.knots[..self.len]
124 .iter()
125 .all(|(sp, dp)| (dp - sp).abs() <= COLLINEAR_EPSILON_DP)
126 }
127
128 pub fn knots(self) -> [(f32, f32); MAX_FONT_SCALE_KNOTS] {
130 self.knots
131 }
132
133 pub fn knot_count(self) -> usize {
135 self.len
136 }
137
138 pub fn fingerprint(self) -> u32 {
141 self.fingerprint ^ self.scale.to_bits()
142 }
143
144 pub fn sp_to_dp(self, sp: f32) -> f32 {
151 if !sp.is_finite() {
152 return sp;
153 }
154 if self.len == 0 {
155 return sp * self.scale;
156 }
157 let magnitude = sp.abs();
158 let sign = if sp.is_sign_negative() { -1.0 } else { 1.0 };
159 let knots = &self.knots[..self.len];
160 let (first_sp, first_dp) = knots[0];
161 if magnitude <= first_sp {
162 return sign * magnitude * (first_dp / first_sp);
163 }
164 let (last_sp, last_dp) = knots[self.len - 1];
165 if magnitude >= last_sp {
166 return sign * magnitude * (last_dp / last_sp);
167 }
168 for window in knots.windows(2) {
169 let (low_sp, low_dp) = window[0];
170 let (high_sp, high_dp) = window[1];
171 if magnitude <= high_sp {
172 let t = (magnitude - low_sp) / (high_sp - low_sp);
173 return sign * (low_dp + (high_dp - low_dp) * t);
174 }
175 }
176 sign * magnitude * (last_dp / last_sp)
177 }
178}
179
180fn compress(samples: &[(f32, f32)]) -> Option<Vec<(f32, f32)>> {
183 if samples.len() < 2 {
184 return None;
185 }
186 let mut previous_sp = 0.0f32;
187 for (sp, dp) in samples {
188 if !sp.is_finite() || !dp.is_finite() || *sp <= previous_sp || *dp <= 0.0 {
189 return None;
190 }
191 previous_sp = *sp;
192 }
193 let mut kept: Vec<(f32, f32)> = Vec::with_capacity(samples.len());
194 kept.push(samples[0]);
195 for index in 1..samples.len() - 1 {
196 let (low_sp, low_dp) = *kept.last().expect("the first sample was pushed");
197 let (sp, dp) = samples[index];
198 let (high_sp, high_dp) = samples[index + 1];
199 let t = (sp - low_sp) / (high_sp - low_sp);
200 let straight = low_dp + (high_dp - low_dp) * t;
201 if (dp - straight).abs() > COLLINEAR_EPSILON_DP {
202 kept.push(samples[index]);
203 }
204 }
205 kept.push(samples[samples.len() - 1]);
206 if kept.len() > MAX_FONT_SCALE_KNOTS {
207 return None;
208 }
209 Some(kept)
210}
211
212fn fingerprint(scale: f32, knots: &[(f32, f32)]) -> u32 {
213 let mut hash = 2166136261u32;
214 let mut mix = |bits: u32| {
215 hash ^= bits;
216 hash = hash.wrapping_mul(16777619);
217 };
218 mix(scale.to_bits());
219 for (sp, dp) in knots {
220 mix(sp.to_bits());
221 mix(dp.to_bits());
222 }
223 hash
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 const PLATFORM_124: [(f32, f32); 10] = [
236 (8.0, 9.92),
237 (10.0, 12.4),
238 (12.0, 14.88),
239 (14.0, 17.84),
240 (16.0, 19.36),
241 (18.0, 20.88),
242 (20.0, 22.88),
243 (24.0, 25.92),
244 (30.0, 30.0),
245 (100.0, 100.0),
246 ];
247
248 fn platform_124() -> FontScaleCurve {
249 FontScaleCurve::from_samples(1.24, &PLATFORM_124)
250 }
251
252 #[test]
253 fn a_curve_with_no_knots_multiplies() {
254 let curve = FontScaleCurve::linear(1.24);
255 assert!(curve.is_linear());
256 assert_eq!(curve.sp_to_dp(13.0), 13.0 * 1.24);
257 assert_eq!(curve.sp_to_dp(0.4), 0.4 * 1.24);
258 assert_eq!(curve.scale(), 1.24);
259 }
260
261 #[test]
262 fn the_identity_curve_leaves_a_size_alone() {
263 assert!(FontScaleCurve::linear(1.0).is_identity());
264 assert!(!FontScaleCurve::linear(1.24).is_identity());
265 assert!(!platform_124().is_identity());
266 let flat: Vec<(f32, f32)> = (1..=40).map(|sp| (sp as f32, sp as f32)).collect();
269 assert!(FontScaleCurve::from_samples(1.0, &flat).is_identity());
270 }
271
272 #[test]
273 fn the_platform_curve_is_reproduced_at_every_size_the_platform_was_asked() {
274 let curve = platform_124();
277 for (sp, expected_px) in [
278 (0.4f32, 0.992f32),
279 (12.0, 29.76),
280 (13.0, 32.72),
281 (14.0, 35.68),
282 (15.0, 37.2),
283 (16.0, 38.72),
284 (18.0, 41.76),
285 (19.0, 43.76),
286 (20.0, 45.76),
287 (24.0, 51.84),
288 (30.0, 60.0),
289 (40.0, 80.0),
290 (100.0, 200.0),
291 ] {
292 let px = curve.sp_to_dp(sp) * 2.0;
293 assert!(
294 (px - expected_px).abs() < 1.0e-3,
295 "{sp}sp resolved to {px}px where the platform answered {expected_px}px",
296 );
297 }
298 }
299
300 #[test]
301 fn the_thirteen_sp_secondary_label_is_where_multiplying_goes_wrong() {
302 let curve = platform_124();
307 assert!((curve.sp_to_dp(13.0) * 2.0 - 32.72).abs() < 1.0e-3);
308 assert!((FontScaleCurve::linear(1.24).sp_to_dp(13.0) * 2.0 - 32.24).abs() < 1.0e-3);
309 }
310
311 #[test]
312 fn collinear_samples_are_dropped_and_the_curve_still_answers_the_same() {
313 let curve = platform_124();
314 assert_eq!(curve.knot_count(), 8);
317 let dense: Vec<(f32, f32)> = (1..=120)
318 .map(|sp| {
319 let sp = sp as f32;
320 (sp, curve.sp_to_dp(sp))
321 })
322 .collect();
323 let resampled = FontScaleCurve::from_samples(1.24, &dense);
324 for step in 1..=1200 {
325 let sp = step as f32 * 0.1;
326 assert!(
327 (resampled.sp_to_dp(sp) - curve.sp_to_dp(sp)).abs() < 1.0e-3,
328 "{sp}sp: {} against {}",
329 resampled.sp_to_dp(sp),
330 curve.sp_to_dp(sp),
331 );
332 }
333 }
334
335 #[test]
336 fn samples_the_platform_could_not_have_produced_fall_back_to_multiplying() {
337 for samples in [
338 &[][..],
339 &[(12.0, 14.88)][..],
340 &[(12.0, 14.88), (12.0, 15.0)][..],
341 &[(14.0, 17.84), (12.0, 14.88)][..],
342 &[(12.0, f32::NAN), (14.0, 17.84)][..],
343 &[(12.0, 0.0), (14.0, 17.84)][..],
344 ] {
345 let curve = FontScaleCurve::from_samples(1.24, samples);
346 assert!(curve.is_linear(), "{samples:?} was accepted");
347 assert_eq!(curve.sp_to_dp(13.0), 13.0 * 1.24);
348 }
349 }
350
351 #[test]
352 fn more_bends_than_there_are_knots_falls_back_rather_than_truncating() {
353 let zigzag: Vec<(f32, f32)> = (1..=40)
356 .map(|sp| {
357 let sp = sp as f32;
358 (sp, sp * if sp as i32 % 2 == 0 { 1.5 } else { 1.2 })
359 })
360 .collect();
361 assert!(FontScaleCurve::from_samples(1.24, &zigzag).is_linear());
362 }
363
364 #[test]
365 fn a_negative_size_keeps_its_sign() {
366 let curve = platform_124();
367 assert!((curve.sp_to_dp(-13.0) + curve.sp_to_dp(13.0)).abs() < 1.0e-6);
368 assert!(FontScaleCurve::linear(1.24).sp_to_dp(-13.0) < 0.0);
369 }
370
371 #[test]
372 fn a_size_that_is_not_a_number_comes_back_unchanged() {
373 let curve = platform_124();
374 assert!(curve.sp_to_dp(f32::NAN).is_nan());
375 assert_eq!(curve.sp_to_dp(f32::INFINITY), f32::INFINITY);
376 }
377
378 #[test]
379 fn curves_that_convert_differently_do_not_share_a_fingerprint() {
380 let platform = platform_124();
381 assert_ne!(
382 platform.fingerprint(),
383 FontScaleCurve::linear(1.24).fingerprint()
384 );
385 assert_ne!(
386 FontScaleCurve::linear(1.0).fingerprint(),
387 FontScaleCurve::linear(1.24).fingerprint()
388 );
389 assert_eq!(platform.fingerprint(), platform_124().fingerprint());
390 }
391}