Skip to main content

read_fonts/ps/
transform.rs

1//! Font transform types.
2
3use super::num;
4use crate::Cursor;
5use types::Fixed;
6
7/// A fixed point font matrix.
8pub type FontMatrix = types::Matrix<Fixed>;
9
10/// Simple fixed point matrix multiplication with a scaling factor.
11///
12/// Note: this transforms the translation component of `b` by the upper 2x2 of
13/// `a`. This matches the offset transform FreeType uses when concatenating
14/// the matrices from the top and font dicts.
15fn combine_scaled(a: &FontMatrix, b: &FontMatrix, scale: i32) -> FontMatrix {
16    // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/base/ftcalc.c#L719>
17    let a = a.elements();
18    let b = b.elements();
19    let val = Fixed::from_i32(scale);
20    let xx = a[0].mul_div(b[0], val) + a[2].mul_div(b[1], val);
21    let yx = a[1].mul_div(b[0], val) + a[3].mul_div(b[1], val);
22    let xy = a[0].mul_div(b[2], val) + a[2].mul_div(b[3], val);
23    let yy = a[1].mul_div(b[2], val) + a[3].mul_div(b[3], val);
24    let x = b[4];
25    let y = b[5];
26    let dx = x.mul_div(a[0], val) + y.mul_div(a[2], val);
27    let dy = x.mul_div(a[1], val) + y.mul_div(a[3], val);
28    FontMatrix::from_elements([xx, yx, xy, yy, dx, dy])
29}
30
31/// Check for a degenerate matrix.
32/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/f1cd6dbfa0c98f352b698448f40ac27e8fb3832e/src/base/ftcalc.c#L725>
33pub(crate) fn is_degenerate(matrix: &FontMatrix) -> bool {
34    let [mut xx, mut yx, mut xy, mut yy, ..] = matrix.elements().map(|x| x.to_bits() as i64);
35    let val = xx.abs() | yx.abs() | xy.abs() | yy.abs();
36    if val == 0 || val > 0x7FFFFFFF {
37        return true;
38    }
39    // Scale the matrix to avoid temp1 overflow
40    let msb = 32 - (val as i32).leading_zeros() - 1;
41    let shift = msb as i32 - 12;
42    if shift > 0 {
43        xx >>= shift;
44        xy >>= shift;
45        yx >>= shift;
46        yy >>= shift;
47    }
48    let temp1 = 32 * (xx * yy - xy * yx).abs();
49    let temp2 = (xx * xx) + (xy * xy) + (yx * yx) + (yy * yy);
50    if temp1 <= temp2 {
51        return true;
52    }
53    false
54}
55
56/// Combination of a matrix and optional scale.
57#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
58pub struct Transform {
59    /// Affine font matrix.
60    pub matrix: FontMatrix,
61    /// Fixed point scale factor.
62    ///
63    /// This is assumed to convert from font units to 26.6 values.
64    pub scale: Option<Fixed>,
65}
66
67impl Transform {
68    /// The transform that doesn't modify coordinates or metrics.
69    pub const IDENTITY: Self = Self {
70        matrix: FontMatrix::IDENTITY,
71        scale: None,
72    };
73
74    /// Computes a scale factor for the given ppem and upem values.
75    pub fn compute_scale(ppem: f32, upem: i32) -> Fixed {
76        Fixed::from_bits((ppem * 64.0) as i32) / Fixed::from_bits(upem.max(1))
77    }
78
79    /// Applies the transform to a horizontal metric such as an advance
80    /// width.
81    pub fn transform_h_metric(&self, metric: Fixed) -> Fixed {
82        let mut metric = Fixed::from_bits(metric.to_i32());
83        if self.matrix.xx != Fixed::ONE {
84            // x scale
85            metric *= self.matrix.xx;
86        }
87        // x translation
88        metric += self.matrix.dx;
89        if let Some(scale) = self.scale {
90            // Multiplying by scale converts to 26.6 but we want to keep the
91            // result in 16.16
92            Fixed::from_bits((metric * scale).to_bits() << 10)
93        } else {
94            // Metric is currently in font units. Convert back to 16.16
95            Fixed::from_bits(metric.to_bits() << 16)
96        }
97    }
98}
99
100/// An affine matrix with a scaling factor.
101#[derive(Copy, Clone, PartialEq, Eq, Debug)]
102pub struct ScaledFontMatrix {
103    /// The matrix.
104    pub matrix: FontMatrix,
105    /// The dynamic scale factor used when parsing this matrix.
106    pub scale: i32,
107}
108
109impl ScaledFontMatrix {
110    /// Parses a font matrix using dynamic scaling factors.
111    ///
112    /// Returns the matrix and an adjusted upem factor.
113    pub(crate) fn parse(cursor: &mut Cursor) -> Option<Self> {
114        let mut values = [Fixed::ZERO; 6];
115        let mut scalings = [0i32; 6];
116        let mut max_scaling = i32::MIN;
117        let mut min_scaling = i32::MAX;
118        for (value, scaling) in values.iter_mut().zip(&mut scalings) {
119            let (v, s) = num::parse_fixed_dynamic(cursor).ok()?;
120            if v != Fixed::ZERO {
121                max_scaling = max_scaling.max(s);
122                min_scaling = min_scaling.min(s);
123            }
124            *value = v;
125            *scaling = s;
126        }
127        if !(-9..=0).contains(&max_scaling)
128            || (max_scaling - min_scaling < 0)
129            || (max_scaling - min_scaling) > 9
130        {
131            return None;
132        }
133        for (value, scaling) in values.iter_mut().zip(scalings) {
134            if *value == Fixed::ZERO {
135                continue;
136            }
137            let divisor = num::BCD_POWER_TENS[(max_scaling - scaling) as usize];
138            let half_divisor = divisor >> 1;
139            if *value < Fixed::ZERO {
140                if i32::MIN + half_divisor < value.to_bits() {
141                    *value = Fixed::from_bits((value.to_bits() - half_divisor) / divisor);
142                } else {
143                    *value = Fixed::from_bits(i32::MIN / divisor);
144                }
145            } else if i32::MAX - half_divisor > value.to_bits() {
146                *value = Fixed::from_bits((value.to_bits() + half_divisor) / divisor);
147            } else {
148                *value = Fixed::from_bits(i32::MAX / divisor);
149            }
150        }
151        let matrix = FontMatrix::from_elements(values);
152        // Check for a degenerate matrix
153        if is_degenerate(&matrix) {
154            return None;
155        }
156        let scale = num::BCD_POWER_TENS[(-max_scaling) as usize];
157        Some(Self { matrix, scale })
158    }
159
160    /// Concatenates this matrix, from a top dictionary, with `sub`, from a
161    /// font dictionary, and normalizes the result.
162    ///
163    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/f1cd6dbfa0c98f352b698448f40ac27e8fb3832e/src/cff/cffobjs.c#L746>
164    #[must_use]
165    pub fn combine(&self, sub: &ScaledFontMatrix) -> ScaledFontMatrix {
166        let scaling = if self.scale > 1 && sub.scale > 1 {
167            self.scale.min(sub.scale)
168        } else {
169            1
170        };
171        let matrix = combine_scaled(&self.matrix, &sub.matrix, scaling);
172        let scale = Fixed::from_bits(sub.scale)
173            .mul_div(Fixed::from_bits(self.scale), Fixed::from_bits(scaling));
174        ScaledFontMatrix {
175            matrix,
176            scale: scale.to_bits(),
177        }
178        .normalize()
179    }
180
181    /// Compute a new font matrix and UPEM scale factor where the Y scale of
182    /// the matrix is 1.0.    
183    #[must_use]
184    pub fn normalize(&self) -> Self {
185        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/f1cd6dbfa0c98f352b698448f40ac27e8fb3832e/src/cff/cffobjs.c#L727>
186        let mut matrix = self.matrix.elements();
187        let mut scaled_upem = self.scale;
188        let factor = if matrix[3] != Fixed::ZERO {
189            matrix[3].abs()
190        } else {
191            // Use yx if yy is zero
192            matrix[1].abs()
193        };
194        if factor != Fixed::ONE {
195            scaled_upem = (Fixed::from_bits(scaled_upem) / factor).to_bits();
196            for value in &mut matrix {
197                *value /= factor;
198            }
199        }
200        // FT shifts off the fractional parts of the translation?
201        for offset in matrix[4..6].iter_mut() {
202            *offset = Fixed::from_bits(offset.to_bits() >> 16);
203        }
204        Self {
205            matrix: FontMatrix::from_elements(matrix),
206            scale: scaled_upem,
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn compute_scale() {
217        assert_eq!(Transform::compute_scale(1000.0, 1000).to_bits(), 64 << 16);
218        assert_eq!(Transform::compute_scale(500.0, 1000).to_bits(), 32 << 16);
219        assert_eq!(Transform::compute_scale(2000.0, 1000).to_bits(), 128 << 16);
220        assert_eq!(Transform::compute_scale(16.0, 1000).to_bits(), 67109);
221    }
222
223    #[test]
224    fn h_metric_identity_integral() {
225        for metric in [Fixed::ZERO, Fixed::ONE, Fixed::NEG_ONE, Fixed::from_i32(42)] {
226            assert_eq!(Transform::IDENTITY.transform_h_metric(metric), metric);
227        }
228    }
229
230    #[test]
231    fn h_metric_identity_fractional() {
232        // Like FT, metrics are rounded to font units before applying matrix
233        // and scale
234        for metric in [
235            Fixed::from_f64(42.5),
236            Fixed::from_f64(-20.1),
237            Fixed::from_f64(18.8),
238        ] {
239            assert_eq!(
240                Transform::IDENTITY.transform_h_metric(metric),
241                metric.round()
242            );
243        }
244    }
245
246    #[test]
247    fn h_metric_matrix_scale() {
248        let transform = Transform {
249            matrix: FontMatrix::from_elements([2.0, 0.0, 0.0, 1.0, 0.0, 0.0].map(Fixed::from_f64)),
250            scale: None,
251        };
252        // metric.round() * 2
253        let pairs = [(42.5, 86.0), (-20.1, -40.0), (18.8, 38.0)];
254        for (metric, transformed_metric) in pairs {
255            assert_eq!(
256                transform
257                    .transform_h_metric(Fixed::from_f64(metric))
258                    .to_f64(),
259                transformed_metric
260            );
261        }
262    }
263
264    #[test]
265    fn h_metric_matrix_scale_offset() {
266        let transform = Transform {
267            matrix: FontMatrix::from_elements(
268                [2.0, 0.0, 0.0, 1.0, 10.0 / 65536.0, 0.0].map(Fixed::from_f64),
269            ),
270            scale: None,
271        };
272        // metric.round() * 2 + 10
273        let pairs = [(42.5, 96.0), (-20.1, -30.0), (18.8, 48.0)];
274        for (metric, transformed_metric) in pairs {
275            assert_eq!(
276                transform
277                    .transform_h_metric(Fixed::from_f64(metric))
278                    .to_f64(),
279                transformed_metric
280            );
281        }
282    }
283
284    #[test]
285    fn h_metric_scale() {
286        let transform = Transform {
287            matrix: FontMatrix::IDENTITY,
288            // Scale by 0.5
289            scale: Some(Fixed::from_i32(32)),
290        };
291        // metric.round() / 2
292        let pairs = [(42.5, 21.5), (-20.1, -10.0), (18.8, 9.5)];
293        for (metric, transformed_metric) in pairs {
294            assert_eq!(
295                transform
296                    .transform_h_metric(Fixed::from_f64(metric))
297                    .to_f64(),
298                transformed_metric
299            );
300        }
301    }
302
303    #[test]
304    fn h_metric_scale_matrix_scale_offset() {
305        let transform = Transform {
306            matrix: FontMatrix::from_elements(
307                [4.0, 0.0, 0.0, 1.0, 10.0 / 65536.0, 0.0].map(Fixed::from_f64),
308            ),
309            // Scale by 0.5
310            scale: Some(Fixed::from_i32(32)),
311        };
312        // (metric.round() * 4 + 10) / 2
313        let pairs = [(42.5, 91.0), (-20.1, -35.0), (18.8, 43.0)];
314        for (metric, transformed_metric) in pairs {
315            assert_eq!(
316                transform
317                    .transform_h_metric(Fixed::from_f64(metric))
318                    .to_f64(),
319                transformed_metric
320            );
321        }
322    }
323
324    /// See <https://github.com/googlefonts/fontations/issues/1595>
325    #[test]
326    fn degenerate_matrix_check_doesnt_overflow() {
327        // Values taken from font in the above issue
328        let matrix = FontMatrix::from_elements([
329            Fixed::from_bits(639999672),
330            Fixed::ZERO,
331            Fixed::ZERO,
332            Fixed::from_bits(639999672),
333            Fixed::ZERO,
334            Fixed::ZERO,
335        ]);
336        // Just don't panic with overflow
337        is_degenerate(&matrix);
338        // Try again with all max values
339        is_degenerate(&FontMatrix::from_elements([Fixed::MAX; 6]));
340        // And all min values
341        is_degenerate(&FontMatrix::from_elements([Fixed::MIN; 6]));
342    }
343
344    #[test]
345    fn normalize_matrix() {
346        // This matrix has a y scale of 0.5 so we should produce a new matrix
347        // with a y scale of 1.0 and a scale factor of 2
348        let matrix = ScaledFontMatrix {
349            matrix: FontMatrix::from_elements([65536, 0, 0, 32768, 0, 0].map(Fixed::from_bits)),
350            scale: 1,
351        };
352        let normalized = matrix.normalize();
353        let expected_normalized = [131072, 0, 0, 65536, 0, 0].map(Fixed::from_bits);
354        assert_eq!(normalized.matrix.elements(), expected_normalized);
355        assert_eq!(normalized.scale, 2);
356    }
357
358    #[test]
359    fn combine_matrix() {
360        let a = FontMatrix::from_elements([0.5, 0.75, -1.0, 2.0, 0.0, 0.0].map(Fixed::from_f64));
361        let b = FontMatrix::from_elements([1.5, -1.0, 0.25, -1.0, 1.0, 2.0].map(Fixed::from_f64));
362        let expected = [1.75, -0.875, 1.125, -1.8125, -1.5, 4.75].map(Fixed::from_f64);
363        let result = combine_scaled(&a, &b, 1);
364        assert_eq!(result.elements(), expected);
365    }
366}