skrifa/outline/glyf/hint/
round.rs1use super::{super::F26Dot6, graphics::GraphicsState};
4
5#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
7pub enum RoundMode {
8 #[default]
12 Grid,
13 HalfGrid,
17 DoubleGrid,
21 DownToGrid,
25 UpToGrid,
29 Off,
33 Super,
42 Super45,
48}
49
50#[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 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 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 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 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 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 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 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 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}