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.
15pub fn 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    /// Compute a new font matrix and UPEM scale factor where the Y scale of
161    /// the matrix is 1.0.    
162    #[must_use]
163    pub fn normalize(&self) -> Self {
164        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/f1cd6dbfa0c98f352b698448f40ac27e8fb3832e/src/cff/cffobjs.c#L727>
165        let mut matrix = self.matrix.elements();
166        let mut scaled_upem = self.scale;
167        let factor = if matrix[3] != Fixed::ZERO {
168            matrix[3].abs()
169        } else {
170            // Use yx if yy is zero
171            matrix[1].abs()
172        };
173        if factor != Fixed::ONE {
174            scaled_upem = (Fixed::from_bits(scaled_upem) / factor).to_bits();
175            for value in &mut matrix {
176                *value /= factor;
177            }
178        }
179        // FT shifts off the fractional parts of the translation?
180        for offset in matrix[4..6].iter_mut() {
181            *offset = Fixed::from_bits(offset.to_bits() >> 16);
182        }
183        Self {
184            matrix: FontMatrix::from_elements(matrix),
185            scale: scaled_upem,
186        }
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn compute_scale() {
196        assert_eq!(Transform::compute_scale(1000.0, 1000).to_bits(), 64 << 16);
197        assert_eq!(Transform::compute_scale(500.0, 1000).to_bits(), 32 << 16);
198        assert_eq!(Transform::compute_scale(2000.0, 1000).to_bits(), 128 << 16);
199        assert_eq!(Transform::compute_scale(16.0, 1000).to_bits(), 67109);
200    }
201
202    #[test]
203    fn h_metric_identity_integral() {
204        for metric in [Fixed::ZERO, Fixed::ONE, Fixed::NEG_ONE, Fixed::from_i32(42)] {
205            assert_eq!(Transform::IDENTITY.transform_h_metric(metric), metric);
206        }
207    }
208
209    #[test]
210    fn h_metric_identity_fractional() {
211        // Like FT, metrics are rounded to font units before applying matrix
212        // and scale
213        for metric in [
214            Fixed::from_f64(42.5),
215            Fixed::from_f64(-20.1),
216            Fixed::from_f64(18.8),
217        ] {
218            assert_eq!(
219                Transform::IDENTITY.transform_h_metric(metric),
220                metric.round()
221            );
222        }
223    }
224
225    #[test]
226    fn h_metric_matrix_scale() {
227        let transform = Transform {
228            matrix: FontMatrix::from_elements([2.0, 0.0, 0.0, 1.0, 0.0, 0.0].map(Fixed::from_f64)),
229            scale: None,
230        };
231        // metric.round() * 2
232        let pairs = [(42.5, 86.0), (-20.1, -40.0), (18.8, 38.0)];
233        for (metric, transformed_metric) in pairs {
234            assert_eq!(
235                transform
236                    .transform_h_metric(Fixed::from_f64(metric))
237                    .to_f64(),
238                transformed_metric
239            );
240        }
241    }
242
243    #[test]
244    fn h_metric_matrix_scale_offset() {
245        let transform = Transform {
246            matrix: FontMatrix::from_elements(
247                [2.0, 0.0, 0.0, 1.0, 10.0 / 65536.0, 0.0].map(Fixed::from_f64),
248            ),
249            scale: None,
250        };
251        // metric.round() * 2 + 10
252        let pairs = [(42.5, 96.0), (-20.1, -30.0), (18.8, 48.0)];
253        for (metric, transformed_metric) in pairs {
254            assert_eq!(
255                transform
256                    .transform_h_metric(Fixed::from_f64(metric))
257                    .to_f64(),
258                transformed_metric
259            );
260        }
261    }
262
263    #[test]
264    fn h_metric_scale() {
265        let transform = Transform {
266            matrix: FontMatrix::IDENTITY,
267            // Scale by 0.5
268            scale: Some(Fixed::from_i32(32)),
269        };
270        // metric.round() / 2
271        let pairs = [(42.5, 21.5), (-20.1, -10.0), (18.8, 9.5)];
272        for (metric, transformed_metric) in pairs {
273            assert_eq!(
274                transform
275                    .transform_h_metric(Fixed::from_f64(metric))
276                    .to_f64(),
277                transformed_metric
278            );
279        }
280    }
281
282    #[test]
283    fn h_metric_scale_matrix_scale_offset() {
284        let transform = Transform {
285            matrix: FontMatrix::from_elements(
286                [4.0, 0.0, 0.0, 1.0, 10.0 / 65536.0, 0.0].map(Fixed::from_f64),
287            ),
288            // Scale by 0.5
289            scale: Some(Fixed::from_i32(32)),
290        };
291        // (metric.round() * 4 + 10) / 2
292        let pairs = [(42.5, 91.0), (-20.1, -35.0), (18.8, 43.0)];
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    /// See <https://github.com/googlefonts/fontations/issues/1595>
304    #[test]
305    fn degenerate_matrix_check_doesnt_overflow() {
306        // Values taken from font in the above issue
307        let matrix = FontMatrix::from_elements([
308            Fixed::from_bits(639999672),
309            Fixed::ZERO,
310            Fixed::ZERO,
311            Fixed::from_bits(639999672),
312            Fixed::ZERO,
313            Fixed::ZERO,
314        ]);
315        // Just don't panic with overflow
316        is_degenerate(&matrix);
317        // Try again with all max values
318        is_degenerate(&FontMatrix::from_elements([Fixed::MAX; 6]));
319        // And all min values
320        is_degenerate(&FontMatrix::from_elements([Fixed::MIN; 6]));
321    }
322
323    #[test]
324    fn normalize_matrix() {
325        // This matrix has a y scale of 0.5 so we should produce a new matrix
326        // with a y scale of 1.0 and a scale factor of 2
327        let matrix = ScaledFontMatrix {
328            matrix: FontMatrix::from_elements([65536, 0, 0, 32768, 0, 0].map(Fixed::from_bits)),
329            scale: 1,
330        };
331        let normalized = matrix.normalize();
332        let expected_normalized = [131072, 0, 0, 65536, 0, 0].map(Fixed::from_bits);
333        assert_eq!(normalized.matrix.elements(), expected_normalized);
334        assert_eq!(normalized.scale, 2);
335    }
336
337    #[test]
338    fn combine_matrix() {
339        let a = FontMatrix::from_elements([0.5, 0.75, -1.0, 2.0, 0.0, 0.0].map(Fixed::from_f64));
340        let b = FontMatrix::from_elements([1.5, -1.0, 0.25, -1.0, 1.0, 2.0].map(Fixed::from_f64));
341        let expected = [1.75, -0.875, 1.125, -1.8125, -1.5, 4.75].map(Fixed::from_f64);
342        let result = combine_scaled(&a, &b, 1);
343        assert_eq!(result.elements(), expected);
344    }
345}