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}