Skip to main content

skrifa/outline/glyf/hint/
round.rs

1//! Rounding state.
2
3use super::{super::F26Dot6, graphics::GraphicsState};
4
5/// Rounding strategies supported by the interpreter.
6#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
7pub enum RoundMode {
8    /// Distances are rounded to the closest grid line.
9    ///
10    /// Set by `RTG` instruction.
11    #[default]
12    Grid,
13    /// Distances are rounded to the nearest half grid line.
14    ///
15    /// Set by `RTHG` instruction.
16    HalfGrid,
17    /// Distances are rounded to the closest half or integer pixel.
18    ///
19    /// Set by `RTDG` instruction.
20    DoubleGrid,
21    /// Distances are rounded down to the closest integer grid line.
22    ///
23    /// Set by `RDTG` instruction.
24    DownToGrid,
25    /// Distances are rounded up to the closest integer pixel boundary.
26    ///
27    /// Set by `RUTG` instruction.
28    UpToGrid,
29    /// Rounding is turned off.
30    ///
31    /// Set by `ROFF` instruction.
32    Off,
33    /// Allows fine control over the effects of the round state variable by
34    /// allowing you to set the values of three components of the round_state:
35    /// period, phase, and threshold.
36    ///
37    /// More formally, maps the domain of 26.6 fixed point numbers into a set
38    /// of discrete values that are separated by equal distances.
39    ///
40    /// Set by `SROUND` instruction.
41    Super,
42    /// Analogous to `Super`. The grid period is sqrt(2)/2 pixels rather than 1
43    /// pixel. It is useful for measuring at a 45 degree angle with the
44    /// coordinate axes.
45    ///
46    /// Set by `S45ROUND` instruction.
47    Super45,
48}
49
50/// Graphics state that controls rounding.
51///
52/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#round%20state>
53#[derive(Copy, Clone, Debug)]
54pub struct RoundState {
55    pub mode: RoundMode,
56    pub threshold: i32,
57    pub phase: i32,
58    pub period: i32,
59}
60
61impl Default for RoundState {
62    fn default() -> Self {
63        Self {
64            mode: RoundMode::Grid,
65            threshold: 0,
66            phase: 0,
67            period: 64,
68        }
69    }
70}
71
72impl RoundState {
73    pub fn round(&self, distance: F26Dot6) -> F26Dot6 {
74        use super::math;
75        use RoundMode::*;
76        let distance = distance.to_bits();
77        let round_bias = self.threshold.wrapping_sub(self.phase);
78        let neg_distance = distance.wrapping_neg();
79        let result = match self.mode {
80            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1958>
81            HalfGrid => {
82                if distance >= 0 {
83                    math::floor(distance).wrapping_add(32).max(0)
84                } else {
85                    (math::floor(neg_distance).wrapping_add(32).wrapping_neg()).min(0)
86                }
87            }
88            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1913>
89            Grid => {
90                if distance >= 0 {
91                    math::round(distance).max(0)
92                } else {
93                    (math::round(neg_distance).wrapping_neg()).min(0)
94                }
95            }
96            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2094>
97            DoubleGrid => {
98                if distance >= 0 {
99                    math::round_pad(distance, 32).max(0)
100                } else {
101                    (math::round_pad(neg_distance, 32).wrapping_neg()).min(0)
102                }
103            }
104            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2005>
105            DownToGrid => {
106                if distance >= 0 {
107                    math::floor(distance).max(0)
108                } else {
109                    (math::floor(neg_distance).wrapping_neg()).min(0)
110                }
111            }
112            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2049>
113            UpToGrid => {
114                if distance >= 0 {
115                    math::ceil(distance).max(0)
116                } else {
117                    (math::ceil(neg_distance).wrapping_neg()).min(0)
118                }
119            }
120            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2145>
121            Super => {
122                if distance >= 0 {
123                    let val = (distance.wrapping_add(round_bias) & self.period.wrapping_neg())
124                        .wrapping_add(self.phase);
125                    if val < 0 {
126                        self.phase
127                    } else {
128                        val
129                    }
130                } else {
131                    let val = ((round_bias.wrapping_sub(distance)) & self.period.wrapping_neg())
132                        .wrapping_neg()
133                        .wrapping_sub(self.phase);
134                    if val > 0 {
135                        self.phase.wrapping_neg()
136                    } else {
137                        val
138                    }
139                }
140            }
141            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2199>
142            Super45 => {
143                if distance >= 0 {
144                    let val = distance
145                        .wrapping_add(round_bias)
146                        .wrapping_div(self.period)
147                        .wrapping_mul(self.period)
148                        .wrapping_add(self.phase);
149                    if val < 0 {
150                        self.phase
151                    } else {
152                        val
153                    }
154                } else {
155                    let val = round_bias
156                        .wrapping_sub(distance)
157                        .wrapping_div(self.period)
158                        .wrapping_mul(self.period)
159                        .wrapping_neg()
160                        .wrapping_sub(self.phase);
161                    if val > 0 {
162                        self.phase.wrapping_neg()
163                    } else {
164                        val
165                    }
166                }
167            }
168            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1870>
169            Off => distance,
170        };
171        F26Dot6::from_bits(result)
172    }
173}
174
175impl GraphicsState<'_> {
176    pub fn round(&self, distance: F26Dot6) -> F26Dot6 {
177        self.round_state.round(distance)
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::{F26Dot6, RoundMode, RoundState};
184
185    #[test]
186    fn round_to_grid() {
187        round_cases(
188            RoundMode::Grid,
189            &[(0, 0), (32, 64), (-32, -64), (64, 64), (50, 64)],
190        );
191    }
192
193    #[test]
194    fn round_to_half_grid() {
195        round_cases(
196            RoundMode::HalfGrid,
197            &[(0, 32), (32, 32), (-32, -32), (64, 96), (50, 32)],
198        );
199    }
200
201    #[test]
202    fn round_to_double_grid() {
203        round_cases(
204            RoundMode::DoubleGrid,
205            &[(0, 0), (32, 32), (-32, -32), (64, 64), (50, 64)],
206        );
207    }
208
209    #[test]
210    fn round_down_to_grid() {
211        round_cases(
212            RoundMode::DownToGrid,
213            &[(0, 0), (32, 0), (-32, 0), (64, 64), (50, 0)],
214        );
215    }
216
217    #[test]
218    fn round_up_to_grid() {
219        round_cases(
220            RoundMode::UpToGrid,
221            &[(0, 0), (32, 64), (-32, -64), (64, 64), (50, 64)],
222        );
223    }
224
225    #[test]
226    fn round_off() {
227        round_cases(
228            RoundMode::Off,
229            &[(0, 0), (32, 32), (-32, -32), (64, 64), (50, 50)],
230        );
231    }
232
233    #[test]
234    fn negative_min_value_does_not_panic_with_overflow() {
235        let value = F26Dot6::from_bits(i32::MIN);
236        for mode in [
237            RoundMode::Grid,
238            RoundMode::HalfGrid,
239            RoundMode::DoubleGrid,
240            RoundMode::DownToGrid,
241            RoundMode::UpToGrid,
242            RoundMode::Off,
243        ] {
244            let state = RoundState {
245                mode,
246                ..Default::default()
247            };
248            let _ = state.round(value);
249        }
250    }
251
252    #[test]
253    fn super_round_does_not_panic_with_extreme_values() {
254        let state = RoundState {
255            mode: RoundMode::Super,
256            threshold: i32::MAX,
257            phase: i32::MIN,
258            period: i32::MIN,
259        };
260        let _ = state.round(F26Dot6::from_bits(i32::MAX));
261        let _ = state.round(F26Dot6::from_bits(i32::MIN));
262    }
263
264    #[test]
265    fn super45_round_does_not_panic_with_extreme_values() {
266        let state = RoundState {
267            mode: RoundMode::Super45,
268            threshold: i32::MAX,
269            phase: i32::MIN,
270            period: -1,
271        };
272        let _ = state.round(F26Dot6::from_bits(i32::MAX));
273        let _ = state.round(F26Dot6::from_bits(i32::MIN));
274    }
275
276    fn round_cases(mode: RoundMode, cases: &[(i32, i32)]) {
277        for (value, expected) in cases.iter().copied() {
278            let value = F26Dot6::from_bits(value);
279            let expected = F26Dot6::from_bits(expected);
280            let state = RoundState {
281                mode,
282                ..Default::default()
283            };
284            let result = state.round(value);
285            assert_eq!(
286                result, expected,
287                "mismatch in rounding: {mode:?}({value}) = {result} (expected {expected})"
288            );
289        }
290    }
291}