/src/fontations/skrifa/src/outline/autohint/metrics/scale.rs
Line | Count | Source |
1 | | //! Metrics scaling. |
2 | | //! |
3 | | //! Uses the widths and blues computations to generate unscaled metrics for a |
4 | | //! given style/script. |
5 | | //! |
6 | | //! Then applies a scaling factor to those metrics, computes a potentially |
7 | | //! modified scale, and tags active blue zones. |
8 | | |
9 | | use super::super::{ |
10 | | metrics::{ |
11 | | fixed_div, fixed_mul, fixed_mul_div, pix_round, BlueZones, Scale, ScaledAxisMetrics, |
12 | | ScaledBlue, ScaledStyleMetrics, ScaledWidth, UnscaledAxisMetrics, UnscaledBlue, |
13 | | UnscaledStyleMetrics, WidthMetrics, |
14 | | }, |
15 | | shape::{Shaper, ShaperMode}, |
16 | | style::{ScriptGroup, StyleClass}, |
17 | | topo::Dimension, |
18 | | QuirksMode, |
19 | | }; |
20 | | use crate::{instance::NormalizedCoord, prelude::Size, FontRef, MetadataProvider}; |
21 | | use raw::types::F2Dot14; |
22 | | |
23 | | impl UnscaledStyleMetrics { |
24 | | /// Creates a set of metrics for the given font, normalized coordinates |
25 | | /// and style class. |
26 | 0 | pub fn new(font: &FontRef, coords: &[NormalizedCoord], style: &StyleClass) -> Self { |
27 | 0 | let shaper_mode = if cfg!(feature = "autohint_shaping") { |
28 | 0 | ShaperMode::BestEffort |
29 | | } else { |
30 | 0 | ShaperMode::Nominal |
31 | | }; |
32 | 0 | let shaper = Shaper::new(font, shaper_mode); |
33 | 0 | compute_unscaled_style_metrics(&shaper, coords, style, QuirksMode::Aot) |
34 | 0 | } |
35 | | |
36 | | /// Applies the given scale to this set of style metrics. |
37 | 0 | pub fn scale(&self, scale: Scale) -> ScaledStyleMetrics { |
38 | 0 | scale_style_metrics(self, scale, QuirksMode::Aot) |
39 | 0 | } |
40 | | } |
41 | | |
42 | | /// Computes unscaled metrics for the Latin writing system. |
43 | | /// |
44 | | /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1134> |
45 | 0 | pub(crate) fn compute_unscaled_style_metrics( |
46 | 0 | shaper: &Shaper, |
47 | 0 | coords: &[F2Dot14], |
48 | 0 | style: &StyleClass, |
49 | 0 | quirks: QuirksMode, |
50 | 0 | ) -> UnscaledStyleMetrics { |
51 | 0 | let charmap = shaper.charmap(); |
52 | | // We don't attempt to produce any metrics if we don't have a Unicode |
53 | | // cmap |
54 | | // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1146> |
55 | 0 | if charmap.is_symbol() { |
56 | 0 | return UnscaledStyleMetrics { |
57 | 0 | class_ix: style.index as u16, |
58 | 0 | axes: [ |
59 | 0 | UnscaledAxisMetrics { |
60 | 0 | dim: Dimension::Horizontal, |
61 | 0 | ..Default::default() |
62 | 0 | }, |
63 | 0 | UnscaledAxisMetrics { |
64 | 0 | dim: Dimension::Vertical, |
65 | 0 | ..Default::default() |
66 | 0 | }, |
67 | 0 | ], |
68 | 0 | ..Default::default() |
69 | 0 | }; |
70 | 0 | } |
71 | 0 | let [hwidths, vwidths] = super::widths::compute_widths(shaper, coords, style, quirks); |
72 | 0 | let [hblues, vblues] = super::blues::compute_unscaled_blues(shaper, coords, style); |
73 | 0 | let glyph_metrics = shaper.font().glyph_metrics(Size::unscaled(), coords); |
74 | 0 | let mut digit_advance = None; |
75 | 0 | let mut digits_have_same_width = true; |
76 | 0 | for ch in '0'..='9' { |
77 | 0 | if let Some(advance) = charmap |
78 | 0 | .map(ch) |
79 | 0 | .and_then(|gid| glyph_metrics.advance_width(gid)) |
80 | | { |
81 | 0 | if digit_advance.is_some() && digit_advance != Some(advance) { |
82 | 0 | digits_have_same_width = false; |
83 | 0 | break; |
84 | 0 | } |
85 | 0 | digit_advance = Some(advance); |
86 | 0 | } |
87 | | } |
88 | 0 | UnscaledStyleMetrics { |
89 | 0 | class_ix: style.index as u16, |
90 | 0 | digits_have_same_width, |
91 | 0 | axes: [ |
92 | 0 | UnscaledAxisMetrics { |
93 | 0 | dim: Dimension::Horizontal, |
94 | 0 | blues: hblues, |
95 | 0 | width_metrics: hwidths.0, |
96 | 0 | widths: hwidths.1, |
97 | 0 | }, |
98 | 0 | UnscaledAxisMetrics { |
99 | 0 | dim: Dimension::Vertical, |
100 | 0 | blues: vblues, |
101 | 0 | width_metrics: vwidths.0, |
102 | 0 | widths: vwidths.1, |
103 | 0 | }, |
104 | 0 | ], |
105 | 0 | } |
106 | 0 | } |
107 | | |
108 | | /// Computes scaled metrics for the Latin writing system. |
109 | | /// |
110 | | /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1491> |
111 | 0 | pub(crate) fn scale_style_metrics( |
112 | 0 | unscaled_metrics: &UnscaledStyleMetrics, |
113 | 0 | mut scale: Scale, |
114 | 0 | quirks: QuirksMode, |
115 | 0 | ) -> ScaledStyleMetrics { |
116 | 0 | let scale_axis_fn = if unscaled_metrics.style_class().script.group == ScriptGroup::Default { |
117 | 0 | scale_default_axis_metrics |
118 | 0 | } else { |
119 | 0 | scale_cjk_axis_metrics |
120 | 0 | }; |
121 | 0 | let mut scale_axis = |axis: &UnscaledAxisMetrics| { |
122 | 0 | scale_axis_fn( |
123 | 0 | axis.dim, |
124 | 0 | &axis.widths, |
125 | 0 | axis.width_metrics, |
126 | 0 | &axis.blues, |
127 | 0 | &mut scale, |
128 | 0 | quirks, |
129 | 0 | ) |
130 | 0 | }; |
131 | 0 | let axes = [ |
132 | 0 | scale_axis(&unscaled_metrics.axes[0]), |
133 | 0 | scale_axis(&unscaled_metrics.axes[1]), |
134 | 0 | ]; |
135 | 0 | ScaledStyleMetrics { scale, axes } |
136 | 0 | } |
137 | | |
138 | | /// Computes scaled metrics for a single axis. |
139 | | /// |
140 | | /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1168> |
141 | 0 | fn scale_default_axis_metrics( |
142 | 0 | dim: Dimension, |
143 | 0 | widths: &[i32], |
144 | 0 | width_metrics: WidthMetrics, |
145 | 0 | blues: &[UnscaledBlue], |
146 | 0 | scale: &mut Scale, |
147 | 0 | quirks: QuirksMode, |
148 | 0 | ) -> ScaledAxisMetrics { |
149 | 0 | let mut axis = ScaledAxisMetrics { |
150 | 0 | dim, |
151 | 0 | ..Default::default() |
152 | 0 | }; |
153 | 0 | if dim == Dimension::Horizontal { |
154 | 0 | axis.scale = scale.x_scale; |
155 | 0 | axis.delta = scale.x_delta; |
156 | 0 | } else { |
157 | 0 | axis.scale = scale.y_scale; |
158 | 0 | axis.delta = scale.y_delta; |
159 | 0 | }; |
160 | | // Correct Y scale to optimize alignment |
161 | 0 | if let Some(blue_ix) = blues |
162 | 0 | .iter() |
163 | 0 | .position(|blue| blue.zones.contains(BlueZones::ADJUSTMENT)) |
164 | | { |
165 | 0 | let unscaled_blue = &blues[blue_ix]; |
166 | 0 | let scaled = fixed_mul(axis.scale, unscaled_blue.overshoot); |
167 | 0 | let fitted = (scaled + 40) & !63; |
168 | 0 | if scaled != fitted && dim == Dimension::Vertical { |
169 | 0 | let new_scale = fixed_mul_div(axis.scale, fitted, scaled); |
170 | | // Scaling should not adjust by more than 2 pixels |
171 | 0 | let mut max_height = scale.units_per_em; |
172 | 0 | for blue in blues { |
173 | 0 | max_height = max_height.max(blue.ascender).max(-blue.descender); |
174 | 0 | } |
175 | 0 | let mut dist = fixed_mul(max_height, new_scale - axis.scale).abs(); |
176 | 0 | dist &= !127; |
177 | 0 | if dist == 0 { |
178 | 0 | axis.scale = new_scale; |
179 | 0 | scale.y_scale = new_scale; |
180 | 0 | } |
181 | 0 | } |
182 | 0 | } |
183 | | // Now scale the widths. FreeType ensures there is always at least one |
184 | | // width entry (the standard width), even if width extraction found none. |
185 | 0 | axis.width_metrics = width_metrics; |
186 | 0 | if widths.is_empty() && quirks == QuirksMode::Aot { |
187 | 0 | let scaled = fixed_mul(axis.scale, axis.width_metrics.standard_width); |
188 | 0 | axis.widths.push(ScaledWidth { |
189 | 0 | scaled, |
190 | 0 | fitted: scaled, |
191 | 0 | }); |
192 | 0 | } else { |
193 | 0 | for unscaled_width in widths { |
194 | 0 | let scaled = fixed_mul(axis.scale, *unscaled_width); |
195 | 0 | axis.widths.push(ScaledWidth { |
196 | 0 | scaled, |
197 | 0 | fitted: scaled, |
198 | 0 | }); |
199 | 0 | } |
200 | | } |
201 | | // Compute extra light property: this is a standard width that is |
202 | | // less than 5/8 pixels |
203 | 0 | axis.width_metrics.is_extra_light = |
204 | 0 | fixed_mul(axis.width_metrics.standard_width, axis.scale) < (32 + 8); |
205 | 0 | if dim == Dimension::Vertical { |
206 | | // And scale the blue zones |
207 | 0 | for unscaled_blue in blues { |
208 | 0 | let scaled_position = fixed_mul(axis.scale, unscaled_blue.position) + axis.delta; |
209 | 0 | let scaled_overshoot = fixed_mul(axis.scale, unscaled_blue.overshoot) + axis.delta; |
210 | 0 | let mut blue = ScaledBlue { |
211 | 0 | position: ScaledWidth { |
212 | 0 | scaled: scaled_position, |
213 | 0 | fitted: scaled_position, |
214 | 0 | }, |
215 | 0 | overshoot: ScaledWidth { |
216 | 0 | scaled: scaled_overshoot, |
217 | 0 | fitted: scaled_overshoot, |
218 | 0 | }, |
219 | 0 | zones: unscaled_blue.zones, |
220 | 0 | is_active: false, |
221 | 0 | }; |
222 | | // Only activate blue zones less than 3/4 pixel tall |
223 | 0 | let dist = fixed_mul(unscaled_blue.position - unscaled_blue.overshoot, axis.scale); |
224 | 0 | if (-48..=48).contains(&dist) { |
225 | 0 | let mut delta = dist.abs(); |
226 | 0 | if delta < 32 { |
227 | 0 | delta = 0; |
228 | 0 | } else if delta < 48 { |
229 | 0 | delta = 32; |
230 | 0 | } else { |
231 | 0 | delta = 64; |
232 | 0 | } |
233 | 0 | if dist < 0 { |
234 | 0 | delta = -delta; |
235 | 0 | } |
236 | 0 | blue.position.fitted = pix_round(blue.position.scaled); |
237 | 0 | blue.overshoot.fitted = blue.position.fitted - delta; |
238 | 0 | blue.is_active = true; |
239 | 0 | } |
240 | 0 | axis.blues.push(blue); |
241 | | } |
242 | | // Use sub-top blue zone if it doesn't overlap with another |
243 | | // non-sub-top blue zone |
244 | 0 | for blue_ix in 0..axis.blues.len() { |
245 | 0 | let blue = axis.blues[blue_ix]; |
246 | 0 | if !blue.zones.is_sub_top() || !blue.is_active { |
247 | 0 | continue; |
248 | 0 | } |
249 | 0 | for blue2 in &axis.blues { |
250 | 0 | if blue2.zones.is_sub_top() || !blue2.is_active { |
251 | 0 | continue; |
252 | 0 | } |
253 | 0 | if blue2.position.fitted <= blue.overshoot.fitted |
254 | 0 | && blue2.overshoot.fitted >= blue.position.fitted |
255 | | { |
256 | 0 | axis.blues[blue_ix].is_active = false; |
257 | 0 | break; |
258 | 0 | } |
259 | | } |
260 | | } |
261 | 0 | } |
262 | 0 | axis |
263 | 0 | } |
264 | | |
265 | | /// Computes scaled metrics for a single axis for the CJK script group. |
266 | | /// |
267 | | /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L661> |
268 | 0 | fn scale_cjk_axis_metrics( |
269 | 0 | dim: Dimension, |
270 | 0 | widths: &[i32], |
271 | 0 | width_metrics: WidthMetrics, |
272 | 0 | blues: &[UnscaledBlue], |
273 | 0 | scale: &mut Scale, |
274 | 0 | _quirks: QuirksMode, |
275 | 0 | ) -> ScaledAxisMetrics { |
276 | 0 | let mut axis = ScaledAxisMetrics { |
277 | 0 | dim, |
278 | 0 | ..Default::default() |
279 | 0 | }; |
280 | 0 | axis.dim = dim; |
281 | 0 | if dim == Dimension::Horizontal { |
282 | 0 | axis.scale = scale.x_scale; |
283 | 0 | axis.delta = scale.x_delta; |
284 | 0 | } else { |
285 | 0 | axis.scale = scale.y_scale; |
286 | 0 | axis.delta = scale.y_delta; |
287 | 0 | }; |
288 | 0 | let scale = axis.scale; |
289 | | // Scale the blue zones |
290 | 0 | for unscaled_blue in blues { |
291 | 0 | let position = fixed_mul(unscaled_blue.position, scale) + axis.delta; |
292 | 0 | let overshoot = fixed_mul(unscaled_blue.overshoot, scale) + axis.delta; |
293 | 0 | let mut blue = ScaledBlue { |
294 | 0 | position: ScaledWidth { |
295 | 0 | scaled: position, |
296 | 0 | fitted: position, |
297 | 0 | }, |
298 | 0 | overshoot: ScaledWidth { |
299 | 0 | scaled: overshoot, |
300 | 0 | fitted: overshoot, |
301 | 0 | }, |
302 | 0 | zones: unscaled_blue.zones, |
303 | 0 | is_active: false, |
304 | 0 | }; |
305 | | // A blue zone is only active if it is less than 3/4 pixels tall |
306 | 0 | let dist = fixed_mul(unscaled_blue.position - unscaled_blue.overshoot, scale); |
307 | 0 | if (-48..=48).contains(&dist) { |
308 | 0 | blue.position.fitted = pix_round(blue.position.scaled); |
309 | | // For CJK, "overshoot" is actually undershoot |
310 | 0 | let delta1 = fixed_div(blue.position.fitted, scale) - unscaled_blue.overshoot; |
311 | 0 | let mut delta2 = fixed_mul(delta1.abs(), scale); |
312 | 0 | if delta2 < 32 { |
313 | 0 | delta2 = 0; |
314 | 0 | } else { |
315 | 0 | delta2 = pix_round(delta2); |
316 | 0 | } |
317 | 0 | if delta1 < 0 { |
318 | 0 | delta2 = -delta2; |
319 | 0 | } |
320 | 0 | blue.overshoot.fitted = blue.position.fitted - delta2; |
321 | 0 | blue.is_active = true; |
322 | 0 | } |
323 | 0 | axis.blues.push(blue); |
324 | | } |
325 | | // FreeType never seems to compute scaled width values. We'll just |
326 | | // match this behavior for now. |
327 | | // <https://github.com/googlefonts/fontations/issues/1129> |
328 | 0 | for _ in 0..widths.len() { |
329 | 0 | axis.widths.push(ScaledWidth::default()); |
330 | 0 | } |
331 | 0 | axis.width_metrics = width_metrics; |
332 | 0 | axis |
333 | 0 | } |
334 | | |
335 | | #[cfg(test)] |
336 | | mod tests { |
337 | | use super::{ |
338 | | super::super::{shape::ShaperMode, style}, |
339 | | *, |
340 | | }; |
341 | | use crate::attribute::Style; |
342 | | use raw::{FontRef, TableProvider}; |
343 | | |
344 | | #[test] |
345 | | fn scaled_metrics_default() { |
346 | | // Note: expected values scraped from a FreeType debugging |
347 | | // session |
348 | | let scaled_metrics = make_scaled_metrics( |
349 | | font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS, |
350 | | StyleClass::HEBR, |
351 | | ); |
352 | | // Check scale and deltas |
353 | | assert_eq!(scaled_metrics.scale.x_scale, 67109); |
354 | | assert_eq!(scaled_metrics.scale.y_scale, 67109); |
355 | | assert_eq!(scaled_metrics.scale.x_delta, 0); |
356 | | assert_eq!(scaled_metrics.scale.y_delta, 0); |
357 | | // Horizontal widths |
358 | | let h_axis = &scaled_metrics.axes[0]; |
359 | | let expected_h_widths = [55]; |
360 | | // No horizontal blues |
361 | | check_axis(h_axis, &expected_h_widths, &[]); |
362 | | // Not extra light |
363 | | assert!(!h_axis.width_metrics.is_extra_light); |
364 | | // Vertical widths |
365 | | let v_axis = &scaled_metrics.axes[1]; |
366 | | let expected_v_widths = [22, 112]; |
367 | | // Vertical blues |
368 | | #[rustfmt::skip] |
369 | | let expected_v_blues = [ |
370 | | // ((scaled_pos, fitted_pos), (scaled_shoot, fitted_shoot), flags, is_active) |
371 | | ScaledBlue::from(((606, 576), (606, 576), BlueZones::TOP, true)), |
372 | | ScaledBlue::from(((0, 0), (-9, 0), BlueZones::default(), true)), |
373 | | ScaledBlue::from(((-246, -256), (-246, -256), BlueZones::default(), true)), |
374 | | ]; |
375 | | check_axis(v_axis, &expected_v_widths, &expected_v_blues); |
376 | | // This one is extra light |
377 | | assert!(v_axis.width_metrics.is_extra_light); |
378 | | } |
379 | | |
380 | | #[test] |
381 | | fn cjk_scaled_metrics() { |
382 | | // Note: expected values scraped from a FreeType debugging |
383 | | // session |
384 | | let scaled_metrics = make_scaled_metrics( |
385 | | font_test_data::NOTOSERIFTC_AUTOHINT_METRICS, |
386 | | StyleClass::HANI, |
387 | | ); |
388 | | // Check scale and deltas |
389 | | assert_eq!(scaled_metrics.scale.x_scale, 67109); |
390 | | assert_eq!(scaled_metrics.scale.y_scale, 67109); |
391 | | assert_eq!(scaled_metrics.scale.x_delta, 0); |
392 | | assert_eq!(scaled_metrics.scale.y_delta, 0); |
393 | | // Horizontal widths |
394 | | let h_axis = &scaled_metrics.axes[0]; |
395 | | let expected_h_widths = [0]; |
396 | | check_axis(h_axis, &expected_h_widths, &[]); |
397 | | // Not extra light |
398 | | assert!(!h_axis.width_metrics.is_extra_light); |
399 | | // Vertical widths |
400 | | let v_axis = &scaled_metrics.axes[1]; |
401 | | let expected_v_widths = [0]; |
402 | | // Vertical blues |
403 | | #[rustfmt::skip] |
404 | | let expected_v_blues = [ |
405 | | // ((scaled_pos, fitted_pos), (scaled_shoot, fitted_shoot), flags, is_active) |
406 | | ScaledBlue::from(((857, 832), (844, 832), BlueZones::TOP, true)), |
407 | | ScaledBlue::from(((-80, -64), (-68, -64), BlueZones::default(), true)), |
408 | | ]; |
409 | | // No horizontal blues |
410 | | check_axis(v_axis, &expected_v_widths, &expected_v_blues); |
411 | | // Also not extra light |
412 | | assert!(!v_axis.width_metrics.is_extra_light); |
413 | | } |
414 | | |
415 | | fn make_scaled_metrics(font_data: &[u8], style_class: usize) -> ScaledStyleMetrics { |
416 | | let font = FontRef::new(font_data).unwrap(); |
417 | | let class = &style::STYLE_CLASSES[style_class]; |
418 | | let shaper = Shaper::new(&font, ShaperMode::Nominal); |
419 | | let unscaled_metrics = compute_unscaled_style_metrics( |
420 | | &shaper, |
421 | | Default::default(), |
422 | | class, |
423 | | QuirksMode::default(), |
424 | | ); |
425 | | let scale = Scale::new( |
426 | | 16.0, |
427 | | font.head().unwrap().units_per_em() as i32, |
428 | | Style::Normal, |
429 | | Default::default(), |
430 | | class.script.group, |
431 | | ); |
432 | | scale_style_metrics(&unscaled_metrics, scale, QuirksMode::default()) |
433 | | } |
434 | | |
435 | | fn check_axis( |
436 | | axis: &ScaledAxisMetrics, |
437 | | expected_widths: &[i32], |
438 | | expected_blues: &[ScaledBlue], |
439 | | ) { |
440 | | let widths = axis |
441 | | .widths |
442 | | .iter() |
443 | | .map(|width| width.scaled) |
444 | | .collect::<Vec<_>>(); |
445 | | assert_eq!(widths, expected_widths); |
446 | | assert_eq!(axis.blues.as_slice(), expected_blues); |
447 | | } |
448 | | |
449 | | impl From<(i32, i32)> for ScaledWidth { |
450 | | fn from(value: (i32, i32)) -> Self { |
451 | | Self { |
452 | | scaled: value.0, |
453 | | fitted: value.1, |
454 | | } |
455 | | } |
456 | | } |
457 | | |
458 | | impl From<((i32, i32), (i32, i32), BlueZones, bool)> for ScaledBlue { |
459 | | fn from(value: ((i32, i32), (i32, i32), BlueZones, bool)) -> Self { |
460 | | Self { |
461 | | position: value.0.into(), |
462 | | overshoot: value.1.into(), |
463 | | zones: value.2, |
464 | | is_active: value.3, |
465 | | } |
466 | | } |
467 | | } |
468 | | } |