/src/fontations/skrifa/src/outline/autohint/topo/edges.rs
Line | Count | Source (jump to first uncovered line) |
1 | | //! Edge detection. |
2 | | //! |
3 | | //! Edges are sets of segments that all lie within a threshold based on |
4 | | //! stem widths. |
5 | | //! |
6 | | //! Here we compute edges from the segment list, assign properties (round, |
7 | | //! serif, links) and then associate them with blue zones. |
8 | | |
9 | | use super::{ |
10 | | super::{ |
11 | | metrics::{fixed_div, fixed_mul, Scale, ScaledAxisMetrics, ScaledBlue, UnscaledBlue}, |
12 | | outline::Direction, |
13 | | style::ScriptGroup, |
14 | | }, |
15 | | Axis, Edge, Segment, |
16 | | }; |
17 | | |
18 | | /// Links segments to edges, using feature analysis for selection. |
19 | | /// |
20 | | /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2128> |
21 | 3.85M | pub(crate) fn compute_edges( |
22 | 3.85M | axis: &mut Axis, |
23 | 3.85M | metrics: &ScaledAxisMetrics, |
24 | 3.85M | top_to_bottom_hinting: bool, |
25 | 3.85M | y_scale: i32, |
26 | 3.85M | group: ScriptGroup, |
27 | 3.85M | ) { |
28 | 3.85M | axis.edges.clear(); |
29 | 3.85M | let scale = metrics.scale; |
30 | | // This is always passed as 0 in functions that take hinting direction |
31 | | // in CJK |
32 | | // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1114> |
33 | 3.85M | let top_to_bottom_hinting = if axis.dim == Axis::HORIZONTAL || group != ScriptGroup::Default { |
34 | 2.05M | false |
35 | | } else { |
36 | 1.80M | top_to_bottom_hinting |
37 | | }; |
38 | | // Ignore horizontal segments less than 1 pixel in length |
39 | 3.85M | let segment_length_threshold = if axis.dim == Axis::HORIZONTAL { |
40 | 1.41M | fixed_div(64, y_scale) |
41 | | } else { |
42 | 2.44M | 0 |
43 | | }; |
44 | | // Also ignore segments with a width delta larger than 0.5 pixels |
45 | 3.85M | let segment_width_threshold = fixed_div(32, scale); |
46 | 3.85M | // Ensure that edge distance threshold is less than or equal to |
47 | 3.85M | // 0.25 pixels |
48 | 3.85M | let initial_threshold = metrics.width_metrics.edge_distance_threshold; |
49 | | const EDGE_DISTANCE_THRESHOLD_MAX: i32 = 64 / 4; |
50 | 3.85M | let edge_distance_threshold = if group == ScriptGroup::Default { |
51 | 2.57M | fixed_div( |
52 | 2.57M | fixed_mul(initial_threshold, scale).min(EDGE_DISTANCE_THRESHOLD_MAX), |
53 | 2.57M | scale, |
54 | 2.57M | ) |
55 | | } else { |
56 | | // CJK uses a slightly different computation here |
57 | | // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1043> |
58 | 1.28M | let threshold = fixed_mul(initial_threshold, scale); |
59 | 1.28M | if threshold > EDGE_DISTANCE_THRESHOLD_MAX { |
60 | 758k | fixed_div(EDGE_DISTANCE_THRESHOLD_MAX, scale) |
61 | | } else { |
62 | 524k | initial_threshold |
63 | | } |
64 | | }; |
65 | | // Now build the sorted table of edges by looping over all segments |
66 | | // to find a matching edge, adding a new one if not found. |
67 | | // We can't iterate segments because we make mutable calls on `axis` |
68 | | // below which causes overlapping borrows |
69 | 13.7M | for segment_ix in 0..axis.segments.len() { |
70 | 13.7M | let segment = &axis.segments[segment_ix]; |
71 | 13.7M | if group == ScriptGroup::Default { |
72 | | // Ignore segments that are too short, too wide or direction-less |
73 | 9.36M | if (segment.height as i32) < segment_length_threshold |
74 | 9.18M | || (segment.delta as i32 > segment_width_threshold) |
75 | 9.05M | || segment.dir == Direction::None |
76 | | { |
77 | 702k | continue; |
78 | 8.66M | } |
79 | 8.66M | // Ignore serif edges that are smaller than 1.5 pixels |
80 | 8.66M | if segment.serif_ix.is_some() |
81 | 563k | && (2 * segment.height as i32) < (3 * segment_length_threshold) |
82 | | { |
83 | 3.30k | continue; |
84 | 8.66M | } |
85 | 4.36M | } |
86 | | // Look for a corresponding edge for this segment |
87 | 13.0M | let mut best_dist = i32::MAX; |
88 | 13.0M | let mut best_edge_ix = None; |
89 | 63.3M | for edge_ix in 0..axis.edges.len() { |
90 | 63.3M | let edge = &axis.edges[edge_ix]; |
91 | 63.3M | let dist = (segment.pos as i32 - edge.fpos as i32).abs(); |
92 | 63.3M | if dist < edge_distance_threshold && edge.dir == segment.dir && dist < best_dist { |
93 | 635k | if group == ScriptGroup::Default { |
94 | 330k | best_edge_ix = Some(edge_ix); |
95 | 330k | break; |
96 | 305k | } |
97 | | // For CJK, we add some additional checks |
98 | | // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1073> |
99 | 305k | if let Some(link) = segment.link(&axis.segments).copied() { |
100 | | // Check whether all linked segments of the candidate edge |
101 | | // can make a single edge |
102 | 235k | let first_ix = edge.first_ix as usize; |
103 | 235k | let mut seg1 = &axis.segments[first_ix]; |
104 | 235k | let mut dist2 = 0; |
105 | | loop { |
106 | 308k | if let Some(link1) = seg1.link(&axis.segments).copied() { |
107 | 281k | dist2 = (link.pos as i32 - link1.pos as i32).abs(); |
108 | 281k | if dist2 >= edge_distance_threshold { |
109 | 10.8k | break; |
110 | 270k | } |
111 | 27.7k | } |
112 | 298k | if seg1.edge_next_ix == Some(first_ix as u16) { |
113 | 224k | break; |
114 | 73.8k | } |
115 | 73.8k | if let Some(next) = seg1.next_in_edge(&axis.segments) { |
116 | 73.8k | seg1 = next; |
117 | 73.8k | } else { |
118 | 0 | break; |
119 | | } |
120 | | } |
121 | 235k | if dist2 >= edge_distance_threshold { |
122 | 10.8k | continue; |
123 | 224k | } |
124 | 69.9k | } |
125 | 294k | best_dist = dist; |
126 | 294k | best_edge_ix = Some(edge_ix); |
127 | 62.7M | } |
128 | | } |
129 | 13.0M | if let Some(edge_ix) = best_edge_ix { |
130 | 624k | axis.append_segment_to_edge(segment_ix, edge_ix); |
131 | 12.4M | } else { |
132 | 12.4M | // We couldn't find an edge, so add a new one for this segment |
133 | 12.4M | let opos = fixed_mul(segment.pos as i32, scale); |
134 | 12.4M | let edge = Edge { |
135 | 12.4M | fpos: segment.pos, |
136 | 12.4M | opos, |
137 | 12.4M | pos: opos, |
138 | 12.4M | dir: segment.dir, |
139 | 12.4M | first_ix: segment_ix as u16, |
140 | 12.4M | last_ix: segment_ix as u16, |
141 | 12.4M | ..Default::default() |
142 | 12.4M | }; |
143 | 12.4M | axis.insert_edge(edge, top_to_bottom_hinting); |
144 | 12.4M | axis.segments[segment_ix].edge_next_ix = Some(segment_ix as u16); |
145 | 12.4M | } |
146 | | } |
147 | 3.85M | if group == ScriptGroup::Default { |
148 | | // Loop again to find single point segments without a direction and |
149 | | // associate them with an existing edge if possible |
150 | 9.36M | for segment_ix in 0..axis.segments.len() { |
151 | 9.36M | let segment = &axis.segments[segment_ix]; |
152 | 9.36M | if segment.dir != Direction::None { |
153 | 8.82M | continue; |
154 | 549k | } |
155 | | // Try to find an edge that coincides with this segment within the |
156 | | // threshold |
157 | 549k | if let Some(edge_ix) = axis |
158 | 549k | .edges |
159 | 549k | .iter() |
160 | 549k | .enumerate() |
161 | 2.17M | .filter_map(|(ix, edge)| { |
162 | 2.17M | ((segment.pos as i32 - edge.fpos as i32).abs() < edge_distance_threshold) |
163 | 2.17M | .then_some(ix) |
164 | 2.17M | }) |
165 | 549k | .next() |
166 | 12.0k | { |
167 | 12.0k | // We found an edge, link everything up |
168 | 12.0k | axis.append_segment_to_edge(segment_ix, edge_ix); |
169 | 537k | } |
170 | | } |
171 | 1.28M | } |
172 | 3.85M | link_segments_to_edges(axis); |
173 | 3.85M | compute_edge_properties(axis); |
174 | 3.85M | } |
175 | | |
176 | | /// Edges get reordered as they're built so we need to assign edge indices to |
177 | | /// segments in a second pass. |
178 | 3.85M | fn link_segments_to_edges(axis: &mut Axis) { |
179 | 3.85M | let segments = axis.segments.as_mut_slice(); |
180 | 12.4M | for edge_ix in 0..axis.edges.len() { |
181 | 12.4M | let edge = &axis.edges[edge_ix]; |
182 | 12.4M | let mut ix = edge.first_ix as usize; |
183 | 12.4M | let last_ix = edge.last_ix as usize; |
184 | | loop { |
185 | 13.0M | let segment = &mut segments[ix]; |
186 | 13.0M | segment.edge_ix = Some(edge_ix as u16); |
187 | 13.0M | if ix == last_ix { |
188 | 12.4M | break; |
189 | 636k | } |
190 | 636k | ix = segment |
191 | 636k | .edge_next_ix |
192 | 636k | .map(|ix| ix as usize) |
193 | 636k | .unwrap_or(last_ix); |
194 | | } |
195 | | } |
196 | 3.85M | } |
197 | | |
198 | | /// Compute the edge properties based on the series of segments that make |
199 | | /// up the edge. |
200 | | /// |
201 | | /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2339> |
202 | 3.85M | fn compute_edge_properties(axis: &mut Axis) { |
203 | 3.85M | let edges = axis.edges.as_mut_slice(); |
204 | 3.85M | let segments = axis.segments.as_slice(); |
205 | 12.4M | for edge_ix in 0..edges.len() { |
206 | 12.4M | let mut roundness = 0; |
207 | 12.4M | let mut straightness = 0; |
208 | 12.4M | let edge = edges[edge_ix]; |
209 | 12.4M | let mut segment_ix = edge.first_ix as usize; |
210 | 12.4M | let last_segment_ix = edge.last_ix as usize; |
211 | | loop { |
212 | | // This loop can modify the current edge, so make sure we |
213 | | // reload it here |
214 | 13.0M | let edge = edges[edge_ix]; |
215 | 13.0M | let segment = &segments[segment_ix]; |
216 | 13.0M | let next_segment_ix = segment.edge_next_ix; |
217 | 13.0M | // Check roundness |
218 | 13.0M | if segment.flags & Segment::ROUND != 0 { |
219 | 5.01M | roundness += 1; |
220 | 8.03M | } else { |
221 | 8.03M | straightness += 1; |
222 | 8.03M | } |
223 | | // Check for serifs |
224 | 13.0M | let is_serif = if let Some(serif_ix) = segment.serif_ix { |
225 | 884k | let serif = &segments[serif_ix as usize]; |
226 | 884k | serif.edge_ix.is_some() && serif.edge_ix != Some(edge_ix as u16) |
227 | | } else { |
228 | 12.1M | false |
229 | | }; |
230 | | // Check for links |
231 | 13.0M | if is_serif |
232 | 12.2M | || (segment.link_ix.is_some() |
233 | 10.5M | && segments[segment.link_ix.unwrap() as usize] |
234 | 10.5M | .edge_ix |
235 | 10.5M | .is_some()) |
236 | | { |
237 | 11.3M | let (edge2_ix, segment2_ix) = if is_serif { |
238 | 798k | (edge.serif_ix, segment.serif_ix) |
239 | | } else { |
240 | 10.5M | (edge.link_ix, segment.link_ix) |
241 | | }; |
242 | 11.3M | let edge2_ix = if let (Some(edge2_ix), Some(segment2_ix)) = (edge2_ix, segment2_ix) |
243 | | { |
244 | 438k | let edge2 = &edges[edge2_ix as usize]; |
245 | 438k | let edge_delta = (edge.fpos as i32 - edge2.fpos as i32).abs(); |
246 | 438k | let segment2 = &segments[segment2_ix as usize]; |
247 | 438k | let segment_delta = (segment.pos as i32 - segment2.pos as i32).abs(); |
248 | 438k | if segment_delta < edge_delta { |
249 | 34.9k | segment2.edge_ix |
250 | | } else { |
251 | 403k | Some(edge2_ix) |
252 | | } |
253 | 10.8M | } else if let Some(segment2_ix) = segment2_ix { |
254 | 10.8M | segments[segment2_ix as usize].edge_ix |
255 | | } else { |
256 | 0 | edge2_ix |
257 | | }; |
258 | 11.3M | if is_serif { |
259 | 798k | edges[edge_ix].serif_ix = edge2_ix; |
260 | 798k | edges[edge2_ix.unwrap() as usize].flags |= Edge::SERIF; |
261 | 10.5M | } else { |
262 | 10.5M | edges[edge_ix].link_ix = edge2_ix; |
263 | 10.5M | } |
264 | 1.70M | } |
265 | 13.0M | if segment_ix == last_segment_ix { |
266 | 12.4M | break; |
267 | 636k | } |
268 | 636k | segment_ix = next_segment_ix |
269 | 636k | .map(|ix| ix as usize) |
270 | 636k | .unwrap_or(last_segment_ix); |
271 | | } |
272 | 12.4M | let edge = &mut edges[edge_ix]; |
273 | 12.4M | edge.flags = Edge::NORMAL; |
274 | 12.4M | if roundness > 0 && roundness >= straightness { |
275 | 4.75M | edge.flags |= Edge::ROUND; |
276 | 7.65M | } |
277 | | // Drop serifs for linked edges |
278 | 12.4M | if edge.serif_ix.is_some() && edge.link_ix.is_some() { |
279 | 22.8k | edge.serif_ix = None; |
280 | 12.3M | } |
281 | | } |
282 | 3.85M | } |
283 | | |
284 | | /// Compute all edges which lie within blue zones. |
285 | | /// |
286 | | /// For Latin, this is only done for the vertical axis. |
287 | | /// |
288 | | /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2503> |
289 | 2.09M | pub(crate) fn compute_blue_edges( |
290 | 2.09M | axis: &mut Axis, |
291 | 2.09M | scale: &Scale, |
292 | 2.09M | unscaled_blues: &[UnscaledBlue], |
293 | 2.09M | blues: &[ScaledBlue], |
294 | 2.09M | group: ScriptGroup, |
295 | 2.09M | ) { |
296 | 2.09M | // For the default script group, don't compute blues in the horizontal |
297 | 2.09M | // direction |
298 | 2.09M | // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3572> |
299 | 2.09M | if axis.dim != Axis::VERTICAL && group == ScriptGroup::Default { |
300 | 0 | return; |
301 | 2.09M | } |
302 | 2.09M | let axis_scale = if axis.dim == Axis::HORIZONTAL { |
303 | 0 | scale.x_scale |
304 | | } else { |
305 | 2.09M | scale.y_scale |
306 | | }; |
307 | | // Initial threshold |
308 | 2.09M | let initial_best_dest = fixed_mul(scale.units_per_em / 40, axis_scale).min(64 / 2); |
309 | 9.28M | for edge in &mut axis.edges { |
310 | 7.18M | let mut best_blue = None; |
311 | 7.18M | let mut best_is_neutral = false; |
312 | 7.18M | // Initial threshold as a fraction of em size with a max distance |
313 | 7.18M | // of 0.5 pixels |
314 | 7.18M | let mut best_dist = initial_best_dest; |
315 | 14.4M | for (unscaled_blue, blue) in unscaled_blues.iter().zip(blues) { |
316 | | // Ignore inactive blue zones |
317 | 14.4M | if !blue.is_active { |
318 | 1.17M | continue; |
319 | 13.2M | } |
320 | 13.2M | let is_top = blue.zones.is_top_like(); |
321 | 13.2M | let is_neutral = blue.zones.is_neutral(); |
322 | 13.2M | let is_major_dir = edge.dir == axis.major_dir; |
323 | 13.2M | // Both directions are handled for neutral blues |
324 | 13.2M | if is_top ^ is_major_dir || is_neutral { |
325 | | // Compare to reference position |
326 | 7.27M | let (ref_pos, matching_blue) = if group == ScriptGroup::Default { |
327 | 7.16M | (unscaled_blue.position, blue.position) |
328 | | } else { |
329 | | // For CJK, we take the blue with the smallest delta |
330 | | // from the edge |
331 | | // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1356> |
332 | 110k | if (edge.fpos as i32 - unscaled_blue.position).abs() |
333 | 110k | > (edge.fpos as i32 - unscaled_blue.overshoot).abs() |
334 | | { |
335 | 0 | (unscaled_blue.overshoot, blue.overshoot) |
336 | | } else { |
337 | 110k | (unscaled_blue.position, blue.position) |
338 | | } |
339 | | }; |
340 | 7.27M | let dist = fixed_mul((edge.fpos as i32 - ref_pos).abs(), axis_scale); |
341 | 7.27M | if dist < best_dist { |
342 | 2.05M | best_dist = dist; |
343 | 2.05M | best_blue = Some(matching_blue); |
344 | 2.05M | best_is_neutral = is_neutral; |
345 | 5.22M | } |
346 | 7.27M | if group == ScriptGroup::Default { |
347 | | // Now compare to overshoot position for the default script |
348 | | // group |
349 | | // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2579> |
350 | 7.16M | if edge.flags & Edge::ROUND != 0 && dist != 0 && !is_neutral { |
351 | 1.77M | let is_under_ref = (edge.fpos as i32) < unscaled_blue.position; |
352 | 1.77M | if is_top ^ is_under_ref { |
353 | 305k | let dist = fixed_mul( |
354 | 305k | (edge.fpos as i32 - unscaled_blue.overshoot).abs(), |
355 | 305k | axis_scale, |
356 | 305k | ); |
357 | 305k | if dist < best_dist { |
358 | 8.28k | best_dist = dist; |
359 | 8.28k | best_blue = Some(blue.overshoot); |
360 | 8.28k | best_is_neutral = is_neutral; |
361 | 297k | } |
362 | 1.46M | } |
363 | 5.39M | } |
364 | 110k | } |
365 | 5.97M | } |
366 | | } |
367 | 7.18M | if let Some(best_blue) = best_blue { |
368 | 2.05M | edge.blue_edge = Some(best_blue); |
369 | 2.05M | if best_is_neutral { |
370 | 12.5k | edge.flags |= Edge::NEUTRAL; |
371 | 2.04M | } |
372 | 5.13M | } |
373 | | } |
374 | 2.09M | } |
375 | | |
376 | | #[cfg(test)] |
377 | | mod tests { |
378 | | use super::{ |
379 | | super::super::{ |
380 | | metrics::{self, ScaledWidth}, |
381 | | outline::Outline, |
382 | | shape::{Shaper, ShaperMode}, |
383 | | style, |
384 | | }, |
385 | | super::segments, |
386 | | *, |
387 | | }; |
388 | | use crate::{attribute::Style, MetadataProvider}; |
389 | | use raw::{types::GlyphId, FontRef, TableProvider}; |
390 | | |
391 | | #[test] |
392 | | fn edges_default() { |
393 | | let expected_h_edges = [ |
394 | | Edge { |
395 | | fpos: 15, |
396 | | opos: 15, |
397 | | pos: 15, |
398 | | flags: Edge::ROUND, |
399 | | dir: Direction::Up, |
400 | | blue_edge: None, |
401 | | link_ix: Some(3), |
402 | | serif_ix: None, |
403 | | scale: 0, |
404 | | first_ix: 1, |
405 | | last_ix: 1, |
406 | | }, |
407 | | Edge { |
408 | | fpos: 123, |
409 | | opos: 126, |
410 | | pos: 126, |
411 | | flags: 0, |
412 | | dir: Direction::Up, |
413 | | blue_edge: None, |
414 | | link_ix: Some(2), |
415 | | serif_ix: None, |
416 | | scale: 0, |
417 | | first_ix: 0, |
418 | | last_ix: 0, |
419 | | }, |
420 | | Edge { |
421 | | fpos: 186, |
422 | | opos: 190, |
423 | | pos: 190, |
424 | | flags: 0, |
425 | | dir: Direction::Down, |
426 | | blue_edge: None, |
427 | | link_ix: Some(1), |
428 | | serif_ix: None, |
429 | | scale: 0, |
430 | | first_ix: 4, |
431 | | last_ix: 4, |
432 | | }, |
433 | | Edge { |
434 | | fpos: 205, |
435 | | opos: 210, |
436 | | pos: 210, |
437 | | flags: Edge::ROUND, |
438 | | dir: Direction::Down, |
439 | | blue_edge: None, |
440 | | link_ix: Some(0), |
441 | | serif_ix: None, |
442 | | scale: 0, |
443 | | first_ix: 3, |
444 | | last_ix: 3, |
445 | | }, |
446 | | ]; |
447 | | let expected_v_edges = [ |
448 | | Edge { |
449 | | fpos: -240, |
450 | | opos: -246, |
451 | | pos: -246, |
452 | | flags: 0, |
453 | | dir: Direction::Left, |
454 | | blue_edge: Some(ScaledWidth { |
455 | | scaled: -246, |
456 | | fitted: -256, |
457 | | }), |
458 | | link_ix: None, |
459 | | serif_ix: Some(1), |
460 | | scale: 0, |
461 | | first_ix: 3, |
462 | | last_ix: 3, |
463 | | }, |
464 | | Edge { |
465 | | fpos: 481, |
466 | | opos: 493, |
467 | | pos: 493, |
468 | | flags: 0, |
469 | | dir: Direction::Left, |
470 | | blue_edge: None, |
471 | | link_ix: Some(2), |
472 | | serif_ix: None, |
473 | | scale: 0, |
474 | | first_ix: 0, |
475 | | last_ix: 0, |
476 | | }, |
477 | | Edge { |
478 | | fpos: 592, |
479 | | opos: 606, |
480 | | pos: 606, |
481 | | flags: Edge::ROUND | Edge::SERIF, |
482 | | dir: Direction::Right, |
483 | | blue_edge: Some(ScaledWidth { |
484 | | scaled: 606, |
485 | | fitted: 576, |
486 | | }), |
487 | | link_ix: Some(1), |
488 | | serif_ix: None, |
489 | | scale: 0, |
490 | | first_ix: 2, |
491 | | last_ix: 2, |
492 | | }, |
493 | | Edge { |
494 | | fpos: 647, |
495 | | opos: 663, |
496 | | pos: 663, |
497 | | flags: 0, |
498 | | dir: Direction::Right, |
499 | | blue_edge: None, |
500 | | link_ix: None, |
501 | | serif_ix: Some(2), |
502 | | scale: 0, |
503 | | first_ix: 1, |
504 | | last_ix: 1, |
505 | | }, |
506 | | ]; |
507 | | check_edges( |
508 | | font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS, |
509 | | GlyphId::new(9), |
510 | | style::StyleClass::HEBR, |
511 | | &expected_h_edges, |
512 | | &expected_v_edges, |
513 | | ); |
514 | | } |
515 | | |
516 | | #[test] |
517 | | fn edges_cjk() { |
518 | | let expected_h_edges = [ |
519 | | Edge { |
520 | | fpos: 138, |
521 | | opos: 141, |
522 | | pos: 141, |
523 | | flags: 0, |
524 | | dir: Direction::Up, |
525 | | blue_edge: None, |
526 | | link_ix: Some(1), |
527 | | serif_ix: None, |
528 | | scale: 0, |
529 | | first_ix: 8, |
530 | | last_ix: 8, |
531 | | }, |
532 | | Edge { |
533 | | fpos: 201, |
534 | | opos: 206, |
535 | | pos: 206, |
536 | | flags: 0, |
537 | | dir: Direction::Down, |
538 | | blue_edge: None, |
539 | | link_ix: Some(0), |
540 | | serif_ix: None, |
541 | | scale: 0, |
542 | | first_ix: 7, |
543 | | last_ix: 7, |
544 | | }, |
545 | | Edge { |
546 | | fpos: 458, |
547 | | opos: 469, |
548 | | pos: 469, |
549 | | flags: 0, |
550 | | dir: Direction::Down, |
551 | | blue_edge: None, |
552 | | link_ix: None, |
553 | | serif_ix: None, |
554 | | scale: 0, |
555 | | first_ix: 2, |
556 | | last_ix: 2, |
557 | | }, |
558 | | Edge { |
559 | | fpos: 569, |
560 | | opos: 583, |
561 | | pos: 583, |
562 | | flags: 0, |
563 | | dir: Direction::Down, |
564 | | blue_edge: None, |
565 | | link_ix: None, |
566 | | serif_ix: None, |
567 | | scale: 0, |
568 | | first_ix: 6, |
569 | | last_ix: 6, |
570 | | }, |
571 | | Edge { |
572 | | fpos: 670, |
573 | | opos: 686, |
574 | | pos: 686, |
575 | | flags: 0, |
576 | | dir: Direction::Up, |
577 | | blue_edge: None, |
578 | | link_ix: Some(6), |
579 | | serif_ix: None, |
580 | | scale: 0, |
581 | | first_ix: 1, |
582 | | last_ix: 1, |
583 | | }, |
584 | | Edge { |
585 | | fpos: 693, |
586 | | opos: 710, |
587 | | pos: 710, |
588 | | flags: 0, |
589 | | dir: Direction::Up, |
590 | | blue_edge: None, |
591 | | link_ix: None, |
592 | | serif_ix: Some(7), |
593 | | scale: 0, |
594 | | first_ix: 4, |
595 | | last_ix: 4, |
596 | | }, |
597 | | Edge { |
598 | | fpos: 731, |
599 | | opos: 749, |
600 | | pos: 749, |
601 | | flags: 0, |
602 | | dir: Direction::Down, |
603 | | blue_edge: None, |
604 | | link_ix: Some(4), |
605 | | serif_ix: None, |
606 | | scale: 0, |
607 | | first_ix: 0, |
608 | | last_ix: 0, |
609 | | }, |
610 | | Edge { |
611 | | fpos: 849, |
612 | | opos: 869, |
613 | | pos: 869, |
614 | | flags: 0, |
615 | | dir: Direction::Up, |
616 | | blue_edge: None, |
617 | | link_ix: Some(8), |
618 | | serif_ix: None, |
619 | | scale: 0, |
620 | | first_ix: 5, |
621 | | last_ix: 5, |
622 | | }, |
623 | | Edge { |
624 | | fpos: 911, |
625 | | opos: 933, |
626 | | pos: 933, |
627 | | flags: 0, |
628 | | dir: Direction::Down, |
629 | | blue_edge: None, |
630 | | link_ix: Some(7), |
631 | | serif_ix: None, |
632 | | scale: 0, |
633 | | first_ix: 3, |
634 | | last_ix: 3, |
635 | | }, |
636 | | ]; |
637 | | let expected_v_edges = [ |
638 | | Edge { |
639 | | fpos: -78, |
640 | | opos: -80, |
641 | | pos: -80, |
642 | | flags: Edge::ROUND, |
643 | | dir: Direction::Left, |
644 | | blue_edge: Some(ScaledWidth { |
645 | | scaled: -80, |
646 | | fitted: -64, |
647 | | }), |
648 | | link_ix: None, |
649 | | serif_ix: None, |
650 | | scale: 0, |
651 | | first_ix: 8, |
652 | | last_ix: 8, |
653 | | }, |
654 | | Edge { |
655 | | fpos: 3, |
656 | | opos: 3, |
657 | | pos: 3, |
658 | | flags: Edge::ROUND, |
659 | | dir: Direction::Right, |
660 | | blue_edge: None, |
661 | | link_ix: None, |
662 | | serif_ix: None, |
663 | | scale: 0, |
664 | | first_ix: 4, |
665 | | last_ix: 4, |
666 | | }, |
667 | | Edge { |
668 | | fpos: 133, |
669 | | opos: 136, |
670 | | pos: 136, |
671 | | flags: Edge::ROUND, |
672 | | dir: Direction::Left, |
673 | | blue_edge: None, |
674 | | link_ix: None, |
675 | | serif_ix: None, |
676 | | scale: 0, |
677 | | first_ix: 2, |
678 | | last_ix: 2, |
679 | | }, |
680 | | Edge { |
681 | | fpos: 547, |
682 | | opos: 560, |
683 | | pos: 560, |
684 | | flags: 0, |
685 | | dir: Direction::Left, |
686 | | blue_edge: None, |
687 | | link_ix: None, |
688 | | serif_ix: Some(5), |
689 | | scale: 0, |
690 | | first_ix: 6, |
691 | | last_ix: 6, |
692 | | }, |
693 | | Edge { |
694 | | fpos: 576, |
695 | | opos: 590, |
696 | | pos: 590, |
697 | | flags: 0, |
698 | | dir: Direction::Right, |
699 | | blue_edge: None, |
700 | | link_ix: Some(5), |
701 | | serif_ix: None, |
702 | | scale: 0, |
703 | | first_ix: 5, |
704 | | last_ix: 5, |
705 | | }, |
706 | | Edge { |
707 | | fpos: 576, |
708 | | opos: 590, |
709 | | pos: 590, |
710 | | flags: 0, |
711 | | dir: Direction::Left, |
712 | | blue_edge: None, |
713 | | link_ix: Some(4), |
714 | | serif_ix: None, |
715 | | scale: 0, |
716 | | first_ix: 7, |
717 | | last_ix: 7, |
718 | | }, |
719 | | Edge { |
720 | | fpos: 729, |
721 | | opos: 746, |
722 | | pos: 746, |
723 | | flags: 0, |
724 | | dir: Direction::Left, |
725 | | blue_edge: None, |
726 | | link_ix: Some(7), |
727 | | serif_ix: None, |
728 | | scale: 0, |
729 | | first_ix: 1, |
730 | | last_ix: 1, |
731 | | }, |
732 | | Edge { |
733 | | fpos: 758, |
734 | | opos: 776, |
735 | | pos: 776, |
736 | | flags: 0, |
737 | | dir: Direction::Right, |
738 | | blue_edge: None, |
739 | | link_ix: Some(6), |
740 | | serif_ix: None, |
741 | | scale: 0, |
742 | | first_ix: 0, |
743 | | last_ix: 3, |
744 | | }, |
745 | | Edge { |
746 | | fpos: 788, |
747 | | opos: 807, |
748 | | pos: 807, |
749 | | flags: Edge::ROUND, |
750 | | dir: Direction::Left, |
751 | | blue_edge: None, |
752 | | link_ix: None, |
753 | | serif_ix: None, |
754 | | scale: 0, |
755 | | first_ix: 9, |
756 | | last_ix: 9, |
757 | | }, |
758 | | ]; |
759 | | check_edges( |
760 | | font_test_data::NOTOSERIFTC_AUTOHINT_METRICS, |
761 | | GlyphId::new(9), |
762 | | style::StyleClass::HANI, |
763 | | &expected_h_edges, |
764 | | &expected_v_edges, |
765 | | ); |
766 | | } |
767 | | |
768 | | fn check_edges( |
769 | | font_data: &[u8], |
770 | | glyph_id: GlyphId, |
771 | | style_class: usize, |
772 | | expected_h_edges: &[Edge], |
773 | | expected_v_edges: &[Edge], |
774 | | ) { |
775 | | let font = FontRef::new(font_data).unwrap(); |
776 | | let shaper = Shaper::new(&font, ShaperMode::Nominal); |
777 | | let class = &style::STYLE_CLASSES[style_class]; |
778 | | let unscaled_metrics = |
779 | | metrics::compute_unscaled_style_metrics(&shaper, Default::default(), class); |
780 | | let scale = metrics::Scale::new( |
781 | | 16.0, |
782 | | font.head().unwrap().units_per_em() as i32, |
783 | | Style::Normal, |
784 | | Default::default(), |
785 | | class.script.group, |
786 | | ); |
787 | | let scaled_metrics = metrics::scale_style_metrics(&unscaled_metrics, scale); |
788 | | let glyphs = font.outline_glyphs(); |
789 | | let glyph = glyphs.get(glyph_id).unwrap(); |
790 | | let mut outline = Outline::default(); |
791 | | outline.fill(&glyph, Default::default()).unwrap(); |
792 | | let mut axes = [ |
793 | | Axis::new(Axis::HORIZONTAL, outline.orientation), |
794 | | Axis::new(Axis::VERTICAL, outline.orientation), |
795 | | ]; |
796 | | for (dim, axis) in axes.iter_mut().enumerate() { |
797 | | segments::compute_segments(&mut outline, axis, class.script.group); |
798 | | segments::link_segments( |
799 | | &outline, |
800 | | axis, |
801 | | scaled_metrics.axes[dim].scale, |
802 | | class.script.group, |
803 | | unscaled_metrics.axes[dim].max_width(), |
804 | | ); |
805 | | compute_edges( |
806 | | axis, |
807 | | &scaled_metrics.axes[dim], |
808 | | class.script.hint_top_to_bottom, |
809 | | scaled_metrics.axes[1].scale, |
810 | | class.script.group, |
811 | | ); |
812 | | compute_blue_edges( |
813 | | axis, |
814 | | &scale, |
815 | | &unscaled_metrics.axes[dim].blues, |
816 | | &scaled_metrics.axes[dim].blues, |
817 | | class.script.group, |
818 | | ); |
819 | | } |
820 | | assert_eq!(axes[Axis::HORIZONTAL].edges.as_slice(), expected_h_edges); |
821 | | assert_eq!(axes[Axis::VERTICAL].edges.as_slice(), expected_v_edges); |
822 | | } |
823 | | } |