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 result = match self.mode {
78            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1958>
79            HalfGrid => {
80                if distance >= 0 {
81                    (math::floor(distance) + 32).max(0)
82                } else {
83                    (-(math::floor(-distance) + 32)).min(0)
84                }
85            }
86            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1913>
87            Grid => {
88                if distance >= 0 {
89                    math::round(distance).max(0)
90                } else {
91                    (-math::round(-distance)).min(0)
92                }
93            }
94            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2094>
95            DoubleGrid => {
96                if distance >= 0 {
97                    math::round_pad(distance, 32).max(0)
98                } else {
99                    (-math::round_pad(-distance, 32)).min(0)
100                }
101            }
102            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2005>
103            DownToGrid => {
104                if distance >= 0 {
105                    math::floor(distance).max(0)
106                } else {
107                    (-math::floor(-distance)).min(0)
108                }
109            }
110            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2049>
111            UpToGrid => {
112                if distance >= 0 {
113                    math::ceil(distance).max(0)
114                } else {
115                    (-math::ceil(-distance)).min(0)
116                }
117            }
118            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2145>
119            Super => {
120                if distance >= 0 {
121                    let val =
122                        ((distance + (self.threshold - self.phase)) & -self.period) + self.phase;
123                    if val < 0 {
124                        self.phase
125                    } else {
126                        val
127                    }
128                } else {
129                    let val =
130                        -(((self.threshold - self.phase) - distance) & -self.period) - self.phase;
131                    if val > 0 {
132                        -self.phase
133                    } else {
134                        val
135                    }
136                }
137            }
138            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2199>
139            Super45 => {
140                if distance >= 0 {
141                    let val = (((distance + (self.threshold - self.phase)) / self.period)
142                        * self.period)
143                        + self.phase;
144                    if val < 0 {
145                        self.phase
146                    } else {
147                        val
148                    }
149                } else {
150                    let val = -((((self.threshold - self.phase) - distance) / self.period)
151                        * self.period)
152                        - self.phase;
153                    if val > 0 {
154                        -self.phase
155                    } else {
156                        val
157                    }
158                }
159            }
160            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1870>
161            Off => distance,
162        };
163        F26Dot6::from_bits(result)
164    }
165}
166
167impl GraphicsState<'_> {
168    pub fn round(&self, distance: F26Dot6) -> F26Dot6 {
169        self.round_state.round(distance)
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::{F26Dot6, RoundMode, RoundState};
176
177    #[test]
178    fn round_to_grid() {
179        round_cases(
180            RoundMode::Grid,
181            &[(0, 0), (32, 64), (-32, -64), (64, 64), (50, 64)],
182        );
183    }
184
185    #[test]
186    fn round_to_half_grid() {
187        round_cases(
188            RoundMode::HalfGrid,
189            &[(0, 32), (32, 32), (-32, -32), (64, 96), (50, 32)],
190        );
191    }
192
193    #[test]
194    fn round_to_double_grid() {
195        round_cases(
196            RoundMode::DoubleGrid,
197            &[(0, 0), (32, 32), (-32, -32), (64, 64), (50, 64)],
198        );
199    }
200
201    #[test]
202    fn round_down_to_grid() {
203        round_cases(
204            RoundMode::DownToGrid,
205            &[(0, 0), (32, 0), (-32, 0), (64, 64), (50, 0)],
206        );
207    }
208
209    #[test]
210    fn round_up_to_grid() {
211        round_cases(
212            RoundMode::UpToGrid,
213            &[(0, 0), (32, 64), (-32, -64), (64, 64), (50, 64)],
214        );
215    }
216
217    #[test]
218    fn round_off() {
219        round_cases(
220            RoundMode::Off,
221            &[(0, 0), (32, 32), (-32, -32), (64, 64), (50, 50)],
222        );
223    }
224
225    fn round_cases(mode: RoundMode, cases: &[(i32, i32)]) {
226        for (value, expected) in cases.iter().copied() {
227            let value = F26Dot6::from_bits(value);
228            let expected = F26Dot6::from_bits(expected);
229            let state = RoundState {
230                mode,
231                ..Default::default()
232            };
233            let result = state.round(value);
234            assert_eq!(
235                result, expected,
236                "mismatch in rounding: {mode:?}({value}) = {result} (expected {expected})"
237            );
238        }
239    }
240}